30 Commits

Author SHA1 Message Date
jiqiu2021
eb41d924b4 Merge branch 'main' into config_app - Remove .github/workflows/build.yml 2025-06-25 23:15:37 +08:00
jiqiu2021
f5bfc60f46 fix: merge ci 2025-06-25 23:06:03 +08:00
Ji qiu
5dc5f56383 Merge pull request #10 from goseungduk/main
Change upload-artifact version v3 to v4
2025-06-25 22:53:05 +08:00
Ji qiu
8c70dbeffc Merge pull request #11 from jiqiu2022/config_app
Config app
2025-06-25 22:52:14 +08:00
jiqiu2021
744edbd311 fix: fix scripts 2025-06-25 22:47:44 +08:00
jiqiu2021
276a8bd324 fix: rm proxy ports 2025-06-25 22:40:13 +08:00
jiqiu2021
83435babaa fix: fix action script 2025-06-25 22:21:31 +08:00
jiqiu2021
4af96025cb fix: update action to v4 2025-06-25 22:11:37 +08:00
jiqiu2021
ce7ef53ac5 feat: support all branchs 2025-06-25 21:56:45 +08:00
jiqiu2021
e5e0c2a1da feat: github build scripts 2025-06-25 21:53:13 +08:00
jiqiu2021
850e7d0e87 feat: 增加自动打包脚本,一键打包成品模块 2025-06-25 21:41:28 +08:00
jiqiu2021
1076c1e711 feat: 成功加载了so 并且调用了riru hide等逻辑
准备增加自定义linker
2025-06-25 21:21:07 +08:00
jiqiu2021
f557d71874 add app manager logic 2025-06-25 19:19:57 +08:00
jiqiu2021
5632194bda feat:add so manager 2025-06-25 18:27:51 +08:00
jiqiu2021
7d8b86f374 feat:add so manager and config manager 2025-06-25 18:08:46 +08:00
jiqiu2021
499a26feec init global config view and app list view 2025-06-25 17:41:00 +08:00
jiqiu2021
4fe44ae346 init config app 2025-06-25 17:12:54 +08:00
goseungduk
9e8002863d change upload-artifact version v3 to v4 2025-06-08 14:31:08 +09:00
jiqiu2021
d5eae1d69c change readme 2024-11-20 20:55:03 +08:00
Ji qiu
3744b91958 Merge pull request #4 from jiqiu2022/hide_inject
fix errlog
2024-11-16 23:05:19 +08:00
jiqiu2021
933b962d6e fix errlog 2024-11-16 23:04:25 +08:00
Ji qiu
1a5a3c3bc2 Merge pull request #3 from jiqiu2022/hide_inject
Hide inject
2024-11-16 23:01:09 +08:00
jiqiu2021
8b489d0c5c fix 正常应用隐藏失败,增加LOG日志方便排查,已经正式机型测试成功 2024-11-16 23:00:12 +08:00
jiqiu2021
8e715b8148 fix 隐藏失败的问题,并且处理了原来riru内存泄漏的问题 2024-11-16 21:56:05 +08:00
jiqiu2021
b50d500319 补全文件 2024-11-15 15:33:18 +08:00
jiqiu2021
a50ca00c54 修改隐藏so名称 2024-11-15 15:31:05 +08:00
jiqiu2021
79cb1e47b3 修改隐藏so名称 2024-11-15 15:29:26 +08:00
jiqiu2021
074e03ca15 增加riru注入隐藏,手上没手机,没法注入,有问题再修吧 2024-11-15 15:27:04 +08:00
Ji qiu
cca6a126ac Merge pull request #2 from jiqiu2022/update-readme
Update readme
2024-11-15 14:24:13 +08:00
jiqiu2021
14ad58ec74 添加readme 2024-11-15 14:22:19 +08:00
70 changed files with 4348 additions and 69 deletions

View File

@@ -1,27 +0,0 @@
name: Build
on:
workflow_dispatch:
inputs:
package_name:
description: "Package name of the game:"
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: gradle
- run: |
chmod +x ./gradlew
sed -i 's/moduleDescription = "/moduleDescription = "(${{ github.event.inputs.package_name }}) /g' module.gradle
sed -i "s/com.game.packagename/${{ github.event.inputs.package_name }}/g" module/src/main/cpp/game.h
./gradlew :module:assembleRelease
- uses: actions/upload-artifact@v3
with:
name: zygisk-il2cppdumper
path: out/magisk_module_release/

