Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a23161e0f | ||
|
|
964c975cdd | ||
|
|
6110216556 | ||
|
|
e02a3df7fc | ||
|
|
cb64bc7d48 | ||
|
|
fc24ab3455 | ||
|
|
7e7b38caf6 | ||
|
|
b8b8dafed0 | ||
|
|
7b7389c0a0 | ||
|
|
122878f8fa |
@@ -12,13 +12,15 @@
|
|||||||
|
|
||||||
项目已完全开源,包含面具模块、管理APP以及所有打包脚本,并配置了GitHub CI自动构建。欢迎各位开发者贡献代码,提交PR。
|
项目已完全开源,包含面具模块、管理APP以及所有打包脚本,并配置了GitHub CI自动构建。欢迎各位开发者贡献代码,提交PR。
|
||||||
|
|
||||||
### 版本规划
|
### 版本规划&更新记录
|
||||||
|
版本规划:
|
||||||
- **v1.x**:专注功能添加,暂不考虑反检测
|
- **v1.x**:专注功能添加,暂不考虑反检测
|
||||||
- **v2.x**:实现各种检测绕过,达到100%无痕注入
|
- **v2.x**:实现各种检测绕过,达到100%无痕注入
|
||||||
|
|
||||||
## 致谢
|
更新记录:
|
||||||
|
- **v1.2**: 增加gadget配置的自动生成,支持脚本和server模式,解决了若干bug,增加了全局注入延迟设置
|
||||||
|
|
||||||
|
## 致谢
|
||||||
**项目地址**:[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
|
**项目地址**:[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
|
||||||
|
|
||||||
特别感谢以下项目和开发者(按时间顺序):
|
特别感谢以下项目和开发者(按时间顺序):
|
||||||
|
|||||||
@@ -140,7 +140,11 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection);
|
RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection);
|
||||||
RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection);
|
RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection);
|
||||||
RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection);
|
RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection);
|
||||||
CheckBox checkboxEnableGadget = dialogView.findViewById(R.id.checkboxEnableGadget);
|
RadioGroup gadgetConfigGroup = dialogView.findViewById(R.id.gadgetConfigGroup);
|
||||||
|
RadioButton radioNoGadget = dialogView.findViewById(R.id.radioNoGadget);
|
||||||
|
RadioButton radioUseGlobalGadget = dialogView.findViewById(R.id.radioUseGlobalGadget);
|
||||||
|
RadioButton radioUseCustomGadget = dialogView.findViewById(R.id.radioUseCustomGadget);
|
||||||
|
TextView tvGlobalGadgetInfo = dialogView.findViewById(R.id.tvGlobalGadgetInfo);
|
||||||
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
|
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
|
||||||
|
|
||||||
appIcon.setImageDrawable(appInfo.getAppIcon());
|
appIcon.setImageDrawable(appInfo.getAppIcon());
|
||||||
@@ -158,30 +162,71 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load gadget config
|
// Load gadget config
|
||||||
ConfigManager.GadgetConfig gadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
boolean useGlobalGadget = configManager.getAppUseGlobalGadget(appInfo.getPackageName());
|
||||||
checkboxEnableGadget.setChecked(gadgetConfig != null);
|
ConfigManager.GadgetConfig appSpecificGadget = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||||
btnConfigureGadget.setEnabled(gadgetConfig != null);
|
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
|
||||||
|
|
||||||
// Setup gadget listeners
|
// Update global gadget info
|
||||||
checkboxEnableGadget.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
if (globalGadget != null) {
|
||||||
btnConfigureGadget.setEnabled(isChecked);
|
String info = "全局: " + globalGadget.gadgetName;
|
||||||
if (!isChecked) {
|
if (globalGadget.mode.equals("server")) {
|
||||||
// Remove gadget config when unchecked
|
info += " (端口: " + globalGadget.port + ")";
|
||||||
|
}
|
||||||
|
tvGlobalGadgetInfo.setText(info);
|
||||||
|
} else {
|
||||||
|
tvGlobalGadgetInfo.setText("未配置全局Gadget");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial radio selection
|
||||||
|
if (!useGlobalGadget && appSpecificGadget != null) {
|
||||||
|
radioUseCustomGadget.setChecked(true);
|
||||||
|
btnConfigureGadget.setVisibility(View.VISIBLE);
|
||||||
|
btnConfigureGadget.setEnabled(true);
|
||||||
|
} else if (useGlobalGadget && globalGadget != null) {
|
||||||
|
radioUseGlobalGadget.setChecked(true);
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
radioNoGadget.setChecked(true);
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup gadget radio group listener
|
||||||
|
gadgetConfigGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (checkedId == R.id.radioNoGadget) {
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||||
|
} else if (checkedId == R.id.radioUseGlobalGadget) {
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), true);
|
||||||
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||||
|
} else if (checkedId == R.id.radioUseCustomGadget) {
|
||||||
|
btnConfigureGadget.setVisibility(View.VISIBLE);
|
||||||
|
btnConfigureGadget.setEnabled(true);
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Configure button listener
|
||||||
btnConfigureGadget.setOnClickListener(v -> {
|
btnConfigureGadget.setOnClickListener(v -> {
|
||||||
ConfigManager.GadgetConfig currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
ConfigManager.GadgetConfig currentConfig = null;
|
||||||
|
if (!useGlobalGadget) {
|
||||||
|
currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||||
|
}
|
||||||
if (currentConfig == null) {
|
if (currentConfig == null) {
|
||||||
currentConfig = new ConfigManager.GadgetConfig();
|
currentConfig = new ConfigManager.GadgetConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
GadgetConfigDialog gadgetDialog = GadgetConfigDialog.newInstance(currentConfig);
|
GadgetConfigDialog dialog = new GadgetConfigDialog(
|
||||||
gadgetDialog.setOnGadgetConfigListener(config -> {
|
getContext(),
|
||||||
|
"配置" + appInfo.getAppName() + "的Gadget",
|
||||||
|
currentConfig,
|
||||||
|
config -> {
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
|
||||||
});
|
}
|
||||||
gadgetDialog.show(getParentFragmentManager(), "gadget_config");
|
);
|
||||||
|
dialog.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup SO list
|
// Setup SO list
|
||||||
@@ -216,16 +261,6 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
}
|
}
|
||||||
configManager.setAppInjectionMethod(appInfo.getPackageName(), selectedMethod);
|
configManager.setAppInjectionMethod(appInfo.getPackageName(), selectedMethod);
|
||||||
|
|
||||||
// Save gadget config if enabled
|
|
||||||
if (checkboxEnableGadget.isChecked()) {
|
|
||||||
ConfigManager.GadgetConfig currentGadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
|
||||||
if (currentGadgetConfig == null) {
|
|
||||||
// Create default config if not already configured
|
|
||||||
currentGadgetConfig = new ConfigManager.GadgetConfig();
|
|
||||||
configManager.setAppGadgetConfig(appInfo.getPackageName(), currentGadgetConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save SO selection
|
// Save SO selection
|
||||||
if (soListRecyclerView.getAdapter() != null) {
|
if (soListRecyclerView.getAdapter() != null) {
|
||||||
SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter();
|
SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter();
|
||||||
|
|||||||
@@ -163,20 +163,44 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure SO storage directory exists
|
||||||
|
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
|
||||||
|
Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
|
||||||
|
|
||||||
// Copy SO file to our storage
|
// Copy SO file to our storage
|
||||||
|
Log.i(TAG, "Copying SO file from: " + originalPath + " to: " + storedPath);
|
||||||
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
|
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
|
||||||
|
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
|
// Verify the file was actually copied
|
||||||
|
Shell.Result verifyResult = Shell.cmd("test -f \"" + storedPath + "\" && echo 'exists'").exec();
|
||||||
|
if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
|
||||||
|
Log.e(TAG, "File copy appeared successful but file not found at: " + storedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proper permissions for SO file (readable and executable)
|
||||||
|
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + storedPath + "\"").exec();
|
||||||
|
if (!chmodResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set permissions on SO file: " + String.join("\n", chmodResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
SoFile soFile = new SoFile();
|
SoFile soFile = new SoFile();
|
||||||
soFile.name = fileName;
|
soFile.name = fileName;
|
||||||
soFile.storedPath = storedPath;
|
soFile.storedPath = storedPath;
|
||||||
soFile.originalPath = originalPath;
|
soFile.originalPath = originalPath;
|
||||||
config.globalSoFiles.add(soFile);
|
config.globalSoFiles.add(soFile);
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully added SO file: " + fileName + " to storage");
|
||||||
|
|
||||||
if (deleteOriginal) {
|
if (deleteOriginal) {
|
||||||
Shell.cmd("rm \"" + originalPath + "\"").exec();
|
Shell.cmd("rm \"" + originalPath + "\"").exec();
|
||||||
|
Log.i(TAG, "Deleted original file: " + originalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to copy SO file: " + String.join("\n", result.getErr()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,11 +289,46 @@ public class ConfigManager {
|
|||||||
public GadgetConfig getAppGadgetConfig(String packageName) {
|
public GadgetConfig getAppGadgetConfig(String packageName) {
|
||||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
if (appConfig == null) {
|
if (appConfig == null) {
|
||||||
return null;
|
// If no app config, return global gadget config
|
||||||
|
return config.globalGadgetConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If app is set to use global gadget, return global config
|
||||||
|
if (appConfig.useGlobalGadget) {
|
||||||
|
return config.globalGadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return app-specific gadget config
|
||||||
return appConfig.gadgetConfig;
|
return appConfig.gadgetConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GadgetConfig getGlobalGadgetConfig() {
|
||||||
|
return config.globalGadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlobalGadgetConfig(GadgetConfig gadgetConfig) {
|
||||||
|
config.globalGadgetConfig = gadgetConfig;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getAppUseGlobalGadget(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
return true; // Default to use global
|
||||||
|
}
|
||||||
|
return appConfig.useGlobalGadget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppUseGlobalGadget(String packageName, boolean useGlobal) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
appConfig.useGlobalGadget = useGlobal;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
|
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
|
||||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
if (appConfig == null) {
|
if (appConfig == null) {
|
||||||
@@ -405,28 +464,36 @@ public class ConfigManager {
|
|||||||
// Create files directory in app's data dir
|
// Create files directory in app's data dir
|
||||||
String filesDir = "/data/data/" + packageName + "/files";
|
String filesDir = "/data/data/" + packageName + "/files";
|
||||||
|
|
||||||
// Use su -c for better compatibility
|
Log.i(TAG, "Deploying SO files to: " + filesDir);
|
||||||
Shell.Result mkdirResult = Shell.cmd("su -c 'mkdir -p " + filesDir + "'").exec();
|
|
||||||
|
// Create directory without su -c for better compatibility
|
||||||
|
Shell.Result mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
|
||||||
if (!mkdirResult.isSuccess()) {
|
if (!mkdirResult.isSuccess()) {
|
||||||
Log.e(TAG, "Failed to create directory: " + filesDir);
|
Log.e(TAG, "Failed to create directory: " + filesDir);
|
||||||
Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
|
Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
|
||||||
// Try without su -c
|
|
||||||
mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
|
|
||||||
if (!mkdirResult.isSuccess()) {
|
|
||||||
Log.e(TAG, "Also failed without su -c");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Set proper permissions and ownership
|
// Set proper permissions and ownership for the files directory
|
||||||
Shell.cmd("chmod 755 " + filesDir).exec();
|
Shell.cmd("chmod 771 " + filesDir).exec();
|
||||||
|
|
||||||
// Get UID for the package
|
// Get UID and GID for the package
|
||||||
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
|
Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
|
||||||
String uid = "";
|
String uid = "";
|
||||||
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
|
if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
|
||||||
uid = uidResult.getOut().get(0).trim();
|
uid = uidResult.getOut().get(0).trim();
|
||||||
Log.i(TAG, "Package UID: " + uid);
|
Log.i(TAG, "Package UID: " + uid);
|
||||||
|
|
||||||
|
// Set ownership of files directory to match app
|
||||||
|
Shell.Result chownDirResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + filesDir + "\"").exec();
|
||||||
|
if (!chownDirResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set directory ownership: " + String.join("\n", chownDirResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SELinux context for the directory
|
||||||
|
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + filesDir + "\"").exec();
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to get package UID");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy each SO file configured for this app
|
// Copy each SO file configured for this app
|
||||||
@@ -438,39 +505,66 @@ public class ConfigManager {
|
|||||||
Shell.Result checkResult = Shell.cmd("test -f \"" + soFile.storedPath + "\" && echo 'exists'").exec();
|
Shell.Result checkResult = Shell.cmd("test -f \"" + soFile.storedPath + "\" && echo 'exists'").exec();
|
||||||
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
|
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
|
||||||
Log.e(TAG, "Source SO file not found: " + soFile.storedPath);
|
Log.e(TAG, "Source SO file not found: " + soFile.storedPath);
|
||||||
|
// Log more details about the missing file
|
||||||
|
Shell.Result lsResult = Shell.cmd("ls -la \"" + SO_STORAGE_DIR + "\"").exec();
|
||||||
|
Log.e(TAG, "Contents of SO storage dir: " + String.join("\n", lsResult.getOut()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
|
Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
|
||||||
|
|
||||||
// Copy file using cat to avoid permission issues
|
// First, ensure the destination directory exists and has proper permissions
|
||||||
String copyCmd = "cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"";
|
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
|
||||||
Shell.Result result = Shell.cmd(copyCmd).exec();
|
Shell.cmd("chmod 755 \"" + filesDir + "\"").exec();
|
||||||
|
|
||||||
|
// Copy file using cp with force flag
|
||||||
|
Shell.Result result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
if (!result.isSuccess()) {
|
||||||
Log.e(TAG, "Failed with cat, trying cp");
|
Log.e(TAG, "Failed with cp, trying cat method");
|
||||||
// Fallback to cp
|
Log.e(TAG, "cp error: " + String.join("\n", result.getErr()));
|
||||||
result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
|
// Fallback to cat method
|
||||||
|
result = Shell.cmd("cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"").exec();
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
Log.e(TAG, "Also failed with cat method");
|
||||||
|
Log.e(TAG, "cat error: " + String.join("\n", result.getErr()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set permissions
|
// Set permissions - SO files need to be readable and executable
|
||||||
Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
|
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
|
||||||
|
if (!chmodResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set permissions: " + String.join("\n", chmodResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
// Set ownership if we have the UID
|
// Set ownership to match the app's UID
|
||||||
if (!uid.isEmpty()) {
|
if (!uid.isEmpty()) {
|
||||||
Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
|
Shell.Result chownResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
|
||||||
|
if (!chownResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set ownership: " + String.join("\n", chownResult.getErr()));
|
||||||
|
// Try alternative method
|
||||||
|
Shell.cmd("chown " + uid + ".app_" + uid + " \"" + destPath + "\"").exec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the file was copied
|
// Set SELinux context to match app's data files
|
||||||
Shell.Result verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
|
Shell.Result contextResult = Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + destPath + "\"").exec();
|
||||||
|
if (!contextResult.isSuccess()) {
|
||||||
|
Log.w(TAG, "Failed to set SELinux context (this may be normal on some devices)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file was copied with correct permissions
|
||||||
|
Shell.Result verifyResult = Shell.cmd("ls -laZ \"" + destPath + "\" 2>/dev/null").exec();
|
||||||
|
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
||||||
|
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
||||||
|
} else {
|
||||||
|
// Fallback verification without SELinux context
|
||||||
|
verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
|
||||||
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
||||||
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
|
Log.e(TAG, "Failed to verify SO file copy: " + destPath);
|
||||||
// Try another verification method
|
|
||||||
Shell.Result sizeResult = Shell.cmd("stat -c %s \"" + destPath + "\" 2>/dev/null").exec();
|
|
||||||
if (sizeResult.isSuccess() && !sizeResult.getOut().isEmpty()) {
|
|
||||||
Log.i(TAG, "File exists with size: " + sizeResult.getOut().get(0) + " bytes");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,8 +572,9 @@ public class ConfigManager {
|
|||||||
Log.i(TAG, "Deployment complete for: " + packageName);
|
Log.i(TAG, "Deployment complete for: " + packageName);
|
||||||
|
|
||||||
// Deploy gadget config if configured
|
// Deploy gadget config if configured
|
||||||
if (appConfig.gadgetConfig != null) {
|
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
|
||||||
deployGadgetConfigFile(packageName, appConfig.gadgetConfig);
|
if (gadgetToUse != null) {
|
||||||
|
deployGadgetConfigFile(packageName, gadgetToUse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,8 +622,9 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up gadget config file if exists
|
// Clean up gadget config file if exists
|
||||||
if (appConfig.gadgetConfig != null) {
|
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
|
||||||
String gadgetConfigName = appConfig.gadgetConfig.gadgetName.replace(".so", ".config.so");
|
if (gadgetToUse != null) {
|
||||||
|
String gadgetConfigName = gadgetToUse.gadgetName.replace(".so", ".config.so");
|
||||||
String configPath = filesDir + "/" + gadgetConfigName;
|
String configPath = filesDir + "/" + gadgetConfigName;
|
||||||
|
|
||||||
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
|
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
|
||||||
@@ -562,6 +658,7 @@ public class ConfigManager {
|
|||||||
public int injectionDelay = 2; // Default 2 seconds
|
public int injectionDelay = 2; // Default 2 seconds
|
||||||
public List<SoFile> globalSoFiles = new ArrayList<>();
|
public List<SoFile> globalSoFiles = new ArrayList<>();
|
||||||
public Map<String, AppConfig> perAppConfig = new HashMap<>();
|
public Map<String, AppConfig> perAppConfig = new HashMap<>();
|
||||||
|
public GadgetConfig globalGadgetConfig = null; // Global gadget configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AppConfig {
|
public static class AppConfig {
|
||||||
@@ -569,6 +666,7 @@ public class ConfigManager {
|
|||||||
public List<SoFile> soFiles = new ArrayList<>();
|
public List<SoFile> soFiles = new ArrayList<>();
|
||||||
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
|
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
|
||||||
public GadgetConfig gadgetConfig = null;
|
public GadgetConfig gadgetConfig = null;
|
||||||
|
public boolean useGlobalGadget = true; // Whether to use global gadget settings
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SoFile {
|
public static class SoFile {
|
||||||
|
|||||||
@@ -115,18 +115,19 @@ public class FileUtils {
|
|||||||
// Make file readable
|
// Make file readable
|
||||||
tempFile.setReadable(true, false);
|
tempFile.setReadable(true, false);
|
||||||
|
|
||||||
// Use root to copy to a more permanent location
|
// First copy to /data/local/tmp as a temporary location
|
||||||
String targetPath = "/data/local/tmp/" + fileName;
|
String tempTargetPath = "/data/local/tmp/" + fileName;
|
||||||
Shell.Result result = Shell.cmd(
|
Shell.Result result = Shell.cmd(
|
||||||
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + targetPath + "\"",
|
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + tempTargetPath + "\"",
|
||||||
"chmod 644 \"" + targetPath + "\""
|
"chmod 644 \"" + tempTargetPath + "\""
|
||||||
).exec();
|
).exec();
|
||||||
|
|
||||||
// Clean up temp file
|
// Clean up temp file
|
||||||
tempFile.delete();
|
tempFile.delete();
|
||||||
|
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
return targetPath;
|
// Return the temporary path - it will be moved to the proper location by addGlobalSoFile
|
||||||
|
return tempTargetPath;
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
|
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class GadgetConfigDialog extends DialogFragment {
|
|||||||
// Configuration data
|
// Configuration data
|
||||||
private ConfigManager.GadgetConfig config;
|
private ConfigManager.GadgetConfig config;
|
||||||
private OnGadgetConfigListener listener;
|
private OnGadgetConfigListener listener;
|
||||||
|
private String customTitle;
|
||||||
|
|
||||||
// Flag to prevent recursive updates
|
// Flag to prevent recursive updates
|
||||||
private boolean isUpdatingUI = false;
|
private boolean isUpdatingUI = false;
|
||||||
@@ -75,6 +76,21 @@ public class GadgetConfigDialog extends DialogFragment {
|
|||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constructor for non-fragment usage
|
||||||
|
public GadgetConfigDialog(Context context, String title, ConfigManager.GadgetConfig config, OnGadgetConfigListener listener) {
|
||||||
|
// This constructor is for compatibility with direct dialog creation
|
||||||
|
// The actual dialog will be created in show() method
|
||||||
|
this.savedContext = context;
|
||||||
|
this.customTitle = title;
|
||||||
|
this.config = config != null ? config : new ConfigManager.GadgetConfig();
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default constructor required for DialogFragment
|
||||||
|
public GadgetConfigDialog() {
|
||||||
|
// Empty constructor required
|
||||||
|
}
|
||||||
|
|
||||||
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
|
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
@@ -129,8 +145,10 @@ public class GadgetConfigDialog extends DialogFragment {
|
|||||||
setupListeners();
|
setupListeners();
|
||||||
updateJsonPreview();
|
updateJsonPreview();
|
||||||
|
|
||||||
|
String title = customTitle != null ? customTitle : "Gadget 配置";
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(getContext())
|
return new MaterialAlertDialogBuilder(getContext())
|
||||||
.setTitle("Gadget 配置")
|
.setTitle(title)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
@@ -567,6 +585,50 @@ public class GadgetConfigDialog extends DialogFragment {
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show method for non-fragment usage
|
||||||
|
public void show() {
|
||||||
|
if (getContext() == null) {
|
||||||
|
throw new IllegalStateException("Context is required for non-fragment usage");
|
||||||
|
}
|
||||||
|
|
||||||
|
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
|
||||||
|
initViews(view);
|
||||||
|
|
||||||
|
// Initialize config if null
|
||||||
|
if (config == null) {
|
||||||
|
config = new ConfigManager.GadgetConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
setupListeners();
|
||||||
|
updateJsonPreview();
|
||||||
|
|
||||||
|
String title = customTitle != null ? customTitle : "Gadget 配置";
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(getContext())
|
||||||
|
.setTitle(title)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Context savedContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Context getContext() {
|
||||||
|
Context context = super.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
return savedContext;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for non-fragment usage needs to save context
|
||||||
|
public void setContext(Context context) {
|
||||||
|
this.savedContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
private String getPathFromUri(Uri uri) {
|
private String getPathFromUri(Uri uri) {
|
||||||
String path = null;
|
String path = null;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import android.widget.RadioGroup;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -28,6 +30,8 @@ public class SettingsFragment extends Fragment {
|
|||||||
private RadioButton radioShowAll;
|
private RadioButton radioShowAll;
|
||||||
private RadioButton radioHideSystem;
|
private RadioButton radioHideSystem;
|
||||||
private EditText editInjectionDelay;
|
private EditText editInjectionDelay;
|
||||||
|
private TextView tvGlobalGadgetStatus;
|
||||||
|
private Button btnConfigureGlobalGadget;
|
||||||
private ConfigManager configManager;
|
private ConfigManager configManager;
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
@@ -59,6 +63,8 @@ public class SettingsFragment extends Fragment {
|
|||||||
radioShowAll = view.findViewById(R.id.radio_show_all);
|
radioShowAll = view.findViewById(R.id.radio_show_all);
|
||||||
radioHideSystem = view.findViewById(R.id.radio_hide_system);
|
radioHideSystem = view.findViewById(R.id.radio_hide_system);
|
||||||
editInjectionDelay = view.findViewById(R.id.editInjectionDelay);
|
editInjectionDelay = view.findViewById(R.id.editInjectionDelay);
|
||||||
|
tvGlobalGadgetStatus = view.findViewById(R.id.tvGlobalGadgetStatus);
|
||||||
|
btnConfigureGlobalGadget = view.findViewById(R.id.btnConfigureGlobalGadget);
|
||||||
|
|
||||||
configManager = new ConfigManager(getContext());
|
configManager = new ConfigManager(getContext());
|
||||||
}
|
}
|
||||||
@@ -79,6 +85,9 @@ public class SettingsFragment extends Fragment {
|
|||||||
// Load injection delay
|
// Load injection delay
|
||||||
int injectionDelay = configManager.getInjectionDelay();
|
int injectionDelay = configManager.getInjectionDelay();
|
||||||
editInjectionDelay.setText(String.valueOf(injectionDelay));
|
editInjectionDelay.setText(String.valueOf(injectionDelay));
|
||||||
|
|
||||||
|
// Load global gadget status
|
||||||
|
updateGlobalGadgetStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupListeners() {
|
private void setupListeners() {
|
||||||
@@ -124,6 +133,11 @@ public class SettingsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Global gadget configuration button
|
||||||
|
btnConfigureGlobalGadget.setOnClickListener(v -> {
|
||||||
|
showGlobalGadgetConfigDialog();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
||||||
@@ -133,4 +147,34 @@ public class SettingsFragment extends Fragment {
|
|||||||
public boolean isHideSystemApps() {
|
public boolean isHideSystemApps() {
|
||||||
return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
|
return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateGlobalGadgetStatus() {
|
||||||
|
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
|
||||||
|
if (globalGadget != null) {
|
||||||
|
String status = "已配置: " + globalGadget.gadgetName;
|
||||||
|
if (globalGadget.mode.equals("server")) {
|
||||||
|
status += " (Server模式, 端口: " + globalGadget.port + ")";
|
||||||
|
} else {
|
||||||
|
status += " (Script模式)";
|
||||||
|
}
|
||||||
|
tvGlobalGadgetStatus.setText(status);
|
||||||
|
} else {
|
||||||
|
tvGlobalGadgetStatus.setText("未配置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showGlobalGadgetConfigDialog() {
|
||||||
|
// Use existing GadgetConfigDialog
|
||||||
|
GadgetConfigDialog dialog = new GadgetConfigDialog(
|
||||||
|
getContext(),
|
||||||
|
"全局Gadget配置",
|
||||||
|
configManager.getGlobalGadgetConfig(),
|
||||||
|
gadgetConfig -> {
|
||||||
|
// Save global gadget configuration
|
||||||
|
configManager.setGlobalGadgetConfig(gadgetConfig);
|
||||||
|
updateGlobalGadgetStatus();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,35 +88,67 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:layout_marginBottom="8dp">
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
android:id="@+id/checkboxEnableGadget"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="启用 Gadget 配置"
|
android:text="Gadget 配置"
|
||||||
android:layout_marginEnd="8dp" />
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/gadgetConfigGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioNoGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="不使用Gadget"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioUseGlobalGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="使用全局Gadget配置" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvGlobalGadgetInfo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:text="未配置全局Gadget"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioUseCustomGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="自定义Gadget配置" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnConfigureGadget"
|
android:id="@+id/btnConfigureGadget"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="36dp"
|
android:layout_height="wrap_content"
|
||||||
android:text="配置"
|
android:text="配置Gadget"
|
||||||
android:textSize="12sp"
|
android:textSize="14sp"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:enabled="false" />
|
android:enabled="false"
|
||||||
|
android:visibility="gone"
|
||||||
</LinearLayout>
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Gadget可用于Frida调试,勾选后可配置监听地址和端口"
|
android:text="Gadget可用于Frida调试,可配置监听地址和端口"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|||||||
@@ -143,6 +143,53 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- 全局Gadget配置 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="全局Gadget配置"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置全局默认的Gadget设置,应用可以选择使用或覆盖"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvGlobalGadgetStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="未配置"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnConfigureGlobalGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置全局Gadget"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- 其他设置可以在这里添加 -->
|
<!-- 其他设置可以在这里添加 -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -88,12 +88,8 @@ void load_so_file_custom_linker(const char *game_data_dir, const Config::SoFile
|
|||||||
void hack_thread_func(const char *game_data_dir, const char *package_name, JavaVM *vm) {
|
void hack_thread_func(const char *game_data_dir, const char *package_name, JavaVM *vm) {
|
||||||
LOGI("Hack thread started for package: %s", package_name);
|
LOGI("Hack thread started for package: %s", package_name);
|
||||||
|
|
||||||
// Get injection delay from config
|
// Note: Delay is now handled in main thread before this thread is created
|
||||||
int delay = Config::getInjectionDelay();
|
LOGI("Starting injection immediately (delay already applied in main thread)");
|
||||||
LOGI("Waiting %d seconds before injection", delay);
|
|
||||||
|
|
||||||
// Wait for app to initialize and files to be copied
|
|
||||||
sleep(delay);
|
|
||||||
|
|
||||||
// Get injection method for this app
|
// Get injection method for this app
|
||||||
Config::InjectionMethod method = Config::getAppInjectionMethod(package_name);
|
Config::InjectionMethod method = Config::getAppInjectionMethod(package_name);
|
||||||
@@ -135,5 +131,5 @@ void hack_prepare(const char *game_data_dir, const char *package_name, void *dat
|
|||||||
LOGI("hack_prepare called for package: %s, dir: %s", package_name, game_data_dir);
|
LOGI("hack_prepare called for package: %s, dir: %s", package_name, game_data_dir);
|
||||||
|
|
||||||
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
|
std::thread hack_thread(hack_thread_func, game_data_dir, package_name, vm);
|
||||||
hack_thread.detach();
|
hack_thread.join();
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,13 @@ public:
|
|||||||
// Get JavaVM
|
// Get JavaVM
|
||||||
JavaVM *vm = nullptr;
|
JavaVM *vm = nullptr;
|
||||||
if (env->GetJavaVM(&vm) == JNI_OK) {
|
if (env->GetJavaVM(&vm) == JNI_OK) {
|
||||||
|
// Get injection delay from config
|
||||||
|
int delay = Config::getInjectionDelay();
|
||||||
|
LOGI("Main thread blocking for %d seconds before injection", delay);
|
||||||
|
|
||||||
|
// Block main thread for the delay period
|
||||||
|
sleep(delay);
|
||||||
|
|
||||||
// Then start hack thread with JavaVM
|
// Then start hack thread with JavaVM
|
||||||
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length, vm);
|
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length, vm);
|
||||||
hack_thread.detach();
|
hack_thread.detach();
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ set(SOURCES
|
|||||||
|
|
||||||
find_library(log-lib log)
|
find_library(log-lib log)
|
||||||
|
|
||||||
# Build as shared library
|
# Build as static library to be linked into main module
|
||||||
add_library(mylinker SHARED ${SOURCES})
|
add_library(mylinker STATIC ${SOURCES})
|
||||||
|
|
||||||
target_link_libraries(mylinker ${log-lib})
|
target_link_libraries(mylinker ${log-lib})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user