diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
new file mode 100644
index 0000000..4a8df81
--- /dev/null
+++ b/.github/workflows/build-release.yml
@@ -0,0 +1,153 @@
+name: Build and Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version tag (e.g., v1.0.0)'
+ required: true
+ default: 'v1.0.0'
+ branch:
+ description: 'Branch to build from'
+ required: false
+ default: 'main'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ submodules: 'recursive'
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Cache Gradle dependencies
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install NDK and CMake
+ run: |
+ echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;25.2.9519653"
+ echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "cmake;3.22.1"
+
+ - name: Build ConfigApp
+ run: |
+ cd configapp
+ ../gradlew assembleDebug
+ cd ..
+
+ - name: Build Module
+ run: |
+ cd module
+ ../gradlew assembleRelease
+ cd ..
+
+ - name: Package Module
+ run: |
+ # 获取版本信息
+ if [ "${{ github.event_name }}" == "push" ]; then
+ VERSION="${GITHUB_REF#refs/tags/}"
+ else
+ VERSION="${{ github.event.inputs.version }}"
+ fi
+ VERSION_CODE=$(echo $VERSION | sed 's/[^0-9]//g')
+
+ # 创建临时目录
+ TEMP_DIR="build/magisk_module"
+ rm -rf $TEMP_DIR
+ mkdir -p $TEMP_DIR
+
+ # 创建 module.prop
+ cat > $TEMP_DIR/module.prop << EOF
+ id=zygisk-myinjector
+ name=Zygisk MyInjector
+ version=$VERSION
+ versionCode=$VERSION_CODE
+ author=jiqiu2022
+ description=A Zygisk module for dynamic library injection with ConfigApp
+ EOF
+
+ # 复制文件
+ cp module/service.sh $TEMP_DIR/
+ chmod 755 $TEMP_DIR/service.sh
+
+ # 创建 zygisk 目录并复制 so 文件
+ mkdir -p $TEMP_DIR/zygisk
+ for arch in armeabi-v7a arm64-v8a x86 x86_64; do
+ SO_PATH="module/build/intermediates/stripped_native_libs/release/out/lib/$arch/libmyinjector.so"
+ if [ -f "$SO_PATH" ]; then
+ cp "$SO_PATH" "$TEMP_DIR/zygisk/$arch.so"
+ fi
+ done
+
+ # 复制 ConfigApp APK
+ cp configapp/build/outputs/apk/debug/configapp-debug.apk $TEMP_DIR/configapp.apk
+
+ # 创建 META-INF 目录
+ mkdir -p $TEMP_DIR/META-INF/com/google/android
+ touch $TEMP_DIR/META-INF/com/google/android/update-binary
+ touch $TEMP_DIR/META-INF/com/google/android/updater-script
+
+ # 打包
+ cd $TEMP_DIR
+ zip -r ../../zygisk-myinjector-$VERSION.zip *
+ cd ../..
+
+ # 列出文件内容
+ echo "Module contents:"
+ unzip -l zygisk-myinjector-$VERSION.zip
+
+ # 重命名 APK 以包含版本号
+ cp configapp/build/outputs/apk/debug/configapp-debug.apk ./ConfigApp-$VERSION.apk
+
+ - name: Create Release
+ id: create_release
+ uses: softprops/action-gh-release@v2
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ tag_name: ${{ github.event_name == 'push' && github.ref || github.event.inputs.version }}
+ name: Release ${{ github.event_name == 'push' && github.ref || github.event.inputs.version }}
+ body: |
+ ## Zygisk MyInjector Release
+
+ ### 功能特性
+ - 动态 SO 注入
+ - 图形化配置界面
+ - 支持 Riru Hide
+ - 自动安装配置应用
+
+ ### 安装说明
+ 1. 下载 `zygisk-myinjector-*.zip`
+ 2. 在 Magisk Manager 中安装模块
+ 3. 重启设备
+ 4. ConfigApp 会自动安装
+
+ ### 更新日志
+ 请查看 [commits](https://github.com/${{ github.repository }}/commits/${{ github.sha }})
+ draft: false
+ prerelease: false
+ files: |
+ ./zygisk-myinjector-*.zip
+ ./ConfigApp-*.apk
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..13608e5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,118 @@
+name: CI Build
+
+on:
+ push:
+ branches: [ '**' ] # 所有分支的推送都会触发
+ pull_request:
+ branches: [ main, develop, master ]
+ workflow_dispatch: # 允许手动触发
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ submodules: 'recursive'
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Cache Gradle dependencies
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install NDK and CMake
+ run: |
+ echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;25.2.9519653"
+ echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "cmake;3.22.1"
+
+ - name: Build ConfigApp
+ run: |
+ cd configapp
+ ../gradlew assembleDebug
+ cd ..
+
+ - name: Build Module
+ run: |
+ cd module
+ ../gradlew assembleRelease
+ cd ..
+
+ - name: Package Module for Testing
+ run: |
+ # 创建临时目录
+ TEMP_DIR="build/magisk_module"
+ rm -rf $TEMP_DIR
+ mkdir -p $TEMP_DIR
+
+ # 创建 module.prop
+ cat > $TEMP_DIR/module.prop << EOF
+ id=zygisk-myinjector
+ name=Zygisk MyInjector
+ version=dev-${{ github.sha }}
+ versionCode=9999
+ author=jiqiu2022
+ description=A Zygisk module for dynamic library injection with ConfigApp (CI Build)
+ EOF
+
+ # 复制文件
+ cp module/service.sh $TEMP_DIR/
+ chmod 755 $TEMP_DIR/service.sh
+
+ # 创建 zygisk 目录并复制 so 文件
+ mkdir -p $TEMP_DIR/zygisk
+ for arch in armeabi-v7a arm64-v8a x86 x86_64; do
+ SO_PATH="module/build/intermediates/stripped_native_libs/release/out/lib/$arch/libmyinjector.so"
+ if [ -f "$SO_PATH" ]; then
+ cp "$SO_PATH" "$TEMP_DIR/zygisk/$arch.so"
+ fi
+ done
+
+ # 复制 ConfigApp APK
+ cp configapp/build/outputs/apk/debug/configapp-debug.apk $TEMP_DIR/configapp.apk
+
+ # 创建 META-INF 目录
+ mkdir -p $TEMP_DIR/META-INF/com/google/android
+ touch $TEMP_DIR/META-INF/com/google/android/update-binary
+ touch $TEMP_DIR/META-INF/com/google/android/updater-script
+
+ # 打包
+ cd $TEMP_DIR
+ zip -r ../../zygisk-myinjector-ci.zip *
+ cd ../..
+
+ # 列出文件内容
+ echo "Module contents:"
+ unzip -l zygisk-myinjector-ci.zip
+
+ - name: Upload Module Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: module-ci-${{ github.sha }}
+ path: zygisk-myinjector-ci.zip
+ retention-days: 7
+
+ - name: Upload ConfigApp Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: configapp-ci-${{ github.sha }}
+ path: configapp/build/outputs/apk/debug/configapp-debug.apk
+ retention-days: 7
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 5e02054..b138350 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,6 +20,7 @@ allprojects {
repositories {
mavenCentral()
google()
+ maven { url 'https://jitpack.io' }
}
}
diff --git a/build_all.sh b/build_all.sh
new file mode 100755
index 0000000..0f55f64
--- /dev/null
+++ b/build_all.sh
@@ -0,0 +1,152 @@
+#!/bin/bash
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# 模块信息
+MODULE_ID="zygisk-myinjector"
+MODULE_VERSION="1.0"
+MODULE_VERSION_CODE="100"
+
+echo -e "${GREEN}========================================${NC}"
+echo -e "${GREEN} Zygisk MyInjector 构建脚本${NC}"
+echo -e "${GREEN}========================================${NC}"
+
+# 清理之前的构建
+echo -e "\n${YELLOW}[1/5] 清理旧构建文件...${NC}"
+rm -rf build/magisk_module
+rm -f build/*.zip
+mkdir -p build
+
+# 构建 ConfigApp
+echo -e "\n${YELLOW}[2/5] 构建 ConfigApp...${NC}"
+cd configapp
+if ../gradlew assembleDebug; then
+ echo -e "${GREEN}✓ ConfigApp 构建成功${NC}"
+ cd ..
+else
+ echo -e "${RED}✗ ConfigApp 构建失败${NC}"
+ cd ..
+ exit 1
+fi
+
+# 构建 Magisk 模块
+echo -e "\n${YELLOW}[3/5] 构建 Magisk 模块原生库...${NC}"
+cd module
+if ../gradlew assembleRelease; then
+ echo -e "${GREEN}✓ 模块原生库构建成功${NC}"
+ cd ..
+else
+ echo -e "${RED}✗ 模块原生库构建失败${NC}"
+ cd ..
+ exit 1
+fi
+
+# 准备打包
+echo -e "\n${YELLOW}[4/5] 准备打包文件...${NC}"
+
+# 创建临时目录
+TEMP_DIR="build/magisk_module"
+mkdir -p $TEMP_DIR
+
+# 创建 module.prop
+cat > $TEMP_DIR/module.prop << EOF
+id=$MODULE_ID
+name=Zygisk MyInjector
+version=v$MODULE_VERSION
+versionCode=$MODULE_VERSION_CODE
+author=jiqiu
+description=A Zygisk module for dynamic library injection with ConfigApp
+EOF
+echo -e " ${GREEN}✓ 创建 module.prop${NC}"
+
+# 复制 service.sh
+if [ -f "module/service.sh" ]; then
+ cp module/service.sh $TEMP_DIR/
+ chmod 755 $TEMP_DIR/service.sh
+ echo -e " ${GREEN}✓ 复制 service.sh${NC}"
+else
+ echo -e " ${RED}✗ 未找到 service.sh${NC}"
+fi
+
+# 创建 zygisk 目录并复制 so 文件
+mkdir -p $TEMP_DIR/zygisk
+SO_COUNT=0
+
+# 查找并复制 so 文件
+for arch in armeabi-v7a arm64-v8a x86 x86_64; do
+ SO_PATH="module/build/intermediates/stripped_native_libs/release/out/lib/$arch/libmyinjector.so"
+ if [ -f "$SO_PATH" ]; then
+ cp "$SO_PATH" "$TEMP_DIR/zygisk/$arch.so"
+ echo -e " ${GREEN}✓ 复制 $arch.so${NC}"
+ ((SO_COUNT++))
+ fi
+done
+
+if [ $SO_COUNT -eq 0 ]; then
+ echo -e " ${RED}✗ 未找到任何 SO 文件${NC}"
+ exit 1
+fi
+
+# 复制 ConfigApp APK
+APK_PATH="configapp/build/outputs/apk/debug/configapp-debug.apk"
+if [ -f "$APK_PATH" ]; then
+ cp "$APK_PATH" "$TEMP_DIR/configapp.apk"
+ echo -e " ${GREEN}✓ 复制 ConfigApp APK${NC}"
+
+ # 显示 APK 信息
+ APK_SIZE=$(du -h "$APK_PATH" | cut -f1)
+ echo -e " APK 大小: $APK_SIZE"
+else
+ echo -e " ${RED}✗ 未找到 ConfigApp APK${NC}"
+ exit 1
+fi
+
+# 创建 META-INF 目录(Magisk 需要)
+mkdir -p $TEMP_DIR/META-INF/com/google/android
+touch $TEMP_DIR/META-INF/com/google/android/update-binary
+touch $TEMP_DIR/META-INF/com/google/android/updater-script
+
+# 打包
+echo -e "\n${YELLOW}[5/5] 打包模块...${NC}"
+ZIP_NAME="${MODULE_ID}-${MODULE_VERSION}.zip"
+cd $TEMP_DIR
+zip -r ../$ZIP_NAME * -x "*.DS_Store" > /dev/null 2>&1
+cd ../..
+
+# 显示结果
+echo -e "\n${GREEN}========================================${NC}"
+echo -e "${GREEN}✓ 构建完成!${NC}"
+echo -e "${GREEN}========================================${NC}"
+echo -e "\n模块文件: ${GREEN}build/$ZIP_NAME${NC}"
+
+# 显示模块内容
+echo -e "\n模块内容:"
+unzip -l build/$ZIP_NAME | grep -E "(\.so|\.apk|\.prop|\.sh)" | while read line; do
+ echo -e " $line"
+done
+
+# 显示模块大小
+MODULE_SIZE=$(du -h build/$ZIP_NAME | cut -f1)
+echo -e "\n模块大小: ${GREEN}$MODULE_SIZE${NC}"
+
+# 安装说明
+echo -e "\n${YELLOW}安装方法:${NC}"
+echo -e " 1. 将模块传输到手机:"
+echo -e " ${GREEN}adb push build/$ZIP_NAME /sdcard/${NC}"
+echo -e " 2. 在 Magisk Manager 中安装模块"
+echo -e " 3. 重启手机"
+echo -e "\n${YELLOW}验证安装:${NC}"
+echo -e " ${GREEN}adb shell pm list packages | grep com.jiqiu.configapp${NC}"
+echo -e " ${GREEN}adb shell cat /data/local/tmp/myinjector_install.log${NC}"
+
+# 可选:直接安装到设备
+if [ "$1" == "--install" ]; then
+ echo -e "\n${YELLOW}正在安装到设备...${NC}"
+ adb push build/$ZIP_NAME /data/local/tmp/
+ adb shell su -c "magisk --install-module /data/local/tmp/$ZIP_NAME"
+ echo -e "${GREEN}✓ 安装完成,请重启设备${NC}"
+fi
\ No newline at end of file
diff --git a/configapp/.gitignore b/configapp/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/configapp/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/configapp/build.gradle b/configapp/build.gradle
new file mode 100644
index 0000000..56e0e18
--- /dev/null
+++ b/configapp/build.gradle
@@ -0,0 +1,62 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.jiqiu.configapp'
+ compileSdk 34
+
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
+
+ defaultConfig {
+ applicationId "com.jiqiu.configapp"
+ minSdk 24
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ coreLibraryDesugaringEnabled false
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.10.0'
+ implementation 'androidx.activity:activity:1.8.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+
+ // Fragment and Navigation dependencies
+ implementation 'androidx.fragment:fragment:1.6.2'
+ implementation 'androidx.navigation:navigation-fragment:2.7.5'
+ implementation 'androidx.navigation:navigation-ui:2.7.5'
+
+ // RecyclerView for app list
+ 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'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+}
\ No newline at end of file
diff --git a/configapp/proguard-rules.pro b/configapp/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/configapp/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/configapp/src/androidTest/java/com/jiqiu/configapp/ExampleInstrumentedTest.java b/configapp/src/androidTest/java/com/jiqiu/configapp/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..93150e5
--- /dev/null
+++ b/configapp/src/androidTest/java/com/jiqiu/configapp/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.jiqiu.configapp;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.jiqiu.configapp", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/configapp/src/main/AndroidManifest.xml b/configapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0ddd386
--- /dev/null
+++ b/configapp/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/java/com/jiqiu/configapp/AppInfo.java b/configapp/src/main/java/com/jiqiu/configapp/AppInfo.java
new file mode 100644
index 0000000..80032ce
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/AppInfo.java
@@ -0,0 +1,73 @@
+package com.jiqiu.configapp;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * 应用程序信息数据模型
+ */
+public class AppInfo {
+ private String appName; // 应用名称
+ private String packageName; // 包名
+ private Drawable appIcon; // 应用图标
+ private boolean isSystemApp; // 是否为系统应用
+ private boolean isEnabled; // 是否启用注入
+
+ public AppInfo(String appName, String packageName, Drawable appIcon, boolean isSystemApp) {
+ this.appName = appName;
+ this.packageName = packageName;
+ this.appIcon = appIcon;
+ this.isSystemApp = isSystemApp;
+ this.isEnabled = false; // 默认不启用注入
+ }
+
+ // Getter 和 Setter 方法
+ public String getAppName() {
+ return appName;
+ }
+
+ public void setAppName(String appName) {
+ this.appName = appName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public Drawable getAppIcon() {
+ return appIcon;
+ }
+
+ public void setAppIcon(Drawable appIcon) {
+ this.appIcon = appIcon;
+ }
+
+ public boolean isSystemApp() {
+ return isSystemApp;
+ }
+
+ public void setSystemApp(boolean systemApp) {
+ isSystemApp = systemApp;
+ }
+
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "AppInfo{" +
+ "appName='" + appName + '\'' +
+ ", packageName='" + packageName + '\'' +
+ ", isSystemApp=" + isSystemApp +
+ ", isEnabled=" + isEnabled +
+ '}';
+ }
+}
diff --git a/configapp/src/main/java/com/jiqiu/configapp/AppListAdapter.java b/configapp/src/main/java/com/jiqiu/configapp/AppListAdapter.java
new file mode 100644
index 0000000..45a8f51
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/AppListAdapter.java
@@ -0,0 +1,141 @@
+package com.jiqiu.configapp;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.switchmaterial.SwitchMaterial;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 应用列表适配器
+ */
+public class AppListAdapter extends RecyclerView.Adapter {
+
+ private List appList;
+ private List filteredAppList;
+ private OnAppToggleListener onAppToggleListener;
+ private OnAppClickListener onAppClickListener;
+
+ public interface OnAppToggleListener {
+ void onAppToggle(AppInfo appInfo, boolean isEnabled);
+ }
+
+ public interface OnAppClickListener {
+ void onAppClick(AppInfo appInfo);
+ }
+
+ public AppListAdapter() {
+ this.appList = new ArrayList<>();
+ this.filteredAppList = new ArrayList<>();
+ }
+
+ public void setAppList(List appList) {
+ this.appList = appList;
+ this.filteredAppList = new ArrayList<>(appList);
+ notifyDataSetChanged();
+ }
+
+ public void setOnAppToggleListener(OnAppToggleListener listener) {
+ this.onAppToggleListener = listener;
+ }
+
+ public void setOnAppClickListener(OnAppClickListener listener) {
+ this.onAppClickListener = listener;
+ }
+
+ public void filterApps(String query, boolean hideSystemApps) {
+ filteredAppList.clear();
+
+ for (AppInfo app : appList) {
+ // 过滤系统应用
+ if (hideSystemApps && app.isSystemApp()) {
+ continue;
+ }
+
+ // 搜索过滤
+ if (query == null || query.isEmpty() ||
+ app.getAppName().toLowerCase().contains(query.toLowerCase()) ||
+ app.getPackageName().toLowerCase().contains(query.toLowerCase())) {
+ filteredAppList.add(app);
+ }
+ }
+
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public AppViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_app, parent, false);
+ return new AppViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AppViewHolder holder, int position) {
+ AppInfo appInfo = filteredAppList.get(position);
+ holder.bind(appInfo);
+ }
+
+ @Override
+ public int getItemCount() {
+ return filteredAppList.size();
+ }
+
+ class AppViewHolder extends RecyclerView.ViewHolder {
+ private ImageView appIcon;
+ private TextView appName;
+ private TextView packageName;
+ private TextView systemAppLabel;
+ private SwitchMaterial switchEnable;
+
+ public AppViewHolder(@NonNull View itemView) {
+ super(itemView);
+ appIcon = itemView.findViewById(R.id.app_icon);
+ appName = itemView.findViewById(R.id.app_name);
+ packageName = itemView.findViewById(R.id.package_name);
+ systemAppLabel = itemView.findViewById(R.id.system_app_label);
+ switchEnable = itemView.findViewById(R.id.switch_enable);
+ }
+
+ public void bind(AppInfo appInfo) {
+ appIcon.setImageDrawable(appInfo.getAppIcon());
+ appName.setText(appInfo.getAppName());
+ packageName.setText(appInfo.getPackageName());
+
+ // 显示系统应用标签
+ if (appInfo.isSystemApp()) {
+ systemAppLabel.setVisibility(View.VISIBLE);
+ } else {
+ systemAppLabel.setVisibility(View.GONE);
+ }
+
+ // 设置开关状态
+ switchEnable.setOnCheckedChangeListener(null); // 清除之前的监听器
+ switchEnable.setChecked(appInfo.isEnabled());
+
+ // 设置开关监听器
+ switchEnable.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ appInfo.setEnabled(isChecked);
+ if (onAppToggleListener != null) {
+ onAppToggleListener.onAppToggle(appInfo, isChecked);
+ }
+ });
+
+ // 设置整个item的点击监听器
+ itemView.setOnClickListener(v -> {
+ if (onAppClickListener != null) {
+ onAppClickListener.onAppClick(appInfo);
+ }
+ });
+ }
+ }
+}
diff --git a/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java b/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java
new file mode 100644
index 0000000..0916b71
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java
@@ -0,0 +1,324 @@
+package com.jiqiu.configapp;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.app.Dialog;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.switchmaterial.SwitchMaterial;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * 应用列表Fragment
+ */
+public class AppListFragment extends Fragment implements AppListAdapter.OnAppToggleListener, AppListAdapter.OnAppClickListener {
+
+ private RecyclerView recyclerView;
+ private AppListAdapter adapter;
+ private TextInputEditText searchEditText;
+ private ProgressBar progressBar;
+
+ private List allApps;
+ private boolean hideSystemApps = false;
+ private ConfigManager configManager;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_app_list, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ configManager = new ConfigManager(requireContext());
+ // Ensure module directories exist
+ configManager.ensureModuleDirectories();
+
+ initViews(view);
+ setupRecyclerView();
+ setupSearchView();
+ loadApps();
+ }
+
+ private void initViews(View view) {
+ recyclerView = view.findViewById(R.id.recycler_view_apps);
+ searchEditText = view.findViewById(R.id.search_edit_text);
+ progressBar = view.findViewById(R.id.progress_bar);
+ }
+
+ private void setupRecyclerView() {
+ adapter = new AppListAdapter();
+ adapter.setOnAppToggleListener(this);
+ adapter.setOnAppClickListener(this);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ recyclerView.setAdapter(adapter);
+ }
+
+ private void setupSearchView() {
+ searchEditText.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) {
+ filterApps(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+ }
+
+ private void loadApps() {
+ progressBar.setVisibility(View.VISIBLE);
+ recyclerView.setVisibility(View.GONE);
+
+ new LoadAppsTask().execute();
+ }
+
+ private void filterApps(String query) {
+ if (adapter != null) {
+ adapter.filterApps(query, hideSystemApps);
+ }
+ }
+
+ public void setHideSystemApps(boolean hideSystemApps) {
+ this.hideSystemApps = hideSystemApps;
+ filterApps(searchEditText.getText().toString());
+ }
+
+ @Override
+ public void onAppToggle(AppInfo appInfo, boolean isEnabled) {
+ // 保存应用的启用状态到配置文件
+ configManager.setAppEnabled(appInfo.getPackageName(), isEnabled);
+ android.util.Log.d("AppListFragment",
+ "App " + appInfo.getAppName() + " toggle: " + isEnabled);
+ }
+
+ @Override
+ public void onAppClick(AppInfo appInfo) {
+ showAppConfigDialog(appInfo);
+ }
+
+ private void showAppConfigDialog(AppInfo appInfo) {
+ View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_app_config, null);
+
+ // Set app info
+ ImageView appIcon = dialogView.findViewById(R.id.appIcon);
+ TextView appName = dialogView.findViewById(R.id.appName);
+ TextView packageName = dialogView.findViewById(R.id.packageName);
+ RecyclerView soListRecyclerView = dialogView.findViewById(R.id.soListRecyclerView);
+ TextView emptyText = dialogView.findViewById(R.id.emptyText);
+ SwitchMaterial switchHideInjection = dialogView.findViewById(R.id.switchHideInjection);
+
+ appIcon.setImageDrawable(appInfo.getAppIcon());
+ appName.setText(appInfo.getAppName());
+ packageName.setText(appInfo.getPackageName());
+
+ // Load current config
+ boolean hideInjection = configManager.getHideInjection();
+ switchHideInjection.setChecked(hideInjection);
+
+ // Setup SO list
+ List globalSoFiles = configManager.getAllSoFiles();
+ List appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());
+
+ if (globalSoFiles.isEmpty()) {
+ emptyText.setVisibility(View.VISIBLE);
+ soListRecyclerView.setVisibility(View.GONE);
+ } else {
+ emptyText.setVisibility(View.GONE);
+ soListRecyclerView.setVisibility(View.VISIBLE);
+
+ SoSelectionAdapter soAdapter = new SoSelectionAdapter(globalSoFiles, appSoFiles);
+ soListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ soListRecyclerView.setAdapter(soAdapter);
+ }
+
+ // Create dialog
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext())
+ .setTitle("配置注入")
+ .setView(dialogView)
+ .setPositiveButton("保存", (dialog, which) -> {
+ // Save hide injection setting
+ configManager.setHideInjection(switchHideInjection.isChecked());
+
+ // Save SO selection
+ if (soListRecyclerView.getAdapter() != null) {
+ SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter();
+ List selectedSoFiles = adapter.getSelectedSoFiles();
+
+ // Clear existing SO files for this app
+ for (ConfigManager.SoFile existingSo : appSoFiles) {
+ configManager.removeSoFileFromApp(appInfo.getPackageName(), existingSo);
+ }
+
+ // Add selected SO files
+ for (ConfigManager.SoFile soFile : selectedSoFiles) {
+ configManager.addSoFileToApp(appInfo.getPackageName(), soFile);
+ }
+ }
+ })
+ .setNegativeButton("取消", null);
+
+ builder.show();
+ }
+
+ // Inner class for SO selection adapter
+ private static class SoSelectionAdapter extends RecyclerView.Adapter {
+ private List globalSoFiles;
+ private List selectedSoFiles;
+
+ public SoSelectionAdapter(List globalSoFiles, List appSoFiles) {
+ this.globalSoFiles = globalSoFiles;
+ this.selectedSoFiles = new ArrayList<>(appSoFiles);
+ }
+
+ public List getSelectedSoFiles() {
+ return new ArrayList<>(selectedSoFiles);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_so_selection, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ ConfigManager.SoFile soFile = globalSoFiles.get(position);
+ holder.bind(soFile, selectedSoFiles);
+ }
+
+ @Override
+ public int getItemCount() {
+ return globalSoFiles.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ CheckBox checkBox;
+ TextView nameText;
+ TextView pathText;
+
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ checkBox = itemView.findViewById(R.id.checkBox);
+ nameText = itemView.findViewById(R.id.textName);
+ pathText = itemView.findViewById(R.id.textPath);
+ }
+
+ void bind(ConfigManager.SoFile soFile, List selectedList) {
+ nameText.setText(soFile.name);
+ pathText.setText(soFile.originalPath);
+
+ // Check if this SO is selected
+ boolean isSelected = false;
+ for (ConfigManager.SoFile selected : selectedList) {
+ if (selected.storedPath.equals(soFile.storedPath)) {
+ isSelected = true;
+ break;
+ }
+ }
+
+ checkBox.setOnCheckedChangeListener(null);
+ checkBox.setChecked(isSelected);
+
+ checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (isChecked) {
+ selectedList.add(soFile);
+ } else {
+ selectedList.removeIf(s -> s.storedPath.equals(soFile.storedPath));
+ }
+ });
+
+ itemView.setOnClickListener(v -> checkBox.toggle());
+ }
+ }
+ }
+
+ /**
+ * 异步加载应用列表
+ */
+ private class LoadAppsTask extends AsyncTask> {
+
+ @Override
+ protected List doInBackground(Void... voids) {
+ List apps = new ArrayList<>();
+ PackageManager pm = getContext().getPackageManager();
+
+ List installedApps = pm.getInstalledApplications(PackageManager.GET_META_DATA);
+
+ for (ApplicationInfo appInfo : installedApps) {
+ try {
+ String appName = pm.getApplicationLabel(appInfo).toString();
+ String packageName = appInfo.packageName;
+ boolean isSystemApp = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+
+ AppInfo app = new AppInfo(
+ appName,
+ packageName,
+ pm.getApplicationIcon(appInfo),
+ isSystemApp
+ );
+
+ // 从配置中加载启用状态
+ app.setEnabled(configManager.isAppEnabled(packageName));
+
+ apps.add(app);
+ } catch (Exception e) {
+ // 忽略无法获取信息的应用
+ e.printStackTrace();
+ }
+ }
+
+ // 按应用名称排序
+ Collections.sort(apps, new Comparator() {
+ @Override
+ public int compare(AppInfo o1, AppInfo o2) {
+ return o1.getAppName().compareToIgnoreCase(o2.getAppName());
+ }
+ });
+
+ return apps;
+ }
+
+ @Override
+ protected void onPostExecute(List apps) {
+ allApps = apps;
+ adapter.setAppList(apps);
+
+ progressBar.setVisibility(View.GONE);
+ recyclerView.setVisibility(View.VISIBLE);
+
+ // 应用当前的过滤设置
+ filterApps(searchEditText.getText().toString());
+ }
+ }
+}
diff --git a/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
new file mode 100644
index 0000000..e1c2e7e
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
@@ -0,0 +1,402 @@
+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 | Shell.FLAG_MOUNT_MASTER)
+ .setTimeout(30));
+ }
+
+ public ConfigManager(Context context) {
+ this.context = context;
+ this.gson = new GsonBuilder().setPrettyPrinting().create();
+
+ // Ensure we get root shell on creation
+ Shell.getShell();
+
+ loadConfig();
+ }
+
+ public boolean isRootAvailable() {
+ return Shell.getShell().isRoot();
+ }
+
+ public void ensureModuleDirectories() {
+ // Check root access first
+ if (!isRootAvailable()) {
+ Log.e(TAG, "Root access not available!");
+ return;
+ }
+
+ // Create module directories
+ Shell.Result result1 = Shell.cmd("mkdir -p " + MODULE_PATH).exec();
+ if (!result1.isSuccess()) {
+ Log.e(TAG, "Failed to create module directory: " + MODULE_PATH);
+ }
+
+ Shell.Result result2 = Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
+ if (!result2.isSuccess()) {
+ Log.e(TAG, "Failed to create SO storage directory: " + SO_STORAGE_DIR);
+ }
+
+ // Set permissions
+ Shell.cmd("chmod 755 " + MODULE_PATH).exec();
+ Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
+
+ // Verify directories exist
+ Shell.Result verify = Shell.cmd("ls -la " + MODULE_PATH).exec();
+ if (verify.isSuccess()) {
+ Log.i(TAG, "Module directory ready: " + String.join("\n", verify.getOut()));
+ }
+ }
+
+ 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();
+
+ // 自动部署或清理 SO 文件
+ if (enabled) {
+ deploySoFilesToApp(packageName);
+ } else {
+ cleanupAppSoFiles(packageName);
+ }
+ }
+
+ public List getAppSoFiles(String packageName) {
+ AppConfig appConfig = config.perAppConfig.get(packageName);
+ if (appConfig == null) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(appConfig.soFiles);
+ }
+
+ public List getAllSoFiles() {
+ if (config.globalSoFiles == null) {
+ config.globalSoFiles = new ArrayList<>();
+ }
+ return new ArrayList<>(config.globalSoFiles);
+ }
+
+ public void addGlobalSoFile(String originalPath, boolean deleteOriginal) {
+ if (config.globalSoFiles == null) {
+ config.globalSoFiles = new ArrayList<>();
+ }
+
+ // 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;
+ config.globalSoFiles.add(soFile);
+
+ if (deleteOriginal) {
+ Shell.cmd("rm \"" + originalPath + "\"").exec();
+ }
+
+ saveConfig();
+ }
+ }
+
+ public void removeGlobalSoFile(SoFile soFile) {
+ if (config.globalSoFiles == null) return;
+
+ config.globalSoFiles.remove(soFile);
+ // Delete the stored file
+ Shell.cmd("rm \"" + soFile.storedPath + "\"").exec();
+ saveConfig();
+ }
+
+ public void addSoFileToApp(String packageName, SoFile globalSoFile) {
+ AppConfig appConfig = config.perAppConfig.get(packageName);
+ if (appConfig == null) {
+ appConfig = new AppConfig();
+ config.perAppConfig.put(packageName, appConfig);
+ }
+
+ // Check if already added
+ for (SoFile existing : appConfig.soFiles) {
+ if (existing.storedPath.equals(globalSoFile.storedPath)) {
+ return; // Already added
+ }
+ }
+
+ // Add reference to the global SO file
+ appConfig.soFiles.add(globalSoFile);
+ saveConfig();
+
+ // If app is enabled, deploy the new SO file
+ if (appConfig.enabled) {
+ deploySoFilesToApp(packageName);
+ }
+ }
+
+ public void removeSoFileFromApp(String packageName, SoFile soFile) {
+ AppConfig appConfig = config.perAppConfig.get(packageName);
+ if (appConfig == null) return;
+
+ appConfig.soFiles.removeIf(s -> s.storedPath.equals(soFile.storedPath));
+ saveConfig();
+
+ // If app is enabled, re-deploy to update SO files
+ if (appConfig.enabled) {
+ deploySoFilesToApp(packageName);
+ }
+ }
+
+ public boolean getHideInjection() {
+ return config.hideInjection;
+ }
+
+ public void setHideInjection(boolean hide) {
+ config.hideInjection = hide;
+ saveConfig();
+ }
+
+ // Copy SO files directly to app's data directory
+ private void deploySoFilesToApp(String packageName) {
+ AppConfig appConfig = config.perAppConfig.get(packageName);
+ if (appConfig == null || appConfig.soFiles.isEmpty()) {
+ Log.w(TAG, "No SO files to deploy for: " + packageName);
+ return;
+ }
+
+ // First check if we have root access
+ if (!Shell.getShell().isRoot()) {
+ Log.e(TAG, "No root access available!");
+ return;
+ }
+
+ // Create files directory in app's data dir
+ String filesDir = "/data/data/" + packageName + "/files";
+
+ // Use su -c for better compatibility
+ Shell.Result mkdirResult = Shell.cmd("su -c 'mkdir -p " + filesDir + "'").exec();
+ if (!mkdirResult.isSuccess()) {
+ Log.e(TAG, "Failed to create directory: " + filesDir);
+ Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
+ // Try without su -c
+ mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
+ if (!mkdirResult.isSuccess()) {
+ Log.e(TAG, "Also failed without su -c");
+ return;
+ }
+ }
+
+ // Set proper permissions and ownership
+ Shell.cmd("chmod 755 " + filesDir).exec();
+
+ // Get UID for the package
+ Shell.Result uidResult = Shell.cmd("stat -c %u /data/data/" + packageName).exec();
+ String uid = "";
+ if (uidResult.isSuccess() && !uidResult.getOut().isEmpty()) {
+ uid = uidResult.getOut().get(0).trim();
+ Log.i(TAG, "Package UID: " + uid);
+ }
+
+ // Copy each SO file configured for this app
+ for (SoFile soFile : appConfig.soFiles) {
+ // Extract mapped filename
+ String mappedName = new File(soFile.storedPath).getName();
+ String destPath = filesDir + "/" + mappedName;
+
+ // Check if source file exists
+ Shell.Result checkResult = Shell.cmd("test -f \"" + soFile.storedPath + "\" && echo 'exists'").exec();
+ if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
+ Log.e(TAG, "Source SO file not found: " + soFile.storedPath);
+ continue;
+ }
+
+ Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
+
+ // Copy file using cat to avoid permission issues
+ String copyCmd = "cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"";
+ Shell.Result result = Shell.cmd(copyCmd).exec();
+
+ if (!result.isSuccess()) {
+ Log.e(TAG, "Failed with cat, trying cp");
+ // Fallback to cp
+ result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
+ }
+
+ // Set permissions
+ Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
+
+ // Set ownership if we have the UID
+ if (!uid.isEmpty()) {
+ Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
+ }
+
+ // Verify the file was copied
+ Shell.Result verifyResult = Shell.cmd("ls -la \"" + destPath + "\" 2>/dev/null").exec();
+ if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
+ Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
+ } else {
+ Log.e(TAG, "Failed to verify SO file copy: " + destPath);
+ // Try another verification method
+ Shell.Result sizeResult = Shell.cmd("stat -c %s \"" + destPath + "\" 2>/dev/null").exec();
+ if (sizeResult.isSuccess() && !sizeResult.getOut().isEmpty()) {
+ Log.i(TAG, "File exists with size: " + sizeResult.getOut().get(0) + " bytes");
+ }
+ }
+ }
+
+ Log.i(TAG, "Deployment complete for: " + packageName);
+ }
+
+ // Clean up deployed SO files when app is disabled
+ private void cleanupAppSoFiles(String packageName) {
+ AppConfig appConfig = config.perAppConfig.get(packageName);
+ if (appConfig == null || appConfig.soFiles.isEmpty()) {
+ Log.w(TAG, "No SO files to clean up for: " + packageName);
+ return;
+ }
+
+ // First check if we have root access
+ if (!Shell.getShell().isRoot()) {
+ Log.e(TAG, "No root access available!");
+ return;
+ }
+
+ String filesDir = "/data/data/" + packageName + "/files";
+
+ // Only delete the SO files we deployed, not the entire directory
+ for (SoFile soFile : appConfig.soFiles) {
+ String mappedName = new File(soFile.storedPath).getName();
+ String filePath = filesDir + "/" + mappedName;
+
+ Log.i(TAG, "Cleaning up: " + filePath);
+
+ // Check if file exists before trying to delete
+ Shell.Result checkResult = Shell.cmd("test -f \"" + filePath + "\" && echo 'exists'").exec();
+ if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
+ // Try to remove the file
+ Shell.Result result = Shell.cmd("rm -f \"" + filePath + "\"").exec();
+
+ // Verify deletion
+ Shell.Result verifyResult = Shell.cmd("test -f \"" + filePath + "\" && echo 'still_exists'").exec();
+ if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
+ Log.i(TAG, "Successfully deleted SO file: " + filePath);
+ } else {
+ Log.e(TAG, "Failed to delete SO file: " + filePath);
+ // Try with su -c
+ Shell.cmd("su -c 'rm -f \"" + filePath + "\"'").exec();
+ }
+ } else {
+ Log.w(TAG, "SO file not found for cleanup: " + filePath);
+ }
+ }
+
+ Log.i(TAG, "Cleanup complete for: " + packageName);
+ }
+
+ // Deploy SO files for all enabled apps
+ public void deployAllSoFiles() {
+ for (Map.Entry entry : config.perAppConfig.entrySet()) {
+ if (entry.getValue().enabled) {
+ deploySoFilesToApp(entry.getKey());
+ }
+ }
+ }
+
+ // Data classes
+ public static class ModuleConfig {
+ public boolean enabled = true;
+ public boolean hideInjection = false;
+ public List globalSoFiles = new ArrayList<>();
+ public Map perAppConfig = new HashMap<>();
+ }
+
+ public static class AppConfig {
+ public boolean enabled = false;
+ public List 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/configapp/src/main/java/com/jiqiu/configapp/FileBrowserActivity.java b/configapp/src/main/java/com/jiqiu/configapp/FileBrowserActivity.java
new file mode 100644
index 0000000..725913d
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/FileBrowserActivity.java
@@ -0,0 +1,287 @@
+package com.jiqiu.configapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.topjohnwu.superuser.Shell;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class FileBrowserActivity extends AppCompatActivity {
+
+ private static final String TAG = "FileBrowser";
+ public static final String EXTRA_START_PATH = "start_path";
+ public static final String EXTRA_FILE_FILTER = "file_filter";
+ public static final String EXTRA_SELECTED_PATH = "selected_path";
+
+ private RecyclerView recyclerView;
+ private TextView currentPathText;
+ private View emptyView;
+ private FileListAdapter adapter;
+
+ private String currentPath;
+ private String fileFilter = ".so";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_file_browser);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle("选择SO文件");
+
+ currentPathText = findViewById(R.id.currentPath);
+ recyclerView = findViewById(R.id.recyclerView);
+ emptyView = findViewById(R.id.emptyView);
+
+ adapter = new FileListAdapter();
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.setAdapter(adapter);
+
+ // Get start path from intent
+ String startPath = getIntent().getStringExtra(EXTRA_START_PATH);
+ if (startPath == null) {
+ startPath = "/data/local/tmp";
+ }
+ fileFilter = getIntent().getStringExtra(EXTRA_FILE_FILTER);
+ if (fileFilter == null) {
+ fileFilter = ".so";
+ }
+
+ // Check if we have root access
+ if (!Shell.getShell().isRoot()) {
+ Toast.makeText(this, "需要Root权限才能浏览文件", Toast.LENGTH_LONG).show();
+ Log.e(TAG, "No root access");
+ }
+
+ currentPath = startPath;
+ loadFiles();
+ }
+
+ private void loadFiles() {
+ currentPathText.setText(currentPath);
+
+ List items = new ArrayList<>();
+
+ // Add parent directory if not root
+ if (!"/".equals(currentPath)) {
+ items.add(new FileItem("..", true, true));
+ }
+
+ // List files using root
+ Log.d(TAG, "Loading files from: " + currentPath);
+ Shell.Result result = Shell.cmd("ls -la " + currentPath + " 2>/dev/null").exec();
+ Log.d(TAG, "ls command success: " + result.isSuccess() + ", output lines: " + result.getOut().size());
+
+ if (result.isSuccess()) {
+ for (String line : result.getOut()) {
+ // Skip empty lines, total line, and symbolic links
+ if (line.trim().isEmpty() || line.startsWith("total") || line.contains("->")) {
+ continue;
+ }
+
+ // Try to parse ls output - handle different formats
+ String name = null;
+ boolean isDirectory = false;
+ boolean isReadable = true;
+
+ // Check if line starts with permissions (drwxr-xr-x format)
+ if (line.matches("^[dlrwxst-]{10}.*")) {
+ String[] parts = line.split("\\s+", 9);
+ if (parts.length >= 9) {
+ String permissions = parts[0];
+ name = parts[parts.length - 1];
+ isDirectory = permissions.startsWith("d");
+ isReadable = permissions.length() > 1 && permissions.charAt(1) == 'r';
+ }
+ } else {
+ // Simple format, just the filename
+ name = line.trim();
+ // Check if it's a directory by trying to list it
+ Shell.Result dirCheck = Shell.cmd("test -d \"" + currentPath + "/" + name + "\" && echo 'dir'").exec();
+ isDirectory = dirCheck.isSuccess() && !dirCheck.getOut().isEmpty();
+ }
+
+ if (name != null && !".".equals(name) && !"..".equals(name)) {
+ // Filter files by extension
+ if (!isDirectory && fileFilter != null && !name.endsWith(fileFilter)) {
+ continue;
+ }
+
+ items.add(new FileItem(name, isDirectory, isReadable));
+ }
+ }
+ } else {
+ // If ls fails, try a simpler approach
+ Shell.Result simpleResult = Shell.cmd("cd " + currentPath + " && for f in *; do echo \"$f\"; done").exec();
+ if (simpleResult.isSuccess()) {
+ for (String name : simpleResult.getOut()) {
+ if (!name.trim().isEmpty() && !"*".equals(name)) {
+ Shell.Result dirCheck = Shell.cmd("test -d \"" + currentPath + "/" + name + "\" && echo 'dir'").exec();
+ boolean isDirectory = dirCheck.isSuccess() && !dirCheck.getOut().isEmpty();
+
+ // Filter files by extension
+ if (!isDirectory && fileFilter != null && !name.endsWith(fileFilter)) {
+ continue;
+ }
+
+ items.add(new FileItem(name, isDirectory, true));
+ }
+ }
+ }
+ }
+
+ // If still no items and not root, add some common directories to try
+ if (items.size() <= 1 && "/data/local/tmp".equals(currentPath)) {
+ // Try to create a test file to verify access
+ Shell.cmd("touch /data/local/tmp/test_access.tmp && rm /data/local/tmp/test_access.tmp").exec();
+
+ // Add any .so files we can find
+ Shell.Result findResult = Shell.cmd("find " + currentPath + " -maxdepth 1 -name '*.so' -type f 2>/dev/null").exec();
+ if (findResult.isSuccess()) {
+ for (String path : findResult.getOut()) {
+ if (!path.trim().isEmpty()) {
+ String name = path.substring(path.lastIndexOf('/') + 1);
+ items.add(new FileItem(name, false, true));
+ }
+ }
+ }
+ }
+
+ Collections.sort(items, (a, b) -> {
+ if (a.isDirectory != b.isDirectory) {
+ return a.isDirectory ? -1 : 1;
+ }
+ return a.name.compareToIgnoreCase(b.name);
+ });
+
+ adapter.setItems(items);
+ emptyView.setVisibility(items.isEmpty() || (items.size() == 1 && "..".equals(items.get(0).name)) ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ onBackPressed();
+ return true;
+ }
+
+ class FileItem {
+ String name;
+ boolean isDirectory;
+ boolean isReadable;
+
+ FileItem(String name, boolean isDirectory, boolean isReadable) {
+ this.name = name;
+ this.isDirectory = isDirectory;
+ this.isReadable = isReadable;
+ }
+ }
+
+ class FileListAdapter extends RecyclerView.Adapter {
+ private List items = new ArrayList<>();
+
+ void setItems(List items) {
+ this.items = items;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_file, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.bind(items.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return items.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ ImageView icon;
+ TextView name;
+ TextView info;
+
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ icon = itemView.findViewById(R.id.fileIcon);
+ name = itemView.findViewById(R.id.fileName);
+ info = itemView.findViewById(R.id.fileInfo);
+ }
+
+ void bind(FileItem item) {
+ name.setText(item.name);
+
+ if (item.isDirectory) {
+ icon.setImageResource(android.R.drawable.ic_menu_agenda);
+ info.setText("文件夹");
+ } else {
+ icon.setImageResource(android.R.drawable.ic_menu_save);
+ info.setText("SO文件");
+ }
+
+ if (!item.isReadable) {
+ itemView.setAlpha(0.5f);
+ } else {
+ itemView.setAlpha(1.0f);
+ }
+
+ itemView.setOnClickListener(v -> {
+ if ("..".equals(item.name)) {
+ // Go to parent directory
+ int lastSlash = currentPath.lastIndexOf('/');
+ if (lastSlash > 0) {
+ currentPath = currentPath.substring(0, lastSlash);
+ } else {
+ currentPath = "/";
+ }
+ loadFiles();
+ } else if (item.isDirectory) {
+ if (!item.isReadable) {
+ Toast.makeText(FileBrowserActivity.this,
+ "没有权限访问此目录", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if ("/".equals(currentPath)) {
+ currentPath = "/" + item.name;
+ } else {
+ currentPath = currentPath + "/" + item.name;
+ }
+ loadFiles();
+ } else {
+ // File selected
+ String selectedPath = currentPath + "/" + item.name;
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(EXTRA_SELECTED_PATH, selectedPath);
+ setResult(Activity.RESULT_OK, resultIntent);
+ finish();
+ }
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java b/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java
new file mode 100644
index 0000000..6311488
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/MainActivity.java
@@ -0,0 +1,100 @@
+package com.jiqiu.configapp;
+
+import android.os.Bundle;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+
+public class MainActivity extends AppCompatActivity implements SettingsFragment.OnSettingsChangeListener {
+
+ private BottomNavigationView bottomNavigationView;
+ private AppListFragment appListFragment;
+ private SettingsFragment settingsFragment;
+ private SoManagerFragment soManagerFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this);
+ setContentView(R.layout.activity_main);
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
+
+ initViews();
+ setupBottomNavigation();
+
+ // 默认显示应用列表
+ if (savedInstanceState == null) {
+ showAppListFragment();
+ }
+ }
+
+ private void initViews() {
+ bottomNavigationView = findViewById(R.id.bottom_navigation);
+ }
+
+ private void setupBottomNavigation() {
+ bottomNavigationView.setOnItemSelectedListener(item -> {
+ int itemId = item.getItemId();
+ if (itemId == R.id.navigation_apps) {
+ showAppListFragment();
+ return true;
+ } else if (itemId == R.id.navigation_so_manager) {
+ showSoManagerFragment();
+ return true;
+ } else if (itemId == R.id.navigation_settings) {
+ showSettingsFragment();
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void showAppListFragment() {
+ if (appListFragment == null) {
+ appListFragment = new AppListFragment();
+ }
+ showFragment(appListFragment);
+ }
+
+ private void showSoManagerFragment() {
+ if (soManagerFragment == null) {
+ soManagerFragment = new SoManagerFragment();
+ }
+ showFragment(soManagerFragment);
+ }
+
+ private void showSettingsFragment() {
+ if (settingsFragment == null) {
+ settingsFragment = new SettingsFragment();
+ settingsFragment.setOnSettingsChangeListener(this);
+ }
+ showFragment(settingsFragment);
+ }
+
+ private void showFragment(Fragment fragment) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ FragmentTransaction transaction = fragmentManager.beginTransaction();
+ transaction.replace(R.id.nav_host_fragment, fragment);
+ transaction.commit();
+ }
+
+ @Override
+ public void onHideSystemAppsChanged(boolean hideSystemApps) {
+ // 当设置改变时,通知应用列表Fragment更新过滤
+ if (appListFragment != null) {
+ appListFragment.setHideSystemApps(hideSystemApps);
+ }
+ }
+}
\ No newline at end of file
diff --git a/configapp/src/main/java/com/jiqiu/configapp/SettingsFragment.java b/configapp/src/main/java/com/jiqiu/configapp/SettingsFragment.java
new file mode 100644
index 0000000..c8bedf5
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/SettingsFragment.java
@@ -0,0 +1,98 @@
+package com.jiqiu.configapp;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+/**
+ * 设置Fragment
+ */
+public class SettingsFragment extends Fragment {
+
+ private static final String PREFS_NAME = "MyInjectorSettings";
+ private static final String KEY_HIDE_SYSTEM_APPS = "hide_system_apps";
+
+ private RadioGroup radioGroupFilter;
+ private RadioButton radioShowAll;
+ private RadioButton radioHideSystem;
+
+ private SharedPreferences sharedPreferences;
+ private OnSettingsChangeListener settingsChangeListener;
+
+ public interface OnSettingsChangeListener {
+ void onHideSystemAppsChanged(boolean hideSystemApps);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_settings, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ initViews(view);
+ initSharedPreferences();
+ loadSettings();
+ setupListeners();
+ }
+
+ private void initViews(View view) {
+ radioGroupFilter = view.findViewById(R.id.radio_group_filter);
+ radioShowAll = view.findViewById(R.id.radio_show_all);
+ radioHideSystem = view.findViewById(R.id.radio_hide_system);
+ }
+
+ private void initSharedPreferences() {
+ sharedPreferences = getContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ }
+
+ private void loadSettings() {
+ boolean hideSystemApps = sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
+
+ if (hideSystemApps) {
+ radioHideSystem.setChecked(true);
+ } else {
+ radioShowAll.setChecked(true);
+ }
+ }
+
+ private void setupListeners() {
+ radioGroupFilter.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ boolean hideSystemApps = (checkedId == R.id.radio_hide_system);
+
+ // 保存设置
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(KEY_HIDE_SYSTEM_APPS, hideSystemApps);
+ editor.apply();
+
+ // 通知设置变化
+ if (settingsChangeListener != null) {
+ settingsChangeListener.onHideSystemAppsChanged(hideSystemApps);
+ }
+ }
+ });
+ }
+
+ public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
+ this.settingsChangeListener = listener;
+ }
+
+ public boolean isHideSystemApps() {
+ return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
+ }
+}
diff --git a/configapp/src/main/java/com/jiqiu/configapp/SoListAdapter.java b/configapp/src/main/java/com/jiqiu/configapp/SoListAdapter.java
new file mode 100644
index 0000000..c1b8cff
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/SoListAdapter.java
@@ -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 {
+
+ private List soFiles = new ArrayList<>();
+ private OnSoFileActionListener listener;
+
+ public interface OnSoFileActionListener {
+ void onDeleteClick(ConfigManager.SoFile soFile);
+ }
+
+ public void setSoFiles(List 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);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/configapp/src/main/java/com/jiqiu/configapp/SoManagerFragment.java b/configapp/src/main/java/com/jiqiu/configapp/SoManagerFragment.java
new file mode 100644
index 0000000..1f4ea15
--- /dev/null
+++ b/configapp/src/main/java/com/jiqiu/configapp/SoManagerFragment.java
@@ -0,0 +1,279 @@
+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 globalSoFiles = new ArrayList<>();
+
+ private ActivityResultLauncher filePickerLauncher;
+ private ActivityResultLauncher fileBrowserLauncher;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ configManager = new ConfigManager(requireContext());
+ // Ensure module directories exist
+ configManager.ensureModuleDirectories();
+
+ // 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);
+ }
+ }
+ }
+ );
+
+ // Initialize file browser
+ fileBrowserLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
+ String path = result.getData().getStringExtra(FileBrowserActivity.EXTRA_SELECTED_PATH);
+ if (path != null) {
+ showDeleteOriginalDialog(path);
+ }
+ }
+ }
+ );
+ }
+
+ @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();
+ // Also ensure common directories exist
+ Shell.cmd("mkdir -p /data/local/tmp").exec();
+ Shell.cmd("chmod 777 /data/local/tmp").exec();
+ loadSoFiles();
+ }
+ }
+
+ private void loadSoFiles() {
+ // Load global SO files from config
+ globalSoFiles = configManager.getAllSoFiles();
+ 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) {
+ 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, ".so");
+ 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, ".so");
+ fileBrowserLauncher.launch(intent);
+ }
+ })
+ .setNegativeButton("取消", null)
+ .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.setText("/data/local/tmp/");
+ 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()) {
+ showDeleteOriginalDialog(path);
+ }
+ })
+ .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);
+ }
+ showDeleteOriginalDialog(path);
+ }
+ }
+
+ private void showDeleteOriginalDialog(String path) {
+ new MaterialAlertDialogBuilder(requireContext())
+ .setTitle("删除原文件")
+ .setMessage("是否删除原始SO文件?\n\n文件路径:" + path)
+ .setPositiveButton("删除原文件", (dialog, which) -> {
+ addSoFile(path, true);
+ })
+ .setNegativeButton("保留原文件", (dialog, which) -> {
+ addSoFile(path, false);
+ })
+ .setNeutralButton("取消", null)
+ .show();
+ }
+
+ 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;
+ }
+
+ // Add to global SO files
+ configManager.addGlobalSoFile(path, deleteOriginal);
+
+ // Reload the list
+ loadSoFiles();
+ Toast.makeText(getContext(), "SO文件已添加", 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) {
+ configManager.removeGlobalSoFile(soFile);
+ loadSoFiles();
+ Toast.makeText(getContext(), "SO文件已删除", Toast.LENGTH_SHORT).show();
+ }
+}
\ No newline at end of file
diff --git a/configapp/src/main/res/drawable/ic_launcher_background.xml b/configapp/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/configapp/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/configapp/src/main/res/drawable/ic_launcher_foreground.xml b/configapp/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/configapp/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/drawable/system_app_badge.xml b/configapp/src/main/res/drawable/system_app_badge.xml
new file mode 100644
index 0000000..c9d729c
--- /dev/null
+++ b/configapp/src/main/res/drawable/system_app_badge.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/configapp/src/main/res/layout/activity_app_so_config.xml b/configapp/src/main/res/layout/activity_app_so_config.xml
new file mode 100644
index 0000000..9b80163
--- /dev/null
+++ b/configapp/src/main/res/layout/activity_app_so_config.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/activity_file_browser.xml b/configapp/src/main/res/layout/activity_file_browser.xml
new file mode 100644
index 0000000..a661436
--- /dev/null
+++ b/configapp/src/main/res/layout/activity_file_browser.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/activity_main.xml b/configapp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..caa48f7
--- /dev/null
+++ b/configapp/src/main/res/layout/activity_main.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/dialog_app_config.xml b/configapp/src/main/res/layout/dialog_app_config.xml
new file mode 100644
index 0000000..bc1df5e
--- /dev/null
+++ b/configapp/src/main/res/layout/dialog_app_config.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/dialog_input.xml b/configapp/src/main/res/layout/dialog_input.xml
new file mode 100644
index 0000000..cbdb0ea
--- /dev/null
+++ b/configapp/src/main/res/layout/dialog_input.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/fragment_app_list.xml b/configapp/src/main/res/layout/fragment_app_list.xml
new file mode 100644
index 0000000..5ab2ffa
--- /dev/null
+++ b/configapp/src/main/res/layout/fragment_app_list.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/configapp/src/main/res/layout/fragment_settings.xml b/configapp/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 0000000..3270a17
--- /dev/null
+++ b/configapp/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/configapp/src/main/res/layout/fragment_so_manager.xml b/configapp/src/main/res/layout/fragment_so_manager.xml
new file mode 100644
index 0000000..a8153c3
--- /dev/null
+++ b/configapp/src/main/res/layout/fragment_so_manager.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/item_app.xml b/configapp/src/main/res/layout/item_app.xml
new file mode 100644
index 0000000..755d0a9
--- /dev/null
+++ b/configapp/src/main/res/layout/item_app.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/configapp/src/main/res/layout/item_file.xml b/configapp/src/main/res/layout/item_file.xml
new file mode 100644
index 0000000..187d4ce
--- /dev/null
+++ b/configapp/src/main/res/layout/item_file.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/item_so_file.xml b/configapp/src/main/res/layout/item_so_file.xml
new file mode 100644
index 0000000..5e959d1
--- /dev/null
+++ b/configapp/src/main/res/layout/item_so_file.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/layout/item_so_selection.xml b/configapp/src/main/res/layout/item_so_selection.xml
new file mode 100644
index 0000000..3c4d5fa
--- /dev/null
+++ b/configapp/src/main/res/layout/item_so_selection.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/menu/bottom_nav_menu.xml b/configapp/src/main/res/menu/bottom_nav_menu.xml
new file mode 100644
index 0000000..753e065
--- /dev/null
+++ b/configapp/src/main/res/menu/bottom_nav_menu.xml
@@ -0,0 +1,17 @@
+
+
diff --git a/configapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/configapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/configapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/configapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/configapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/configapp/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/configapp/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/configapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/configapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/configapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/configapp/src/main/res/mipmap-mdpi/ic_launcher.webp b/configapp/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/configapp/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/configapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/configapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/configapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/configapp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/configapp/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/configapp/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/configapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/configapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/configapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/configapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/configapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/configapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/configapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/configapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/configapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/configapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/configapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/configapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/configapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/configapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/configapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/configapp/src/main/res/values-night/themes.xml b/configapp/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..5ba89c0
--- /dev/null
+++ b/configapp/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/main/res/values/colors.xml b/configapp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/configapp/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/configapp/src/main/res/values/strings.xml b/configapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..273e2d2
--- /dev/null
+++ b/configapp/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+
+ MyInjector Config
+
+
+ 应用列表
+ SO库管理
+ 全局设置
+
+
+ 搜索应用
+ 系统应用
+ 正在加载应用列表...
+
+
+ 全局设置
+ 过滤系统应用
+ 选择是否在应用列表中显示系统应用
+ 显示所有应用
+ 隐藏系统应用
+
+
+ 关于
+ MyInjector 配置应用,用于管理注入设置
+
\ No newline at end of file
diff --git a/configapp/src/main/res/values/themes.xml b/configapp/src/main/res/values/themes.xml
new file mode 100644
index 0000000..a66bf8c
--- /dev/null
+++ b/configapp/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/configapp/src/test/java/com/jiqiu/configapp/ExampleUnitTest.java b/configapp/src/test/java/com/jiqiu/configapp/ExampleUnitTest.java
new file mode 100644
index 0000000..a08b1f3
--- /dev/null
+++ b/configapp/src/test/java/com/jiqiu/configapp/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.jiqiu.configapp;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 01b80d7..53682c1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,3 +17,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
+# Fix TLS handshake issues
+systemProp.https.protocols=TLSv1.2,TLSv1.3
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/module/build.gradle b/module/build.gradle
index 915645c..3402c04 100644
--- a/module/build.gradle
+++ b/module/build.gradle
@@ -66,6 +66,17 @@ afterEvaluate {
from("$buildDir/intermediates/stripped_native_libs/$variantLowered/out/lib") {
into 'lib'
}
+ // Copy service.sh
+ from("$projectDir") {
+ include 'service.sh'
+ }
+ // Copy ConfigApp APK if it exists
+ def apkFile = file("$rootDir/configapp/build/outputs/apk/debug/configapp-debug.apk")
+ if (apkFile.exists()) {
+ from(apkFile) {
+ rename { 'configapp.apk' }
+ }
+ }
doLast {
file("$magiskDir/zygisk").mkdir()
fileTree("$magiskDir/lib").visit { f ->
diff --git a/module/service.sh b/module/service.sh
new file mode 100755
index 0000000..8abbbff
--- /dev/null
+++ b/module/service.sh
@@ -0,0 +1,98 @@
+#!/system/bin/sh
+MODDIR=${0%/*}
+
+# 确保路径定义
+export PATH=/system/bin:/system/xbin:$PATH
+
+# 定义日志函数
+log() {
+ echo "[MyInjector] $(date '+%Y-%m-%d %H:%M:%S') $1" >> /data/local/tmp/myinjector_install.log
+}
+
+# APK 文件路径
+APK_PATH="$MODDIR/configapp.apk"
+
+# 检查 APK 是否存在
+if [ ! -f "$APK_PATH" ]; then
+ log "APK 文件不存在: $APK_PATH"
+ exit 1
+fi
+
+# 等待系统完全启动
+log "等待系统启动完成"
+while [ "$(getprop sys.boot_completed)" != "1" ]; do
+ sleep 1
+done
+sleep 5 # 额外等待,确保服务启动完成
+
+# 检查 pm 是否可用
+log "检查 pm 命令状态"
+while ! pm list packages >/dev/null 2>&1; do
+ sleep 1
+done
+
+# 检查是否已安装
+INSTALLED=$(pm list packages com.jiqiu.configapp 2>/dev/null)
+if [ -n "$INSTALLED" ]; then
+ log "ConfigApp 已安装,检查版本"
+ # 可以在这里添加版本检查逻辑
+else
+ log "ConfigApp 未安装,开始安装"
+fi
+
+# 获取系统版本
+SDK_VERSION=$(getprop ro.build.version.sdk)
+log "检测到系统版本: SDK $SDK_VERSION"
+
+# 根据系统版本选择安装方法
+if [ "$SDK_VERSION" -ge 29 ]; then
+ # 高版本 Android(SDK >= 29)
+ log "使用高版本安装逻辑"
+ {
+ INSTALL_SESSION=$(pm install-create -r)
+ if [ $? -ne 0 ]; then
+ log "创建安装会话失败"
+ exit 1
+ fi
+ log "安装会话创建成功: $INSTALL_SESSION"
+
+ pm install-write "$INSTALL_SESSION" 0 "$APK_PATH"
+ if [ $? -ne 0 ]; then
+ log "写入 APK 文件失败"
+ log "降级,使用低版本安装逻辑"
+ pm install -r "$APK_PATH" >> /data/local/tmp/myinjector_install.log 2>&1
+ if [ $? -ne 0 ]; then
+ log "APK 安装失败"
+ exit 1
+ fi
+ log "APK 安装完成"
+ exit 0
+ fi
+ log "APK 写入成功"
+
+ pm install-commit "$INSTALL_SESSION"
+ if [ $? -ne 0 ]; then
+ log "提交安装会话失败"
+ exit 1
+ fi
+ log "APK 安装完成"
+ } >> /data/local/tmp/myinjector_install.log 2>&1
+else
+ # 低版本 Android(SDK < 29)
+ log "使用低版本安装逻辑"
+ pm install -r "$APK_PATH" >> /data/local/tmp/myinjector_install.log 2>&1
+ if [ $? -ne 0 ]; then
+ log "APK 安装失败"
+ exit 1
+ fi
+ log "APK 安装完成"
+fi
+
+# 确保模块目录权限正确
+chmod -R 755 /data/adb/modules/zygisk-myinjector
+chown -R root:root /data/adb/modules/zygisk-myinjector
+
+log "ConfigApp 安装脚本执行完成"
+
+# 脚本完成
+exit 0
\ No newline at end of file
diff --git a/module/src/main/cpp/CMakeLists.txt b/module/src/main/cpp/CMakeLists.txt
index 659cb82..84b4a40 100644
--- a/module/src/main/cpp/CMakeLists.txt
+++ b/module/src/main/cpp/CMakeLists.txt
@@ -35,7 +35,8 @@ aux_source_directory(xdl xdl-src)
add_library(${MODULE_NAME} SHARED
main.cpp
- hack.cpp
+ hack_new.cpp
+ config.cpp
newriruhide.cpp
pmparser.cpp
${xdl-src})
diff --git a/module/src/main/cpp/config.cpp b/module/src/main/cpp/config.cpp
new file mode 100644
index 0000000..1f3cab3
--- /dev/null
+++ b/module/src/main/cpp/config.cpp
@@ -0,0 +1,191 @@
+#include "config.h"
+#include
+#include
+#include
+
+#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 getAppSoFiles(const std::string& packageName) {
+ if (!g_configLoaded) {
+ readConfig();
+ }
+
+ auto it = g_config.perAppConfig.find(packageName);
+ if (it != g_config.perAppConfig.end()) {
+ LOGD("Found app config for %s with %zu SO files", packageName.c_str(), it->second.soFiles.size());
+ return it->second.soFiles;
+ }
+ LOGD("No app config found for %s", packageName.c_str());
+ return {};
+ }
+
+ bool shouldHideInjection() {
+ if (!g_configLoaded) {
+ readConfig();
+ }
+ return g_config.hideInjection;
+ }
+}
\ No newline at end of file
diff --git a/module/src/main/cpp/config.h b/module/src/main/cpp/config.h
new file mode 100644
index 0000000..7401555
--- /dev/null
+++ b/module/src/main/cpp/config.h
@@ -0,0 +1,40 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include
+#include
+#include
+
+namespace Config {
+
+ struct SoFile {
+ std::string name;
+ std::string storedPath;
+ std::string originalPath;
+ };
+
+ struct AppConfig {
+ bool enabled = false;
+ std::vector soFiles;
+ };
+
+ struct ModuleConfig {
+ bool enabled = true;
+ bool hideInjection = false;
+ std::unordered_map 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 getAppSoFiles(const std::string& packageName);
+
+ // Get hide injection setting
+ bool shouldHideInjection();
+}
+
+#endif // CONFIG_H
\ No newline at end of file
diff --git a/module/src/main/cpp/hack.h b/module/src/main/cpp/hack.h
index 30912ed..0f787bf 100644
--- a/module/src/main/cpp/hack.h
+++ b/module/src/main/cpp/hack.h
@@ -7,6 +7,6 @@
#include
-void hack_prepare(const char *game_data_dir, void *data, size_t length);
+void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length);
#endif //ZYGISK_IL2CPPDUMPER_HACK_H
diff --git a/module/src/main/cpp/hack_new.cpp b/module/src/main/cpp/hack_new.cpp
new file mode 100644
index 0000000..68c5f21
--- /dev/null
+++ b/module/src/main/cpp/hack_new.cpp
@@ -0,0 +1,72 @@
+#include "hack.h"
+#include "config.h"
+#include "log.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// External function from newriruhide.cpp
+extern "C" void riru_hide(const char *name);
+
+void load_so_file(const char *game_data_dir, const Config::SoFile &soFile) {
+ // Extract the mapped filename from storedPath (e.g., "1750851324251_libmylib.so")
+ const char *mapped_name = strrchr(soFile.storedPath.c_str(), '/');
+ if (!mapped_name) {
+ mapped_name = soFile.storedPath.c_str();
+ } else {
+ mapped_name++; // Skip the '/'
+ }
+
+ // The file should already be in app's files directory
+ char so_path[512];
+ snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, mapped_name);
+
+ // Check if file exists
+ if (access(so_path, F_OK) != 0) {
+ LOGE("SO file not found: %s", so_path);
+ return;
+ }
+
+ // Load the SO file
+ void *handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
+ if (handle) {
+ LOGI("Successfully loaded SO: %s (mapped: %s)", soFile.name.c_str(), mapped_name);
+
+ // Hide if configured
+ if (Config::shouldHideInjection()) {
+ // Hide using the mapped name since that's what we loaded
+ riru_hide(mapped_name);
+ LOGI("Applied riru_hide to: %s", mapped_name);
+ }
+ } else {
+ LOGE("Failed to load SO: %s - %s", so_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 and files to be copied
+ sleep(2);
+
+ // 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 (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
+ load_so_file(game_data_dir, soFile);
+ }
+}
+
+void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length) {
+ LOGI("hack_prepare called for package: %s, dir: %s", package_name, game_data_dir);
+
+ std::thread hack_thread(hack_thread_func, game_data_dir, package_name);
+ hack_thread.detach();
+}
\ No newline at end of file
diff --git a/module/src/main/cpp/main.cpp b/module/src/main/cpp/main.cpp
index 4594e0c..59755dd 100644
--- a/module/src/main/cpp/main.cpp
+++ b/module/src/main/cpp/main.cpp
@@ -6,11 +6,15 @@
#include
#include
#include
+#include
+#include
+#include
#include "hack.h"
#include "zygisk.hpp"
#include "game.h"
#include "log.h"
#include "dlfcn.h"
+#include "config.h"
using zygisk::Api;
using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs;
@@ -20,6 +24,7 @@ public:
void onLoad(Api *api, JNIEnv *env) override {
this->api = api;
this->env = env;
+ enable_hack = false;
}
void preAppSpecialize(AppSpecializeArgs *args) override {
@@ -37,7 +42,8 @@ public:
void postAppSpecialize(const AppSpecializeArgs *) override {
if (enable_hack) {
- std::thread hack_thread(hack_prepare, _data_dir, data, length);
+ // Then start hack thread
+ std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length);
hack_thread.detach();
}
}
@@ -47,15 +53,25 @@ private:
JNIEnv *env;
bool enable_hack;
char *_data_dir;
+ char *_package_name;
void *data;
size_t length;
-
+
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);
enable_hack = true;
_data_dir = new char[strlen(app_data_dir) + 1];
strcpy(_data_dir, app_data_dir);
+ _package_name = new char[strlen(package_name) + 1];
+ strcpy(_package_name, package_name);
+
+ // ConfigApp is responsible for copying SO files
+ // We just need to load them
#if defined(__i386__)
auto path = "zygisk/armeabi-v7a.so";
diff --git a/settings.gradle b/settings.gradle
index a843339..3804165 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,3 +3,4 @@ include ':module'
import org.apache.tools.ant.DirectoryScanner
DirectoryScanner.removeDefaultExclude('**/.gitattributes')
+include ':configapp'