179
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,179 @@
name: CI Build
on:
push:
branches: [ '**' ] # 所有分支的推送都会触发
pull_request:
branches: [ main, develop, master ]
workflow_dispatch: # 允许手动触发
inputs:
create_release:
description: 'Create a release after build'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'
release_tag:
description: 'Release tag (only if creating release)'
required: false
default: 'ci-latest'
permissions:
contents: write # 允许创建 release
packages: write # 允许上传包
actions: read # 允许读取 actions
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
- name: Prepare Release Files
if: github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true'
run: |
# 准备发布文件
mkdir -p release-files
cp zygisk-myinjector-ci.zip release-files/zygisk-myinjector-${{ github.event.inputs.release_tag }}.zip
cp configapp/build/outputs/apk/debug/configapp-debug.apk release-files/ConfigApp-${{ github.event.inputs.release_tag }}.apk
echo "Release files:"
ls -la release-files/
- name: Create Release
if: github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true'
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.event.inputs.release_tag }}
name: CI Release ${{ github.event.inputs.release_tag }}
body: |
## CI Build Release
- **Build Type**: Manual CI Release
- **Commit**: ${{ github.sha }}
- **Branch**: ${{ github.ref_name }}
- **Run ID**: ${{ github.run_id }}
### 下载
- 模块文件: `zygisk-myinjector-${{ github.event.inputs.release_tag }}.zip`
- 配置应用: `ConfigApp-${{ github.event.inputs.release_tag }}.apk`
### 安装说明
1. 在 Magisk Manager 中安装模块 ZIP
2. 重启设备
3. ConfigApp 会自动安装
---
*这是一个 CI 构建版本,可能不稳定*
draft: false
prerelease: true
files: |
release-files/zygisk-myinjector-*.zip
release-files/ConfigApp-*.apk

View File

@@ -1,21 +1,65 @@
# Zygisk-Il2CppDumper
Il2CppDumper with Zygisk, dump il2cpp data at runtime, can bypass protection, encryption and obfuscation.
# [Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
中文说明请戳[这里](README.zh-CN.md)
## How to use
1. Install [Magisk](https://github.com/topjohnwu/Magisk) v24 or later and enable Zygisk
2. Build module
最新开发进度:**多模块注入完成maps隐藏注入完成**并且修复了riru内存泄漏问题 现在仍然面临soinfo链表遍历无法隐藏的情况
我会初步进行框架层面的hook进行隐藏进一步使用自定义linker来伪装成系统库的加载。
原项目https://github.com/Perfare/Zygisk-Il2CppDumper
本项目在原项目基础上做局部更改,请支持原项目作者劳动成果
1. 安装[Magisk](https://github.com/topjohnwu/Magisk) v24以上版本并开启Zygisk
2. 生成模块
- GitHub Actions
1. Fork this repo
2. Go to the **Actions** tab in your forked repo
3. In the left sidebar, click the **Build** workflow.
4. Above the list of workflow runs, select **Run workflow**
5. Input the game package name and click **Run workflow**
6. Wait for the action to complete and download the artifact
1. Fork这个项目
2. 在你fork的项目中选择**Actions**选项卡
3. 在左边的侧边栏中,单击**Build**
4. 选择**Run workflow**
5. 输入游戏包名并点击**Run workflow**
6. 等待操作完成并下载
- Android Studio
1. Download the source code
2. Edit `game.h`, modify `AimPackageName` to the game package name
3. Use Android Studio to run the gradle task `:module:assembleRelease` to compile, the zip package will be generated in the `out` folder
3. Install module in Magisk
4. Start the game, `dump.cs` will be generated in the `/data/data/AimPackageName/files/` directory
1. 下载源码
2. 编辑`game.h`, 修改`GamePackageName`为游戏包名
3. 使用Android Studio运行gradle任务`:module:assembleRelease`编译zip包会生成在`out`文件夹下
3. 在Magisk里安装模块
4. 将要注入的so放入到/data/local/tmp下修改为test.so
(部分手机第一次注入不会成功,请重启,再之后的注入会成功)
多模块注入已经开发完成:
```
void hack_start(const char *game_data_dir,JavaVM *vm) {
load_so(game_data_dir,vm,"test");
//如果要注入多个so那么就在这里不断的添加load_so函数即可
}
```
目前正在开发的分支:
1. 使用Java的System.load加载so
2. 注入多个so的分支(已完成)
计划开发:
1. 第一步仿照Riru将注入的so进行内存上的初步隐藏可以对抗部分业务检测游戏安全相关已经补齐建议不要尝试已经开发完成
2. 第二步实现一个自定义的linker进行更深层次的注入隐藏
3. 第三步,搭配对应配套手机的内核模块对注入的模块进行进一步完美擦除,达到完美注入的目的
以此项目为脚手架的计划开发:
1. 一个全新的Frida框架保留大部分原生api并可以过任何相关注入检测
2. 一个全新的Trace框架高性能Trace速度是Stallker的60倍并且支持更全面的信息打印。具体效果可以参考看雪帖子
3. 一个全新的无痕调试框架支持像GDB一样调试没有ptrace痕迹两种思路进行无痕调试基于硬件断点以及基于VM

View File

@@ -1,19 +0,0 @@
# Zygisk-Il2CppDumper
Zygisk版Il2CppDumper在游戏运行时dump il2cpp数据可以绕过保护加密以及混淆。
## 如何食用
1. 安装[Magisk](https://github.com/topjohnwu/Magisk) v24以上版本并开启Zygisk
2. 生成模块
- GitHub Actions
1. Fork这个项目
2. 在你fork的项目中选择**Actions**选项卡
3. 在左边的侧边栏中,单击**Build**
4. 选择**Run workflow**
5. 输入游戏包名并点击**Run workflow**
6. 等待操作完成并下载
- Android Studio
1. 下载源码
2. 编辑`game.h`, 修改`AimPackageName`为游戏包名
3. 使用Android Studio运行gradle任务`:module:assembleRelease`编译zip包会生成在`out`文件夹下
3. 在Magisk里安装模块
4. 启动游戏,会在`/data/data/AimPackageName/files/`目录下生成`dump.cs`

View File

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

152
build_all.sh Executable file
View File

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

1
configapp/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

62
configapp/build.gradle Normal file
View File

@@ -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'
}

21
configapp/proguard-rules.pro vendored Normal file
View File

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

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 查询已安装应用的权限 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ZygiskMyInjector">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".FileBrowserActivity"
android:parentActivityName=".MainActivity" />
</application>
</manifest>

View File

@@ -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 +
'}';
}
}

