@@ -140,6 +140,8 @@ 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);
|
||||||
|
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
|
||||||
|
|
||||||
appIcon.setImageDrawable(appInfo.getAppIcon());
|
appIcon.setImageDrawable(appInfo.getAppIcon());
|
||||||
appName.setText(appInfo.getAppName());
|
appName.setText(appInfo.getAppName());
|
||||||
@@ -155,6 +157,33 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
|
|||||||
radioStandardInjection.setChecked(true);
|
radioStandardInjection.setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load gadget config
|
||||||
|
ConfigManager.GadgetConfig gadgetConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||||
|
checkboxEnableGadget.setChecked(gadgetConfig != null);
|
||||||
|
btnConfigureGadget.setEnabled(gadgetConfig != null);
|
||||||
|
|
||||||
|
// Setup gadget listeners
|
||||||
|
checkboxEnableGadget.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
btnConfigureGadget.setEnabled(isChecked);
|
||||||
|
if (!isChecked) {
|
||||||
|
// Remove gadget config when unchecked
|
||||||
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnConfigureGadget.setOnClickListener(v -> {
|
||||||
|
ConfigManager.GadgetConfig currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||||
|
if (currentConfig == null) {
|
||||||
|
currentConfig = new ConfigManager.GadgetConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
GadgetConfigDialog gadgetDialog = GadgetConfigDialog.newInstance(currentConfig);
|
||||||
|
gadgetDialog.setOnGadgetConfigListener(config -> {
|
||||||
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
|
||||||
|
});
|
||||||
|
gadgetDialog.show(getParentFragmentManager(), "gadget_config");
|
||||||
|
});
|
||||||
|
|
||||||
// Setup SO list
|
// Setup SO list
|
||||||
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
|
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
|
||||||
List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());
|
List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());
|
||||||
@@ -187,6 +216,16 @@ 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();
|
||||||
|
|||||||
@@ -253,6 +253,141 @@ public class ConfigManager {
|
|||||||
saveConfig();
|
saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getInjectionDelay() {
|
||||||
|
return config.injectionDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInjectionDelay(int delay) {
|
||||||
|
config.injectionDelay = delay;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GadgetConfig getAppGadgetConfig(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return appConfig.gadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old gadget from SO list if exists
|
||||||
|
if (appConfig.gadgetConfig != null) {
|
||||||
|
String oldGadgetName = appConfig.gadgetConfig.gadgetName;
|
||||||
|
appConfig.soFiles.removeIf(soFile -> soFile.name.equals(oldGadgetName));
|
||||||
|
}
|
||||||
|
|
||||||
|
appConfig.gadgetConfig = gadgetConfig;
|
||||||
|
|
||||||
|
// Add new gadget to SO list if configured
|
||||||
|
if (gadgetConfig != null) {
|
||||||
|
// Check if gadget SO file exists in global storage
|
||||||
|
String gadgetPath = SO_STORAGE_DIR + "/" + gadgetConfig.gadgetName;
|
||||||
|
Shell.Result checkResult = Shell.cmd("test -f \"" + gadgetPath + "\" && echo 'exists'").exec();
|
||||||
|
|
||||||
|
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
||||||
|
// Add gadget as a SO file
|
||||||
|
SoFile gadgetSoFile = new SoFile();
|
||||||
|
gadgetSoFile.name = gadgetConfig.gadgetName;
|
||||||
|
gadgetSoFile.storedPath = gadgetPath;
|
||||||
|
gadgetSoFile.originalPath = gadgetPath;
|
||||||
|
|
||||||
|
// Check if already in list
|
||||||
|
boolean alreadyExists = false;
|
||||||
|
for (SoFile soFile : appConfig.soFiles) {
|
||||||
|
if (soFile.name.equals(gadgetSoFile.name)) {
|
||||||
|
alreadyExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
appConfig.soFiles.add(gadgetSoFile);
|
||||||
|
Log.i(TAG, "Added gadget SO to app's SO list: " + gadgetSoFile.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Gadget SO file not found in storage: " + gadgetPath);
|
||||||
|
Log.w(TAG, "Please ensure " + gadgetConfig.gadgetName + " is added to SO library");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
|
||||||
|
// If app is enabled, deploy both gadget SO and config file
|
||||||
|
if (appConfig.enabled) {
|
||||||
|
if (gadgetConfig != null) {
|
||||||
|
deployGadgetConfigFile(packageName, gadgetConfig);
|
||||||
|
}
|
||||||
|
// Re-deploy all SO files including gadget
|
||||||
|
deploySoFilesToApp(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deployGadgetConfigFile(String packageName, GadgetConfig gadgetConfig) {
|
||||||
|
try {
|
||||||
|
// Create gadget config JSON
|
||||||
|
String configJson;
|
||||||
|
if ("script".equals(gadgetConfig.mode)) {
|
||||||
|
configJson = String.format(
|
||||||
|
"{\n" +
|
||||||
|
" \"interaction\": {\n" +
|
||||||
|
" \"type\": \"script\",\n" +
|
||||||
|
" \"path\": \"%s\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}",
|
||||||
|
gadgetConfig.scriptPath
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
configJson = String.format(
|
||||||
|
"{\n" +
|
||||||
|
" \"interaction\": {\n" +
|
||||||
|
" \"type\": \"listen\",\n" +
|
||||||
|
" \"address\": \"%s\",\n" +
|
||||||
|
" \"port\": %d,\n" +
|
||||||
|
" \"on_port_conflict\": \"%s\",\n" +
|
||||||
|
" \"on_load\": \"%s\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}",
|
||||||
|
gadgetConfig.address,
|
||||||
|
gadgetConfig.port,
|
||||||
|
gadgetConfig.onPortConflict,
|
||||||
|
gadgetConfig.onLoad
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to temp file
|
||||||
|
String tempFile = context.getCacheDir() + "/" + gadgetConfig.gadgetName + ".config";
|
||||||
|
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||||
|
writer.write(configJson);
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
// Copy to app's files directory
|
||||||
|
String filesDir = "/data/data/" + packageName + "/files";
|
||||||
|
String gadgetConfigName = gadgetConfig.gadgetName.replace(".so", ".config.so");
|
||||||
|
String targetPath = filesDir + "/" + gadgetConfigName;
|
||||||
|
|
||||||
|
Shell.Result copyResult = Shell.cmd("cp " + tempFile + " " + targetPath).exec();
|
||||||
|
if (copyResult.isSuccess()) {
|
||||||
|
// Set permissions
|
||||||
|
Shell.cmd("chmod 644 " + targetPath).exec();
|
||||||
|
Log.i(TAG, "Deployed gadget config to: " + targetPath);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to deploy gadget config: " + String.join("\n", copyResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
new java.io.File(tempFile).delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to create gadget config file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Copy SO files directly to app's data directory
|
// Copy SO files directly to app's data directory
|
||||||
private void deploySoFilesToApp(String packageName) {
|
private void deploySoFilesToApp(String packageName) {
|
||||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
@@ -341,6 +476,11 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Deployment complete for: " + packageName);
|
Log.i(TAG, "Deployment complete for: " + packageName);
|
||||||
|
|
||||||
|
// Deploy gadget config if configured
|
||||||
|
if (appConfig.gadgetConfig != null) {
|
||||||
|
deployGadgetConfigFile(packageName, appConfig.gadgetConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up deployed SO files when app is disabled
|
// Clean up deployed SO files when app is disabled
|
||||||
@@ -386,6 +526,23 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up gadget config file if exists
|
||||||
|
if (appConfig.gadgetConfig != null) {
|
||||||
|
String gadgetConfigName = appConfig.gadgetConfig.gadgetName.replace(".so", ".config.so");
|
||||||
|
String configPath = filesDir + "/" + gadgetConfigName;
|
||||||
|
|
||||||
|
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
|
||||||
|
if (checkConfigResult.isSuccess() && !checkConfigResult.getOut().isEmpty()) {
|
||||||
|
Shell.Result deleteResult = Shell.cmd("rm -f \"" + configPath + "\"").exec();
|
||||||
|
if (deleteResult.isSuccess()) {
|
||||||
|
Log.i(TAG, "Deleted gadget config: " + configPath);
|
||||||
|
} else {
|
||||||
|
// Try with su -c
|
||||||
|
Shell.cmd("su -c 'rm -f \"" + configPath + "\"'").exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Cleanup complete for: " + packageName);
|
Log.i(TAG, "Cleanup complete for: " + packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +559,7 @@ public class ConfigManager {
|
|||||||
public static class ModuleConfig {
|
public static class ModuleConfig {
|
||||||
public boolean enabled = true;
|
public boolean enabled = true;
|
||||||
public boolean hideInjection = false;
|
public boolean hideInjection = false;
|
||||||
|
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<>();
|
||||||
}
|
}
|
||||||
@@ -410,6 +568,7 @@ public class ConfigManager {
|
|||||||
public boolean enabled = false;
|
public boolean enabled = false;
|
||||||
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 static class SoFile {
|
public static class SoFile {
|
||||||
@@ -425,4 +584,17 @@ public class ConfigManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class GadgetConfig {
|
||||||
|
public String mode = "server"; // "server" or "script"
|
||||||
|
// Server mode config
|
||||||
|
public String address = "0.0.0.0";
|
||||||
|
public int port = 27042;
|
||||||
|
public String onPortConflict = "fail";
|
||||||
|
public String onLoad = "wait";
|
||||||
|
// Script mode config
|
||||||
|
public String scriptPath = "/data/local/tmp/script.js";
|
||||||
|
// Common config
|
||||||
|
public String gadgetName = "libgadget.so";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
170
configapp/src/main/java/com/jiqiu/configapp/FileUtils.java
Normal file
170
configapp/src/main/java/com/jiqiu/configapp/FileUtils.java
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class FileUtils {
|
||||||
|
private static final String TAG = "FileUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get real file path from URI, handling both file:// and content:// URIs
|
||||||
|
* @param context Context
|
||||||
|
* @param uri The URI to resolve
|
||||||
|
* @return The real file path, or null if unable to resolve
|
||||||
|
*/
|
||||||
|
public static String getRealPathFromUri(Context context, Uri uri) {
|
||||||
|
if (uri == null) return null;
|
||||||
|
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
if (scheme == null) return null;
|
||||||
|
|
||||||
|
// Handle file:// URIs
|
||||||
|
if ("file".equals(scheme)) {
|
||||||
|
return uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle content:// URIs
|
||||||
|
if ("content".equals(scheme)) {
|
||||||
|
// For content URIs, we need to copy the file to a temporary location
|
||||||
|
return copyFileFromContentUri(context, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct path extraction as fallback
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
// Some file managers return paths like /external_files/...
|
||||||
|
// Try to resolve these to actual paths
|
||||||
|
if (path.contains(":")) {
|
||||||
|
String[] parts = path.split(":");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
String type = parts[0];
|
||||||
|
String relativePath = parts[1];
|
||||||
|
|
||||||
|
// Common storage locations
|
||||||
|
if (type.endsWith("/primary")) {
|
||||||
|
return "/storage/emulated/0/" + relativePath;
|
||||||
|
} else if (type.contains("external")) {
|
||||||
|
return "/storage/emulated/0/" + relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any file:// prefix
|
||||||
|
if (path.startsWith("file://")) {
|
||||||
|
path = path.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path exists
|
||||||
|
Shell.Result result = Shell.cmd("test -f \"" + path + "\" && echo 'exists'").exec();
|
||||||
|
if (result.isSuccess() && !result.getOut().isEmpty()) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a file from content URI to temporary location
|
||||||
|
* @param context Context
|
||||||
|
* @param uri Content URI
|
||||||
|
* @return Path to copied file, or null on failure
|
||||||
|
*/
|
||||||
|
private static String copyFileFromContentUri(Context context, Uri uri) {
|
||||||
|
ContentResolver resolver = context.getContentResolver();
|
||||||
|
String fileName = getFileName(context, uri);
|
||||||
|
|
||||||
|
if (fileName == null || !fileName.endsWith(".so")) {
|
||||||
|
fileName = "temp_" + System.currentTimeMillis() + ".so";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temp directory
|
||||||
|
File tempDir = new File(context.getCacheDir(), "so_temp");
|
||||||
|
if (!tempDir.exists()) {
|
||||||
|
tempDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
File tempFile = new File(tempDir, fileName);
|
||||||
|
|
||||||
|
try (InputStream inputStream = resolver.openInputStream(uri);
|
||||||
|
OutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||||
|
|
||||||
|
if (inputStream == null) {
|
||||||
|
Log.e(TAG, "Unable to open input stream for URI: " + uri);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make file readable
|
||||||
|
tempFile.setReadable(true, false);
|
||||||
|
|
||||||
|
// Use root to copy to a more permanent location
|
||||||
|
String targetPath = "/data/local/tmp/" + fileName;
|
||||||
|
Shell.Result result = Shell.cmd(
|
||||||
|
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + targetPath + "\"",
|
||||||
|
"chmod 644 \"" + targetPath + "\""
|
||||||
|
).exec();
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
tempFile.delete();
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
return targetPath;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Error copying file from content URI", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file name from URI
|
||||||
|
* @param context Context
|
||||||
|
* @param uri URI to get name from
|
||||||
|
* @return File name or null
|
||||||
|
*/
|
||||||
|
private static String getFileName(Context context, Uri uri) {
|
||||||
|
String fileName = null;
|
||||||
|
|
||||||
|
if ("content".equals(uri.getScheme())) {
|
||||||
|
try (Cursor cursor = context.getContentResolver().query(
|
||||||
|
uri, null, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
if (nameIndex >= 0) {
|
||||||
|
fileName = cursor.getString(nameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting file name from URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName == null) {
|
||||||
|
fileName = uri.getLastPathSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.database.Cursor;
|
||||||
|
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.DialogFragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class GadgetConfigDialog extends DialogFragment {
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
private RadioGroup modeRadioGroup;
|
||||||
|
private RadioButton radioModeServer;
|
||||||
|
private RadioButton radioModeScript;
|
||||||
|
private LinearLayout serverModeLayout;
|
||||||
|
private LinearLayout scriptModeLayout;
|
||||||
|
private RadioGroup addressRadioGroup;
|
||||||
|
private RadioButton radioAddressAll;
|
||||||
|
private RadioButton radioAddressLocal;
|
||||||
|
private RadioButton radioAddressCustom;
|
||||||
|
private EditText editCustomAddress;
|
||||||
|
private EditText editPort;
|
||||||
|
private RadioGroup portConflictRadioGroup;
|
||||||
|
private RadioButton radioConflictFail;
|
||||||
|
private RadioButton radioConflictPickNext;
|
||||||
|
private RadioGroup onLoadRadioGroup;
|
||||||
|
private RadioButton radioLoadWait;
|
||||||
|
private RadioButton radioLoadResume;
|
||||||
|
private EditText editScriptPath;
|
||||||
|
private EditText editGadgetName;
|
||||||
|
private EditText editJsonPreview;
|
||||||
|
|
||||||
|
// Configuration data
|
||||||
|
private ConfigManager.GadgetConfig config;
|
||||||
|
private OnGadgetConfigListener listener;
|
||||||
|
|
||||||
|
// Flag to prevent recursive updates
|
||||||
|
private boolean isUpdatingUI = false;
|
||||||
|
|
||||||
|
// Activity result launchers
|
||||||
|
private ActivityResultLauncher<Intent> fileBrowserLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> filePickerLauncher;
|
||||||
|
|
||||||
|
public interface OnGadgetConfigListener {
|
||||||
|
void onGadgetConfigSaved(ConfigManager.GadgetConfig config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GadgetConfigDialog newInstance(ConfigManager.GadgetConfig config) {
|
||||||
|
GadgetConfigDialog dialog = new GadgetConfigDialog();
|
||||||
|
dialog.config = config != null ? config : new ConfigManager.GadgetConfig();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Initialize file browser launcher
|
||||||
|
fileBrowserLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
String selectedPath = result.getData().getStringExtra("selected_path");
|
||||||
|
if (selectedPath != null) {
|
||||||
|
editScriptPath.setText(selectedPath);
|
||||||
|
config.scriptPath = selectedPath;
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize file picker launcher
|
||||||
|
filePickerLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
Uri uri = result.getData().getData();
|
||||||
|
if (uri != null) {
|
||||||
|
String path = getPathFromUri(uri);
|
||||||
|
if (path != null) {
|
||||||
|
editScriptPath.setText(path);
|
||||||
|
config.scriptPath = path;
|
||||||
|
updateJsonPreview();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "无法获取文件路径", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||||
|
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
|
||||||
|
|
||||||
|
initViews(view);
|
||||||
|
loadConfig();
|
||||||
|
setupListeners();
|
||||||
|
updateJsonPreview();
|
||||||
|
|
||||||
|
return new MaterialAlertDialogBuilder(getContext())
|
||||||
|
.setTitle("Gadget 配置")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews(View view) {
|
||||||
|
modeRadioGroup = view.findViewById(R.id.modeRadioGroup);
|
||||||
|
radioModeServer = view.findViewById(R.id.radioModeServer);
|
||||||
|
radioModeScript = view.findViewById(R.id.radioModeScript);
|
||||||
|
serverModeLayout = view.findViewById(R.id.serverModeLayout);
|
||||||
|
scriptModeLayout = view.findViewById(R.id.scriptModeLayout);
|
||||||
|
addressRadioGroup = view.findViewById(R.id.addressRadioGroup);
|
||||||
|
radioAddressAll = view.findViewById(R.id.radioAddressAll);
|
||||||
|
radioAddressLocal = view.findViewById(R.id.radioAddressLocal);
|
||||||
|
radioAddressCustom = view.findViewById(R.id.radioAddressCustom);
|
||||||
|
editCustomAddress = view.findViewById(R.id.editCustomAddress);
|
||||||
|
editPort = view.findViewById(R.id.editPort);
|
||||||
|
portConflictRadioGroup = view.findViewById(R.id.portConflictRadioGroup);
|
||||||
|
radioConflictFail = view.findViewById(R.id.radioConflictFail);
|
||||||
|
radioConflictPickNext = view.findViewById(R.id.radioConflictPickNext);
|
||||||
|
onLoadRadioGroup = view.findViewById(R.id.onLoadRadioGroup);
|
||||||
|
radioLoadWait = view.findViewById(R.id.radioLoadWait);
|
||||||
|
radioLoadResume = view.findViewById(R.id.radioLoadResume);
|
||||||
|
editScriptPath = view.findViewById(R.id.editScriptPath);
|
||||||
|
editGadgetName = view.findViewById(R.id.editGadgetName);
|
||||||
|
editJsonPreview = view.findViewById(R.id.editJsonPreview);
|
||||||
|
|
||||||
|
// File select button
|
||||||
|
View btnSelectScript = view.findViewById(R.id.btnSelectScript);
|
||||||
|
if (btnSelectScript != null) {
|
||||||
|
btnSelectScript.setOnClickListener(v -> selectScriptFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
isUpdatingUI = true;
|
||||||
|
|
||||||
|
// Load mode
|
||||||
|
if ("script".equals(config.mode)) {
|
||||||
|
radioModeScript.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.GONE);
|
||||||
|
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
radioModeServer.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
scriptModeLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load address
|
||||||
|
if ("127.0.0.1".equals(config.address)) {
|
||||||
|
radioAddressLocal.setChecked(true);
|
||||||
|
} else if ("0.0.0.0".equals(config.address)) {
|
||||||
|
radioAddressAll.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioAddressCustom.setChecked(true);
|
||||||
|
editCustomAddress.setText(config.address);
|
||||||
|
editCustomAddress.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load port
|
||||||
|
editPort.setText(String.valueOf(config.port));
|
||||||
|
|
||||||
|
// Load port conflict handling
|
||||||
|
if ("pick-next".equals(config.onPortConflict)) {
|
||||||
|
radioConflictPickNext.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioConflictFail.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load on load handling
|
||||||
|
if ("resume".equals(config.onLoad)) {
|
||||||
|
radioLoadResume.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioLoadWait.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load script path
|
||||||
|
editScriptPath.setText(config.scriptPath);
|
||||||
|
|
||||||
|
// Load gadget name
|
||||||
|
editGadgetName.setText(config.gadgetName);
|
||||||
|
|
||||||
|
isUpdatingUI = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupListeners() {
|
||||||
|
// Mode radio group listener
|
||||||
|
modeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
if (checkedId == R.id.radioModeScript) {
|
||||||
|
config.mode = "script";
|
||||||
|
serverModeLayout.setVisibility(View.GONE);
|
||||||
|
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
config.mode = "server";
|
||||||
|
serverModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
scriptModeLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Address radio group listener
|
||||||
|
addressRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
if (checkedId == R.id.radioAddressCustom) {
|
||||||
|
editCustomAddress.setEnabled(true);
|
||||||
|
editCustomAddress.requestFocus();
|
||||||
|
} else {
|
||||||
|
editCustomAddress.setEnabled(false);
|
||||||
|
if (checkedId == R.id.radioAddressAll) {
|
||||||
|
config.address = "0.0.0.0";
|
||||||
|
} else if (checkedId == R.id.radioAddressLocal) {
|
||||||
|
config.address = "127.0.0.1";
|
||||||
|
}
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom address text watcher
|
||||||
|
editCustomAddress.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI && radioAddressCustom.isChecked()) {
|
||||||
|
config.address = s.toString().trim();
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Port text watcher
|
||||||
|
editPort.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
try {
|
||||||
|
int port = Integer.parseInt(s.toString());
|
||||||
|
if (port >= 1 && port <= 65535) {
|
||||||
|
config.port = port;
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore invalid input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Port conflict radio group listener
|
||||||
|
portConflictRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.onPortConflict = (checkedId == R.id.radioConflictPickNext) ? "pick-next" : "fail";
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// On load radio group listener
|
||||||
|
onLoadRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.onLoad = (checkedId == R.id.radioLoadResume) ? "resume" : "wait";
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Script path text watcher
|
||||||
|
editScriptPath.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.scriptPath = s.toString().trim();
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gadget name text watcher
|
||||||
|
editGadgetName.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.gadgetName = s.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON preview text watcher
|
||||||
|
editJsonPreview.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
parseJsonAndUpdateUI(s.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateJsonPreview() {
|
||||||
|
if (isUpdatingUI) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject root = new JSONObject();
|
||||||
|
JSONObject interaction = new JSONObject();
|
||||||
|
|
||||||
|
if ("script".equals(config.mode)) {
|
||||||
|
interaction.put("type", "script");
|
||||||
|
interaction.put("path", config.scriptPath);
|
||||||
|
} else {
|
||||||
|
interaction.put("type", "listen");
|
||||||
|
interaction.put("address", config.address);
|
||||||
|
interaction.put("port", config.port);
|
||||||
|
interaction.put("on_port_conflict", config.onPortConflict);
|
||||||
|
interaction.put("on_load", config.onLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.put("interaction", interaction);
|
||||||
|
|
||||||
|
isUpdatingUI = true;
|
||||||
|
editJsonPreview.setText(root.toString(2));
|
||||||
|
isUpdatingUI = false;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// Should not happen
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseJsonAndUpdateUI(String json) {
|
||||||
|
try {
|
||||||
|
JSONObject root = new JSONObject(json);
|
||||||
|
JSONObject interaction = root.getJSONObject("interaction");
|
||||||
|
|
||||||
|
isUpdatingUI = true;
|
||||||
|
|
||||||
|
// Update mode
|
||||||
|
String type = interaction.getString("type");
|
||||||
|
if ("script".equals(type)) {
|
||||||
|
config.mode = "script";
|
||||||
|
radioModeScript.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.GONE);
|
||||||
|
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Update script path
|
||||||
|
if (interaction.has("path")) {
|
||||||
|
config.scriptPath = interaction.getString("path");
|
||||||
|
editScriptPath.setText(config.scriptPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.mode = "server";
|
||||||
|
radioModeServer.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
scriptModeLayout.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
// Update address
|
||||||
|
String address = interaction.getString("address");
|
||||||
|
config.address = address;
|
||||||
|
if ("0.0.0.0".equals(address)) {
|
||||||
|
radioAddressAll.setChecked(true);
|
||||||
|
editCustomAddress.setEnabled(false);
|
||||||
|
} else if ("127.0.0.1".equals(address)) {
|
||||||
|
radioAddressLocal.setChecked(true);
|
||||||
|
editCustomAddress.setEnabled(false);
|
||||||
|
} else {
|
||||||
|
radioAddressCustom.setChecked(true);
|
||||||
|
editCustomAddress.setText(address);
|
||||||
|
editCustomAddress.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update port
|
||||||
|
config.port = interaction.getInt("port");
|
||||||
|
editPort.setText(String.valueOf(config.port));
|
||||||
|
|
||||||
|
// Update port conflict
|
||||||
|
String onPortConflict = interaction.getString("on_port_conflict");
|
||||||
|
config.onPortConflict = onPortConflict;
|
||||||
|
if ("pick-next".equals(onPortConflict)) {
|
||||||
|
radioConflictPickNext.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioConflictFail.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update on load
|
||||||
|
String onLoad = interaction.getString("on_load");
|
||||||
|
config.onLoad = onLoad;
|
||||||
|
if ("resume".equals(onLoad)) {
|
||||||
|
radioLoadResume.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioLoadWait.setChecked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingUI = false;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// Invalid JSON, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveConfig() {
|
||||||
|
if (listener != null) {
|
||||||
|
// Ensure gadget name is not empty
|
||||||
|
if (config.gadgetName == null || config.gadgetName.trim().isEmpty()) {
|
||||||
|
config.gadgetName = "libgadget.so";
|
||||||
|
}
|
||||||
|
listener.onGadgetConfigSaved(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectScriptFile() {
|
||||||
|
String[] options = {"浏览文件系统", "从外部文件管理器选择", "手动输入路径"};
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("选择 Script 文件")
|
||||||
|
.setItems(options, (dialog, which) -> {
|
||||||
|
if (which == 0) {
|
||||||
|
openFileBrowser();
|
||||||
|
} else if (which == 1) {
|
||||||
|
openFilePicker();
|
||||||
|
} else {
|
||||||
|
showPathInputDialog();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openFileBrowser() {
|
||||||
|
// Show path selection dialog first
|
||||||
|
String[] paths = {
|
||||||
|
"/data/local/tmp",
|
||||||
|
"/sdcard",
|
||||||
|
"/sdcard/Download",
|
||||||
|
"/storage/emulated/0",
|
||||||
|
"自定义路径..."
|
||||||
|
};
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("选择起始目录")
|
||||||
|
.setItems(paths, (dialog, which) -> {
|
||||||
|
if (which == paths.length - 1) {
|
||||||
|
// Custom path
|
||||||
|
showCustomPathDialog();
|
||||||
|
} else {
|
||||||
|
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
|
||||||
|
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, paths[which]);
|
||||||
|
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".js");
|
||||||
|
fileBrowserLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCustomPathDialog() {
|
||||||
|
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
|
||||||
|
android.widget.EditText editText = view.findViewById(android.R.id.edit);
|
||||||
|
editText.setText("/");
|
||||||
|
editText.setHint("输入起始路径");
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("自定义起始路径")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("确定", (dialog, which) -> {
|
||||||
|
String path = editText.getText().toString().trim();
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
|
||||||
|
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, path);
|
||||||
|
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".js");
|
||||||
|
fileBrowserLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openFilePicker() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
// Add MIME types that might help filter JS files
|
||||||
|
String[] mimeTypes = {"text/javascript", "application/javascript", "text/plain", "*/*"};
|
||||||
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||||
|
// Suggest starting location
|
||||||
|
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||||
|
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||||
|
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.setText("/data/local/tmp/");
|
||||||
|
editText.setHint("/data/local/tmp/script.js");
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("输入 Script 文件路径")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("确定", (dialog, which) -> {
|
||||||
|
String path = editText.getText().toString().trim();
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
editScriptPath.setText(path);
|
||||||
|
config.scriptPath = path;
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPathFromUri(Uri uri) {
|
||||||
|
String path = null;
|
||||||
|
|
||||||
|
// Try to get path from MediaStore
|
||||||
|
if ("content".equals(uri.getScheme())) {
|
||||||
|
try {
|
||||||
|
ContentResolver resolver = getContext().getContentResolver();
|
||||||
|
try (Cursor cursor = resolver.query(uri, new String[]{"_data"}, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int columnIndex = cursor.getColumnIndex("_data");
|
||||||
|
if (columnIndex != -1) {
|
||||||
|
path = cursor.getString(columnIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try DocumentsContract if MediaStore fails
|
||||||
|
if (path == null && DocumentsContract.isDocumentUri(getContext(), uri)) {
|
||||||
|
try {
|
||||||
|
String docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
if (uri.getAuthority().equals("com.android.externalstorage.documents")) {
|
||||||
|
String[] split = docId.split(":");
|
||||||
|
if (split.length >= 2) {
|
||||||
|
String type = split[0];
|
||||||
|
if ("primary".equalsIgnoreCase(type)) {
|
||||||
|
path = "/storage/emulated/0/" + split[1];
|
||||||
|
} else {
|
||||||
|
path = "/storage/" + type + "/" + split[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("file".equals(uri.getScheme())) {
|
||||||
|
path = uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.RadioButton;
|
import android.widget.RadioButton;
|
||||||
import android.widget.RadioGroup;
|
import android.widget.RadioGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.text.Editable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -24,6 +27,8 @@ public class SettingsFragment extends Fragment {
|
|||||||
private RadioGroup radioGroupFilter;
|
private RadioGroup radioGroupFilter;
|
||||||
private RadioButton radioShowAll;
|
private RadioButton radioShowAll;
|
||||||
private RadioButton radioHideSystem;
|
private RadioButton radioHideSystem;
|
||||||
|
private EditText editInjectionDelay;
|
||||||
|
private ConfigManager configManager;
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private OnSettingsChangeListener settingsChangeListener;
|
private OnSettingsChangeListener settingsChangeListener;
|
||||||
@@ -53,6 +58,9 @@ public class SettingsFragment extends Fragment {
|
|||||||
radioGroupFilter = view.findViewById(R.id.radio_group_filter);
|
radioGroupFilter = view.findViewById(R.id.radio_group_filter);
|
||||||
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);
|
||||||
|
|
||||||
|
configManager = new ConfigManager(getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSharedPreferences() {
|
private void initSharedPreferences() {
|
||||||
@@ -67,6 +75,10 @@ public class SettingsFragment extends Fragment {
|
|||||||
} else {
|
} else {
|
||||||
radioShowAll.setChecked(true);
|
radioShowAll.setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load injection delay
|
||||||
|
int injectionDelay = configManager.getInjectionDelay();
|
||||||
|
editInjectionDelay.setText(String.valueOf(injectionDelay));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupListeners() {
|
private void setupListeners() {
|
||||||
@@ -86,6 +98,32 @@ public class SettingsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Injection delay listener
|
||||||
|
editInjectionDelay.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
String text = s.toString().trim();
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
try {
|
||||||
|
int delay = Integer.parseInt(text);
|
||||||
|
// Limit delay between 0 and 60 seconds
|
||||||
|
if (delay < 0) delay = 0;
|
||||||
|
if (delay > 60) delay = 60;
|
||||||
|
|
||||||
|
configManager.setInjectionDelay(delay);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore invalid input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ public class SoManagerFragment extends Fragment {
|
|||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
// Add MIME types that might help filter SO files
|
||||||
|
String[] mimeTypes = {"application/octet-stream", "application/x-sharedlib", "*/*"};
|
||||||
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||||
|
// Suggest starting location
|
||||||
|
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||||
|
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||||
filePickerLauncher.launch(intent);
|
filePickerLauncher.launch(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,14 +225,12 @@ public class SoManagerFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleFileSelection(Uri uri) {
|
private void handleFileSelection(Uri uri) {
|
||||||
// Get real path from URI
|
// Get real path from URI using proper URI handling
|
||||||
String path = uri.getPath();
|
String path = FileUtils.getRealPathFromUri(requireContext(), uri);
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
// Remove the file:// prefix if present
|
|
||||||
if (path.startsWith("file://")) {
|
|
||||||
path = path.substring(7);
|
|
||||||
}
|
|
||||||
showDeleteOriginalDialog(path);
|
showDeleteOriginalDialog(path);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "无法获取文件路径,请尝试其他方式", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
@@ -64,7 +68,7 @@
|
|||||||
android:id="@+id/soListRecyclerView"
|
android:id="@+id/soListRecyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:maxHeight="200dp"
|
android:nestedScrollingEnabled="false"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -77,13 +81,52 @@
|
|||||||
android:textColor="?android:attr/textColorTertiary"
|
android:textColor="?android:attr/textColorTertiary"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
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_height="wrap_content"
|
||||||
|
android:text="启用 Gadget 配置"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnConfigureGadget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:text="配置"
|
||||||
|
android:textSize="12sp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:enabled="false" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget可用于Frida调试,勾选后可配置监听地址和端口"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="注入方式"
|
android:text="注入方式"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -140,3 +183,5 @@
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
299
configapp/src/main/res/layout/dialog_gadget_config.xml
Normal file
299
configapp/src/main/res/layout/dialog_gadget_config.xml
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView 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">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 配置"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="提示:请确保对应的 gadget SO 文件已添加到 SO 库中"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 模式选择 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 模式"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/modeRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioModeServer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Server 模式"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioModeScript"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Script 模式" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- Server 模式配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/serverModeLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- 监听地址配置 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="监听地址"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/addressRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioAddressAll"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="0.0.0.0 (监听所有接口)"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioAddressLocal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="127.0.0.1 (仅本地)" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioAddressCustom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="自定义" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editCustomAddress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="输入自定义地址"
|
||||||
|
android:inputType="text"
|
||||||
|
android:enabled="false"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 端口配置 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="监听端口"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editPort"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="27042"
|
||||||
|
android:hint="1-65535"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 端口冲突处理 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="端口冲突处理"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/portConflictRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioConflictFail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="fail (启动失败)"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioConflictPickNext"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="pick-next (尝试下一个端口)" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- 加载处理方式 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="加载时处理方式"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/onLoadRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioLoadWait"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="wait (等待连接)"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioLoadResume"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="resume (立即继续)" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Script 模式配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/scriptModeLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Script 文件路径"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editScriptPath"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="/data/local/tmp/script.js"
|
||||||
|
android:inputType="text"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnSelectScript"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="选择"
|
||||||
|
android:textSize="12sp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="提示:Script 模式下,Gadget 会在程序入口点执行前自动加载并运行指定的脚本文件"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Gadget 名称 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 文件名"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editGadgetName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="libgadget.so"
|
||||||
|
android:hint="例如: libgadget.so"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- JSON 预览区域 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置预览 (可直接编辑)"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardCornerRadius="4dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editJsonPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="150dp"
|
||||||
|
android:gravity="top"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:overScrollMode="always" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
@@ -72,6 +72,77 @@
|
|||||||
|
|
||||||
</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_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="注入延迟时间"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="设置SO文件注入前的等待时间,以确保应用完全初始化"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="延迟时间(秒):"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editInjectionDelay"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="2"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:hint="0-60"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="秒"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="建议值:2-5秒。某些应用可能需要更长时间。"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ namespace Config {
|
|||||||
} else if (json[valueStart] == 't' || json[valueStart] == 'f') {
|
} else if (json[valueStart] == 't' || json[valueStart] == 'f') {
|
||||||
// Boolean value
|
// Boolean value
|
||||||
return (json.substr(valueStart, 4) == "true") ? "true" : "false";
|
return (json.substr(valueStart, 4) == "true") ? "true" : "false";
|
||||||
|
} else {
|
||||||
|
// Number value
|
||||||
|
size_t valueEnd = json.find_first_of(",} \t\n", valueStart);
|
||||||
|
if (valueEnd == std::string::npos) {
|
||||||
|
return json.substr(valueStart);
|
||||||
|
}
|
||||||
|
return json.substr(valueStart, valueEnd - valueStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
@@ -85,11 +92,44 @@ namespace Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse gadgetConfig if exists
|
||||||
|
size_t gadgetPos = appJson.find("\"gadgetConfig\"");
|
||||||
|
if (gadgetPos != std::string::npos) {
|
||||||
|
size_t gadgetObjStart = appJson.find("{", gadgetPos);
|
||||||
|
size_t gadgetObjEnd = appJson.find("}", gadgetObjStart);
|
||||||
|
|
||||||
|
if (gadgetObjStart != std::string::npos && gadgetObjEnd != std::string::npos) {
|
||||||
|
std::string gadgetObj = appJson.substr(gadgetObjStart, gadgetObjEnd - gadgetObjStart + 1);
|
||||||
|
|
||||||
|
GadgetConfig* gadgetConfig = new GadgetConfig();
|
||||||
|
|
||||||
|
std::string address = extractValue(gadgetObj, "address");
|
||||||
|
if (!address.empty()) gadgetConfig->address = address;
|
||||||
|
|
||||||
|
std::string portStr = extractValue(gadgetObj, "port");
|
||||||
|
if (!portStr.empty()) gadgetConfig->port = std::stoi(portStr);
|
||||||
|
|
||||||
|
std::string onPortConflict = extractValue(gadgetObj, "onPortConflict");
|
||||||
|
if (!onPortConflict.empty()) gadgetConfig->onPortConflict = onPortConflict;
|
||||||
|
|
||||||
|
std::string onLoad = extractValue(gadgetObj, "onLoad");
|
||||||
|
if (!onLoad.empty()) gadgetConfig->onLoad = onLoad;
|
||||||
|
|
||||||
|
std::string gadgetName = extractValue(gadgetObj, "gadgetName");
|
||||||
|
if (!gadgetName.empty()) gadgetConfig->gadgetName = gadgetName;
|
||||||
|
|
||||||
|
appConfig.gadgetConfig = gadgetConfig;
|
||||||
|
LOGD("Loaded gadget config: %s:%d, name: %s",
|
||||||
|
gadgetConfig->address.c_str(), gadgetConfig->port, gadgetConfig->gadgetName.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g_config.perAppConfig[packageName] = appConfig;
|
g_config.perAppConfig[packageName] = appConfig;
|
||||||
const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" :
|
const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" :
|
||||||
appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard";
|
appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard";
|
||||||
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu",
|
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu, gadget: %s",
|
||||||
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size());
|
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(),
|
||||||
|
appConfig.gadgetConfig ? "yes" : "no");
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleConfig readConfig() {
|
ModuleConfig readConfig() {
|
||||||
@@ -118,7 +158,13 @@ namespace Config {
|
|||||||
std::string hideStr = extractValue(json, "hideInjection");
|
std::string hideStr = extractValue(json, "hideInjection");
|
||||||
g_config.hideInjection = (hideStr == "true");
|
g_config.hideInjection = (hideStr == "true");
|
||||||
|
|
||||||
LOGD("Module enabled: %d, hide injection: %d", g_config.enabled, g_config.hideInjection);
|
std::string delayStr = extractValue(json, "injectionDelay");
|
||||||
|
if (!delayStr.empty()) {
|
||||||
|
g_config.injectionDelay = std::stoi(delayStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Module enabled: %d, hide injection: %d, injection delay: %d",
|
||||||
|
g_config.enabled, g_config.hideInjection, g_config.injectionDelay);
|
||||||
|
|
||||||
// Parse perAppConfig
|
// Parse perAppConfig
|
||||||
size_t perAppPos = json.find("\"perAppConfig\"");
|
size_t perAppPos = json.find("\"perAppConfig\"");
|
||||||
@@ -212,4 +258,11 @@ namespace Config {
|
|||||||
}
|
}
|
||||||
return InjectionMethod::STANDARD;
|
return InjectionMethod::STANDARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getInjectionDelay() {
|
||||||
|
if (!g_configLoaded) {
|
||||||
|
readConfig();
|
||||||
|
}
|
||||||
|
return g_config.injectionDelay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,15 +19,25 @@ namespace Config {
|
|||||||
CUSTOM_LINKER = 2
|
CUSTOM_LINKER = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct GadgetConfig {
|
||||||
|
std::string address = "0.0.0.0";
|
||||||
|
int port = 27042;
|
||||||
|
std::string onPortConflict = "fail";
|
||||||
|
std::string onLoad = "wait";
|
||||||
|
std::string gadgetName = "libgadget.so";
|
||||||
|
};
|
||||||
|
|
||||||
struct AppConfig {
|
struct AppConfig {
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
InjectionMethod injectionMethod = InjectionMethod::STANDARD;
|
InjectionMethod injectionMethod = InjectionMethod::STANDARD;
|
||||||
std::vector<SoFile> soFiles;
|
std::vector<SoFile> soFiles;
|
||||||
|
GadgetConfig* gadgetConfig = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModuleConfig {
|
struct ModuleConfig {
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
bool hideInjection = false;
|
bool hideInjection = false;
|
||||||
|
int injectionDelay = 2; // Default 2 seconds
|
||||||
std::unordered_map<std::string, AppConfig> perAppConfig;
|
std::unordered_map<std::string, AppConfig> perAppConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +55,9 @@ namespace Config {
|
|||||||
|
|
||||||
// Get injection method for specific app
|
// Get injection method for specific app
|
||||||
InjectionMethod getAppInjectionMethod(const std::string& packageName);
|
InjectionMethod getAppInjectionMethod(const std::string& packageName);
|
||||||
|
|
||||||
|
// Get injection delay in seconds
|
||||||
|
int getInjectionDelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // CONFIG_H
|
#endif // CONFIG_H
|
||||||
@@ -88,8 +88,12 @@ 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);
|
||||||
|
|
||||||
// Wait a bit for app to initialize and files to be copied
|
// Get injection delay from config
|
||||||
sleep(2);
|
int delay = Config::getInjectionDelay();
|
||||||
|
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);
|
||||||
@@ -103,6 +107,12 @@ void hack_thread_func(const char *game_data_dir, const char *package_name, JavaV
|
|||||||
|
|
||||||
// Load each SO file using the configured method
|
// Load each SO file using the configured method
|
||||||
for (const auto &soFile : soFiles) {
|
for (const auto &soFile : soFiles) {
|
||||||
|
// Skip config files
|
||||||
|
if (soFile.name.find(".config.so") != std::string::npos) {
|
||||||
|
LOGI("Skipping config file: %s", soFile.name.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
|
LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
|
||||||
|
|
||||||
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
|
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
|
||||||
|
|||||||
Reference in New Issue
Block a user