Compare commits
30 Commits
multiblock
...
config_app
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb41d924b4 | ||
|
|
f5bfc60f46 | ||
|
|
5dc5f56383 | ||
|
|
8c70dbeffc | ||
|
|
744edbd311 | ||
|
|
276a8bd324 | ||
|
|
83435babaa | ||
|
|
4af96025cb | ||
|
|
ce7ef53ac5 | ||
|
|
e5e0c2a1da | ||
|
|
850e7d0e87 | ||
|
|
1076c1e711 | ||
|
|
f557d71874 | ||
|
|
5632194bda | ||
|
|
7d8b86f374 | ||
|
|
499a26feec | ||
|
|
4fe44ae346 | ||
|
|
9e8002863d | ||
|
|
d5eae1d69c | ||
|
|
3744b91958 | ||
|
|
933b962d6e | ||
|
|
1a5a3c3bc2 | ||
|
|
8b489d0c5c | ||
|
|
8e715b8148 | ||
|
|
b50d500319 | ||
|
|
a50ca00c54 | ||
|
|
79cb1e47b3 | ||
|
|
074e03ca15 | ||
|
|
cca6a126ac | ||
|
|
14ad58ec74 |
27
.github/workflows/build.yml
vendored
@@ -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
@@ -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
|
||||
78
README.md
@@ -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)
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -20,6 +20,7 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
build_all.sh
Executable 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
@@ -0,0 +1 @@
|
||||
/build
|
||||
62
configapp/build.gradle
Normal 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
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
29
configapp/src/main/AndroidManifest.xml
Normal 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>
|
||||
73
configapp/src/main/java/com/jiqiu/configapp/AppInfo.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
141
configapp/src/main/java/com/jiqiu/configapp/AppListAdapter.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
324
configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
402
configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
configapp/src/main/java/com/jiqiu/configapp/MainActivity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
170
configapp/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
30
configapp/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
6
configapp/src/main/res/drawable/system_app_badge.xml
Normal 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>
|
||||
42
configapp/src/main/res/layout/activity_app_so_config.xml
Normal 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>
|
||||
67
configapp/src/main/res/layout/activity_file_browser.xml
Normal 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>
|
||||
32
configapp/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
96
configapp/src/main/res/layout/dialog_app_config.xml
Normal 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>
|
||||
14
configapp/src/main/res/layout/dialog_input.xml
Normal 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>
|
||||
42
configapp/src/main/res/layout/fragment_app_list.xml
Normal 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>
|
||||
108
configapp/src/main/res/layout/fragment_settings.xml
Normal 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>
|
||||
82
configapp/src/main/res/layout/fragment_so_manager.xml
Normal 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>
|
||||
75
configapp/src/main/res/layout/item_app.xml
Normal 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>
|
||||
44
configapp/src/main/res/layout/item_file.xml
Normal 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>
|
||||
93
configapp/src/main/res/layout/item_so_file.xml
Normal 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>
|
||||
42
configapp/src/main/res/layout/item_so_selection.xml
Normal 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>
|
||||
17
configapp/src/main/res/menu/bottom_nav_menu.xml
Normal 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>
|
||||
6
configapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
@@ -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>
|
||||
BIN
configapp/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
configapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
configapp/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
configapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
configapp/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
configapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
configapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
configapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
configapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
configapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
7
configapp/src/main/res/values-night/themes.xml
Normal 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>
|
||||
5
configapp/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
24
configapp/src/main/res/values/strings.xml
Normal 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>
|
||||
9
configapp/src/main/res/values/themes.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -0,0 +1,98 @@
|
||||
#!/system/bin/sh
|
||||
MODDIR=${0%/*}
|
||||
|
||||
# 确保路径定义
|
||||
export PATH=/system/bin:/system/xbin:$PATH
|
||||
|
||||
# 定义日志函数
|
||||
log() {
|
||||
echo "[MyInjector] $(date '+%Y-%m-%d %H:%M:%S') $1" >> /data/local/tmp/myinjector_install.log
|
||||
}
|
||||
|
||||
# APK 文件路径
|
||||
APK_PATH="$MODDIR/configapp.apk"
|
||||
|
||||
# 检查 APK 是否存在
|
||||
if [ ! -f "$APK_PATH" ]; then
|
||||
log "APK 文件不存在: $APK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 等待系统完全启动
|
||||
log "等待系统启动完成"
|
||||
while [ "$(getprop sys.boot_completed)" != "1" ]; do
|
||||
sleep 1
|
||||
done
|
||||
sleep 5 # 额外等待,确保服务启动完成
|
||||
|
||||
# 检查 pm 是否可用
|
||||
log "检查 pm 命令状态"
|
||||
while ! pm list packages >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 检查是否已安装
|
||||
INSTALLED=$(pm list packages com.jiqiu.configapp 2>/dev/null)
|
||||
if [ -n "$INSTALLED" ]; then
|
||||
log "ConfigApp 已安装,检查版本"
|
||||
# 可以在这里添加版本检查逻辑
|
||||
else
|
||||
log "ConfigApp 未安装,开始安装"
|
||||
fi
|
||||
|
||||
# 获取系统版本
|
||||
SDK_VERSION=$(getprop ro.build.version.sdk)
|
||||
log "检测到系统版本: SDK $SDK_VERSION"
|
||||
|
||||
# 根据系统版本选择安装方法
|
||||
if [ "$SDK_VERSION" -ge 29 ]; then
|
||||
# 高版本 Android(SDK >= 29)
|
||||
log "使用高版本安装逻辑"
|
||||
{
|
||||
INSTALL_SESSION=$(pm install-create -r)
|
||||
if [ $? -ne 0 ]; then
|
||||
log "创建安装会话失败"
|
||||
exit 1
|
||||
fi
|
||||
log "安装会话创建成功: $INSTALL_SESSION"
|
||||
|
||||
pm install-write "$INSTALL_SESSION" 0 "$APK_PATH"
|
||||
if [ $? -ne 0 ]; then
|
||||
log "写入 APK 文件失败"
|
||||
log "降级,使用低版本安装逻辑"
|
||||
pm install -r "$APK_PATH" >> /data/local/tmp/myinjector_install.log 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log "APK 安装失败"
|
||||
exit 1
|
||||
fi
|
||||
log "APK 安装完成"
|
||||
exit 0
|
||||
fi
|
||||
log "APK 写入成功"
|
||||
|
||||
pm install-commit "$INSTALL_SESSION"
|
||||
if [ $? -ne 0 ]; then
|
||||
log "提交安装会话失败"
|
||||
exit 1
|
||||
fi
|
||||
log "APK 安装完成"
|
||||
} >> /data/local/tmp/myinjector_install.log 2>&1
|
||||
else
|
||||
# 低版本 Android(SDK < 29)
|
||||
log "使用低版本安装逻辑"
|
||||
pm install -r "$APK_PATH" >> /data/local/tmp/myinjector_install.log 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log "APK 安装失败"
|
||||
exit 1
|
||||
fi
|
||||
log "APK 安装完成"
|
||||
fi
|
||||
|
||||
# 确保模块目录权限正确
|
||||
chmod -R 755 /data/adb/modules/zygisk-myinjector
|
||||
chown -R root:root /data/adb/modules/zygisk-myinjector
|
||||
|
||||
log "ConfigApp 安装脚本执行完成"
|
||||
|
||||
# 脚本完成
|
||||
exit 0
|
||||
@@ -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)
|
||||
|
||||
|
||||
191
module/src/main/cpp/config.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
40
module/src/main/cpp/config.h
Normal 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
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
72
module/src/main/cpp/hack_new.cpp
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
134
module/src/main/cpp/newriruhide.cpp
Normal 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;
|
||||
}
|
||||
19
module/src/main/cpp/newriruhide.h
Normal 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
|
||||
307
module/src/main/cpp/pmparser.cpp
Normal 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++;
|
||||
}
|
||||
}
|
||||
99
module/src/main/cpp/pmparser.h
Normal 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
|
||||
@@ -3,3 +3,4 @@ include ':module'
|
||||
import org.apache.tools.ant.DirectoryScanner
|
||||
|
||||
DirectoryScanner.removeDefaultExclude('**/.gitattributes')
|
||||
include ':configapp'
|
||||
|
||||