View File

@@ -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<AppListAdapter.AppViewHolder> {
private List<AppInfo> appList;
private List<AppInfo> 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<AppInfo> 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);
}
});
}
}
}

View File

@@ -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<AppInfo> 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<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
List<ConfigManager.SoFile> 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<ConfigManager.SoFile> 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<SoSelectionAdapter.ViewHolder> {
private List<ConfigManager.SoFile> globalSoFiles;
private List<ConfigManager.SoFile> selectedSoFiles;
public SoSelectionAdapter(List<ConfigManager.SoFile> globalSoFiles, List<ConfigManager.SoFile> appSoFiles) {
this.globalSoFiles = globalSoFiles;
this.selectedSoFiles = new ArrayList<>(appSoFiles);
}
public List<ConfigManager.SoFile> 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<ConfigManager.SoFile> 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<Void, Void, List<AppInfo>> {
@Override
protected List<AppInfo> doInBackground(Void... voids) {
List<AppInfo> apps = new ArrayList<>();
PackageManager pm = getContext().getPackageManager();
List<ApplicationInfo> 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<AppInfo>() {
@Override
public int compare(AppInfo o1, AppInfo o2) {
return o1.getAppName().compareToIgnoreCase(o2.getAppName());
}
});
return apps;
}
@Override
protected void onPostExecute(List<AppInfo> apps) {
allApps = apps;
adapter.setAppList(apps);
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
// 应用当前的过滤设置
filterApps(searchEditText.getText().toString());
}
}
}

View File

@@ -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<SoFile> getAppSoFiles(String packageName) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
return new ArrayList<>();
}
return new ArrayList<>(appConfig.soFiles);
}
public List<SoFile> 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<String, AppConfig> 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<SoFile> globalSoFiles = new ArrayList<>();
public Map<String, AppConfig> perAppConfig = new HashMap<>();
}
public static class AppConfig {
public boolean enabled = false;
public List<SoFile> soFiles = new ArrayList<>();
}
public static class SoFile {
public String name;
public String storedPath;
public String originalPath;
@Override
public boolean equals(Object obj) {
if (obj instanceof SoFile) {
return storedPath.equals(((SoFile) obj).storedPath);
}
return false;
}
}
}

View File

