feat:add so manager and config manager

This commit is contained in:
jiqiu2021
2025-06-25 18:08:46 +08:00
parent 499a26feec
commit 7d8b86f374
18 changed files with 1029 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
maven { url 'https://jitpack.io' }
} }
} }

View File

@@ -23,8 +23,8 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_1_8
} }
} }
@@ -43,6 +43,12 @@ dependencies {
// RecyclerView for app list // RecyclerView for app list
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
// Root access library
implementation 'com.github.topjohnwu.libsu:core:6.0.0'
// JSON parsing
implementation 'com.google.code.gson:gson:2.10.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

View File

@@ -36,6 +36,7 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
private List<AppInfo> allApps; private List<AppInfo> allApps;
private boolean hideSystemApps = false; private boolean hideSystemApps = false;
private ConfigManager configManager;
@Nullable @Nullable
@Override @Override
@@ -48,6 +49,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
configManager = new ConfigManager(requireContext());
initViews(view); initViews(view);
setupRecyclerView(); setupRecyclerView();
setupSearchView(); setupSearchView();
@@ -102,8 +105,8 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
@Override @Override
public void onAppToggle(AppInfo appInfo, boolean isEnabled) { public void onAppToggle(AppInfo appInfo, boolean isEnabled) {
// 这里可以保存应用的启用状态到配置文件或数据库 // 保存应用的启用状态到配置文件
// 暂时只是打印日志 configManager.setAppEnabled(appInfo.getPackageName(), isEnabled);
android.util.Log.d("AppListFragment", android.util.Log.d("AppListFragment",
"App " + appInfo.getAppName() + " toggle: " + isEnabled); "App " + appInfo.getAppName() + " toggle: " + isEnabled);
} }
@@ -133,6 +136,9 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
isSystemApp isSystemApp
); );
// 从配置中加载启用状态
app.setEnabled(configManager.isAppEnabled(packageName));
apps.add(app); apps.add(app);
} catch (Exception e) { } catch (Exception e) {
// 忽略无法获取信息的应用 // 忽略无法获取信息的应用

View File

@@ -0,0 +1,179 @@
package com.jiqiu.configapp;
import android.content.Context;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConfigManager {
private static final String TAG = "ConfigManager";
public static final String MODULE_PATH = "/data/adb/modules/zygisk-myinjector";
public static final String CONFIG_FILE = MODULE_PATH + "/config.json";
public static final String SO_STORAGE_DIR = MODULE_PATH + "/so_files";
private final Context context;
private final Gson gson;
private ModuleConfig config;
static {
// Configure Shell to use root
Shell.enableVerboseLogging = BuildConfig.DEBUG;
Shell.setDefaultBuilder(Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10));
}
public ConfigManager(Context context) {
this.context = context;
this.gson = new GsonBuilder().setPrettyPrinting().create();
loadConfig();
}
public boolean isRootAvailable() {
return Shell.getShell().isRoot();
}
public void ensureModuleDirectories() {
Shell.cmd("mkdir -p " + MODULE_PATH).exec();
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
}
private void loadConfig() {
Shell.Result result = Shell.cmd("cat " + CONFIG_FILE).exec();
if (result.isSuccess() && !result.getOut().isEmpty()) {
String json = String.join("\n", result.getOut());
try {
config = gson.fromJson(json, ModuleConfig.class);
} catch (Exception e) {
Log.e(TAG, "Failed to parse config", e);
config = new ModuleConfig();
}
} else {
config = new ModuleConfig();
}
}
public void saveConfig() {
String json = gson.toJson(config);
// Write to temp file first
String tempFile = context.getCacheDir() + "/config.json";
try {
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
writer.write(json);
writer.close();
// Copy to module directory with root
Shell.cmd("cp " + tempFile + " " + CONFIG_FILE).exec();
Shell.cmd("chmod 644 " + CONFIG_FILE).exec();
// Clean up temp file
new File(tempFile).delete();
} catch (Exception e) {
Log.e(TAG, "Failed to save config", e);
}
}
public boolean isAppEnabled(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
return appConfig != null && appConfig.enabled;
}
public void setAppEnabled(String packageName, boolean enabled) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
appConfig.enabled = enabled;
saveConfig();
}
public List<SoFile> getAppSoFiles(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
return new ArrayList<>();
}
return new ArrayList<>(appConfig.soFiles);
}
public void addSoFileToApp(String packageName, String originalPath, boolean deleteOriginal) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
// Generate unique filename
String fileName = new File(originalPath).getName();
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
// Copy SO file to our storage
Shell.Result result = Shell.cmd("cp " + originalPath + " " + storedPath).exec();
if (result.isSuccess()) {
SoFile soFile = new SoFile();
soFile.name = fileName;
soFile.storedPath = storedPath;
soFile.originalPath = originalPath;
appConfig.soFiles.add(soFile);
if (deleteOriginal) {
Shell.cmd("rm " + originalPath).exec();
}
saveConfig();
}
}
public void removeSoFileFromApp(String packageName, SoFile soFile) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) return;
appConfig.soFiles.remove(soFile);
// Delete the stored file
Shell.cmd("rm " + soFile.storedPath).exec();
saveConfig();
}
public boolean getHideInjection() {
return config.hideInjection;
}
public void setHideInjection(boolean hide) {
config.hideInjection = hide;
saveConfig();
}
// Data classes
public static class ModuleConfig {
public boolean enabled = true;
public boolean hideInjection = false;
public Map<String, AppConfig> perAppConfig = new HashMap<>();
}
public static class AppConfig {
public boolean enabled = false;
public List<SoFile> soFiles = new ArrayList<>();
}
public static class SoFile {
public String name;
public String storedPath;
public String originalPath;
@Override
public boolean equals(Object obj) {
if (obj instanceof SoFile) {
return storedPath.equals(((SoFile) obj).storedPath);
}
return false;
}
}
}

