├── HookwormForAndroid ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── resource │ │ ├── META-INF │ │ │ └── com │ │ │ │ └── google │ │ │ │ └── android │ │ │ │ ├── updater-script │ │ │ │ └── update-binary │ │ ├── uninstall.sh │ │ ├── versions.properties │ │ ├── post-fs-data.sh │ │ ├── riru.sh │ │ ├── verify.sh │ │ └── customize.sh │ │ ├── java │ │ └── com │ │ │ └── wuyr │ │ │ └── hookworm │ │ │ ├── core │ │ │ ├── ModuleInfo.java │ │ │ ├── Main.java │ │ │ └── Hookworm.kt │ │ │ ├── extensions │ │ │ ├── SimpleActivityLifecycleCallbacks.kt │ │ │ ├── PhoneLayoutInflater.kt │ │ │ └── HookwormExtensions.kt │ │ │ └── utils │ │ │ ├── Impactor.java │ │ │ └── ReflectUtil.kt │ │ └── cpp │ │ ├── CMakeLists.txt │ │ ├── riru.h │ │ └── main.cpp ├── proguard-rules.pro ├── module.properties.sample ├── module.properties └── build.gradle.kts ├── settings.gradle ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ └── colors.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── wuyr │ │ │ └── hookwormforwanandroid │ │ │ ├── ImageAdapter.kt │ │ │ └── Main.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── wuyr │ │ │ └── hookwormforwanandroid │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── wuyr │ │ └── hookwormforwanandroid │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /HookwormForAndroid/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/META-INF/com/google/android/updater-script: -------------------------------------------------------------------------------- 1 | #MAGISK 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':HookwormForAndroid' 2 | include ':app' 3 | rootProject.name = "Hookworm For WanAndroid" -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Hookworm For WanAndroid 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyr/HookwormForWanAndroidTest/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | MODDIR=${0%/*} 3 | [ ! -f "$MODDIR/riru.sh" ] && exit 1 4 | . $MODDIR/riru.sh 5 | 6 | rm -rf "$RIRU_MODULE_PATH" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jan 25 10:35:31 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 7 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/versions.properties: -------------------------------------------------------------------------------- 1 | compileSdkVersion=30 2 | 3 | minSdkVersion=23 4 | 5 | targetSdkVersion=30 6 | 7 | cmakeVersion=3.10.2 8 | 9 | core-ktxVersion=1.3.2 10 | 11 | maxRiruApiVersionCode=10 12 | 13 | minRiruApiVersionCode=9 14 | 15 | minRiruApiVersionName=v22.0 -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/post-fs-data.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | MODDIR=${0%/*} 3 | [ ! -f "$MODDIR/riru.sh" ] && exit 1 4 | . $MODDIR/riru.sh 5 | 6 | # Rename module.prop.new 7 | if [ -f "$RIRU_MODULE_PATH/module.prop.new" ]; then 8 | rm "$RIRU_MODULE_PATH/module.prop" 9 | mv "$RIRU_MODULE_PATH/module.prop.new" "$RIRU_MODULE_PATH/module.prop" 10 | fi 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/wuyr/hookwormforwanandroid/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookwormforwanandroid 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/java/com/wuyr/hookworm/core/ModuleInfo.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookworm.core; 2 | 3 | /** 4 | * The methods return value will automatically filled before compilation. DO NOT MODIFY 5 | * 6 | * @author wuyr 7 | * @github https://github.com/wuyr/HookwormForAndroid 8 | * @since 2020-09-15 下午7:12 9 | */ 10 | class ModuleInfo { 11 | 12 | static String getDexPath() { 13 | return ""; 14 | } 15 | 16 | static String getMainClass() { 17 | return ""; 18 | } 19 | 20 | static String getSOPath() { 21 | return ""; 22 | } 23 | 24 | static boolean hasSOFile() { 25 | return false; 26 | } 27 | 28 | static boolean isDebug() { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/wuyr/hookwormforwanandroid/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookwormforwanandroid 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.wuyr.hookwormforwanandroid", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/java/com/wuyr/hookworm/core/Main.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookworm.core; 2 | 3 | import android.util.Log; 4 | 5 | import java.lang.reflect.Method; 6 | 7 | /** 8 | * Hookworm core file, called by JNI. DO NOT MODIFY 9 | * 10 | * @author wuyr 11 | * @github https://github.com/wuyr/HookwormForAndroid 12 | * @since 2020-09-15 下午6:45 13 | */ 14 | public class Main { 15 | 16 | public static final String TAG = "Hookworm"; 17 | 18 | public static void main(String processName) { 19 | try { 20 | Hookworm.init(); 21 | Class mainClass = Class.forName(ModuleInfo.getMainClass()); 22 | Method mainMethod = mainClass.getMethod("main", String.class); 23 | mainMethod.invoke(null, processName); 24 | } catch (Throwable t) { 25 | Log.e(TAG, t.toString(), t); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /HookwormForAndroid/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontobfuscate 24 | 25 | -keep class com.wuyr.hookworm.core.Main { 26 | public void main(java.lang.String); 27 | } -------------------------------------------------------------------------------- /HookwormForAndroid/module.properties.sample: -------------------------------------------------------------------------------- 1 | # 模块唯一标识,只能使用字母 + 下划线组合,如:my_module_id 2 | moduleId= 3 | 4 | # 模块名称,自由填写 5 | moduleName= 6 | 7 | # 模块作者,自由填写 8 | moduleAuthor= 9 | 10 | # 模块描述,自由填写 11 | moduleDescription= 12 | 13 | # 模块版本名,自由填写 14 | moduleVersionName= 15 | 16 | # 模块版本号,只能填数字 17 | moduleVersionCode= 18 | 19 | # 扩展的so文件存放目录名,一般情况下不用填写,留空即可 20 | # 这是以后为了反检测用的 21 | libraryPath= 22 | 23 | # 主入口类名,例:com.demo.ModuleMain 24 | # 注意!此类的 main(String) 方法不能被混淆! 25 | # 如开启混淆,请在proguard-rules.pro中加入: 26 | # -keep class com.demo.ModuleMain { 27 | # public void main(java.lang.String); 28 | # } 29 | # com.demo.ModuleMain 替换成你自己的入口类全称 30 | moduleMainClass= 31 | 32 | # 目标进程名/包名,即要寄生的目标。不填写则寄生所有进程。 33 | # 同时寄生多个目标,用 ; 分隔,如: com.demo.application1;com.demo.application2 34 | targetProcessName= 35 | 36 | # 编译完毕自动安装模块(需要手机已通过adb连接到电脑(只能连一台),并已安装Magisk和Riru模块!) 37 | # 1为开启,其他值为关闭 38 | automaticInstallation=0 39 | 40 | # Debug提供最小化安装(除首次安装外免重启)能力,可以快速测试模块(注意:正式发布时务必关闭此选项!) 41 | # 开启此选项需先开启automaticInstallation 42 | debug=0 -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/java/com/wuyr/hookworm/extensions/SimpleActivityLifecycleCallbacks.kt: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookworm.extensions 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | 7 | /** 8 | * @author wuyr 9 | * @github https://github.com/wuyr/HookwormForAndroid 10 | * @since 2020-09-22 上午10:59 11 | */ 12 | open class SimpleActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { 13 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { 14 | } 15 | 16 | override fun onActivityStarted(activity: Activity) { 17 | } 18 | 19 | override fun onActivityResumed(activity: Activity) { 20 | } 21 | 22 | override fun onActivityPaused(activity: Activity) { 23 | } 24 | 25 | override fun onActivityStopped(activity: Activity) { 26 | } 27 | 28 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { 29 | } 30 | 31 | override fun onActivityDestroyed(activity: Activity) { 32 | } 33 | } -------------------------------------------------------------------------------- /HookwormForAndroid/module.properties: -------------------------------------------------------------------------------- 1 | # 模块唯一标识,只能使用字母 + 下划线组合,如:my_module_id 2 | moduleId=hookworm_for_wanandroid 3 | 4 | # 模块名称,自由填写 5 | moduleName=Hookworm For WanAndroid 6 | 7 | # 模块作者,自由填写 8 | moduleAuthor=wuyr 9 | 10 | # 模块描述,自由填写 11 | moduleDescription=Hookworm For WanAndroid 12 | 13 | # 模块版本名,自由填写 14 | moduleVersionName=v1.0.0 15 | 16 | # 模块版本号,只能填数字 17 | moduleVersionCode=1 18 | 19 | # 扩展的so文件存放目录名,一般情况下不用填写,留空即可 20 | # 这是以后为了反检测用的 21 | libraryPath= 22 | 23 | # 主入口类名,例:com.demo.ModuleMain 24 | # 注意!此类的 main(String) 方法不能被混淆! 25 | # 如开启混淆,请在proguard-rules.pro中加入: 26 | # -keep class com.demo.ModuleMain { 27 | # public void main(java.lang.String); 28 | # } 29 | # com.demo.ModuleMain 替换成你自己的入口类全称 30 | moduleMainClass=com.wuyr.hookwormforwanandroid.Main 31 | 32 | # 目标进程名/包名,即要寄生的目标。不填写则寄生所有进程。 33 | # 同时寄生多个目标,用 ; 分隔,如: com.demo.application1;com.demo.application2 34 | targetProcessName=per.goweii.wanandroid 35 | 36 | # 编译完毕自动安装模块(需要手机已通过adb连接到电脑(只能连一台),并已安装Magisk和Riru模块!) 37 | # 1为开启,其他值为关闭 38 | automaticInstallation=1 39 | 40 | # Debug提供最小化安装(除首次安装外免重启)能力,可以快速测试模块(注意:正式发布时务必关闭此选项!) 41 | # 开启此选项需先开启automaticInstallation 42 | debug=1 -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/java/com/wuyr/hookworm/utils/Impactor.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookworm.utils; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | /** 6 | * @author wuyr 7 | * @github https://github.com/wuyr/HookwormForAndroid 8 | * @since 2020-12-23 上午12:25 9 | */ 10 | public class Impactor { 11 | 12 | /** 13 | * Copy from me.weishu.reflection.BootstrapClass(me.weishu:free_reflection) 14 | */ 15 | public static void hiddenApiExemptions() throws Throwable { 16 | Method forName = Class.class.getDeclaredMethod("forName", String.class); 17 | Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class); 18 | 19 | Class VMRuntime = (Class) forName.invoke(null, "dalvik.system.VMRuntime"); 20 | Method getRuntime = (Method) getDeclaredMethod.invoke(VMRuntime, "getRuntime", null); 21 | Method setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(VMRuntime, "setHiddenApiExemptions", new Class[]{String[].class}); 22 | setHiddenApiExemptions.invoke(getRuntime.invoke(null), new Object[]{new String[]{"L"}}); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.2" 9 | 10 | defaultConfig { 11 | applicationId "com.wuyr.hookwormforwanandroid" 12 | minSdkVersion 23 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 37 | compileOnly 'androidx.core:core-ktx:1.3.2' 38 | compileOnly 'androidx.appcompat:appcompat:1.2.0' 39 | compileOnly 'com.google.android.material:material:1.2.1' 40 | compileOnly 'com.github.bumptech.glide:glide:4.11.0' 41 | implementation project(path: ':HookwormForAndroid') 42 | testImplementation 'junit:junit:4.13.1' 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 45 | } -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4.1) 2 | 3 | if (NOT DEFINED MODULE_NAME) 4 | message(FATAL_ERROR "MODULE_NAME is not set") 5 | endif () 6 | 7 | add_definitions(-DDEX_PATH=${DEX_PATH}) 8 | add_definitions(-DMAIN_CLASS=${MAIN_CLASS}) 9 | add_definitions(-DPROCESS_NAME_ARRAY=${PROCESS_NAME_ARRAY}) 10 | add_definitions(-DPROCESS_NAME_ARRAY_SIZE=${PROCESS_NAME_ARRAY_SIZE}) 11 | add_definitions(-DRIRU_MODULE) 12 | add_definitions(-DRIRU_MODULE_API_VERSION=${RIRU_MODULE_API_VERSION}) 13 | add_definitions(-DRIRU_MODULE_VERSION=${RIRU_MODULE_VERSION}) 14 | add_definitions(-DRIRU_MODULE_VERSION_NAME=${RIRU_MODULE_VERSION_NAME}) 15 | 16 | message("Build type: ${CMAKE_BUILD_TYPE}") 17 | 18 | set(CMAKE_CXX_STANDARD 11) 19 | 20 | set(LINKER_FLAGS "-ffixed-x18 -Wl,--hash-style=both") 21 | set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections") 22 | 23 | if (CMAKE_BUILD_TYPE STREQUAL "Release") 24 | set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden") 25 | set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections") 26 | else () 27 | set(C_FLAGS "${C_FLAGS} -O0") 28 | endif () 29 | 30 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}") 31 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS}") 32 | 33 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") 34 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}") 35 | 36 | add_library(${MODULE_NAME} SHARED main.cpp) 37 | set_target_properties(${MODULE_NAME} PROPERTIES LINK_FLAGS_RELEASE -s) 38 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/riru.sh: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | RIRU_PATH="/data/adb/riru" 3 | RIRU_MODULE_ID="%%%RIRU_MODULE_ID%%%" 4 | RIRU_MODULE_PATH="$RIRU_PATH/modules/$RIRU_MODULE_ID" 5 | RIRU_SECONTEXT="u:object_r:magisk_file:s0" 6 | 7 | check_riru_version() { 8 | RIRU_MIN_API_VERSION=%%%RIRU_MIN_API_VERSION%%% 9 | RIRU_MIN_VERSION_NAME="%%%RIRU_MIN_VERSION_NAME%%%" 10 | 11 | if [ ! -f "$RIRU_PATH/api_version" ] && [ ! -f "$RIRU_PATH/api_version.new" ]; then 12 | ui_print "*********************************************************" 13 | ui_print "! Riru $RIRU_MIN_VERSION_NAME or above is required" 14 | ui_print "! Please install Riru from Magisk Manager or https://github.com/RikkaApps/Riru/releases" 15 | abort "*********************************************************" 16 | fi 17 | RIRU_API_VERSION=$(cat "$RIRU_PATH/api_version.new") || RIRU_API_VERSION=$(cat "$RIRU_PATH/api_version") || RIRU_API_VERSION=0 18 | [ "$RIRU_API_VERSION" -eq "$RIRU_API_VERSION" ] || RIRU_API_VERSION=0 19 | ui_print "- Riru API version: $RIRU_API_VERSION" 20 | if [ "$RIRU_API_VERSION" -lt $RIRU_MIN_API_VERSION ]; then 21 | ui_print "*********************************************************" 22 | ui_print "! Riru $RIRU_MIN_VERSION_NAME or above is required" 23 | ui_print "! Please upgrade Riru from Magisk Manager or https://github.com/RikkaApps/Riru/releases" 24 | abort "*********************************************************" 25 | fi 26 | } 27 | 28 | check_architecture() { 29 | if [ "$ARCH" != "arm" ] && [ "$ARCH" != "arm64" ] && [ "$ARCH" != "x86" ] && [ "$ARCH" != "x64" ]; then 30 | abort "! Unsupported platform: $ARCH" 31 | else 32 | ui_print "- Device platform: $ARCH" 33 | fi 34 | } 35 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/verify.sh: -------------------------------------------------------------------------------- 1 | TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" 2 | mkdir "$TMPDIR_FOR_VERIFY" 3 | 4 | abort_verify() { 5 | ui_print "*********************************************************" 6 | ui_print "! $1" 7 | ui_print "! This zip may be corrupted, please try downloading again" 8 | abort "*********************************************************" 9 | } 10 | 11 | # extract 12 | extract_dir() { 13 | zip=$1 14 | file=$2 15 | dir=$3 16 | echo "unzip -o $zip $file/*" 17 | unzip -o "$zip" "$file/*" 18 | echo "mkdir -p $dir" 19 | mkdir -p "$dir" 20 | parent_dir=$( 21 | if [ "${file:0:1}" = '/' ]; then echo -ne "/"; else echo -ne ""; fi 22 | echo "${file//\// }" | awk -F " " '{print $1}' 23 | ) 24 | echo "cp -Rv $parent_dir $dir/" 25 | cp -Rv "$parent_dir" "$dir/" 26 | echo "rm -rf $parent_dir" 27 | rm -rf "$parent_dir" 28 | } 29 | 30 | extract() { 31 | zip=$1 32 | file=$2 33 | dir=$3 34 | junk_paths=$4 35 | [ -z "$junk_paths" ] && junk_paths=false 36 | opts="-o" 37 | [ $junk_paths = true ] && opts="-oj" 38 | 39 | file_path="" 40 | hash_path="" 41 | if [ $junk_paths = true ]; then 42 | file_path="$dir/$(basename "$file")" 43 | hash_path="$TMPDIR_FOR_VERIFY/$(basename "$file").sha256sum" 44 | else 45 | file_path="$dir/$file" 46 | hash_path="$TMPDIR_FOR_VERIFY/$file.sha256sum" 47 | fi 48 | 49 | unzip $opts "$zip" "$file" -d "$dir" >&2 50 | [ -f "$file_path" ] || abort_verify "$file not exists" 51 | 52 | unzip $opts "$zip" "$file.sha256sum" -d "$TMPDIR_FOR_VERIFY" >&2 53 | [ -f "$hash_path" ] || abort_verify "$file.sha256sum not exists" 54 | 55 | (echo "$(cat "$hash_path") $file_path" | sha256sum -c -s -) || abort_verify "Failed to verify $file" 56 | ui_print "- Verified $file" >&1 57 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/ 42 | .idea/workspace.xml 43 | .idea/tasks.xml 44 | .idea/gradle.xml 45 | .idea/assetWizardSettings.xml 46 | .idea/dictionaries 47 | .idea/libraries 48 | # Android Studio 3 in .gitignore file. 49 | .idea/caches 50 | .idea/modules.xml 51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 52 | .idea/navEditor.xml 53 | 54 | # Keystore files 55 | # Uncomment the following lines if you do not want to check your keystore files in. 56 | #*.jks 57 | #*.keystore 58 | 59 | # External native build folder generated in Android Studio 2.2 and later 60 | .externalNativeBuild 61 | .cxx/ 62 | 63 | # Google Services (e.g. APIs or Firebase) 64 | # google-services.json 65 | 66 | # Freeline 67 | freeline.py 68 | freeline/ 69 | freeline_project_description.json 70 | 71 | # fastlane 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots 75 | fastlane/test_output 76 | fastlane/readme.md 77 | 78 | # Version control 79 | vcs.xml 80 | 81 | # lint 82 | lint/intermediates/ 83 | lint/generated/ 84 | lint/outputs/ 85 | lint/tmp/ 86 | # lint/reports/ 87 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/wuyr/hookwormforwanandroid/ImageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookwormforwanandroid 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import androidx.viewpager.widget.PagerAdapter 8 | import com.bumptech.glide.Glide 9 | 10 | /** 11 | * @author wuyr 12 | * @github https://github.com/wuyr/HookwormForWanAndroidTest 13 | * @since 2021-01-25 下午2:28 14 | */ 15 | class ImageAdapter(context: Context) : PagerAdapter() { 16 | 17 | private val imageUrls = arrayOf( 18 | "https://c-ssl.duitang.com/uploads/item/201708/13/20170813095305_FSQhj.thumb.700_0.jpeg", 19 | "https://c-ssl.duitang.com/uploads/item/201512/05/20151205212633_nFx3d.thumb.700_0.jpeg", 20 | "https://c-ssl.duitang.com/uploads/item/201606/12/20160612235102_z3dja.thumb.700_0.jpeg", 21 | "https://c-ssl.duitang.com/uploads/item/201707/27/20170727121828_Z5TRA.thumb.700_0.png", 22 | "https://c-ssl.duitang.com/uploads/item/201707/27/20170727122213_3HBaN.thumb.700_0.png", 23 | "https://c-ssl.duitang.com/uploads/item/201512/04/20151204202153_nEUMt.thumb.700_0.jpeg" 24 | ) 25 | 26 | private val imageViews = ArrayList().apply { 27 | imageUrls.forEach { url -> 28 | add(ImageView(context).apply { 29 | scaleType = ImageView.ScaleType.CENTER_CROP 30 | Glide.with(context).load(url).into(this) 31 | }) 32 | } 33 | } 34 | 35 | override fun instantiateItem(container: ViewGroup, position: Int) = 36 | imageViews[position].also { container.addView(it) } 37 | 38 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) = 39 | container.removeView(imageViews[position]) 40 | 41 | override fun getCount() = imageUrls.size 42 | 43 | override fun isViewFromObject(view: View, `object`: Any) = view == `object` 44 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/java/com/wuyr/hookworm/extensions/PhoneLayoutInflater.kt: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookworm.extensions 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import org.xmlpull.v1.XmlPullParser 9 | import kotlin.jvm.Throws 10 | 11 | /** 12 | * @author wuyr 13 | * @github https://github.com/wuyr/HookwormForAndroid 14 | * @since 2020-09-20 下午5:29 15 | */ 16 | class PhoneLayoutInflater : LayoutInflater { 17 | 18 | constructor(context: Context?) : super(context) 19 | constructor(original: LayoutInflater?, newContext: Context?) : super(original, newContext) { 20 | if (original is PhoneLayoutInflater) { 21 | postInflateListener = original.postInflateListener 22 | } 23 | } 24 | 25 | /** 26 | * 在inflate完成后回调 27 | */ 28 | var postInflateListener: ((resourceId: Int, resourceName: String, rootView: View?) -> View?)? = 29 | null 30 | set(value) { 31 | field = value 32 | isInPostInflate = false 33 | } 34 | 35 | private companion object { 36 | private val sClassPrefixList = arrayOf("android.widget.", "android.webkit.", "android.app.") 37 | } 38 | 39 | override fun cloneInContext(newContext: Context?) = 40 | PhoneLayoutInflater(this, newContext) 41 | 42 | @Throws(ClassNotFoundException::class) 43 | override fun onCreateView(name: String?, attrs: AttributeSet?): View? = 44 | sClassPrefixList.forEach { prefix -> 45 | try { 46 | createView(name, prefix, attrs)?.let { return it } 47 | } catch (e: Exception) { 48 | } 49 | }.run { super.onCreateView(name, attrs) } 50 | 51 | private var currentResourceId = 0 52 | private var currentResourceName = "" 53 | 54 | override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View { 55 | currentResourceId = resource 56 | currentResourceName = context.resources.getResourceEntryName(resource) 57 | return super.inflate(resource, root, attachToRoot) 58 | } 59 | 60 | private var isInPostInflate = false 61 | 62 | override fun inflate(parser: XmlPullParser?, root: ViewGroup?, attachToRoot: Boolean): View? = 63 | super.inflate(parser, root, attachToRoot).let { rootView -> 64 | if (!isInPostInflate && postInflateListener != null) { 65 | isInPostInflate = true 66 | postInflateListener?.invoke(currentResourceId, currentResourceName, rootView).also { 67 | isInPostInflate = false 68 | } 69 | } else rootView.also { 70 | currentResourceId = 0 71 | currentResourceName = "" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/META-INF/com/google/android/update-binary: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | 3 | ################# 4 | # Initialization 5 | ################# 6 | 7 | umask 022 8 | 9 | # Global vars 10 | TMPDIR=/dev/tmp 11 | PERSISTDIR=/sbin/.magisk/mirror/persist 12 | 13 | rm -rf $TMPDIR 2>/dev/null 14 | mkdir -p $TMPDIR 15 | 16 | # echo before loading util_functions 17 | ui_print() { echo "$1"; } 18 | 19 | require_new_magisk() { 20 | ui_print "*******************************" 21 | ui_print " Please install Magisk v19.0+! " 22 | ui_print "*******************************" 23 | exit 1 24 | } 25 | 26 | is_legacy_script() { 27 | unzip -l "$ZIPFILE" install.sh | grep -q install.sh 28 | return $? 29 | } 30 | 31 | print_modname() { 32 | local len 33 | len=`echo -n $MODNAME | wc -c` 34 | len=$((len + 2)) 35 | local pounds=`printf "%${len}s" | tr ' ' '*'` 36 | ui_print "$pounds" 37 | ui_print " $MODNAME " 38 | ui_print "$pounds" 39 | ui_print "*******************" 40 | ui_print " Powered by Magisk " 41 | ui_print "*******************" 42 | } 43 | 44 | ############## 45 | # Environment 46 | ############## 47 | 48 | OUTFD=$2 49 | ZIPFILE=$3 50 | 51 | mount /data 2>/dev/null 52 | 53 | # Load utility functions 54 | [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk 55 | . /data/adb/magisk/util_functions.sh 56 | [ $MAGISK_VER_CODE -gt 18100 ] || require_new_magisk 57 | 58 | # Preperation for flashable zips 59 | setup_flashable 60 | 61 | # Mount partitions 62 | mount_partitions 63 | 64 | # Detect version and architecture 65 | api_level_arch_detect 66 | 67 | # Setup busybox and binaries 68 | $BOOTMODE && boot_actions || recovery_actions 69 | 70 | ############## 71 | # Preparation 72 | ############## 73 | 74 | # Extract prop file 75 | unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2 76 | [ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!" 77 | 78 | $BOOTMODE && MODDIRNAME=modules_update || MODDIRNAME=modules 79 | MODULEROOT=$NVBASE/$MODDIRNAME 80 | MODID=`grep_prop id $TMPDIR/module.prop` 81 | MODPATH=$MODULEROOT/$MODID 82 | MODNAME=`grep_prop name $TMPDIR/module.prop` 83 | 84 | # Create mod paths 85 | rm -rf $MODPATH 2>/dev/null 86 | mkdir -p $MODPATH 87 | 88 | ########## 89 | # Install 90 | ########## 91 | 92 | if is_legacy_script; then 93 | unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2 94 | 95 | # Load install script 96 | . $TMPDIR/install.sh 97 | 98 | # Callbacks 99 | print_modname 100 | on_install 101 | 102 | # Custom uninstaller 103 | [ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh 104 | 105 | # Skip mount 106 | $SKIPMOUNT && touch $MODPATH/skip_mount 107 | 108 | # prop file 109 | $PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop 110 | 111 | # Module info 112 | cp -af $TMPDIR/module.prop $MODPATH/module.prop 113 | 114 | # post-fs-data scripts 115 | $POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh 116 | 117 | # service scripts 118 | $LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh 119 | 120 | ui_print "- Setting permissions" 121 | set_permissions 122 | else 123 | print_modname 124 | 125 | unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2 126 | 127 | if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then 128 | ui_print "- Extracting module files" 129 | unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2 130 | 131 | # Default permissions 132 | set_perm_recursive $MODPATH 0 0 0755 0644 133 | fi 134 | 135 | # Load customization script 136 | [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh 137 | fi 138 | 139 | # Handle replace folders 140 | for TARGET in $REPLACE; do 141 | ui_print "- Replace target: $TARGET" 142 | mktouch $MODPATH$TARGET/.replace 143 | done 144 | 145 | if $BOOTMODE; then 146 | # Update info for Magisk Manager 147 | mktouch $NVBASE/modules/$MODID/update 148 | cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop 149 | fi 150 | 151 | # Copy over custom sepolicy rules 152 | if [ -f $MODPATH/sepolicy.rule -a -e $PERSISTDIR ]; then 153 | ui_print "- Installing custom sepolicy patch" 154 | PERSISTMOD=$PERSISTDIR/magisk/$MODID 155 | mkdir -p $PERSISTMOD 156 | cp -af $MODPATH/sepolicy.rule $PERSISTMOD/sepolicy.rule 157 | fi 158 | 159 | # Remove stuffs that don't belong to modules 160 | rm -rf \ 161 | $MODPATH/system/placeholder $MODPATH/customize.sh \ 162 | $MODPATH/README.md $MODPATH/.git* 2>/dev/null 163 | 164 | ############## 165 | # Finalizing 166 | ############## 167 | 168 | cd / 169 | $BOOTMODE || recovery_cleanup 170 | rm -rf $TMPDIR 171 | 172 | ui_print "*********************************" 173 | ui_print "Module installation is completed." 174 | ui_print "*********************************" 175 | exit 0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 一个基于Magisk&Riru的Module,可以助你用超低成本开发各种Hook插件,无须Xposed 2 | 3 | >### 此Module仅供大家学习研究,请勿用于商业用途。利用此Module进行非法行为造成的一切后果自负! 4 | 5 | ### 博客详情: 6 | 7 | ### 背景: 8 | 前段时间在WanAndroid每日一问里有个问题:**"应用进程中那4个Binder线程分别是跟谁通讯?"** 9 | 10 | 一番简单分析无果后,就想着写个Xposed插件来hook Thread对象的创建,但是看到有同学提醒:**"Xposed插件的`handleLoadPackage`方法是在`handleBindApplication`时才回调的!"**。 没办法,只能找其他的方案了。 11 | 12 | 忽然想到了Magisk,但是我又不会做Magisk插件…… 13 | 14 | 第二天看了下自己一直在用的那个【开启微信指纹支付】的Magisk模块源码(其实之前也看过好多遍了,一直没看懂,一头雾水),发现核心代码其实就是libs下面的那个APK的dex! 15 | 16 | 反编译看了下,大概摸清了思路,但是想到用这种方式(先手动打包成apk放在插件项目libs下)开发起来太繁琐了,而且维护起来成本又高,还没有一个规范的模板,这样就很难抽出来为自己所用。 17 | 18 | 于是心有不甘的我又继续在github上面搜Riru相关的模块,看到一个叫【QQ Simplify】的项目,是用来阉割QQ一些 “花里胡哨” 的功能的,看了下代码,它是在进程Fork之后,用反射把ServiceFetcher里面的LayoutInflater对象换成自己的代理类,这样就可以在布局inflate时,选择性地把一些View的宽高set为0,达到隐藏的效果。 19 | 20 | 结合这两个项目的部分代码以及思路,我封装出来一个入侵程度非常低的Module,开发新的插件的话,只需要添加这个Module的依赖,然后在`module.properties`中配置一下模块属性就行了,非常简单! 21 | 22 |
23 | 24 | ### 原理: 25 | 有一个叫Riru的Magisk模块,它会把系统的libmemtrack.so替换掉(新版改为指定`ro.dalvik.vm.native.bridge`属性值为自己的so),并对外公开Zygote初始化进程的一些API,比如`forkAndSpecializePost`。 26 | 27 | 在Zygote Fork进程的前后,都会对外 “发通知” ,如果趁这个时机向指定进程注入自己的代码,那么,当进程启动完成后,自己的代码就运行在指定进程内了,这样就可以~~为所欲为~~扩展一些功能,或者更改某些逻辑等等。 28 | 29 |
30 | 31 | ### Q&A: 32 | **这个Module能做什么?** 33 | 34 | 要知道,你的代码是运行在目标进程中的,这就相当于你参与了目标app的开发! 35 | 36 | 所以理论上目标进程中的所有数据以及行为,都能修改成你想要的结果,只要你能拿到对应的对象。 37 | 38 | 至于怎样拿到对象,这就看具体场景了。 39 | 40 | 比如你想修改某个app的某个页面按钮点击行为,那就可以先监听目标Activity的生命周期来获取到对应的Activity对象,进一步find到View实例然后给它重新set一个OnClickListener。 41 | 42 | 再比如你想修改某个app的启动图,一个比较通用的方法是:监听对应Activity的onCreate,在这里把`Activity.mWindow`的`mContentParent`替换成你自定义的FrameLayout,这样你就能在`onLayout`之前找到显示启动图的View实例,并对它做手脚。 43 | 44 | **它跟Xposed的关系/区别?** 45 | 46 | 可以说是完全无关系的。 47 | 48 | 从能力上来看,很明显Xposed更强大,不过相对的,Xposed插件开发起来难度也会高一些,因为它的优势主要体现在能监听任何一个方法调用,这就非常考验你对目标app代码的熟悉程度了,如果没掌握一定的逆向知识是搞不来的。 49 | 50 | 反观这个Module,它的能力是不如Xposed的,比如它不能监听哪些Class被加载,不能直接感知到哪些对象被创建。只能用一些比较原始的方法来修改数据和行为,比如反射,动态代理等。优势是开发成本很低,甚至你不用反编译目标app,没有逆向基础也可以,只需要一个*UIAutomatorViewer*工具来帮助获取到布局结构和资源id就能着手开发了。还有就是,目标app很难感知到这个插件的存在,它不像Xposed在异常堆栈中能看到相关字眼。无论安装和运行,都可以说是不留痕迹的。 51 | 52 |
53 | 54 | ### 亮点: 55 | - 配置成本极低,添加这个Module依赖就行了,你只需关注你自己的代码逻辑; 56 | 57 | - 所有的配置都集中到了module.properties文件里,直接修改这个文件即可,比如主入口类,目标进程等; 58 | 59 | - 提供了一些基本的API,方便进行Hook工作,一般情况下,只需要几行代码就能监听到按钮的点击,或者布局的加载了; 60 | 61 | - **自动刷入!** 是的,从此解放双手,像开发普通应用那样,编译完就能自动刷入手机了,如果是手动安装的话,每次至少浪费20秒时间; 62 | 63 | - **免重启(最小化)安装!** 提供快速调试的能力; 64 | 65 |
66 | 67 | ### 使用: 68 | 1. 首先,clone或直接下载这个Module(注意!这只是一个Module,需要被依赖到一个APP Module才能正常运作); 69 | 70 | 2. 新建一个Android项目,Minimum SDK至少为23(即6.0)。为了避免不必要的麻烦,Language请选择Kotlin而不是默认的Java,因为这个Module用到了Kotlin; 71 | 72 | 3. 新建好项目后,创建一个入口类,名字随便,比如就叫ModuleMain。然后,在里面声明一个 ***public static void main(String packageName)*** 方法,这个方法是必须有的!当插件启动后会被调用; 73 | 74 | 4. 导入刚刚下载Module,并依赖到主模块中; 75 | 76 | 5. 配置插件属性,先把模块根目录下的`module.properties.sample`复制一份,并改名为`module.properties`,然后编辑这个`module.properties`,根据里面的注释来完善插件信息,比如给moduleMainClass属性填上刚刚创建的ModuleMain完整类名(带包名); 77 | 78 |
79 | 80 | **module.properties属性如下:** 81 | 82 | - **moduleId**:模块唯一标识,只能使用字母 + 下划线组合,如:my_module_id; 83 | 84 | - **moduleName**:模块名称,自由填写; 85 | 86 | - **moduleAuthor**:模块作者,自由填写; 87 | 88 | - **moduleDescription**:模块描述,自由填写; 89 | 90 | - **moduleVersionName**:模块版本名,自由填写; 91 | 92 | - **moduleVersionCode**:模块版本号,自由填写; 93 | 94 | - **moduleMainClass**:主入口类名,例:com.demo.ModuleMain; 95 | 96 | - **targetProcessName**:目标进程名/包名,即要寄生的目标。不填写则寄生所有进程。同时寄生多个目标,用 ; 分隔,如: com.demo.application1;com.demo.application2; 97 | 98 | - **automaticInstallation**:编译完毕自动安装模块(需要手机已通过adb连接到电脑(只能连一台),并已安装Magisk和Riru模块!)。1为开启,其他值为关闭; 99 | 100 | - **debug**:Debug提供最小化安装(除首次安装外免重启)能力,可以快速测试模块(开启此选项需先开启automaticInstallation)。 101 | 102 | 配置好这些属性之后,就可以编写代码,编译打包运行了! 103 | 注意,编译打包的话,需要运行***project:assemble***(Tasks/build/assemble)这个Task,不是***app:assemble***也不是***module:assemble***! 104 | 105 |
106 | 107 | ### 常用API: 108 | 109 | |Name|Description| 110 | |------|-----------| 111 | |Hookworm.setTransferClassLoader(true)|转接插件Dex的ClassLoader
如果引用到了目标应用的一些自定义类或接口(或第三方库),则需要开启转接,否则会报ClassNotFoundException| 112 | |Hookworm.setHookGlobalLayoutInflater(true)|劫持全局的LayoutInflater| 113 | |Hookworm.setOnApplicationInitializedListener|监听Application初始化| 114 | |Hookworm.registerActivityLifecycleCallbacks|监听Activity的生命周期| 115 | |Hookworm.registerPostInflateListener|拦截LayoutInflater布局加载| 116 | |Hookworm.getApplication|获取进程Application实例| 117 | |Hookworm.getActivities|获取进程存活Activity实例集合| 118 | |Hookworm.findActivityByClassName|根据完整类名查找Activity对象| 119 | |HookwormExtensions.findViewByIDName|根据资源id名来查找View实例| 120 | |HookwormExtensions.findAllViewsByIDName|根据资源id名来查找所有对应的View实例| 121 | |HookwormExtensions.findViewByText|根据显示的文本来查找View实例| 122 | |HookwormExtensions.findAllViewsByText|根据显示的文本来查找所有对应的View实例| 123 | |HookwormExtensions.containsText|检测目标View是否包含某些文本| 124 | 125 |
126 | 127 | ### Hookworm地址: 128 | 129 |
130 | 131 | ### 感谢: 132 | 首先感谢鸿神和[WanAndroid每日一问](https://wanandroid.com/wenda),如果没有那天的那个问题就没有这个库。 133 | 134 | 感谢[Fingerprint pay for WeChat](https://github.com/eritpchy/Fingerprint-pay-magisk-wechat)和[QQ Simplify](https://github.com/Kr328/Riru-QQSimplify),从这两个项目中学到很多思路以及代码。。。 135 | 136 | 感谢[Riru](https://github.com/RikkaApps/Riru)和[Magisk](https://github.com/topjohnwu/Magisk),这两个东西是此Module的根基。 137 | 138 | 最后感谢大家的小星星🌟🌟,虽然可能屏幕前的你还没有点,但是先谢谢了! 139 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/resource/customize.sh: -------------------------------------------------------------------------------- 1 | SKIPUNZIP=1 2 | 3 | # extract verify.sh 4 | ui_print "- Extracting verify.sh" 5 | unzip -o "$ZIPFILE" 'verify.sh' -d "$TMPDIR" >&2 6 | if [ ! -f "$TMPDIR/verify.sh" ]; then 7 | ui_print "*********************************************************" 8 | ui_print "! Unable to extract verify.sh!" 9 | ui_print "! This zip may be corrupted, please try downloading again" 10 | abort "*********************************************************" 11 | fi 12 | . $TMPDIR/verify.sh 13 | 14 | # extract riru.sh 15 | extract "$ZIPFILE" 'riru.sh' "$MODPATH" 16 | . $MODPATH/riru.sh 17 | 18 | check_riru_version 19 | check_architecture 20 | 21 | # extract libs 22 | ui_print "- Extracting module files" 23 | 24 | extract "$ZIPFILE" 'module.prop' "$MODPATH" 25 | extract "$ZIPFILE" 'post-fs-data.sh' "$MODPATH" 26 | extract "$ZIPFILE" 'uninstall.sh' "$MODPATH" 27 | 28 | get_bit() { 29 | if [ "$(getprop ro.build.version.sdk)" -ge "30" ]; then 30 | search_result="$(find /data/app -name "$1*")" 31 | package_dir="$(basename "$(dirname "$search_result")")/$(basename "$search_result")" 32 | if [[ "$(dirname "$package_dir")" == "app" ]]; then 33 | package_dir="$(basename "$package_dir")" 34 | fi 35 | else 36 | package_dir="$(ls "/data/app" | grep "$1")" 37 | fi 38 | if [ -d "/data/app/$package_dir" ]; then 39 | lib_dir="/data/app/$package_dir/lib" 40 | oat_dir="/data/app/$package_dir/oat" 41 | abilist=$(getprop "ro.product.cpu.abilist") 42 | if [[ -d "$lib_dir/arm64" || -d "$lib_dir/x86_64" ]] || [[ -d "$oat_dir/arm64" || -d "$oat_dir/x86_64" ]]; then 43 | echo "64" 44 | elif [[ -d "$lib_dir/arm" || -d "$lib_dir/x86" ]] || [[ -d "$oat_dir/arm" || -d "$oat_dir/x86" ]]; then 45 | echo "32" 46 | elif [[ $abilist == *arm64* || $abilist == *x86_64* ]]; then 47 | echo "64" 48 | else 49 | echo "32" 50 | fi 51 | fi 52 | } 53 | 54 | process_32bit_libraries() { 55 | echo "processing 32 bit libraries..." 56 | lib=$(ls "$MODPATH/system/lib") 57 | for file in $lib; do 58 | if [ "$file" != "libriru_$RIRU_MODULE_ID.so" ] && [ "$file" != "libriru_$RIRU_MODULE_ID.so.sha256sum" ] && [ "${file##*.}" != "sha256sum" ]; then 59 | file="$MODPATH/system/lib/$file" 60 | target_process=$(grep_prop target_process_name "$TMPDIR/module.prop") 61 | target_process_list=${target_process//;/ } 62 | for package_name in $target_process_list; do 63 | bit="$(get_bit "$package_name")" 64 | if [ "$bit" == "32" ]; then 65 | target_so_dir="/data/data/$package_name/$library_path" 66 | ui_print "mkdir -p $target_so_dir" 67 | mkdir -p "$target_so_dir" 68 | ui_print "cp $file $target_so_dir" 69 | cp "$file" "$target_so_dir/" 70 | fi 71 | done 72 | ui_print "rm $file" 73 | rm "$file" 74 | ui_print "rm $file.sha256sum" 75 | rm "$file.sha256sum" 76 | fi 77 | done 78 | } 79 | 80 | process_64bit_libraries() { 81 | echo "processing 64 bit libraries..." 82 | lib=$(ls "$MODPATH/system/lib64") 83 | for file in $lib; do 84 | if [ "$file" != "libriru_$RIRU_MODULE_ID.so" ] && [ "$file" != "libriru_$RIRU_MODULE_ID.so.sha256sum" ] && [ "${file##*.}" != "sha256sum" ]; then 85 | file="$MODPATH/system/lib64/$file" 86 | target_process=$(grep_prop target_process_name "$TMPDIR/module.prop") 87 | target_process_list=${target_process//;/ } 88 | for package_name in $target_process_list; do 89 | bit="$(get_bit "$package_name")" 90 | if [ "$bit" == "64" ]; then 91 | target_so_dir="/data/data/$package_name/$library_path" 92 | ui_print "mkdir -p $target_so_dir" 93 | mkdir -p "$target_so_dir" 94 | ui_print "cp $file $target_so_dir" 95 | cp "$file" "$target_so_dir/" 96 | fi 97 | done 98 | ui_print "rm $file" 99 | rm "$file" 100 | ui_print "rm $file.sha256sum" 101 | rm "$file.sha256sum" 102 | fi 103 | done 104 | } 105 | 106 | library_path=$(grep_prop library_path "$TMPDIR/module.prop") 107 | 108 | if [ "$ARCH" = "x86" ] || [ "$ARCH" = "x64" ]; then 109 | ui_print "- Extracting x86 libraries" 110 | extract_dir "$ZIPFILE" "system_x86/lib" "$MODPATH" 111 | mv "$MODPATH/system_x86/lib" "$MODPATH/system/lib" 112 | process_32bit_libraries 113 | 114 | if [ "$IS64BIT" = true ]; then 115 | ui_print "- Extracting x64 libraries" 116 | extract_dir "$ZIPFILE" "system_x86/lib64" "$MODPATH" 117 | mv "$MODPATH/system_x86/lib64" "$MODPATH/system/lib64" 118 | process_64bit_libraries 119 | fi 120 | else 121 | ui_print "- Extracting arm libraries" 122 | extract_dir "$ZIPFILE" "system/lib" "$MODPATH" 123 | process_32bit_libraries 124 | 125 | if [ "$IS64BIT" = true ]; then 126 | ui_print "- Extracting arm64 libraries" 127 | extract_dir "$ZIPFILE" "system/lib64" "$MODPATH" 128 | process_64bit_libraries 129 | fi 130 | fi 131 | 132 | extract "$ZIPFILE" "extras.files" "$TMPDIR" 133 | 134 | cat "$TMPDIR/extras.files" >&1 | while read file; do 135 | extract "$ZIPFILE" "$file" "$MODPATH" 136 | done 137 | 138 | set_perm_recursive "$MODPATH" 0 0 0755 0644 139 | 140 | # extract Riru files 141 | ui_print "- Extracting extra files" 142 | [ -d "$RIRU_MODULE_PATH" ] || mkdir -p "$RIRU_MODULE_PATH" || abort "! Can't create $RIRU_MODULE_PATH" 143 | 144 | rm -f "$RIRU_MODULE_PATH/module.prop.new" 145 | extract "$ZIPFILE" 'riru/module.prop.new' "$RIRU_MODULE_PATH" true 146 | set_perm "$RIRU_MODULE_PATH/module.prop.new" 0 0 0600 $RIRU_SECONTEXT 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/wuyr/hookwormforwanandroid/Main.kt: -------------------------------------------------------------------------------- 1 | package com.wuyr.hookwormforwanandroid 2 | 3 | import android.app.AlertDialog 4 | import android.util.Log 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.RecyclerView 9 | import androidx.viewpager.widget.PagerAdapter 10 | import androidx.viewpager.widget.ViewPager 11 | import com.wuyr.hookworm.core.Hookworm 12 | import com.wuyr.hookworm.extensions.findViewByIDName 13 | import com.wuyr.hookworm.extensions.setOnClickProxy 14 | import com.wuyr.hookworm.utils.get 15 | import com.wuyr.hookworm.utils.set 16 | 17 | /** 18 | * @author wuyr 19 | * @github https://github.com/wuyr/HookwormForWanAndroidTest 20 | * @since 2021-01-25 上午10:54 21 | */ 22 | object Main { 23 | 24 | private fun Any?.logD() = Log.d("Main", toString()) 25 | 26 | @JvmStatic 27 | fun main(processName: String) { 28 | Hookworm.transferClassLoader = true 29 | val mainActivity = "per.goweii.wanandroid.module.main.activity.MainActivity" 30 | // 拦截mainActivity的布局加载 31 | Hookworm.registerPostInflateListener(mainActivity) { _, resourceName, rootView -> 32 | rootView?.apply { 33 | if (resourceName == "banner") { 34 | hookBanner(resourceName) 35 | } 36 | hookArticleItem(resourceName) 37 | removeTabs(resourceName) 38 | } 39 | } 40 | } 41 | 42 | private fun View.removeTabs(resourceName: String) { 43 | // 查找ll_bb,监听其子View的添加 44 | findViewByIDName("ll_bb")?.setOnHierarchyChangeListener( 45 | object : ViewGroup.OnHierarchyChangeListener { 46 | override fun onChildViewAdded(parent: View, child: View) { 47 | // 转成ViewGroup 48 | (parent as ViewGroup).run { 49 | // 当子View数量大于2时移除最后一个 50 | if (childCount > 2) { 51 | removeViewAt(2) 52 | } 53 | } 54 | } 55 | 56 | override fun onChildViewRemoved(parent: View?, child: View?) { 57 | } 58 | }) 59 | // 查找id名为“vp_tab”的ViewPager 60 | findViewByIDName("vp_tab")?.let { viewPager -> 61 | // 监听Adapter变更 62 | viewPager.addOnAdapterChangeListener { _, _, newAdapter -> 63 | viewPager.post { 64 | newAdapter?.let { adapter -> 65 | // 取出Adapter变量mPages 66 | adapter::class.get>(adapter, "mPages")?.let { pages -> 67 | // 通过反射创建长度为2的数组 68 | val newPages = java.lang.reflect.Array.newInstance( 69 | Class.forName("per.goweii.basic.core.adapter.TabFragmentPagerAdapter\$Page"), 70 | 2 71 | ) as Array 72 | // 只取前面2个元素 73 | newPages[0] = pages[0] 74 | newPages[1] = pages[1] 75 | // 重新赋值 76 | adapter::class.set(adapter, "mPages", newPages) 77 | } 78 | // 通知Adapter数据变更 79 | adapter.notifyDataSetChanged() 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | private fun View.hookArticleItem(resourceName: String) { 87 | // 根据id名“rv” 找到首页文章列表RecyclerView实例 88 | findViewByIDName("rv")?.let { recyclerView -> 89 | // 监听Item的attach状态 90 | recyclerView.addOnChildAttachStateChangeListener(object : 91 | RecyclerView.OnChildAttachStateChangeListener { 92 | 93 | private val dialog = AlertDialog.Builder(context).setMessage("禁止点击!").create() 94 | 95 | private val onClickProxy: (view: View, oldListener: View.OnClickListener?) -> Unit = 96 | { view, oldListener -> 97 | // 查找id名为“tv_title”的TextView 98 | view.findViewByIDName("tv_title")?.let { titleView -> 99 | // 检查是否包含 “每日一问” 字眼 100 | if (titleView.text.toString().contains("每日一问")) { 101 | // 有则交给宿主处理 102 | oldListener?.onClick(view) 103 | } else { 104 | // 没有就弹出dialog 105 | dialog.show() 106 | } 107 | } ?: oldListener?.onClick(view) // 没找到,交给宿主去处理 108 | } 109 | 110 | override fun onChildViewAttachedToWindow(child: View) { 111 | // 在Item每次attach之后重新设置点击代理 112 | child.setOnClickProxy(onClickProxy) 113 | } 114 | 115 | override fun onChildViewDetachedFromWindow(child: View) { 116 | } 117 | }) 118 | } 119 | } 120 | 121 | private fun View.hookBanner(resourceName: String) { 122 | // 根据id名称: "bannerViewPager" 查找ViewPager 123 | findViewByIDName("bannerViewPager")?.let { viewPager -> 124 | viewPager.addOnAdapterChangeListener(object : ViewPager.OnAdapterChangeListener { 125 | 126 | private val adapter = ImageAdapter(context) 127 | 128 | override fun onAdapterChanged( 129 | viewPager: ViewPager, oldAdapter: PagerAdapter?, newAdapter: PagerAdapter? 130 | ) { 131 | viewPager.removeOnAdapterChangeListener(this) 132 | viewPager.adapter = adapter 133 | viewPager.addOnAdapterChangeListener(this) 134 | } 135 | }) 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/cpp/riru.h: -------------------------------------------------------------------------------- 1 | #ifndef RIRU_H 2 | #define RIRU_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | // --------------------------------------------------------- 13 | 14 | typedef void(onModuleLoaded_v9)(); 15 | 16 | typedef int(shouldSkipUid_v9)(int uid); 17 | 18 | typedef void(nativeForkAndSpecializePre_v9)( 19 | JNIEnv *env, jclass cls, jint *uid, jint *gid, jintArray *gids, jint *runtimeFlags, 20 | jobjectArray *rlimits, jint *mountExternal, jstring *seInfo, jstring *niceName, 21 | jintArray *fdsToClose, jintArray *fdsToIgnore, jboolean *is_child_zygote, 22 | jstring *instructionSet, jstring *appDataDir, jboolean *isTopApp, jobjectArray *pkgDataInfoList, 23 | jobjectArray *whitelistedDataInfoList, jboolean *bindMountAppDataDirs, jboolean *bindMountAppStorageDirs); 24 | 25 | typedef void(nativeForkAndSpecializePost_v9)(JNIEnv *env, jclass cls, jint res); 26 | 27 | typedef void(nativeForkSystemServerPre_v9)( 28 | JNIEnv *env, jclass cls, uid_t *uid, gid_t *gid, jintArray *gids, jint *runtimeFlags, 29 | jobjectArray *rlimits, jlong *permittedCapabilities, jlong *effectiveCapabilities); 30 | 31 | typedef void(nativeForkSystemServerPost_v9)(JNIEnv *env, jclass cls, jint res); 32 | 33 | typedef void(nativeSpecializeAppProcessPre_v9)( 34 | JNIEnv *env, jclass cls, jint *uid, jint *gid, jintArray *gids, jint *runtimeFlags, 35 | jobjectArray *rlimits, jint *mountExternal, jstring *seInfo, jstring *niceName, 36 | jboolean *startChildZygote, jstring *instructionSet, jstring *appDataDir, 37 | jboolean *isTopApp, jobjectArray *pkgDataInfoList, jobjectArray *whitelistedDataInfoList, 38 | jboolean *bindMountAppDataDirs, jboolean *bindMountAppStorageDirs); 39 | 40 | typedef void(nativeSpecializeAppProcessPost_v9)(JNIEnv *env, jclass cls); 41 | 42 | typedef struct { 43 | int supportHide; 44 | int version; 45 | const char *versionName; 46 | onModuleLoaded_v9 *onModuleLoaded; 47 | shouldSkipUid_v9 *shouldSkipUid; 48 | nativeForkAndSpecializePre_v9 *forkAndSpecializePre; 49 | nativeForkAndSpecializePost_v9 *forkAndSpecializePost; 50 | nativeForkSystemServerPre_v9 *forkSystemServerPre; 51 | nativeForkSystemServerPost_v9 *forkSystemServerPost; 52 | nativeSpecializeAppProcessPre_v9 *specializeAppProcessPre; 53 | nativeSpecializeAppProcessPost_v9 *specializeAppProcessPost; 54 | } RiruModuleInfoV9; 55 | 56 | typedef RiruModuleInfoV9 RiruModuleInfoV10; 57 | 58 | // --------------------------------------------------------- 59 | 60 | typedef void *(RiruGetFunc_v9)(uint32_t token, const char *name); 61 | 62 | typedef void (RiruSetFunc_v9)(uint32_t token, const char *name, void *func); 63 | 64 | typedef void *(RiruGetJNINativeMethodFunc_v9)(uint32_t token, const char *className, const char *name, const char *signature); 65 | 66 | typedef void (RiruSetJNINativeMethodFunc_v9)(uint32_t token, const char *className, const char *name, const char *signature, void *func); 67 | 68 | typedef const JNINativeMethod *(RiruGetOriginalJNINativeMethodFunc_v9)(const char *className, const char *name, const char *signature); 69 | 70 | typedef void *(RiruGetGlobalValue_v9)(const char *key); 71 | 72 | typedef void(RiruPutGlobalValue_v9)(const char *key, void *value); 73 | 74 | typedef struct { 75 | 76 | uint32_t token; 77 | RiruGetFunc_v9 *getFunc; 78 | RiruGetJNINativeMethodFunc_v9 *getJNINativeMethodFunc; 79 | RiruSetFunc_v9 *setFunc; 80 | RiruSetJNINativeMethodFunc_v9 *setJNINativeMethodFunc; 81 | RiruGetOriginalJNINativeMethodFunc_v9 *getOriginalJNINativeMethodFunc; 82 | RiruGetGlobalValue_v9 *getGlobalValue; 83 | RiruPutGlobalValue_v9 *putGlobalValue; 84 | } RiruApiV9; 85 | 86 | typedef RiruApiV9 RiruApiV10; 87 | 88 | typedef void *(RiruInit_t)(void *); 89 | 90 | #ifdef RIRU_MODULE 91 | #define RIRUD_ADDRESS "rirud" 92 | 93 | #define RIRU_EXPORT __attribute__((visibility("default"))) __attribute__((used)) 94 | 95 | /* 96 | * Init will be called three times. 97 | * 98 | * The first time: 99 | * Returns the highest version number supported by both Riru and the module. 100 | * 101 | * arg: (int *) Riru's API version 102 | * returns: (int *) the highest possible API version 103 | * 104 | * The second time: 105 | * Returns the RiruModuleX struct created by the module (X is the return of the first call). 106 | * 107 | * arg: (RiruApiVX *) RiruApi strcut, this pointer can be saved for further use 108 | * returns: (RiruModuleX *) RiruModule strcut 109 | * 110 | * The second time: 111 | * Let the module to cleanup (such as RiruModuleX struct created before). 112 | * 113 | * arg: null 114 | * returns: (ignored) 115 | * 116 | */ 117 | void* init(void *arg) RIRU_EXPORT; 118 | 119 | extern int riru_api_version; 120 | extern RiruApiV9 *riru_api_v9; 121 | #define riru_api_v10 riru_api_v9 122 | 123 | inline void *riru_get_func(const char *name) { 124 | if (riru_api_version == 9 || riru_api_version == 10) { 125 | return riru_api_v9->getFunc(riru_api_v9->token, name); 126 | } 127 | return NULL; 128 | } 129 | 130 | inline void *riru_get_native_method_func(const char *className, const char *name, const char *signature) { 131 | if (riru_api_version == 9 || riru_api_version == 10) { 132 | return riru_api_v9->getJNINativeMethodFunc(riru_api_v9->token, className, name, signature); 133 | } 134 | return NULL; 135 | } 136 | 137 | inline const JNINativeMethod *riru_get_original_native_methods(const char *className, const char *name, const char *signature) { 138 | if (riru_api_version == 9 || riru_api_version == 10) { 139 | return riru_api_v9->getOriginalJNINativeMethodFunc(className, name, signature); 140 | } 141 | return NULL; 142 | } 143 | 144 | inline void riru_set_func(const char *name, void *func) { 145 | if (riru_api_version == 9 || riru_api_version == 10) { 146 | riru_api_v9->setFunc(riru_api_v9->token, name, func); 147 | } 148 | } 149 | 150 | inline void riru_set_native_method_func(const char *className, const char *name, const char *signature, 151 | void *func) { 152 | if (riru_api_version == 9 || riru_api_version == 10) { 153 | riru_api_v9->setJNINativeMethodFunc(riru_api_v9->token, className, name, signature, func); 154 | } 155 | } 156 | 157 | inline void *riru_get_global_value(const char *key) { 158 | if (riru_api_version == 9 || riru_api_version == 10) { 159 | return riru_api_v9->getGlobalValue(key); 160 | } 161 | return NULL; 162 | } 163 | 164 | inline void riru_put_global_value(const char *key, void *value) { 165 | if (riru_api_version == 9 || riru_api_version == 10) { 166 | riru_api_v9->putGlobalValue(key, value); 167 | } 168 | } 169 | 170 | #endif 171 | 172 | #ifdef __cplusplus 173 | } 174 | #endif 175 | 176 | #endif //RIRU_H -------------------------------------------------------------------------------- /HookwormForAndroid/src/main/java/com/wuyr/hookworm/utils/ReflectUtil.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST", "KDocMissingDocumentation", "PublicApiImplicitType", "unused") 2 | 3 | package com.wuyr.hookworm.utils 4 | 5 | import android.util.Log 6 | import java.lang.reflect.Field 7 | import java.lang.reflect.Modifier 8 | import kotlin.reflect.KClass 9 | 10 | /** 11 | * @author wuyr 12 | * @github https://github.com/wuyr/HookwormForAndroid 13 | * @since 2020-09-10 上午11:32 14 | */ 15 | const val TAG = "ReflectUtil" 16 | 17 | /** 18 | * 发生异常是否抛出(默认不抛出,只打印堆栈信息) 19 | */ 20 | var throwReflectException: Boolean = false 21 | 22 | /** 23 | * 给对象成员变量设置新的值(可以修改final属性,静态的基本类型除外) 24 | * 25 | * @param target 目标对象 26 | * @param fieldName 目标变量名 27 | * @param value 新的值 28 | * 29 | * @return true为成功 30 | */ 31 | fun Class<*>.set(target: Any?, fieldName: String, value: Any?) = try { 32 | getDeclaredField(fieldName).apply { 33 | isAccessible = true 34 | if (isLocked()) unlock() 35 | set(target, value) 36 | } 37 | true 38 | } catch (e: Exception) { 39 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e) 40 | false 41 | } 42 | 43 | private fun Field.isLocked() = modifiers and Modifier.FINAL != 0 44 | 45 | private fun Field.unlock() = let { target -> 46 | try { 47 | Field::class.java.getDeclaredField("modifiers") 48 | } catch (e: Exception) { 49 | Field::class.java.getDeclaredField("accessFlags") 50 | }.run { 51 | isAccessible = true 52 | setInt(target, target.modifiers and Modifier.FINAL.inv()) 53 | } 54 | } 55 | 56 | /** 57 | * 获取目标对象的变量值 58 | * 59 | * @param target 目标对象 60 | * @param fieldName 目标变量名 61 | * 62 | * @return 目标变量值(获取失败则返回null) 63 | */ 64 | fun Class<*>.get(target: Any?, fieldName: String) = try { 65 | getDeclaredField(fieldName).run { 66 | isAccessible = true 67 | get(target) as? T? 68 | } 69 | } catch (e: Exception) { 70 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e) 71 | null 72 | } 73 | 74 | /** 75 | * 调用目标对象的方法 76 | * 77 | * @param target 目标对象 78 | * @param methodName 目标方法名 79 | * @param paramsPairs 参数类型和参数值的键值对。示例: 80 | *
 81 |  *  val view = LayoutInflater::class.invoke(layoutInflater, "tryInflatePrecompiled",
 82 |  *      Int::class to R.layout.view_test,
 83 |  *      Resource::class to context.resource,
 84 |  *      ViewGroup::class to rootView,
 85 |  *      Boolean::class to false
 86 |  *  )
 87 |  * 