@@ -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<FileItem> 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<FileListAdapter.ViewHolder> {
private List<FileItem> items = new ArrayList<>();
void setItems(List<FileItem> 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();
}
});
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -0,0 +1,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<ConfigManager.SoFile> globalSoFiles = new ArrayList<>();
private ActivityResultLauncher<Intent> filePickerLauncher;
private ActivityResultLauncher<Intent> 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();
}
}

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF9800" />
<corners android:radius="12dp" />
</shape>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="暂无可用的SO文件\n请先在SO库管理中添加"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/currentPath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurfaceVariant"
android:padding="12dp"
android:text="/data/local/tmp"
android:textSize="14sp"
android:fontFamily="monospace"
android:textColor="?attr/colorOnSurfaceVariant" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="4dp" />
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:alpha="0.3"
android:src="@android:drawable/ic_menu_search" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="此目录为空"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- Fragment容器 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 底部导航栏 -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<ImageView
android:id="@+id/appIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_launcher_foreground" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/appName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:text="应用名称" />
<TextView
android:id="@+id/packageName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="com.example.app" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="选择要注入的SO文件"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/soListRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="200dp"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="暂无可用的SO文件\n请先在SO库管理中添加"
android:textSize="14sp"
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchHideInjection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="隐藏注入"
android:layout_marginTop="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用Riru Hide隐藏注入的SO文件"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginTop="4dp" />
</LinearLayout>

View File

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

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".AppListFragment">
<!-- 搜索框 -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/search_apps">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 应用列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_apps"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="vertical" />
<!-- 加载进度条 -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SettingsFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 全局设置标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/global_settings"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<!-- 过滤系统应用设置 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/filter_system_apps"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/filter_system_apps_desc"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="12dp" />
<RadioGroup
android:id="@+id/radio_group_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/radio_show_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/show_all_apps"
android:checked="true" />
<RadioButton
android:id="@+id/radio_hide_system"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hide_system_apps" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 其他设置可以在这里添加 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/about"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_description"
android:textSize="14sp"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

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

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<!-- 应用图标 -->
<ImageView
android:id="@+id/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="12dp"
android:scaleType="centerCrop" />
<!-- 应用信息 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginTop="2dp" />
<!-- 系统应用标签 -->
<TextView
android:id="@+id/system_app_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/system_app"
android:textSize="10sp"
android:textColor="@android:color/white"
android:background="@drawable/system_app_badge"
android:padding="4dp"
android:layout_marginTop="4dp"
android:visibility="gone" />
</LinearLayout>
<!-- 启用开关 -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/fileIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:src="@android:drawable/ic_menu_save"
android:tint="?attr/colorPrimary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/fileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="example.so"
android:singleLine="true"
android:ellipsize="middle" />
<TextView
android:id="@+id/fileInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="SO文件" />
</LinearLayout>
</LinearLayout>

View File

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

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:text="example.so" />
<TextView
android:id="@+id/textPath"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="/data/local/tmp/example.so"
android:singleLine="true"
android:ellipsize="middle" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_apps"
android:icon="@android:drawable/ic_menu_view"
android:title="@string/title_apps" />
<item
android:id="@+id/navigation_so_manager"
android:icon="@android:drawable/ic_menu_save"
android:title="@string/title_so_manager" />
<item
android:id="@+id/navigation_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/title_settings" />
</menu>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.ZygiskMyInjector" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,24 @@
<resources>
<string name="app_name">MyInjector Config</string>
<!-- 底部导航 -->
<string name="title_apps">应用列表</string>
<string name="title_so_manager">SO库管理</string>
<string name="title_settings">全局设置</string>
<!-- 应用列表 -->
<string name="search_apps">搜索应用</string>
<string name="system_app">系统应用</string>
<string name="loading_apps">正在加载应用列表...</string>
<!-- 设置页面 -->
<string name="global_settings">全局设置</string>
<string name="filter_system_apps">过滤系统应用</string>
<string name="filter_system_apps_desc">选择是否在应用列表中显示系统应用</string>
<string name="show_all_apps">显示所有应用</string>
<string name="hide_system_apps">隐藏系统应用</string>
<!-- 关于 -->
<string name="about">关于</string>
<string name="app_description">MyInjector 配置应用,用于管理注入设置</string>
</resources>

View File

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.ZygiskMyInjector" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.ZygiskMyInjector" parent="Base.Theme.ZygiskMyInjector" />
</resources>

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

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

0
gradlew vendored Normal file → Executable file
View File

View File

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

98
module/service.sh Executable file
View File

@@ -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
# 高版本 AndroidSDK >= 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
# 低版本 AndroidSDK < 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

View File

@@ -35,7 +35,10 @@ 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})
target_link_libraries(${MODULE_NAME} log)

View File

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

View File

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

View File

