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 @@ + + + + +