88 | * 89 | * @return 方法返回值 90 | */ 91 | fun Class<*>.invoke( 92 | target: Any?, 93 | methodName: String, 94 | vararg paramsPairs: Pair, Any?> = emptyArray() 95 | ) = try { 96 | getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run { 97 | isAccessible = true 98 | invoke(target, *paramsPairs.map { it.second }.toTypedArray()) as? T? 99 | } 100 | } catch (e: Exception) { 101 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e) 102 | null 103 | } 104 | 105 | /** 106 | * 同上,此乃调用void方法,即无返回值 107 | */ 108 | fun Class<*>.invokeVoid( 109 | target: Any?, 110 | methodName: String, 111 | vararg paramsPairs: Pair, Any?> = emptyArray() 112 | ) { 113 | try { 114 | getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run { 115 | isAccessible = true 116 | invoke(target, *paramsPairs.map { it.second }.toTypedArray()) 117 | } 118 | } catch (e: Exception) { 119 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e) 120 | } 121 | } 122 | 123 | /** 124 | * 创建目标类对象 125 | * 126 | * @param paramsPairs 参数类型和参数值的键值对。示例: 127 | *
128 |  *  val context = ContextImpl::class.newInstance(
129 |  *      ActivityThread::class to ...,
130 |  *      LoadedApk::class to ...,
131 |  *      String::class to ...,
132 |  *      IBinder::class to ...,
133 |  *  )
134 |  *
135 |  *  @return 目标对象新实例
136 |  */
137 | fun  Class<*>.newInstance(vararg paramsPairs: Pair, Any?> = emptyArray()) = try {
138 |     if (paramsPairs.isEmpty()) newInstance()
139 |     else getDeclaredConstructor(*paramsPairs.map { it.first.java }.toTypedArray()).run {
140 |         isAccessible = true
141 |         newInstance(*paramsPairs.map { it.second }.toTypedArray()) as? T?
142 |     }
143 | } catch (e: Exception) {
144 |     if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
145 |     null
146 | }
147 | 
148 | fun  KClass<*>.invoke(
149 |     target: Any?,
150 |     methodName: String,
151 |     vararg paramsPairs: Pair, Any?> = emptyArray()
152 | ) = try {
153 |     java.run {
154 |         getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run {
155 |             isAccessible = true
156 |             invoke(target, *paramsPairs.map { it.second }.toTypedArray()) as? T?
157 |         }
158 |     }
159 | } catch (e: Exception) {
160 |     if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
161 |     null
162 | }
163 | 
164 | fun KClass<*>.invokeVoid(
165 |     target: Any?,
166 |     methodName: String,
167 |     vararg paramsPairs: Pair, Any?> = emptyArray()
168 | ) {
169 |     try {
170 |         java.run {
171 |             getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run {
172 |                 isAccessible = true
173 |                 invoke(target, *paramsPairs.map { it.second }.toTypedArray())
174 |             }
175 |         }
176 |     } catch (e: Exception) {
177 |         if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
178 |     }
179 | }
180 | 
181 | fun  KClass<*>.newInstance(vararg paramsPairs: Pair, Any?> = emptyArray()) = try {
182 |     java.run {
183 |         getDeclaredConstructor(*paramsPairs.map { it.first.java }.toTypedArray()).run {
184 |             isAccessible = true
185 |             newInstance(*paramsPairs.map { it.second }.toTypedArray()) as? T?
186 |         }
187 |     }
188 | } catch (e: Exception) {
189 |     if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
190 |     null
191 | }
192 | 
193 | fun String.set(target: Any?, fieldName: String, value: Any?) =
194 |     Class.forName(this).set(target, fieldName, value)
195 | 
196 | fun  String.get(target: Any?, fieldName: String) = Class.forName(this).get(target, fieldName)
197 | 
198 | fun  String.invoke(target: Any?, methodName: String, vararg paramsPairs: Pair, Any?>) =
199 |     Class.forName(this).invoke(target, methodName, *paramsPairs)
200 | 
201 | fun String.invokeVoid(target: Any?, methodName: String, vararg paramsPairs: Pair, Any?>) =
202 |     Class.forName(this).invokeVoid(target, methodName, *paramsPairs)
203 | 
204 | fun  String.newInstance(vararg paramsPairs: Pair, Any?>) =
205 |     Class.forName(this).newInstance(*paramsPairs)
206 | 
207 | fun KClass<*>.set(target: Any?, fieldName: String, value: Any?) = java.set(target, fieldName, value)
208 | 
209 | fun  KClass<*>.get(target: Any?, fieldName: String) = java.get(target, fieldName)