@@ -18,6 +18,7 @@
#include <sys/stat.h>
//#include <asm-generic/fcntl.h>
#include <fcntl.h>
#include "newriruhide.h"
void load_so(const char *game_data_dir, JavaVM *vm, const char *soname) {
bool load = false;
LOGI("hack_start %s", game_data_dir);
@@ -76,6 +77,9 @@ void load_so(const char *game_data_dir, JavaVM *vm, const char *soname) {
if (handle) {
LOGI("Successfully loaded %s", new_so_path);
load = true;
char new_soname[256];
sprintf(new_soname, "%s.so", soname);
riru_hide(new_soname);
break;
} else {
LOGE("Failed to load %s: %s", new_so_path, dlerror());

View File

@@ -7,6 +7,6 @@
#include <stddef.h>
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

View File

@@ -0,0 +1,72 @@
#include "hack.h"
#include "config.h"
#include "log.h"
#include <cstring>
#include <thread>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
// 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();
}

View File

@@ -12,5 +12,5 @@
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno))
#endif //ZYGISK_IL2CPPDUMPER_LOG_H

View File

@@ -6,11 +6,15 @@
#include <sys/types.h>
#include <unistd.h>
#include <cinttypes>
#include <dirent.h>
#include <errno.h>
#include <time.h>
#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";

View File

@@ -0,0 +1,134 @@
//
// Created by Mac on 2024/11/15.
//
// 给riru修复了内存泄漏的问题
#include "newriruhide.h"
/**
* Magic to hide from /proc/###/maps, the idea is from Haruue Icymoon (https://github.com/haruue)
*/
extern "C" {
int riru_hide(const char *name) ;
}
#ifdef __LP64__
#define LIB_PATH "/system/lib64/"
#else
#define LIB_PATH "/system/lib/"
#endif
struct hide_struct {
procmaps_struct *original;
uintptr_t backup_address;
};
static int get_prot(const procmaps_struct *procstruct) {
int prot = 0;
if (procstruct->is_r) {
prot |= PROT_READ;
}
if (procstruct->is_w) {
prot |= PROT_WRITE;
}
if (procstruct->is_x) {
prot |= PROT_EXEC;
}
return prot;
}
#define FAILURE_RETURN(exp, failure_value) ({ \
__typeof__(exp) _rc; \
_rc = (exp); \
if (_rc == failure_value) { \
PLOGE(#exp); \
return 1; \
} \
_rc; })
static int do_hide(hide_struct *data) {
auto procstruct = data->original;
auto start = (uintptr_t) procstruct->addr_start;
auto end = (uintptr_t) procstruct->addr_end;
auto length = end - start;
int prot = get_prot(procstruct);
// backup
data->backup_address = (uintptr_t) FAILURE_RETURN(
mmap(nullptr, length, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0),
MAP_FAILED);
LOGD("%" PRIxPTR"-%" PRIxPTR" %s %ld %s is backup to %" PRIxPTR, start, end, procstruct->perm,
procstruct->offset,
procstruct->pathname, data->backup_address);
if (procstruct->is_r || procstruct->is_x) { // If readable or executable
LOGD("memcpy -> backup");
memcpy((void *) data->backup_address, (void *) start, length);
// Unmap original memory region
LOGD("munmap original");
FAILURE_RETURN(munmap((void *) start, length), -1);
// Remap backup memory to original location
LOGD("mmap original with backup");
FAILURE_RETURN(mmap((void *) start, length, prot, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0),
MAP_FAILED);
}
return 0;
}
int riru_hide(const char *name) {
procmaps_iterator *maps = pmparser_parse(-1);
if (maps == nullptr) {
LOGE("cannot parse the memory map");
return false;
}
char buf[PATH_MAX];
hide_struct *data = nullptr;
size_t data_count = 0;
procmaps_struct *maps_tmp;
while ((maps_tmp = pmparser_next(maps)) != nullptr) {
bool matched = false;
#ifdef DEBUG_APP
matched = strstr(maps_tmp->pathname, "libriru.so");
#endif
matched = strstr(maps_tmp->pathname, name) != nullptr;
// Match the memory regions we want to hide
if (!matched) continue;
LOGI("matched %s", maps_tmp->pathname);
auto start = (uintptr_t) maps_tmp->addr_start;
auto end = (uintptr_t) maps_tmp->addr_end;
if (maps_tmp->is_r || maps_tmp->is_x) { // If memory is readable or executable
if (data) {
data = (hide_struct *) realloc(data, sizeof(hide_struct) * (data_count + 1));
} else {
data = (hide_struct *) malloc(sizeof(hide_struct));
}
data[data_count].original = maps_tmp;
data_count += 1;
}
LOGD("%" PRIxPTR"-%" PRIxPTR" %s %ld %s", start, end, maps_tmp->perm, maps_tmp->offset,
maps_tmp->pathname);
}
for (int i = 0; i < data_count; ++i) {
LOGI("do_hide %d", i);
do_hide(&data[i]);
}
// Free backup memory to avoid leaks
for (int i = 0; i < data_count; ++i) {
FAILURE_RETURN(munmap((void *) data[i].backup_address,
(uintptr_t) data[i].original->addr_end - (uintptr_t) data[i].original->addr_start), -1);
}
if (data) free(data);
pmparser_free(maps);
return 0;
}

