Compare commits
55 Commits
hide_injec
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2b785f1e | ||
|
|
29a678c6dd | ||
|
|
6a79189290 | ||
|
|
7e23ed7281 | ||
|
|
aba55bfa57 | ||
|
|
ab46a223f1 | ||
|
|
3e5453ecf8 | ||
|
|
ec89b7bf2c | ||
|
|
cc47334970 | ||
|
|
8a23161e0f | ||
|
|
964c975cdd | ||
|
|
6110216556 | ||
|
|
e02a3df7fc | ||
|
|
cb64bc7d48 | ||
|
|
fc24ab3455 | ||
|
|
7e7b38caf6 | ||
|
|
b8b8dafed0 | ||
|
|
7b7389c0a0 | ||
|
|
122878f8fa | ||
|
|
13ee77e96f | ||
|
|
e4ca55d6cd | ||
|
|
f955835df5 | ||
|
|
7e5a96cd78 | ||
|
|
cc4fb60b7b | ||
|
|
0c713a26da | ||
|
|
fee5600129 | ||
|
|
cec1db61fb | ||
|
|
d201c72e22 | ||
|
|
7b7f6de828 | ||
|
|
92a640a3c7 | ||
|
|
680a93cba8 | ||
|
|
da73a3f9bd | ||
|
|
d793712a13 | ||
|
|
aa6709b9d9 | ||
|
|
eb41d924b4 | ||
|
|
f5bfc60f46 | ||
|
|
5dc5f56383 | ||
|
|
8c70dbeffc | ||
|
|
744edbd311 | ||
|
|
276a8bd324 | ||
|
|
83435babaa | ||
|
|
4af96025cb | ||
|
|
ce7ef53ac5 | ||
|
|
e5e0c2a1da | ||
|
|
850e7d0e87 | ||
|
|
1076c1e711 | ||
|
|
f557d71874 | ||
|
|
5632194bda | ||
|
|
7d8b86f374 | ||
|
|
499a26feec | ||
|
|
4fe44ae346 | ||
|
|
9e8002863d | ||
|
|
d5eae1d69c | ||
|
|
3744b91958 | ||
|
|
1a5a3c3bc2 |
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
|
||||||
225
README.md
@@ -1,48 +1,189 @@
|
|||||||
# [Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
|
# Zygisk注入器 - 动动手指轻松注入并隐藏你的SO文件
|
||||||
|
|
||||||
|
## 前言
|
||||||
|
|
||||||
|
作者在去年发布了[Zygisk注入模块的实现](https://bbs.kanxue.com/thread-283790.htm),成功实现了模块的自动注入和Riru隐藏功能。
|
||||||
|
|
||||||
|
然而,每次更换目标应用都需要重新编译,操作繁琐,导致用户采用率不高。为了解决这个问题,作者对项目进行了全面重构,带来了以下改进:
|
||||||
|
|
||||||
|
- **图形化界面**:告别命令行,操作更直观
|
||||||
|
- **一键安装**:面具模块快速部署
|
||||||
|
- **配套管理APP**:轻松管理注入配置
|
||||||
|
|
||||||
|
项目已完全开源,包含面具模块、管理APP以及所有打包脚本,并配置了GitHub CI自动构建。欢迎各位开发者贡献代码,提交PR。
|
||||||
|
|
||||||
|
### 版本规划&更新记录
|
||||||
|
版本规划:
|
||||||
|
- **v1.x**:专注功能添加,暂不考虑反检测
|
||||||
|
- **v2.x**:实现各种检测绕过,达到100%无痕注入
|
||||||
|
|
||||||
|
更新记录:
|
||||||
|
- **v1.2**: 增加gadget配置的自动生成,支持脚本和server模式,解决了若干bug,增加了全局注入延迟设置
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
**项目地址**:[https://github.com/jiqiu2022/Zygisk-MyInjector](https://github.com/jiqiu2022/Zygisk-MyInjector)
|
||||||
|
|
||||||
|
特别感谢以下项目和开发者(按时间顺序):
|
||||||
|
|
||||||
|
- [Zygisk-Il2CppDumper](https://github.com/Perfare/Zygisk-Il2CppDumper) - 提供最原始的注入思路
|
||||||
|
- Riru - 提供隐藏思路
|
||||||
|
- 小佳大佬 - 提供App界面化的思路
|
||||||
|
- [soLoader](https://github.com/SoyBeanMilkx/soLoader) - 提供自定义linker注入的全部逻辑,期待认识作者和作者一起继续完善
|
||||||
|
|
||||||
|
如果对自定义linker感兴趣的大佬,推荐soLoader作者的文章https://yuuki.cool/2025/06/15/CustomLinker/
|
||||||
|
|
||||||
|
## 使用教程
|
||||||
|
|
||||||
|
### 步骤一:下载并安装面具模块
|
||||||
|
|
||||||
|
1. 前往 [GitHub Release](https://github.com/jiqiu2022/Zygisk-MyInjector/tags) 页面下载最新版本的面具模块
|
||||||
|
|
||||||
|
本教程以 [v1.1.0](https://github.com/jiqiu2022/Zygisk-MyInjector/releases/tag/v1.1.0) 版本为例
|
||||||
|
|
||||||
|
2. 下载模块文件(如下图所示):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. 安装到手机,支持所有面具模块相关的APP(如 KernelSU、APatch 等)
|
||||||
|
|
||||||
|
> **注意**:如果您的APP不支持Zygisk注入,请额外安装 Zygisk-Next 模块,否则可能导致注入失败。
|
||||||
|
|
||||||
|
### 步骤二:准备SO文件并配置
|
||||||
|
|
||||||
|
#### 1. 添加SO文件
|
||||||
|
|
||||||
|
进入"SO文件管理",点击"增加SO文件":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
选择要注入的SO文件:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
本教程使用以下两个SO文件进行测试:
|
||||||
|
- `libmylib.so`(全注入测试)
|
||||||
|
- `libgadget.so`(普通注入方式)
|
||||||
|
|
||||||
|
> **提示**:后文将解释为什么自定义linker无法注入 `libgadget.so`。
|
||||||
|
|
||||||
|
#### 2. 处理原文件
|
||||||
|
|
||||||
|
选择SO后,系统会询问是否删除原文件:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **设计理念**:此功能可防止某些APP扫描tmp目录中的SO文件进行风控检测。
|
||||||
|
|
||||||
|
添加成功后,SO管理界面会显示已添加的文件:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 步骤三:配置目标APP
|
||||||
|
|
||||||
|
本教程以珍惜大佬的Hunter为例演示配置过程。
|
||||||
|
|
||||||
|
1. 点击空白处,配置注入参数:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. 保存配置后,开启注入开关:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 步骤四:验证注入效果
|
||||||
|
|
||||||
|
打开目标APP,查看日志输出:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
成功打印"我已经成功加载",表明注入成功。
|
||||||
|
|
||||||
|
#### 测试Gadget注入
|
||||||
|
|
||||||
|
切换到gadget进行测试:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **特性**:本注入模块支持同时注入多个SO文件。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
注入成功:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **说明**:由于使用的Hunter版本较旧,可能无法检测到注入。建议使用新版本进行测试。
|
||||||
|
|
||||||
|
#### 测试其他注入方式
|
||||||
|
|
||||||
|
测试Riru Hide功能:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **提示**:修改配置后,建议先关闭再开启注入开关,确保新配置生效。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Riru Hide成功生效。
|
||||||
|
|
||||||
|
测试自定义Linker加载:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
加载成功:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 编译指南
|
||||||
|
|
||||||
|
### 自动编译
|
||||||
|
|
||||||
|
参考项目中的 [CI配置文件](https://github.com/jiqiu2022/Zygisk-MyInjector/blob/main/.github/workflows/ci.yml)。
|
||||||
|
|
||||||
|
如果您fork了本项目,只需在本地修改代码并push,云端将自动编译。
|
||||||
|
|
||||||
|
### 本地编译
|
||||||
|
|
||||||
|
使用项目提供的 `build_all.sh` 脚本进行编译。
|
||||||
|
|
||||||
|
> **环境要求**:需要Java 17环境。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
### 注入流程详解
|
||||||
|
|
||||||
|
1. **SO文件管理**
|
||||||
|
- 用户选择SO文件后,APP自动将其复制到 `/data/adb/modules/module-name` 目录下进行统一管理
|
||||||
|
2. **配置生成**
|
||||||
|
- 完成APP配置后,管理器在 `/data/adb/modules/module-name` 下创建config文件
|
||||||
|
- 面具模块启动时读取此配置,执行SO加载
|
||||||
|
3. **注入激活**
|
||||||
|
- 开启注入时:APP将管理器中的SO复制到目标APP目录,并将config状态设置为开启
|
||||||
|
- 关闭注入时:APP自动删除已复制的SO文件,将config状态设置为关闭
|
||||||
|
## 为什么自定义linker不能注入gadget
|
||||||
|
|
||||||
|
```
|
||||||
|
frida_KjnwyG_detect_location: assertion failed: (our_range != null)
|
||||||
|
Bail out! Frida:ERROR:../lib/KjnwyG/KjnwyG.vala:809:frida_KjnwyG_detect_location: assertion failed: (our_range != null)
|
||||||
|
Aborted
|
||||||
|
```
|
||||||
|
|
||||||
|
因为gadget的init_array里面其中有一个函数,尝试使用maps和soinfo获取到自己模块的信息,防止递归调用。
|
||||||
|
|
||||||
|
但是自定义linker的soinfo和内存段都是自己分配的,导致gadget找不到自己,发生了断言失败,我们可以简单的去掉断言,但是可能造成其他bug,后续作者会和小佳一起修复。
|
||||||
|
|
||||||
|
当然还有一种复杂的方法,就是用注入的so的soinfo,替换成要注入的so的,多见于360等壳子,等后续作者会实现。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
原项目https://github.com/Perfare/Zygisk-Il2CppDumper
|
## 后续更新计划
|
||||||
|
|
||||||
本项目在原项目基础上做局部更改,请支持原项目作者劳动成果
|
- **深度隐藏**:联动内核模块,提供maps等更深层次的隐藏功能
|
||||||
|
- **用户体验优化**:
|
||||||
|
- 增加gadget配置一键生成功能
|
||||||
|
- 提供更友好的界面交互
|
||||||
|
- 支持批量配置管理
|
||||||
|
|
||||||
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`, 修改`GamePackageName`为游戏包名
|
|
||||||
3. 使用Android Studio运行gradle任务`:module:assembleRelease`编译,zip包会生成在`out`文件夹下
|
|
||||||
3. 在Magisk里安装模块
|
|
||||||
|
|
||||||
4. 将要注入的so放入到/data/local/tmp下修改为test.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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 584 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 916 KiB |
|
After Width: | Height: | Size: 902 KiB |
|
After Width: | Height: | Size: 832 KiB |
|
After Width: | Height: | Size: 870 KiB |
|
After Width: | Height: | Size: 906 KiB |
|
After Width: | Height: | Size: 926 KiB |
@@ -20,6 +20,7 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
156
build_all.sh
Executable file
@@ -0,0 +1,156 @@
|
|||||||
|
#!/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 需要)
|
||||||
|
if [ -d "template/magisk_module/META-INF" ]; then
|
||||||
|
cp -r template/magisk_module/META-INF $TEMP_DIR/
|
||||||
|
echo -e " ${GREEN}✓ 复制 META-INF${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗ 未找到 META-INF 模板${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 打包
|
||||||
|
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_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
417
configapp/src/main/java/com/jiqiu/configapp/AppListFragment.java
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
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.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
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);
|
||||||
|
RadioGroup injectionMethodGroup = dialogView.findViewById(R.id.injectionMethodGroup);
|
||||||
|
RadioButton radioStandardInjection = dialogView.findViewById(R.id.radioStandardInjection);
|
||||||
|
RadioButton radioRiruInjection = dialogView.findViewById(R.id.radioRiruInjection);
|
||||||
|
RadioButton radioCustomLinkerInjection = dialogView.findViewById(R.id.radioCustomLinkerInjection);
|
||||||
|
RadioGroup gadgetConfigGroup = dialogView.findViewById(R.id.gadgetConfigGroup);
|
||||||
|
RadioButton radioNoGadget = dialogView.findViewById(R.id.radioNoGadget);
|
||||||
|
RadioButton radioUseGlobalGadget = dialogView.findViewById(R.id.radioUseGlobalGadget);
|
||||||
|
RadioButton radioUseCustomGadget = dialogView.findViewById(R.id.radioUseCustomGadget);
|
||||||
|
TextView tvGlobalGadgetInfo = dialogView.findViewById(R.id.tvGlobalGadgetInfo);
|
||||||
|
com.google.android.material.button.MaterialButton btnConfigureGadget = dialogView.findViewById(R.id.btnConfigureGadget);
|
||||||
|
|
||||||
|
appIcon.setImageDrawable(appInfo.getAppIcon());
|
||||||
|
appName.setText(appInfo.getAppName());
|
||||||
|
packageName.setText(appInfo.getPackageName());
|
||||||
|
|
||||||
|
// Load current config
|
||||||
|
String injectionMethod = configManager.getAppInjectionMethod(appInfo.getPackageName());
|
||||||
|
if ("custom_linker".equals(injectionMethod)) {
|
||||||
|
radioCustomLinkerInjection.setChecked(true);
|
||||||
|
} else if ("riru".equals(injectionMethod)) {
|
||||||
|
radioRiruInjection.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioStandardInjection.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load gadget config
|
||||||
|
boolean useGlobalGadget = configManager.getAppUseGlobalGadget(appInfo.getPackageName());
|
||||||
|
ConfigManager.GadgetConfig appSpecificGadget = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||||
|
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
|
||||||
|
|
||||||
|
// Update global gadget info
|
||||||
|
if (globalGadget != null) {
|
||||||
|
String info = "全局: " + globalGadget.gadgetName;
|
||||||
|
if (globalGadget.mode.equals("server")) {
|
||||||
|
info += " (端口: " + globalGadget.port + ")";
|
||||||
|
}
|
||||||
|
tvGlobalGadgetInfo.setText(info);
|
||||||
|
} else {
|
||||||
|
tvGlobalGadgetInfo.setText("未配置全局Gadget");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial radio selection
|
||||||
|
if (!useGlobalGadget && appSpecificGadget != null) {
|
||||||
|
radioUseCustomGadget.setChecked(true);
|
||||||
|
btnConfigureGadget.setVisibility(View.VISIBLE);
|
||||||
|
btnConfigureGadget.setEnabled(true);
|
||||||
|
} else if (useGlobalGadget && globalGadget != null) {
|
||||||
|
radioUseGlobalGadget.setChecked(true);
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
radioNoGadget.setChecked(true);
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup gadget radio group listener
|
||||||
|
gadgetConfigGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (checkedId == R.id.radioNoGadget) {
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||||
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||||
|
} else if (checkedId == R.id.radioUseGlobalGadget) {
|
||||||
|
btnConfigureGadget.setVisibility(View.GONE);
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), true);
|
||||||
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), null);
|
||||||
|
} else if (checkedId == R.id.radioUseCustomGadget) {
|
||||||
|
btnConfigureGadget.setVisibility(View.VISIBLE);
|
||||||
|
btnConfigureGadget.setEnabled(true);
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure button listener
|
||||||
|
btnConfigureGadget.setOnClickListener(v -> {
|
||||||
|
ConfigManager.GadgetConfig currentConfig = null;
|
||||||
|
if (!useGlobalGadget) {
|
||||||
|
currentConfig = configManager.getAppGadgetConfig(appInfo.getPackageName());
|
||||||
|
}
|
||||||
|
if (currentConfig == null) {
|
||||||
|
currentConfig = new ConfigManager.GadgetConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
GadgetConfigDialog dialog = new GadgetConfigDialog(
|
||||||
|
getContext(),
|
||||||
|
"配置" + appInfo.getAppName() + "的Gadget",
|
||||||
|
currentConfig,
|
||||||
|
config -> {
|
||||||
|
configManager.setAppUseGlobalGadget(appInfo.getPackageName(), false);
|
||||||
|
configManager.setAppGadgetConfig(appInfo.getPackageName(), config);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dialog.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 injection method
|
||||||
|
String selectedMethod;
|
||||||
|
if (radioCustomLinkerInjection.isChecked()) {
|
||||||
|
selectedMethod = "custom_linker";
|
||||||
|
} else if (radioRiruInjection.isChecked()) {
|
||||||
|
selectedMethod = "riru";
|
||||||
|
} else {
|
||||||
|
selectedMethod = "standard";
|
||||||
|
}
|
||||||
|
configManager.setAppInjectionMethod(appInfo.getPackageName(), selectedMethod);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
954
configapp/src/main/java/com/jiqiu/configapp/ConfigManager.java
Normal file
@@ -0,0 +1,954 @@
|
|||||||
|
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";
|
||||||
|
public static final String KPM_MODULE_PATH = MODULE_PATH + "/injectHide.kpm";
|
||||||
|
public static final String KPM_HIDE_CONFIG = "/data/local/tmp/kpm_hide_config.txt";
|
||||||
|
private static final String KPM_MODULE_NAME = "hideInject";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Gson gson;
|
||||||
|
private ModuleConfig config;
|
||||||
|
private final Object kpmLock = new Object(); // 用于同步 KPM 操作
|
||||||
|
|
||||||
|
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<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep original filename
|
||||||
|
String fileName = new File(originalPath).getName();
|
||||||
|
String storedPath = SO_STORAGE_DIR + "/" + fileName;
|
||||||
|
|
||||||
|
// Check if file already exists with same name
|
||||||
|
for (SoFile existing : config.globalSoFiles) {
|
||||||
|
if (existing.name.equals(fileName)) {
|
||||||
|
Log.w(TAG, "SO file with same name already exists: " + fileName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure SO storage directory exists
|
||||||
|
Shell.cmd("mkdir -p " + SO_STORAGE_DIR).exec();
|
||||||
|
Shell.cmd("chmod 755 " + SO_STORAGE_DIR).exec();
|
||||||
|
|
||||||
|
// Copy SO file to our storage
|
||||||
|
Log.i(TAG, "Copying SO file from: " + originalPath + " to: " + storedPath);
|
||||||
|
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
// Verify the file was actually copied
|
||||||
|
Shell.Result verifyResult = Shell.cmd("test -f \"" + storedPath + "\" && echo 'exists'").exec();
|
||||||
|
if (!verifyResult.isSuccess() || verifyResult.getOut().isEmpty()) {
|
||||||
|
Log.e(TAG, "File copy appeared successful but file not found at: " + storedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proper permissions for SO file (readable and executable)
|
||||||
|
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + storedPath + "\"").exec();
|
||||||
|
if (!chmodResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set permissions on SO file: " + String.join("\n", chmodResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
SoFile soFile = new SoFile();
|
||||||
|
soFile.name = fileName;
|
||||||
|
soFile.storedPath = storedPath;
|
||||||
|
soFile.originalPath = originalPath;
|
||||||
|
config.globalSoFiles.add(soFile);
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully added SO file: " + fileName + " to storage");
|
||||||
|
|
||||||
|
if (deleteOriginal) {
|
||||||
|
Shell.cmd("rm \"" + originalPath + "\"").exec();
|
||||||
|
Log.i(TAG, "Deleted original file: " + originalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to copy SO file: " + String.join("\n", result.getErr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppInjectionMethod(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
return "standard"; // Default to standard
|
||||||
|
}
|
||||||
|
return appConfig.injectionMethod != null ? appConfig.injectionMethod : "standard";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppInjectionMethod(String packageName, String method) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
appConfig.injectionMethod = method;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInjectionDelay() {
|
||||||
|
return config.injectionDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInjectionDelay(int delay) {
|
||||||
|
config.injectionDelay = delay;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GadgetConfig getAppGadgetConfig(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
// If no app config, return global gadget config
|
||||||
|
return config.globalGadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If app is set to use global gadget, return global config
|
||||||
|
if (appConfig.useGlobalGadget) {
|
||||||
|
return config.globalGadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return app-specific gadget config
|
||||||
|
return appConfig.gadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GadgetConfig getGlobalGadgetConfig() {
|
||||||
|
return config.globalGadgetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlobalGadgetConfig(GadgetConfig gadgetConfig) {
|
||||||
|
config.globalGadgetConfig = gadgetConfig;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getAppUseGlobalGadget(String packageName) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
return true; // Default to use global
|
||||||
|
}
|
||||||
|
return appConfig.useGlobalGadget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppUseGlobalGadget(String packageName, boolean useGlobal) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
appConfig.useGlobalGadget = useGlobal;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppGadgetConfig(String packageName, GadgetConfig gadgetConfig) {
|
||||||
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
|
if (appConfig == null) {
|
||||||
|
appConfig = new AppConfig();
|
||||||
|
config.perAppConfig.put(packageName, appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old gadget from SO list if exists
|
||||||
|
if (appConfig.gadgetConfig != null) {
|
||||||
|
String oldGadgetName = appConfig.gadgetConfig.gadgetName;
|
||||||
|
appConfig.soFiles.removeIf(soFile -> soFile.name.equals(oldGadgetName));
|
||||||
|
}
|
||||||
|
|
||||||
|
appConfig.gadgetConfig = gadgetConfig;
|
||||||
|
|
||||||
|
// Add new gadget to SO list if configured
|
||||||
|
if (gadgetConfig != null) {
|
||||||
|
// Check if gadget SO file exists in global storage
|
||||||
|
String gadgetPath = SO_STORAGE_DIR + "/" + gadgetConfig.gadgetName;
|
||||||
|
Shell.Result checkResult = Shell.cmd("test -f \"" + gadgetPath + "\" && echo 'exists'").exec();
|
||||||
|
|
||||||
|
if (checkResult.isSuccess() && !checkResult.getOut().isEmpty()) {
|
||||||
|
// Add gadget as a SO file
|
||||||
|
SoFile gadgetSoFile = new SoFile();
|
||||||
|
gadgetSoFile.name = gadgetConfig.gadgetName;
|
||||||
|
gadgetSoFile.storedPath = gadgetPath;
|
||||||
|
gadgetSoFile.originalPath = gadgetPath;
|
||||||
|
|
||||||
|
// Check if already in list
|
||||||
|
boolean alreadyExists = false;
|
||||||
|
for (SoFile soFile : appConfig.soFiles) {
|
||||||
|
if (soFile.name.equals(gadgetSoFile.name)) {
|
||||||
|
alreadyExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
appConfig.soFiles.add(gadgetSoFile);
|
||||||
|
Log.i(TAG, "Added gadget SO to app's SO list: " + gadgetSoFile.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Gadget SO file not found in storage: " + gadgetPath);
|
||||||
|
Log.w(TAG, "Please ensure " + gadgetConfig.gadgetName + " is added to SO library");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
|
||||||
|
// If app is enabled, deploy both gadget SO and config file
|
||||||
|
if (appConfig.enabled) {
|
||||||
|
if (gadgetConfig != null) {
|
||||||
|
deployGadgetConfigFile(packageName, gadgetConfig);
|
||||||
|
}
|
||||||
|
// Re-deploy all SO files including gadget
|
||||||
|
deploySoFilesToApp(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deployGadgetConfigFile(String packageName, GadgetConfig gadgetConfig) {
|
||||||
|
try {
|
||||||
|
// Create gadget config JSON
|
||||||
|
String configJson;
|
||||||
|
if ("script".equals(gadgetConfig.mode)) {
|
||||||
|
configJson = String.format(
|
||||||
|
"{\n" +
|
||||||
|
" \"interaction\": {\n" +
|
||||||
|
" \"type\": \"script\",\n" +
|
||||||
|
" \"path\": \"%s\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}",
|
||||||
|
gadgetConfig.scriptPath
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
configJson = String.format(
|
||||||
|
"{\n" +
|
||||||
|
" \"interaction\": {\n" +
|
||||||
|
" \"type\": \"listen\",\n" +
|
||||||
|
" \"address\": \"%s\",\n" +
|
||||||
|
" \"port\": %d,\n" +
|
||||||
|
" \"on_port_conflict\": \"%s\",\n" +
|
||||||
|
" \"on_load\": \"%s\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}",
|
||||||
|
gadgetConfig.address,
|
||||||
|
gadgetConfig.port,
|
||||||
|
gadgetConfig.onPortConflict,
|
||||||
|
gadgetConfig.onLoad
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to temp file
|
||||||
|
String tempFile = context.getCacheDir() + "/" + gadgetConfig.gadgetName + ".config";
|
||||||
|
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||||
|
writer.write(configJson);
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
// Copy to app's files directory
|
||||||
|
String filesDir = "/data/data/" + packageName + "/files";
|
||||||
|
String gadgetConfigName = gadgetConfig.gadgetName.replace(".so", ".config.so");
|
||||||
|
String targetPath = filesDir + "/" + gadgetConfigName;
|
||||||
|
|
||||||
|
Shell.Result copyResult = Shell.cmd("cp " + tempFile + " " + targetPath).exec();
|
||||||
|
if (copyResult.isSuccess()) {
|
||||||
|
// Set permissions
|
||||||
|
Shell.cmd("chmod 644 " + targetPath).exec();
|
||||||
|
Log.i(TAG, "Deployed gadget config to: " + targetPath);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to deploy gadget config: " + String.join("\n", copyResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
new java.io.File(tempFile).delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to create gadget config file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
|
||||||
|
Log.i(TAG, "Deploying SO files to: " + filesDir);
|
||||||
|
|
||||||
|
// Create directory without su -c for better compatibility
|
||||||
|
Shell.Result mkdirResult = Shell.cmd("mkdir -p " + filesDir).exec();
|
||||||
|
if (!mkdirResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to create directory: " + filesDir);
|
||||||
|
Log.e(TAG, "Error: " + String.join("\n", mkdirResult.getErr()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proper permissions and ownership for the files directory
|
||||||
|
Shell.cmd("chmod 771 " + filesDir).exec();
|
||||||
|
|
||||||
|
// Get UID and GID 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);
|
||||||
|
|
||||||
|
// Set ownership of files directory to match app
|
||||||
|
Shell.Result chownDirResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + filesDir + "\"").exec();
|
||||||
|
if (!chownDirResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set directory ownership: " + String.join("\n", chownDirResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SELinux context for the directory
|
||||||
|
Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + filesDir + "\"").exec();
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to get package UID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy each SO file configured for this app
|
||||||
|
for (SoFile soFile : appConfig.soFiles) {
|
||||||
|
// Use original filename
|
||||||
|
String destPath = filesDir + "/" + soFile.name;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
// Log more details about the missing file
|
||||||
|
Shell.Result lsResult = Shell.cmd("ls -la \"" + SO_STORAGE_DIR + "\"").exec();
|
||||||
|
Log.e(TAG, "Contents of SO storage dir: " + String.join("\n", lsResult.getOut()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Copying: " + soFile.storedPath + " to " + destPath);
|
||||||
|
|
||||||
|
// First, ensure the destination directory exists and has proper permissions
|
||||||
|
Shell.cmd("mkdir -p \"" + filesDir + "\"").exec();
|
||||||
|
Shell.cmd("chmod 755 \"" + filesDir + "\"").exec();
|
||||||
|
|
||||||
|
// Copy file using cp with force flag
|
||||||
|
Shell.Result result = Shell.cmd("cp -f \"" + soFile.storedPath + "\" \"" + destPath + "\"").exec();
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed with cp, trying cat method");
|
||||||
|
Log.e(TAG, "cp error: " + String.join("\n", result.getErr()));
|
||||||
|
// Fallback to cat method
|
||||||
|
result = Shell.cmd("cat \"" + soFile.storedPath + "\" > \"" + destPath + "\"").exec();
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
Log.e(TAG, "Also failed with cat method");
|
||||||
|
Log.e(TAG, "cat error: " + String.join("\n", result.getErr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set permissions - SO files need to be readable and executable
|
||||||
|
Shell.Result chmodResult = Shell.cmd("chmod 755 \"" + destPath + "\"").exec();
|
||||||
|
if (!chmodResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set permissions: " + String.join("\n", chmodResult.getErr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ownership to match the app's UID
|
||||||
|
if (!uid.isEmpty()) {
|
||||||
|
Shell.Result chownResult = Shell.cmd("chown " + uid + ":" + uid + " \"" + destPath + "\"").exec();
|
||||||
|
if (!chownResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to set ownership: " + String.join("\n", chownResult.getErr()));
|
||||||
|
// Try alternative method
|
||||||
|
Shell.cmd("chown " + uid + ".app_" + uid + " \"" + destPath + "\"").exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SELinux context to match app's data files
|
||||||
|
Shell.Result contextResult = Shell.cmd("chcon u:object_r:app_data_file:s0 \"" + destPath + "\"").exec();
|
||||||
|
if (!contextResult.isSuccess()) {
|
||||||
|
Log.w(TAG, "Failed to set SELinux context (this may be normal on some devices)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file was copied with correct permissions
|
||||||
|
Shell.Result verifyResult = Shell.cmd("ls -laZ \"" + destPath + "\" 2>/dev/null").exec();
|
||||||
|
if (verifyResult.isSuccess() && !verifyResult.getOut().isEmpty()) {
|
||||||
|
Log.i(TAG, "Successfully deployed: " + String.join(" ", verifyResult.getOut()));
|
||||||
|
} else {
|
||||||
|
// Fallback verification without SELinux context
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Deployment complete for: " + packageName);
|
||||||
|
|
||||||
|
// Deploy gadget config if configured
|
||||||
|
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
|
||||||
|
if (gadgetToUse != null) {
|
||||||
|
deployGadgetConfigFile(packageName, gadgetToUse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Use original filename
|
||||||
|
String filePath = filesDir + "/" + soFile.name;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up gadget config file if exists
|
||||||
|
ConfigManager.GadgetConfig gadgetToUse = getAppGadgetConfig(packageName);
|
||||||
|
if (gadgetToUse != null) {
|
||||||
|
String gadgetConfigName = gadgetToUse.gadgetName.replace(".so", ".config.so");
|
||||||
|
String configPath = filesDir + "/" + gadgetConfigName;
|
||||||
|
|
||||||
|
Shell.Result checkConfigResult = Shell.cmd("test -f \"" + configPath + "\" && echo 'exists'").exec();
|
||||||
|
if (checkConfigResult.isSuccess() && !checkConfigResult.getOut().isEmpty()) {
|
||||||
|
Shell.Result deleteResult = Shell.cmd("rm -f \"" + configPath + "\"").exec();
|
||||||
|
if (deleteResult.isSuccess()) {
|
||||||
|
Log.i(TAG, "Deleted gadget config: " + configPath);
|
||||||
|
} else {
|
||||||
|
// Try with su -c
|
||||||
|
Shell.cmd("su -c 'rm -f \"" + configPath + "\"'").exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== KPM Module Management ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 KPM 模块是否已加载
|
||||||
|
*/
|
||||||
|
public boolean isKpmModuleLoaded() {
|
||||||
|
Shell.Result result = Shell.cmd("lsmod | grep " + KPM_MODULE_NAME).exec();
|
||||||
|
return result.isSuccess() && !result.getOut().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 KPM 模块
|
||||||
|
*/
|
||||||
|
public boolean loadKpmModule() {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
Log.e(TAG, "Root access not available!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if module file exists
|
||||||
|
Shell.Result checkResult = Shell.cmd("test -f \"" + KPM_MODULE_PATH + "\" && echo 'exists'").exec();
|
||||||
|
if (!checkResult.isSuccess() || checkResult.getOut().isEmpty()) {
|
||||||
|
Log.e(TAG, "KPM module file not found: " + KPM_MODULE_PATH);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already loaded
|
||||||
|
if (isKpmModuleLoaded()) {
|
||||||
|
Log.i(TAG, "KPM module already loaded");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load module
|
||||||
|
Shell.Result result = Shell.cmd("insmod \"" + KPM_MODULE_PATH + "\"").exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
Log.i(TAG, "KPM module loaded successfully");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to load KPM module: " + String.join("\n", result.getErr()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载 KPM 模块
|
||||||
|
*/
|
||||||
|
public boolean unloadKpmModule() {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
Log.e(TAG, "Root access not available!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isKpmModuleLoaded()) {
|
||||||
|
Log.i(TAG, "KPM module not loaded");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.Result result = Shell.cmd("rmmod " + KPM_MODULE_NAME).exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
Log.i(TAG, "KPM module unloaded successfully");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to unload KPM module: " + String.join("\n", result.getErr()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载 KPM 模块
|
||||||
|
*/
|
||||||
|
public boolean reloadKpmModule() {
|
||||||
|
// 使用锁防止并发重载
|
||||||
|
synchronized (kpmLock) {
|
||||||
|
Log.i(TAG, "Reloading KPM module...");
|
||||||
|
|
||||||
|
// Unload first
|
||||||
|
if (isKpmModuleLoaded()) {
|
||||||
|
if (!unloadKpmModule()) {
|
||||||
|
Log.e(TAG, "Failed to unload module before reload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give kernel more time to cleanup (1 second for safety)
|
||||||
|
// 等待时间足够让内核完全清理资源
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.w(TAG, "Sleep interrupted during module reload", e);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify unload was successful
|
||||||
|
int retries = 5;
|
||||||
|
for (int i = 0; i < retries; i++) {
|
||||||
|
if (!isKpmModuleLoaded()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Module still loaded, waiting... (" + (i+1) + "/" + retries + ")");
|
||||||
|
try {
|
||||||
|
Thread.sleep(200);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKpmModuleLoaded()) {
|
||||||
|
Log.e(TAG, "Module still loaded after unload attempts, aborting reload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load again
|
||||||
|
boolean success = loadKpmModule();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Log.i(TAG, "Module reloaded successfully");
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to load module after unload");
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} // synchronized
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 KPM 隐藏配置并重载模块
|
||||||
|
*/
|
||||||
|
public boolean updateKpmHideConfig(List<String> soNames) {
|
||||||
|
if (!isRootAvailable()) {
|
||||||
|
Log.e(TAG, "Root access not available!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build config content
|
||||||
|
StringBuilder configContent = new StringBuilder();
|
||||||
|
for (String soName : soNames) {
|
||||||
|
configContent.append(soName).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to temp file
|
||||||
|
String tempFile = context.getCacheDir() + "/kpm_hide_config.txt";
|
||||||
|
try {
|
||||||
|
java.io.FileWriter writer = new java.io.FileWriter(tempFile);
|
||||||
|
writer.write(configContent.toString());
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
// Ensure module directory exists
|
||||||
|
Shell.cmd("mkdir -p \"" + MODULE_PATH + "\"").exec();
|
||||||
|
|
||||||
|
// Ensure /data/local/tmp exists and is writable
|
||||||
|
Shell.cmd("mkdir -p /data/local/tmp && chmod 777 /data/local/tmp").exec();
|
||||||
|
|
||||||
|
// Copy to /data/local/tmp with root
|
||||||
|
Shell.Result copyResult = Shell.cmd("cp \"" + tempFile + "\" \"" + KPM_HIDE_CONFIG + "\"").exec();
|
||||||
|
if (!copyResult.isSuccess()) {
|
||||||
|
Log.e(TAG, "Failed to copy KPM config: " + String.join("\n", copyResult.getErr()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.cmd("chmod 666 \"" + KPM_HIDE_CONFIG + "\"").exec();
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
new File(tempFile).delete();
|
||||||
|
|
||||||
|
Log.i(TAG, "KPM hide config updated with " + soNames.size() + " entries");
|
||||||
|
|
||||||
|
// Reload module to apply changes
|
||||||
|
return reloadKpmModule();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to update KPM config", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前隐藏的 SO 列表
|
||||||
|
*/
|
||||||
|
public List<String> getHiddenSoList() {
|
||||||
|
List<String> hiddenList = new ArrayList<>();
|
||||||
|
|
||||||
|
Shell.Result result = Shell.cmd("cat \"" + KPM_HIDE_CONFIG + "\"").exec();
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
for (String line : result.getOut()) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
hiddenList.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hiddenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 SO 到隐藏列表
|
||||||
|
*/
|
||||||
|
public boolean addSoToHideList(String soName) {
|
||||||
|
List<String> hiddenList = getHiddenSoList();
|
||||||
|
if (!hiddenList.contains(soName)) {
|
||||||
|
hiddenList.add(soName);
|
||||||
|
return updateKpmHideConfig(hiddenList);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从隐藏列表移除 SO
|
||||||
|
*/
|
||||||
|
public boolean removeSoFromHideList(String soName) {
|
||||||
|
List<String> hiddenList = getHiddenSoList();
|
||||||
|
if (hiddenList.remove(soName)) {
|
||||||
|
return updateKpmHideConfig(hiddenList);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可隐藏的 SO 文件列表(从已启用应用中提取)
|
||||||
|
*/
|
||||||
|
public List<String> getAvailableSoList() {
|
||||||
|
List<String> availableSos = new ArrayList<>();
|
||||||
|
|
||||||
|
// Always include our injector library
|
||||||
|
availableSos.add("libmyinjector.so");
|
||||||
|
|
||||||
|
// Add SOs from all enabled apps
|
||||||
|
for (Map.Entry<String, AppConfig> entry : config.perAppConfig.entrySet()) {
|
||||||
|
AppConfig appConfig = entry.getValue();
|
||||||
|
if (appConfig.enabled && appConfig.soFiles != null) {
|
||||||
|
for (SoFile soFile : appConfig.soFiles) {
|
||||||
|
if (!availableSos.contains(soFile.name)) {
|
||||||
|
availableSos.add(soFile.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add global SO files
|
||||||
|
if (config.globalSoFiles != null) {
|
||||||
|
for (SoFile soFile : config.globalSoFiles) {
|
||||||
|
if (!availableSos.contains(soFile.name)) {
|
||||||
|
availableSos.add(soFile.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableSos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data classes
|
||||||
|
public static class ModuleConfig {
|
||||||
|
public boolean enabled = true;
|
||||||
|
public boolean hideInjection = false;
|
||||||
|
public int injectionDelay = 2; // Default 2 seconds
|
||||||
|
public List<SoFile> globalSoFiles = new ArrayList<>();
|
||||||
|
public Map<String, AppConfig> perAppConfig = new HashMap<>();
|
||||||
|
public GadgetConfig globalGadgetConfig = null; // Global gadget configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AppConfig {
|
||||||
|
public boolean enabled = false;
|
||||||
|
public List<SoFile> soFiles = new ArrayList<>();
|
||||||
|
public String injectionMethod = "standard"; // "standard", "riru" or "custom_linker"
|
||||||
|
public GadgetConfig gadgetConfig = null;
|
||||||
|
public boolean useGlobalGadget = true; // Whether to use global gadget settings
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GadgetConfig {
|
||||||
|
public String mode = "server"; // "server" or "script"
|
||||||
|
// Server mode config
|
||||||
|
public String address = "0.0.0.0";
|
||||||
|
public int port = 27042;
|
||||||
|
public String onPortConflict = "fail";
|
||||||
|
public String onLoad = "wait";
|
||||||
|
// Script mode config
|
||||||
|
public String scriptPath = "/data/local/tmp/script.js";
|
||||||
|
// Common config
|
||||||
|
public String gadgetName = "libgadget.so";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
configapp/src/main/java/com/jiqiu/configapp/FileUtils.java
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class FileUtils {
|
||||||
|
private static final String TAG = "FileUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get real file path from URI, handling both file:// and content:// URIs
|
||||||
|
* @param context Context
|
||||||
|
* @param uri The URI to resolve
|
||||||
|
* @return The real file path, or null if unable to resolve
|
||||||
|
*/
|
||||||
|
public static String getRealPathFromUri(Context context, Uri uri) {
|
||||||
|
if (uri == null) return null;
|
||||||
|
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
if (scheme == null) return null;
|
||||||
|
|
||||||
|
// Handle file:// URIs
|
||||||
|
if ("file".equals(scheme)) {
|
||||||
|
return uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle content:// URIs
|
||||||
|
if ("content".equals(scheme)) {
|
||||||
|
// For content URIs, we need to copy the file to a temporary location
|
||||||
|
return copyFileFromContentUri(context, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct path extraction as fallback
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
// Some file managers return paths like /external_files/...
|
||||||
|
// Try to resolve these to actual paths
|
||||||
|
if (path.contains(":")) {
|
||||||
|
String[] parts = path.split(":");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
String type = parts[0];
|
||||||
|
String relativePath = parts[1];
|
||||||
|
|
||||||
|
// Common storage locations
|
||||||
|
if (type.endsWith("/primary")) {
|
||||||
|
return "/storage/emulated/0/" + relativePath;
|
||||||
|
} else if (type.contains("external")) {
|
||||||
|
return "/storage/emulated/0/" + relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any file:// prefix
|
||||||
|
if (path.startsWith("file://")) {
|
||||||
|
path = path.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path exists
|
||||||
|
Shell.Result result = Shell.cmd("test -f \"" + path + "\" && echo 'exists'").exec();
|
||||||
|
if (result.isSuccess() && !result.getOut().isEmpty()) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a file from content URI to temporary location
|
||||||
|
* @param context Context
|
||||||
|
* @param uri Content URI
|
||||||
|
* @return Path to copied file, or null on failure
|
||||||
|
*/
|
||||||
|
private static String copyFileFromContentUri(Context context, Uri uri) {
|
||||||
|
ContentResolver resolver = context.getContentResolver();
|
||||||
|
String fileName = getFileName(context, uri);
|
||||||
|
|
||||||
|
if (fileName == null || !fileName.endsWith(".so")) {
|
||||||
|
fileName = "temp_" + System.currentTimeMillis() + ".so";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temp directory
|
||||||
|
File tempDir = new File(context.getCacheDir(), "so_temp");
|
||||||
|
if (!tempDir.exists()) {
|
||||||
|
tempDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
File tempFile = new File(tempDir, fileName);
|
||||||
|
|
||||||
|
try (InputStream inputStream = resolver.openInputStream(uri);
|
||||||
|
OutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||||
|
|
||||||
|
if (inputStream == null) {
|
||||||
|
Log.e(TAG, "Unable to open input stream for URI: " + uri);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make file readable
|
||||||
|
tempFile.setReadable(true, false);
|
||||||
|
|
||||||
|
// First copy to /data/local/tmp as a temporary location
|
||||||
|
String tempTargetPath = "/data/local/tmp/" + fileName;
|
||||||
|
Shell.Result result = Shell.cmd(
|
||||||
|
"cp \"" + tempFile.getAbsolutePath() + "\" \"" + tempTargetPath + "\"",
|
||||||
|
"chmod 644 \"" + tempTargetPath + "\""
|
||||||
|
).exec();
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
tempFile.delete();
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
// Return the temporary path - it will be moved to the proper location by addGlobalSoFile
|
||||||
|
return tempTargetPath;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to copy file to /data/local/tmp/");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Error copying file from content URI", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file name from URI
|
||||||
|
* @param context Context
|
||||||
|
* @param uri URI to get name from
|
||||||
|
* @return File name or null
|
||||||
|
*/
|
||||||
|
private static String getFileName(Context context, Uri uri) {
|
||||||
|
String fileName = null;
|
||||||
|
|
||||||
|
if ("content".equals(uri.getScheme())) {
|
||||||
|
try (Cursor cursor = context.getContentResolver().query(
|
||||||
|
uri, null, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
if (nameIndex >= 0) {
|
||||||
|
fileName = cursor.getString(nameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting file name from URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName == null) {
|
||||||
|
fileName = uri.getLastPathSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,676 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.database.Cursor;
|
||||||
|
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.DialogFragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class GadgetConfigDialog extends DialogFragment {
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
private RadioGroup modeRadioGroup;
|
||||||
|
private RadioButton radioModeServer;
|
||||||
|
private RadioButton radioModeScript;
|
||||||
|
private LinearLayout serverModeLayout;
|
||||||
|
private LinearLayout scriptModeLayout;
|
||||||
|
private RadioGroup addressRadioGroup;
|
||||||
|
private RadioButton radioAddressAll;
|
||||||
|
private RadioButton radioAddressLocal;
|
||||||
|
private RadioButton radioAddressCustom;
|
||||||
|
private EditText editCustomAddress;
|
||||||
|
private EditText editPort;
|
||||||
|
private RadioGroup portConflictRadioGroup;
|
||||||
|
private RadioButton radioConflictFail;
|
||||||
|
private RadioButton radioConflictPickNext;
|
||||||
|
private RadioGroup onLoadRadioGroup;
|
||||||
|
private RadioButton radioLoadWait;
|
||||||
|
private RadioButton radioLoadResume;
|
||||||
|
private EditText editScriptPath;
|
||||||
|
private EditText editGadgetName;
|
||||||
|
private EditText editJsonPreview;
|
||||||
|
|
||||||
|
// Configuration data
|
||||||
|
private ConfigManager.GadgetConfig config;
|
||||||
|
private OnGadgetConfigListener listener;
|
||||||
|
private String customTitle;
|
||||||
|
|
||||||
|
// Flag to prevent recursive updates
|
||||||
|
private boolean isUpdatingUI = false;
|
||||||
|
|
||||||
|
// Activity result launchers
|
||||||
|
private ActivityResultLauncher<Intent> fileBrowserLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> filePickerLauncher;
|
||||||
|
|
||||||
|
public interface OnGadgetConfigListener {
|
||||||
|
void onGadgetConfigSaved(ConfigManager.GadgetConfig config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GadgetConfigDialog newInstance(ConfigManager.GadgetConfig config) {
|
||||||
|
GadgetConfigDialog dialog = new GadgetConfigDialog();
|
||||||
|
dialog.config = config != null ? config : new ConfigManager.GadgetConfig();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for non-fragment usage
|
||||||
|
public GadgetConfigDialog(Context context, String title, ConfigManager.GadgetConfig config, OnGadgetConfigListener listener) {
|
||||||
|
// This constructor is for compatibility with direct dialog creation
|
||||||
|
// The actual dialog will be created in show() method
|
||||||
|
this.savedContext = context;
|
||||||
|
this.customTitle = title;
|
||||||
|
this.config = config != null ? config : new ConfigManager.GadgetConfig();
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default constructor required for DialogFragment
|
||||||
|
public GadgetConfigDialog() {
|
||||||
|
// Empty constructor required
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnGadgetConfigListener(OnGadgetConfigListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Initialize file browser launcher
|
||||||
|
fileBrowserLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
String selectedPath = result.getData().getStringExtra("selected_path");
|
||||||
|
if (selectedPath != null) {
|
||||||
|
editScriptPath.setText(selectedPath);
|
||||||
|
config.scriptPath = selectedPath;
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize file picker launcher
|
||||||
|
filePickerLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == android.app.Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
Uri uri = result.getData().getData();
|
||||||
|
if (uri != null) {
|
||||||
|
String path = getPathFromUri(uri);
|
||||||
|
if (path != null) {
|
||||||
|
editScriptPath.setText(path);
|
||||||
|
config.scriptPath = path;
|
||||||
|
updateJsonPreview();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "无法获取文件路径", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||||
|
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
|
||||||
|
|
||||||
|
initViews(view);
|
||||||
|
loadConfig();
|
||||||
|
setupListeners();
|
||||||
|
updateJsonPreview();
|
||||||
|
|
||||||
|
String title = customTitle != null ? customTitle : "Gadget 配置";
|
||||||
|
|
||||||
|
return new MaterialAlertDialogBuilder(getContext())
|
||||||
|
.setTitle(title)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews(View view) {
|
||||||
|
modeRadioGroup = view.findViewById(R.id.modeRadioGroup);
|
||||||
|
radioModeServer = view.findViewById(R.id.radioModeServer);
|
||||||
|
radioModeScript = view.findViewById(R.id.radioModeScript);
|
||||||
|
serverModeLayout = view.findViewById(R.id.serverModeLayout);
|
||||||
|
scriptModeLayout = view.findViewById(R.id.scriptModeLayout);
|
||||||
|
addressRadioGroup = view.findViewById(R.id.addressRadioGroup);
|
||||||
|
radioAddressAll = view.findViewById(R.id.radioAddressAll);
|
||||||
|
radioAddressLocal = view.findViewById(R.id.radioAddressLocal);
|
||||||
|
radioAddressCustom = view.findViewById(R.id.radioAddressCustom);
|
||||||
|
editCustomAddress = view.findViewById(R.id.editCustomAddress);
|
||||||
|
editPort = view.findViewById(R.id.editPort);
|
||||||
|
portConflictRadioGroup = view.findViewById(R.id.portConflictRadioGroup);
|
||||||
|
radioConflictFail = view.findViewById(R.id.radioConflictFail);
|
||||||
|
radioConflictPickNext = view.findViewById(R.id.radioConflictPickNext);
|
||||||
|
onLoadRadioGroup = view.findViewById(R.id.onLoadRadioGroup);
|
||||||
|
radioLoadWait = view.findViewById(R.id.radioLoadWait);
|
||||||
|
radioLoadResume = view.findViewById(R.id.radioLoadResume);
|
||||||
|
editScriptPath = view.findViewById(R.id.editScriptPath);
|
||||||
|
editGadgetName = view.findViewById(R.id.editGadgetName);
|
||||||
|
editJsonPreview = view.findViewById(R.id.editJsonPreview);
|
||||||
|
|
||||||
|
// File select button
|
||||||
|
View btnSelectScript = view.findViewById(R.id.btnSelectScript);
|
||||||
|
if (btnSelectScript != null) {
|
||||||
|
btnSelectScript.setOnClickListener(v -> selectScriptFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
isUpdatingUI = true;
|
||||||
|
|
||||||
|
// Load mode
|
||||||
|
if ("script".equals(config.mode)) {
|
||||||
|
radioModeScript.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.GONE);
|
||||||
|
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
radioModeServer.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
scriptModeLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load address
|
||||||
|
if ("127.0.0.1".equals(config.address)) {
|
||||||
|
radioAddressLocal.setChecked(true);
|
||||||
|
} else if ("0.0.0.0".equals(config.address)) {
|
||||||
|
radioAddressAll.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioAddressCustom.setChecked(true);
|
||||||
|
editCustomAddress.setText(config.address);
|
||||||
|
editCustomAddress.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load port
|
||||||
|
editPort.setText(String.valueOf(config.port));
|
||||||
|
|
||||||
|
// Load port conflict handling
|
||||||
|
if ("pick-next".equals(config.onPortConflict)) {
|
||||||
|
radioConflictPickNext.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioConflictFail.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load on load handling
|
||||||
|
if ("resume".equals(config.onLoad)) {
|
||||||
|
radioLoadResume.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioLoadWait.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load script path
|
||||||
|
editScriptPath.setText(config.scriptPath);
|
||||||
|
|
||||||
|
// Load gadget name
|
||||||
|
editGadgetName.setText(config.gadgetName);
|
||||||
|
|
||||||
|
isUpdatingUI = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupListeners() {
|
||||||
|
// Mode radio group listener
|
||||||
|
modeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
if (checkedId == R.id.radioModeScript) {
|
||||||
|
config.mode = "script";
|
||||||
|
serverModeLayout.setVisibility(View.GONE);
|
||||||
|
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
config.mode = "server";
|
||||||
|
serverModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
scriptModeLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Address radio group listener
|
||||||
|
addressRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
if (checkedId == R.id.radioAddressCustom) {
|
||||||
|
editCustomAddress.setEnabled(true);
|
||||||
|
editCustomAddress.requestFocus();
|
||||||
|
} else {
|
||||||
|
editCustomAddress.setEnabled(false);
|
||||||
|
if (checkedId == R.id.radioAddressAll) {
|
||||||
|
config.address = "0.0.0.0";
|
||||||
|
} else if (checkedId == R.id.radioAddressLocal) {
|
||||||
|
config.address = "127.0.0.1";
|
||||||
|
}
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom address text watcher
|
||||||
|
editCustomAddress.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) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI && radioAddressCustom.isChecked()) {
|
||||||
|
config.address = s.toString().trim();
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Port text watcher
|
||||||
|
editPort.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) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
try {
|
||||||
|
int port = Integer.parseInt(s.toString());
|
||||||
|
if (port >= 1 && port <= 65535) {
|
||||||
|
config.port = port;
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore invalid input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Port conflict radio group listener
|
||||||
|
portConflictRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.onPortConflict = (checkedId == R.id.radioConflictPickNext) ? "pick-next" : "fail";
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// On load radio group listener
|
||||||
|
onLoadRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.onLoad = (checkedId == R.id.radioLoadResume) ? "resume" : "wait";
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Script path text watcher
|
||||||
|
editScriptPath.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) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.scriptPath = s.toString().trim();
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gadget name text watcher
|
||||||
|
editGadgetName.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) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
config.gadgetName = s.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON preview text watcher
|
||||||
|
editJsonPreview.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) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (!isUpdatingUI) {
|
||||||
|
parseJsonAndUpdateUI(s.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateJsonPreview() {
|
||||||
|
if (isUpdatingUI) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject root = new JSONObject();
|
||||||
|
JSONObject interaction = new JSONObject();
|
||||||
|
|
||||||
|
if ("script".equals(config.mode)) {
|
||||||
|
interaction.put("type", "script");
|
||||||
|
interaction.put("path", config.scriptPath);
|
||||||
|
} else {
|
||||||
|
interaction.put("type", "listen");
|
||||||
|
interaction.put("address", config.address);
|
||||||
|
interaction.put("port", config.port);
|
||||||
|
interaction.put("on_port_conflict", config.onPortConflict);
|
||||||
|
interaction.put("on_load", config.onLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.put("interaction", interaction);
|
||||||
|
|
||||||
|
isUpdatingUI = true;
|
||||||
|
editJsonPreview.setText(root.toString(2));
|
||||||
|
isUpdatingUI = false;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// Should not happen
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseJsonAndUpdateUI(String json) {
|
||||||
|
try {
|
||||||
|
JSONObject root = new JSONObject(json);
|
||||||
|
JSONObject interaction = root.getJSONObject("interaction");
|
||||||
|
|
||||||
|
isUpdatingUI = true;
|
||||||
|
|
||||||
|
// Update mode
|
||||||
|
String type = interaction.getString("type");
|
||||||
|
if ("script".equals(type)) {
|
||||||
|
config.mode = "script";
|
||||||
|
radioModeScript.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.GONE);
|
||||||
|
scriptModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Update script path
|
||||||
|
if (interaction.has("path")) {
|
||||||
|
config.scriptPath = interaction.getString("path");
|
||||||
|
editScriptPath.setText(config.scriptPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.mode = "server";
|
||||||
|
radioModeServer.setChecked(true);
|
||||||
|
serverModeLayout.setVisibility(View.VISIBLE);
|
||||||
|
scriptModeLayout.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
// Update address
|
||||||
|
String address = interaction.getString("address");
|
||||||
|
config.address = address;
|
||||||
|
if ("0.0.0.0".equals(address)) {
|
||||||
|
radioAddressAll.setChecked(true);
|
||||||
|
editCustomAddress.setEnabled(false);
|
||||||
|
} else if ("127.0.0.1".equals(address)) {
|
||||||
|
radioAddressLocal.setChecked(true);
|
||||||
|
editCustomAddress.setEnabled(false);
|
||||||
|
} else {
|
||||||
|
radioAddressCustom.setChecked(true);
|
||||||
|
editCustomAddress.setText(address);
|
||||||
|
editCustomAddress.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update port
|
||||||
|
config.port = interaction.getInt("port");
|
||||||
|
editPort.setText(String.valueOf(config.port));
|
||||||
|
|
||||||
|
// Update port conflict
|
||||||
|
String onPortConflict = interaction.getString("on_port_conflict");
|
||||||
|
config.onPortConflict = onPortConflict;
|
||||||
|
if ("pick-next".equals(onPortConflict)) {
|
||||||
|
radioConflictPickNext.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioConflictFail.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update on load
|
||||||
|
String onLoad = interaction.getString("on_load");
|
||||||
|
config.onLoad = onLoad;
|
||||||
|
if ("resume".equals(onLoad)) {
|
||||||
|
radioLoadResume.setChecked(true);
|
||||||
|
} else {
|
||||||
|
radioLoadWait.setChecked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingUI = false;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// Invalid JSON, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveConfig() {
|
||||||
|
if (listener != null) {
|
||||||
|
// Ensure gadget name is not empty
|
||||||
|
if (config.gadgetName == null || config.gadgetName.trim().isEmpty()) {
|
||||||
|
config.gadgetName = "libgadget.so";
|
||||||
|
}
|
||||||
|
listener.onGadgetConfigSaved(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectScriptFile() {
|
||||||
|
String[] options = {"浏览文件系统", "从外部文件管理器选择", "手动输入路径"};
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("选择 Script 文件")
|
||||||
|
.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, ".js");
|
||||||
|
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, ".js");
|
||||||
|
fileBrowserLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openFilePicker() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
// Add MIME types that might help filter JS files
|
||||||
|
String[] mimeTypes = {"text/javascript", "application/javascript", "text/plain", "*/*"};
|
||||||
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||||
|
// Suggest starting location
|
||||||
|
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||||
|
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||||
|
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/script.js");
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("输入 Script 文件路径")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("确定", (dialog, which) -> {
|
||||||
|
String path = editText.getText().toString().trim();
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
editScriptPath.setText(path);
|
||||||
|
config.scriptPath = path;
|
||||||
|
updateJsonPreview();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show method for non-fragment usage
|
||||||
|
public void show() {
|
||||||
|
if (getContext() == null) {
|
||||||
|
throw new IllegalStateException("Context is required for non-fragment usage");
|
||||||
|
}
|
||||||
|
|
||||||
|
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_gadget_config, null);
|
||||||
|
initViews(view);
|
||||||
|
|
||||||
|
// Initialize config if null
|
||||||
|
if (config == null) {
|
||||||
|
config = new ConfigManager.GadgetConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
setupListeners();
|
||||||
|
updateJsonPreview();
|
||||||
|
|
||||||
|
String title = customTitle != null ? customTitle : "Gadget 配置";
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(getContext())
|
||||||
|
.setTitle(title)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("保存", (dialog, which) -> saveConfig())
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Context savedContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Context getContext() {
|
||||||
|
Context context = super.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
return savedContext;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for non-fragment usage needs to save context
|
||||||
|
public void setContext(Context context) {
|
||||||
|
this.savedContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPathFromUri(Uri uri) {
|
||||||
|
String path = null;
|
||||||
|
|
||||||
|
// Try to get path from MediaStore
|
||||||
|
if ("content".equals(uri.getScheme())) {
|
||||||
|
try {
|
||||||
|
ContentResolver resolver = getContext().getContentResolver();
|
||||||
|
try (Cursor cursor = resolver.query(uri, new String[]{"_data"}, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int columnIndex = cursor.getColumnIndex("_data");
|
||||||
|
if (columnIndex != -1) {
|
||||||
|
path = cursor.getString(columnIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try DocumentsContract if MediaStore fails
|
||||||
|
if (path == null && DocumentsContract.isDocumentUri(getContext(), uri)) {
|
||||||
|
try {
|
||||||
|
String docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
if (uri.getAuthority().equals("com.android.externalstorage.documents")) {
|
||||||
|
String[] split = docId.split(":");
|
||||||
|
if (split.length >= 2) {
|
||||||
|
String type = split[0];
|
||||||
|
if ("primary".equalsIgnoreCase(type)) {
|
||||||
|
path = "/storage/emulated/0/" + split[1];
|
||||||
|
} else {
|
||||||
|
path = "/storage/" + type + "/" + split[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("file".equals(uri.getScheme())) {
|
||||||
|
path = uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
configapp/src/main/java/com/jiqiu/configapp/HideSoAdapter.java
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SO 文件隐藏列表适配器
|
||||||
|
*/
|
||||||
|
public class HideSoAdapter extends RecyclerView.Adapter<HideSoAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private final List<KpmHideFragment.HideSoItem> items;
|
||||||
|
private final OnItemCheckedChangeListener listener;
|
||||||
|
|
||||||
|
public interface OnItemCheckedChangeListener {
|
||||||
|
void onItemCheckedChanged(KpmHideFragment.HideSoItem item, boolean isChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HideSoAdapter(List<KpmHideFragment.HideSoItem> items,
|
||||||
|
OnItemCheckedChangeListener listener) {
|
||||||
|
this.items = items;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_hide_so, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
KpmHideFragment.HideSoItem item = items.get(position);
|
||||||
|
holder.bind(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
private final CheckBox cbHide;
|
||||||
|
private final TextView tvSoName;
|
||||||
|
private final TextView tvSoStatus;
|
||||||
|
private final TextView tvFixedBadge;
|
||||||
|
|
||||||
|
public ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
cbHide = itemView.findViewById(R.id.cbHide);
|
||||||
|
tvSoName = itemView.findViewById(R.id.tvSoName);
|
||||||
|
tvSoStatus = itemView.findViewById(R.id.tvSoStatus);
|
||||||
|
tvFixedBadge = itemView.findViewById(R.id.tvFixedBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(KpmHideFragment.HideSoItem item) {
|
||||||
|
tvSoName.setText(item.soName);
|
||||||
|
|
||||||
|
// 设置勾选状态
|
||||||
|
cbHide.setOnCheckedChangeListener(null); // 先移除监听器避免触发
|
||||||
|
cbHide.setChecked(item.isHidden);
|
||||||
|
|
||||||
|
// 显示状态
|
||||||
|
if (item.isFixed) {
|
||||||
|
tvSoStatus.setText("必需项 - 始终隐藏");
|
||||||
|
tvSoStatus.setTextColor(itemView.getContext().getResources()
|
||||||
|
.getColor(android.R.color.holo_green_dark, null));
|
||||||
|
tvFixedBadge.setVisibility(View.VISIBLE);
|
||||||
|
cbHide.setEnabled(false);
|
||||||
|
cbHide.setChecked(true); // 固定项始终勾选
|
||||||
|
} else {
|
||||||
|
tvSoStatus.setText(item.isHidden ? "已隐藏" : "未隐藏");
|
||||||
|
tvSoStatus.setTextColor(itemView.getContext().getResources()
|
||||||
|
.getColor(android.R.color.darker_gray, null));
|
||||||
|
tvFixedBadge.setVisibility(View.GONE);
|
||||||
|
cbHide.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置点击监听器
|
||||||
|
cbHide.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
if (listener != null && !item.isFixed) {
|
||||||
|
listener.onItemCheckedChanged(item, isChecked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 整行点击也触发勾选
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
if (!item.isFixed) {
|
||||||
|
cbHide.setChecked(!cbHide.isChecked());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
249
configapp/src/main/java/com/jiqiu/configapp/KpmHideFragment.java
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package com.jiqiu.configapp;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
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.card.MaterialCardView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KPM 隐藏功能 Fragment
|
||||||
|
* 管理 KPM 内核模块和 SO 文件隐藏配置
|
||||||
|
*/
|
||||||
|
public class KpmHideFragment extends Fragment {
|
||||||
|
|
||||||
|
private static final String TAG = "KpmHideFragment";
|
||||||
|
|
||||||
|
private TextView tvModuleStatus;
|
||||||
|
private TextView tvModuleInfo;
|
||||||
|
private TextView tvConfigPath;
|
||||||
|
private Button btnReloadModule;
|
||||||
|
private RecyclerView rvSoList;
|
||||||
|
|
||||||
|
private ConfigManager configManager;
|
||||||
|
private HideSoAdapter adapter;
|
||||||
|
private ExecutorService executor;
|
||||||
|
private Handler mainHandler;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_kpm_hide, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
initViews(view);
|
||||||
|
initExecutor();
|
||||||
|
initConfigManager();
|
||||||
|
setupListeners();
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initViews(View view) {
|
||||||
|
tvModuleStatus = view.findViewById(R.id.tvModuleStatus);
|
||||||
|
tvModuleInfo = view.findViewById(R.id.tvModuleInfo);
|
||||||
|
tvConfigPath = view.findViewById(R.id.tvConfigPath);
|
||||||
|
btnReloadModule = view.findViewById(R.id.btnReloadModule);
|
||||||
|
rvSoList = view.findViewById(R.id.rvSoList);
|
||||||
|
|
||||||
|
rvSoList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initExecutor() {
|
||||||
|
executor = Executors.newSingleThreadExecutor();
|
||||||
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initConfigManager() {
|
||||||
|
configManager = new ConfigManager(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupListeners() {
|
||||||
|
btnReloadModule.setOnClickListener(v -> reloadModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadData() {
|
||||||
|
// 显示配置路径
|
||||||
|
tvConfigPath.setText("配置文件: " + ConfigManager.KPM_HIDE_CONFIG);
|
||||||
|
|
||||||
|
// 异步加载模块状态和 SO 列表
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
final boolean isLoaded = configManager.isKpmModuleLoaded();
|
||||||
|
final List<String> availableSos = configManager.getAvailableSoList();
|
||||||
|
final List<String> hiddenSos = configManager.getHiddenSoList();
|
||||||
|
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
updateModuleStatus(isLoaded);
|
||||||
|
setupSoList(availableSos, hiddenSos);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error loading data", e);
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
Toast.makeText(getContext(), "加载数据失败: " + e.getMessage(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateModuleStatus(boolean isLoaded) {
|
||||||
|
if (isLoaded) {
|
||||||
|
tvModuleStatus.setText("● 已加载");
|
||||||
|
tvModuleStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark, null));
|
||||||
|
tvModuleInfo.setText("KPM 内核模块运行中\n模块名称: hideInject\n版本: 0.0.1");
|
||||||
|
} else {
|
||||||
|
tvModuleStatus.setText("● 未加载");
|
||||||
|
tvModuleStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark, null));
|
||||||
|
tvModuleInfo.setText("KPM 内核模块未运行\n请检查模块文件是否存在或手动重载");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupSoList(List<String> availableSos, List<String> hiddenSos) {
|
||||||
|
List<HideSoItem> items = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String soName : availableSos) {
|
||||||
|
HideSoItem item = new HideSoItem();
|
||||||
|
item.soName = soName;
|
||||||
|
item.isHidden = hiddenSos.contains(soName);
|
||||||
|
// libmyinjector.so 是固定隐藏的
|
||||||
|
item.isFixed = soName.equals("libmyinjector.so");
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = new HideSoAdapter(items, this::onSoItemCheckedChanged);
|
||||||
|
rvSoList.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSoItemCheckedChanged(HideSoItem item, boolean isChecked) {
|
||||||
|
if (item.isFixed) {
|
||||||
|
// 固定项不允许取消勾选
|
||||||
|
Toast.makeText(getContext(), "libmyinjector.so 是必需的,不能取消隐藏",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步更新配置
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
boolean success;
|
||||||
|
if (isChecked) {
|
||||||
|
success = configManager.addSoToHideList(item.soName);
|
||||||
|
} else {
|
||||||
|
success = configManager.removeSoFromHideList(item.soName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean finalSuccess = success;
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
if (finalSuccess) {
|
||||||
|
item.isHidden = isChecked;
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
isChecked ? "已添加到隐藏列表" : "已从隐藏列表移除",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
// 更新模块状态
|
||||||
|
refreshModuleStatus();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "更新失败,请检查日志",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
// 恢复原来的状态
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error updating SO hide status", e);
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
Toast.makeText(getContext(), "更新失败: " + e.getMessage(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadModule() {
|
||||||
|
btnReloadModule.setEnabled(false);
|
||||||
|
btnReloadModule.setText("重载中...");
|
||||||
|
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
final boolean success = configManager.reloadKpmModule();
|
||||||
|
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
btnReloadModule.setEnabled(true);
|
||||||
|
btnReloadModule.setText("重载模块");
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Toast.makeText(getContext(), "模块重载成功", Toast.LENGTH_SHORT).show();
|
||||||
|
refreshModuleStatus();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "模块重载失败,请查看日志",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error reloading module", e);
|
||||||
|
mainHandler.post(() -> {
|
||||||
|
btnReloadModule.setEnabled(true);
|
||||||
|
btnReloadModule.setText("重载模块");
|
||||||
|
Toast.makeText(getContext(), "重载失败: " + e.getMessage(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshModuleStatus() {
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
final boolean isLoaded = configManager.isKpmModuleLoaded();
|
||||||
|
mainHandler.post(() -> updateModuleStatus(isLoaded));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error refreshing module status", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (executor != null && !executor.isShutdown()) {
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SO 隐藏项数据类
|
||||||
|
*/
|
||||||
|
public static class HideSoItem {
|
||||||
|
public String soName;
|
||||||
|
public boolean isHidden;
|
||||||
|
public boolean isFixed; // 是否是固定隐藏项(不可取消)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
111
configapp/src/main/java/com/jiqiu/configapp/MainActivity.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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;
|
||||||
|
private KpmHideFragment kpmHideFragment;
|
||||||
|
|
||||||
|
@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_kpm_hide) {
|
||||||
|
showKpmHideFragment();
|
||||||
|
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 showKpmHideFragment() {
|
||||||
|
if (kpmHideFragment == null) {
|
||||||
|
kpmHideFragment = new KpmHideFragment();
|
||||||
|
}
|
||||||
|
showFragment(kpmHideFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,180 @@
|
|||||||
|
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 android.widget.EditText;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
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 EditText editInjectionDelay;
|
||||||
|
private TextView tvGlobalGadgetStatus;
|
||||||
|
private Button btnConfigureGlobalGadget;
|
||||||
|
private ConfigManager configManager;
|
||||||
|
|
||||||
|
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);
|
||||||
|
editInjectionDelay = view.findViewById(R.id.editInjectionDelay);
|
||||||
|
tvGlobalGadgetStatus = view.findViewById(R.id.tvGlobalGadgetStatus);
|
||||||
|
btnConfigureGlobalGadget = view.findViewById(R.id.btnConfigureGlobalGadget);
|
||||||
|
|
||||||
|
configManager = new ConfigManager(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load injection delay
|
||||||
|
int injectionDelay = configManager.getInjectionDelay();
|
||||||
|
editInjectionDelay.setText(String.valueOf(injectionDelay));
|
||||||
|
|
||||||
|
// Load global gadget status
|
||||||
|
updateGlobalGadgetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Injection delay listener
|
||||||
|
editInjectionDelay.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) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
String text = s.toString().trim();
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
try {
|
||||||
|
int delay = Integer.parseInt(text);
|
||||||
|
// Limit delay between 0 and 60 seconds
|
||||||
|
if (delay < 0) delay = 0;
|
||||||
|
if (delay > 60) delay = 60;
|
||||||
|
|
||||||
|
configManager.setInjectionDelay(delay);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignore invalid input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global gadget configuration button
|
||||||
|
btnConfigureGlobalGadget.setOnClickListener(v -> {
|
||||||
|
showGlobalGadgetConfigDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSettingsChangeListener(OnSettingsChangeListener listener) {
|
||||||
|
this.settingsChangeListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHideSystemApps() {
|
||||||
|
return sharedPreferences.getBoolean(KEY_HIDE_SYSTEM_APPS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateGlobalGadgetStatus() {
|
||||||
|
ConfigManager.GadgetConfig globalGadget = configManager.getGlobalGadgetConfig();
|
||||||
|
if (globalGadget != null) {
|
||||||
|
String status = "已配置: " + globalGadget.gadgetName;
|
||||||
|
if (globalGadget.mode.equals("server")) {
|
||||||
|
status += " (Server模式, 端口: " + globalGadget.port + ")";
|
||||||
|
} else {
|
||||||
|
status += " (Script模式)";
|
||||||
|
}
|
||||||
|
tvGlobalGadgetStatus.setText(status);
|
||||||
|
} else {
|
||||||
|
tvGlobalGadgetStatus.setText("未配置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showGlobalGadgetConfigDialog() {
|
||||||
|
// Use existing GadgetConfigDialog
|
||||||
|
GadgetConfigDialog dialog = new GadgetConfigDialog(
|
||||||
|
getContext(),
|
||||||
|
"全局Gadget配置",
|
||||||
|
configManager.getGlobalGadgetConfig(),
|
||||||
|
gadgetConfig -> {
|
||||||
|
// Save global gadget configuration
|
||||||
|
configManager.setGlobalGadgetConfig(gadgetConfig);
|
||||||
|
updateGlobalGadgetStatus();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,283 @@
|
|||||||
|
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);
|
||||||
|
// Add MIME types that might help filter SO files
|
||||||
|
String[] mimeTypes = {"application/octet-stream", "application/x-sharedlib", "*/*"};
|
||||||
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||||
|
// Suggest starting location
|
||||||
|
intent.putExtra("android.provider.extra.INITIAL_URI",
|
||||||
|
android.net.Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload"));
|
||||||
|
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 using proper URI handling
|
||||||
|
String path = FileUtils.getRealPathFromUri(requireContext(), uri);
|
||||||
|
if (path != null) {
|
||||||
|
showDeleteOriginalDialog(path);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "无法获取文件路径,请尝试其他方式", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
219
configapp/src/main/res/layout/dialog_app_config.xml
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView 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">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
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:nestedScrollingEnabled="false"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 配置"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/gadgetConfigGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioNoGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="不使用Gadget"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioUseGlobalGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="使用全局Gadget配置" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvGlobalGadgetInfo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:text="未配置全局Gadget"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioUseCustomGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="自定义Gadget配置" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnConfigureGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置Gadget"
|
||||||
|
android:textSize="14sp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:enabled="false"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget可用于Frida调试,可配置监听地址和端口"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="注入方式"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/injectionMethodGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioStandardInjection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="标准注入"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="使用标准dlopen方式注入"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioRiruInjection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Riru注入" />
|
||||||
|
|
||||||
|
<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="32dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioCustomLinkerInjection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="自定义Linker注入" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="使用自定义ELF加载器进行注入"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginStart="32dp" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
299
configapp/src/main/res/layout/dialog_gadget_config.xml
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView 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">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 配置"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="提示:请确保对应的 gadget SO 文件已添加到 SO 库中"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 模式选择 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 模式"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/modeRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioModeServer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Server 模式"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioModeScript"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Script 模式" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- Server 模式配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/serverModeLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- 监听地址配置 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="监听地址"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/addressRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioAddressAll"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="0.0.0.0 (监听所有接口)"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioAddressLocal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="127.0.0.1 (仅本地)" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioAddressCustom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="自定义" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editCustomAddress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="输入自定义地址"
|
||||||
|
android:inputType="text"
|
||||||
|
android:enabled="false"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 端口配置 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="监听端口"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editPort"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="27042"
|
||||||
|
android:hint="1-65535"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 端口冲突处理 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="端口冲突处理"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/portConflictRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioConflictFail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="fail (启动失败)"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioConflictPickNext"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="pick-next (尝试下一个端口)" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- 加载处理方式 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="加载时处理方式"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/onLoadRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioLoadWait"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="wait (等待连接)"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioLoadResume"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="resume (立即继续)" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Script 模式配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/scriptModeLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Script 文件路径"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editScriptPath"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="/data/local/tmp/script.js"
|
||||||
|
android:inputType="text"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnSelectScript"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="选择"
|
||||||
|
android:textSize="12sp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="提示:Script 模式下,Gadget 会在程序入口点执行前自动加载并运行指定的脚本文件"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Gadget 名称 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Gadget 文件名"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editGadgetName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="libgadget.so"
|
||||||
|
android:hint="例如: libgadget.so"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- JSON 预览区域 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置预览 (可直接编辑)"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardCornerRadius="4dp">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editJsonPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="150dp"
|
||||||
|
android:gravity="top"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:overScrollMode="always" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
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>
|
||||||
202
configapp/src/main/res/layout/fragment_kpm_hide.xml
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView 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:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".KpmHideFragment">
|
||||||
|
|
||||||
|
<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="KPM 注入隐藏"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- KPM 模块状态卡片 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<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="模块状态"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvModuleStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="● 检查中..."
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvModuleInfo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="正在检查模块状态..."
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnReloadModule"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="重载模块"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</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"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardBackgroundColor="#FFF3E0">
|
||||||
|
|
||||||
|
<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="ℹ️ 使用说明"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• KPM 内核模块可以隐藏进程的内存映射信息\n• libmyinjector.so 将自动隐藏(不可取消)\n• 勾选其他 SO 文件可隐藏其在 /proc/[pid]/maps 中的显示\n• 每次更改都会自动重载内核模块以应用配置"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:textColor="#E65100"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvConfigPath"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置文件: /data/local/tmp/kpm_hide_config.txt"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
</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"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<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="🔒 固定隐藏项"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="以下库文件将始终被隐藏:"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• libmyinjector.so (Zygisk 注入器)"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textColor="@android:color/holo_green_dark" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- SO 隐藏列表 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<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="可选隐藏 SO 列表"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="从已配置的应用中选择需要隐藏的 SO 文件"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvSoList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="100dp"
|
||||||
|
tools:listitem="@layout/item_hide_so" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
226
configapp/src/main/res/layout/fragment_settings.xml
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<?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="注入延迟时间"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="设置SO文件注入前的等待时间,以确保应用完全初始化"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="延迟时间(秒):"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editInjectionDelay"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="2"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:hint="0-60"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="秒"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="建议值:2-5秒。某些应用可能需要更长时间。"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- 全局Gadget配置 -->
|
||||||
|
<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="全局Gadget配置"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置全局默认的Gadget设置,应用可以选择使用或覆盖"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvGlobalGadgetStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="未配置"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnConfigureGlobalGadget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="配置全局Gadget"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</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>
|
||||||
58
configapp/src/main/res/layout/item_hide_so.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?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="12dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/cbHide"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSoName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="libexample.so"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSoStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="未隐藏"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFixedBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="固定"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:background="@android:color/holo_green_dark"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</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>
|
||||||
22
configapp/src/main/res/menu/bottom_nav_menu.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?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_kpm_hide"
|
||||||
|
android:icon="@android:drawable/ic_secure"
|
||||||
|
android:title="@string/title_kpm_hide" />
|
||||||
|
|
||||||
|
<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>
|
||||||
46
configapp/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">MyInjector Config</string>
|
||||||
|
|
||||||
|
<!-- 底部导航 -->
|
||||||
|
<string name="title_apps">应用列表</string>
|
||||||
|
<string name="title_so_manager">SO库管理</string>
|
||||||
|
<string name="title_kpm_hide">KPM隐藏</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>
|
||||||
|
|
||||||
|
<!-- KPM 隐藏 -->
|
||||||
|
<string name="kpm_hide_title">KPM 注入隐藏</string>
|
||||||
|
<string name="kpm_module_status">模块状态</string>
|
||||||
|
<string name="kpm_module_loaded">已加载</string>
|
||||||
|
<string name="kpm_module_not_loaded">未加载</string>
|
||||||
|
<string name="kpm_module_info">KPM 内核模块信息</string>
|
||||||
|
<string name="kpm_reload_module">重载模块</string>
|
||||||
|
<string name="kpm_reload_success">模块重载成功</string>
|
||||||
|
<string name="kpm_reload_failed">模块重载失败</string>
|
||||||
|
<string name="kpm_config_path">配置文件路径</string>
|
||||||
|
<string name="kpm_fixed_items">固定隐藏项</string>
|
||||||
|
<string name="kpm_optional_items">可选隐藏 SO 列表</string>
|
||||||
|
<string name="kpm_usage_info">使用说明</string>
|
||||||
|
<string name="kpm_fixed_badge">固定</string>
|
||||||
|
<string name="kpm_hidden">已隐藏</string>
|
||||||
|
<string name="kpm_not_hidden">未隐藏</string>
|
||||||
|
<string name="kpm_add_success">已添加到隐藏列表</string>
|
||||||
|
<string name="kpm_remove_success">已从隐藏列表移除</string>
|
||||||
|
<string name="kpm_update_failed">更新失败</string>
|
||||||
|
<string name="kpm_fixed_cannot_uncheck">libmyinjector.so 是必需的,不能取消隐藏</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
|
android.useAndroidX=true
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# Fix TLS handshake issues
|
||||||
|
systemProp.https.protocols=TLSv1.2,TLSv1.3
|
||||||
|
|||||||
@@ -66,6 +66,21 @@ afterEvaluate {
|
|||||||
from("$buildDir/intermediates/stripped_native_libs/$variantLowered/out/lib") {
|
from("$buildDir/intermediates/stripped_native_libs/$variantLowered/out/lib") {
|
||||||
into 'lib'
|
into 'lib'
|
||||||
}
|
}
|
||||||
|
// Copy service.sh
|
||||||
|
from("$projectDir") {
|
||||||
|
include 'service.sh'
|
||||||
|
}
|
||||||
|
// Copy KPM module
|
||||||
|
from("$projectDir/kpm") {
|
||||||
|
include 'injectHide.kpm'
|
||||||
|
}
|
||||||
|
// 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 {
|
doLast {
|
||||||
file("$magiskDir/zygisk").mkdir()
|
file("$magiskDir/zygisk").mkdir()
|
||||||
fileTree("$magiskDir/lib").visit { f ->
|
fileTree("$magiskDir/lib").visit { f ->
|
||||||
|
|||||||
BIN
module/kpm/injectHide.kpm
Normal file
139
module/service.sh
Executable file
@@ -0,0 +1,139 @@
|
|||||||
|
#!/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 安装脚本执行完成"
|
||||||
|
|
||||||
|
# ==================== KPM 模块加载 ====================
|
||||||
|
|
||||||
|
# KPM 模块路径
|
||||||
|
KPM_MODULE="$MODDIR/injectHide.kpm"
|
||||||
|
KPM_CONFIG="/data/local/tmp/kpm_hide_config.txt"
|
||||||
|
|
||||||
|
log "开始加载 KPM 内核模块"
|
||||||
|
|
||||||
|
# 检查 KPM 模块文件是否存在
|
||||||
|
if [ ! -f "$KPM_MODULE" ]; then
|
||||||
|
log "KPM 模块文件不存在: $KPM_MODULE"
|
||||||
|
else
|
||||||
|
log "找到 KPM 模块文件: $KPM_MODULE"
|
||||||
|
|
||||||
|
# 创建初始配置文件(如果不存在)
|
||||||
|
if [ ! -f "$KPM_CONFIG" ]; then
|
||||||
|
log "创建初始 KPM 配置文件"
|
||||||
|
# 确保 /data/local/tmp 目录存在且权限正确
|
||||||
|
mkdir -p /data/local/tmp
|
||||||
|
chmod 777 /data/local/tmp
|
||||||
|
echo "libmyinjector.so" > "$KPM_CONFIG"
|
||||||
|
chmod 666 "$KPM_CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 等待一段时间确保系统稳定
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 加载 KPM 模块
|
||||||
|
log "正在加载 KPM 模块..."
|
||||||
|
insmod "$KPM_MODULE" 2>&1 | while read line; do
|
||||||
|
log "insmod: $line"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 检查模块是否加载成功
|
||||||
|
if lsmod | grep -q "hideInject"; then
|
||||||
|
log "KPM 模块加载成功!"
|
||||||
|
else
|
||||||
|
log "KPM 模块加载失败,请检查日志"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 脚本完成
|
||||||
|
exit 0
|
||||||
@@ -29,17 +29,22 @@ set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}")
|
|||||||
|
|
||||||
include_directories(
|
include_directories(
|
||||||
xdl/include
|
xdl/include
|
||||||
|
mylinker/include
|
||||||
)
|
)
|
||||||
|
|
||||||
aux_source_directory(xdl xdl-src)
|
aux_source_directory(xdl xdl-src)
|
||||||
|
|
||||||
|
# Build mylinker as a subdirectory
|
||||||
|
add_subdirectory(mylinker)
|
||||||
|
|
||||||
add_library(${MODULE_NAME} SHARED
|
add_library(${MODULE_NAME} SHARED
|
||||||
main.cpp
|
main.cpp
|
||||||
hack.cpp
|
hack_new.cpp
|
||||||
|
config.cpp
|
||||||
newriruhide.cpp
|
newriruhide.cpp
|
||||||
pmparser.cpp
|
pmparser.cpp
|
||||||
${xdl-src})
|
${xdl-src})
|
||||||
target_link_libraries(${MODULE_NAME} log)
|
target_link_libraries(${MODULE_NAME} log mylinker)
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
add_custom_command(TARGET ${MODULE_NAME} POST_BUILD
|
add_custom_command(TARGET ${MODULE_NAME} POST_BUILD
|
||||||
|
|||||||
268
module/src/main/cpp/config.cpp
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
#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";
|
||||||
|
} else {
|
||||||
|
// Number value
|
||||||
|
size_t valueEnd = json.find_first_of(",} \t\n", valueStart);
|
||||||
|
if (valueEnd == std::string::npos) {
|
||||||
|
return json.substr(valueStart);
|
||||||
|
}
|
||||||
|
return json.substr(valueStart, valueEnd - valueStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 injection method
|
||||||
|
std::string methodStr = extractValue(appJson, "injectionMethod");
|
||||||
|
if (methodStr == "2" || methodStr == "custom_linker") {
|
||||||
|
appConfig.injectionMethod = InjectionMethod::CUSTOM_LINKER;
|
||||||
|
} else if (methodStr == "1" || methodStr == "riru") {
|
||||||
|
appConfig.injectionMethod = InjectionMethod::RIRU;
|
||||||
|
} else {
|
||||||
|
appConfig.injectionMethod = InjectionMethod::STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse gadgetConfig if exists
|
||||||
|
size_t gadgetPos = appJson.find("\"gadgetConfig\"");
|
||||||
|
if (gadgetPos != std::string::npos) {
|
||||||
|
size_t gadgetObjStart = appJson.find("{", gadgetPos);
|
||||||
|
size_t gadgetObjEnd = appJson.find("}", gadgetObjStart);
|
||||||
|
|
||||||
|
if (gadgetObjStart != std::string::npos && gadgetObjEnd != std::string::npos) {
|
||||||
|
std::string gadgetObj = appJson.substr(gadgetObjStart, gadgetObjEnd - gadgetObjStart + 1);
|
||||||
|
|
||||||
|
GadgetConfig* gadgetConfig = new GadgetConfig();
|
||||||
|
|
||||||
|
std::string address = extractValue(gadgetObj, "address");
|
||||||
|
if (!address.empty()) gadgetConfig->address = address;
|
||||||
|
|
||||||
|
std::string portStr = extractValue(gadgetObj, "port");
|
||||||
|
if (!portStr.empty()) gadgetConfig->port = std::stoi(portStr);
|
||||||
|
|
||||||
|
std::string onPortConflict = extractValue(gadgetObj, "onPortConflict");
|
||||||
|
if (!onPortConflict.empty()) gadgetConfig->onPortConflict = onPortConflict;
|
||||||
|
|
||||||
|
std::string onLoad = extractValue(gadgetObj, "onLoad");
|
||||||
|
if (!onLoad.empty()) gadgetConfig->onLoad = onLoad;
|
||||||
|
|
||||||
|
std::string gadgetName = extractValue(gadgetObj, "gadgetName");
|
||||||
|
if (!gadgetName.empty()) gadgetConfig->gadgetName = gadgetName;
|
||||||
|
|
||||||
|
appConfig.gadgetConfig = gadgetConfig;
|
||||||
|
LOGD("Loaded gadget config: %s:%d, name: %s",
|
||||||
|
gadgetConfig->address.c_str(), gadgetConfig->port, gadgetConfig->gadgetName.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_config.perAppConfig[packageName] = appConfig;
|
||||||
|
const char* methodName = appConfig.injectionMethod == InjectionMethod::CUSTOM_LINKER ? "custom_linker" :
|
||||||
|
appConfig.injectionMethod == InjectionMethod::RIRU ? "riru" : "standard";
|
||||||
|
LOGD("Loaded config for app: %s, enabled: %d, method: %s, SO files: %zu, gadget: %s",
|
||||||
|
packageName.c_str(), appConfig.enabled, methodName, appConfig.soFiles.size(),
|
||||||
|
appConfig.gadgetConfig ? "yes" : "no");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
std::string delayStr = extractValue(json, "injectionDelay");
|
||||||
|
if (!delayStr.empty()) {
|
||||||
|
g_config.injectionDelay = std::stoi(delayStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Module enabled: %d, hide injection: %d, injection delay: %d",
|
||||||
|
g_config.enabled, g_config.hideInjection, g_config.injectionDelay);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
InjectionMethod getAppInjectionMethod(const std::string& packageName) {
|
||||||
|
if (!g_configLoaded) {
|
||||||
|
readConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = g_config.perAppConfig.find(packageName);
|
||||||
|
if (it != g_config.perAppConfig.end()) {
|
||||||
|
return it->second.injectionMethod;
|
||||||
|
}
|
||||||
|
return InjectionMethod::STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getInjectionDelay() {
|
||||||
|
if (!g_configLoaded) {
|
||||||
|
readConfig();
|
||||||
|
}
|
||||||
|
return g_config.injectionDelay;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
module/src/main/cpp/config.h
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class InjectionMethod {
|
||||||
|
STANDARD = 0,
|
||||||
|
RIRU = 1,
|
||||||
|
CUSTOM_LINKER = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GadgetConfig {
|
||||||
|
std::string address = "0.0.0.0";
|
||||||
|
int port = 27042;
|
||||||
|
std::string onPortConflict = "fail";
|
||||||
|
std::string onLoad = "wait";
|
||||||
|
std::string gadgetName = "libgadget.so";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppConfig {
|
||||||
|
bool enabled = false;
|
||||||
|
InjectionMethod injectionMethod = InjectionMethod::STANDARD;
|
||||||
|
std::vector<SoFile> soFiles;
|
||||||
|
GadgetConfig* gadgetConfig = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModuleConfig {
|
||||||
|
bool enabled = true;
|
||||||
|
bool hideInjection = false;
|
||||||
|
int injectionDelay = 2; // Default 2 seconds
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Get injection method for specific app
|
||||||
|
InjectionMethod getAppInjectionMethod(const std::string& packageName);
|
||||||
|
|
||||||
|
// Get injection delay in seconds
|
||||||
|
int getInjectionDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CONFIG_H
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Perfare on 2020/7/4.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef ZYGISK_IL2CPPDUMPER_GAME_H
|
|
||||||
#define ZYGISK_IL2CPPDUMPER_GAME_H
|
|
||||||
|
|
||||||
#define AimPackageName "com.tencent.mobileqq"
|
|
||||||
|
|
||||||
#endif //ZYGISK_IL2CPPDUMPER_GAME_H
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Perfare on 2020/7/4.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "hack.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "xdl.h"
|
|
||||||
#include <cstring>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <sys/system_properties.h>
|
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <jni.h>
|
|
||||||
#include <thread>
|
|
||||||
#include <sys/mman.h>
|
|
||||||
#include <linux/unistd.h>
|
|
||||||
#include <array>
|
|
||||||
#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);
|
|
||||||
|
|
||||||
// 构建新文件路径,使用传入的 soname 参数
|
|
||||||
char new_so_path[256];
|
|
||||||
snprintf(new_so_path, sizeof(new_so_path), "%s/files/%s.so", game_data_dir, soname);
|
|
||||||
|
|
||||||
// 构建源文件路径
|
|
||||||
char src_path[256];
|
|
||||||
snprintf(src_path, sizeof(src_path), "/data/local/tmp/%s.so", soname);
|
|
||||||
|
|
||||||
// 打开源文件
|
|
||||||
int src_fd = open(src_path, O_RDONLY);
|
|
||||||
if (src_fd < 0) {
|
|
||||||
LOGE("Failed to open %s: %s (errno: %d)", src_path, strerror(errno), errno);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开目标文件
|
|
||||||
int dest_fd = open(new_so_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
||||||
if (dest_fd < 0) {
|
|
||||||
LOGE("Failed to open %s", new_so_path);
|
|
||||||
close(src_fd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制文件内容
|
|
||||||
char buffer[4096];
|
|
||||||
ssize_t bytes;
|
|
||||||
while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
|
|
||||||
if (write(dest_fd, buffer, bytes) != bytes) {
|
|
||||||
LOGE("Failed to write to %s", new_so_path);
|
|
||||||
close(src_fd);
|
|
||||||
close(dest_fd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭文件描述符
|
|
||||||
close(src_fd);
|
|
||||||
close(dest_fd);
|
|
||||||
|
|
||||||
// 修改文件权限
|
|
||||||
if (chmod(new_so_path, 0755) != 0) {
|
|
||||||
LOGE("Failed to change permissions on %s: %s (errno: %d)", new_so_path, strerror(errno), errno);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
LOGI("Successfully changed permissions to 755 on %s", new_so_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载 .so 文件
|
|
||||||
void *handle;
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
handle = dlopen(new_so_path, RTLD_NOW | RTLD_LOCAL);
|
|
||||||
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());
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果加载失败
|
|
||||||
if (!load) {
|
|
||||||
LOGI("%s.so not found in thread %d", soname, gettid());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找 JNI_OnLoad 并调用
|
|
||||||
// void (*setupSignalHandler)();
|
|
||||||
// *(void **) (&setupSignalHandler) = dlsym(handle, "setupSignalHandler");
|
|
||||||
//
|
|
||||||
// if (setupSignalHandler) {
|
|
||||||
// LOGI("setupSignalHandler symbol found, calling setupSignalHandler.");
|
|
||||||
// setupSignalHandler(); // 调用找到的函数
|
|
||||||
// } else {
|
|
||||||
// LOGE("setupSignalHandler symbol not found in %s", new_so_path);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
void hack_start(const char *game_data_dir,JavaVM *vm) {
|
|
||||||
load_so(game_data_dir,vm,"test");
|
|
||||||
//如果要注入多个so,那么就在这里不断的添加load_so函数即可
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GetLibDir(JavaVM *vms) {
|
|
||||||
JNIEnv *env = nullptr;
|
|
||||||
vms->AttachCurrentThread(&env, nullptr);
|
|
||||||
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
|
|
||||||
if (activity_thread_clz != nullptr) {
|
|
||||||
jmethodID currentApplicationId = env->GetStaticMethodID(activity_thread_clz,
|
|
||||||
"currentApplication",
|
|
||||||
"()Landroid/app/Application;");
|
|
||||||
if (currentApplicationId) {
|
|
||||||
jobject application = env->CallStaticObjectMethod(activity_thread_clz,
|
|
||||||
currentApplicationId);
|
|
||||||
jclass application_clazz = env->GetObjectClass(application);
|
|
||||||
if (application_clazz) {
|
|
||||||
jmethodID get_application_info = env->GetMethodID(application_clazz,
|
|
||||||
"getApplicationInfo",
|
|
||||||
"()Landroid/content/pm/ApplicationInfo;");
|
|
||||||
if (get_application_info) {
|
|
||||||
jobject application_info = env->CallObjectMethod(application,
|
|
||||||
get_application_info);
|
|
||||||
jfieldID native_library_dir_id = env->GetFieldID(
|
|
||||||
env->GetObjectClass(application_info), "nativeLibraryDir",
|
|
||||||
"Ljava/lang/String;");
|
|
||||||
if (native_library_dir_id) {
|
|
||||||
auto native_library_dir_jstring = (jstring) env->GetObjectField(
|
|
||||||
application_info, native_library_dir_id);
|
|
||||||
auto path = env->GetStringUTFChars(native_library_dir_jstring, nullptr);
|
|
||||||
LOGI("lib dir %s", path);
|
|
||||||
std::string lib_dir(path);
|
|
||||||
env->ReleaseStringUTFChars(native_library_dir_jstring, path);
|
|
||||||
return lib_dir;
|
|
||||||
} else {
|
|
||||||
LOGE("nativeLibraryDir not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("getApplicationInfo not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("application class not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("currentApplication not found");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGE("ActivityThread not found");
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string GetNativeBridgeLibrary() {
|
|
||||||
auto value = std::array<char, PROP_VALUE_MAX>();
|
|
||||||
__system_property_get("ro.dalvik.vm.native.bridge", value.data());
|
|
||||||
return {value.data()};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NativeBridgeCallbacks {
|
|
||||||
uint32_t version;
|
|
||||||
void *initialize;
|
|
||||||
|
|
||||||
void *(*loadLibrary)(const char *libpath, int flag);
|
|
||||||
|
|
||||||
void *(*getTrampoline)(void *handle, const char *name, const char *shorty, uint32_t len);
|
|
||||||
|
|
||||||
void *isSupported;
|
|
||||||
void *getAppEnv;
|
|
||||||
void *isCompatibleWith;
|
|
||||||
void *getSignalHandler;
|
|
||||||
void *unloadLibrary;
|
|
||||||
void *getError;
|
|
||||||
void *isPathSupported;
|
|
||||||
void *initAnonymousNamespace;
|
|
||||||
void *createNamespace;
|
|
||||||
void *linkNamespaces;
|
|
||||||
|
|
||||||
void *(*loadLibraryExt)(const char *libpath, int flag, void *ns);
|
|
||||||
};
|
|
||||||
|
|
||||||
bool NativeBridgeLoad(const char *game_data_dir, int api_level, void *data, size_t length) {
|
|
||||||
//TODO 等待houdini初始化
|
|
||||||
sleep(5);
|
|
||||||
|
|
||||||
auto libart = dlopen("libart.so", RTLD_NOW);
|
|
||||||
auto JNI_GetCreatedJavaVMs = (jint (*)(JavaVM **, jsize, jsize *)) dlsym(libart,
|
|
||||||
"JNI_GetCreatedJavaVMs");
|
|
||||||
LOGI("JNI_GetCreatedJavaVMs %p", JNI_GetCreatedJavaVMs);
|
|
||||||
JavaVM *vms_buf[1];
|
|
||||||
JavaVM *vms;
|
|
||||||
jsize num_vms;
|
|
||||||
jint status = JNI_GetCreatedJavaVMs(vms_buf, 1, &num_vms);
|
|
||||||
if (status == JNI_OK && num_vms > 0) {
|
|
||||||
vms = vms_buf[0];
|
|
||||||
} else {
|
|
||||||
LOGE("GetCreatedJavaVMs error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto lib_dir = GetLibDir(vms);
|
|
||||||
if (lib_dir.empty()) {
|
|
||||||
LOGE("GetLibDir error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (lib_dir.find("/lib/x86") != std::string::npos) {
|
|
||||||
LOGI("no need NativeBridge");
|
|
||||||
munmap(data, length);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto nb = dlopen("libhoudini.so", RTLD_NOW);
|
|
||||||
if (!nb) {
|
|
||||||
auto native_bridge = GetNativeBridgeLibrary();
|
|
||||||
LOGI("native bridge: %s", native_bridge.data());
|
|
||||||
nb = dlopen(native_bridge.data(), RTLD_NOW);
|
|
||||||
}
|
|
||||||
if (nb) {
|
|
||||||
LOGI("nb %p", nb);
|
|
||||||
auto callbacks = (NativeBridgeCallbacks *) dlsym(nb, "NativeBridgeItf");
|
|
||||||
if (callbacks) {
|
|
||||||
LOGI("NativeBridgeLoadLibrary %p", callbacks->loadLibrary);
|
|
||||||
LOGI("NativeBridgeLoadLibraryExt %p", callbacks->loadLibraryExt);
|
|
||||||
LOGI("NativeBridgeGetTrampoline %p", callbacks->getTrampoline);
|
|
||||||
int fd = syscall(__NR_memfd_create, "anon", MFD_CLOEXEC);
|
|
||||||
ftruncate(fd, (off_t) length);
|
|
||||||
void *mem = mmap(nullptr, length, PROT_WRITE, MAP_SHARED, fd, 0);
|
|
||||||
memcpy(mem, data, length);
|
|
||||||
munmap(mem, length);
|
|
||||||
munmap(data, length);
|
|
||||||
char path[PATH_MAX];
|
|
||||||
snprintf(path, PATH_MAX, "/proc/self/fd/%d", fd);
|
|
||||||
LOGI("arm path %s", path);
|
|
||||||
|
|
||||||
void *arm_handle;
|
|
||||||
if (api_level >= 26) {
|
|
||||||
arm_handle = callbacks->loadLibraryExt(path, RTLD_NOW, (void *) 3);
|
|
||||||
} else {
|
|
||||||
arm_handle = callbacks->loadLibrary(path, RTLD_NOW);
|
|
||||||
}
|
|
||||||
if (arm_handle) {
|
|
||||||
LOGI("arm handle %p", arm_handle);
|
|
||||||
auto init = (void (*)(JavaVM *, void *)) callbacks->getTrampoline(arm_handle,
|
|
||||||
"JNI_OnLoad",
|
|
||||||
nullptr, 0);
|
|
||||||
LOGI("JNI_OnLoad %p", init);
|
|
||||||
init(vms, (void *) game_data_dir);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void hack_prepare(const char *_data_dir, void *data, size_t length) {
|
|
||||||
LOGI("hack thread: %d", gettid());
|
|
||||||
int api_level = android_get_device_api_level();
|
|
||||||
LOGI("api level: %d", api_level);
|
|
||||||
|
|
||||||
#if defined(__i386__) || defined(__x86_64__)
|
|
||||||
if (!NativeBridgeLoad(_data_dir, api_level, data, length)) {
|
|
||||||
#endif
|
|
||||||
hack_start(_data_dir, nullptr);
|
|
||||||
#if defined(__i386__) || defined(__x86_64__)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(__arm__) || defined(__aarch64__)
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
|
||||||
auto game_data_dir = (const char *) reserved;
|
|
||||||
std::thread hack_thread(hack_start, game_data_dir,vm);
|
|
||||||
hack_thread.detach();
|
|
||||||
return JNI_VERSION_1_6;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
#define ZYGISK_IL2CPPDUMPER_HACK_H
|
#define ZYGISK_IL2CPPDUMPER_HACK_H
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <jni.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, JavaVM *vm);
|
||||||
|
|
||||||
#endif //ZYGISK_IL2CPPDUMPER_HACK_H
|
#endif //ZYGISK_IL2CPPDUMPER_HACK_H
|
||||||
|
|||||||
135
module/src/main/cpp/hack_new.cpp
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#include "hack.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "mylinker.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <thread>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
// External function from newriruhide.cpp
|
||||||
|
extern "C" void riru_hide(const char *name);
|
||||||
|
|
||||||
|
void load_so_file_standard(const char *game_data_dir, const Config::SoFile &soFile) {
|
||||||
|
// Use original filename
|
||||||
|
char so_path[512];
|
||||||
|
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (access(so_path, F_OK) != 0) {
|
||||||
|
LOGE("SO file not found: %s", so_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the SO file using standard dlopen (no hiding)
|
||||||
|
void *handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (handle) {
|
||||||
|
LOGI("Successfully loaded SO via standard dlopen: %s", soFile.name.c_str());
|
||||||
|
} else {
|
||||||
|
LOGE("Failed to load SO via standard dlopen: %s - %s", so_path, dlerror());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_so_file_riru(const char *game_data_dir, const Config::SoFile &soFile) {
|
||||||
|
// Use original filename
|
||||||
|
char so_path[512];
|
||||||
|
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (access(so_path, F_OK) != 0) {
|
||||||
|
LOGE("SO file not found: %s", so_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the SO file using dlopen (Riru method)
|
||||||
|
void *handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (handle) {
|
||||||
|
LOGI("Successfully loaded SO via Riru: %s", soFile.name.c_str());
|
||||||
|
|
||||||
|
// Hide if configured
|
||||||
|
if (Config::shouldHideInjection()) {
|
||||||
|
// Hide using the original name
|
||||||
|
riru_hide(soFile.name.c_str());
|
||||||
|
LOGI("Applied riru_hide to: %s", soFile.name.c_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGE("Failed to load SO via Riru: %s - %s", so_path, dlerror());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_so_file_custom_linker(const char *game_data_dir, const Config::SoFile &soFile, JavaVM *vm) {
|
||||||
|
// Use original filename
|
||||||
|
char so_path[512];
|
||||||
|
snprintf(so_path, sizeof(so_path), "%s/files/%s", game_data_dir, soFile.name.c_str());
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (access(so_path, F_OK) != 0) {
|
||||||
|
LOGE("SO file not found: %s", so_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the SO file using custom linker
|
||||||
|
if (mylinker_load_library(so_path, vm)) {
|
||||||
|
LOGI("Successfully loaded SO via custom linker: %s", soFile.name.c_str());
|
||||||
|
|
||||||
|
// Custom linker doesn't appear in maps, so no need to hide
|
||||||
|
if (Config::shouldHideInjection()) {
|
||||||
|
LOGI("Custom linker injection is inherently hidden");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGE("Failed to load SO via custom linker: %s", so_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hack_thread_func(const char *game_data_dir, const char *package_name, JavaVM *vm) {
|
||||||
|
LOGI("Hack thread started for package: %s", package_name);
|
||||||
|
|
||||||
|
// Note: Delay is now handled in main thread before this thread is created
|
||||||
|
LOGI("Starting injection immediately (delay already applied in main thread)");
|
||||||
|
|
||||||
|
// Get injection method for this app
|
||||||
|
Config::InjectionMethod method = Config::getAppInjectionMethod(package_name);
|
||||||
|
const char* methodName = method == Config::InjectionMethod::CUSTOM_LINKER ? "Custom Linker" :
|
||||||
|
method == Config::InjectionMethod::RIRU ? "Riru" : "Standard";
|
||||||
|
LOGI("Using injection method: %s", methodName);
|
||||||
|
|
||||||
|
// 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 using the configured method
|
||||||
|
for (const auto &soFile : soFiles) {
|
||||||
|
// Skip config files
|
||||||
|
if (soFile.name.find(".config.so") != std::string::npos) {
|
||||||
|
LOGI("Skipping config file: %s", soFile.name.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("Loading SO: %s (stored as: %s)", soFile.name.c_str(), soFile.storedPath.c_str());
|
||||||
|
|
||||||
|
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
|
||||||
|
load_so_file_custom_linker(game_data_dir, soFile, vm);
|
||||||
|
} else if (method == Config::InjectionMethod::RIRU) {
|
||||||
|
load_so_file_riru(game_data_dir, soFile);
|
||||||
|
} else {
|
||||||
|
load_so_file_standard(game_data_dir, soFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup custom linker resources when done (if used)
|
||||||
|
if (method == Config::InjectionMethod::CUSTOM_LINKER) {
|
||||||
|
// Keep libraries loaded, don't cleanup
|
||||||
|
LOGI("Custom linker injection completed, libraries remain loaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hack_prepare(const char *game_data_dir, const char *package_name, void *data, size_t length, JavaVM *vm) {
|
||||||
|
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, vm);
|
||||||
|
hack_thread.join();
|
||||||
|
}
|
||||||
@@ -6,11 +6,14 @@
|
|||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <time.h>
|
||||||
#include "hack.h"
|
#include "hack.h"
|
||||||
#include "zygisk.hpp"
|
#include "zygisk.hpp"
|
||||||
#include "game.h"
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "dlfcn.h"
|
#include "dlfcn.h"
|
||||||
|
#include "config.h"
|
||||||
using zygisk::Api;
|
using zygisk::Api;
|
||||||
using zygisk::AppSpecializeArgs;
|
using zygisk::AppSpecializeArgs;
|
||||||
using zygisk::ServerSpecializeArgs;
|
using zygisk::ServerSpecializeArgs;
|
||||||
@@ -20,14 +23,12 @@ public:
|
|||||||
void onLoad(Api *api, JNIEnv *env) override {
|
void onLoad(Api *api, JNIEnv *env) override {
|
||||||
this->api = api;
|
this->api = api;
|
||||||
this->env = env;
|
this->env = env;
|
||||||
|
enable_hack = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void preAppSpecialize(AppSpecializeArgs *args) override {
|
void preAppSpecialize(AppSpecializeArgs *args) override {
|
||||||
auto package_name = env->GetStringUTFChars(args->nice_name, nullptr);
|
auto package_name = env->GetStringUTFChars(args->nice_name, nullptr);
|
||||||
auto app_data_dir = env->GetStringUTFChars(args->app_data_dir, nullptr);
|
auto app_data_dir = env->GetStringUTFChars(args->app_data_dir, nullptr);
|
||||||
// if (strcmp(package_name, AimPackageName) == 0){
|
|
||||||
// args->runtime_flags=8451;
|
|
||||||
// }
|
|
||||||
LOGI("preAppSpecialize %s %s %d", package_name, app_data_dir,args->runtime_flags);
|
LOGI("preAppSpecialize %s %s %d", package_name, app_data_dir,args->runtime_flags);
|
||||||
|
|
||||||
preSpecialize(package_name, app_data_dir);
|
preSpecialize(package_name, app_data_dir);
|
||||||
@@ -37,8 +38,22 @@ public:
|
|||||||
|
|
||||||
void postAppSpecialize(const AppSpecializeArgs *) override {
|
void postAppSpecialize(const AppSpecializeArgs *) override {
|
||||||
if (enable_hack) {
|
if (enable_hack) {
|
||||||
std::thread hack_thread(hack_prepare, _data_dir, data, length);
|
// Get JavaVM
|
||||||
hack_thread.detach();
|
JavaVM *vm = nullptr;
|
||||||
|
if (env->GetJavaVM(&vm) == JNI_OK) {
|
||||||
|
// Get injection delay from config
|
||||||
|
int delay = Config::getInjectionDelay();
|
||||||
|
LOGI("Main thread blocking for %d seconds before injection", delay);
|
||||||
|
|
||||||
|
// Block main thread for the delay period
|
||||||
|
sleep(delay);
|
||||||
|
|
||||||
|
// Then start hack thread with JavaVM
|
||||||
|
std::thread hack_thread(hack_prepare, _data_dir, _package_name, data, length, vm);
|
||||||
|
hack_thread.detach();
|
||||||
|
} else {
|
||||||
|
LOGE("Failed to get JavaVM");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,15 +62,25 @@ private:
|
|||||||
JNIEnv *env;
|
JNIEnv *env;
|
||||||
bool enable_hack;
|
bool enable_hack;
|
||||||
char *_data_dir;
|
char *_data_dir;
|
||||||
|
char *_package_name;
|
||||||
void *data;
|
void *data;
|
||||||
size_t length;
|
size_t length;
|
||||||
|
|
||||||
void preSpecialize(const char *package_name, const char *app_data_dir) {
|
void preSpecialize(const char *package_name, const char *app_data_dir) {
|
||||||
if (strcmp(package_name, AimPackageName) == 0) {
|
// Read configuration
|
||||||
|
Config::readConfig();
|
||||||
|
|
||||||
|
// Check if this app is enabled for injection
|
||||||
|
if (Config::isAppEnabled(package_name)) {
|
||||||
LOGI("成功注入目标进程: %s", package_name);
|
LOGI("成功注入目标进程: %s", package_name);
|
||||||
enable_hack = true;
|
enable_hack = true;
|
||||||
_data_dir = new char[strlen(app_data_dir) + 1];
|
_data_dir = new char[strlen(app_data_dir) + 1];
|
||||||
strcpy(_data_dir, app_data_dir);
|
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__)
|
#if defined(__i386__)
|
||||||
auto path = "zygisk/armeabi-v7a.so";
|
auto path = "zygisk/armeabi-v7a.so";
|
||||||
|
|||||||
29
module/src/main/cpp/mylinker/CMakeLists.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.18.1)
|
||||||
|
project(ElfLoader)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
|
include_directories(include)
|
||||||
|
|
||||||
|
set(SOURCES
|
||||||
|
mylinker.cpp
|
||||||
|
elf_loader.cpp
|
||||||
|
elf_reader.cpp
|
||||||
|
memory_manager.cpp
|
||||||
|
relocator.cpp
|
||||||
|
soinfo_manager.cpp
|
||||||
|
utils.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
find_library(log-lib log)
|
||||||
|
|
||||||
|
# Build as static library to be linked into main module
|
||||||
|
add_library(mylinker STATIC ${SOURCES})
|
||||||
|
|
||||||
|
target_link_libraries(mylinker ${log-lib})
|
||||||
|
|
||||||
|
# Only build executable for standalone testing
|
||||||
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
add_executable(elf_loader_test main.cpp elf_loader.cpp elf_reader.cpp memory_manager.cpp relocator.cpp soinfo_manager.cpp utils.cpp)
|
||||||
|
target_link_libraries(elf_loader_test ${log-lib})
|
||||||
|
endif()
|
||||||
187
module/src/main/cpp/mylinker/elf_loader.cpp
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#include "elf_loader.h"
|
||||||
|
|
||||||
|
ElfLoader::ElfLoader() : loaded_si_(nullptr) {
|
||||||
|
reader_ = std::make_unique<ElfReader>();
|
||||||
|
memory_manager_ = std::make_unique<MemoryManager>();
|
||||||
|
soinfo_manager_ = std::make_unique<SoinfoManager>();
|
||||||
|
relocator_ = std::make_unique<Relocator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfLoader::~ElfLoader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ElfLoader::LoadLibrary(const char* path) {
|
||||||
|
LOGI("Loading library: %s", path);
|
||||||
|
|
||||||
|
if (!reader_->Open(path)) {
|
||||||
|
LOGE("Failed to open %s", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reader_->Read()) {
|
||||||
|
LOGE("Failed to read ELF file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memory_manager_->ReserveAddressSpace(reader_->GetProgramHeaders(),
|
||||||
|
reader_->GetProgramHeaderCount())) {
|
||||||
|
LOGE("Failed to reserve address space");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memory_manager_->LoadSegments(reader_->GetProgramHeaders(),
|
||||||
|
reader_->GetProgramHeaderCount(),
|
||||||
|
reader_->GetMappedAddr(),
|
||||||
|
reader_->GetFileSize())) {
|
||||||
|
LOGE("Failed to load segments");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memory_manager_->FindPhdr(reader_->GetProgramHeaders(),
|
||||||
|
reader_->GetProgramHeaderCount())) {
|
||||||
|
LOGE("Failed to find program headers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* basename = strrchr(path, '/');
|
||||||
|
basename = basename ? basename + 1 : path;
|
||||||
|
loaded_si_ = soinfo_manager_->GetOrCreateSoinfo(basename);
|
||||||
|
|
||||||
|
if (!soinfo_manager_->UpdateSoinfo(loaded_si_, memory_manager_.get(), reader_.get())) {
|
||||||
|
LOGE("Failed to update soinfo");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!soinfo_manager_->PrelinkImage(loaded_si_)) {
|
||||||
|
LOGE("Failed to prelink image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memory_manager_->ProtectSegments(reader_->GetProgramHeaders(),
|
||||||
|
reader_->GetProgramHeaderCount())) {
|
||||||
|
LOGE("Failed to protect segments");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relocator_->LinkImage(loaded_si_)) {
|
||||||
|
LOGE("Failed to link image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_->Close();
|
||||||
|
|
||||||
|
LOGI("Successfully loaded %s", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ElfLoader::CallConstructors() {
|
||||||
|
if (loaded_si_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Constructors already called during linking");
|
||||||
|
}
|
||||||
|
|
||||||
|
void* ElfLoader::GetSymbol(const char* name) {
|
||||||
|
if (loaded_si_ == nullptr) {
|
||||||
|
LOGE("loaded_si_ is null");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == nullptr) {
|
||||||
|
LOGE("Symbol name is null");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Looking for symbol: %s", name);
|
||||||
|
LOGD("soinfo state: symtab=%p, strtab=%p, gnu_bucket=%p, bucket=%p",
|
||||||
|
loaded_si_->symtab, loaded_si_->strtab, loaded_si_->gnu_bucket, loaded_si_->bucket);
|
||||||
|
|
||||||
|
if (loaded_si_->symtab != nullptr) {
|
||||||
|
if (loaded_si_->gnu_bucket != nullptr) {
|
||||||
|
LOGD("Trying GNU hash lookup for %s", name);
|
||||||
|
uint32_t hash = relocator_->gnu_hash(name);
|
||||||
|
LOGD("GNU hash for %s: 0x%x", name, hash);
|
||||||
|
|
||||||
|
ElfW(Sym)* sym = relocator_->gnu_lookup(hash, name, loaded_si_);
|
||||||
|
if (sym != nullptr && sym->st_shndx != SHN_UNDEF) {
|
||||||
|
ElfW(Addr) addr = sym->st_value + loaded_si_->load_bias;
|
||||||
|
LOGD("Found symbol %s via GNU hash: st_value=0x%llx, load_bias=0x%llx, final_addr=0x%llx",
|
||||||
|
name, (unsigned long long)sym->st_value, (unsigned long long)loaded_si_->load_bias, (unsigned long long)addr);
|
||||||
|
|
||||||
|
if (addr >= loaded_si_->base && addr < loaded_si_->base + loaded_si_->size) {
|
||||||
|
return reinterpret_cast<void*>(addr);
|
||||||
|
} else {
|
||||||
|
LOGE("Symbol %s address 0x%llx out of range [0x%llx, 0x%llx)",
|
||||||
|
name, (unsigned long long)addr, (unsigned long long)loaded_si_->base,
|
||||||
|
(unsigned long long)(loaded_si_->base + loaded_si_->size));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGD("Symbol %s not found via GNU hash", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded_si_->bucket != nullptr) {
|
||||||
|
LOGD("Trying ELF hash lookup for %s", name);
|
||||||
|
unsigned hash = relocator_->elf_hash(name);
|
||||||
|
LOGD("ELF hash for %s: 0x%x", name, hash);
|
||||||
|
|
||||||
|
ElfW(Sym)* sym = relocator_->elf_lookup(hash, name, loaded_si_);
|
||||||
|
if (sym != nullptr && sym->st_shndx != SHN_UNDEF) {
|
||||||
|
ElfW(Addr) addr = sym->st_value + loaded_si_->load_bias;
|
||||||
|
LOGD("Found symbol %s via ELF hash: st_value=0x%llx, load_bias=0x%llx, final_addr=0x%llx",
|
||||||
|
name, (unsigned long long)sym->st_value, (unsigned long long)loaded_si_->load_bias, (unsigned long long)addr);
|
||||||
|
|
||||||
|
if (addr >= loaded_si_->base && addr < loaded_si_->base + loaded_si_->size) {
|
||||||
|
return reinterpret_cast<void*>(addr);
|
||||||
|
} else {
|
||||||
|
LOGE("Symbol %s address 0x%llx out of range [0x%llx, 0x%llx)",
|
||||||
|
name, (unsigned long long)addr, (unsigned long long)loaded_si_->base,
|
||||||
|
(unsigned long long)(loaded_si_->base + loaded_si_->size));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGD("Symbol %s not found via ELF hash", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded_si_->gnu_bucket == nullptr && loaded_si_->bucket == nullptr) {
|
||||||
|
LOGD("No hash tables available, trying linear search");
|
||||||
|
|
||||||
|
if (loaded_si_->strtab != nullptr) {
|
||||||
|
size_t sym_count = 0;
|
||||||
|
if (loaded_si_->nchain > 0) {
|
||||||
|
sym_count = loaded_si_->nchain;
|
||||||
|
} else {
|
||||||
|
LOGD("Cannot determine symbol table size");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Trying linear search with %zu symbols", sym_count);
|
||||||
|
for (size_t i = 0; i < sym_count; ++i) {
|
||||||
|
ElfW(Sym)* sym = &loaded_si_->symtab[i];
|
||||||
|
if (sym->st_name != 0 && sym->st_shndx != SHN_UNDEF) {
|
||||||
|
const char* sym_name = loaded_si_->strtab + sym->st_name;
|
||||||
|
if (strcmp(sym_name, name) == 0) {
|
||||||
|
ElfW(Addr) addr = sym->st_value + loaded_si_->load_bias;
|
||||||
|
LOGD("Found symbol %s via linear search: st_value=0x%llx, load_bias=0x%llx, final_addr=0x%llx",
|
||||||
|
name, (unsigned long long)sym->st_value, (unsigned long long)loaded_si_->load_bias, (unsigned long long)addr);
|
||||||
|
|
||||||
|
if (addr >= loaded_si_->base && addr < loaded_si_->base + loaded_si_->size) {
|
||||||
|
return reinterpret_cast<void*>(addr);
|
||||||
|
} else {
|
||||||
|
LOGE("Symbol %s address 0x%llx out of range [0x%llx, 0x%llx)",
|
||||||
|
name, (unsigned long long)addr, (unsigned long long)loaded_si_->base, (unsigned long long)(loaded_si_->base + loaded_si_->size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOGD("Symbol %s not found via linear search", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGE("Symbol table is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGE("Symbol %s not found in any method", name);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
149
module/src/main/cpp/mylinker/elf_reader.cpp
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#include "elf_reader.h"
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
ElfReader::ElfReader() : fd_(-1), file_size_(0), file_offset_(0),
|
||||||
|
mapped_file_(nullptr), phdr_table_(nullptr), phdr_num_(0) {
|
||||||
|
memset(&header_, 0, sizeof(header_));
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfReader::~ElfReader() {
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ElfReader::Open(const char* path) {
|
||||||
|
path_ = path;
|
||||||
|
|
||||||
|
struct stat sb;
|
||||||
|
fd_ = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd_ < 0) {
|
||||||
|
LOGE("Cannot open %s: %s", path, strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fstat(fd_, &sb) < 0) {
|
||||||
|
LOGE("Cannot stat %s: %s", path, strerror(errno));
|
||||||
|
close(fd_);
|
||||||
|
fd_ = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_size_ = sb.st_size;
|
||||||
|
|
||||||
|
// 映射整个文件到内存
|
||||||
|
mapped_file_ = mmap(nullptr, file_size_, PROT_READ, MAP_PRIVATE, fd_, 0);
|
||||||
|
if (mapped_file_ == MAP_FAILED) {
|
||||||
|
LOGE("Cannot mmap %s: %s", path, strerror(errno));
|
||||||
|
close(fd_);
|
||||||
|
fd_ = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ElfReader::Read() {
|
||||||
|
if (!ReadElfHeader()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VerifyElfHeader()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReadProgramHeaders()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ElfReader::Close() {
|
||||||
|
if (mapped_file_ != nullptr && mapped_file_ != MAP_FAILED) {
|
||||||
|
munmap(mapped_file_, file_size_);
|
||||||
|
mapped_file_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fd_ >= 0) {
|
||||||
|
close(fd_);
|
||||||
|
fd_ = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phdr_table_ != nullptr) {
|
||||||
|
free(phdr_table_);
|
||||||
|
phdr_table_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ElfReader::ReadElfHeader() {
|
||||||
|
if (file_size_ < sizeof(ElfW(Ehdr))) {
|
||||||
|
LOGE("File too small for ELF header");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(&header_, mapped_file_, sizeof(header_));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ElfReader::VerifyElfHeader() {
|
||||||
|
if (memcmp(header_.e_ident, ELFMAG, SELFMAG) != 0) {
|
||||||
|
LOGE("Invalid ELF magic");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_.e_ident[EI_CLASS] != ELFCLASS64) {
|
||||||
|
LOGE("Not a 64-bit ELF file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_.e_machine != EM_AARCH64) {
|
||||||
|
LOGE("Not an ARM64 ELF file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_.e_version != EV_CURRENT) {
|
||||||
|
LOGE("Invalid ELF version");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_.e_type != ET_DYN) {
|
||||||
|
LOGE("Not a shared object");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("ELF Header: type=%d, machine=%d, entry=0x%llx, phoff=0x%llx, phnum=%d",
|
||||||
|
header_.e_type, header_.e_machine, (unsigned long long)header_.e_entry,
|
||||||
|
(unsigned long long)header_.e_phoff, header_.e_phnum);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ElfReader::ReadProgramHeaders() {
|
||||||
|
phdr_num_ = header_.e_phnum;
|
||||||
|
|
||||||
|
if (phdr_num_ == 0) {
|
||||||
|
LOGE("No program headers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header_.e_phentsize != sizeof(ElfW(Phdr))) {
|
||||||
|
LOGE("Invalid program header size");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size = phdr_num_ * sizeof(ElfW(Phdr));
|
||||||
|
|
||||||
|
if (header_.e_phoff + size > file_size_) {
|
||||||
|
LOGE("Program headers out of file bounds");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
phdr_table_ = static_cast<ElfW(Phdr)*>(malloc(size));
|
||||||
|
if (phdr_table_ == nullptr) {
|
||||||
|
LOGE("Cannot allocate memory for program headers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(phdr_table_, static_cast<char*>(mapped_file_) + header_.e_phoff, size);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
47
module/src/main/cpp/mylinker/include/common.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <elf.h>
|
||||||
|
#include <link.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#define LOG_TAG "CustomLinker"
|
||||||
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
#if defined(__LP64__)
|
||||||
|
#define ELFW(what) ELF64_ ## what
|
||||||
|
#else
|
||||||
|
#define ELFW(what) ELF32_ ## what
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PAGE_SIZE 4096
|
||||||
|
#define PAGE_MASK (~(PAGE_SIZE - 1))
|
||||||
|
#define PAGE_START(addr) ((addr) & PAGE_MASK)
|
||||||
|
#define PAGE_END(addr) PAGE_START((addr) + PAGE_SIZE - 1)
|
||||||
|
#define PAGE_OFFSET(addr) ((addr) & (PAGE_SIZE - 1))
|
||||||
|
|
||||||
|
// 权限标志转换
|
||||||
|
#define PFLAGS_TO_PROT(x) (((x) & PF_R) ? PROT_READ : 0) | \
|
||||||
|
(((x) & PF_W) ? PROT_WRITE : 0) | \
|
||||||
|
(((x) & PF_X) ? PROT_EXEC : 0)
|
||||||
|
|
||||||
|
struct soinfo;
|
||||||
|
class ElfReader;
|
||||||
|
class MemoryManager;
|
||||||
|
class Relocator;
|
||||||
27
module/src/main/cpp/mylinker/include/elf_loader.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "elf_reader.h"
|
||||||
|
#include "memory_manager.h"
|
||||||
|
#include "soinfo_manager.h"
|
||||||
|
#include "relocator.h"
|
||||||
|
|
||||||
|
class ElfLoader {
|
||||||
|
public:
|
||||||
|
ElfLoader();
|
||||||
|
~ElfLoader();
|
||||||
|
|
||||||
|
bool LoadLibrary(const char* path);
|
||||||
|
|
||||||
|
void CallConstructors();
|
||||||
|
|
||||||
|
void* GetSymbol(const char* name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<ElfReader> reader_;
|
||||||
|
std::unique_ptr<MemoryManager> memory_manager_;
|
||||||
|
std::unique_ptr<SoinfoManager> soinfo_manager_;
|
||||||
|
std::unique_ptr<Relocator> relocator_;
|
||||||
|
|
||||||
|
soinfo* loaded_si_;
|
||||||
|
};
|
||||||
38
module/src/main/cpp/mylinker/include/elf_reader.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
class ElfReader {
|
||||||
|
public:
|
||||||
|
ElfReader();
|
||||||
|
~ElfReader();
|
||||||
|
|
||||||
|
bool Open(const char* path);
|
||||||
|
bool Read();
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
const ElfW(Ehdr)* GetHeader() const { return &header_; }
|
||||||
|
const ElfW(Phdr)* GetProgramHeaders() const { return phdr_table_; }
|
||||||
|
size_t GetProgramHeaderCount() const { return phdr_num_; }
|
||||||
|
|
||||||
|
const char* GetPath() const { return path_.c_str(); }
|
||||||
|
int GetFd() const { return fd_; }
|
||||||
|
size_t GetFileSize() const { return file_size_; }
|
||||||
|
void* GetMappedAddr() const { return mapped_file_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ReadElfHeader();
|
||||||
|
bool ReadProgramHeaders();
|
||||||
|
bool VerifyElfHeader();
|
||||||
|
|
||||||
|
std::string path_;
|
||||||
|
int fd_;
|
||||||
|
size_t file_size_;
|
||||||
|
off64_t file_offset_;
|
||||||
|
|
||||||
|
void* mapped_file_;
|
||||||
|
|
||||||
|
ElfW(Ehdr) header_;
|
||||||
|
ElfW(Phdr)* phdr_table_;
|
||||||
|
size_t phdr_num_;
|
||||||
|
};
|
||||||
34
module/src/main/cpp/mylinker/include/memory_manager.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
class MemoryManager {
|
||||||
|
public:
|
||||||
|
MemoryManager();
|
||||||
|
~MemoryManager();
|
||||||
|
|
||||||
|
bool ReserveAddressSpace(const ElfW(Phdr)* phdr_table, size_t phdr_num);
|
||||||
|
|
||||||
|
bool LoadSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num,
|
||||||
|
void* mapped_file, size_t file_size);
|
||||||
|
|
||||||
|
bool FindPhdr(const ElfW(Phdr)* phdr_table, size_t phdr_num);
|
||||||
|
|
||||||
|
bool ProtectSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num);
|
||||||
|
|
||||||
|
void* GetLoadStart() const { return load_start_; }
|
||||||
|
size_t GetLoadSize() const { return load_size_; }
|
||||||
|
ElfW(Addr) GetLoadBias() const { return load_bias_; }
|
||||||
|
const ElfW(Phdr)* GetLoadedPhdr() const { return loaded_phdr_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool CheckPhdr(ElfW(Addr) loaded, const ElfW(Phdr)* phdr_table, size_t phdr_num);
|
||||||
|
size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table,
|
||||||
|
size_t phdr_count,
|
||||||
|
ElfW(Addr)* min_vaddr);
|
||||||
|
|
||||||
|
void* load_start_;
|
||||||
|
size_t load_size_;
|
||||||
|
ElfW(Addr) load_bias_;
|
||||||
|
const ElfW(Phdr)* loaded_phdr_;
|
||||||
|
};
|
||||||
17
module/src/main/cpp/mylinker/include/mylinker.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
__attribute__((visibility("default"))) bool mylinker_load_library(const char* library_path, JavaVM* vm);
|
||||||
|
|
||||||
|
__attribute__((visibility("default"))) void* mylinker_get_symbol(const char* library_path, const char* symbol_name);
|
||||||
|
|
||||||
|
__attribute__((visibility("default"))) void mylinker_cleanup();
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
25
module/src/main/cpp/mylinker/include/relocator.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "soinfo_manager.h"
|
||||||
|
|
||||||
|
class Relocator {
|
||||||
|
public:
|
||||||
|
Relocator();
|
||||||
|
~Relocator();
|
||||||
|
|
||||||
|
bool RelocateImage(soinfo* si);
|
||||||
|
|
||||||
|
bool LinkImage(soinfo* si);
|
||||||
|
|
||||||
|
uint32_t gnu_hash(const char* name);
|
||||||
|
unsigned elf_hash(const char* name);
|
||||||
|
|
||||||
|
ElfW(Sym)* gnu_lookup(uint32_t hash, const char* name, soinfo* si);
|
||||||
|
ElfW(Sym)* elf_lookup(unsigned hash, const char* name, soinfo* si);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ProcessRelaRelocation(soinfo* si, const ElfW(Rela)* rela);
|
||||||
|
|
||||||
|
ElfW(Addr) FindSymbolAddress(const char* name, soinfo* si);
|
||||||
|
};
|
||||||
75
module/src/main/cpp/mylinker/include/soinfo_manager.h
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
// soinfo结构体定义(简化版)
|
||||||
|
struct soinfo {
|
||||||
|
const char* name;
|
||||||
|
ElfW(Addr) base;
|
||||||
|
size_t size;
|
||||||
|
ElfW(Addr) load_bias;
|
||||||
|
|
||||||
|
const ElfW(Phdr)* phdr;
|
||||||
|
size_t phnum;
|
||||||
|
|
||||||
|
ElfW(Addr) entry;
|
||||||
|
|
||||||
|
// Dynamic段信息
|
||||||
|
ElfW(Dyn)* dynamic;
|
||||||
|
size_t dynamic_count;
|
||||||
|
|
||||||
|
// 符号表相关
|
||||||
|
const char* strtab;
|
||||||
|
ElfW(Sym)* symtab;
|
||||||
|
size_t nbucket;
|
||||||
|
size_t nchain;
|
||||||
|
uint32_t* bucket;
|
||||||
|
uint32_t* chain;
|
||||||
|
|
||||||
|
// 重定位相关
|
||||||
|
ElfW(Rela)* plt_rela;
|
||||||
|
size_t plt_rela_count;
|
||||||
|
ElfW(Rela)* rela;
|
||||||
|
size_t rela_count;
|
||||||
|
|
||||||
|
// GNU hash
|
||||||
|
size_t gnu_nbucket;
|
||||||
|
uint32_t* gnu_bucket;
|
||||||
|
uint32_t* gnu_chain;
|
||||||
|
uint32_t gnu_maskwords;
|
||||||
|
uint32_t gnu_shift2;
|
||||||
|
ElfW(Addr)* gnu_bloom_filter;
|
||||||
|
|
||||||
|
// 初始化函数
|
||||||
|
void (*init_func)();
|
||||||
|
void (**init_array)();
|
||||||
|
size_t init_array_count;
|
||||||
|
void (**fini_array)();
|
||||||
|
size_t fini_array_count;
|
||||||
|
|
||||||
|
// 依赖库
|
||||||
|
std::vector<std::string> needed_libs;
|
||||||
|
|
||||||
|
uint32_t flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SoinfoManager {
|
||||||
|
public:
|
||||||
|
SoinfoManager();
|
||||||
|
~SoinfoManager();
|
||||||
|
|
||||||
|
soinfo* GetOrCreateSoinfo(const char* name);
|
||||||
|
|
||||||
|
bool UpdateSoinfo(soinfo* si, MemoryManager* mm, ElfReader* reader);
|
||||||
|
|
||||||
|
bool PrelinkImage(soinfo* si);
|
||||||
|
|
||||||
|
soinfo* FindSoinfo(const char* name);
|
||||||
|
soinfo* GetCurrentSoinfo();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ParseDynamic(soinfo* si);
|
||||||
|
void ApplyRelaSections(soinfo* si);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<soinfo>> soinfo_map_;
|
||||||
|
};
|
||||||
21
module/src/main/cpp/mylinker/include/utils.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
namespace Utils {
|
||||||
|
bool safe_add(off64_t* out, off64_t a, size_t b);
|
||||||
|
|
||||||
|
soinfo* get_soinfo(const char* so_name);
|
||||||
|
|
||||||
|
void* getMapData(int fd, off64_t base_offset, size_t elf_offset, size_t size);
|
||||||
|
|
||||||
|
ElfW(Addr) get_export_func(const char* path, const char* func_name);
|
||||||
|
|
||||||
|
inline size_t page_start(size_t addr) {
|
||||||
|
return addr & ~(PAGE_SIZE - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline size_t page_offset(size_t addr) {
|
||||||
|
return addr & (PAGE_SIZE - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
module/src/main/cpp/mylinker/main.cpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#include "elf_loader.h"
|
||||||
|
|
||||||
|
int (*yuuki_test_func) (int, int) = nullptr;
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("Usage: %s <so_file_path>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("Starting custom linker for: %s", argv[1]);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (access(argv[1], F_OK) != 0) {
|
||||||
|
LOGE("File does not exist: %s", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access(argv[1], R_OK) != 0) {
|
||||||
|
LOGE("File is not readable: %s", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfLoader loader;
|
||||||
|
if (loader.LoadLibrary(argv[1])) {
|
||||||
|
printf("Successfully loaded %s\n", argv[1]);
|
||||||
|
|
||||||
|
void* test_func = loader.GetSymbol("yuuki_test");
|
||||||
|
if (test_func) {
|
||||||
|
printf("Found yuuki_test function at %p\n", test_func);
|
||||||
|
yuuki_test_func = (int (*)(int, int)) test_func;
|
||||||
|
|
||||||
|
// 测试函数调用
|
||||||
|
printf("Testing function call: 1 + 1 = %d\n", yuuki_test_func(1, 1));
|
||||||
|
printf("Testing function call: 5 + 3 = %d\n", yuuki_test_func(5, 3));
|
||||||
|
} else {
|
||||||
|
printf("Failed to find yuuki_test function\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
printf("Failed to load %s\n", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// logcat | grep "CustomLinker"
|
||||||
|
// logcat | grep "TEST_SO"
|
||||||
|
// ./data/local/tmp/elf_loader /storage/emulated/0/yuuki/test.so
|
||||||
241
module/src/main/cpp/mylinker/memory_manager.cpp
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#include "memory_manager.h"
|
||||||
|
|
||||||
|
MemoryManager::MemoryManager() : load_start_(nullptr), load_size_(0),
|
||||||
|
load_bias_(0), loaded_phdr_(nullptr) {
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryManager::~MemoryManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MemoryManager::ReserveAddressSpace(const ElfW(Phdr)* phdr_table, size_t phdr_num) {
|
||||||
|
ElfW(Addr) min_vaddr;
|
||||||
|
load_size_ = phdr_table_get_load_size(phdr_table, phdr_num, &min_vaddr);
|
||||||
|
|
||||||
|
if (load_size_ == 0) {
|
||||||
|
LOGE("No loadable segments");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Load size: 0x%zx, min_vaddr: 0x%llx", load_size_, (unsigned long long)min_vaddr);
|
||||||
|
|
||||||
|
void* start = mmap(nullptr, load_size_, PROT_NONE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||||
|
if (start == MAP_FAILED) {
|
||||||
|
LOGE("Cannot reserve %zu bytes: %s", load_size_, strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_start_ = start;
|
||||||
|
load_bias_ = reinterpret_cast<ElfW(Addr)>(start) - min_vaddr;
|
||||||
|
|
||||||
|
LOGD("Reserved address space at %p, bias: 0x%llx", start, (unsigned long long)load_bias_);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MemoryManager::LoadSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num,
|
||||||
|
void* mapped_file, size_t file_size) {
|
||||||
|
LOGD("Starting LoadSegments: phdr_num=%zu, file_size=%zu", phdr_num, file_size);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < phdr_num; ++i) {
|
||||||
|
const ElfW(Phdr)* phdr = &phdr_table[i];
|
||||||
|
|
||||||
|
if (phdr->p_type != PT_LOAD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Processing LOAD segment %zu: vaddr=0x%llx, memsz=0x%llx, filesz=0x%llx, offset=0x%llx",
|
||||||
|
i, (unsigned long long)phdr->p_vaddr, (unsigned long long)phdr->p_memsz,
|
||||||
|
(unsigned long long)phdr->p_filesz, (unsigned long long)phdr->p_offset);
|
||||||
|
|
||||||
|
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
|
||||||
|
ElfW(Addr) seg_end = seg_start + phdr->p_memsz;
|
||||||
|
|
||||||
|
ElfW(Addr) seg_page_start = PAGE_START(seg_start);
|
||||||
|
ElfW(Addr) seg_page_end = PAGE_END(seg_end);
|
||||||
|
|
||||||
|
ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz;
|
||||||
|
|
||||||
|
ElfW(Addr) file_start = phdr->p_offset;
|
||||||
|
ElfW(Addr) file_end = file_start + phdr->p_filesz;
|
||||||
|
|
||||||
|
ElfW(Addr) file_page_start = PAGE_START(file_start);
|
||||||
|
|
||||||
|
if (file_end > file_size) {
|
||||||
|
LOGE("Invalid file size: file_end=0x%llx > file_size=0x%zx", (unsigned long long)file_end, file_size);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phdr->p_filesz > 0) {
|
||||||
|
void* seg_addr = reinterpret_cast<void*>(seg_page_start);
|
||||||
|
size_t seg_size = seg_page_end - seg_page_start;
|
||||||
|
|
||||||
|
if (mprotect(seg_addr, seg_size, PROT_READ | PROT_WRITE) < 0) {
|
||||||
|
LOGE("Cannot mprotect for loading: %s", strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* src = static_cast<char*>(mapped_file) + phdr->p_offset;
|
||||||
|
void* dst = reinterpret_cast<void*>(seg_start);
|
||||||
|
|
||||||
|
LOGD("Copying segment %zu: src=%p (offset=0x%llx), dst=%p, size=0x%llx",
|
||||||
|
i, src, (unsigned long long)phdr->p_offset, dst, (unsigned long long)phdr->p_filesz);
|
||||||
|
|
||||||
|
if (static_cast<char*>(src) + phdr->p_filesz > static_cast<char*>(mapped_file) + file_size) {
|
||||||
|
LOGE("Source copy would exceed file bounds");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reinterpret_cast<ElfW(Addr)>(dst) + phdr->p_filesz > seg_page_end) {
|
||||||
|
LOGE("Destination copy would exceed segment bounds");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(dst, src, phdr->p_filesz);
|
||||||
|
|
||||||
|
LOGD("Successfully copied segment %zu", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phdr->p_memsz > phdr->p_filesz) {
|
||||||
|
ElfW(Addr) bss_start = seg_start + phdr->p_filesz;
|
||||||
|
ElfW(Addr) bss_end = seg_start + phdr->p_memsz;
|
||||||
|
size_t bss_size = bss_end - bss_start;
|
||||||
|
|
||||||
|
LOGD("Zeroing BSS: start=0x%llx, size=0x%zx", (unsigned long long)bss_start, bss_size);
|
||||||
|
memset(reinterpret_cast<void*>(bss_start), 0, bss_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfW(Addr) aligned_file_end = PAGE_END(seg_file_end);
|
||||||
|
if (seg_page_end > aligned_file_end) {
|
||||||
|
size_t zeromap_size = seg_page_end - aligned_file_end;
|
||||||
|
void* zeromap = mmap(reinterpret_cast<void*>(aligned_file_end),
|
||||||
|
zeromap_size,
|
||||||
|
PROT_READ | PROT_WRITE,
|
||||||
|
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE,
|
||||||
|
-1, 0);
|
||||||
|
if (zeromap == MAP_FAILED) {
|
||||||
|
LOGE("Cannot zero fill gap: %s", strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOGD("Zero-filled gap: addr=%p, size=0x%zx", zeromap, zeromap_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("LoadSegments complete");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MemoryManager::FindPhdr(const ElfW(Phdr)* phdr_table, size_t phdr_num) {
|
||||||
|
const ElfW(Phdr)* phdr_limit = phdr_table + phdr_num;
|
||||||
|
|
||||||
|
for (const ElfW(Phdr)* phdr = phdr_table; phdr < phdr_limit; ++phdr) {
|
||||||
|
if (phdr->p_type == PT_PHDR) {
|
||||||
|
return CheckPhdr(load_bias_ + phdr->p_vaddr, phdr_table, phdr_num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ElfW(Phdr)* phdr = phdr_table; phdr < phdr_limit; ++phdr) {
|
||||||
|
if (phdr->p_type == PT_LOAD) {
|
||||||
|
if (phdr->p_offset == 0) {
|
||||||
|
ElfW(Addr) elf_addr = load_bias_ + phdr->p_vaddr;
|
||||||
|
const ElfW(Ehdr)* ehdr = reinterpret_cast<const ElfW(Ehdr)*>(elf_addr);
|
||||||
|
ElfW(Addr) offset = ehdr->e_phoff;
|
||||||
|
return CheckPhdr(reinterpret_cast<ElfW(Addr)>(ehdr) + offset, phdr_table, phdr_num);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Using original phdr_table as loaded_phdr");
|
||||||
|
loaded_phdr_ = phdr_table;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MemoryManager::ProtectSegments(const ElfW(Phdr)* phdr_table, size_t phdr_num) {
|
||||||
|
for (size_t i = 0; i < phdr_num; ++i) {
|
||||||
|
const ElfW(Phdr)* phdr = &phdr_table[i];
|
||||||
|
|
||||||
|
if (phdr->p_type != PT_LOAD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
|
||||||
|
ElfW(Addr) seg_page_start = PAGE_START(seg_start);
|
||||||
|
ElfW(Addr) seg_page_end = PAGE_END(seg_start + phdr->p_memsz);
|
||||||
|
|
||||||
|
int prot = PFLAGS_TO_PROT(phdr->p_flags);
|
||||||
|
|
||||||
|
if (mprotect(reinterpret_cast<void*>(seg_page_start),
|
||||||
|
seg_page_end - seg_page_start, prot) < 0) {
|
||||||
|
LOGE("Cannot protect segment %zu: %s", i, strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Protected segment %zu: 0x%llx-0x%llx, prot: %d",
|
||||||
|
i, (unsigned long long)seg_page_start, (unsigned long long)seg_page_end, prot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MemoryManager::CheckPhdr(ElfW(Addr) loaded, const ElfW(Phdr)* phdr_table, size_t phdr_num) {
|
||||||
|
const ElfW(Phdr)* phdr_limit = phdr_table + phdr_num;
|
||||||
|
ElfW(Addr) loaded_end = loaded + (phdr_num * sizeof(ElfW(Phdr)));
|
||||||
|
|
||||||
|
for (const ElfW(Phdr)* phdr = phdr_table; phdr < phdr_limit; ++phdr) {
|
||||||
|
if (phdr->p_type != PT_LOAD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
|
||||||
|
ElfW(Addr) seg_end = phdr->p_filesz + seg_start;
|
||||||
|
|
||||||
|
if (seg_start <= loaded && loaded_end <= seg_end) {
|
||||||
|
loaded_phdr_ = reinterpret_cast<const ElfW(Phdr)*>(loaded);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGE("Loaded phdr %p not in loadable segment", reinterpret_cast<void*>(loaded));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t MemoryManager::phdr_table_get_load_size(const ElfW(Phdr)* phdr_table,
|
||||||
|
size_t phdr_count,
|
||||||
|
ElfW(Addr)* min_vaddr) {
|
||||||
|
ElfW(Addr) min_addr = UINTPTR_MAX;
|
||||||
|
ElfW(Addr) max_addr = 0;
|
||||||
|
|
||||||
|
bool found_pt_load = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < phdr_count; ++i) {
|
||||||
|
const ElfW(Phdr)* phdr = &phdr_table[i];
|
||||||
|
|
||||||
|
if (phdr->p_type != PT_LOAD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
found_pt_load = true;
|
||||||
|
|
||||||
|
if (phdr->p_vaddr < min_addr) {
|
||||||
|
min_addr = phdr->p_vaddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phdr->p_vaddr + phdr->p_memsz > max_addr) {
|
||||||
|
max_addr = phdr->p_vaddr + phdr->p_memsz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found_pt_load) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
min_addr = PAGE_START(min_addr);
|
||||||
|
max_addr = PAGE_END(max_addr);
|
||||||
|
|
||||||
|
if (min_vaddr != nullptr) {
|
||||||
|
*min_vaddr = min_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max_addr - min_addr;
|
||||||
|
}
|
||||||