feat:add so manager and config manager
This commit is contained in:
@@ -20,6 +20,7 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +42,12 @@ dependencies {
|
|||||||
|
|
||||||
// RecyclerView for app list
|
// RecyclerView for app list
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
|
||||||
|
// Root access library
|
||||||
|
implementation 'com.github.topjohnwu.libsu:core:6.0.0'
|
||||||
|
|
||||||
|
// JSON parsing
|
||||||
|
implementation 'com.google.code.gson:gson:2.10.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
|
|
||||||
private List<AppInfo> allApps;
|
private List<AppInfo> allApps;
|
||||||
private boolean hideSystemApps = false;
|
private boolean hideSystemApps = false;
|
||||||
|
private ConfigManager configManager;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
@@ -48,6 +49,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
configManager = new ConfigManager(requireContext());
|
||||||
|
|
||||||
initViews(view);
|
initViews(view);
|
||||||
setupRecyclerView();
|
setupRecyclerView();
|
||||||
setupSearchView();
|
setupSearchView();
|
||||||
@@ -102,8 +105,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAppToggle(AppInfo appInfo, boolean isEnabled) {
|
public void onAppToggle(AppInfo appInfo, boolean isEnabled) {
|
||||||
// 这里可以保存应用的启用状态到配置文件或数据库
|
// 保存应用的启用状态到配置文件
|
||||||
// 暂时只是打印日志
|
configManager.setAppEnabled(appInfo.getPackageName(), isEnabled);
|
||||||
android.util.Log.d("AppListFragment",
|
android.util.Log.d("AppListFragment",
|
||||||
"App " + appInfo.getAppName() + " toggle: " + isEnabled);
|
"App " + appInfo.getAppName() + " toggle: " + isEnabled);
|
||||||
}
|
}
|
||||||
@@ -133,6 +136,9 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
isSystemApp
|
isSystemApp
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 从配置中加载启用状态
|
||||||
|
app.setEnabled(configManager.isAppEnabled(packageName));
|
||||||
|
|
||||||
apps.add(app);
|
apps.add(app);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 忽略无法获取信息的应用
|
// 忽略无法获取信息的应用
|
||||||
|
|||||||
179
configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
Normal file
179
configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ConfigManager {
|
||||||
|
private static final String TAG = "ConfigManager";
|
||||||
|
public static final String MODULE_PATH = "/data/adb/modules/zygisk-myinjector";
|
||||||
|
public static final String CONFIG_FILE = MODULE_PATH + "/config.json";
|
||||||
|
public static final String SO_STORAGE_DIR = MODULE_PATH + "/so_files";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Gson gson;
|
||||||
|
private ModuleConfig config;
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Configure Shell to use root
|
||||||
|
Shell.enableVerboseLogging = BuildConfig.DEBUG;
|
||||||
|
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||||
|
.setFlags(Shell.FLAG_REDIRECT_STDERR)
|
||||||
|
.setTimeout(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigManager(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRootAvailable() {
|
||||||
|
return Shell.getShell().isRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureModuleDirectories() {
|
||||||
|
Shell.cmd("mkdir -p " + MODULE_PATH).exec();
|
||||||
|
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
Shell.Result result = Shell.cmd("cat " + CONFIG_FILE).exec();
|
||||||
|
if (result.isSuccess() && !result.getOut().isEmpty()) {
|
||||||
|
String json = String.join("\n", result.getOut());
|
||||||
|
try {
|
||||||
|
config = gson.fromJson(json, ModuleConfig.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to parse config", e);
|
||||||
|
config = new ModuleConfig();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config = new ModuleConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveConfig() {
|
||||||
|
String json = gson.toJson(config);
|
||||||
|
// Write to temp file first
|
||||||
|
String tempFile = context.getCacheDir() + "/config.json";
|
||||||
|
try {
|
||||||
|
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||||
|
writer.write(json);
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
// Copy to module directory with root
|
||||||
|
Shell.cmd("cp " + tempFile + " " + CONFIG_FILE).exec();
|
||||||
|
Shell.cmd("chmod 644 " + CONFIG_FILE).exec();
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
new File(tempFile).delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to save config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAppEnabled(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
return appConfig != null && appConfig.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppEnabled(String packageName, boolean enabled) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
appConfig.enabled = enabled;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SoFile> getAppSoFiles(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return new ArrayList<>(appConfig.soFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSoFileToApp(String packageName, String originalPath, boolean deleteOriginal) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
String fileName = new File(originalPath).getName();
|
||||||
|
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
|
||||||
|
|
||||||
|
// Copy SO file to our storage
|
||||||
|
Shell.Result result = Shell.cmd("cp " + originalPath + " " + storedPath).exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
SoFile soFile = new SoFile();
|
||||||
|
soFile.name = fileName;
|
||||||
|
soFile.storedPath = storedPath;
|
||||||
|
soFile.originalPath = originalPath;
|
||||||
|
appConfig.soFiles.add(soFile);
|
||||||
|
|
||||||
|
if (deleteOriginal) {
|
||||||
|
Shell.cmd("rm " + originalPath).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSoFileFromApp(String packageName, SoFile soFile) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) return;
|
||||||
|
|
||||||
|
appConfig.soFiles.remove(soFile);
|
||||||
|
// Delete the stored file
|
||||||
|
Shell.cmd("rm " + soFile.storedPath).exec();
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getHideInjection() {
|
||||||
|
return config.hideInjection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHideInjection(boolean hide) {
|
||||||
|
config.hideInjection = hide;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data classes
|
||||||
|
public static class ModuleConfig {
|
||||||
|
public boolean enabled = true;
|
||||||
|
public boolean hideInjection = false;
|
||||||
|
public Map<String, AppConfig> perAppConfig = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AppConfig {
|
||||||
|
public boolean enabled = false;
|
||||||
|
public List<SoFile> soFiles = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SoFile {
|
||||||
|
public String name;
|
||||||
|
public String storedPath;
|
||||||
|
public String originalPath;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof SoFile) {
|
||||||
|
return storedPath.equals(((SoFile) obj).storedPath);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
|
|||||||
private BottomNavigationView bottomNavigationView;
|
private BottomNavigationView bottomNavigationView;
|
||||||
private AppListFragment appListFragment;
|
private AppListFragment appListFragment;
|
||||||
private SettingsFragment settingsFragment;
|
private SettingsFragment settingsFragment;
|
||||||
|
private SoManagerFragment soManagerFragment;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -49,6 +50,9 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
|
|||||||
if (itemId == R.id.navigation_apps) {
|
if (itemId == R.id.navigation_apps) {
|
||||||
showAppListFragment();
|
showAppListFragment();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (itemId == R.id.navigation_so_manager) {
|
||||||
|
showSoManagerFragment();
|
||||||
|
return true;
|
||||||
} else if (itemId == R.id.navigation_settings) {
|
} else if (itemId == R.id.navigation_settings) {
|
||||||
showSettingsFragment();
|
showSettingsFragment();
|
||||||
return true;
|
return true;
|
||||||
@@ -64,6 +68,13 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
|
|||||||
showFragment(appListFragment);
|
showFragment(appListFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showSoManagerFragment() {
|
||||||
|
if (soManagerFragment == null) {
|
||||||
|
soManagerFragment = new SoManagerFragment();
|
||||||
|
}
|
||||||
|
showFragment(soManagerFragment);
|
||||||
|
}
|
||||||
|
|
||||||
private void showSettingsFragment() {
|
private void showSettingsFragment() {
|
||||||
if (settingsFragment == null) {
|
if (settingsFragment == null) {
|
||||||
settingsFragment = new SettingsFragment();
|
settingsFragment = new SettingsFragment();
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SoListAdapter extends RecyclerView.Adapter<SoListAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private List<ConfigManager.SoFile> soFiles = new ArrayList<>();
|
||||||
|
private OnSoFileActionListener listener;
|
||||||
|
|
||||||
|
public interface OnSoFileActionListener {
|
||||||
|
void onDeleteClick(ConfigManager.SoFile soFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoFiles(List<ConfigManager.SoFile> files) {
|
||||||
|
this.soFiles = files;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSoFileActionListener(OnSoFileActionListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_so_file, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
ConfigManager.SoFile soFile = soFiles.get(position);
|
||||||
|
holder.bind(soFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return soFiles.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private TextView textFileName;
|
||||||
|
private TextView textFilePath;
|
||||||
|
private ImageButton buttonDelete;
|
||||||
|
|
||||||
|
public ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
textFileName = itemView.findViewById(R.id.textFileName);
|
||||||
|
textFilePath = itemView.findViewById(R.id.textFilePath);
|
||||||
|
buttonDelete = itemView.findViewById(R.id.buttonDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(ConfigManager.SoFile soFile) {
|
||||||
|
textFileName.setText(soFile.name);
|
||||||
|
textFilePath.setText(soFile.originalPath);
|
||||||
|
|
||||||
|
buttonDelete.setOnClickListener(v -> {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onDeleteClick(soFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SoManagerFragment extends Fragment {
|
||||||
|
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private LinearLayout emptyView;
|
||||||
|
private SoListAdapter adapter;
|
||||||
|
private ConfigManager configManager;
|
||||||
|
private List<ConfigManager.SoFile> globalSoFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> filePickerLauncher;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
configManager = new ConfigManager(requireContext());
|
||||||
|
|
||||||
|
// Initialize file picker
|
||||||
|
filePickerLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
Uri uri = result.getData().getData();
|
||||||
|
if (uri != null) {
|
||||||
|
handleFileSelection(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_so_manager, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
recyclerView = view.findViewById(R.id.recyclerView);
|
||||||
|
emptyView = view.findViewById(R.id.emptyView);
|
||||||
|
FloatingActionButton fabAdd = view.findViewById(R.id.fabAdd);
|
||||||
|
|
||||||
|
// Setup RecyclerView
|
||||||
|
adapter = new SoListAdapter();
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
adapter.setOnSoFileActionListener(this::showDeleteConfirmation);
|
||||||
|
|
||||||
|
// Setup FAB
|
||||||
|
fabAdd.setOnClickListener(v -> showAddSoDialog());
|
||||||
|
|
||||||
|
// Check root access
|
||||||
|
if (!configManager.isRootAvailable()) {
|
||||||
|
Toast.makeText(getContext(), "需要Root权限", Toast.LENGTH_LONG).show();
|
||||||
|
} else {
|
||||||
|
configManager.ensureModuleDirectories();
|
||||||
|
loadSoFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSoFiles() {
|
||||||
|
// Load global SO files from the storage directory
|
||||||
|
Shell.Result result = Shell.cmd("ls -la " + ConfigManager.SO_STORAGE_DIR).exec();
|
||||||
|
|
||||||
|
globalSoFiles.clear();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
for (String line : result.getOut()) {
|
||||||
|
if (line.contains(".so")) {
|
||||||
|
// Parse file info
|
||||||
|
String[] parts = line.split("\\s+");
|
||||||
|
if (parts.length >= 9) {
|
||||||
|
String fileName = parts[parts.length - 1];
|
||||||
|
ConfigManager.SoFile soFile = new ConfigManager.SoFile();
|
||||||
|
soFile.name = fileName;
|
||||||
|
soFile.storedPath = ConfigManager.SO_STORAGE_DIR + "/" + fileName;
|
||||||
|
soFile.originalPath = soFile.storedPath; // For display
|
||||||
|
globalSoFiles.add(soFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI() {
|
||||||
|
if (globalSoFiles.isEmpty()) {
|
||||||
|
emptyView.setVisibility(View.VISIBLE);
|
||||||
|
recyclerView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
emptyView.setVisibility(View.GONE);
|
||||||
|
recyclerView.setVisibility(View.VISIBLE);
|
||||||
|
adapter.setSoFiles(globalSoFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showAddSoDialog() {
|
||||||
|
String[] options = {"从文件管理器选择", "输入路径"};
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("添加SO文件")
|
||||||
|
.setItems(options, (dialog, which) -> {
|
||||||
|
if (which == 0) {
|
||||||
|
openFilePicker();
|
||||||
|
} else {
|
||||||
|
showPathInputDialog();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openFilePicker() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
filePickerLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPathInputDialog() {
|
||||||
|
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
|
||||||
|
android.widget.EditText editText = view.findViewById(android.R.id.edit);
|
||||||
|
editText.setHint("/data/local/tmp/example.so");
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("输入SO文件路径")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("添加", (dialog, which) -> {
|
||||||
|
String path = editText.getText().toString().trim();
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
addSoFile(path, false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFileSelection(Uri uri) {
|
||||||
|
// Get real path from URI
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
// Remove the file:// prefix if present
|
||||||
|
if (path.startsWith("file://")) {
|
||||||
|
path = path.substring(7);
|
||||||
|
}
|
||||||
|
addSoFile(path, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSoFile(String path, boolean deleteOriginal) {
|
||||||
|
// Verify file exists
|
||||||
|
Shell.Result result = Shell.cmd("test -f " + path + " && echo 'exists'").exec();
|
||||||
|
if (!result.isSuccess() || result.getOut().isEmpty()) {
|
||||||
|
Toast.makeText(getContext(), "文件不存在: " + path, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
String fileName = new File(path).getName();
|
||||||
|
String storedPath = ConfigManager.SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
result = Shell.cmd("cp " + path + " " + storedPath).exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
ConfigManager.SoFile soFile = new ConfigManager.SoFile();
|
||||||
|
soFile.name = fileName;
|
||||||
|
soFile.storedPath = storedPath;
|
||||||
|
soFile.originalPath = path;
|
||||||
|
globalSoFiles.add(soFile);
|
||||||
|
|
||||||
|
if (deleteOriginal) {
|
||||||
|
Shell.cmd("rm " + path).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "复制文件失败", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDeleteConfirmation(ConfigManager.SoFile soFile) {
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("删除SO文件")
|
||||||
|
.setMessage("确定要删除 " + soFile.name + " 吗?")
|
||||||
|
.setPositiveButton("删除", (dialog, which) -> {
|
||||||
|
deleteSoFile(soFile);
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSoFile(ConfigManager.SoFile soFile) {
|
||||||
|
Shell.cmd("rm " + soFile.storedPath).exec();
|
||||||
|
globalSoFiles.remove(soFile);
|
||||||
|
updateUI();
|
||||||
|
Toast.makeText(getContext(), "SO文件已删除", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
configapp/src/main/res/layout/dialog_input.xml
Normal file
14
configapp/src/main/res/layout/dialog_input.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@android:id/edit"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
69
configapp/src/main/res/layout/fragment_so_manager.xml
Normal file
69
configapp/src/main/res/layout/fragment_so_manager.xml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:title="SO文件管理" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/emptyView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:alpha="0.3"
|
||||||
|
android:src="@drawable/ic_launcher_foreground" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="暂无SO文件"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="点击右下角按钮添加SO文件"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorTertiary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabAdd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:tint="@android:color/white" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
93
configapp/src/main/res/layout/item_so_file.xml
Normal file
93
configapp/src/main/res/layout/item_so_file.xml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@android:drawable/ic_menu_save"
|
||||||
|
android:tint="?attr/colorPrimary" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textFileName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="example.so" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textFilePath"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:text="/data/local/tmp/example.so"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/buttonDelete"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_menu_delete"
|
||||||
|
android:tint="?attr/colorError" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutApps"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="使用此SO的应用: "
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textAppCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="0"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?attr/colorPrimary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -5,6 +5,11 @@
|
|||||||
android:icon="@android:drawable/ic_menu_view"
|
android:icon="@android:drawable/ic_menu_view"
|
||||||
android:title="@string/title_apps" />
|
android:title="@string/title_apps" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_so_manager"
|
||||||
|
android:icon="@android:drawable/ic_menu_save"
|
||||||
|
android:title="@string/title_so_manager" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_settings"
|
android:id="@+id/navigation_settings"
|
||||||
android:icon="@android:drawable/ic_menu_preferences"
|
android:icon="@android:drawable/ic_menu_preferences"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<!-- 底部导航 -->
|
<!-- 底部导航 -->
|
||||||
<string name="title_apps">应用列表</string>
|
<string name="title_apps">应用列表</string>
|
||||||
|
<string name="title_so_manager">SO库管理</string>
|
||||||
<string name="title_settings">全局设置</string>
|
<string name="title_settings">全局设置</string>
|
||||||
|
|
||||||
<!-- 应用列表 -->
|
<!-- 应用列表 -->
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ aux_source_directory(xdl xdl-src)
|
|||||||
|
|
||||||
add_library(${MODULE_NAME} SHARED
|
add_library(${MODULE_NAME} SHARED
|
||||||
main.cpp
|
main.cpp
|
||||||
hack.cpp
|
hack_new.cpp
|
||||||
|
config.cpp
|
||||||
newriruhide.cpp
|
newriruhide.cpp
|
||||||
pmparser.cpp
|
pmparser.cpp
|
||||||
${xdl-src})
|
${xdl-src})
|
||||||
|
|||||||
189
module/src/main/cpp/config.cpp
Normal file
189
module/src/main/cpp/config.cpp
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#include "config.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "MyInjector"
|
||||||
|
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
namespace Config {
|
||||||
|
|
||||||
|
static ModuleConfig g_config;
|
||||||
|
static bool g_configLoaded = false;
|
||||||
|
|
||||||
|
// Simple JSON parser for our specific format
|
||||||
|
std::string extractValue(const std::string& json, const std::string& key) {
|
||||||
|
size_t keyPos = json.find("\"" + key + "\"");
|
||||||
|
if (keyPos == std::string::npos) return "";
|
||||||
|
|
||||||
|
size_t colonPos = json.find(":", keyPos);
|
||||||
|
if (colonPos == std::string::npos) return "";
|
||||||
|
|
||||||
|
size_t valueStart = json.find_first_not_of(" \t\n", colonPos + 1);
|
||||||
|
if (valueStart == std::string::npos) return "";
|
||||||
|
|
||||||
|
if (json[valueStart] == '"') {
|
||||||
|
// String value
|
||||||
|
size_t valueEnd = json.find('"', valueStart + 1);
|
||||||
|
if (valueEnd == std::string::npos) return "";
|
||||||
|
return json.substr(valueStart + 1, valueEnd - valueStart - 1);
|
||||||
|
} else if (json[valueStart] == 't' || json[valueStart] == 'f') {
|
||||||
|
// Boolean value
|
||||||
|
return (json.substr(valueStart, 4) == "true") ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseAppConfig(const std::string& packageName, const std::string& appJson) {
|
||||||
|
AppConfig appConfig;
|
||||||
|
|
||||||
|
// Parse enabled
|
||||||
|
std::string enabledStr = extractValue(appJson, "enabled");
|
||||||
|
appConfig.enabled = (enabledStr == "true");
|
||||||
|
|
||||||
|
// Parse soFiles array
|
||||||
|
size_t soFilesPos = appJson.find("\"soFiles\"");
|
||||||
|
if (soFilesPos != std::string::npos) {
|
||||||
|
size_t arrayStart = appJson.find("[", soFilesPos);
|
||||||
|
size_t arrayEnd = appJson.find("]", arrayStart);
|
||||||
|
|
||||||
|
if (arrayStart != std::string::npos && arrayEnd != std::string::npos) {
|
||||||
|
std::string soFilesArray = appJson.substr(arrayStart + 1, arrayEnd - arrayStart - 1);
|
||||||
|
|
||||||
|
// Parse each SO file object
|
||||||
|
size_t objStart = 0;
|
||||||
|
while ((objStart = soFilesArray.find("{", objStart)) != std::string::npos) {
|
||||||
|
size_t objEnd = soFilesArray.find("}", objStart);
|
||||||
|
if (objEnd == std::string::npos) break;
|
||||||
|
|
||||||
|
std::string soFileObj = soFilesArray.substr(objStart, objEnd - objStart + 1);
|
||||||
|
|
||||||
|
SoFile soFile;
|
||||||
|
soFile.name = extractValue(soFileObj, "name");
|
||||||
|
soFile.storedPath = extractValue(soFileObj, "storedPath");
|
||||||
|
soFile.originalPath = extractValue(soFileObj, "originalPath");
|
||||||
|
|
||||||
|
if (!soFile.storedPath.empty()) {
|
||||||
|
appConfig.soFiles.push_back(soFile);
|
||||||
|
LOGD("Added SO file: %s at %s", soFile.name.c_str(), soFile.storedPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
objStart = objEnd + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_config.perAppConfig[packageName] = appConfig;
|
||||||
|
LOGD("Loaded config for app: %s, enabled: %d, SO files: %zu",
|
||||||
|
packageName.c_str(), appConfig.enabled, appConfig.soFiles.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleConfig readConfig() {
|
||||||
|
if (g_configLoaded) {
|
||||||
|
return g_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* configPath = "/data/adb/modules/zygisk-myinjector/config.json";
|
||||||
|
std::ifstream file(configPath);
|
||||||
|
|
||||||
|
if (!file.is_open()) {
|
||||||
|
LOGE("Failed to open config file: %s", configPath);
|
||||||
|
g_configLoaded = true;
|
||||||
|
return g_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
std::string json = buffer.str();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Parse global settings
|
||||||
|
std::string enabledStr = extractValue(json, "enabled");
|
||||||
|
g_config.enabled = (enabledStr != "false");
|
||||||
|
|
||||||
|
std::string hideStr = extractValue(json, "hideInjection");
|
||||||
|
g_config.hideInjection = (hideStr == "true");
|
||||||
|
|
||||||
|
LOGD("Module enabled: %d, hide injection: %d", g_config.enabled, g_config.hideInjection);
|
||||||
|
|
||||||
|
// Parse perAppConfig
|
||||||
|
size_t perAppPos = json.find("\"perAppConfig\"");
|
||||||
|
if (perAppPos != std::string::npos) {
|
||||||
|
size_t objStart = json.find("{", perAppPos + 14);
|
||||||
|
size_t objEnd = json.rfind("}");
|
||||||
|
|
||||||
|
if (objStart != std::string::npos && objEnd != std::string::npos) {
|
||||||
|
std::string perAppObj = json.substr(objStart + 1, objEnd - objStart - 1);
|
||||||
|
|
||||||
|
// Find each package config
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < perAppObj.length()) {
|
||||||
|
// Find package name
|
||||||
|
size_t pkgStart = perAppObj.find("\"", pos);
|
||||||
|
if (pkgStart == std::string::npos) break;
|
||||||
|
|
||||||
|
size_t pkgEnd = perAppObj.find("\"", pkgStart + 1);
|
||||||
|
if (pkgEnd == std::string::npos) break;
|
||||||
|
|
||||||
|
std::string packageName = perAppObj.substr(pkgStart + 1, pkgEnd - pkgStart - 1);
|
||||||
|
|
||||||
|
// Find app config object
|
||||||
|
size_t appObjStart = perAppObj.find("{", pkgEnd);
|
||||||
|
if (appObjStart == std::string::npos) break;
|
||||||
|
|
||||||
|
// Find matching closing brace
|
||||||
|
int braceCount = 1;
|
||||||
|
size_t appObjEnd = appObjStart + 1;
|
||||||
|
while (appObjEnd < perAppObj.length() && braceCount > 0) {
|
||||||
|
if (perAppObj[appObjEnd] == '{') braceCount++;
|
||||||
|
else if (perAppObj[appObjEnd] == '}') braceCount--;
|
||||||
|
appObjEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (braceCount == 0) {
|
||||||
|
std::string appConfigStr = perAppObj.substr(appObjStart, appObjEnd - appObjStart);
|
||||||
|
parseAppConfig(packageName, appConfigStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = appObjEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_configLoaded = true;
|
||||||
|
return g_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAppEnabled(const std::string& packageName) {
|
||||||
|
if (!g_configLoaded) {
|
||||||
|
readConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = g_config.perAppConfig.find(packageName);
|
||||||
|
if (it != g_config.perAppConfig.end()) {
|
||||||
|
return it->second.enabled;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SoFile> getAppSoFiles(const std::string& packageName) {
|
||||||
|
if (!g_configLoaded) {
|
||||||
|
readConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = g_config.perAppConfig.find(packageName);
|
||||||
|
if (it != g_config.perAppConfig.end()) {
|
||||||
|
return it->second.soFiles;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldHideInjection() {
|
||||||
|
if (!g_configLoaded) {
|
||||||
|
readConfig();
|
||||||
|
}
|
||||||
|
return g_config.hideInjection;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
module/src/main/cpp/config.h
Normal file
40
module/src/main/cpp/config.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Config {
|
||||||
|
|
||||||
|
struct SoFile {
|
||||||
|
std::string name;
|
||||||
|
std::string storedPath;
|
||||||
|
std::string originalPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppConfig {
|
||||||
|
bool enabled = false;
|
||||||
|
std::vector<SoFile> soFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModuleConfig {
|
||||||
|
bool enabled = true;
|
||||||
|
bool hideInjection = false;
|
||||||
|
std::unordered_map<std::string, AppConfig> perAppConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read configuration from file
|
||||||
|
ModuleConfig readConfig();
|
||||||
|
|
||||||
|
// Check if app is enabled for injection
|
||||||
|
bool isAppEnabled(const std::string& packageName);
|
||||||
|
|
||||||
|
// Get SO files for specific app
|
||||||
|
std::vector<SoFile> getAppSoFiles(const std::string& packageName);
|
||||||
|
|
||||||
|
// Get hide injection setting
|
||||||
|
bool shouldHideInjection();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CONFIG_H
|
||||||
97
module/src/main/cpp/hack_new.cpp
Normal file
97
module/src/main/cpp/hack_new.cpp
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#include "hack.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <thread>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
void load_so_file(const char *game_data_dir, const Config::SoFile &soFile) {
|
||||||
|
char dest_path[512];
|
||||||
|
snprintf(dest_path, sizeof(dest_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
|
||||||
|
|
||||||
|
// Copy SO file from storage to app directory
|
||||||
|
int src_fd = open(soFile.storedPath.c_str(), O_RDONLY);
|
||||||
|
if (src_fd < 0) {
|
||||||
|
LOGE("Failed to open source SO: %s", soFile.storedPath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dest_fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
|
||||||
|
if (dest_fd < 0) {
|
||||||
|
LOGE("Failed to create dest SO: %s", dest_path);
|
||||||
|
close(src_fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buffer[4096];
|
||||||
|
ssize_t bytes;
|
||||||
|
while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
|
||||||
|
if (write(dest_fd, buffer, bytes) != bytes) {
|
||||||
|
LOGE("Failed to write SO file");
|
||||||
|
close(src_fd);
|
||||||
|
close(dest_fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(src_fd);
|
||||||
|
close(dest_fd);
|
||||||
|
chmod(dest_path, 0755);
|
||||||
|
|
||||||
|
// Load the SO file
|
||||||
|
void *handle = dlopen(dest_path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (handle) {
|
||||||
|
LOGI("Successfully loaded SO: %s", soFile.name.c_str());
|
||||||
|
|
||||||
|
// Hide if configured
|
||||||
|
if (Config::shouldHideInjection()) {
|
||||||
|
// Call hide function if available
|
||||||
|
void (*hide_func)(const char*) = (void(*)(const char*))dlsym(handle, "riru_hide");
|
||||||
|
if (hide_func) {
|
||||||
|
hide_func(soFile.name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGE("Failed to load SO: %s - %s", dest_path, dlerror());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hack_thread_func(const char *game_data_dir, const char *package_name) {
|
||||||
|
LOGI("Hack thread started for package: %s", package_name);
|
||||||
|
|
||||||
|
// Wait a bit for app to initialize
|
||||||
|
sleep(1);
|
||||||
|
|
||||||
|
// Get SO files for this app
|
||||||
|
auto soFiles = Config::getAppSoFiles(package_name);
|
||||||
|
LOGI("Found %zu SO files to load", soFiles.size());
|
||||||
|
|
||||||
|
// Load each SO file
|
||||||
|
for (const auto &soFile : soFiles) {
|
||||||
|
LOGI("Loading SO: %s", soFile.name.c_str());
|
||||||
|
load_so_file(game_data_dir, soFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hack_prepare(const char *game_data_dir, void *data, size_t length) {
|
||||||
|
// Get package name from game_data_dir
|
||||||
|
// Format: /data/user/0/com.example.app or /data/data/com.example.app
|
||||||
|
const char *package_name = nullptr;
|
||||||
|
if (strstr(game_data_dir, "/data/user/")) {
|
||||||
|
package_name = strrchr(game_data_dir, '/');
|
||||||
|
if (package_name) package_name++;
|
||||||
|
} else if (strstr(game_data_dir, "/data/data/")) {
|
||||||
|
package_name = game_data_dir + strlen("/data/data/");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!package_name) {
|
||||||
|
LOGE("Failed to extract package name from: %s", game_data_dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread hack_thread(hack_thread_func, game_data_dir, package_name);
|
||||||
|
hack_thread.detach();
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "game.h"
|
#include "game.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "dlfcn.h"
|
#include "dlfcn.h"
|
||||||
|
#include "config.h"
|
||||||
using zygisk::Api;
|
using zygisk::Api;
|
||||||
using zygisk::AppSpecializeArgs;
|
using zygisk::AppSpecializeArgs;
|
||||||
using zygisk::ServerSpecializeArgs;
|
using zygisk::ServerSpecializeArgs;
|
||||||
@@ -51,7 +52,11 @@ private:
|
|||||||
size_t length;
|
size_t length;
|
||||||
|
|
||||||
void preSpecialize(const char *package_name, const char *app_data_dir) {
|
void preSpecialize(const char *package_name, const char *app_data_dir) {
|
||||||
if (strcmp(package_name, AimPackageName) == 0) {
|
// Read configuration
|
||||||
|
Config::readConfig();
|
||||||
|
|
||||||
|
// Check if this app is enabled for injection
|
||||||
|
if (Config::isAppEnabled(package_name)) {
|
||||||
LOGI("成功注入目标进程: %s", package_name);
|
LOGI("成功注入目标进程: %s", package_name);
|
||||||
enable_hack = true;
|
enable_hack = true;
|
||||||
_data_dir = new char[strlen(app_data_dir) + 1];
|
_data_dir = new char[strlen(app_data_dir) + 1];
|
||||||
|
|||||||
Reference in New Issue
Block a user