View File

@@ -0,0 +1,19 @@
//
// Created by Mac on 2024/11/15.
//
#ifndef ZYGISK_MYINJECTOR_NEWRIRUHIDE_H
#define ZYGISK_MYINJECTOR_NEWRIRUHIDE_H
#define EXPORT __attribute__((visibility("default"))) __attribute__((used))
#include <cinttypes>
#include <sys/mman.h>
#include <set>
#include <string_view>
#include "pmparser.h"
#include "android/log.h"
#include "log.h"
extern "C" {
int riru_hide(const char *name) EXPORT;
}
#endif //ZYGISK_MYINJECTOR_NEWRIRUHIDE_H

View File

@@ -0,0 +1,307 @@
/*
@Author : ouadimjamal@gmail.com
@date : December 2015
Permission to use, copy, modify, distribute, and sell this software and its
documentation for any purpose is hereby granted without fee, provided that
the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation. No representations are made about the suitability of this
software for any purpose. It is provided "as is" without express or
implied warranty.
*/
#include "pmparser.h"
#include "log.h"
/**
* gobal variables
*/
//procmaps_struct* g_last_head=NULL;
//procmaps_struct* g_current=NULL;
procmaps_iterator* pmparser_parse(int pid){
LOGI("pmparser_parse called with pid: %d", pid);
procmaps_iterator* maps_it = (procmaps_iterator *)malloc(sizeof(procmaps_iterator));
if (!maps_it) {
LOGI("Failed to allocate memory for procmaps_iterator");
return NULL;
}
LOGI("Allocated memory for procmaps_iterator: %p", maps_it);
char maps_path[500];
if(pid >= 0 ){
snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);
LOGI("Constructed maps_path for pid: %s", maps_path);
} else {
snprintf(maps_path, sizeof(maps_path), "/proc/self/maps");
LOGI("Constructed maps_path for self: %s", maps_path);
}
FILE* file = fopen(maps_path, "r");
if(!file){
LOGI("pmparser: cannot open the memory maps, %s", strerror(errno));
free(maps_it);
return NULL;
}
LOGI("Opened maps file: %s", maps_path);
int ind = 0;
char buf[PROCMAPS_LINE_MAX_LENGTH];
procmaps_struct* list_maps = NULL;
procmaps_struct* tmp;
procmaps_struct* current_node = NULL;
char addr1[20], addr2[20], perm[8], offset[20], dev[10], inode[30], pathname[PATH_MAX];
while (fgets(buf, PROCMAPS_LINE_MAX_LENGTH, file)) {
LOGI("Read line %d: %s", ind + 1, buf);
// 分配一个新的节点
tmp = (procmaps_struct*)malloc(sizeof(procmaps_struct));
if (!tmp) {
LOGI("Failed to allocate memory for procmaps_struct at line %d", ind + 1);
fclose(file);
// 需要释放已分配的节点,避免内存泄漏
procmaps_struct* iter = list_maps;
while (iter) {
procmaps_struct* next = iter->next;
free(iter);
iter = next;
}
free(maps_it);
return NULL;
}
LOGI("Allocated memory for procmaps_struct: %p", tmp);
// 填充节点
_pmparser_split_line(buf, addr1, addr2, perm, offset, dev, inode, pathname);
LOGI("Parsed line %d - addr1: %s, addr2: %s, perm: %s, offset: %s, dev: %s, inode: %s, pathname: %s",
ind + 1, addr1, addr2, perm, offset, dev, inode, pathname);
// 使用临时变量解析地址
unsigned long tmp_addr_start_ul, tmp_addr_end_ul;
if (sscanf(addr1, "%lx", &tmp_addr_start_ul) != 1) {
LOGI("Failed to parse addr_start at line %d", ind + 1);
free(tmp);
continue;
}
if (sscanf(addr2, "%lx", &tmp_addr_end_ul) != 1) {
LOGI("Failed to parse addr_end at line %d", ind + 1);
free(tmp);
continue;
}
LOGI("Parsed addresses - addr_start: 0x%lx, addr_end: 0x%lx", tmp_addr_start_ul, tmp_addr_end_ul);
tmp->addr_start = (void*)tmp_addr_start_ul;
tmp->addr_end = (void*)tmp_addr_end_ul;
// size
tmp->length = (unsigned long)((char*)tmp->addr_end - (char*)tmp->addr_start);
LOGI("Calculated length: %lu", tmp->length);
// perm
strncpy(tmp->perm, perm, sizeof(tmp->perm) - 1);
tmp->perm[sizeof(tmp->perm) - 1] = '\0';
tmp->is_r = (perm[0] == 'r');
tmp->is_w = (perm[1] == 'w');
tmp->is_x = (perm[2] == 'x');
tmp->is_p = (perm[3] == 'p');
LOGI("Permissions - is_r: %d, is_w: %d, is_x: %d, is_p: %d", tmp->is_r, tmp->is_w, tmp->is_x, tmp->is_p);
// offset
if (sscanf(offset, "%lx", &tmp->offset) != 1) {
LOGI("Failed to parse offset at line %d", ind + 1);
free(tmp);
continue;
}
LOGI("Parsed offset: 0x%lx", tmp->offset);
// device
strncpy(tmp->dev, dev, sizeof(tmp->dev) - 1);
tmp->dev[sizeof(tmp->dev) - 1] = '\0';
LOGI("Device: %s", tmp->dev);
// inode
tmp->inode = atoi(inode);
LOGI("Inode: %d", tmp->inode);
// pathname
strncpy(tmp->pathname, pathname, sizeof(tmp->pathname) - 1);
tmp->pathname[sizeof(tmp->pathname) - 1] = '\0';
LOGI("Pathname: %s", tmp->pathname);
tmp->next = NULL;
// 连接节点到链表
if(ind == 0){
list_maps = tmp;
current_node = list_maps;
LOGI("Initialized list_maps with first node: %p", list_maps);
}
else{
current_node->next = tmp;
current_node = tmp;
LOGI("Appended node to list_maps: %p", tmp);
}
ind++;
}
if (ferror(file)) {
LOGI("Error occurred while reading the maps file");
// 释放已分配的节点和 maps_it
procmaps_struct* iter = list_maps;
while (iter) {
procmaps_struct* next = iter->next;
free(iter);
iter = next;
}
fclose(file);
free(maps_it);
return NULL;
}
// 关闭文件
fclose(file);
LOGI("Closed maps file: %s", maps_path);
// 设置迭代器
maps_it->head = list_maps;
maps_it->current = list_maps;
LOGI("Initialized procmaps_iterator - head: %p, current: %p", maps_it->head, maps_it->current);
return maps_it;
}
procmaps_struct* pmparser_next(procmaps_iterator* p_procmaps_it){
if(p_procmaps_it->current == NULL)
return NULL;
procmaps_struct* p_current = p_procmaps_it->current;
p_procmaps_it->current = p_procmaps_it->current->next;
return p_current;
/*
if(g_current==NULL){
g_current=g_last_head;
}else
g_current=g_current->next;
return g_current;
*/
}
void pmparser_free(procmaps_iterator* p_procmaps_it){
procmaps_struct* maps_list = p_procmaps_it->head;
if(maps_list==NULL) return ;
procmaps_struct* act=maps_list;
procmaps_struct* nxt=act->next;
while(act!=NULL){
free(act);
act=nxt;
if(nxt!=NULL)
nxt=nxt->next;
}
free(p_procmaps_it);
}
void _pmparser_split_line(
char*buf,char*addr1,char*addr2,
char*perm,char* offset,char* device,char*inode,
char* pathname){
//
int orig=0;
int i=0;
//addr1
while(buf[i]!='-'){
addr1[i-orig]=buf[i];
i++;
}
addr1[i]='\0';
i++;
//addr2
orig=i;
while(buf[i]!='\t' && buf[i]!=' '){
addr2[i-orig]=buf[i];
i++;
}
addr2[i-orig]='\0';
//perm
while(buf[i]=='\t' || buf[i]==' ')
i++;
orig=i;
while(buf[i]!='\t' && buf[i]!=' '){
perm[i-orig]=buf[i];
i++;
}
perm[i-orig]='\0';
//offset
while(buf[i]=='\t' || buf[i]==' ')
i++;
orig=i;
while(buf[i]!='\t' && buf[i]!=' '){
offset[i-orig]=buf[i];
i++;
}
offset[i-orig]='\0';
//dev
while(buf[i]=='\t' || buf[i]==' ')
i++;
orig=i;
while(buf[i]!='\t' && buf[i]!=' '){
device[i-orig]=buf[i];
i++;
}
device[i-orig]='\0';
//inode
while(buf[i]=='\t' || buf[i]==' ')
i++;
orig=i;
while(buf[i]!='\t' && buf[i]!=' '){
inode[i-orig]=buf[i];
i++;
}
inode[i-orig]='\0';
//pathname
pathname[0]='\0';
while(buf[i]=='\t' || buf[i]==' ')
i++;
orig=i;
while(buf[i]!='\t' && buf[i]!=' ' && buf[i]!='\n'){
pathname[i-orig]=buf[i];
i++;
}
pathname[i-orig]='\0';
}
void pmparser_print(procmaps_struct* map, int order){
procmaps_struct* tmp=map;
int id=0;
if(order<0) order=-1;
while(tmp!=NULL){
//(unsigned long) tmp->addr_start;
if(order==id || order==-1){
printf("Backed by:\t%s\n",strlen(tmp->pathname)==0?"[anonym*]":tmp->pathname);
printf("Range:\t\t%p-%p\n",tmp->addr_start,tmp->addr_end);
printf("Length:\t\t%ld\n",tmp->length);
printf("Offset:\t\t%ld\n",tmp->offset);
printf("Permissions:\t%s\n",tmp->perm);
printf("Inode:\t\t%d\n",tmp->inode);
printf("Device:\t\t%s\n",tmp->dev);
}
if(order!=-1 && id>order)
tmp=NULL;
else if(order==-1){
printf("#################################\n");
tmp=tmp->next;
}else tmp=tmp->next;
id++;
}
}