--------------------------------------------------------------------------------
/HookwormForAndroid/src/main/cpp/main.cpp:
--------------------------------------------------------------------------------
  1 | #include 
  2 | #include 
  3 | #include 
  4 | #include 
  5 | 
  6 | #include "riru.h"
  7 | 
  8 | static char *jstring2char(JNIEnv *env, jstring target) {
  9 |     char *result = nullptr;
 10 |     if (target) {
 11 |         const char *targetChar = env->GetStringUTFChars(target, nullptr);
 12 |         if (targetChar != nullptr) {
 13 |             int len = strlen(targetChar);
 14 |             result = (char *) malloc((len + 1) * sizeof(char));
 15 |             if (result != nullptr) {
 16 |                 memset(result, 0, len + 1);
 17 |                 memcpy(result, targetChar, len);
 18 |             }
 19 |             env->ReleaseStringUTFChars(target, targetChar);
 20 |         }
 21 |     }
 22 |     return result;
 23 | }
 24 | 
 25 | static bool equals(const char *target1, const char *target2) {
 26 |     if (target1 == nullptr && target2 == nullptr) {
 27 |         return true;
 28 |     } else {
 29 |         if (target1 != nullptr && target2 != nullptr) {
 30 |             return strcmp(target1, target2) == 0;
 31 |         } else {
 32 |             return false;
 33 |         }
 34 |     }
 35 | }
 36 | 
 37 | static bool shouldInject(const char *current_process_name) {
 38 |     const char *target_process_name[] = PROCESS_NAME_ARRAY;
 39 |     int target_process_size = PROCESS_NAME_ARRAY_SIZE;
 40 |     if (target_process_size == 0) {
 41 |         return true;
 42 |     }
 43 |     for (auto &i : target_process_name) {
 44 |         if (equals(i, current_process_name)) {
 45 |             return true;
 46 |         }
 47 |     }
 48 |     return false;
 49 | }
 50 | 
 51 | static void inject_dex(JNIEnv *env, const char *dexPath, const char *optimizedDirectory,
 52 |                        const char *mainClassName, const char *processName) {
 53 | 
 54 |     //get class: ClassLoader
 55 |     jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
 56 | 
 57 |     //get method: ClassLoader.getSystemClassLoader()
 58 |     jmethodID getSystemClassLoaderMethodID = env->GetStaticMethodID(
 59 |             classLoaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
 60 | 
 61 |     //invoke method: ClassLoader.getSystemClassLoader(), got ClassLoader object
 62 |     jobject systemClassLoader = env->CallStaticObjectMethod(classLoaderClass,
 63 |                                                             getSystemClassLoaderMethodID);
 64 | 
 65 |     //get class: DexClassLoader
 66 |     jclass dexClassLoaderClass = env->FindClass("dalvik/system/DexClassLoader");
 67 | 
 68 |     //get constructor: DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
 69 |     jmethodID dexClassLoaderConstructorID = env->GetMethodID(dexClassLoaderClass, "",
 70 |                                                              "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
 71 | 
 72 |     //new instance: DexClassLoader(dexPath, optimizedDirectory, null, systemClassLoader), got DexClassLoader object
 73 |     jobject dexClassLoader = env->NewObject(dexClassLoaderClass, dexClassLoaderConstructorID,
 74 |                                             env->NewStringUTF(dexPath),
 75 |                                             env->NewStringUTF(optimizedDirectory), NULL,
 76 |                                             systemClassLoader);
 77 | 
 78 |     //get method: DexClassLoader.loadClass(String name)
 79 |     jmethodID loadClassMethodID = env->GetMethodID(
 80 |             dexClassLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
 81 | 
 82 |     //invoke method: DexClassLoader.loadClass(mainClassName), got Main class
 83 |     auto mainClass = (jclass) env->CallObjectMethod(
 84 |             dexClassLoader, loadClassMethodID, env->NewStringUTF(mainClassName));
 85 | 
 86 |     //get method: Main.main(String processName)
 87 |     jmethodID mainMethodID = env->GetStaticMethodID(
 88 |             mainClass, "main", "(Ljava/lang/String;)V");
 89 | 
 90 |     //invoke method: Main.main(processName)
 91 |     env->CallStaticVoidMethod(mainClass, mainMethodID, env->NewStringUTF(processName));
 92 | }
 93 | 
 94 | static char *process_name = nullptr;
 95 | static char *app_data_dir = nullptr;
 96 | 
 97 | static void forkAndSpecializePre(
 98 |         JNIEnv *env, jclass clazz, jint *_uid, jint *gid, jintArray *gids, jint *runtimeFlags,
 99 |         jobjectArray *rlimits, jint *mountExternal, jstring *seInfo, jstring *niceName,
100 |         jintArray *fdsToClose, jintArray *fdsToIgnore, jboolean *is_child_zygote,
101 |         jstring *instructionSet, jstring *appDataDir, jboolean *isTopApp,
102 |         jobjectArray *pkgDataInfoList,
103 |         jobjectArray *whitelistedDataInfoList, jboolean *bindMountAppDataDirs,
104 |         jboolean *bindMountAppStorageDirs) {
105 |     char *current_process_name = jstring2char(env, *niceName);
106 |     if (shouldInject(current_process_name)) {
107 |         process_name = current_process_name;
108 |         app_data_dir = jstring2char(env, *appDataDir);
109 |     } else {
110 |         if (process_name) {
111 |             free(process_name);
112 |             process_name = nullptr;
113 |         }
114 |     }
115 | }
116 | 
117 | static void forkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) {
118 |     if (res == 0) {
119 |         //normal process
120 |         if (process_name && app_data_dir) {
121 |             inject_dex(env, DEX_PATH, app_data_dir, MAIN_CLASS, process_name);
122 |             free(app_data_dir);
123 |             app_data_dir = nullptr;
124 |             free(process_name);
125 |             process_name = nullptr;
126 |         }
127 |     }
128 | }
129 | 
130 | extern "C" {
131 | 
132 | int riru_api_version;
133 | RiruApiV9 *riru_api_v9;
134 | 
135 | void *init(void *arg) {
136 |     static int step = 0;
137 |     step += 1;
138 | 
139 |     static void *_module;
140 | 
141 |     switch (step) {
142 |         case 1: {
143 |             auto core_max_api_version = *(int *) arg;
144 |             riru_api_version =
145 |                     core_max_api_version <= RIRU_MODULE_API_VERSION ? core_max_api_version
146 |                                                                     : RIRU_MODULE_API_VERSION;
147 |             return &riru_api_version;
148 |         }
149 |         case 2: {
150 |             switch (riru_api_version) {
151 |                 // RiruApiV10 and RiruModuleInfoV10 are equal to V9
152 |                 case 10:
153 |                 case 9: {
154 |                     riru_api_v9 = (RiruApiV9 *) arg;
155 | 
156 |                     auto module = (RiruModuleInfoV9 *) malloc(sizeof(RiruModuleInfoV9));
157 |                     memset(module, 0, sizeof(RiruModuleInfoV9));
158 |                     _module = module;
159 | 
160 |                     module->supportHide = true;
161 | 
162 |                     module->version = RIRU_MODULE_VERSION;
163 |                     module->versionName = RIRU_MODULE_VERSION_NAME;
164 |                     module->forkAndSpecializePre = forkAndSpecializePre;
165 |                     module->forkAndSpecializePost = forkAndSpecializePost;
166 |                     return module;
167 |                 }
168 |                 default: {
169 |                     return nullptr;
170 |                 }
171 |             }
172 |         }
173 |         case 3: {
174 |             free(_module);
175 |             return nullptr;
176 |         }
177 |         default: {
178 |             return nullptr;
179 |         }
180 |     }
181 | }
182 | }


--------------------------------------------------------------------------------
/HookwormForAndroid/src/main/java/com/wuyr/hookworm/extensions/HookwormExtensions.kt:
--------------------------------------------------------------------------------
  1 | @file:Suppress("UNCHECKED_CAST")
  2 | 
  3 | package com.wuyr.hookworm.extensions
  4 | 
  5 | import android.annotation.SuppressLint
  6 | import android.app.Activity
  7 | import android.view.MotionEvent
  8 | import android.view.View
  9 | import android.view.ViewGroup
 10 | import android.widget.TextView
 11 | import androidx.core.view.forEach
 12 | import com.wuyr.hookworm.utils.get
 13 | import com.wuyr.hookworm.utils.invoke
 14 | import kotlin.reflect.KClass
 15 | 
 16 | /**
 17 |  * @author wuyr
 18 |  * @github https://github.com/wuyr/HookwormForAndroid
 19 |  * @since 2020-09-22 上午10:29
 20 |  */
 21 | 
 22 | /**
 23 |  * 根据类型来查找所有对应的View实例
 24 |  *
 25 |  *  @return 对应的View集合
 26 |  */
 27 | fun Activity.findAllViewsByClass(clazz: KClass): List {
 28 |     val result = ArrayList()
 29 |     fun fill(view: View) {
 30 |         if (clazz.isInstance(view)) {
 31 |             result += view
 32 |         }
 33 |         if (view is ViewGroup) {
 34 |             view.forEach { fill(it) }
 35 |         }
 36 |     }
 37 |     fill(window.decorView)
 38 |     return result
 39 | }
 40 | 
 41 | /**
 42 |  * 根据类型来查找所有对应的View实例
 43 |  *
 44 |  *  @return 对应的View集合
 45 |  */
 46 | fun View.findAllViewsByClass(clazz: KClass): List {
 47 |     val result = ArrayList()
 48 |     fun fill(view: View) {
 49 |         if (clazz.isInstance(view)) {
 50 |             result += view
 51 |         }
 52 |         if (view is ViewGroup) {
 53 |             view.forEach { fill(it) }
 54 |         }
 55 |     }
 56 |     fill(this)
 57 |     return result
 58 | }
 59 | 
 60 | /**
 61 |  * 根据资源id名来查找View实例
 62 |  *
 63 |  *  @param idName id名数组(即:可同时匹配多个id名)
 64 |  *  @return 对应的View,找不到即为null
 65 |  */
 66 | fun  Activity.findViewByIDName(vararg idName: String): V? =
 67 |     findAllViewsByIDName(*idName).let {
 68 |         if (it.isEmpty()) null else it[0] as V?
 69 |     }
 70 | 
 71 | /**
 72 |  * 根据资源id名来查找所有对应的View实例
 73 |  *
 74 |  *  @param idName id名数组(即:可同时匹配多个id名)
 75 |  *  @return 对应的View集合
 76 |  */
 77 | fun Activity.findAllViewsByIDName(vararg idName: String): List {
 78 |     val result = ArrayList()
 79 |     fun fill(view: View) {
 80 |         idName.forEach { name ->
 81 |             if (name == runCatching { resources.getResourceEntryName(view.id) }.getOrNull()) result += view
 82 |         }
 83 |         if (view is ViewGroup) {
 84 |             view.forEach { fill(it) }
 85 |         }
 86 |     }
 87 |     fill(window.decorView)
 88 |     return result
 89 | }
 90 | 
 91 | /**
 92 |  * 根据资源id名来查找View实例
 93 |  *
 94 |  *  @param idName id名数组(即:可同时匹配多个id名)
 95 |  *  @return 对应的View,找不到即为null
 96 |  */
 97 | fun  View.findViewByIDName(vararg idName: String): V? =
 98 |     findAllViewsByIDName(*idName).let {
 99 |         if (it.isEmpty()) null else it[0] as V?
100 |     }
101 | 
102 | /**
103 |  * 根据资源id名来查找所有对应的View实例
104 |  *
105 |  *  @param idName id名数组(即:可同时匹配多个id名)
106 |  *  @return 对应的View集合
107 |  */
108 | fun View.findAllViewsByIDName(vararg idName: String): List {
109 |     val result = ArrayList()
110 |     fun fill(view: View) {
111 |         idName.forEach { name ->
112 |             if (name == runCatching { resources.getResourceEntryName(view.id) }.getOrNull()) result += view
113 |         }
114 |         if (view is ViewGroup) {
115 |             view.forEach { fill(it) }
116 |         }
117 |     }
118 |     fill(this)
119 |     return result
120 | }
121 | 
122 | /**
123 |  * 根据显示的文本来查找View实例
124 |  *
125 |  *  @param textList 文本数组(即:可同时匹配多个文本)
126 |  *  @return 对应的View,找不到即为null
127 |  */
128 | fun  Activity.findViewByText(vararg textList: String): V? {
129 |     fun find(view: View): View? {
130 |         if (view is TextView) {
131 |             return if (textList.any { it == view.text.toString() }) view else null
132 |         } else {
133 |             val nodeText = view.createAccessibilityNodeInfo().text?.toString()
134 |             if (textList.any { it == nodeText }) {
135 |                 return view
136 |             }
137 |             if (view is ViewGroup) {
138 |                 view.forEach { child -> find(child)?.let { return it } }
139 |             }
140 |         }
141 |         return null
142 |     }
143 |     return find(window.decorView) as V
144 | }
145 | 
146 | /**
147 |  * 根据显示的文本来查找所有对应的View实例
148 |  *
149 |  *  @param textList 文本数组(即:可同时匹配多个文本)
150 |  *  @return 对应的View集合
151 |  */
152 | fun Activity.findAllViewsByText(vararg textList: String): List {
153 |     val result = ArrayList()
154 |     fun fill(view: View) {
155 |         if (view is TextView) {
156 |             if (textList.any { it == view.text.toString() }) result += view
157 |         } else {
158 |             val nodeText = view.createAccessibilityNodeInfo().text?.toString()
159 |             if (textList.any { it == nodeText }) result += view
160 |             if (view is ViewGroup) {
161 |                 view.forEach { fill(it) }
162 |             }
163 |         }
164 |     }
165 |     fill(window.decorView)
166 |     return result
167 | }
168 | 
169 | /**
170 |  * 根据显示的文本来查找View实例
171 |  *
172 |  *  @param textList 文本数组(即:可同时匹配多个文本)
173 |  *  @return 对应的View,找不到即为null
174 |  */
175 | fun  View.findViewByText(vararg textList: String): V? {
176 |     fun find(view: View): View? {
177 |         if (view is TextView) {
178 |             return if (textList.any { it == view.text.toString() }) view else null
179 |         } else {
180 |             val nodeText = view.createAccessibilityNodeInfo().text?.toString()
181 |             if (textList.any { it == nodeText }) {
182 |                 return view
183 |             }
184 |             if (view is ViewGroup) {
185 |                 view.forEach { child -> find(child)?.let { return it } }
186 |             }
187 |         }
188 |         return null
189 |     }
190 |     return find(this) as V
191 | }
192 | 
193 | /**
194 |  * 根据显示的文本来查找所有对应的View实例
195 |  *
196 |  *  @param textList 文本数组(即:可同时匹配多个文本)
197 |  *  @return 对应的View集合
198 |  */
199 | fun View.findAllViewsByText(vararg textList: String): List {
200 |     val result = ArrayList()
201 |     fun fill(view: View) {
202 |         if (view is TextView) {
203 |             if (textList.any { it == view.text.toString() }) result += view
204 |         } else {
205 |             val nodeText = view.createAccessibilityNodeInfo().text?.toString()
206 |             if (textList.any { it == nodeText }) result += view
207 |             if (view is ViewGroup) {
208 |                 view.forEach { fill(it) }
209 |             }
210 |         }
211 |     }
212 |     fill(this)
213 |     return result
214 | }
215 | 
216 | /**
217 |  * 检测目标View是否包含某些文本
218 |  *
219 |  * @param targetText 要检测的文本集合(可同时检测多个)
220 |  * @param recursive 是否递归查找
221 |  * @return 有找到则返回true,反之false
222 |  */
223 | fun View.containsText(vararg targetText: String, recursive: Boolean = false): Boolean {
224 |     val identifier = if (this is TextView) {
225 |         if (text.isNullOrEmpty()) {
226 |             if (hint.isNullOrEmpty()) "" else hint.toString()
227 |         } else text.toString()
228 |     } else createAccessibilityNodeInfo().text?.toString() ?: ""
229 | 
230 |     targetText.forEach { t -> if (identifier.contains(t)) return true }
231 |     if (recursive && this is ViewGroup) {
232 |         forEach { if (it.containsText(*targetText, recursive = true)) return true }
233 |     }
234 |     return false
235 | }
236 | 
237 | /**
238 |  * 设置目标View的点击代理
239 |  *
240 |  * @param proxyListener 点击回调lambda,参数oldListener即为原来的OnClickListener实例
241 |  */
242 | fun View.setOnClickProxy(proxyListener: (view: View, oldListener: View.OnClickListener?) -> Unit) {
243 |     if (!isClickable) isClickable = true
244 |     val oldListener = View::class.invoke(this, "getListenerInfo")?.let {
245 |         it::class.get(it, "mOnClickListener")
246 |     }
247 |     setOnClickListener { proxyListener(it, oldListener) }
248 | }
249 | 
250 | /**
251 |  * 设置目标View的长按代理
252 |  *
253 |  * @param proxyListener 长按回调lambda,参数oldListener即为原来的OnLongClickListener实例
254 |  */
255 | fun View.setOnLongClickProxy(proxyListener: (view: View, oldListener: View.OnLongClickListener?) -> Boolean) {
256 |     if (!isLongClickable) isLongClickable = true
257 |     val oldListener = View::class.invoke(this, "getListenerInfo")?.let {
258 |         it::class.get(it, "mOnLongClickListener")
259 |     }
260 |     setOnLongClickListener { proxyListener(it, oldListener) }
261 | }
262 | 
263 | /**
264 |  * 设置目标View的触摸代理
265 |  *
266 |  * @param proxyListener 触摸回调lambda,参数oldListener即为原来的OnTouchListener实例
267 |  */
268 | @SuppressLint("ClickableViewAccessibility")
269 | fun View.setOnTouchProxy(proxyListener: (view: View, event: MotionEvent, oldListener: View.OnTouchListener?) -> Boolean) {
270 |     val oldListener = View::class.invoke(this, "getListenerInfo")?.let {
271 |         it::class.get(it, "mOnTouchListener")
272 |     }
273 |     setOnTouchListener { v, event ->
274 |         proxyListener(v, event, oldListener)
275 |     }
276 | }
277 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
  1 |                                  Apache License
  2 |                            Version 2.0, January 2004
  3 |                         http://www.apache.org/licenses/
  4 | 
  5 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 | 
  7 |    1. Definitions.
  8 | 
  9 |       "License" shall mean the terms and conditions for use, reproduction,
 10 |       and distribution as defined by Sections 1 through 9 of this document.
 11 | 
 12 |       "Licensor" shall mean the copyright owner or entity authorized by
 13 |       the copyright owner that is granting the License.
 14 | 
 15 |       "Legal Entity" shall mean the union of the acting entity and all
 16 |       other entities that control, are controlled by, or are under common
 17 |       control with that entity. For the purposes of this definition,
 18 |       "control" means (i) the power, direct or indirect, to cause the
 19 |       direction or management of such entity, whether by contract or
 20 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 21 |       outstanding shares, or (iii) beneficial ownership of such entity.
 22 | 
 23 |       "You" (or "Your") shall mean an individual or Legal Entity
 24 |       exercising permissions granted by this License.
 25 | 
 26 |       "Source" form shall mean the preferred form for making modifications,
 27 |       including but not limited to software source code, documentation
 28 |       source, and configuration files.
 29 | 
 30 |       "Object" form shall mean any form resulting from mechanical
 31 |       transformation or translation of a Source form, including but
 32 |       not limited to compiled object code, generated documentation,
 33 |       and conversions to other media types.
 34 | 
 35 |       "Work" shall mean the work of authorship, whether in Source or
 36 |       Object form, made available under the License, as indicated by a
 37 |       copyright notice that is included in or attached to the work
 38 |       (an example is provided in the Appendix below).
 39 | 
 40 |       "Derivative Works" shall mean any work, whether in Source or Object
 41 |       form, that is based on (or derived from) the Work and for which the
 42 |       editorial revisions, annotations, elaborations, or other modifications
 43 |       represent, as a whole, an original work of authorship. For the purposes
 44 |       of this License, Derivative Works shall not include works that remain
 45 |       separable from, or merely link (or bind by name) to the interfaces of,
 46 |       the Work and Derivative Works thereof.
 47 | 
 48 |       "Contribution" shall mean any work of authorship, including
 49 |       the original version of the Work and any modifications or additions
 50 |       to that Work or Derivative Works thereof, that is intentionally
 51 |       submitted to Licensor for inclusion in the Work by the copyright owner
 52 |       or by an individual or Legal Entity authorized to submit on behalf of
 53 |       the copyright owner. For the purposes of this definition, "submitted"
 54 |       means any form of electronic, verbal, or written communication sent
 55 |       to the Licensor or its representatives, including but not limited to
 56 |       communication on electronic mailing lists, source code control systems,
 57 |       and issue tracking systems that are managed by, or on behalf of, the
 58 |       Licensor for the purpose of discussing and improving the Work, but
 59 |       excluding communication that is conspicuously marked or otherwise
 60 |       designated in writing by the copyright owner as "Not a Contribution."
 61 | 
 62 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 63 |       on behalf of whom a Contribution has been received by Licensor and
 64 |       subsequently incorporated within the Work.
 65 | 
 66 |    2. Grant of Copyright License. Subject to the terms and conditions of
 67 |       this License, each Contributor hereby grants to You a perpetual,
 68 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 69 |       copyright license to reproduce, prepare Derivative Works of,
 70 |       publicly display, publicly perform, sublicense, and distribute the
 71 |       Work and such Derivative Works in Source or Object form.
 72 | 
 73 |    3. Grant of Patent License. Subject to the terms and conditions of
 74 |       this License, each Contributor hereby grants to You a perpetual,
 75 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 76 |       (except as stated in this section) patent license to make, have made,
 77 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 78 |       where such license applies only to those patent claims licensable
 79 |       by such Contributor that are necessarily infringed by their
 80 |       Contribution(s) alone or by combination of their Contribution(s)
 81 |       with the Work to which such Contribution(s) was submitted. If You
 82 |       institute patent litigation against any entity (including a
 83 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 84 |       or a Contribution incorporated within the Work constitutes direct
 85 |       or contributory patent infringement, then any patent licenses
 86 |       granted to You under this License for that Work shall terminate
 87 |       as of the date such litigation is filed.
 88 | 
 89 |    4. Redistribution. You may reproduce and distribute copies of the
 90 |       Work or Derivative Works thereof in any medium, with or without
 91 |       modifications, and in Source or Object form, provided that You
 92 |       meet the following conditions:
 93 | 
 94 |       (a) You must give any other recipients of the Work or
 95 |           Derivative Works a copy of this License; and
 96 | 
 97 |       (b) You must cause any modified files to carry prominent notices
 98 |           stating that You changed the files; and
 99 | 
100 |       (c) You must retain, in the Source form of any Derivative Works
101 |           that You distribute, all copyright, patent, trademark, and
102 |           attribution notices from the Source form of the Work,
103 |           excluding those notices that do not pertain to any part of
104 |           the Derivative Works; and
105 | 
106 |       (d) If the Work includes a "NOTICE" text file as part of its
107 |           distribution, then any Derivative Works that You distribute must
108 |           include a readable copy of the attribution notices contained
109 |           within such NOTICE file, excluding those notices that do not
110 |           pertain to any part of the Derivative Works, in at least one
111 |           of the following places: within a NOTICE text file distributed
112 |           as part of the Derivative Works; within the Source form or
113 |           documentation, if provided along with the Derivative Works; or,
114 |           within a display generated by the Derivative Works, if and
115 |           wherever such third-party notices normally appear. The contents
116 |           of the NOTICE file are for informational purposes only and
117 |           do not modify the License. You may add Your own attribution
118 |           notices within Derivative Works that You distribute, alongside
119 |           or as an addendum to the NOTICE text from the Work, provided
120 |           that such additional attribution notices cannot be construed
121 |           as modifying the License.
122 | 
123 |       You may add Your own copyright statement to Your modifications and
124 |       may provide additional or different license terms and conditions
125 |       for use, reproduction, or distribution of Your modifications, or
126 |       for any such Derivative Works as a whole, provided Your use,
127 |       reproduction, and distribution of the Work otherwise complies with
128 |       the conditions stated in this License.
129 | 
130 |    5. Submission of Contributions. Unless You explicitly state otherwise,
131 |       any Contribution intentionally submitted for inclusion in the Work
132 |       by You to the Licensor shall be under the terms and conditions of
133 |       this License, without any additional terms or conditions.
134 |       Notwithstanding the above, nothing herein shall supersede or modify
135 |       the terms of any separate license agreement you may have executed
136 |       with Licensor regarding such Contributions.
137 | 
138 |    6. Trademarks. This License does not grant permission to use the trade
139 |       names, trademarks, service marks, or product names of the Licensor,
140 |       except as required for reasonable and customary use in describing the
141 |       origin of the Work and reproducing the content of the NOTICE file.
142 | 
143 |    7. Disclaimer of Warranty. Unless required by applicable law or
144 |       agreed to in writing, Licensor provides the Work (and each
145 |       Contributor provides its Contributions) on an "AS IS" BASIS,
146 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 |       implied, including, without limitation, any warranties or conditions
148 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 |       PARTICULAR PURPOSE. You are solely responsible for determining the
150 |       appropriateness of using or redistributing the Work and assume any
151 |       risks associated with Your exercise of permissions under this License.
152 | 
153 |    8. Limitation of Liability. In no event and under no legal theory,
154 |       whether in tort (including negligence), contract, or otherwise,
155 |       unless required by applicable law (such as deliberate and grossly
156 |       negligent acts) or agreed to in writing, shall any Contributor be
157 |       liable to You for damages, including any direct, indirect, special,
158 |       incidental, or consequential damages of any character arising as a
159 |       result of this License or out of the use or inability to use the
160 |       Work (including but not limited to damages for loss of goodwill,
161 |       work stoppage, computer failure or malfunction, or any and all
162 |       other commercial damages or losses), even if such Contributor
163 |       has been advised of the possibility of such damages.
164 | 
165 |    9. Accepting Warranty or Additional Liability. While redistributing
166 |       the Work or Derivative Works thereof, You may choose to offer,
167 |       and charge a fee for, acceptance of support, warranty, indemnity,
168 |       or other liability obligations and/or rights consistent with this
169 |       License. However, in accepting such obligations, You may act only
170 |       on Your own behalf and on Your sole responsibility, not on behalf
171 |       of any other Contributor, and only if You agree to indemnify,
172 |       defend, and hold each Contributor harmless for any liability
173 |       incurred by, or claims asserted against, such Contributor by reason
174 |       of your accepting any such warranty or additional liability.
175 | 
176 |    END OF TERMS AND CONDITIONS
177 | 
178 |    APPENDIX: How to apply the Apache License to your work.
179 | 
180 |       To apply the Apache License to your work, attach the following
181 |       boilerplate notice, with the fields enclosed by brackets "[]"
182 |       replaced with your own identifying information. (Don't include
183 |       the brackets!)  The text should be enclosed in the appropriate
184 |       comment syntax for the file format. We also recommend that a
185 |       file or class name and description of purpose be included on the
186 |       same "printed page" as the copyright notice for easier
187 |       identification within third-party archives.
188 | 
189 |    Copyright [yyyy] [name of copyright owner]
190 | 
191 |    Licensed under the Apache License, Version 2.0 (the "License");
192 |    you may not use this file except in compliance with the License.
193 |    You may obtain a copy of the License at
194 | 
195 |        http://www.apache.org/licenses/LICENSE-2.0
196 | 
197 |    Unless required by applicable law or agreed to in writing, software
198 |    distributed under the License is distributed on an "AS IS" BASIS,
199 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 |    See the License for the specific language governing permissions and
201 |    limitations under the License.
202 | 


--------------------------------------------------------------------------------
/HookwormForAndroid/build.gradle.kts:
--------------------------------------------------------------------------------
  1 | import com.android.build.api.dsl.ExternalNativeCmakeOptions
  2 | import javassist.ClassPool
  3 | import javassist.CtMethod
  4 | import org.apache.tools.ant.filters.FixCrLfFilter
  5 | import org.gradle.internal.os.OperatingSystem
  6 | import org.gradle.kotlin.dsl.support.zipTo
  7 | import java.security.MessageDigest
  8 | import java.util.*
  9 | import kotlin.concurrent.thread
 10 | 
 11 | plugins {
 12 |     id("com.android.library")
 13 |     kotlin("android")
 14 |     kotlin("android.extensions")
 15 | }
 16 | 
 17 | buildscript {
 18 |     dependencies {
 19 |         classpath("javassist:javassist:3.9.0.GA")
 20 |     }
 21 | }
 22 | 
 23 | if (!file("module.properties").exists()) {
 24 |     error("Please copy \"module.properties.sample\" and rename to \"module.properties\" and fill in the module information!")
 25 | }
 26 | 
 27 | val versionsProp =
 28 |     Properties().apply { load(file("src/main/resource/versions.properties").inputStream()) }
 29 | val moduleCompileSdkVersion = versionsProp.getProperty("compileSdkVersion").toInt()
 30 | val moduleMinSdkVersion = versionsProp.getProperty("minSdkVersion").toInt()
 31 | val moduleTargetSdkVersion = versionsProp.getProperty("targetSdkVersion").toInt()
 32 | val cmakeVersion: String = versionsProp.getProperty("cmakeVersion")
 33 | val corektxVersion: String = versionsProp.getProperty("core-ktxVersion")
 34 | val maxRiruApiVersionCode = versionsProp.getProperty("maxRiruApiVersionCode").toInt()
 35 | val minRiruApiVersionCode = versionsProp.getProperty("minRiruApiVersionCode").toInt()
 36 | val minRiruApiVersionName: String = versionsProp.getProperty("minRiruApiVersionName")
 37 | 
 38 | val moduleProp =
 39 |     Properties().apply { load(file("module.properties").inputStream()) }
 40 | val moduleId: String = moduleProp.getProperty("moduleId")
 41 | val moduleName: String = moduleProp.getProperty("moduleName")
 42 | val moduleAuthor: String = moduleProp.getProperty("moduleAuthor")
 43 | val moduleDescription: String = moduleProp.getProperty("moduleDescription")
 44 | val moduleVersionName: String = moduleProp.getProperty("moduleVersionName")
 45 | val moduleVersionCode: String = moduleProp.getProperty("moduleVersionCode")
 46 | val moduleMainClass: String = moduleProp.getProperty("moduleMainClass")
 47 | val targetProcessName: String = moduleProp.getProperty("targetProcessName")
 48 | val libraryPath: String =
 49 |     moduleProp.getProperty("libraryPath").let { if (it.isEmpty()) moduleId else it }
 50 | val automaticInstallation: Boolean =
 51 |     moduleProp.getProperty("automaticInstallation").let { it == "1" || it == "true" }
 52 | val debug: Boolean = moduleProp.getProperty("debug").let { it == "1" || it == "true" }
 53 | val moduleDexPath: String = "${if (debug) "data/local/tmp" else "system/framework"}/$moduleId.dex"
 54 | val hookwormMainClass = "com/wuyr/hookworm/core/Main"
 55 | val targetProcessNameList =
 56 |     targetProcessName.split(";").filter { it.isNotBlank() && it.isNotEmpty() }
 57 | val processNameArray = targetProcessNameList.joinToString("\", \"", "{\"", "\"}")
 58 | val processNameArraySize = targetProcessNameList.size
 59 | val platform: OperatingSystem = OperatingSystem.current()
 60 | 
 61 | checkProperties()
 62 | rootProject.allprojects.forEach { it.buildDir.deleteRecursively() }
 63 | 
 64 | android {
 65 |     compileSdkVersion(moduleCompileSdkVersion)
 66 |     defaultConfig {
 67 |         minSdkVersion(moduleMinSdkVersion)
 68 |         targetSdkVersion(moduleTargetSdkVersion)
 69 |         externalNativeBuild { cmake { addDefinitions() } }
 70 |     }
 71 |     buildTypes {
 72 |         named("release") {
 73 |             isMinifyEnabled = true
 74 |             proguardFiles(
 75 |                 getDefaultProguardFile("proguard-android-optimize.txt"),
 76 |                 "proguard-rules.pro"
 77 |             )
 78 |         }
 79 |     }
 80 |     compileOptions {
 81 |         sourceCompatibility = JavaVersion.VERSION_1_8
 82 |         targetCompatibility = JavaVersion.VERSION_1_8
 83 |     }
 84 |     kotlinOptions {
 85 |         jvmTarget = "1.8"
 86 |     }
 87 |     externalNativeBuild {
 88 |         cmake {
 89 |             path = file("src/main/cpp/CMakeLists.txt")
 90 |             version = cmakeVersion
 91 |         }
 92 |     }
 93 | 
 94 |     libraryVariants.all {
 95 |         if (name == "release") {
 96 |             sdkDirectory.buildModule()
 97 |         }
 98 |     }
 99 | }
100 | dependencies {
101 |     implementation("androidx.core:core-ktx:$corektxVersion")
102 | }
103 | 
104 | fun checkProperties() {
105 |     if (moduleId.isEmpty()) {
106 |         error("moduleId must be fill out!")
107 |     }
108 |     if (moduleMainClass.isEmpty()) {
109 |         error("moduleMainClass must be fill out!")
110 |     }
111 | }
112 | 
113 | fun ExternalNativeCmakeOptions.addDefinitions() = arguments(
114 |     "-DDEX_PATH=\"$moduleDexPath\"",
115 |     "-DMAIN_CLASS=\"$hookwormMainClass\"",
116 |     "-DPROCESS_NAME_ARRAY=$processNameArray",
117 |     "-DPROCESS_NAME_ARRAY_SIZE=$processNameArraySize",
118 |     "-DMODULE_NAME=riru_$moduleId",
119 |     "-DRIRU_MODULE_API_VERSION=$maxRiruApiVersionCode",
120 |     "-DRIRU_MODULE_VERSION=$moduleVersionCode",
121 |     "-DRIRU_MODULE_VERSION_NAME=\"$moduleVersionName\""
122 | )
123 | 
124 | var magiskDir = ""
125 | 
126 | fun File.buildModule() {
127 |     initModuleInfo()
128 |     val task = rootProject.project("app").tasks.find { it.name == "assemble" }
129 |         ?: error("Please dependent on to an app module!")
130 |     val buildDir = task.project.buildDir.also { it.deleteRecursively() }
131 |     task.doLast {
132 |         val zipPath = buildDir.resolve("intermediates/magisk/").apply {
133 |             deleteRecursively()
134 |             magiskDir = absolutePath
135 |             mkdirs()
136 |         }
137 |         copy {
138 |             into(zipPath)
139 |             processResource()
140 |             processScript()
141 |             zipTree(File(buildDir, "outputs/apk/release/app-release-unsigned.apk")
142 |                 .also { if (!it.exists()) error("${it.name} not found!") }).let { apkFileTree ->
143 |                 processLibs(apkFileTree)
144 |                 processDex(apkFileTree)
145 |             }
146 |         }
147 |         zipPath.apply {
148 |             val prop = "name=$moduleName\n" +
149 |                     "version=$moduleVersionName\n" +
150 |                     "versionCode=$moduleVersionCode\n" +
151 |                     "author=$moduleAuthor\n" +
152 |                     "description=$moduleDescription"
153 |             resolve("module.prop").writeText(
154 |                 "id=$moduleId\n$prop\ntarget_process_name=$targetProcessName\nlibrary_path=$libraryPath"
155 |             )
156 |             resolve("riru").apply {
157 |                 mkdir()
158 |                 resolve("module.prop.new").writeText(
159 |                     "$prop\nminApi=$minRiruApiVersionCode"
160 |                 )
161 |             }
162 |             resolve("extras.files").run {
163 |                 if (debug) {
164 |                     createNewFile()
165 |                 } else {
166 |                     writeText("${moduleDexPath}\n")
167 |                 }
168 |             }
169 |             fixLineBreaks()
170 |             generateSHA256Sum()
171 |         }
172 |         buildDir.resolve("outputs/module").also { it.mkdirs() }.run {
173 |             val moduleFile = File(this, "${moduleId}_$moduleVersionName.zip")
174 |             zipTo(moduleFile, zipPath)
175 |             if (automaticInstallation) {
176 |                 if (isDeviceConnected(this@buildModule)) {
177 |                     if (installModuleFailed(this@buildModule, moduleFile, zipPath)) {
178 |                         openInGUI()
179 |                         error("Module installation failed!")
180 |                     }
181 |                 } else {
182 |                     openInGUI()
183 |                     error("Device not connected or connected more than one device!")
184 |                 }
185 |             } else {
186 |                 openInGUI()
187 |             }
188 |         }
189 |     }
190 | }
191 | 
192 | fun initModuleInfo() = (project.tasks.find { it.name == "compileReleaseJavaWithJavac" }
193 |     ?: error("Task 'compileReleaseJavaWithJavac' not found!"))
194 |     .doLast {
195 |         val classPool = ClassPool.getDefault()
196 |         val moduleInfoClassPath =
197 |             buildDir.resolve("intermediates/javac/release/classes").absolutePath
198 |         classPool.insertClassPath(moduleInfoClassPath)
199 |         classPool.getCtClass("com.wuyr.hookworm.core.ModuleInfo").run {
200 |             getDeclaredMethod("getMainClass").name = "getMainClassOld"
201 |             addMethod(
202 |                 CtMethod.make("static String getMainClass(){ return \"$moduleMainClass\"; }", this)
203 |             )
204 |             getDeclaredMethod("getDexPath").name = "getDexPathOld"
205 |             addMethod(
206 |                 CtMethod.make("static String getDexPath(){ return \"$moduleDexPath\"; }", this)
207 |             )
208 | 
209 |             if (debug) {
210 |                 getDeclaredMethod("isDebug").name = "isDebugOld"
211 |                 addMethod(CtMethod.make("static boolean isDebug(){ return true; }", this))
212 |             }
213 | 
214 |             val hasSOFile =
215 |                 (rootProject.project("app").tasks.find { it.name == "mergeReleaseNativeLibs" }
216 |                     ?: error("Task 'mergeReleaseNativeLibs' not found!")).run {
217 |                     this is com.android.build.gradle.internal.tasks.MergeNativeLibsTask && externalLibNativeLibs.files.any {
218 |                         it.isDirectory && it.list()?.isNotEmpty() == true
219 |                     }
220 |                 }
221 |             if (hasSOFile) {
222 |                 getDeclaredMethod("hasSOFile").name = "hasSOFileOld"
223 |                 addMethod(CtMethod.make("static boolean hasSOFile(){ return true; }", this))
224 | 
225 |                 getDeclaredMethod("getSOPath").name = "getSOPathOld"
226 |                 addMethod(
227 |                     CtMethod.make(
228 |                         "static String getSOPath(){ return \"$libraryPath\"; }",
229 |                         this
230 |                     )
231 |                 )
232 |             }
233 |             writeFile(moduleInfoClassPath)
234 |             detach()
235 |         }
236 |     }
237 | 
238 | fun CopySpec.processResource() = from(file("src/main/resource")) {
239 |     exclude("riru.sh", "versions.properties")
240 | }
241 | 
242 | fun CopySpec.processScript() =
243 |     from(file("src/main/resource/riru.sh")) {
244 |         filter { line ->
245 |             line.replace("%%%RIRU_MODULE_ID%%%", moduleId)
246 |                 .replace("%%%RIRU_MIN_API_VERSION%%%", minRiruApiVersionCode.toString())
247 |                 .replace("%%%RIRU_MIN_VERSION_NAME%%%", minRiruApiVersionName)
248 |         }
249 |         filter(FixCrLfFilter::class.java)
250 |     }
251 | 
252 | fun CopySpec.processLibs(apkFileTree: FileTree) = from(apkFileTree) {
253 |     include("lib/**")
254 |     eachFile {
255 |         path = path.replace("lib/armeabi-v7a", "system/lib")
256 |             .replace("lib/armeabi", "system/lib")
257 |             .replace("lib/x86_64", "system_x86/lib64")
258 |             .replace("lib/x86", "system_x86/lib")
259 |             .replace("lib/arm64-v8a", "system/lib64")
260 |     }
261 | }
262 | 
263 | fun CopySpec.processDex(apkFileTree: FileTree) = from(apkFileTree) {
264 |     include("classes.dex")
265 |     eachFile { path = moduleDexPath }
266 | }
267 | 
268 | fun File.fixLineBreaks() {
269 |     val ignoreDirs = arrayOf("system", "system_x86")
270 |     val ignoreSuffix = arrayOf("so", "dex")
271 |     fun walk(file: File) {
272 |         if (file.isDirectory) {
273 |             if (!ignoreDirs.contains(file.name)) {
274 |                 file.listFiles()?.forEach { if (!ignoreDirs.contains(it.name)) walk(it) }
275 |             }
276 |         } else {
277 |             if (ignoreSuffix.none { file.absolutePath.endsWith(it) }) {
278 |                 file.readText().run {
279 |                     if (contains("\r\n")) file.writeText(replace("\r\n", "\n"))
280 |                 }
281 |             }
282 |         }
283 |     }
284 |     walk(this)
285 | }
286 | 
287 | fun File.generateSHA256Sum() = fileTree(this).matching {
288 |     exclude("customize.sh", "verify.sh", "META-INF")
289 | }.filter { it.isFile }.forEach { file ->
290 |     File(file.absolutePath + ".sha256sum").writeText(
291 |         MessageDigest.getInstance("SHA-256").digest(file.readBytes()).run {
292 |             joinToString("") { String.format("%02x", it.toInt() and 0xFF) }
293 |         })
294 | }
295 | 
296 | fun File.openInGUI() = platform.runCatching {
297 |     Runtime.getRuntime().exec(
298 |         "${
299 |             when {
300 |                 isWindows -> "explorer"
301 |                 isLinux -> "nautilus"
302 |                 isMacOsX -> "open"
303 |                 else -> ""
304 |             }
305 |         } ${this@openInGUI}"
306 |     )
307 | }.isSuccess
308 | 
309 | val platformArgs: Array
310 |     get() = if (platform.isWindows) arrayOf("cmd", "/C") else arrayOf("/bin/bash", "-c")
311 | 
312 | val File.adb: String get() = if (platform.isWindows) "SET Path=$this/platform-tools&&adb" else "$this/platform-tools/adb"
313 | val File.adbWithoutSetup: String get() = if (platform.isWindows) "adb" else "$this/platform-tools/adb"
314 | 
315 | fun isDeviceConnected(sdkDirectory: File) =
316 |     exec("${sdkDirectory.adb} devices").count { it == '\n' } == 3
317 | 
318 | fun exec(command: String) = runCatching {
319 |     Runtime.getRuntime().exec(arrayOf(*platformArgs, command)).run {
320 |         waitFor()
321 |         inputStream.reader().readText().also { destroy() }
322 |     }
323 | }.getOrDefault("")
324 | 
325 | fun installModuleFailed(sdkDirectory: File, moduleFile: File, zipPath: File) =
326 |     if (debug && (exec("${sdkDirectory.adb} shell ls $moduleDexPath&&echo 1").contains("1"))) {
327 |         if (exec("${sdkDirectory.adb} push $magiskDir/$moduleDexPath /data/local/tmp/&&echo 1".also { it.p() })
328 |                 .contains("1")
329 |         ) {
330 |             targetProcessNameList.forEach { processName ->
331 |                 exec("adb shell su -c killall $processName".also { it.p() })
332 |             }
333 |             "*********************************".p()
334 |             "Module installation is completed.".p()
335 |             "*********************************".p()
336 |             false
337 |         } else {
338 |             "*********************************".p()
339 |             "Module installation failed! Please try again.".p()
340 |             "*********************************".p()
341 |             true
342 |         }
343 |     } else {
344 |         exec("${sdkDirectory.adb} shell rm /data/local/tmp/$moduleId.dex".also { it.p() })
345 |         if (debug) {
346 |             exec("${sdkDirectory.adb} push $magiskDir/$moduleDexPath /data/local/tmp/").p()
347 |         }
348 |         "${sdkDirectory.adb} push $moduleFile /data/local/tmp/&&${sdkDirectory.adbWithoutSetup} push $zipPath/META-INF/com/google/android/update-binary /data/local/tmp/&&${sdkDirectory.adbWithoutSetup} shell su".runCatching {
349 |             Runtime.getRuntime().exec(arrayOf(*platformArgs, this)).run {
350 |                 thread(isDaemon = true) { readContentSafely(inputStream) { it.p() } }
351 |                 thread(isDaemon = true) { readContentSafely(errorStream) { it.p() } }
352 |                 outputStream.run {
353 |                     write("cd /data/local/tmp&&BOOTMODE=true sh update-binary dummy 1 ${moduleFile.name}&&rm update-binary&&rm ${moduleFile.name}&&reboot\n".toByteArray())
354 |                     flush()
355 |                     close()
356 |                 }
357 |                 waitFor()
358 |                 destroy()
359 |             }
360 |             false
361 |         }.getOrDefault(true)
362 |     }
363 | 
364 | fun Process.readContentSafely(inputStream: java.io.InputStream, onReadLine: (String) -> Unit) {
365 |     runCatching {
366 |         inputStream.bufferedReader().use { reader ->
367 |             var line = ""
368 |             while (runCatching { exitValue() }.isFailure && reader.readLine()
369 |                     ?.also { line = it } != null
370 |             ) {
371 |                 onReadLine(line)
372 |             }
373 |         }
374 |     }
375 | }
376 | 
377 | fun Any?.p() = println(this)


--------------------------------------------------------------------------------
/HookwormForAndroid/src/main/java/com/wuyr/hookworm/core/Hookworm.kt:
--------------------------------------------------------------------------------
  1 | package com.wuyr.hookworm.core
  2 | 
  3 | import android.annotation.SuppressLint
  4 | import android.app.Activity
  5 | import android.app.Application
  6 | import android.content.Context
  7 | import android.os.Build
  8 | import android.os.Bundle
  9 | import android.os.Handler
 10 | import android.os.Looper
 11 | import android.util.Log
 12 | import android.view.ContextThemeWrapper
 13 | import android.view.View
 14 | import com.wuyr.hookworm.extensions.PhoneLayoutInflater
 15 | import com.wuyr.hookworm.extensions.SimpleActivityLifecycleCallbacks
 16 | import com.wuyr.hookworm.utils.*
 17 | import dalvik.system.DexFile
 18 | import java.io.File
 19 | import java.lang.reflect.Proxy
 20 | import kotlin.concurrent.thread
 21 | 
 22 | /**
 23 |  * @author wuyr
 24 |  * @github https://github.com/wuyr/HookwormForAndroid
 25 |  * @since 2020-09-20 下午3:07
 26 |  */
 27 | @Suppress("unused")
 28 | object Hookworm {
 29 | 
 30 |     /**
 31 |      * 是否转接插件Dex的ClassLoader
 32 |      * 如果引用到了目标应用的一些自定义类或接口(或第三方库),则需要转接,否则会报 [ClassNotFoundException]
 33 |      */
 34 |     @JvmStatic
 35 |     var transferClassLoader = false
 36 |         set(value) {
 37 |             if (field != value) {
 38 |                 field = value
 39 |                 if (isApplicationInitialized && value) {
 40 |                     application.initClassLoader()
 41 |                 }
 42 |             }
 43 |         }
 44 | 
 45 |     /**
 46 |      * 是否劫持全局的LayoutInflater
 47 |      */
 48 |     @JvmStatic
 49 |     var hookGlobalLayoutInflater = false
 50 |         set(value) {
 51 |             if (field != value) {
 52 |                 field = value
 53 |                 if (isApplicationInitialized && value) {
 54 |                     initGlobalLayoutInflater()
 55 |                 }
 56 |             }
 57 |         }
 58 |     private var globalLayoutInflater: PhoneLayoutInflater? = null
 59 | 
 60 |     /**
 61 |      * 进程Application实例
 62 |      */
 63 |     @JvmStatic
 64 |     lateinit var application: Application
 65 |         private set
 66 | 
 67 |     /**
 68 |      * 进程存活Activity实例集合
 69 |      */
 70 |     @JvmStatic
 71 |     val activities = HashMap()
 72 | 
 73 |     /**
 74 |      * 监听Application初始化
 75 |      */
 76 |     @JvmStatic
 77 |     var onApplicationInitializedListener: ((Application) -> Unit)? = null
 78 |     private var isApplicationInitialized = false
 79 | 
 80 |     private val activityLifecycleCallbackList =
 81 |         HashMap()
 82 | 
 83 |     private var postInflateListenerList =
 84 |         HashMap View?)?>()
 85 | 
 86 |     private val tempActivityLifecycleCallbacksList =
 87 |         ArrayList()
 88 | 
 89 |     /**
 90 |      * 拦截LayoutInflater布局加载
 91 |      *
 92 |      *  @param className 对应的Activity类名(完整类名),空字符串则表示拦截所有Activity的布局加载
 93 |      *  @param postInflateListener 用来接收回调的lambda,需返回加载后的View(可在返回前对这个View做手脚)
 94 |      *
 95 |      *  Lambda参数
 96 |      *      resourceId:正在加载的xml ID
 97 |      *      resourceName:正在加载的xml名称
 98 |      *      rootView:加载完成后的View
 99 |      */
100 |     @JvmStatic
101 |     fun registerPostInflateListener(
102 |         className: String,
103 |         postInflateListener: (resourceId: Int, resourceName: String, rootView: View?) -> View?
104 |     ) {
105 |         postInflateListenerList[className] = postInflateListener
106 |         if (className.isEmpty() && hookGlobalLayoutInflater) {
107 |             globalLayoutInflater?.postInflateListener = postInflateListener
108 |         } else {
109 |             activities[className]?.hookLayoutInflater(postInflateListener)
110 |         }
111 |     }
112 | 
113 |     /**
114 |      *  取消拦截LayoutInflater布局加载
115 |      *
116 |      *  @param className 对应的Activity类名(完整类名)
117 |      */
118 |     @JvmStatic
119 |     fun unregisterPostInflateListener(className: String) {
120 |         if (className.isEmpty() && hookGlobalLayoutInflater) {
121 |             globalLayoutInflater?.postInflateListener = null
122 |         }
123 |         postInflateListenerList.remove(className)
124 |         activities[className]?.let { activity ->
125 |             val oldInflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
126 |             if (oldInflater is PhoneLayoutInflater) {
127 |                 oldInflater.postInflateListener = null
128 |             }
129 |         }
130 |     }
131 | 
132 |     /**
133 |      * 监听Activity的生命周期
134 |      *
135 |      *  @param className 对应的Activity类名(完整类名),空字符串表示监听所有Activity
136 |      *  @param callback ActivityLifecycleCallbacks实例
137 |      */
138 |     @JvmStatic
139 |     fun registerActivityLifecycleCallbacks(
140 |         className: String, callback: Application.ActivityLifecycleCallbacks
141 |     ) {
142 |         activityLifecycleCallbackList[className] = callback
143 |     }
144 | 
145 |     /**
146 |      *  取消监听Activity的生命周期
147 |      *
148 |      *  @param className 对应的Activity类名(完整类名)
149 |      */
150 |     @JvmStatic
151 |     fun unregisterActivityLifecycleCallbacks(className: String) =
152 |         activityLifecycleCallbackList.remove(className)
153 | 
154 |     /**
155 |      *  根据完整类名查找Activity对象
156 |      *
157 |      *  @param className 对应的Activity类名(完整类名)
158 |      *  @return 对应的Activity实例,找不到即为null
159 |      */
160 |     @JvmStatic
161 |     fun findActivityByClassName(className: String) = activities[className]
162 | 
163 |     private var initialized = false
164 | 
165 |     @SuppressLint("PrivateApi", "DiscouragedPrivateApi")
166 |     @Suppress("ControlFlowWithEmptyBody")
167 |     @JvmStatic
168 |     fun init() {
169 |         if (initialized) return
170 |         throwReflectException = true
171 |         initialized = true
172 |         thread(isDaemon = true) {
173 |             try {
174 |                 while (Looper.getMainLooper() == null) {
175 |                 }
176 |                 val currentApplicationMethod = Class.forName("android.app.ActivityThread")
177 |                     .getDeclaredMethod("currentApplication").also { it.isAccessible = true }
178 |                 while (currentApplicationMethod.invoke(null) == null) {
179 |                 }
180 |                 "android.app.ActivityThread".invoke(null, "currentApplication")!!.run {
181 |                     application = this
182 |                     if (ModuleInfo.isDebug() && Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
183 |                         hiddenApiExemptions()
184 |                     }
185 |                     initGlobalLayoutInflater()
186 |                     initClassLoader()
187 |                     initLibrary()
188 |                     Handler(Looper.getMainLooper()).post {
189 |                         onApplicationInitializedListener?.invoke(this)
190 |                     }
191 |                     registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks())
192 |                     tempActivityLifecycleCallbacksList.forEach { callback ->
193 |                         registerActivityLifecycleCallbacks(callback)
194 |                     }
195 |                     tempActivityLifecycleCallbacksList.clear()
196 |                     isApplicationInitialized = true
197 |                 }
198 |             } catch (e: Exception) {
199 |                 Log.e(Main.TAG, Log.getStackTraceString(e))
200 |             }
201 |         }
202 |     }
203 | 
204 |     private fun hiddenApiExemptions() {
205 |         try {
206 |             Impactor.hiddenApiExemptions()
207 |         } catch (t: Throwable) {
208 |             try {
209 |                 DexFile(ModuleInfo.getDexPath()).apply {
210 |                     loadClass(Impactor::class.java.canonicalName, null)
211 |                         .invokeVoid(null, "hiddenApiExemptions")
212 |                     close()
213 |                 }
214 |             } catch (t: Throwable) {
215 |                 Log.e(Main.TAG, t.toString(), t)
216 |             }
217 |         }
218 |     }
219 | 
220 |     private fun hookLayoutInflater(className: String, activity: Activity) {
221 |         if (postInflateListenerList.isNotEmpty()) {
222 |             (postInflateListenerList[className]
223 |                 ?: if (hookGlobalLayoutInflater) null else postInflateListenerList[""])
224 |                 ?.also { activity.hookLayoutInflater(it) }
225 |         }
226 |     }
227 | 
228 |     @SuppressLint("PrivateApi")
229 |     private fun Activity.hookLayoutInflater(
230 |         postInflateListener: (resourceId: Int, resourceName: String, rootView: View?) -> View?
231 |     ) {
232 |         val oldInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE)
233 |         if (oldInflater is PhoneLayoutInflater) {
234 |             oldInflater.postInflateListener = postInflateListener
235 |         } else {
236 |             val inflater = PhoneLayoutInflater(this).also {
237 |                 it.postInflateListener = postInflateListener
238 |             }
239 |             try {
240 |                 ContextThemeWrapper::class.set(this, "mInflater", inflater)
241 |             } catch (e: Exception) {
242 |                 Log.e(Main.TAG, "hookLayoutInflater", e)
243 |             }
244 |         }
245 |         val oldWindowInflater = window.layoutInflater
246 |         if (oldWindowInflater is PhoneLayoutInflater) {
247 |             oldWindowInflater.postInflateListener = postInflateListener
248 |         } else {
249 |             try {
250 |                 val phoneWindowClass =
251 |                     Class.forName("com.android.internal.policy.PhoneWindow")
252 |                 if (phoneWindowClass.isInstance(window)) {
253 |                     val inflater = PhoneLayoutInflater(this).also {
254 |                         it.postInflateListener = postInflateListener
255 |                     }
256 |                     phoneWindowClass.set(window, "mLayoutInflater", inflater)
257 |                 }
258 |             } catch (e: Exception) {
259 |                 Log.e(Main.TAG, "hookLayoutInflater", e)
260 |             }
261 |         }
262 |     }
263 | 
264 |     private fun Application.initClassLoader() {
265 |         try {
266 |             if (transferClassLoader) {
267 |                 ClassLoader::class.set(
268 |                     Hookworm::class.java.classLoader, "parent", this::class.java.classLoader
269 |                 )
270 |             }
271 |         } catch (e: Exception) {
272 |             Log.e(Main.TAG, "initClassLoader", e)
273 |         }
274 |     }
275 | 
276 |     private fun initLibrary() {
277 |         try {
278 |             @Suppress("ConstantConditionIf")
279 |             if (ModuleInfo.hasSOFile()) {
280 |                 "dalvik.system.BaseDexClassLoader".get(
281 |                     Hookworm::class.java.classLoader, "pathList"
282 |                 )?.let { pathList ->
283 |                     pathList::class.run {
284 |                         val newDirectories = get>(
285 |                             pathList, "nativeLibraryDirectories"
286 |                         )!! + pathList::class.get>(
287 |                             pathList, "systemNativeLibraryDirectories"
288 |                         )!! + File(application.applicationInfo.dataDir, ModuleInfo.getSOPath())
289 |                         set(
290 |                             pathList, "nativeLibraryPathElements",
291 |                             invoke(
292 |                                 pathList,
293 |                                 "makePathElements",
294 |                                 List::class to newDirectories
295 |                             )
296 |                         )
297 |                     }
298 |                 }
299 |             }
300 |         } catch (e: Exception) {
301 |             Log.e(Main.TAG, "initLibrary", e)
302 |         }
303 |     }
304 | 
305 |     @SuppressLint("PrivateApi")
306 |     private fun initGlobalLayoutInflater() {
307 |         try {
308 |             if (hookGlobalLayoutInflater && globalLayoutInflater == null) {
309 |                 "android.app.SystemServiceRegistry".get>(
310 |                     null, "SYSTEM_SERVICE_FETCHERS"
311 |                 )?.let { fetchers ->
312 |                     fetchers[Context.LAYOUT_INFLATER_SERVICE]?.let { layoutInflaterFetcher ->
313 |                         fetchers[Context.LAYOUT_INFLATER_SERVICE] = Proxy.newProxyInstance(
314 |                             ClassLoader.getSystemClassLoader(),
315 |                             arrayOf(Class.forName("android.app.SystemServiceRegistry\$ServiceFetcher"))
316 |                         ) { _, method, args ->
317 |                             if (method.name == "getService") {
318 |                                 method.invoke(layoutInflaterFetcher, *args ?: arrayOf())
319 |                                 globalLayoutInflater
320 |                                     ?: PhoneLayoutInflater(args[0] as Context?).also {
321 |                                         globalLayoutInflater = it
322 |                                         it.postInflateListener = postInflateListenerList[""]
323 |                                     }
324 |                             } else method.invoke(layoutInflaterFetcher, *args ?: arrayOf())
325 |                         }
326 |                     }
327 |                 }
328 |             }
329 |         } catch (e: Exception) {
330 |             Log.e(Main.TAG, "initGlobalLayoutInflater", e)
331 |         }
332 |     }
333 | 
334 |     /**
335 |      *  监听[Activity.onCreate]方法回调
336 |      *  @param className 对应的Activity类名(完整类名)
337 |      *  @param callback Callback lambda
338 |      *
339 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
340 |      */
341 |     fun registerOnActivityCreated(
342 |         className: String, callback: (Activity, Bundle?) -> Unit
343 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
344 |         override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
345 |             if (activity::class.java.name == className) {
346 |                 callback(activity, savedInstanceState)
347 |             }
348 |         }
349 |     }.apply {
350 |         if (::application.isInitialized) {
351 |             application.registerActivityLifecycleCallbacks(this)
352 |         } else {
353 |             tempActivityLifecycleCallbacksList += this
354 |         }
355 |     }
356 | 
357 |     /**
358 |      *  监听[Activity.onStart]方法回调
359 |      *  @param className 对应的Activity类名(完整类名)
360 |      *  @param callback Callback lambda
361 |      *
362 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
363 |      */
364 |     fun registerOnActivityStarted(
365 |         className: String, callback: (Activity) -> Unit
366 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
367 |         override fun onActivityStarted(activity: Activity) {
368 |             if (activity::class.java.name == className) {
369 |                 callback(activity)
370 |             }
371 |         }
372 |     }.apply {
373 |         if (::application.isInitialized) {
374 |             application.registerActivityLifecycleCallbacks(this)
375 |         } else {
376 |             tempActivityLifecycleCallbacksList += this
377 |         }
378 |     }
379 | 
380 |     /**
381 |      *  监听[Activity.onResume]方法回调
382 |      *  @param className 对应的Activity类名(完整类名)
383 |      *  @param callback Callback lambda
384 |      *
385 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
386 |      */
387 |     fun registerOnActivityResumed(
388 |         className: String, callback: (Activity) -> Unit
389 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
390 |         override fun onActivityResumed(activity: Activity) {
391 |             if (activity::class.java.name == className) {
392 |                 callback(activity)
393 |             }
394 |         }
395 |     }.apply {
396 |         if (::application.isInitialized) {
397 |             application.registerActivityLifecycleCallbacks(this)
398 |         } else {
399 |             tempActivityLifecycleCallbacksList += this
400 |         }
401 |     }
402 | 
403 |     /**
404 |      *  监听[Activity.onPause]方法回调
405 |      *  @param className 对应的Activity类名(完整类名)
406 |      *  @param callback Callback lambda
407 |      *
408 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
409 |      */
410 |     fun registerOnActivityPaused(
411 |         className: String, callback: (Activity) -> Unit
412 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
413 |         override fun onActivityPaused(activity: Activity) {
414 |             if (activity::class.java.name == className) {
415 |                 callback(activity)
416 |             }
417 |         }
418 |     }.apply {
419 |         if (::application.isInitialized) {
420 |             application.registerActivityLifecycleCallbacks(this)
421 |         } else {
422 |             tempActivityLifecycleCallbacksList += this
423 |         }
424 |     }
425 | 
426 |     /**
427 |      *  监听[Activity.onStop]方法回调
428 |      *  @param className 对应的Activity类名(完整类名)
429 |      *  @param callback Callback lambda
430 |      *
431 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
432 |      */
433 |     fun registerOnActivityStopped(
434 |         className: String, callback: (Activity) -> Unit
435 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
436 |         override fun onActivityStopped(activity: Activity) {
437 |             if (activity::class.java.name == className) {
438 |                 callback(activity)
439 |             }
440 |         }
441 |     }.apply {
442 |         if (::application.isInitialized) {
443 |             application.registerActivityLifecycleCallbacks(this)
444 |         } else {
445 |             tempActivityLifecycleCallbacksList += this
446 |         }
447 |     }
448 | 
449 |     /**
450 |      *  监听[Activity.onDestroy]方法回调
451 |      *  @param className 对应的Activity类名(完整类名)
452 |      *  @param callback Callback lambda
453 |      *
454 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
455 |      */
456 |     fun registerOnActivityDestroyed(
457 |         className: String, callback: (Activity) -> Unit
458 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
459 |         override fun onActivityDestroyed(activity: Activity) {
460 |             if (activity::class.java.name == className) {
461 |                 callback(activity)
462 |             }
463 |         }
464 |     }.apply {
465 |         if (::application.isInitialized) {
466 |             application.registerActivityLifecycleCallbacks(this)
467 |         } else {
468 |             tempActivityLifecycleCallbacksList += this
469 |         }
470 |     }
471 | 
472 |     /**
473 |      *  监听[Activity.onSaveInstanceState]方法回调
474 |      *  @param className 对应的Activity类名(完整类名)
475 |      *  @param callback Callback lambda
476 |      *
477 |      *  @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
478 |      */
479 |     fun registerOnActivitySaveInstanceState(
480 |         className: String, callback: (Activity, Bundle) -> Unit
481 |     ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
482 |         override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
483 |             if (activity::class.java.name == className) {
484 |                 callback(activity, outState)
485 |             }
486 |         }
487 |     }.apply {
488 |         if (::application.isInitialized) {
489 |             application.registerActivityLifecycleCallbacks(this)
490 |         } else {
491 |             tempActivityLifecycleCallbacksList += this
492 |         }
493 |     }
494 | 
495 |     private class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
496 | 
497 |         override fun onActivityCreated(
498 |             activity: Activity, savedInstanceState: Bundle?
499 |         ) {
500 |             val className = activity::class.java.name
501 |             hookLayoutInflater(className, activity)
502 |             activities[className] = activity
503 |             activityLifecycleCallbackList[className]
504 |                 ?.onActivityCreated(activity, savedInstanceState)
505 |             activityLifecycleCallbackList[""]
506 |                 ?.onActivityCreated(activity, savedInstanceState)
507 |         }
508 | 
509 |         override fun onActivityStarted(activity: Activity) {
510 |             activityLifecycleCallbackList[activity::class.java.name]
511 |                 ?.onActivityStarted(activity)
512 |             activityLifecycleCallbackList[""]?.onActivityStarted(activity)
513 |         }
514 | 
515 |         override fun onActivityResumed(activity: Activity) {
516 |             activityLifecycleCallbackList[activity::class.java.name]
517 |                 ?.onActivityResumed(activity)
518 |             activityLifecycleCallbackList[""]?.onActivityResumed(activity)
519 |         }
520 | 
521 |         override fun onActivityPaused(activity: Activity) {
522 |             activityLifecycleCallbackList[activity::class.java.name]
523 |                 ?.onActivityPaused(activity)
524 |             activityLifecycleCallbackList[""]?.onActivityPaused(activity)
525 |         }
526 | 
527 |         override fun onActivityStopped(activity: Activity) {
528 |             activityLifecycleCallbackList[activity::class.java.name]
529 |                 ?.onActivityStopped(activity)
530 |             activityLifecycleCallbackList[""]?.onActivityStopped(activity)
531 |         }
532 | 
533 |         override fun onActivityDestroyed(activity: Activity) {
534 |             val className = activity::class.java.name
535 |             activities.remove(className)
536 |             activityLifecycleCallbackList[className]
537 |                 ?.onActivityDestroyed(activity)
538 |             activityLifecycleCallbackList[""]?.onActivityDestroyed(activity)
539 |         }
540 | 
541 |         override fun onActivitySaveInstanceState(
542 |             activity: Activity, outState: Bundle
543 |         ) {
544 |             activityLifecycleCallbackList[activity::class.java.name]
545 |                 ?.onActivitySaveInstanceState(activity, outState)
546 |             activityLifecycleCallbackList[""]
547 |                 ?.onActivitySaveInstanceState(activity, outState)
548 |         }
549 |     }
550 | }
551 | 


--------------------------------------------------------------------------------