View File

@@ -18,6 +18,7 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
private BottomNavigationView bottomNavigationView; private BottomNavigationView bottomNavigationView;
private AppListFragment appListFragment; private AppListFragment appListFragment;
private SettingsFragment settingsFragment; private SettingsFragment settingsFragment;
private SoManagerFragment soManagerFragment;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -49,6 +50,9 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
if (itemId == R.id.navigation_apps) { if (itemId == R.id.navigation_apps) {
showAppListFragment(); showAppListFragment();
return true; return true;
} else if (itemId == R.id.navigation_so_manager) {
showSoManagerFragment();
return true;
} else if (itemId == R.id.navigation_settings) { } else if (itemId == R.id.navigation_settings) {
showSettingsFragment(); showSettingsFragment();
return true; return true;
@@ -64,6 +68,13 @@ public class MainActivity extends AppCompatActivity implements SettingsFragment.
showFragment(appListFragment); showFragment(appListFragment);
} }
private void showSoManagerFragment() {
if (soManagerFragment == null) {
soManagerFragment = new SoManagerFragment();
}
showFragment(soManagerFragment);
}
private void showSettingsFragment() { private void showSettingsFragment() {
if (settingsFragment == null) { if (settingsFragment == null) {
settingsFragment = new SettingsFragment(); settingsFragment = new SettingsFragment();

View File

@@ -0,0 +1,75 @@
package com.jiqiu.configapp;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class SoListAdapter extends RecyclerView.Adapter<SoListAdapter.ViewHolder> {
private List<ConfigManager.SoFile> soFiles = new ArrayList<>();
private OnSoFileActionListener listener;
public interface OnSoFileActionListener {
void onDeleteClick(ConfigManager.SoFile soFile);
}
public void setSoFiles(List<ConfigManager.SoFile> files) {
this.soFiles = files;
notifyDataSetChanged();
}
public void setOnSoFileActionListener(OnSoFileActionListener listener) {
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_so_file, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ConfigManager.SoFile soFile = soFiles.get(position);
holder.bind(soFile);
}
@Override
public int getItemCount() {
return soFiles.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private TextView textFileName;
private TextView textFilePath;
private ImageButton buttonDelete;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textFileName = itemView.findViewById(R.id.textFileName);
textFilePath = itemView.findViewById(R.id.textFilePath);
buttonDelete = itemView.findViewById(R.id.buttonDelete);
}
public void bind(ConfigManager.SoFile soFile) {
textFileName.setText(soFile.name);
textFilePath.setText(soFile.originalPath);
buttonDelete.setOnClickListener(v -> {
if (listener != null) {
listener.onDeleteClick(soFile);
}
});
}
}
}

View File

@@ -0,0 +1,231 @@
package com.jiqiu.configapp;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class SoManagerFragment extends Fragment {
private RecyclerView recyclerView;
private LinearLayout emptyView;
private SoListAdapter adapter;
private ConfigManager configManager;
private List<ConfigManager.SoFile> globalSoFiles = new ArrayList<>();
private ActivityResultLauncher<Intent> filePickerLauncher;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configManager = new ConfigManager(requireContext());
// Initialize file picker
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
handleFileSelection(uri);
}
}
}
);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_so_manager, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recyclerView);
emptyView = view.findViewById(R.id.emptyView);
FloatingActionButton fabAdd = view.findViewById(R.id.fabAdd);
// Setup RecyclerView
adapter = new SoListAdapter();
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
adapter.setOnSoFileActionListener(this::showDeleteConfirmation);
// Setup FAB
fabAdd.setOnClickListener(v -> showAddSoDialog());
// Check root access
if (!configManager.isRootAvailable()) {
Toast.makeText(getContext(), "需要Root权限", Toast.LENGTH_LONG).show();
} else {
configManager.ensureModuleDirectories();
loadSoFiles();
}
}
private void loadSoFiles() {
// Load global SO files from the storage directory
Shell.Result result = Shell.cmd("ls -la " + ConfigManager.SO_STORAGE_DIR).exec();
globalSoFiles.clear();
if (result.isSuccess()) {
for (String line : result.getOut()) {
if (line.contains(".so")) {
// Parse file info
String[] parts = line.split("\\s+");
if (parts.length >= 9) {
String fileName = parts[parts.length - 1];
ConfigManager.SoFile soFile = new ConfigManager.SoFile();
soFile.name = fileName;
soFile.storedPath = ConfigManager.SO_STORAGE_DIR + "/" + fileName;
soFile.originalPath = soFile.storedPath; // For display
globalSoFiles.add(soFile);
}
}
}
}
updateUI();
}
private void updateUI() {
if (globalSoFiles.isEmpty()) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
adapter.setSoFiles(globalSoFiles);
}
}
private void showAddSoDialog() {
String[] options = {"从文件管理器选择", "输入路径"};
new MaterialAlertDialogBuilder(requireContext())
.setTitle("添加SO文件")
.setItems(options, (dialog, which) -> {
if (which == 0) {
openFilePicker();
} else {
showPathInputDialog();
}
})
.show();
}
private void openFilePicker() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
filePickerLauncher.launch(intent);
}
private void showPathInputDialog() {
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
android.widget.EditText editText = view.findViewById(android.R.id.edit);
editText.setHint("/data/local/tmp/example.so");
new MaterialAlertDialogBuilder(requireContext())
.setTitle("输入SO文件路径")
.setView(view)
.setPositiveButton("添加", (dialog, which) -> {
String path = editText.getText().toString().trim();
if (!path.isEmpty()) {
addSoFile(path, false);
}
})
.setNegativeButton("取消", null)
.show();
}
private void handleFileSelection(Uri uri) {
// Get real path from URI
String path = uri.getPath();
if (path != null) {
// Remove the file:// prefix if present
if (path.startsWith("file://")) {
path = path.substring(7);
}
addSoFile(path, false);
}
}
private void addSoFile(String path, boolean deleteOriginal) {
// Verify file exists
Shell.Result result = Shell.cmd("test -f " + path + " && echo 'exists'").exec();
if (!result.isSuccess() || result.getOut().isEmpty()) {
Toast.makeText(getContext(), "文件不存在: " + path, Toast.LENGTH_SHORT).show();
return;
}
// Generate unique filename
String fileName = new File(path).getName();
String storedPath = ConfigManager.SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
// Copy file
result = Shell.cmd("cp " + path + " " + storedPath).exec();
if (result.isSuccess()) {
ConfigManager.SoFile soFile = new ConfigManager.SoFile();
soFile.name = fileName;
soFile.storedPath = storedPath;
soFile.originalPath = path;
globalSoFiles.add(soFile);
if (deleteOriginal) {
Shell.cmd("rm " + path).exec();
}
updateUI();
Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "复制文件失败", Toast.LENGTH_SHORT).show();
}
}
private void showDeleteConfirmation(ConfigManager.SoFile soFile) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle("删除SO文件")
.setMessage("确定要删除 " + soFile.name + " 吗?")
.setPositiveButton("删除", (dialog, which) -> {
deleteSoFile(soFile);
})
.setNegativeButton("取消", null)
.show();
}
private void deleteSoFile(ConfigManager.SoFile soFile) {
Shell.cmd("rm " + soFile.storedPath).exec();
globalSoFiles.remove(soFile);
updateUI();
Toast.makeText(getContext(), "SO文件已删除", Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<EditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</LinearLayout>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="SO文件管理" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
android:visibility="gone">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:alpha="0.3"
android:src="@drawable/ic_launcher_foreground" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无SO文件"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="点击右下角按钮添加SO文件"
android:textSize="14sp"
android:textColor="?android:attr/textColorTertiary" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</LinearLayout>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:src="@android:drawable/ic_menu_save"
android:tint="?attr/colorPrimary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/textFileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:text="example.so" />
<TextView
android:id="@+id/textFilePath"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="/data/local/tmp/example.so"
android:ellipsize="middle"
android:singleLine="true" />
</LinearLayout>
<ImageButton
android:id="@+id/buttonDelete"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_delete"
android:tint="?attr/colorError" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutApps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用此SO的应用: "
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/textAppCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="12sp"
android:textColor="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -5,6 +5,11 @@
android:icon="@android:drawable/ic_menu_view" android:icon="@android:drawable/ic_menu_view"
android:title="@string/title_apps" /> android:title="@string/title_apps" />
<item
android:id="@+id/navigation_so_manager"
android:icon="@android:drawable/ic_menu_save"
android:title="@string/title_so_manager" />
<item <item
android:id="@+id/navigation_settings" android:id="@+id/navigation_settings"
android:icon="@android:drawable/ic_menu_preferences" android:icon="@android:drawable/ic_menu_preferences"

View File

@@ -3,6 +3,7 @@
<!-- 底部导航 --> <!-- 底部导航 -->
<string name="title_apps">应用列表</string> <string name="title_apps">应用列表</string>
<string name="title_so_manager">SO库管理</string>
<string name="title_settings">全局设置</string> <string name="title_settings">全局设置</string>
<!-- 应用列表 --> <!-- 应用列表 -->

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -35,7 +35,8 @@ aux_source_directory(xdl xdl-src)
add_library(${MODULE_NAME} SHARED add_library(${MODULE_NAME} SHARED
main.cpp main.cpp
hack.cpp hack_new.cpp
config.cpp
newriruhide.cpp newriruhide.cpp
pmparser.cpp pmparser.cpp
${xdl-src}) ${xdl-src})

View File

@@ -0,0 +1,189 @@
#include "config.h"
#include <fstream>
#include <sstream>
#include <android/log.h>
#define LOG_TAG "MyInjector"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
namespace Config {
static ModuleConfig g_config;
static bool g_configLoaded = false;
// Simple JSON parser for our specific format
std::string extractValue(const std::string& json, const std::string& key) {
size_t keyPos = json.find("\"" + key + "\"");
if (keyPos == std::string::npos) return "";
size_t colonPos = json.find(":", keyPos);
if (colonPos == std::string::npos) return "";
size_t valueStart = json.find_first_not_of(" \t\n", colonPos + 1);
if (valueStart == std::string::npos) return "";
if (json[valueStart] == '"') {
// String value
size_t valueEnd = json.find('"', valueStart + 1);
if (valueEnd == std::string::npos) return "";
return json.substr(valueStart + 1, valueEnd - valueStart - 1);
} else if (json[valueStart] == 't' || json[valueStart] == 'f') {
// Boolean value
return (json.substr(valueStart, 4) == "true") ? "true" : "false";
}
return "";
}
void parseAppConfig(const std::string& packageName, const std::string& appJson) {
AppConfig appConfig;
// Parse enabled
std::string enabledStr = extractValue(appJson, "enabled");
appConfig.enabled = (enabledStr == "true");
// Parse soFiles array
size_t soFilesPos = appJson.find("\"soFiles\"");
if (soFilesPos != std::string::npos) {
size_t arrayStart = appJson.find("[", soFilesPos);
size_t arrayEnd = appJson.find("]", arrayStart);
if (arrayStart != std::string::npos && arrayEnd != std::string::npos) {
std::string soFilesArray = appJson.substr(arrayStart + 1, arrayEnd - arrayStart - 1);
// Parse each SO file object
size_t objStart = 0;
while ((objStart = soFilesArray.find("{", objStart)) != std::string::npos) {
size_t objEnd = soFilesArray.find("}", objStart);
if (objEnd == std::string::npos) break;
std::string soFileObj = soFilesArray.substr(objStart, objEnd - objStart + 1);
SoFile soFile;
soFile.name = extractValue(soFileObj, "name");
soFile.storedPath = extractValue(soFileObj, "storedPath");
soFile.originalPath = extractValue(soFileObj, "originalPath");
if (!soFile.storedPath.empty()) {
appConfig.soFiles.push_back(soFile);
LOGD("Added SO file: %s at %s", soFile.name.c_str(), soFile.storedPath.c_str());
}
objStart = objEnd + 1;
}
}
}
g_config.perAppConfig[packageName] = appConfig;
LOGD("Loaded config for app: %s, enabled: %d, SO files: %zu",
packageName.c_str(), appConfig.enabled, appConfig.soFiles.size());
}
ModuleConfig readConfig() {
if (g_configLoaded) {
return g_config;
}
const char* configPath = "/data/adb/modules/zygisk-myinjector/config.json";
std::ifstream file(configPath);
if (!file.is_open()) {
LOGE("Failed to open config file: %s", configPath);
g_configLoaded = true;
return g_config;
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string json = buffer.str();
file.close();
// Parse global settings
std::string enabledStr = extractValue(json, "enabled");
g_config.enabled = (enabledStr != "false");
std::string hideStr = extractValue(json, "hideInjection");
g_config.hideInjection = (hideStr == "true");
LOGD("Module enabled: %d, hide injection: %d", g_config.enabled, g_config.hideInjection);
// Parse perAppConfig
size_t perAppPos = json.find("\"perAppConfig\"");
if (perAppPos != std::string::npos) {
size_t objStart = json.find("{", perAppPos + 14);
size_t objEnd = json.rfind("}");
if (objStart != std::string::npos && objEnd != std::string::npos) {
std::string perAppObj = json.substr(objStart + 1, objEnd - objStart - 1);
// Find each package config
size_t pos = 0;
while (pos < perAppObj.length()) {
// Find package name
size_t pkgStart = perAppObj.find("\"", pos);
if (pkgStart == std::string::npos) break;
size_t pkgEnd = perAppObj.find("\"", pkgStart + 1);
if (pkgEnd == std::string::npos) break;
std::string packageName = perAppObj.substr(pkgStart + 1, pkgEnd - pkgStart - 1);
// Find app config object
size_t appObjStart = perAppObj.find("{", pkgEnd);
if (appObjStart == std::string::npos) break;
// Find matching closing brace
int braceCount = 1;
size_t appObjEnd = appObjStart + 1;
while (appObjEnd < perAppObj.length() && braceCount > 0) {
if (perAppObj[appObjEnd] == '{') braceCount++;
else if (perAppObj[appObjEnd] == '}') braceCount--;
appObjEnd++;
}
if (braceCount == 0) {
std::string appConfigStr = perAppObj.substr(appObjStart, appObjEnd - appObjStart);
parseAppConfig(packageName, appConfigStr);
}
pos = appObjEnd;
}
}
}
g_configLoaded = true;
return g_config;
}
bool isAppEnabled(const std::string& packageName) {
if (!g_configLoaded) {
readConfig();
}
auto it = g_config.perAppConfig.find(packageName);
if (it != g_config.perAppConfig.end()) {
return it->second.enabled;
}
return false;
}
std::vector<SoFile> getAppSoFiles(const std::string& packageName) {
if (!g_configLoaded) {
readConfig();
}
auto it = g_config.perAppConfig.find(packageName);
if (it != g_config.perAppConfig.end()) {
return it->second.soFiles;
}
return {};
}
bool shouldHideInjection() {
if (!g_configLoaded) {
readConfig();
}
return g_config.hideInjection;
}
}

View File

@@ -0,0 +1,40 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <string>
#include <vector>
#include <unordered_map>
namespace Config {
struct SoFile {
std::string name;
std::string storedPath;
std::string originalPath;
};
struct AppConfig {
bool enabled = false;
std::vector<SoFile> soFiles;
};
struct ModuleConfig {
bool enabled = true;
bool hideInjection = false;
std::unordered_map<std::string, AppConfig> perAppConfig;
};
// Read configuration from file
ModuleConfig readConfig();
// Check if app is enabled for injection
bool isAppEnabled(const std::string& packageName);
// Get SO files for specific app
std::vector<SoFile> getAppSoFiles(const std::string& packageName);
// Get hide injection setting
bool shouldHideInjection();
}
#endif // CONFIG_H

View File

@@ -0,0 +1,97 @@
#include "hack.h"
#include "config.h"
#include "log.h"
#include <cstring>
#include <thread>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
void load_so_file(const char *game_data_dir, const Config::SoFile &soFile) {
char dest_path[512];
snprintf(dest_path, sizeof(dest_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
// Copy SO file from storage to app directory
int src_fd = open(soFile.storedPath.c_str(), O_RDONLY);
if (src_fd < 0) {
LOGE("Failed to open source SO: %s", soFile.storedPath.c_str());
return;
}
int dest_fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
if (dest_fd < 0) {
LOGE("Failed to create dest SO: %s", dest_path);
close(src_fd);
return;
}
char buffer[4096];
ssize_t bytes;
while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
if (write(dest_fd, buffer, bytes) != bytes) {
LOGE("Failed to write SO file");
close(src_fd);
close(dest_fd);
return;
}
}
close(src_fd);
close(dest_fd);
chmod(dest_path, 0755);
// Load the SO file
void *handle = dlopen(dest_path, RTLD_NOW | RTLD_LOCAL);
if (handle) {
LOGI("Successfully loaded SO: %s", soFile.name.c_str());
// Hide if configured
if (Config::shouldHideInjection()) {
// Call hide function if available
void (*hide_func)(const char*) = (void(*)(const char*))dlsym(handle, "riru_hide");
if (hide_func) {
hide_func(soFile.name.c_str());
}
}
} else {
LOGE("Failed to load SO: %s - %s", dest_path, dlerror());
}
}
void hack_thread_func(const char *game_data_dir, const char *package_name) {
LOGI("Hack thread started for package: %s", package_name);
// Wait a bit for app to initialize
sleep(1);
// Get SO files for this app
auto soFiles = Config::getAppSoFiles(package_name);
LOGI("Found %zu SO files to load", soFiles.size());
// Load each SO file
for (const auto &soFile : soFiles) {
LOGI("Loading SO: %s", soFile.name.c_str());
load_so_file(game_data_dir, soFile);
}
}
void hack_prepare(const char *game_data_dir, void *data, size_t length) {
// Get package name from game_data_dir
// Format: /data/user/0/com.example.app or /data/data/com.example.app
const char *package_name = nullptr;
if (strstr(game_data_dir, "/data/user/")) {
package_name = strrchr(game_data_dir, '/');
if (package_name) package_name++;
} else if (strstr(game_data_dir, "/data/data/")) {
package_name = game_data_dir + strlen("/data/data/");
}
if (!package_name) {
LOGE("Failed to extract package name from: %s", game_data_dir);
return;
}
std::thread hack_thread(hack_thread_func, game_data_dir, package_name);
hack_thread.detach();
}

View File

@@ -11,6 +11,7 @@
#include "game.h" #include "game.h"
#include "log.h" #include "log.h"
#include "dlfcn.h" #include "dlfcn.h"
#include "config.h"
using zygisk::Api; using zygisk::Api;
using zygisk::AppSpecializeArgs; using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs; using zygisk::ServerSpecializeArgs;
@@ -51,7 +52,11 @@ private:
size_t length; size_t length;
void preSpecialize(const char *package_name, const char *app_data_dir) { void preSpecialize(const char *package_name, const char *app_data_dir) {
if (strcmp(package_name, AimPackageName) == 0) { // Read configuration
Config::readConfig();
// Check if this app is enabled for injection
if (Config::isAppEnabled(package_name)) {
LOGI("成功注入目标进程: %s", package_name); LOGI("成功注入目标进程: %s", package_name);
enable_hack = true; enable_hack = true;
_data_dir = new char[strlen(app_data_dir) + 1]; _data_dir = new char[strlen(app_data_dir) + 1];