View File

@@ -0,0 +1,99 @@
/*
@Author : ouadimjamal@gmail.com
@date : December 2015
Permission to use, copy, modify, distribute, and sell this software and its
documentation for any purpose is hereby granted without fee, provided that
the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation. No representations are made about the suitability of this
software for any purpose. It is provided "as is" without express or
implied warranty.
*/
#ifndef H_PMPARSER
#define H_PMPARSER
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <linux/limits.h>
//maximum line length in a procmaps file
#define PROCMAPS_LINE_MAX_LENGTH (PATH_MAX + 100)
/**
* procmaps_struct
* @desc hold all the information about an area in the process's VM
*/
typedef struct procmaps_struct{
void* addr_start; //< start address of the area
void* addr_end; //< end address
unsigned long length; //< size of the range
char perm[5]; //< permissions rwxp
short is_r; //< rewrote of perm with short flags
short is_w;
short is_x;
short is_p;
long offset; //< offset
char dev[12]; //< dev major:minor
int inode; //< inode of the file that backs the area
char pathname[600]; //< the path of the file that backs the area
//chained list
struct procmaps_struct* next; //<handler of the chinaed list
} procmaps_struct;
/**
* procmaps_iterator
* @desc holds iterating information
*/
typedef struct procmaps_iterator{
procmaps_struct* head;
procmaps_struct* current;
} procmaps_iterator;
/**
* pmparser_parse
* @param pid the process id whose memory map to be parser. the current process if pid<0
* @return an iterator over all the nodes
*/
procmaps_iterator* pmparser_parse(int pid);
/**
* pmparser_next
* @description move between areas
* @param p_procmaps_it the iterator to move on step in the chained list
* @return a procmaps structure filled with information about this VM area
*/
procmaps_struct* pmparser_next(procmaps_iterator* p_procmaps_it);
/**
* pmparser_free
* @description should be called at the end to free the resources
* @param p_procmaps_it the iterator structure returned by pmparser_parse
*/
void pmparser_free(procmaps_iterator* p_procmaps_it);
/**
* _pmparser_split_line
* @description internal usage
*/
void _pmparser_split_line(char*buf,char*addr1,char*addr2,char*perm, char* offset, char* device,char*inode,char* pathname);
/**
* pmparser_print
* @param map the head of the list
* @order the order of the area to print, -1 to print everything
*/
void pmparser_print(procmaps_struct* map,int order);
#endif

View File

@@ -3,3 +3,4 @@ include ':module'
import org.apache.tools.ant.DirectoryScanner
DirectoryScanner.removeDefaultExclude('**/.gitattributes')
include ':configapp'