├── .gitignore ├── LICENSE ├── README.md ├── WechatIMG4.jpeg ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── java │ └── com │ │ └── ehook │ │ └── dy │ │ ├── AppGlobal.java │ │ ├── BaseApp.kt │ │ ├── DyHooker.kt │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.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 │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── dy ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── evan │ │ └── dy │ │ ├── Global.java │ │ ├── WeWorkService.java │ │ ├── api │ │ ├── ChatRoomApi.kt │ │ ├── callback │ │ │ └── EICommonCallback.kt │ │ └── model │ │ │ └── LiveRoomMessage.java │ │ ├── hookers │ │ └── MessageHook.kt │ │ ├── interfaces │ │ ├── IApplicationHook.kt │ │ └── IMessageHook.kt │ │ ├── mirror │ │ ├── PackageUtils.kt │ │ └── com │ │ │ ├── bytedance │ │ │ └── android │ │ │ │ └── livesdk │ │ │ │ └── chatroom │ │ │ │ ├── presenter │ │ │ │ ├── Classes.kt │ │ │ │ └── Methods.kt │ │ │ │ └── viewmodule │ │ │ │ ├── Classes.kt │ │ │ │ └── Methods.kt │ │ │ └── ss │ │ │ └── ugc │ │ │ └── live │ │ │ └── sdk │ │ │ └── message │ │ │ └── data │ │ │ └── Classes.kt │ │ ├── plugins │ │ ├── ActivityPlugin.kt │ │ ├── MessageHookPlugin.kt │ │ └── WwEngine.kt │ │ └── utils │ │ └── JsonUtils.java │ └── res │ └── values │ └── strings.xml ├── ehook ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── easy │ │ └── hooker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ehook │ │ │ ├── EasyHook.kt │ │ │ ├── HookGlobal.kt │ │ │ ├── async │ │ │ ├── AsyncHandler.kt │ │ │ └── CrashHandler.kt │ │ │ ├── cache │ │ │ ├── ByteArrayEntry.kt │ │ │ ├── LRUCache.kt │ │ │ ├── LRUDiskCache.kt │ │ │ └── LRUMemoryCache.kt │ │ │ ├── core │ │ │ ├── Clazz.kt │ │ │ ├── EHook.kt │ │ │ ├── HookCenter.kt │ │ │ ├── IHookProvider.kt │ │ │ ├── InterfaceProxy.kt │ │ │ ├── Operation.kt │ │ │ ├── Version.kt │ │ │ └── WaitChannel.kt │ │ │ ├── helper │ │ │ ├── ExtensionHelper.kt │ │ │ ├── ParserHelper.kt │ │ │ ├── ProxyHelper.kt │ │ │ ├── ReflecterHelper.kt │ │ │ └── TryHelper.kt │ │ │ ├── media │ │ │ ├── audio │ │ │ │ ├── AudioHelper.kt │ │ │ │ └── MediaCodecHelper.kt │ │ │ └── image │ │ │ │ └── BitmapHelper.kt │ │ │ ├── okhttp │ │ │ ├── HttpClients.kt │ │ │ ├── ICallbacks.kt │ │ │ ├── IHttpConfigs.kt │ │ │ └── Interceptors.kt │ │ │ ├── plugins │ │ │ ├── ActivityHook.kt │ │ │ ├── SharedEngine.kt │ │ │ └── interfaces │ │ │ │ └── IActivityHook.kt │ │ │ └── utils │ │ │ ├── CmdUtil.kt │ │ │ ├── FileUtil.kt │ │ │ ├── LogUtil.kt │ │ │ ├── MirrorUtil.kt │ │ │ ├── ParallelUtil.kt │ │ │ └── XposedUtil.kt │ ├── jniLibs │ │ ├── arm64-v8a │ │ │ └── libsilk.so │ │ └── armeabi-v7a │ │ │ └── libsilk.so │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── easy │ └── hooker │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | gen/ 13 | 14 | # Gradle files 15 | .gradle/ 16 | build/ 17 | /*/build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Eclipse project files 29 | .classpath 30 | .project 31 | .settings/ 32 | 33 | # Android Studio 34 | .idea/ 35 | *.iml 36 | *.ipr 37 | *.iws 38 | 39 | # Mac system files 40 | .DS_Store 41 | node_modules/ 42 | jiagu/output/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 wangcong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xposed-douyin 2 | 基于Xposed框架写的抖音插件 3 | 4 | 当前适配版本从v11.2.0 5 | **主要功能列表** 6 | > - 直播间关键词回复 7 | > - 群发粉丝消息 未开放 8 | > - 直播间自动点赞 未开放 9 | > - 直播间自动刷礼物 未开放 10 | > - 直播间礼物回谢 未开放 11 | > - 直播间进场欢迎 未开放 12 | > - 直播间自动刷单 未开放 13 | > - 更多............... 14 | 15 | ![功能展示](https://github.com/treexi/xposed-douyin/blob/master/WechatIMG4.jpeg) 16 | ### 欢迎交流 17 | qq:1026147686 18 | 19 | **【声明】:** 20 | 此系列文章主要关于xposed的相关学习,以下所提及到的所有方式皆为学习,如有他人使用本系列学习文章中所提及的知识点用于其他非法用途,本人不承担由此造成的任何后果!! 21 | 项目及插件下架请与作者联系: 1026147686@qq.com 22 | 23 | 24 | -------------------------------------------------------------------------------- /WechatIMG4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/WechatIMG4.jpeg -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | //apply plugin: 'com.google.protobuf' 5 | 6 | android { 7 | compileSdkVersion 29 8 | buildToolsVersion "29.0.3" 9 | defaultConfig { 10 | applicationId "com.ehook.dy" 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | packagingOptions { 25 | exclude 'META-INF/INDEX.LIST' 26 | exclude 'META-INF/io.netty.versions.properties' 27 | } 28 | 29 | lintOptions { 30 | checkReleaseBuilds false 31 | abortOnError false 32 | } 33 | 34 | buildTypes { 35 | release { 36 | minifyEnabled false 37 | debuggable false 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | } 40 | debug { 41 | minifyEnabled false 42 | debuggable true 43 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 44 | } 45 | } 46 | // sourceSets { 47 | // main { 48 | // proto { 49 | // srcDir 'src/main/proto' //proto文件所在路径 50 | // include '**/*.proto' 51 | // } 52 | // java { 53 | // srcDir 'src/main/java' 54 | // } 55 | // } 56 | // } 57 | } 58 | //protobuf { 59 | // protoc { 60 | // // You still need protoc like in the non-Android case 61 | // artifact = 'com.google.protobuf:protoc:3.6.1' 62 | // } 63 | // generateProtoTasks { 64 | // all().each { task -> 65 | // task.builtins { 66 | // remove java 67 | // } 68 | // task.builtins { 69 | // java {} 70 | // } 71 | // } 72 | // } 73 | //} 74 | dependencies { 75 | implementation fileTree(dir: 'libs', include: ['*.jar']) 76 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 77 | implementation 'androidx.appcompat:appcompat:1.1.0' 78 | implementation 'androidx.core:core-ktx:1.0.2' 79 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 80 | testImplementation 'junit:junit:4.12' 81 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 82 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 83 | compileOnly "com.squareup.okhttp3:okhttp:$okhttp" 84 | implementation project(':ehook') 85 | implementation project(":dy") 86 | implementation 'com.google.code.gson:gson:2.8.6' 87 | compileOnly 'de.robv.android.xposed:api:82' 88 | compileOnly 'de.robv.android.xposed:api:82:sources' 89 | implementation "com.google.protobuf.nano:protobuf-javanano:3.2.0rc2" 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 37 | 38 | 41 | 42 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | com.ehook.dy.DyHooker 2 | -------------------------------------------------------------------------------- /app/src/main/java/com/ehook/dy/AppGlobal.java: -------------------------------------------------------------------------------- 1 | package com.ehook.dy; 2 | 3 | 4 | public class AppGlobal { 5 | public static final String TARGET_PACKAGE = "com.ehook.dy"; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/ehook/dy/BaseApp.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.dy 2 | 3 | import android.app.Application 4 | 5 | class BaseApp : Application() { 6 | override fun onCreate() { 7 | super.onCreate() 8 | // if (BuildConfig.DEBUG) { 9 | // CmdUtil.killProcesses(this, "com.tencent.mm") 10 | // CmdUtil.killProcesses(this, "com.tencent.mm") 11 | // } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/ehook/dy/DyHooker.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.dy 2 | 3 | import android.app.ActivityManager 4 | import android.app.Application 5 | import android.content.Context 6 | import android.os.Process 7 | import android.text.TextUtils 8 | import com.ehook.EasyHook 9 | import com.ehook.HookGlobal 10 | import com.ehook.helper.TryHelper.tryVerbosely 11 | import com.ehook.plugins.SharedEngine 12 | import com.ehook.utils.LogUtil 13 | import com.evan.dy.Global 14 | import com.evan.dy.WeWorkService 15 | import com.evan.dy.plugins.WwEngine 16 | import dalvik.system.PathClassLoader 17 | import de.robv.android.xposed.* 18 | import de.robv.android.xposed.callbacks.XC_LoadPackage 19 | import java.io.File 20 | 21 | 22 | class DyHooker : IXposedHookLoadPackage, IXposedHookZygoteInit { 23 | 24 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 25 | tryVerbosely { 26 | if (TextUtils.equals(Global.DY_PACKAGE_NAME, lpparam.processName) && 27 | TextUtils.equals(Global.DY_PACKAGE_NAME, lpparam.packageName) && 28 | EasyHook.isImportantDyProcess(lpparam)) { 29 | hookDyAttachBaseContext(lpparam.classLoader) { 30 | if (TextUtils.equals(Global.DY_PACKAGE_NAME, getCurrentProcessName(it))) { 31 | if (Global.isDebug) { 32 | HookGlobal.unitTestMode = true 33 | hookDyOnFly(lpparam, it) 34 | } else { 35 | hookDy(lpparam, it) 36 | } 37 | } 38 | } 39 | } else if (TextUtils.equals(AppGlobal.TARGET_PACKAGE, lpparam.processName)) { 40 | hookAttachBaseContext(lpparam.classLoader) { 41 | hookLoadHooker(lpparam.classLoader) 42 | } 43 | } 44 | } 45 | } 46 | private fun getCurrentProcessName(context: Context): String? { 47 | val pid = Process.myPid() 48 | var processName = "" 49 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 50 | for (process in manager.runningAppProcesses) { 51 | if (process.pid == pid) { 52 | processName = process.processName 53 | } 54 | } 55 | return processName 56 | } 57 | private fun hookLoadHooker(classLoader: ClassLoader) { 58 | 59 | XposedHelpers.findAndHookMethod( 60 | "com.ehook.dy.MainActivity", classLoader, 61 | "checkHook", object : XC_MethodReplacement() { 62 | override fun replaceHookedMethod(param: MethodHookParam): Any = true 63 | }) 64 | } 65 | 66 | override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam?) { 67 | LogUtil.e( 68 | "initZygote ${startupParam?.modulePath} ${startupParam?.startsSystemServer}" 69 | ) 70 | } 71 | 72 | private fun hookAttachBaseContext(classLoader: ClassLoader, callback: (Context) -> Unit) { 73 | XposedHelpers.findAndHookMethod( 74 | "android.content.ContextWrapper", 75 | classLoader, 76 | "attachBaseContext", 77 | Context::class.java, 78 | object : XC_MethodHook() { 79 | override fun afterHookedMethod(param: MethodHookParam?) { 80 | callback(param?.thisObject as? Application ?: return) 81 | } 82 | }) 83 | } 84 | private fun hookDyAttachBaseContext(classLoader: ClassLoader, callback: (Context) -> Unit) { 85 | XposedHelpers.findAndHookMethod( 86 | "com.ss.android.ugc.aweme.app.host.HostApplication", 87 | classLoader, 88 | "attachBaseContext", 89 | Context::class.java, 90 | object : XC_MethodHook() { 91 | override fun afterHookedMethod(param: MethodHookParam?) { 92 | callback(param?.thisObject as? Application ?: return) 93 | } 94 | }) 95 | } 96 | 97 | private fun hookDy(param: XC_LoadPackage.LoadPackageParam, context: Context) { 98 | when (param.packageName) { 99 | Global.DY_PACKAGE_NAME -> { 100 | EasyHook.startup( 101 | lpparam = param, 102 | plugins = WwEngine.plugins, 103 | centers = WwEngine.hookCenters + SharedEngine.hookCenters 104 | ) 105 | WeWorkService.onCreate() 106 | } 107 | } 108 | } 109 | 110 | 111 | private fun hookDyOnFly(param: XC_LoadPackage.LoadPackageParam, context: Context) { 112 | val path = EasyHook.getApplicationApkPath( 113 | context, AppGlobal.TARGET_PACKAGE 114 | ) 115 | when (File(path).exists()) { 116 | true -> { 117 | val pathClassLoader = PathClassLoader(path, ClassLoader.getSystemClassLoader()) 118 | val clazz = pathClassLoader.loadClass(DyHooker::class.java.name) 119 | val instance = clazz.newInstance() 120 | val method = clazz.getDeclaredMethod( 121 | "hookDy", 122 | XC_LoadPackage.LoadPackageParam::class.java, 123 | Context::class.java 124 | ) 125 | method.isAccessible = true 126 | method.invoke(instance, param, context) 127 | } 128 | false -> LogUtil.v("未发现指定的插件") 129 | } 130 | } 131 | 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/ehook/dy/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.dy 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.ehook.EasyHook 7 | import com.ehook.utils.CmdUtil 8 | import kotlinx.android.synthetic.main.activity_main.* 9 | 10 | class MainActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_main) 15 | CmdUtil.isRoot 16 | sample_test.setOnClickListener { 17 | } 18 | 19 | } 20 | 21 | override fun onDestroy() { 22 | super.onDestroy() 23 | } 24 | override fun onResume() { 25 | super.onResume() 26 | if (checkHook()) { 27 | val path = EasyHook.getApplicationApkPath( 28 | this, 29 | AppGlobal.TARGET_PACKAGE 30 | ) 31 | sample_text.text = "hooked = true \n \n $path" 32 | } 33 | } 34 | 35 | fun checkHook(): Boolean { 36 | return false 37 | } 38 | 39 | override fun onBackPressed() { 40 | super.onBackPressed() 41 | System.exit(0) 42 | } 43 | 44 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 45 | super.onActivityResult(requestCode, resultCode, data) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DyPlugin 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.50' 5 | ext.grpc_version = '1.1.2' 6 | 7 | repositories { 8 | mavenCentral() 9 | maven { 10 | url 'https://jitpack.io' 11 | } 12 | maven { url "https://dl.bintray.com/thelasterstar/maven/" } 13 | 14 | jcenter() 15 | google() 16 | 17 | } 18 | dependencies { 19 | classpath 'com.android.tools.build:gradle:3.5.1' 20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 21 | } 22 | } 23 | 24 | allprojects { 25 | repositories { 26 | 27 | mavenCentral() 28 | maven { 29 | url 'https://jitpack.io' 30 | } 31 | maven { url "https://dl.bintray.com/thelasterstar/maven/" } 32 | 33 | jcenter() 34 | google() 35 | 36 | } 37 | } 38 | ext { 39 | versionCode = 100 40 | versionName = "1.00" 41 | minSdkVersion = 21 42 | targetSdkVersion = 28 43 | compileSdkVersion = 28 44 | glide = '4.9.0' 45 | okhttp = '4.4.0' 46 | okhttputils = '2.6.2' 47 | } 48 | 49 | task clean(type: Delete) { 50 | delete rootProject.buildDir 51 | } 52 | -------------------------------------------------------------------------------- /dy/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /dy/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles 'consumer-rules.pro' 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | } 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 31 | implementation 'androidx.appcompat:appcompat:1.1.0' 32 | implementation 'androidx.core:core-ktx:1.1.0' 33 | testImplementation 'junit:junit:4.12' 34 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 36 | compileOnly "com.squareup.okhttp3:okhttp:$okhttp" 37 | implementation project(':ehook') 38 | implementation 'com.google.code.gson:gson:2.8.6' 39 | compileOnly 'de.robv.android.xposed:api:82' 40 | compileOnly 'de.robv.android.xposed:api:82:sources' 41 | implementation "com.google.protobuf.nano:protobuf-javanano:3.2.0rc2" 42 | 43 | } 44 | -------------------------------------------------------------------------------- /dy/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/dy/consumer-rules.pro -------------------------------------------------------------------------------- /dy/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 | -------------------------------------------------------------------------------- /dy/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/Global.java: -------------------------------------------------------------------------------- 1 | package com.evan.dy; 2 | 3 | public class Global { 4 | public static final String DY_PACKAGE_NAME = "com.ss.android.ugc.aweme"; 5 | public static final Boolean isDebug = true; 6 | } 7 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/WeWorkService.java: -------------------------------------------------------------------------------- 1 | package com.evan.dy; 2 | 3 | 4 | public class WeWorkService { 5 | public static final String TAG = "WeWorkService"; 6 | 7 | public static void onCreate() { 8 | // LogUtil.INSTANCE.e(TAG, "WeWorkService onCreate ...."); 9 | // SocketClient.getInstance() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/api/ChatRoomApi.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.api 2 | 3 | import com.ehook.utils.LogUtil 4 | import com.evan.dy.api.callback.EICommonCallback 5 | import com.evan.dy.api.model.LiveRoomMessage 6 | import com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.viewmodule.Methods.ClassCommentWidget_a 7 | 8 | object ChatRoomApi { 9 | 10 | private var commentWidget: Any? = null 11 | 12 | 13 | fun setCommentWidget(commentWidget: Any?) { 14 | this.commentWidget = commentWidget 15 | } 16 | 17 | fun getCommentWidget(): Any? { 18 | return this.commentWidget 19 | } 20 | 21 | 22 | fun handleMessage(liveRoomMessage: LiveRoomMessage) { 23 | if (liveRoomMessage != null) { 24 | when (liveRoomMessage.messageType) { 25 | 6 -> {//礼物 26 | sendRoomTextMsg("666!",null) 27 | } 28 | } 29 | } 30 | } 31 | fun sendRoomTextMsg(content: String, callback: EICommonCallback?) { 32 | LogUtil.e("ChatRoomApi", "sendRoomTextMsg. msg = $content commentWidget=$commentWidget") 33 | if (this.commentWidget == null) { 34 | callback?.onResult(-1, "重新进入", null) 35 | return 36 | } 37 | ClassCommentWidget_a.invoke(commentWidget, content, false, false) 38 | callback?.onResult(0, content, content) 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/api/callback/EICommonCallback.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.api.callback 2 | 3 | 4 | interface EICommonCallback { 5 | fun onResult(code: Int?, message: String?, obj: Any?) 6 | } 7 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/api/model/LiveRoomMessage.java: -------------------------------------------------------------------------------- 1 | package com.evan.dy.api.model; 2 | 3 | import de.robv.android.xposed.XposedHelpers; 4 | 5 | public class LiveRoomMessage { 6 | private final Object obj; 7 | 8 | public LiveRoomMessage(Object obj) { 9 | this.obj = obj; 10 | } 11 | 12 | public Object getObj() { 13 | return obj; 14 | } 15 | 16 | public int getMessageType() { 17 | Object intType = XposedHelpers.callMethod(obj, "getIntType"); 18 | return (int) intType; 19 | } 20 | 21 | 22 | @Override 23 | public String toString() { 24 | return "LiveRoomMessage{" + 25 | // "message=" + message + 26 | ", obj=" + obj + 27 | ", messageType=" + getMessageType() + 28 | '}'; 29 | } 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | public enum MessageEnum { 73 | HELLO(0, "Hello"), 74 | SETTING(0, "Setting"), 75 | GET_SETTING(0, "GetSettting"), 76 | REQUEST_RECONNECT(0, "RequestReconnect"), 77 | DEFAULT(0, "--default--"), 78 | DIGG(0, "WebcastDiggMessage"), 79 | GIFT(0, "WebcastGiftMessage"), 80 | GIFT_GROUP(0, "GiftGroupMessage"), 81 | SYSTEM(0, "SystemMessage"), 82 | CHAT(0, "WebcastChatMessage"), 83 | CONTROL(0, "WebcastControlMessage"), 84 | MEMBER(0, "WebcastMemberMessage"), 85 | ROOM(0, "WebcastRoomMessage"), 86 | SOCIAL(0, "WebcastSocialMessage"), 87 | SCREEN(0, "WebcastScreenChatMessage"), 88 | NOTICE(0, "NoticeListUnreadCountMessage"), 89 | ROOM_START(0, "RoomStartMessage"), 90 | ROOM_NOTIFY(0, "WebcastRoomNotifyMessage"), 91 | REMIND(0, "WebcastNoticeMessage"), 92 | DAILY_RANK(0, "WebcastSunDailyRankMessage"), 93 | ROOM_PUSH(0, "WebcastRoomBottomMessage"), 94 | DOODLE_GIFT(0, "WebcastDoodleGiftMessage"), 95 | MODIFY_DECORATION(0, "WebcastDecorationModifyMethod"), 96 | USER_STATS(0, "WebcastUserStatsMessage"), 97 | IN_ROOM_BANNER_MESSAGE(0, "WebcastInRoomBannerMessage"), 98 | ROOM_RICH_CHAT_MESSAGE(0, "WebcastSpecialPushMessage"), 99 | IM_MESSAGE(0, "IESChatMessage"), 100 | PROMOTION_CARD_MESSAGE(0, "WebcastPushRoomAdCard"), 101 | BANNER_RED_POINT(0, "WebcastInRoomBannerRedPoint"), 102 | LINK_MIC(0, "WebcastLinkMicMethod"), 103 | LINK_MIC_SIGNAL(0, "WebcastLinkMicSignalingMethod"), 104 | NOTICE_COUNT(0, "NoticeCountMessage"), 105 | CLOUD_CONTROL(0, "CloudControl"), 106 | ROOM_IMG_MESSAGE(0, "WebcastRoomImgMessage"), 107 | BATTLE_MODE(0, "WebcastBattleModeMessage"), 108 | GAME_QUIZ(0, "WebcastGamblingStatusChangedMessage"), 109 | LINK_MIC_ARMIES(0, "WebcastLinkMicArmiesMethod"), 110 | LINK_MIC_BATTLE(0, "WebcastLinkMicBattleMethod"), 111 | LINK_MIC_BATTLE_FINISH(0, "WebcastLinkMicBattleFinishMethod"), 112 | LINK_MIC_BATTLE_TASK(0, "WebcastLinkMicBattleTaskMessage"), 113 | LINK_MIC_BATTLE_PUNISH(0, "WebcastLinkMicBattlePunishMethod"), 114 | LOTTERY_EVENT(0, "WebcastLotteryEventMessage"), 115 | TURN_TABLE_BURST_V2(0, "WebcastLotteryBurstMessage"), 116 | COMMON_TOAST(0, "WebcastCommonToastMessage"), 117 | COMMON_GUIDE(0, "WebcastCommonGuideMessage"), 118 | GIFT_UPDATE(0, "WebcastGiftUpdateMessage"), 119 | LUCKY_BOX(0, "WebcastLuckyBoxMessage"), 120 | COMMENT_IMAGE(0, "WebcastCommentsMessage"), 121 | FANS_CLUB_STATISTICS(0, "WebcastFansclubStatisticsMessage"), 122 | USER_SEQ(0, "WebcastRoomUserSeqMessage"), 123 | LIVE_SHOPPING(0, "WebcastLiveShoppingMessage"), 124 | FANS_CLUB(0, "WebcastFansclubMessage"), 125 | CREATE_RED_PACKET(0, "WebcastCreateRedPacketMessage"), 126 | RECOMMEND_GOODS(0, "WebcastVideoLiveGoodsRcmdMessage"), 127 | RECOMMEND_COUPON(0, "WebcastVideoLiveCouponRcmdMessage"), 128 | GOODS_ORDER(0, "WebcastVideoLiveGoodsOrderMessage"), 129 | FANS_CLUB_REVIEW(0, "WebcastFansclubReviewMessage"), 130 | ROOM_VERIFY(0, "WebcastRoomVerifyMessage"), 131 | MEDIA_REPLAY(0, "WebcastMediaLiveReplayVidMessage"), 132 | COMMERCE_SALE_MESSAGE(0, "CommerceSaleMessage"), 133 | FOLLOW_GUIDE(0, "WebcastFollowGuideMessage"), 134 | HONOR_LEVEL_UP(0, "WebcastLevelUpMessage"), 135 | GUIDE_MESSAGE(0, "WebcastGuideMessage"), 136 | FREE_CELL_GIFT_MESSAGE(0, "WebcastFreeCellGiftMessage"), 137 | DUTY_GIFT_MESSAGE(0, "WebcastDutyGiftMessage"), 138 | ASSET_MESSAGE(0, "WebcastAssetMessage"), 139 | DAILY_REGION_RANK(0, "WebcastSunDailyRegionRankMessage"), 140 | IM_DELETE(0, "WebcastImDeleteMessage"), 141 | DOU_PLUS_MESSAGE(0, "WebcastDouplusMessage"), 142 | LIVE_ECOM_MESSAGE(0, "WebcastLiveEcomMessage"), 143 | FRATERNITY_MESSAGE(0, "WebcastBrotherhoodMessage"), 144 | DOUYIN_OFFICIAL_TASK_INFO(0, "WebcastProjectDTaskInfo"), 145 | CEREMONY_MESSAGE(0, "WebcastCeremonyMessage"), 146 | D_H5_MESSAGE(0, "WebcastProjectDModifyH5"), 147 | GAME_GIFT_MESSAGE(0, "WebcastGameGiftMessage"), 148 | DRIVE_GIFT_MESSAGE(0, "WebcastDriveGiftMessage"), 149 | PORTAL_MESSAGE(0, "WebcastPortalMessage"), 150 | NOBLE_UPGRADE_MESSAGE(0, "WebcastNobleUpgradeMessage"), 151 | NOBLE_TOAST_MESSAGE(0, "WebcastNobleToastMessage"), 152 | NOBLE_ENTER_LEAVE_MESSAGE(0, "WebcastNobleEnterLeaveMessage"), 153 | BINDING_GIFT_MESSAGE(0, "WebcastBindingGiftMessage"), 154 | BANNER_UPDATE(0, "WebcastInRoomBannerEvent"), 155 | QUIZ_START_MESSAGE(0, "WebcastQuizStartMessage"), 156 | QUIZ_CHANGE_MESSAGE(0, "WebcastQuizChangeMessage"), 157 | QUIZ_RESULT_MESSAGE(0, "WebcastQuizResultMessage"), 158 | BEGINNER_GUIDE_MESSAGE(0, "WebcastBeginnerGuideMessage"), 159 | GIFT_VOTE_MESSAGE(0, "WebcastGiftVoteMessage"), 160 | CHIJI_NOTICE_MESSAGE(0, "WebcastChijiNoticeMessage"), 161 | POPULAR_CARD_MESSAGE(0, "WebcastPopularCardMessage"), 162 | OFFICIAL_ROOM_MESSAGE(0, "WebcastOfficialRoomMessage"), 163 | IN_ROOM_BANNER_REFRESH_MESSAGE(0, "WebcastInRoomBannerRefreshMessage"), 164 | ROOM_AUTH_MESSAGE(0, "WebcastRoomAuthMessage"), 165 | COMMON_POPUP_MESSAGE(0, "WebcastCommonPopupMessage"), 166 | UPDATE_KOI_ROOM_STATUS_MESSAGE(0, "WebcastUpdateKoiRoomStatusMessage"), 167 | CAR_SHOW_MESSAGE(0, "WebcastCarBallShowMessage"), 168 | CAR_SERIES_INFO_MESSAGE(0, "WebcastCarSeriesInfoMessage"), 169 | CNY_A_TASK_MESSAGE(0, "WebcastCNYATaskMessage"), 170 | CNY_REWARD_MESSAGE(0, "WebcastCNYReward"), 171 | AUTH_NOTIFY_MESSAGE(0, "WebcastAuthorizationNotifyMessage"), 172 | ROOM_CHALLENGE_MESSAGE(0, "WebcastRoomChallengeMessage"), 173 | VIP_SEAT_MESSAGE(0, "WebcastVIPSeatMessage"), 174 | VIP_INFO_MESSAGE(0, "WebcastVIPInfoMessage"), 175 | DOU_PLUS_INDICATOR_MESSAGE(0, "WebcastDouplusIndicatorMessage"), 176 | VERIFY_CODE_MESSAGE(0, "WebcastVerificationCodeMessage"), 177 | ROOM_HOTSPOT_MESSAGE(0, "WebcastRoomHotSentenceMessage"), 178 | NABOB_IM_MESSAGE(0, "WebcastNabobImNoticeMessage"), 179 | TOOLBAR_ITEM_MESSAGE(0, "WebcastToolbarItemMessage"), 180 | STAMP_MESSAGE(0, "WebcastStampMessage"), 181 | COVER_MESSAGE(0, "WebcastUploadCoverMessage"), 182 | LIKE_MESSAGE(0, "WebcastLikeMessage"), 183 | COMMERCE_MESSAGE(0, "WebcastCommerceMessage"), 184 | OFFICIAL_CHANNEL_FOR_USER_MESSAGE(0, "WebcastOChannelUserMessage"), 185 | OFFICIAL_CHANNEL_FOR_ANCHOR_MESSAGE(0, "WebcastOChannelAnchorMessage"); 186 | 187 | private final String messageType; 188 | private final int i; 189 | 190 | MessageEnum(int i, String messageType) { 191 | this.i = i; 192 | this.messageType = messageType; 193 | } 194 | 195 | public String getIntType() { 196 | return messageType; 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/hookers/MessageHook.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.hookers 2 | 3 | import android.util.Log 4 | import com.ehook.HookGlobal 5 | import com.ehook.core.EHook 6 | import com.ehook.core.HookCenter 7 | import com.evan.dy.interfaces.IMessageHook 8 | import com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.presenter.Classes 9 | import com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.presenter.Methods 10 | import com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.viewmodule.Classes.ClassWidget 11 | import com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.viewmodule.Methods.ClassCommentWidget_onPause 12 | import com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.viewmodule.Methods.ClassCommentWidget_onResume 13 | 14 | object MessageHook : HookCenter() { 15 | override val interfaces: List> 16 | get() = listOf(IMessageHook::class.java) 17 | 18 | override fun provideEventHooker(event: String): EHook? { 19 | return when (event) { 20 | "onWidgetResume" -> { 21 | iMethodNotifyHooker( 22 | clazz = ClassWidget, 23 | method = ClassCommentWidget_onResume, 24 | iClazz = IMessageHook::class.java, 25 | iMethodAfter = event, 26 | needObject = true 27 | ) 28 | } 29 | "onWidgetPause" -> { 30 | iMethodNotifyHooker( 31 | clazz = ClassWidget, 32 | method = ClassCommentWidget_onPause, 33 | iClazz = IMessageHook::class.java, 34 | iMethodAfter =event, 35 | needObject = true 36 | ) 37 | } 38 | "onMessage" -> { 39 | iMethodNotifyHooker( 40 | clazz = Classes.ClassChatroomPresenterAz, 41 | method = Methods.onMessage, 42 | iClazz = IMessageHook::class.java, 43 | iMethodAfter = event, 44 | parameterTypes = *arrayOf(com.evan.dy.mirror.com.ss.ugc.live.sdk.message.data.Classes.ClassLiveSdkMessageDataIMessage) 45 | ) 46 | } 47 | else -> { 48 | when { 49 | HookGlobal.unitTestMode -> throw IllegalArgumentException("Unknown event: $event") 50 | else -> { 51 | Log.e(MessageHook::class.java.name, "function not found: $event") 52 | return null 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/interfaces/IApplicationHook.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.interfaces 2 | 3 | import android.content.Context 4 | 5 | interface IApplicationHook { 6 | fun attachBaseContext(context: Context) {} 7 | fun onCreate() {} 8 | } 9 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/interfaces/IMessageHook.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.interfaces 2 | 3 | interface IMessageHook { 4 | 5 | fun onWidgetResume(widget: Any?) 6 | 7 | fun onWidgetPause(widget: Any?) 8 | 9 | fun onMessage(message: Any?) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/mirror/PackageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.mirror 2 | 3 | 4 | object PackageUtils { 5 | @JvmStatic 6 | fun getPath(pathClass: Class<*>): String { 7 | return "${pathClass.name.replace( 8 | "com.evan.dy.mirror.", 9 | "").removeSuffix(".${pathClass.simpleName}")}" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/mirror/com/bytedance/android/livesdk/chatroom/presenter/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.presenter 2 | 3 | import com.ehook.HookGlobal.classLoader 4 | import com.ehook.HookGlobal.lazy 5 | import com.ehook.helper.ReflecterHelper.findClassIfExists 6 | import com.evan.dy.mirror.PackageUtils 7 | 8 | object Classes { 9 | private val packageName = PackageUtils.getPath(javaClass) 10 | 11 | val ClassChatroomPresenterAz: Class<*> by lazy("${javaClass.name}.ClassChatroomPresenterAz") { 12 | findClassIfExists("$packageName.az", classLoader!!) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/mirror/com/bytedance/android/livesdk/chatroom/presenter/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.presenter 2 | 3 | import com.ehook.HookGlobal.lazy 4 | import com.ehook.helper.ReflecterHelper 5 | import com.evan.dy.mirror.com.ss.ugc.live.sdk.message.data.Classes.ClassLiveSdkMessageDataIMessage 6 | import java.lang.reflect.Method 7 | 8 | object Methods { 9 | 10 | val onMessage: Method by lazy("${javaClass.name}.onMessage") { 11 | ReflecterHelper.findMethodIfExists( 12 | Classes.ClassChatroomPresenterAz, 13 | "onMessage", 14 | ClassLiveSdkMessageDataIMessage 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/mirror/com/bytedance/android/livesdk/chatroom/viewmodule/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.viewmodule 2 | 3 | import com.ehook.HookGlobal.classLoader 4 | import com.ehook.HookGlobal.lazy 5 | import com.ehook.helper.ReflecterHelper.findClassIfExists 6 | import com.evan.dy.mirror.PackageUtils 7 | 8 | object Classes { 9 | private val packageName = PackageUtils.getPath(javaClass) 10 | val ClassWidget: Class<*> by lazy("${javaClass.name}.ClassWidget") { 11 | findClassIfExists("$packageName.CommentWidget", classLoader!!) 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/mirror/com/bytedance/android/livesdk/chatroom/viewmodule/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.mirror.com.bytedance.android.livesdk.chatroom.viewmodule 2 | 3 | import com.ehook.HookGlobal.lazy 4 | import com.ehook.core.Clazz 5 | import com.ehook.helper.ReflecterHelper 6 | import java.lang.reflect.Method 7 | 8 | object Methods { 9 | val ClassCommentWidget_a: Method by lazy("${javaClass.name}.ClassCommentWidget_a") { 10 | ReflecterHelper.findMethodsByExactParameters( 11 | Classes.ClassWidget, null, 12 | Clazz.String, Clazz.Boolean, Clazz.Boolean 13 | ).firstOrNull() 14 | } 15 | val ClassCommentWidget_onPause: Method by lazy("${javaClass.name}.ClassCommentWidget_onPause") { 16 | ReflecterHelper.findMethodIfExists(Classes.ClassWidget, "onPause") 17 | } 18 | val ClassCommentWidget_onResume: Method by lazy("${javaClass.name}.ClassCommentWidget_onResume") { 19 | ReflecterHelper.findMethodIfExists(Classes.ClassWidget, "onResume") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/mirror/com/ss/ugc/live/sdk/message/data/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.mirror.com.ss.ugc.live.sdk.message.data 2 | 3 | import com.ehook.HookGlobal.classLoader 4 | import com.ehook.HookGlobal.lazy 5 | import com.ehook.helper.ReflecterHelper.findClassIfExists 6 | import com.evan.dy.mirror.PackageUtils 7 | 8 | object Classes { 9 | private val packageName = PackageUtils.getPath(javaClass) 10 | val ClassLiveSdkMessageDataIMessage: Class<*> by lazy("${javaClass.name}.ClassLiveSdkMessageDataIMessage") { 11 | findClassIfExists("$packageName.IMessage", classLoader!!) 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/plugins/ActivityPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.plugins 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import com.ehook.plugins.interfaces.IActivityHook 6 | import com.ehook.utils.LogUtil 7 | import com.evan.dy.api.ChatRoomApi 8 | import kotlin.jvm.internal.Intrinsics 9 | 10 | 11 | object ActivityPlugin : IActivityHook { 12 | 13 | /* ------------------ IActivityHook ----------------- */ 14 | override fun onCreate(activity: Activity, savedInstanceState: Bundle?) { 15 | LogUtil.e( 16 | ActivityPlugin.javaClass.simpleName, 17 | "onCreate ${activity.javaClass.simpleName}" 18 | ) 19 | if (activity.javaClass.simpleName.contains("ExternalWechatUserMessageListActivity")) { 20 | // Debug.hook() 21 | } 22 | } 23 | 24 | override fun onResume(activity: Activity) { 25 | // LogUtil.e( 26 | // ActivityPlugin.javaClass.simpleName, 27 | // "onResume ${activity.javaClass.simpleName}" 28 | // ) 29 | // ChatRoomApi.setCommentWidget(activity) 30 | 31 | } 32 | 33 | override fun onPause(activity: Activity) { 34 | // LogUtil.e( 35 | // ActivityPlugin.javaClass.simpleName, 36 | // "onPause ${activity.javaClass.simpleName}" 37 | // ) 38 | // if (Intrinsics.areEqual(ChatRoomApi.getCommentWidget(), activity)) { 39 | // ChatRoomApi.setCommentWidget(null) 40 | // } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/plugins/MessageHookPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.plugins 2 | 3 | import com.ehook.utils.LogUtil 4 | import com.evan.dy.api.ChatRoomApi 5 | import com.evan.dy.api.model.LiveRoomMessage 6 | import com.evan.dy.interfaces.IMessageHook 7 | import kotlin.jvm.internal.Intrinsics 8 | 9 | 10 | object MessageHookPlugin : IMessageHook { 11 | override fun onWidgetResume(widget: Any?) { 12 | LogUtil.e(MessageHookPlugin::class.java.simpleName, "onWidgetResume") 13 | ChatRoomApi.setCommentWidget(widget) 14 | } 15 | 16 | override fun onWidgetPause(widget: Any?) { 17 | LogUtil.e(MessageHookPlugin::class.java.simpleName, "onWidgetPause") 18 | if (Intrinsics.areEqual(ChatRoomApi.getCommentWidget(), widget)) { 19 | ChatRoomApi.setCommentWidget(null) 20 | } 21 | } 22 | 23 | override fun onMessage(message: Any?) { 24 | if (message != null) { 25 | val liveRoomMessage = LiveRoomMessage(message) 26 | ChatRoomApi.handleMessage(liveRoomMessage) 27 | } else { 28 | LogUtil.e(MessageHookPlugin::class.java.simpleName, "message null") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/plugins/WwEngine.kt: -------------------------------------------------------------------------------- 1 | package com.evan.dy.plugins 2 | 3 | import com.ehook.core.HookCenter 4 | import com.evan.dy.hookers.MessageHook 5 | 6 | 7 | object WwEngine { 8 | 9 | var hookCenters: List = listOf( 10 | MessageHook 11 | ) 12 | var plugins = listOf( 13 | MessageHookPlugin, 14 | ActivityPlugin 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /dy/src/main/java/com/evan/dy/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.evan.dy.utils; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.JsonIOException; 8 | import com.google.gson.JsonSyntaxException; 9 | import com.google.gson.annotations.SerializedName; 10 | import com.google.gson.stream.JsonReader; 11 | 12 | import org.json.JSONObject; 13 | 14 | import java.io.Reader; 15 | import java.lang.annotation.Annotation; 16 | import java.lang.reflect.Field; 17 | import java.lang.reflect.Type; 18 | 19 | public class JsonUtils { 20 | private static Gson create() { 21 | 22 | return JsonUtils.GSonHolder.gson; 23 | } 24 | 25 | 26 | 27 | private static class GSonHolder { 28 | 29 | private static Gson gson = new Gson(); 30 | } 31 | 32 | public static T fromJson(String json, Class type) throws JsonIOException, JsonSyntaxException { 33 | return create().fromJson(json, type); 34 | } 35 | 36 | public static T fromJsonObject(JSONObject jsonObject, Class type) throws JsonIOException, JsonSyntaxException { 37 | return create().fromJson(jsonObject.toString(), type); 38 | } 39 | 40 | public static T fromJson(String json, Type type) { 41 | return create().fromJson(json, type); 42 | } 43 | 44 | public static T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { 45 | return create().fromJson(reader, typeOfT); 46 | } 47 | 48 | public static T fromJson(Reader json, Class classOfT) throws JsonSyntaxException, JsonIOException { 49 | return create().fromJson(json, classOfT); 50 | } 51 | 52 | public static T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException { 53 | return create().fromJson(json, typeOfT); 54 | } 55 | 56 | 57 | public static String toJson(Object src) { 58 | return create().toJson(src); 59 | } 60 | 61 | public static String toJson(Object src, Type typeOfSrc) { 62 | return create().toJson(src, typeOfSrc); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /dy/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | wework 3 | 4 | -------------------------------------------------------------------------------- /ehook/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | gen -------------------------------------------------------------------------------- /ehook/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | android { 5 | 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles 'consumer-rules.pro' 18 | 19 | externalNativeBuild { 20 | cmake { 21 | version "3.10.2" 22 | cppFlags "-frtti -fexceptions" 23 | } 24 | } 25 | ndk { 26 | moduleName "kernel" 27 | abiFilters "armeabi-v7a", "x86" 28 | cFlags "-DANDROID_NDK" 29 | } 30 | 31 | //Gradle 构建并打包某个特定abi体系架构下的.so库 32 | sourceSets { 33 | main() { 34 | jniLibs.srcDirs=['src/main/jniLibs'] 35 | } 36 | } 37 | } 38 | 39 | packagingOptions { 40 | pickFirst 'lib/armeabi-v7a/libsilk.so' 41 | pickFirst 'lib/arm64/libsilk.so' 42 | pickFirst 'lib/arm64-v8a/libsilk.so' 43 | pickFirst 'lib/x86/libsilk.so' 44 | } 45 | 46 | externalNativeBuild { 47 | cmake { 48 | // path "src/main/cpp/CMakeLists.txt" 49 | } 50 | } 51 | 52 | } 53 | 54 | dependencies { 55 | implementation fileTree(dir: 'libs', include: ['*.jar']) 56 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 57 | implementation 'androidx.appcompat:appcompat:1.1.0' 58 | implementation 'androidx.core:core-ktx:1.1.0' 59 | testImplementation 'junit:junit:4.12' 60 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 62 | implementation "com.squareup.okhttp3:okhttp:$okhttp" 63 | implementation 'com.google.code.gson:gson:2.8.6' 64 | compileOnly 'de.robv.android.xposed:api:82' 65 | compileOnly 'de.robv.android.xposed:api:82:sources' 66 | } 67 | 68 | this.afterEvaluate { 69 | this.copy { 70 | from 'src/main/jniLibs/armeabi-v7a' 71 | into 'src/main/jniLibs/arm64-v8a' 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /ehook/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/ehook/consumer-rules.pro -------------------------------------------------------------------------------- /ehook/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/ehook/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ehook/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 10 15:36:09 CST 2020 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.1.1-all.zip 7 | -------------------------------------------------------------------------------- /ehook/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 | -------------------------------------------------------------------------------- /ehook/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 | -------------------------------------------------------------------------------- /ehook/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 | -------------------------------------------------------------------------------- /ehook/src/androidTest/java/com/easy/hooker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ehook 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.magic.kernel.test", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ehook/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/EasyHook.kt: -------------------------------------------------------------------------------- 1 | package com.ehook 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.util.Log 6 | import com.ehook.utils.LogUtil 7 | import com.ehook.core.HookCenter 8 | import com.ehook.core.IHookProvider 9 | import com.ehook.core.Version 10 | import com.ehook.utils.ParallelUtil.parallelForEach 11 | import com.ehook.utils.XposedUtil 12 | import de.robv.android.xposed.XposedBridge 13 | import de.robv.android.xposed.XposedHelpers 14 | import de.robv.android.xposed.callbacks.XC_LoadPackage 15 | import java.io.File 16 | 17 | object EasyHook { 18 | fun isImportantDyProcess(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 19 | val processName = lpparam.processName 20 | when { 21 | processName.contains(':') -> { 22 | return false 23 | } 24 | } 25 | // 检查微信依赖的JNI库是否存在, 以此判断当前应用是不是微信/企业微信 26 | val features = listOf ( 27 | "libv8.cr.so", 28 | "libavmdl.so", 29 | "libcutsame.so", 30 | "libAkeva.so" 31 | ) 32 | return try { 33 | val libraryDir = File(lpparam.appInfo.nativeLibraryDir) 34 | features.filter { filename -> 35 | File(libraryDir, filename).exists() 36 | }.size >= 2 37 | } catch (t: Throwable) { false } 38 | } 39 | fun isImportantWechatProcess(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 40 | val processName = lpparam.processName 41 | when { 42 | !processName.contains(':') -> { 43 | } 44 | processName.endsWith(":tools") -> { 45 | } 46 | else -> return false 47 | } 48 | // 检查微信依赖的JNI库是否存在, 以此判断当前应用是不是微信/企业微信 49 | val features = listOf ( 50 | "libwechatcommon.so", 51 | "libwechatmm.so", 52 | "libwechatnetwork.so", 53 | "libwechatsight.so", 54 | "libwechatxlog.so" 55 | ) 56 | return try { 57 | val libraryDir = File(lpparam.appInfo.nativeLibraryDir) 58 | features.filter { filename -> 59 | File(libraryDir, filename).exists() 60 | }.size >= 2 61 | } catch (t: Throwable) { false } 62 | } 63 | 64 | fun getSystemContext(): Context { 65 | val activityThreadClass = XposedHelpers.findClass("android.app.ActivityThread", null) 66 | val activityThread = XposedHelpers.callStaticMethod(activityThreadClass, "currentActivityThread") 67 | val context = XposedHelpers.callMethod(activityThread, "getSystemContext") as Context? 68 | return context ?: throw Error("Failed to get system context.") 69 | } 70 | 71 | // 查找apk路径 72 | fun getApplicationApkPath(context: Context, packageName: String): String { 73 | val pm = context.packageManager 74 | val apkPath = pm.getApplicationInfo(packageName, 0)?.publicSourceDir 75 | return apkPath ?: throw Error("Failed to get the APK path of $packageName") 76 | } 77 | 78 | fun getApplicationApkPath(packageName: String): String { 79 | val pm = EasyHook.getSystemContext().packageManager 80 | val apkPath = pm.getApplicationInfo(packageName, 0).publicSourceDir 81 | return apkPath ?: throw Error("Failed to get the APK path of $packageName") 82 | } 83 | 84 | fun getApplicationLibsPath(packageName: String): String = 85 | "${getApplicationApkPath(packageName).removeSuffix("base.apk")}lib/${Build.SUPPORTED_ABIS.first().split("-").first()}" 86 | 87 | fun getApplicationVersion(packageName: String): Version { 88 | val pm = getSystemContext().packageManager 89 | val versionName = pm.getPackageInfo(packageName, 0).versionName 90 | return Version( 91 | versionName 92 | ?: throw Error("Failed to get the version of $packageName") 93 | ) 94 | } 95 | 96 | fun startup(lpparam: XC_LoadPackage.LoadPackageParam, plugins: List?, centers: List) { 97 | XposedBridge.log("Wechat EHooker: ${plugins?.size ?: 0} plugins.") 98 | HookGlobal.init(lpparam) { 99 | when (it) { 100 | true -> { 101 | registerPlugins(plugins, centers) 102 | registerHookers(plugins) 103 | // XposedHelpers.setObjectField( 104 | // EasyHook::class.java.classLoader?.parent, 105 | // "parent", 106 | // lpparam.classLoader 107 | // ) 108 | } 109 | else -> 110 | LogUtil.v("查找初始化失败") 111 | } 112 | } 113 | } 114 | 115 | private fun registerPlugins(plugins: List?, centers: List) { 116 | val observers = plugins?.filter { it !is IHookProvider } ?: listOf() 117 | centers.parallelForEach { center -> 118 | center.interfaces.forEach { `interface` -> 119 | observers.forEach { plugin -> 120 | val assignable = `interface`.isAssignableFrom(plugin::class.java) 121 | if (assignable) { 122 | center.register(`interface`, plugin) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * 检查插件中是否存在自定义的事件, 将它们直接注册到 Xposed 框架上 131 | */ 132 | private fun registerHookers(plugins: List?) { 133 | val providers = plugins?.filter { it is IHookProvider } ?: listOf() 134 | providers.parallelForEach { 135 | (it as IHookProvider).provideStaticHookers()?.forEach { hooker -> 136 | if (!hooker.hasHooked) { 137 | XposedUtil.postHooker(hooker) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/HookGlobal.kt: -------------------------------------------------------------------------------- 1 | package com.ehook 2 | 3 | import android.util.Log 4 | import com.ehook.core.WaitChannel 5 | import com.ehook.helper.ParserHelper.ApkFile 6 | import com.ehook.helper.ParserHelper.ClassTrie 7 | import com.ehook.helper.TryHelper 8 | import de.robv.android.xposed.callbacks.XC_LoadPackage 9 | 10 | object HookGlobal { 11 | 12 | @Suppress("MemberVisibilityCanBePrivate") 13 | const val INIT_TIMEOUT = 2000L // ms 14 | 15 | @Volatile 16 | var unitTestMode: Boolean = false 17 | 18 | private val initChannel = WaitChannel() 19 | 20 | @Volatile 21 | var version: com.ehook.core.Version? = null 22 | get() { 23 | if (!unitTestMode) { 24 | initChannel.wait(INIT_TIMEOUT) 25 | initChannel.done() 26 | } 27 | return field 28 | } 29 | 30 | @Volatile 31 | var packageName: String = "" 32 | get() { 33 | if (!unitTestMode) { 34 | initChannel.wait(INIT_TIMEOUT) 35 | initChannel.done() 36 | } 37 | return field 38 | } 39 | 40 | @Volatile 41 | var classLoader: ClassLoader? = null 42 | get() { 43 | if (!unitTestMode) { 44 | initChannel.wait(INIT_TIMEOUT) 45 | initChannel.done() 46 | } 47 | return field 48 | } 49 | 50 | @Volatile 51 | var classes: ClassTrie? = null 52 | get() { 53 | if (!unitTestMode) { 54 | initChannel.wait(INIT_TIMEOUT) 55 | initChannel.done() 56 | } 57 | return field 58 | } 59 | 60 | inline fun lazy(name: String, crossinline initializer: () -> T?): Lazy { 61 | return if (unitTestMode) { 62 | UnitTestLazyImpl { 63 | initializer() ?: throw Error("Failed to evaluate $name") 64 | } 65 | } else { 66 | lazy(LazyThreadSafetyMode.PUBLICATION) { 67 | when (null) { 68 | version -> throw Error("Invalid version") 69 | packageName -> throw Error("Invalid packageName") 70 | classLoader -> throw Error("Invalid classLoader") 71 | classes -> throw Error("Invalid classes") 72 | } 73 | initializer() ?: throw Error("Failed to evaluate $name") 74 | } 75 | } 76 | } 77 | 78 | class UnitTestLazyImpl(private val initializer: () -> T) : Lazy, 79 | java.io.Serializable { 80 | @Volatile 81 | private var lazyValue: Lazy = lazy(initializer) 82 | 83 | fun refresh() { 84 | lazyValue = lazy(initializer) 85 | } 86 | 87 | override val value: T 88 | get() = lazyValue.value 89 | 90 | override fun toString(): String = lazyValue.toString() 91 | 92 | override fun isInitialized(): Boolean = lazyValue.isInitialized() 93 | } 94 | 95 | @JvmStatic 96 | fun init(lpparam: XC_LoadPackage.LoadPackageParam, callback: (Boolean) -> Unit) { 97 | TryHelper.tryAsynchronously { 98 | if (initChannel.isDone()) { 99 | return@tryAsynchronously 100 | } 101 | 102 | try { 103 | version = EasyHook.getApplicationVersion(lpparam.packageName) 104 | packageName = lpparam.packageName 105 | classLoader = lpparam.classLoader 106 | 107 | Log.e( 108 | HookGlobal::class.java.name, 109 | "init ${lpparam.appInfo.sourceDir} ${lpparam.appInfo.publicSourceDir} \n${version} ${packageName} ${classLoader}" 110 | ) 111 | ApkFile(lpparam.appInfo.sourceDir).use { 112 | classes = it.classTypes 113 | callback(true) 114 | } 115 | } finally { 116 | initChannel.done() 117 | } 118 | } 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/async/AsyncHandler.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.async 2 | 3 | import android.os.Handler 4 | import android.os.HandlerThread 5 | import android.os.Looper 6 | import android.os.Message 7 | 8 | /** 9 | * 用于异步处理消息 10 | */ 11 | open class AsyncHandler : Handler() { 12 | private val mWorkThreadHandler: Handler 13 | private var mWorkLooper: Looper? = null 14 | 15 | companion object { 16 | const val TAG = "AsyncHandler" 17 | } 18 | 19 | /** 20 | * 内部工作Handler,用于处理异步消息 21 | */ 22 | private inner class WorkHandler(looper: Looper, private val replyHandler: Handler) : 23 | Handler(looper) { 24 | override fun handleMessage(msg: Message) { 25 | super.handleMessage(msg) 26 | // 处理异步消息 27 | onAsyncDeal(msg) 28 | // 处理完成后重新将数据发送到之前的线程处理 29 | val reply = Message.obtain() 30 | reply.copyFrom(msg) 31 | reply.target = replyHandler 32 | replyHandler.post { onPostComplete(reply) } 33 | } 34 | 35 | } 36 | 37 | private fun createHandler(looper: Looper): Handler { 38 | return WorkHandler(looper, this) 39 | } 40 | 41 | /** 42 | * 移除还未开始的操作 43 | * @param what msg.what 44 | */ 45 | fun cancelOperation(what: Int) { 46 | mWorkThreadHandler.removeMessages(what) 47 | } 48 | 49 | /** 50 | * 移除还未开始的操作 51 | * @param what msg.what 52 | * @param obj msg.obj 53 | */ 54 | fun cancelOperation(what: Int, obj: Any?) { 55 | mWorkThreadHandler.removeMessages(what, obj) 56 | } 57 | 58 | /** 59 | * 发送消息 60 | * @param msg 61 | */ 62 | fun sendAsyncMessage(msg: Message) { 63 | sendAsyncMessageDelay(msg, 0) 64 | } 65 | 66 | /** 67 | * 延时发送异步消息 68 | * @param msg 消息内容 69 | * @param delayMillis 多少时长后发送 70 | */ 71 | fun sendAsyncMessageDelay(msg: Message, delayMillis: Long) { 72 | val workMsg = Message.obtain() 73 | workMsg.copyFrom(msg) 74 | mWorkThreadHandler.sendMessageDelayed(workMsg, delayMillis) 75 | } 76 | 77 | /** 78 | * 延时循环发送异步消息 79 | * @param asyncMSg 消息内容等 80 | * @param delayMillis 多少时长后发送 81 | * @param intervalMillis 间隔多少时长 82 | */ 83 | fun sendScheduleAsyncMessage(msg: Message, delayMillis: Long, intervalMillis: Long) { 84 | mWorkThreadHandler.postDelayed({ 85 | sendAsyncMessage(msg) 86 | mWorkThreadHandler.post { 87 | sendScheduleAsyncMessage( 88 | msg, 89 | delayMillis, 90 | intervalMillis 91 | ) 92 | } 93 | }, delayMillis) 94 | } 95 | 96 | /** 97 | * 异步处理消息,该消息中所有参数将原本返回至onPostComplete中,也可以在某些处理完成后重赋值该asyncMsg, 98 | * 如果需要监听处理完成的方法,请重写onPostComplete 99 | * @param msg 100 | */ 101 | protected open fun onAsyncDeal(msg: Message) {} 102 | 103 | /** 104 | * 异步处理完成后执行方法 105 | * @param msg 106 | */ 107 | protected open fun onPostComplete(msg: Message) {} 108 | 109 | override fun handleMessage(msg: Message) { 110 | super.handleMessage(msg) 111 | // onAsyncComplete(msg); 112 | } 113 | 114 | init { 115 | synchronized(AsyncHandler::class.java) { 116 | val thread = HandlerThread(AsyncHandler.Companion.TAG) 117 | thread.start() 118 | mWorkLooper = thread.looper 119 | } 120 | mWorkThreadHandler = createHandler(mWorkLooper!!) 121 | } 122 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/async/CrashHandler.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.async 2 | 3 | import com.ehook.helper.defaultFormat 4 | import java.io.File 5 | import java.io.FileOutputStream 6 | import java.io.IOException 7 | import java.util.Date 8 | import java.util.concurrent.Executors 9 | 10 | class CrashHandler: Thread.UncaughtExceptionHandler { 11 | 12 | interface Callback { 13 | fun handException(e: Throwable) 14 | } 15 | 16 | companion object { 17 | 18 | private var instance: CrashHandler? = null 19 | 20 | fun newInstance(): CrashHandler { 21 | if (CrashHandler.Companion.instance == null) { 22 | CrashHandler.Companion.instance = 23 | CrashHandler() 24 | } 25 | return CrashHandler.Companion.instance!! 26 | } 27 | } 28 | 29 | var callback: CrashHandler.Callback? = null 30 | 31 | init { 32 | Thread.setDefaultUncaughtExceptionHandler(this) 33 | } 34 | 35 | override fun uncaughtException(t: Thread, e: Throwable) { 36 | var tmpThrowable: Throwable? = null 37 | if (e.message == null) { 38 | val builder = StringBuilder(256) 39 | builder.append("\n") 40 | for (element in e.getStackTrace()) { 41 | builder.append(element.toString()).append("\n") 42 | } 43 | tmpThrowable = Throwable(builder.toString(), e) 44 | } 45 | if (handleException(tmpThrowable ?: e)) callback?.handException(tmpThrowable ?: e) 46 | } 47 | 48 | private fun handleException(e: Throwable?): Boolean { 49 | return if (e == null) false else try { 50 | Executors.newSingleThreadExecutor().submit { 51 | try { 52 | val file = File(com.ehook.cache.LRUCache.cachePath("crash", "crash_" + Date().defaultFormat() + ".log")) 53 | file.createNewFile() 54 | val fos = FileOutputStream(file) 55 | fos.write(e.message?.toByteArray(Charsets.UTF_8) ?: byteArrayOf()) 56 | fos.close() 57 | } catch (e: IOException) { 58 | } 59 | } 60 | true 61 | } catch (e: Exception) { 62 | false 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/cache/ByteArrayEntry.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.cache 2 | 3 | import java.lang.ref.ReferenceQueue 4 | import java.lang.ref.SoftReference 5 | 6 | class ByteArrayEntry( 7 | val key: String, 8 | value: ByteArray, 9 | queue: ReferenceQueue, 10 | val timestamp: Long = System.currentTimeMillis()): SoftReference(value, queue) { 11 | var size: Long = 0 12 | 13 | init { 14 | size = value.size.toLong() 15 | } 16 | 17 | override fun equals(other: Any?): Boolean { 18 | return super.equals(other) 19 | } 20 | 21 | override fun hashCode(): Int { 22 | return super.hashCode() 23 | } 24 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/cache/LRUCache.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.cache 2 | 3 | import android.os.Environment 4 | import com.ehook.helper.MD5 5 | import java.io.File 6 | 7 | object LRUCache { 8 | 9 | private const val ROOT_CACHE_DIR = "ehook" 10 | private var memoryCache = LRUMemoryCache() 11 | private var diskCache = LRUDiskCache(getCacheDir()) 12 | 13 | fun setup(memoryCacheSize: Long = 0, diskCacheSize: Long = 0) { 14 | if (memoryCacheSize != 0L) { 15 | memoryCache = LRUMemoryCache() 16 | } 17 | diskCache = when (diskCacheSize == 0L) { 18 | true -> LRUDiskCache( 19 | getCacheDir(), 20 | diskCacheSize 21 | ) 22 | false -> LRUDiskCache(getCacheDir()) 23 | } 24 | } 25 | 26 | fun clearCache(callback: (() -> Unit)? = null) { 27 | memoryCache.clearCache() 28 | diskCache.clearCache { callback?.invoke() } 29 | } 30 | 31 | /** ---- 磁盘缓存 ---- */ 32 | fun cacheInDisk(dir: String = "", key: String, params: Map? = null, content: ByteArray): String? = 33 | diskCache.cache( 34 | cachePath( 35 | dir, 36 | key, 37 | params 38 | ), content) 39 | 40 | fun cacheInDisk(dir: String = "", key: String, params: Map? = null, content: ByteArray, callback: (String?) -> Unit) = 41 | diskCache.cache( 42 | cachePath( 43 | dir, 44 | key, 45 | params 46 | ), content, callback) 47 | 48 | fun getFromDisk(dir: String = "", key: String, params: Map? = null): ByteArray? = 49 | diskCache.get( 50 | cachePath( 51 | dir, 52 | key, 53 | params 54 | ) 55 | ) 56 | 57 | fun getFromDisk(dir: String = "", key: String, params: Map?, callback: (ByteArray?) -> Unit) = 58 | diskCache.get( 59 | cachePath( 60 | dir, 61 | key, 62 | params 63 | ), callback) 64 | 65 | fun cacheDiskPath(dir: String = "", key: String, params: Map? = null): String? = 66 | diskCache.exists( 67 | cachePath( 68 | dir, 69 | key, 70 | params 71 | ) 72 | ) 73 | 74 | /** ---- 磁盘缓存 ---- */ 75 | fun cacheInMemory(key: String, params: Map? = null, content: ByteArray) = 76 | memoryCache.cache( 77 | realKey( 78 | key, 79 | params 80 | ), content) 81 | 82 | fun getEntryFromMemory(key: String, params: Map? = null): ByteArrayEntry? = 83 | memoryCache.getEntry( 84 | realKey( 85 | key, 86 | params 87 | ) 88 | ) 89 | 90 | fun getByteArrayFromMemory(key: String, params: Map? = null): ByteArray? = 91 | memoryCache.getByteArray( 92 | realKey( 93 | key, 94 | params 95 | ) 96 | ) 97 | 98 | fun cachePath(dir: String, key: String, params: Map? = null, md5: Boolean = false): String = 99 | getCacheDir(dir) + File.separator + realKey( 100 | key, 101 | params, 102 | md5 103 | ) 104 | 105 | private fun realKey(key: String, params: Map? = null, md5: Boolean = true): String { 106 | val indexOf = key.lastIndexOf(".") 107 | val key = when (indexOf > 0) { 108 | true -> key.substring(0, indexOf) + (if (params != null) params?.toString() else "") + key.substring(indexOf) 109 | false -> key + (if (params != null) params?.toString() else "") 110 | } 111 | return key.MD5() 112 | } 113 | 114 | fun getCacheDir(dir: String = ""): String { 115 | val cacheDir = Environment.getExternalStorageDirectory().path + File.separator + ROOT_CACHE_DIR 116 | return when (dir.isNotEmpty()) { 117 | true -> "$cacheDir${File.separator}${dir.removeSuffix("/")}" 118 | else -> cacheDir 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/cache/LRUDiskCache.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.cache 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.util.Log 6 | import java.io.* 7 | import java.util.* 8 | import java.util.concurrent.* 9 | import kotlin.collections.LinkedHashMap 10 | 11 | /** 12 | * 磁盘缓存 13 | */ 14 | class LRUDiskCache(val cacheDir: String, val maxCacheSize: Long = 1024 * 1024 * 1024) { 15 | 16 | private var cachedSize: Long = 0 17 | private val executorService: ExecutorService = 18 | Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors()) 19 | private val mHandler = Handler(Looper.getMainLooper()) 20 | 21 | /** 用于按照最近最久未使用存储文件的 path 及 lastModified */ 22 | private val linkedHashMap: LinkedHashMap = 23 | object : LinkedHashMap(16, 0.75f, true) { 24 | override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { 25 | val shouldRemove = cachedSize > maxCacheSize 26 | if (shouldRemove) { 27 | val file = File(eldest?.key ?: "") 28 | deleteFile(file) {} 29 | } 30 | return shouldRemove 31 | } 32 | } 33 | 34 | init { 35 | val file = File(cacheDir) 36 | if (!file.exists()) file.mkdirs() 37 | val futureTask = object : FutureTask>(Callable { 38 | return@Callable readCache(file) 39 | }) { 40 | override fun done() { 41 | get().forEach { linkedHashMap[it.value] = it.key } 42 | } 43 | } 44 | executorService.submit(futureTask) 45 | } 46 | 47 | fun cache(path: String, content: ByteArray): String? = 48 | try { 49 | val file = File(path).also { if (it.parentFile?.exists() == false) it.parentFile?.mkdirs() } 50 | val fileOutputStream = FileOutputStream(file) 51 | BufferedOutputStream(fileOutputStream).use { it.write(content) } 52 | file.path 53 | } catch (e: Throwable) { 54 | Log.e(LRUDiskCache::class.java.name, "cache fail: ${e.message}") 55 | null 56 | } 57 | 58 | fun cache(path: String, content: ByteArray, callback: (String?) -> Unit) { 59 | val futureTask = object : FutureTask(Callable { cache(path, content) }) { 60 | override fun done() { 61 | mHandler.post { callback(get()) } 62 | } 63 | } 64 | executorService.submit(futureTask) 65 | } 66 | 67 | fun exists(path: String): String? = if (File(path).exists()) path else null 68 | 69 | fun get(path: String): ByteArray? = 70 | try { 71 | BufferedInputStream(FileInputStream(File(path))).use { it.readBytes() } 72 | } catch (_: Throwable) { 73 | null 74 | } 75 | 76 | fun get(path: String, callback: (ByteArray?) -> Unit) { 77 | val futureTask = object : FutureTask(Callable { get(path) }) { 78 | override fun done() { 79 | mHandler.post { callback(get()) } 80 | } 81 | } 82 | executorService.submit(futureTask) 83 | } 84 | 85 | fun deleteFile(file: File) { 86 | if (file.isDirectory) { 87 | val files = file.listFiles() ?: arrayOf() 88 | for (subFile in files) { 89 | deleteFile(subFile) 90 | } 91 | } else { 92 | cachedSize -= if (file.exists()) file.length() else 0 93 | linkedHashMap.remove(file.path) 94 | file.deleteOnExit() 95 | } 96 | } 97 | 98 | fun deleteFile(file: File, callback: () -> Unit) { 99 | val futureTask = object : FutureTask(Callable { deleteFile(file) }) { 100 | override fun done() { 101 | mHandler.post { callback() } 102 | } 103 | } 104 | executorService.submit(futureTask) 105 | } 106 | 107 | /** 108 | * 清除指定目录下的缓存,如果file == null,则清除所有缓存 109 | */ 110 | fun clearCache(file: File? = null, callback: () -> Unit) { 111 | val futureTask = object : FutureTask(Callable { 112 | if (file == null) { 113 | cachedSize = 0 114 | File(cacheDir).deleteOnExit() 115 | } else { 116 | deleteFile(file) 117 | } 118 | }) { 119 | override fun done() { 120 | mHandler.post { callback() } 121 | } 122 | } 123 | executorService.submit(futureTask) 124 | } 125 | 126 | /** 127 | * 初始化缓存,将所有文件信息读取,并根据上次lastModified修改排序 128 | */ 129 | private fun readCache(file: File): TreeMap { 130 | val treeMap = TreeMap { o1, o2 -> (o1 - o2).toInt() } 131 | if (file.isDirectory) { 132 | val files = file.listFiles() ?: arrayOf() 133 | for (subFile in files) { 134 | treeMap.putAll(readCache(subFile)) 135 | } 136 | } else { 137 | treeMap[file.lastModified()] = file.path 138 | } 139 | return treeMap 140 | } 141 | 142 | } 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/cache/LRUMemoryCache.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.cache 2 | 3 | import java.lang.ref.ReferenceQueue 4 | 5 | /** 6 | * LRU 内存缓存 7 | */ 8 | class LRUMemoryCache(var maxCacheSize: Long = 256 * 1024 * 1024) { 9 | 10 | private var cachedSize: Long = 0 11 | 12 | private val linkedHashMap: LinkedHashMap 13 | 14 | private val referenceQueue = ReferenceQueue() 15 | 16 | init { 17 | linkedHashMap = object : LinkedHashMap(16, 0.75f, true) { 18 | override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { 19 | val shouldRemove = cachedSize > maxCacheSize 20 | if (shouldRemove) { 21 | clearRecycled() 22 | System.gc() 23 | 24 | cachedSize -= eldest?.value?.size ?: 0 25 | } 26 | return shouldRemove 27 | } 28 | } 29 | } 30 | 31 | fun cache(key: String, value: ByteArray) { 32 | val entry = ByteArrayEntry(key, value, referenceQueue) 33 | cache(key, entry) 34 | } 35 | 36 | fun cache(key: String, entry: ByteArrayEntry) { 37 | cachedSize = entry.size 38 | linkedHashMap[key] = entry 39 | } 40 | 41 | fun getEntry(key: String): ByteArrayEntry? = linkedHashMap[key] 42 | 43 | fun getByteArray(key: String): ByteArray? = getEntry(key)?.get() 44 | 45 | fun clearCache() { 46 | linkedHashMap.clear() 47 | cachedSize = 0 48 | System.gc() 49 | System.runFinalization() 50 | } 51 | 52 | private fun clearRecycled() { 53 | var softReference: ByteArrayEntry? = referenceQueue.poll() as? ByteArrayEntry 54 | while (softReference != null) { 55 | if (softReference.get() == null) { 56 | cachedSize -= softReference.size 57 | linkedHashMap.remove(softReference.key) 58 | } 59 | softReference = referenceQueue.poll() as? ByteArrayEntry 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/Clazz.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | import java.lang.Runnable 4 | 5 | object Clazz { 6 | val Boolean = Boolean::class.java 7 | val File = java.io.File::class.java 8 | val FileInputStream = java.io.FileInputStream::class.java 9 | val FileOutputStream = java.io.FileOutputStream::class.java 10 | val Int = Int::class.java 11 | val Float = Float::class.java 12 | val Short = Short::class.java 13 | val Double = Double::class.java 14 | val Iterator = java.util.Iterator::class.java 15 | val Long = Long::class.java 16 | val Map = Map::class.java 17 | val Object = Object::class.java 18 | val String = String::class.java 19 | val CharSequence = CharSequence::class.java 20 | val Activity = android.app.Activity::class.java 21 | val Bundle = android.os.Bundle::class.java 22 | val Configuration = android.content.res.Configuration::class.java 23 | val ContentValues = android.content.ContentValues::class.java 24 | val Context = android.content.Context::class.java 25 | val ContextMenu = android.view.ContextMenu::class.java 26 | val ContextMenuInfo = android.view.ContextMenu.ContextMenuInfo::class.java 27 | val HeaderViewListAdapter = android.widget.HeaderViewListAdapter::class.java 28 | val Intent = android.content.Intent::class.java 29 | val KeyEvent = android.view.KeyEvent::class.java 30 | val ListAdapter = android.widget.ListAdapter::class.java 31 | val ListView = android.widget.ListView::class.java 32 | val Menu = android.view.Menu::class.java 33 | val Message = android.os.Message::class.java 34 | val MotionEvent = android.view.MotionEvent::class.java 35 | val Notification = android.app.Notification::class.java 36 | val NotificationManager = android.app.NotificationManager::class.java 37 | val View = android.view.View::class.java 38 | val ViewGroup = android.view.ViewGroup::class.java 39 | 40 | val Cursor = android.database.Cursor::class.java 41 | 42 | val ByteArray = ByteArray::class.java 43 | val IntArray = IntArray::class.java 44 | val ShortArray = ShortArray::class.java 45 | var LongArray = kotlin.LongArray::class.java 46 | val ObjectArray = Array::class.java 47 | val StringArray = Array::class.java 48 | val List: Class> = MutableList::class.java 49 | val Runnable = Runnable::class.java 50 | val Clazz = Class::class.java 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/EHook.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | /** 4 | * 将一次 EHook 操作封装成对象, 防止对同样的函数反复下钩, 造成难以调查的BUG 5 | * 这个类是线程安全的, 多个线程同时调用只会有一个线程成功下钩 6 | * @property hook 实际向 Xposed 框架注册钩子的回调函数 7 | * @constructor 将一次 EHook 操作封装成一个 EHook 对象 8 | */ 9 | class EHook(private val eHook: () -> Unit) { 10 | /** 11 | * 用来防止重复 EHook 的标记 12 | */ 13 | var hasHooked = false 14 | private set 15 | 16 | /** 17 | * 尝试执行一次 EHook 操作, 如果已经钩过了就不再重复 18 | */ 19 | @Synchronized fun hook() { 20 | if (!hasHooked) { 21 | eHook() 22 | hasHooked = true 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/HookCenter.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | import android.util.Log 4 | import com.ehook.helper.ReflecterHelper.findMethodsByExactName 5 | import com.ehook.helper.TryHelper.tryVerbosely 6 | import com.ehook.utils.LogUtil 7 | import com.ehook.utils.ParallelUtil.parallelForEach 8 | import com.ehook.utils.XposedUtil 9 | import de.robv.android.xposed.XC_MethodHook 10 | import de.robv.android.xposed.XposedHelpers 11 | import java.lang.reflect.Method 12 | import java.util.concurrent.ConcurrentHashMap 13 | 14 | abstract class HookCenter : IHookProvider { 15 | 16 | abstract val interfaces: List> 17 | 18 | private val observers: MutableMap> = ConcurrentHashMap() 19 | 20 | private fun Any.hasEvent(event: String) = 21 | this::class.java.declaredMethods.any { it.name == event } 22 | 23 | private fun register(event: String, observer: Any) { 24 | if (observer.hasEvent(event)) { 25 | val hooker = provideEventHooker(event) 26 | if (hooker != null && !hooker.hasHooked) { 27 | XposedUtil.postHooker(hooker) 28 | } 29 | val existing = observers[event] ?: emptySet() 30 | observers[event] = existing + observer 31 | } 32 | } 33 | 34 | fun register(iClazz: Class<*>, plugin: Any) { 35 | iClazz.methods.forEach { method -> 36 | register(method.name, plugin) 37 | } 38 | } 39 | 40 | fun findObservers(event: String): Set? = observers[event] 41 | 42 | /** 43 | * 通知所有正在观察某个事件的观察者 44 | * 45 | * @param event 具体发生的事件 46 | * @param action 对观察者进行通知的回调函数 47 | */ 48 | inline fun notify(event: String, action: (Any) -> Unit) { 49 | findObservers(event)?.forEach { 50 | tryVerbosely { action(it) } 51 | } 52 | } 53 | 54 | fun iMethodNotifyHooker( 55 | clazz: Class<*>?, method: Method?, 56 | iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null, 57 | needObject: Boolean = false, needResult: Boolean = false, vararg parameterTypes: Class<*> 58 | ): EHook = 59 | iMethodHooker( 60 | clazz, method?.name, 61 | iClazz, iMethodBefore, iMethodAfter, 62 | needObject, needResult, "notify", *parameterTypes 63 | ) 64 | 65 | fun iMethodNotifyHooker( 66 | clazz: Class<*>?, method: String?, 67 | iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null, 68 | needObject: Boolean = false, needResult: Boolean = false, vararg parameterTypes: Class<*> 69 | ): EHook = 70 | iMethodHooker( 71 | clazz, method, 72 | iClazz, iMethodBefore, iMethodAfter, 73 | needObject, needResult, "notify", *parameterTypes 74 | ) 75 | /** 76 | * 通知所有正在观察某个事件的观察者(并行) 77 | * 78 | * @param event 具体发生的事件 79 | * @param action 对观察者进行通知的回调函数 80 | */ 81 | inline fun notifyParallel(event: String, crossinline action: (Any) -> Unit) { 82 | findObservers(event)?.parallelForEach { 83 | tryVerbosely { action(it) } 84 | } 85 | } 86 | 87 | /** 88 | * 通知所有正在观察某个事件的观察者, 并收集它们的反馈 89 | * 90 | * @param event 具体发生的事件 91 | * @param action 对观察者进行通知的回调函数 92 | */ 93 | inline fun notifyForResults(event: String, action: (Any) -> T?): List { 94 | return findObservers(event)?.mapNotNull { 95 | tryVerbosely { action(it) } 96 | } ?: emptyList() 97 | } 98 | 99 | /** 100 | * 通知所有正在观察某个事件的观察者, 并收集它们的反馈, 以确认是否需要拦截该事件 101 | * 102 | * 如果有任何一个观察者返回了 true, 我们就认定当前事件是一个需要被拦截的事件. 例如当微信写文件的时候, 某个观察者 103 | * 检查过文件路径后返回了 true, 那么框架就会拦截这次写文件操作, 向微信返回一个默认值 104 | * 105 | * @param event 具体发生的事件 106 | * @param param 拦截函数调用后得到的 [XC_MethodHook.MethodHookParam] 对象 107 | * @param default 跳过函数调用之后, 仍然需要向 caller 提供一个返回值 108 | * @param action 对观察者进行通知的回调函数 109 | */ 110 | inline fun notifyForBypassFlags( 111 | event: String, 112 | param: XC_MethodHook.MethodHookParam, 113 | default: Any? = null, 114 | action: (Any) -> Boolean 115 | ) { 116 | val shouldBypass = notifyForResults(event, action).any() 117 | if (shouldBypass) { 118 | param.result = default 119 | } 120 | } 121 | 122 | fun iMethodNotifyForBypassFlagsHooker( 123 | clazz: Class<*>?, method: String?, 124 | iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null, 125 | needObject: Boolean = false, needResult: Boolean = false, vararg parameterTypes: Class<*> 126 | ): EHook = 127 | iMethodHooker( 128 | clazz, method, 129 | iClazz, iMethodBefore, iMethodAfter, 130 | needObject, needResult, "notifyForBypassFlags", *parameterTypes 131 | ) 132 | 133 | 134 | /** 135 | * 通知所有正在观察某个事件的观察者, 并收集它们的反馈, 以确认该对这次事件采取什么操作 136 | * 137 | * 在获取了观察者建议的操作之后, 我们会对这些操作的优先级进行排序, 从优先级最高的操作中选择一个予以采纳 138 | * 139 | * @param event 具体发生的事件 140 | * @param param 拦截函数调用后得到的 [XC_MethodHook.MethodHookParam] 对象 141 | * @param action 对观察者进行通知的回调函数 142 | */ 143 | inline fun notifyForOperations( 144 | event: String, 145 | param: XC_MethodHook.MethodHookParam, 146 | action: (Any) -> Operation<*> 147 | ) { 148 | val operations = notifyForResults(event, action) 149 | val result = operations.filter { it.returnEarly }.maxBy { it.priority } 150 | if (result != null) { 151 | if (result.value != null) { 152 | param.result = result.value 153 | } 154 | if (result.error != null) { 155 | param.throwable = result.error 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * hookMethod 162 | */ 163 | private fun iMethodHooker( 164 | clazz: Class<*>?, method: String?, 165 | iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null, 166 | needObject: Boolean = false, needResult: Boolean = false, notifyType: String = "notify", 167 | vararg parameterTypes: Class<*> 168 | ): EHook { 169 | return EHook { 170 | if (clazz == null || method == null) return@EHook 171 | XposedHelpers.findAndHookMethod(clazz, method, *parameterTypes, 172 | object : XC_MethodHook() { 173 | override fun beforeHookedMethod(param: MethodHookParam?) { 174 | if (iClazz == null || iMethodBefore == null) return 175 | iInvoke(iClazz, iMethodBefore, needObject, needResult, param, notifyType) 176 | } 177 | 178 | override fun afterHookedMethod(param: MethodHookParam?) { 179 | // LogUtil.e( 180 | // HookCenter::class.java.name, 181 | // "afterHookedMethod: ${param.toString()}" 182 | // ) 183 | if (iClazz == null || iMethodAfter == null) return 184 | iInvoke(iClazz, iMethodAfter, needObject, needResult, param, notifyType) 185 | } 186 | }) 187 | } 188 | } 189 | 190 | /** 191 | * hookMethod 192 | */ 193 | private fun iConstructorHooker( 194 | clazz: Class<*>?, 195 | iClazz: Class<*>?, iMethodBefore: String? = null, iMethodAfter: String? = null, 196 | needObject: Boolean = false, needResult: Boolean = false, notifyType: String = "notify", 197 | vararg parameterTypes: Class<*> 198 | ): EHook { 199 | return EHook { 200 | if (clazz == null) return@EHook 201 | XposedHelpers.findAndHookConstructor(clazz, *parameterTypes, 202 | object : XC_MethodHook() { 203 | override fun beforeHookedMethod(param: MethodHookParam?) { 204 | if (iClazz == null || iMethodBefore == null) return 205 | iInvoke(iClazz, iMethodBefore, needObject, needResult, param, notifyType) 206 | } 207 | 208 | override fun afterHookedMethod(param: MethodHookParam?) { 209 | if (iClazz == null || iMethodAfter == null) return 210 | Log.e( 211 | HookCenter::class.java.name, 212 | "afterHook ${clazz.name} : $iMethodAfter" 213 | ) 214 | iInvoke(iClazz, iMethodAfter, needObject, needResult, param, notifyType) 215 | } 216 | }) 217 | } 218 | } 219 | 220 | /** 221 | * 调用Ixxx回调方法 222 | */ 223 | private fun iInvoke( 224 | iClazz: Class<*>, method: String, needObject: Boolean, needResult: Boolean, 225 | param: XC_MethodHook.MethodHookParam?, notifyType: String 226 | ) { 227 | val iMethod = findMethodsByExactName(iClazz, method).firstOrNull() 228 | var args = param?.args.orEmpty().toList().toTypedArray().toMutableList() 229 | if (needObject && param?.thisObject != null) { 230 | args.add(0, param!!.thisObject) 231 | } 232 | if (needResult) { 233 | args.add(param!!.result) 234 | } 235 | when (notifyType) { 236 | "notify" -> 237 | notify(method) { 238 | iMethod?.invoke(it, *args.toTypedArray()) 239 | } 240 | else -> {} 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/IHookProvider.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | interface IHookProvider { 4 | 5 | fun provideStaticHookers(): List? = null 6 | 7 | fun provideEventHooker(event: String): EHook? = null 8 | 9 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/InterfaceProxy.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | interface InterfaceProxy { 4 | val interfaces: Class<*> 5 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/Operation.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | /** 4 | * 当插件监听到某个事件发生, 并拦截到相应的函数调用的时候, 插件可能会需要对拦截住的函数进行某些操作, 这个操作需要被封 5 | * 装成一个 [Operation] 对象传递给 SpellBook 框架 6 | */ 7 | class Operation( 8 | val value: T? = null, 9 | val error: Throwable? = null, 10 | val priority: Int = 0, 11 | val returnEarly: Boolean = false 12 | ) { 13 | companion object { 14 | /** 15 | * 创建一个空操作, 表明自己什么也不做 16 | */ 17 | @JvmStatic 18 | fun nop(priority: Int = 0): Operation { 19 | return Operation(priority = priority) 20 | } 21 | 22 | /** 23 | * 创建一个打断操作, 跳过原函数的执行, 直接抛出一个异常 24 | * 25 | * @param error 要抛出的异常 26 | * @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的 27 | * 情况下随机选择一个操作 28 | */ 29 | @JvmStatic 30 | fun interruption(error: Throwable, priority: Int = 0): Operation { 31 | return Operation( 32 | error = error, 33 | priority = priority, 34 | returnEarly = true 35 | ) 36 | } 37 | 38 | /** 39 | * 创建一个替换操作, 跳过原函数的执行, 直接返回一个结果 40 | * 41 | * @param value 要返回的结果 42 | * @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的 43 | * 情况下随机选择一个操作 44 | */ 45 | @JvmStatic 46 | fun replacement(value: T, priority: Int = 0): Operation { 47 | return Operation( 48 | value = value, 49 | priority = priority, 50 | returnEarly = true 51 | ) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/Version.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | /** 4 | * 用于比较 Android 版本字符串的类 5 | */ 6 | class Version(private val versionName: String) { 7 | 8 | private val version: List = 9 | versionName.split('.').mapNotNull(String::toIntOrNull) 10 | 11 | override fun toString() = versionName 12 | 13 | override fun hashCode(): Int = version.hashCode() 14 | 15 | override fun equals(other: Any?): Boolean = when (other) { 16 | null -> false 17 | !is Version -> false 18 | else -> this.version == other.version 19 | } 20 | 21 | operator fun compareTo(other: Version): Int { 22 | var result = 0 23 | when { 24 | this.version.size > other.version.size -> result = 1 25 | this.version.size < other.version.size -> result = -1 26 | } 27 | 28 | var index = 0 29 | while (index < this.version.size && index < other.version.size) { 30 | when { 31 | this.version[index] > other.version[index] -> return 1 32 | this.version[index] < other.version[index] -> return -1 33 | } 34 | index++ 35 | } 36 | 37 | return result 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/core/WaitChannel.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.core 2 | 3 | /** 4 | * 实现的一个安全的 Wait Channel, 用来让若干线程安全地阻塞到事件结束 5 | */ 6 | class WaitChannel { 7 | @Volatile private var done = false 8 | private val channel = Object() 9 | 10 | private val current: Long 11 | get() = System.currentTimeMillis() 12 | 13 | fun wait(timeout: Long = 0L): Boolean { 14 | if (done) return false 15 | 16 | val start = current 17 | synchronized(channel) { 18 | // 处理可能的 spurious wakeup 19 | while (!done && start + timeout > current) { 20 | channel.wait(start + timeout - current) 21 | } 22 | return true 23 | } 24 | } 25 | 26 | fun done() { 27 | if (done) return 28 | 29 | synchronized(channel) { 30 | done = true 31 | channel.notifyAll() 32 | } 33 | } 34 | 35 | fun isDone() = done 36 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/helper/ExtensionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.helper 2 | 3 | import java.security.MessageDigest 4 | import java.text.SimpleDateFormat 5 | import java.util.* 6 | 7 | /** ------- String Extension ------ */ 8 | fun String.firstLetterToLowerCase() = 9 | if (this.isEmpty()) this else this.substring(0, 1).toLowerCase() + this.substring(1) 10 | 11 | fun String.MD5() = toHexString(MessageDigest.getInstance(("MD5")).digest(this.toByteArray())) 12 | 13 | fun String.SHA1() = toHexString(MessageDigest.getInstance("SHA-1").digest(this.toByteArray())) 14 | 15 | fun String.SHA256() =toHexString( MessageDigest.getInstance("SHA-256").digest(this.toByteArray())) 16 | 17 | /** ------- ByteArray Extension ------ */ 18 | fun ByteArray.MD5() = toHexString(MessageDigest.getInstance("MD5").digest(this)) 19 | 20 | fun ByteArray.SHA1() = toHexString(MessageDigest.getInstance("SHA-1").digest(this)) 21 | 22 | fun ByteArray.SHA256() = toHexString(MessageDigest.getInstance("SHA-256").digest(this)) 23 | 24 | /** ------- Date Extension ------ */ 25 | fun Date.defaultFormat() = SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA).format(this) 26 | 27 | fun toHexString(bytes: ByteArray) = 28 | with(StringBuffer()) { 29 | bytes.forEach { 30 | val hex = it.toInt() and 0xFF 31 | val hexString = Integer.toHexString(hex) 32 | when (hexString.length) { 33 | 1 -> this.append("0").append(hexString) 34 | else -> this.append(hexString) 35 | } 36 | } 37 | return@with this.toString() 38 | } 39 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/helper/ParserHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.helper 2 | 3 | import com.ehook.utils.ParallelUtil.parallelForEach 4 | import java.io.Closeable 5 | import java.io.File 6 | import java.nio.Buffer 7 | import java.nio.ByteBuffer 8 | import java.nio.ByteOrder 9 | import java.util.concurrent.ConcurrentHashMap 10 | import java.util.zip.ZipEntry 11 | import java.util.zip.ZipFile 12 | 13 | object ParserHelper { 14 | 15 | /** 16 | * 封装对 APK 文件的解析操作 17 | * 18 | * 参考了 dongliu 的 apk-parser 项目 19 | * 20 | * Refer: https://github.com/hsiafan/apk-parser 21 | */ 22 | @ExperimentalUnsignedTypes 23 | class ApkFile(apkFile: File) : Closeable { 24 | /** 25 | * @suppress 26 | */ 27 | private companion object { 28 | private const val DEX_FILE = "classes.dex" 29 | private const val DEX_ADDITIONAL = "classes%d.dex" 30 | } 31 | 32 | constructor(path: String) : this(File(path)) 33 | 34 | private val zipFile: ZipFile = ZipFile(apkFile) 35 | 36 | private fun readEntry(entry: ZipEntry): ByteArray = 37 | zipFile.getInputStream(entry).use { it.readBytes() } 38 | 39 | override fun close() = 40 | zipFile.close() 41 | 42 | private fun getDexFilePath(idx: Int) = 43 | if (idx == 1) DEX_FILE else String.format(DEX_ADDITIONAL, idx) 44 | 45 | private fun isDexFileExist(idx: Int): Boolean { 46 | val path = getDexFilePath(idx) 47 | return zipFile.getEntry(path) != null 48 | } 49 | 50 | private fun readDexFile(idx: Int): ByteArray { 51 | val path = getDexFilePath(idx) 52 | return readEntry(zipFile.getEntry(path)) 53 | } 54 | 55 | val classTypes: ClassTrie by lazy { 56 | var end = 2 57 | while (isDexFileExist(end)) end++ 58 | 59 | val ret = ClassTrie() 60 | (1 until end).parallelForEach { idx -> 61 | val data = readDexFile(idx) 62 | val buffer = ByteBuffer.wrap(data) 63 | val parser = DexParser(buffer) 64 | ret += parser.parseClassTypes() 65 | } 66 | return@lazy ret.apply { mutable = false } 67 | } 68 | } 69 | 70 | @ExperimentalUnsignedTypes 71 | class DexHeader { 72 | var version: Int = 0 73 | 74 | var checksum: UInt = 0u 75 | 76 | var signature: ByteArray = ByteArray(kSHA1DigestLen) 77 | 78 | var fileSize: UInt = 0u 79 | 80 | var headerSize: UInt = 0u 81 | 82 | var endianTag: UInt = 0u 83 | 84 | var linkSize: UInt = 0u 85 | 86 | var linkOff: UInt = 0u 87 | 88 | var mapOff: UInt = 0u 89 | 90 | var stringIdsSize: Int = 0 91 | 92 | var stringIdsOff: UInt = 0u 93 | 94 | var typeIdsSize: Int = 0 95 | 96 | var typeIdsOff: UInt = 0u 97 | 98 | var protoIdsSize: Int = 0 99 | 100 | var protoIdsOff: UInt = 0u 101 | 102 | var fieldIdsSize: Int = 0 103 | 104 | var fieldIdsOff: UInt = 0u 105 | 106 | var methodIdsSize: Int = 0 107 | 108 | var methodIdsOff: UInt = 0u 109 | 110 | var classDefsSize: Int = 0 111 | 112 | var classDefsOff: UInt = 0u 113 | 114 | var dataSize: Int = 0 115 | 116 | var dataOff: UInt = 0u 117 | 118 | /** 119 | * @suppress 120 | */ 121 | companion object { 122 | const val kSHA1DigestLen = 20 123 | const val kSHA1DigestOutputLen = kSHA1DigestLen * 2 + 1 124 | } 125 | } 126 | 127 | 128 | /** 129 | * 用来储存一个 APK 的 package 结构 130 | * 131 | * 出于性能考虑, 这个类不支持读线程和写线程同时操作, 但支持同类型的线程同时操作 132 | */ 133 | class ClassTrie { 134 | /** 135 | * @suppress 136 | */ 137 | private companion object { 138 | /** 139 | * 用来将 JVM 格式的类型标识符转换为类名 140 | * 141 | * Example: String 的类型标识符为 "Ljava/lang/String;" 142 | * Refer: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 143 | */ 144 | private fun convertJVMTypeToClassName(type: String) = 145 | type.substring(1, type.length - 1).replace('/', '.') 146 | } 147 | 148 | /** 149 | * 读写开关, 用于增强线程间的安全性 150 | * 151 | * 只有开关设为 true 的时候, 写操作才会被执行 152 | * 只有开关设为 false 的时候, 读操作才会返回有效数据 153 | */ 154 | @Volatile 155 | var mutable = true 156 | 157 | /** 158 | * package 结构的根结点 159 | */ 160 | private val root: TrieNode = TrieNode() 161 | 162 | /** 163 | * 插入一个单独的 JVM 格式的类型标识符 164 | */ 165 | operator fun plusAssign(type: String) { 166 | if (mutable) { 167 | root.add(convertJVMTypeToClassName(type)) 168 | } 169 | } 170 | 171 | /** 172 | * 插入一组 JVM 格式的类型标识符 173 | */ 174 | operator fun plusAssign(types: Array) { 175 | types.forEach { this += it } 176 | } 177 | 178 | /** 179 | * 查找指定包里指定深度的所有类 180 | * 181 | * 出于性能方面的考虑, 只有深度相等的类才会被返回, 比如搜索深度为0的时候, 就只返回这个包自己拥有的类, 不包括它 182 | * 里面其他包拥有的类. 183 | */ 184 | fun search(packageName: String, depth: Int): List { 185 | if (mutable) return emptyList() 186 | return root.search(packageName, depth) 187 | } 188 | 189 | /** 190 | * 私有的节点结构 191 | */ 192 | private class TrieNode { 193 | val classes: MutableList = ArrayList(50) 194 | 195 | val children: MutableMap = ConcurrentHashMap() 196 | 197 | fun add(className: String) { 198 | add(className, 0) 199 | } 200 | 201 | private fun add(className: String, pos: Int) { 202 | val delimiterAt = className.indexOf('.', pos) 203 | if (delimiterAt == -1) { 204 | synchronized(this) { 205 | classes.add(className) 206 | } 207 | return 208 | } 209 | val pkg = className.substring(pos, delimiterAt) 210 | if (pkg !in children) { 211 | children[pkg] = TrieNode() 212 | } 213 | children[pkg]!!.add(className, delimiterAt + 1) 214 | } 215 | 216 | fun get(depth: Int = 0): List { 217 | if (depth == 0) return classes 218 | return children.flatMap { it.value.get(depth - 1) } 219 | } 220 | 221 | fun search(packageName: String, depth: Int): List { 222 | return search(packageName, depth, 0) 223 | } 224 | 225 | private fun search(packageName: String, depth: Int, pos: Int): List { 226 | val delimiterAt = packageName.indexOf('.', pos) 227 | if (delimiterAt == -1) { 228 | return when (packageName.isEmpty()) { 229 | true -> classes 230 | false -> { 231 | val pkg = packageName.substring(pos) 232 | return children[pkg]?.get(depth) ?: emptyList() 233 | } 234 | } 235 | } 236 | val pkg = packageName.substring(pos, delimiterAt) 237 | val next = children[pkg] ?: return emptyList() 238 | return next.search(packageName, depth, delimiterAt + 1) 239 | } 240 | } 241 | } 242 | 243 | /** 244 | * 封装对 DEX 格式数据的解析操作 245 | * 246 | * 参考了 dongliu 的 apk-parser 项目 247 | * 248 | * Refer: https://github.com/hsiafan/apk-parser 249 | */ 250 | @ExperimentalUnsignedTypes 251 | class DexParser(buffer: ByteBuffer) { 252 | private val buffer: ByteBuffer = buffer.duplicate().apply { 253 | order(ByteOrder.LITTLE_ENDIAN) 254 | } 255 | 256 | private fun ByteBuffer.readBytes(size: Int) = ByteArray(size).also { get(it) } 257 | 258 | fun parseClassTypes(): Array { 259 | // read magic 260 | val magic = String(buffer.readBytes(8)) 261 | if (!magic.startsWith("dex\n")) { 262 | return arrayOf() 263 | } 264 | val version = Integer.parseInt(magic.substring(4, 7)) 265 | // now the version is 035 266 | if (version < 35) { 267 | // version 009 was used for the M3 releases of the Android platform (November–December 2007), 268 | // and version 013 was used for the M5 releases of the Android platform (February–March 2008) 269 | throw Exception("Dex file version: $version is not supported") 270 | } 271 | 272 | // read header 273 | val header = readDexHeader() 274 | header.version = version 275 | 276 | // read string offsets 277 | val stringOffsets = readStringOffsets(header.stringIdsOff, header.stringIdsSize) 278 | 279 | // read type ids 280 | val typeIds = readTypeIds(header.typeIdsOff, header.typeIdsSize) 281 | 282 | // read class ids 283 | val classIds = readClassIds(header.classDefsOff, header.classDefsSize) 284 | 285 | // read class types 286 | return Array(classIds.size) { i -> 287 | val classId = classIds[i] 288 | val typeId = typeIds[classId] 289 | val offset = stringOffsets[typeId] 290 | readStringAtOffset(offset) 291 | } 292 | } 293 | 294 | private fun readDexHeader() = DexHeader().apply { 295 | checksum = buffer.int.toUInt() 296 | 297 | buffer.get(signature) 298 | 299 | fileSize = buffer.int.toUInt() 300 | headerSize = buffer.int.toUInt() 301 | 302 | endianTag = buffer.int.toUInt() 303 | 304 | linkSize = buffer.int.toUInt() 305 | linkOff = buffer.int.toUInt() 306 | 307 | mapOff = buffer.int.toUInt() 308 | 309 | stringIdsSize = buffer.int 310 | stringIdsOff = buffer.int.toUInt() 311 | 312 | typeIdsSize = buffer.int 313 | typeIdsOff = buffer.int.toUInt() 314 | 315 | protoIdsSize = buffer.int 316 | protoIdsOff = buffer.int.toUInt() 317 | 318 | fieldIdsSize = buffer.int 319 | fieldIdsOff = buffer.int.toUInt() 320 | 321 | methodIdsSize = buffer.int 322 | methodIdsOff = buffer.int.toUInt() 323 | 324 | classDefsSize = buffer.int 325 | classDefsOff = buffer.int.toUInt() 326 | 327 | dataSize = buffer.int 328 | dataOff = buffer.int.toUInt() 329 | } 330 | 331 | private fun readStringOffsets(stringIdsOff: UInt, stringIdsSize: Int): IntArray { 332 | (buffer as Buffer).position(stringIdsOff.toInt()) 333 | return IntArray(stringIdsSize) { 334 | buffer.int 335 | } 336 | } 337 | 338 | private fun readTypeIds(typeIdsOff: UInt, typeIdsSize: Int): IntArray { 339 | (buffer as Buffer).position(typeIdsOff.toInt()) 340 | return IntArray(typeIdsSize) { 341 | buffer.int 342 | } 343 | } 344 | 345 | private fun readClassIds(classDefsOff: UInt, classDefsSize: Int): Array { 346 | (buffer as Buffer).position(classDefsOff.toInt()) 347 | return Array(classDefsSize) { 348 | val classIdx = buffer.int 349 | // access_flags, skip 350 | buffer.int 351 | // superclass_idx, skip 352 | buffer.int 353 | // interfaces_off, skip 354 | buffer.int 355 | // source_file_idx, skip 356 | buffer.int 357 | // annotations_off, skip 358 | buffer.int 359 | // class_data_off, skip 360 | buffer.int 361 | // static_values_off, skip 362 | buffer.int 363 | 364 | classIdx 365 | } 366 | } 367 | 368 | private fun readStringAtOffset(offset: Int): String { 369 | (buffer as Buffer).position(offset) 370 | val len = readULEB128Int() 371 | return readString(len) 372 | } 373 | 374 | private fun readULEB128Int(): Int { 375 | var ret = 0 376 | 377 | var count = 0 378 | var byte: Int 379 | do { 380 | if (count > 4) { 381 | throw Exception("read varints error.") 382 | } 383 | byte = buffer.get().toInt() 384 | ret = ret or (byte and 0x7f shl count * 7) 385 | count++ 386 | } while (byte and 0x80 != 0) 387 | 388 | return ret 389 | } 390 | 391 | private fun readString(len: Int): String { 392 | val chars = CharArray(len) 393 | 394 | for (i in 0 until len) { 395 | val a = buffer.get().toInt() 396 | when { 397 | a and 0x80 == 0 -> { // ascii char 398 | chars[i] = a.toChar() 399 | } 400 | a and 0xe0 == 0xc0 -> { // read one more 401 | val b = buffer.get().toInt() 402 | chars[i] = (a and 0x1F shl 6 or (b and 0x3F)).toChar() 403 | } 404 | a and 0xf0 == 0xe0 -> { 405 | val b = buffer.get().toInt() 406 | val c = buffer.get().toInt() 407 | chars[i] = 408 | (a and 0x0F shl 12 or (b and 0x3F shl 6) or (c and 0x3F)).toChar() 409 | } 410 | else -> { 411 | // throw UTFDataFormatException() 412 | } 413 | } 414 | } 415 | 416 | return String(chars) 417 | } 418 | } 419 | 420 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/helper/ProxyHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.helper 2 | 3 | import com.ehook.HookGlobal 4 | import com.ehook.core.InterfaceProxy 5 | import com.ehook.utils.LogUtil 6 | import java.lang.reflect.Proxy 7 | 8 | object ProxyHelper { 9 | fun newProxyInstance(interfaceProxy: InterfaceProxy): Any { 10 | return Proxy.newProxyInstance( 11 | HookGlobal.classLoader, 12 | arrayOf(interfaceProxy.interfaces) 13 | ) { _, method, args -> 14 | val iMethod = ReflecterHelper.findMethodsByExactName( 15 | interfaceProxy::class.java, 16 | method.name 17 | ).firstOrNull() 18 | iMethod?.invoke(interfaceProxy, * args.orEmpty()) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/helper/ReflecterHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.helper 2 | 3 | import android.util.Log 4 | import com.ehook.HookGlobal 5 | import com.ehook.helper.ParserHelper.ClassTrie 6 | import de.robv.android.xposed.XC_MethodHook 7 | import de.robv.android.xposed.XposedBridge.hookMethod 8 | import de.robv.android.xposed.XposedHelpers.findMethodExactIfExists 9 | import java.lang.reflect.Field 10 | import java.lang.reflect.Method 11 | import java.lang.reflect.Modifier 12 | import java.util.* 13 | import java.util.concurrent.ConcurrentHashMap 14 | import kotlin.jvm.internal.Intrinsics 15 | 16 | /** 17 | * 查找类、方法、属性,用于版本自动匹配 18 | */ 19 | object ReflecterHelper { 20 | 21 | private val fieldCache: MutableMap = ConcurrentHashMap() 22 | 23 | /** 24 | * 用于缓存已经完成的[findMethodExact]的搜索结果 25 | */ 26 | private val methodCache: MutableMap = ConcurrentHashMap() 27 | private val methodsCache: MutableMap = ConcurrentHashMap() 28 | 29 | /** 30 | * 查找指定包里指定深度的所有类 31 | * 32 | * 出于性能方面的考虑, 只有深度相等的类才会被返回, 比如搜索深度为0的时候, 就只返回这个包自己拥有的类, 不包括它 33 | * 里面其他包拥有的类. 34 | * 35 | * @param loader 用于取出 [Class] 对象的加载器 36 | * @param trie 整个 APK 的包结构, 由于 Java 的 [ClassLoader] 对象不支持读取所有类名, 我们必须先通过其他手段 37 | * 解析 APK 结构, 然后才能检索某个包内的所有类, 详情请参见 [ApkFile] 和 [WechatGlobal] 38 | * @param packageName 包名 39 | * @param depth 深度 40 | */ 41 | @JvmStatic 42 | fun findClassesInPackage( 43 | loader: ClassLoader, 44 | trie: ClassTrie, 45 | packageName: String, 46 | depth: Int = 0 47 | ): Classes { 48 | val key = "$depth-$packageName" 49 | val cached = classCache[key] 50 | if (cached != null) { 51 | return cached 52 | } 53 | val classes = Classes(trie.search(packageName, depth).mapNotNull { name -> 54 | findClassIfExists(name, loader) 55 | }) 56 | return classes.also { classCache[key] = classes } 57 | } 58 | 59 | @JvmStatic 60 | fun findMethodIfExists( 61 | clazz: Class<*>, 62 | methodName: String, 63 | vararg parameterTypes: Class<*> 64 | ): Method? = 65 | try { 66 | findMethodExact(clazz, methodName, *parameterTypes) 67 | } catch (_: Throwable) { 68 | null 69 | } 70 | 71 | @JvmStatic 72 | fun findMethodsByExactName(clazz: Class<*>, methodName: String): Methods { 73 | val fullMethodName = "${clazz.name}#$methodName#exactnname" 74 | return when (methodsCache[fullMethodName] != null) { 75 | true -> methodsCache[fullMethodName]!! 76 | else -> Methods(clazz.declaredMethods.filter { return@filter it.name.equals(methodName, false) } 77 | .onEach { it.isAccessible = true }) 78 | .also { methodsCache[fullMethodName] = it } 79 | } 80 | } 81 | /** 82 | * 查找所有满足要求的方法 83 | * 84 | * @param clazz 该方法所属的类 85 | * @param returnType 该方法的返回类型 86 | * @param parameterTypes 该方法的参数类型 87 | */ 88 | @JvmStatic 89 | fun findMethodsByExactParameters( 90 | clazz: Class<*>, 91 | returnType: Class<*>?, 92 | vararg parameterTypes: Class<*> 93 | ): List { 94 | return clazz.declaredMethods.filter { method -> 95 | if (returnType != null && returnType != method.returnType) { 96 | return@filter false 97 | } 98 | 99 | val methodParameterTypes = method.parameterTypes 100 | if (parameterTypes.size != methodParameterTypes.size) { 101 | return@filter false 102 | } 103 | for (i in parameterTypes.indices) { 104 | if (parameterTypes[i] != methodParameterTypes[i]) { 105 | return@filter false 106 | } 107 | } 108 | method.isAccessible = true 109 | return@filter true 110 | } 111 | } 112 | 113 | 114 | /** ------------- Field ------------ */ 115 | @JvmStatic 116 | fun findFieldIfExists(clazz: Class<*>, fieldName: String): Field? { 117 | val fullFieldName = "${clazz.name}#$fieldName" 118 | return if (fieldCache.containsKey(fullFieldName)) { 119 | fieldCache[fullFieldName]?.firstOrNull() 120 | } else try { 121 | clazz.getDeclaredField(fieldName) 122 | } catch (_: NoSuchFieldException) { 123 | null 124 | } 125 | } 126 | 127 | /** 通过其他方式过滤类 */ 128 | class Classes(val classes: List>) { 129 | 130 | /** 131 | * @suppress 132 | */ 133 | private companion object { 134 | private const val TAG = "Reflection" 135 | } 136 | 137 | fun filterBySuper(superClass: Class<*>?): Classes { 138 | return Classes(classes.filter { it.superclass == superClass }.also { 139 | if (it.isEmpty()) { 140 | Log.w( 141 | TAG, 142 | "filterBySuper found nothing, super class = ${superClass?.simpleName}" 143 | ) 144 | } 145 | }) 146 | } 147 | 148 | /** 149 | * 非抽象类 150 | */ 151 | fun filterBySuperImpl(superClass: Class<*>?): Classes { 152 | return Classes(classes.filter { superClass!!.isAssignableFrom(it) && !Modifier.isAbstract(it.modifiers) }.also { 153 | if (it.isEmpty()) { 154 | Log.w( 155 | TAG, 156 | "filterBySuperImpl found nothing, super class = ${superClass?.simpleName}" 157 | ) 158 | } 159 | }) 160 | } 161 | 162 | /** 163 | *  (获取对应类的直接外部类Class对象) 164 | */ 165 | fun filterByEnclosingClass(enclosingClass: Class<*>?): Classes { 166 | return Classes(classes.filter { it.enclosingClass == enclosingClass }.also { 167 | if (it.isEmpty()) { 168 | Log.w( 169 | TAG, 170 | "filterByEnclosingClass found nothing, enclosing class = ${enclosingClass?.simpleName} " 171 | ) 172 | } 173 | }) 174 | } 175 | 176 | fun filterByMethod( 177 | returnType: Class<*>?, 178 | methodName: String, 179 | vararg parameterTypes: Class<*> 180 | ): Classes { 181 | return Classes(classes.filter { clazz -> 182 | val method = findMethodExactIfExists(clazz, methodName, *parameterTypes) 183 | method != null && method.returnType == returnType ?: method.returnType 184 | }.also { 185 | if (it.isEmpty()) { 186 | Log.w( 187 | TAG, 188 | "filterByMethod found nothing, returnType = ${returnType?.simpleName}, methodName = $methodName, parameterTypes = ${parameterTypes.joinToString( 189 | "|" 190 | ) { it.simpleName }}" 191 | ) 192 | } 193 | }) 194 | } 195 | 196 | fun filterByMethod(returnType: Class<*>?, vararg parameterTypes: Class<*>): Classes { 197 | return Classes(classes.filter { clazz -> 198 | findMethodsByExactParameters(clazz, returnType, *parameterTypes).isNotEmpty() 199 | }.also { 200 | if (it.isEmpty()) { 201 | Log.w( 202 | TAG, 203 | "filterByMethod found nothing, returnType = ${returnType?.simpleName}, parameterTypes = ${parameterTypes.joinToString( 204 | "|" 205 | ) { it.simpleName }}" 206 | ) 207 | } 208 | }) 209 | } 210 | fun filterByMethod(methodName: String?): Classes { 211 | return Classes(classes.filter { clazz -> 212 | val method = findMethodExactIfExists(clazz, methodName) 213 | method != null 214 | }.also { 215 | if (it.isEmpty()) { 216 | // Log.w( 217 | // TAG, 218 | // "filterByMethod found nothing, parameterTypes = ${parameterTypes.joinToString( 219 | // "|" 220 | // ) { it.simpleName }}" 221 | // ) 222 | } 223 | }) 224 | } 225 | 226 | fun filterByField(fieldName: String, fieldType: String): Classes { 227 | return Classes(classes.filter { clazz -> 228 | val field = findFieldIfExists(clazz, fieldName) 229 | field != null && field.type.canonicalName == fieldType 230 | }.also { 231 | if (it.isEmpty()) { 232 | Log.w( 233 | TAG, 234 | "filterByField found nothing, fieldName = $fieldName, fieldType = $fieldType" 235 | ) 236 | } 237 | }) 238 | } 239 | 240 | fun filterByField(fieldType: String): Classes { 241 | return Classes(classes.filter { clazz -> 242 | findFieldsWithType(clazz, fieldType).isNotEmpty() 243 | }.also { 244 | if (it.isEmpty()) { 245 | Log.w(TAG, "filterByField found nothing, fieldType = $fieldType") 246 | } 247 | }) 248 | } 249 | 250 | fun firstOrNull(): Class<*>? { 251 | if (classes.size > 1) { 252 | val names = classes.map { it.canonicalName } 253 | Log.w(TAG, "found a signature that matches more than one class: $names") 254 | } 255 | return classes.firstOrNull() 256 | } 257 | } 258 | 259 | /** 通过其他方式过滤方法 */ 260 | class Methods(private val methods: List) { 261 | 262 | fun firstOrNull(): Method? { 263 | if (methods.isNotEmpty()) { 264 | val names = methods.map { it.name } 265 | Log.e(ReflecterHelper.javaClass.name, "found a signature methods: $names") 266 | } 267 | return methods.firstOrNull() 268 | } 269 | 270 | } 271 | 272 | /** 通过其他方式过滤属性 */ 273 | class Fields(private val fields: List) { 274 | 275 | fun filterByModifiers(vararg modifiers: Int): Fields = 276 | if (modifiers.size < 0) this else Fields(fields.filter { 277 | it.modifiers == modifiers.reduce { acc, i -> 278 | acc.or( 279 | i 280 | ) 281 | } 282 | }) 283 | 284 | fun isNotEmpty(): Boolean = fields.isNotEmpty() 285 | 286 | fun firstOrNull(): Field? { 287 | if (fields.isNotEmpty()) { 288 | val names = fields.map { it.name } 289 | Log.e(ReflecterHelper.javaClass.name, "found a signature fields: $names") 290 | } 291 | return fields.firstOrNull() 292 | } 293 | } 294 | 295 | /** 296 | * 利用 Reflection 对指定对象进行浅拷贝 297 | */ 298 | @JvmStatic 299 | fun shadowCopy(obj: Any, copy: Any, clazz: Class<*>? = obj::class.java) { 300 | if (clazz == null) { 301 | return 302 | } 303 | shadowCopy(obj, copy, clazz.superclass) 304 | clazz.declaredFields.forEach { 305 | it.isAccessible = true 306 | it.set(copy, it.get(obj)) 307 | } 308 | } 309 | 310 | /** 311 | * 用于缓存已经完成的[findClassesFromPackage]的搜索结果 312 | */ 313 | private val classCache: MutableMap = ConcurrentHashMap() 314 | 315 | @JvmStatic 316 | fun clearClassCache() { 317 | classCache.clear() 318 | } 319 | 320 | 321 | @JvmStatic 322 | fun clearMethodCache() { 323 | methodCache.clear() 324 | } 325 | 326 | /** 327 | * 查找一个确定的类, 如果不存在返回 null 328 | */ 329 | @JvmStatic 330 | fun findClassIfExists(className: String, classLoader: ClassLoader): Class<*>? { 331 | try { 332 | return Class.forName(className, false, classLoader) 333 | } catch (throwable: Throwable) { 334 | if (HookGlobal.unitTestMode) { 335 | throw throwable 336 | } 337 | } 338 | return null 339 | } 340 | 341 | /** 342 | * 查找指定包里指定深度的所有类 343 | * 344 | * 出于性能方面的考虑, 只有深度相等的类才会被返回, 比如搜索深度为0的时候, 就只返回这个包自己拥有的类, 不包括它 345 | * 里面其他包拥有的类. 346 | * 347 | * @param loader 用于取出 [Class] 对象的加载器 348 | * @param trie 整个 APK 的包结构, 由于 Java 的 [ClassLoader] 对象不支持读取所有类名, 我们必须先通过其他手段 349 | * 解析 APK 结构, 然后才能检索某个包内的所有类, 详情请参见 [ApkFile] 和 [WechatGlobal] 350 | * @param packageName 包名 351 | * @param depth 深度 352 | */ 353 | @JvmStatic 354 | fun findClassesFromPackage( 355 | loader: ClassLoader, 356 | trie: ClassTrie, 357 | packageName: String, 358 | depth: Int = 0 359 | ): Classes { 360 | val key = "$depth-$packageName" 361 | val cached = classCache[key] 362 | if (cached != null) { 363 | return cached 364 | } 365 | val classes = Classes(trie.search(packageName, depth).mapNotNull { name -> 366 | findClassIfExists(name, loader) 367 | }) 368 | return classes.also { classCache[key] = classes } 369 | } 370 | 371 | 372 | /** 373 | * 根据 JVM Specification 生成一个参数签名 374 | */ 375 | @JvmStatic 376 | private fun getParametersString(vararg clazzes: Class<*>): String = 377 | "(" + clazzes.joinToString(",") { it.canonicalName ?: "" } + ")" 378 | 379 | /** 380 | * 查找一个确定的方法, 如果不存在, 抛出 [NoSuchMethodException] 异常 381 | * 382 | * @param clazz 该方法所属的类 383 | * @param methodName 该方法的名称 384 | * @param parameterTypes 该方法的参数类型 385 | */ 386 | @JvmStatic 387 | fun findMethodExact( 388 | clazz: Class<*>, 389 | methodName: String, 390 | vararg parameterTypes: Class<*> 391 | ): Method { 392 | val fullMethodName = 393 | "${clazz.name}#$methodName${getParametersString(*parameterTypes)}#exact" 394 | if (fullMethodName in methodCache) { 395 | return methodCache[fullMethodName] ?: throw NoSuchMethodError(fullMethodName) 396 | } 397 | try { 398 | val method = clazz.getDeclaredMethod(methodName, *parameterTypes).apply { 399 | isAccessible = true 400 | } 401 | return method.also { methodCache[fullMethodName] = method } 402 | } catch (e: NoSuchMethodException) { 403 | methodCache[fullMethodName] = null 404 | throw NoSuchMethodError(fullMethodName) 405 | } 406 | } 407 | 408 | 409 | /** 410 | * 查找指定类中所有特定类型的成员变量 411 | */ 412 | @JvmStatic 413 | fun findFieldsWithType(clazz: Class<*>, typeName: String): List { 414 | return clazz.declaredFields.filter { 415 | it.type.name == typeName 416 | } 417 | } 418 | 419 | /** 420 | * 查找指定类中所有特定泛型的成员变量 421 | */ 422 | @JvmStatic 423 | fun findFieldsWithGenericType(clazz: Class<*>, genericTypeName: String): List { 424 | return clazz.declaredFields.filter { 425 | it.genericType.toString() == genericTypeName 426 | } 427 | } 428 | 429 | /** 430 | * 钩住一个类中所有的方法, 一般只用于测试 431 | */ 432 | @JvmStatic 433 | fun hookAllMethodsInClass(clazz: Class<*>, callback: XC_MethodHook) { 434 | clazz.declaredMethods.forEach { method -> hookMethod(method, callback) } 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/helper/TryHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.helper 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.util.Log 6 | import java.util.concurrent.Callable 7 | import java.util.concurrent.ExecutorService 8 | import java.util.concurrent.Executors 9 | import java.util.concurrent.FutureTask 10 | 11 | object TryHelper { 12 | 13 | val mExecutorService: ExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2) 14 | val mHandler: Handler = Handler(Looper.getMainLooper()) 15 | 16 | @JvmStatic 17 | inline fun trySilently(func: () -> T?): T? = 18 | try { func() } catch (t: Throwable) { null } 19 | 20 | @JvmStatic 21 | inline fun tryVerbosely(func: () -> T?): T? = 22 | try { 23 | func() 24 | } catch (t: Throwable) { 25 | t.printStackTrace() 26 | Log.e(TryHelper.javaClass.name, "tryVerbosely error: ${Log.getStackTraceString(t)}"); null 27 | } 28 | 29 | @JvmStatic 30 | inline fun tryMainThreadly(delayMillis: Long = 0, crossinline func: () -> T?) = 31 | mHandler.postDelayed({ 32 | try { func() } catch (t: Throwable) { 33 | t.printStackTrace() 34 | Log.e(TryHelper.javaClass.name, "tryMainThreadly error: ${Log.getStackTraceString(t)}") 35 | } 36 | }, delayMillis) 37 | 38 | @JvmStatic 39 | inline fun tryMainThreadly(delayMillis: Long = 0, crossinline func: () -> T?, crossinline callback: (T?) -> Unit) = 40 | mHandler.postDelayed({ 41 | callback(try { func() } catch (t: Throwable) { 42 | t.printStackTrace() 43 | Log.e(TryHelper.javaClass.name, "tryMainThreadly callback error: ${Log.getStackTraceString(t)}"); null 44 | }) 45 | }, delayMillis) 46 | 47 | @JvmStatic 48 | inline fun tryAsynchronously(crossinline func: () -> T?) = 49 | mExecutorService.submit { 50 | try { func() } catch (t: Throwable) { 51 | t.printStackTrace() 52 | Log.e(TryHelper.javaClass.name, "tryMainThreadly error: ${Log.getStackTraceString(t)}") 53 | } 54 | } 55 | 56 | @JvmStatic 57 | inline fun tryAsynchronously(crossinline func: () -> T?, crossinline callback: (T?) -> Unit) { 58 | val futureTask = object : FutureTask(Callable { 59 | return@Callable try { func() } catch (t: Throwable) { 60 | t.printStackTrace() 61 | Log.e(TryHelper.javaClass.name, "tryAsynchronously callback error: ${Log.getStackTraceString(t)}"); 62 | null 63 | } 64 | }) { 65 | override fun done() { 66 | callback(get()) 67 | } 68 | } 69 | mExecutorService.submit(futureTask) 70 | } 71 | 72 | @JvmStatic 73 | inline fun tryAsynchronously(retryTimes: Int, crossinline func: () -> Pair) = 74 | mExecutorService.submit { 75 | var currentTimes = 0 76 | try { 77 | while (currentTimes <= retryTimes) { 78 | val result = func() 79 | currentTimes = if (result.first) retryTimes + 1 else currentTimes + 1 80 | } 81 | } catch (t: Throwable) { 82 | t.printStackTrace() 83 | Log.e(TryHelper.javaClass.name, "tryAsynchronously retryTimes error: ${Log.getStackTraceString(t)}}") 84 | } 85 | } 86 | 87 | @JvmStatic 88 | inline fun tryAsynchronously(retryTimes: Int, crossinline func: () -> Pair, crossinline callback: (T?) -> Unit){ 89 | val futureTask = object : FutureTask(Callable { 90 | var currentTimes = 0 91 | var t: T? = null 92 | try { 93 | while (currentTimes <= retryTimes) { 94 | val result = func() 95 | t = result.second 96 | currentTimes = if (result.first) retryTimes + 1 else currentTimes + 1 97 | } 98 | } catch (t: Throwable) { 99 | t.printStackTrace() 100 | Log.e(TryHelper.javaClass.name, "tryAsynchronously retryTimes callback error: ${Log.getStackTraceString(t)}") 101 | } 102 | return@Callable t 103 | }) { 104 | override fun done() { 105 | callback(get()) 106 | } 107 | } 108 | mExecutorService.submit(futureTask) 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/media/audio/AudioHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.media.audio 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import java.util.concurrent.ExecutorService 6 | import java.util.concurrent.Executors 7 | 8 | object AudioHelper { 9 | 10 | private val mExecutorService: ExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 11 | private val mHandler: Handler = Handler(Looper.getMainLooper()) 12 | 13 | /** 14 | * 将文件编码成pcm音频源文件 15 | */ 16 | fun encodeToPcm(sourcePath: String, destPath: String, start: Long = 0, callback: ((Boolean) -> Unit)? = null) { 17 | } 18 | 19 | /** 20 | * 将文件编码成silk音频文件 21 | */ 22 | fun encodeToSilk(sourcePath: String, destPath: String, start: Long, callback: ((Boolean) -> Unit)? = null) { 23 | } 24 | 25 | /** 26 | * 解码silk音频格式文件到pcm 27 | */ 28 | fun decodeSilkToPcm(sourcePath: String, destPath: String, callback: ((Boolean) -> Unit)? = null) { 29 | } 30 | 31 | /** 32 | * 解码silk音频到Amr 33 | */ 34 | // fun decodeSilkToAmr(sourcePath: String, destPath: String, callback: ((Boolean) -> Unit)? = null) { 35 | // 36 | // } 37 | 38 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/media/audio/MediaCodecHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.media.audio 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaExtractor 5 | import android.media.MediaFormat 6 | import android.util.Log 7 | import java.io.File 8 | import java.io.FileInputStream 9 | import java.io.FileOutputStream 10 | 11 | /** 12 | * @property path 音频原始路径 13 | */ 14 | class MediaCodecHelper(filePath: String) { 15 | private val mMediaExtractor = MediaExtractor() 16 | private var mMediaCodecDecoder: MediaCodec? = null 17 | private var mMediaCodecEncoder: MediaCodec? = null 18 | var mSampleRate = 8000 19 | var mBitRate = 8000 20 | 21 | companion object { 22 | const val TIMEOUT_US = 100L 23 | } 24 | 25 | init { 26 | try { 27 | mMediaExtractor.setDataSource(filePath) 28 | } catch (t: Throwable) { 29 | Log.e(MediaCodecHelper.javaClass.name, "parseFrom t1: ${t.message}") 30 | try { 31 | mMediaExtractor.setDataSource(FileInputStream(filePath).fd) 32 | } catch (t: Throwable) { 33 | Log.e(MediaCodecHelper.javaClass.name, "parseFrom t1: ${t.message}") 34 | } 35 | } 36 | 37 | // 音频媒体轨道只有一条,大于的则表示不是单一音频 38 | if (mMediaExtractor.trackCount > 1) { 39 | Log.e(MediaCodecHelper.javaClass.name, "parseFrom trackCount: ${mMediaExtractor.trackCount}") 40 | } 41 | } 42 | 43 | fun initDecoder() { 44 | for (i in 0..mMediaExtractor.trackCount) { 45 | val mediaFormat = mMediaExtractor.getTrackFormat(i) 46 | var mime = mediaFormat.getString(MediaFormat.KEY_MIME) 47 | if (mime.equals("audio/ffmpeg", true)) { 48 | mime = MediaFormat.MIMETYPE_AUDIO_MPEG 49 | } 50 | mediaFormat.setString(MediaFormat.KEY_MIME, mime) 51 | if (mime.startsWith("audio", true)) { 52 | mMediaExtractor.selectTrack(i) 53 | mMediaCodecDecoder = MediaCodec.createDecoderByType(mime) 54 | mMediaCodecDecoder?.configure(mediaFormat, null, null, 0); break 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * @param destPath 解码到文件 61 | * @param start 从某个点开始 62 | */ 63 | fun decode(destPath: String, start: Long = 0): Boolean { 64 | if (mMediaCodecDecoder == null) return false 65 | val decoder = mMediaCodecDecoder!! 66 | if (start > 0) { 67 | mMediaExtractor.seekTo(start * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC) 68 | } 69 | decoder.start() 70 | var info = MediaCodec.BufferInfo() 71 | var isEOS = false 72 | val file = File(destPath).also { if (it.parentFile?.exists() == false) it.parentFile?.mkdirs() } 73 | val fileOutputStream = FileOutputStream(file) 74 | while (!isEOS) { 75 | try { 76 | val inIndex = decoder.dequeueInputBuffer(TIMEOUT_US) 77 | if (inIndex > 0) { 78 | when (val outIndex = decoder.dequeueOutputBuffer(info, TIMEOUT_US)) { 79 | } 80 | if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { 81 | fileOutputStream.close() 82 | Log.e(MediaCodecHelper::class.java.name, "解码完成: BUFFER_FLAG_END_OF_STREAM") 83 | break 84 | } 85 | } 86 | } catch (e: Throwable) { 87 | return false 88 | } 89 | } 90 | return true 91 | } 92 | 93 | fun initEncoder() { 94 | 95 | } 96 | 97 | // 先默认转换为wav 98 | fun encode(mime: String, destPath: String) { 99 | 100 | } 101 | 102 | fun destory() { 103 | mMediaCodecDecoder?.stop() 104 | mMediaCodecDecoder?.release() 105 | mMediaExtractor?.release() 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/media/image/BitmapHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.media.image 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.BitmapFactory.Options 7 | import android.graphics.Matrix 8 | import android.media.ExifInterface 9 | import android.util.Log 10 | import java.io.* 11 | import java.nio.ByteBuffer 12 | import java.nio.channels.Channels 13 | import java.nio.channels.ReadableByteChannel 14 | 15 | object BitmapHelper { 16 | 17 | /** 18 | * 获取到图片的方向 19 | * @param path 图片路径 20 | * @return 21 | */ 22 | fun getDegress(path: String?): Float { 23 | var degree = 0F 24 | try { 25 | val exifInterface = ExifInterface(path) 26 | val orientation: Int = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) 27 | when (orientation) { 28 | ExifInterface.ORIENTATION_ROTATE_90 -> degree = 90F 29 | ExifInterface.ORIENTATION_ROTATE_180 -> degree = 180F 30 | ExifInterface.ORIENTATION_ROTATE_270 -> degree = 270F 31 | } 32 | } catch (e: IOException) { 33 | e.printStackTrace() 34 | } 35 | return degree 36 | } 37 | 38 | /** 39 | * 旋转图片 40 | * @param bitmap 图片 41 | * @param degress 旋转角度 42 | * @return 43 | */ 44 | fun rotateBitmap(bitmap: Bitmap?, degress: Float): Bitmap? { 45 | var bitmap: Bitmap? = bitmap 46 | if (bitmap != null) { 47 | val m = Matrix() 48 | m.postRotate(degress) 49 | bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, m, true) 50 | return bitmap 51 | } 52 | return bitmap 53 | } 54 | 55 | /** 56 | * 计算需要缩放的SampleSize 57 | * @param options 58 | * @param rqsW 59 | * @param rqsH 60 | * @return 61 | */ 62 | fun caculateInSampleSize(options: Options, rqsW: Int, rqsH: Int): Int { 63 | val height: Int = options.outHeight 64 | val width: Int = options.outWidth 65 | var inSampleSize = 1 66 | if (rqsW == 0 || rqsH == 0) return 1 67 | if (height > rqsH || width > rqsW) { 68 | val heightRatio: Int = Math.round(height.toFloat() / rqsH.toFloat()) 69 | val widthRatio: Int = Math.round(width.toFloat() / rqsW.toFloat()) 70 | inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio 71 | } 72 | return inSampleSize 73 | } 74 | 75 | /** 76 | * 压缩指定路径的图片,并得到图片对象 77 | * @param path 78 | * @param rqsW 79 | * @param rqsH 80 | * @return 81 | */ 82 | fun compressBitmap(path: String?, rqsW: Int, rqsH: Int): Bitmap { 83 | val options = Options() 84 | options.inJustDecodeBounds = true 85 | BitmapFactory.decodeFile(path, options) 86 | options.inSampleSize = caculateInSampleSize(options, rqsW, rqsH) 87 | options.inJustDecodeBounds = false 88 | return BitmapFactory.decodeFile(path, options) 89 | } 90 | 91 | /** 92 | * 93 | * @param descriptor 94 | * @param resW 95 | * @param resH 96 | * @return 97 | */ 98 | fun compressBitmap(descriptor: FileDescriptor?, resW: Int, resH: Int): Bitmap { 99 | val options = Options() 100 | options.inJustDecodeBounds = true 101 | BitmapFactory.decodeFileDescriptor(descriptor, null, options) 102 | options.inSampleSize = caculateInSampleSize(options, resW, resH) 103 | options.inJustDecodeBounds = false 104 | return BitmapFactory.decodeFileDescriptor(descriptor, null, options) 105 | } 106 | 107 | /** 108 | * 压缩指定路径图片,并将其保存在缓存目录中,通过isDelSrc判定是否删除源文件,并获取到缓存后的图片路径 109 | * @param srcPath 110 | * @param rqsW 111 | * @param rqsH 112 | * @param isDelSrc 113 | * @return 114 | */ 115 | fun compressBitmap(srcPath: String?, rqsW: Int, rqsH: Int, isDelSrc: Boolean): String? { 116 | var bitmap: Bitmap? = compressBitmap(srcPath, rqsW, rqsH) 117 | val srcFile = File(srcPath) 118 | val desPath: String = com.ehook.cache.LRUCache.cachePath("image", srcFile.name) 119 | val degree = getDegress(srcPath) 120 | return try { 121 | if (degree != 0F) bitmap = rotateBitmap(bitmap, degree) 122 | val file = File(desPath) 123 | val fos = FileOutputStream(file) 124 | bitmap!!.compress(Bitmap.CompressFormat.PNG, 70, fos) 125 | fos.close() 126 | if (isDelSrc) srcFile.deleteOnExit() 127 | desPath 128 | } catch (e: Exception) { 129 | Log.d(BitmapHelper.javaClass.name, "compressBitmap error: ${e.message}"); null 130 | } 131 | } 132 | 133 | /** 134 | * 压缩某个输入流中的图片,可以解决网络输入流压缩问题,并得到图片对象 135 | * @param is 136 | * @param reqsW 137 | * @param reqsH 138 | * @return 139 | */ 140 | fun compressBitmap(`is`: InputStream, reqsW: Int, reqsH: Int): Bitmap? { 141 | return try { 142 | val baos = ByteArrayOutputStream() 143 | val channel: ReadableByteChannel = Channels.newChannel(`is`) 144 | val buffer: ByteBuffer = ByteBuffer.allocate(1024) 145 | while (channel.read(buffer) !== -1) { 146 | buffer.flip() 147 | while (buffer.hasRemaining()) baos.write(buffer.array()) 148 | buffer.clear() 149 | } 150 | val bts: ByteArray = baos.toByteArray() 151 | val bitmap: Bitmap = compressBitmap(bts, reqsW, reqsH) 152 | `is`.close() 153 | channel.close() 154 | baos.close() 155 | bitmap 156 | } catch (e: Exception) { 157 | Log.d(BitmapHelper.javaClass.name, "compressBitmap-is-reqsw-reqsh error: ${e.message}") 158 | null 159 | } 160 | } 161 | 162 | /** 163 | * 压缩制定byte[]图片,并得到压缩后的图像 164 | * @param bts 165 | * @param reqsW 166 | * @param reqsH 167 | * @return 168 | */ 169 | fun compressBitmap(bts: ByteArray, reqsW: Int, reqsH: Int): Bitmap { 170 | val options = Options() 171 | options.inJustDecodeBounds = true 172 | BitmapFactory.decodeByteArray(bts, 0, bts.size, options) 173 | options.inSampleSize = caculateInSampleSize(options, reqsW, reqsH) 174 | options.inJustDecodeBounds = false 175 | return BitmapFactory.decodeByteArray(bts, 0, bts.size, options) 176 | } 177 | 178 | /** 179 | * 压缩已存在的图片对象,并返回压缩后的图片 180 | * @param bitmap 181 | * @param reqsW 182 | * @param reqsH 183 | * @return 184 | */ 185 | fun compressBitmap(bitmap: Bitmap, reqsW: Int, reqsH: Int): Bitmap { 186 | return try { 187 | val baos = ByteArrayOutputStream() 188 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) 189 | val bts: ByteArray = baos.toByteArray() 190 | val res: Bitmap = compressBitmap(bts, reqsW, reqsH) 191 | baos.close() 192 | res 193 | } catch (e: IOException) { 194 | Log.d(BitmapHelper.javaClass.name, "compressBitmap-is-reqsw-reqsh error: ${e.message}") 195 | bitmap 196 | } 197 | } 198 | 199 | /** 200 | * 压缩资源图片,并返回图片对象 201 | * @param res [Resources] 202 | * @param resID 203 | * @param reqsW 204 | * @param reqsH 205 | * @return 206 | */ 207 | fun compressBitmap(res: Resources?, resID: Int, reqsW: Int, reqsH: Int): Bitmap { 208 | val options = Options() 209 | options.inJustDecodeBounds = true 210 | BitmapFactory.decodeResource(res, resID, options) 211 | options.inSampleSize = caculateInSampleSize(options, reqsW, reqsH) 212 | options.inJustDecodeBounds = false 213 | return BitmapFactory.decodeResource(res, resID, options) 214 | } 215 | 216 | /** 217 | * 基于质量的压缩算法, 此方法未 解决压缩后图像失真问题 218 | *

可先调用比例压缩适当压缩图片后,再调用此方法可解决上述问题 219 | * @param bitmap 220 | * @param maxBytes 221 | * @return 222 | */ 223 | fun compressBitmap(bitmap: Bitmap, maxBytes: Long): Bitmap? { 224 | return try { 225 | val baos = ByteArrayOutputStream() 226 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) 227 | var options = 90 228 | while (baos.toByteArray().size > maxBytes) { 229 | baos.reset() 230 | bitmap.compress(Bitmap.CompressFormat.PNG, options, baos) 231 | options -= 10 232 | } 233 | val bts: ByteArray = baos.toByteArray() 234 | val bmp: Bitmap = BitmapFactory.decodeByteArray(bts, 0, bts.size) 235 | baos.close() 236 | bmp 237 | } catch (e: IOException) { 238 | Log.d(BitmapHelper.javaClass.name, "compressBitmap-bitmap-maxbytes error: ${e.message}") 239 | null 240 | } 241 | } 242 | 243 | /** 244 | * 得到制定路径图片的options 245 | * @param srcPath 246 | * @return Options [Options] 247 | */ 248 | fun getBitmapOptions(srcPath: String?): Options { 249 | val options = Options() 250 | options.inJustDecodeBounds = true 251 | BitmapFactory.decodeFile(srcPath, options) 252 | return options 253 | } 254 | 255 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/okhttp/HttpClients.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.okhttp 2 | 3 | import android.util.Log 4 | import com.ehook.cache.LRUCache 5 | import com.ehook.helper.MD5 6 | import com.ehook.helper.TryHelper.tryMainThreadly 7 | import com.ehook.utils.LogUtil 8 | import okhttp3.* 9 | import java.io.IOException 10 | 11 | 12 | object HttpClients { 13 | 14 | /** 15 | * 同步下载资源文件,这里由于企业微信发送文件必须是本地路径,故缓存策略固定为DISK 16 | * @param urlString 下载地址 17 | * @param type 文件类型 18 | * @param retryTimes 重试次数,默认不重试 19 | * 20 | * @return 文件缓存地址 21 | */ 22 | fun download( 23 | urlString: String, 24 | type: IHttpConfigs.Type = IHttpConfigs.Type.DEFAULT, 25 | retryTimes: Int = 0 26 | ): String? { 27 | if (!urlString.startsWith("http")) return null 28 | return try { 29 | val request = Request.Builder().url(urlString).build() 30 | OkHttpClient.Builder() 31 | .addInterceptor(Interceptors.getRetryInterceptor(retryTimes)) 32 | .addInterceptor( 33 | Interceptors.getCacheInterceptor( 34 | IHttpConfigs.CachePolicy.DISK, 35 | type 36 | ) 37 | ) 38 | .build().newCall(request).execute() 39 | val cacheKey = request.url.toString().MD5() 40 | LRUCache.cacheDiskPath(type.value, cacheKey) 41 | } catch (e: Exception) { 42 | Log.e(HttpClients::class.java.name, "download fail: ${e.message}") 43 | null 44 | } 45 | } 46 | 47 | /** 48 | * 异步下载资源文件 49 | * @param urlString 下载地址 50 | * @param type 文件类型 51 | * @param retryTimes 重试次数,默认不重试 52 | * @param iDownloadCallback 下载回调 53 | * @param iProgressRequestCallback 上传进度 54 | * @param iProgressResponseCallback 下载进度 55 | */ 56 | fun download( 57 | urlString: String, type: IHttpConfigs.Type = IHttpConfigs.Type.DEFAULT, 58 | retryTimes: Int = 0, iDownloadCallback: IDownloadCallback, 59 | iProgressRequestCallback: IProgressRequestCallback? = null, 60 | iProgressResponseCallback: IProgressResponseCallback? = null 61 | ) { 62 | val clientBuilder = OkHttpClient.Builder() 63 | .addInterceptor(Interceptors.getRetryInterceptor(retryTimes)) 64 | .addInterceptor(Interceptors.getCacheInterceptor(IHttpConfigs.CachePolicy.DISK, type)) 65 | // if (iProgressRequestCallback != null) { 66 | // clientBuilder.addInterceptor(Interceptors.getProgressRequestInterceptor(iProgressRequestCallback)) 67 | // } 68 | // if (iProgressResponseCallback != null) { 69 | // clientBuilder.addInterceptor(Interceptors.getProgressResponseInterceptor(iProgressResponseCallback)) 70 | // } 71 | 72 | val request = Request.Builder().url(urlString).build() 73 | LogUtil.e(HttpClients.javaClass.simpleName,"start") 74 | clientBuilder.build().newCall(request).enqueue(object : Callback { 75 | override fun onFailure(call: Call, e: IOException) { 76 | LogUtil.e(HttpClients.javaClass.simpleName,"onFailure") 77 | tryMainThreadly { 78 | iDownloadCallback(null, type) 79 | } 80 | } 81 | 82 | override fun onResponse(call: Call, response: Response) { 83 | LogUtil.e(HttpClients.javaClass.simpleName,"response$") 84 | val cacheKey = call.request().url.toString().MD5() 85 | tryMainThreadly { 86 | iDownloadCallback(LRUCache.cacheDiskPath(type.value, cacheKey), type) 87 | } 88 | } 89 | }) 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/okhttp/ICallbacks.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.okhttp 2 | 3 | /** 上传进度回调 */ 4 | typealias IProgressRequestCallback = (bytesWrite: Long, bytesTotal: Long, done: Boolean) -> Unit 5 | 6 | /** 下载进度回调 */ 7 | typealias IProgressResponseCallback = (bytesRead: Long, bytesTotal: Long, done: Boolean) -> Unit 8 | 9 | /** 下载回调 */ 10 | typealias IDownloadCallback = (localPath: String?, type: IHttpConfigs.Type) -> Unit 11 | 12 | /** 下载回调 */ 13 | typealias IDownloadCallback2 = (bArr: ByteArray?, localPath: String?, type: IHttpConfigs.Type) -> Unit 14 | 15 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/okhttp/IHttpConfigs.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.okhttp 2 | 3 | interface IHttpConfigs { 4 | 5 | /** 缓存策略 */ 6 | enum class CachePolicy { 7 | NONE, MEMORY, DISK, ALL 8 | } 9 | 10 | /** 文件类型 */ 11 | enum class Type(var value: String) { 12 | DEFAULT("file"), 13 | FILE("file"), 14 | IMAGE("image"), 15 | VOICE("voice"), 16 | VIDEO("video") 17 | } 18 | 19 | /** 请求方法 常用配置 */ 20 | enum class HttpMethod { 21 | GET, POST, DELETE, PUT, 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/okhttp/Interceptors.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.okhttp 2 | 3 | import com.ehook.cache.LRUCache 4 | import com.ehook.helper.MD5 5 | import okhttp3.Interceptor 6 | import okhttp3.Protocol 7 | import okhttp3.Response 8 | import okhttp3.ResponseBody.Companion.toResponseBody 9 | 10 | object Interceptors { 11 | 12 | /** 重试 */ 13 | fun getRetryInterceptor(maxRetryTimes: Int = 3): Interceptor = 14 | object : Interceptor { 15 | override fun intercept(chain: Interceptor.Chain): Response { 16 | var retryTimes = 0 17 | 18 | val request = chain.request() 19 | var response = chain.proceed(request) 20 | 21 | while (!response.isSuccessful && retryTimes < maxRetryTimes) { 22 | retryTimes += 1 23 | response = chain.proceed(request) 24 | } 25 | return response 26 | } 27 | } 28 | 29 | /** 30 | * 缓存拦截器 31 | * @param cachePolicy IConfigs.CachePolicy 32 | * @param type IConfigs.Type 33 | */ 34 | fun getCacheInterceptor(cachePolicy: IHttpConfigs.CachePolicy = IHttpConfigs.CachePolicy.ALL, type: IHttpConfigs.Type = IHttpConfigs.Type.DEFAULT) = 35 | object : Interceptor { 36 | override fun intercept(chain: Interceptor.Chain): Response { 37 | val cacheKey = chain.request().url.toString().MD5() 38 | var cacheBytes: ByteArray? = 39 | when (cachePolicy) { 40 | IHttpConfigs.CachePolicy.NONE -> null 41 | IHttpConfigs.CachePolicy.MEMORY -> LRUCache.getByteArrayFromMemory(cacheKey) 42 | IHttpConfigs.CachePolicy.DISK -> LRUCache.getFromDisk(type.value, cacheKey) 43 | IHttpConfigs.CachePolicy.ALL -> { 44 | var bytes = LRUCache.getByteArrayFromMemory(cacheKey) 45 | when (bytes == null) { 46 | true -> LRUCache.getFromDisk(type.name, cacheKey) 47 | false -> bytes 48 | } 49 | } 50 | } 51 | 52 | return when (cacheBytes != null) { 53 | true -> { 54 | Response.Builder() 55 | .request(chain.request()) 56 | .protocol(Protocol.HTTP_1_0) 57 | .code(200) 58 | .message("cache response success") 59 | .body(cacheBytes.toResponseBody()) 60 | .build() 61 | } 62 | false -> { 63 | val response = chain.proceed(chain.request()) 64 | val bytes = response.body?.bytes() 65 | if (bytes != null) { 66 | when (cachePolicy) { 67 | IHttpConfigs.CachePolicy.NONE -> {} 68 | IHttpConfigs.CachePolicy.MEMORY -> LRUCache.cacheInMemory(cacheKey, content = bytes) 69 | IHttpConfigs.CachePolicy.DISK -> LRUCache.cacheInDisk(type.value, cacheKey, content = bytes) 70 | IHttpConfigs.CachePolicy.ALL -> { 71 | LRUCache.cacheInMemory(cacheKey, content = bytes) 72 | LRUCache.cacheInDisk(type.value, cacheKey, content = bytes) 73 | } 74 | } 75 | } 76 | response 77 | } 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/plugins/ActivityHook.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.plugins 2 | 3 | import com.ehook.core.Clazz 4 | import com.ehook.core.HookCenter 5 | import com.ehook.plugins.interfaces.IActivityHook 6 | 7 | object ActivityHook : HookCenter() { 8 | 9 | override val interfaces: List> 10 | get() = listOf(IActivityHook::class.java) 11 | 12 | override fun provideEventHooker(event: String) = when (event) { 13 | "onCreate" -> 14 | iMethodNotifyHooker( 15 | clazz = Clazz.Activity, 16 | method = "onCreate", 17 | iClazz = IActivityHook::class.java, 18 | iMethodAfter = "onCreate", 19 | needObject = true, 20 | parameterTypes = *arrayOf(Clazz.Bundle) 21 | ) 22 | "onStart" -> 23 | iMethodNotifyHooker( 24 | clazz = Clazz.Activity, 25 | method = "onStart", 26 | iClazz = IActivityHook::class.java, 27 | iMethodAfter = "onStart", 28 | needObject = true 29 | ) 30 | "onResume" -> 31 | iMethodNotifyHooker( 32 | clazz = Clazz.Activity, 33 | method = "onResume", 34 | iClazz = IActivityHook::class.java, 35 | iMethodAfter = "onResume", 36 | needObject = true 37 | ) 38 | "onPause" -> 39 | iMethodNotifyHooker( 40 | clazz = Clazz.Activity, 41 | method = "onPause", 42 | iClazz = IActivityHook::class.java, 43 | iMethodAfter = "onPause", 44 | needObject = true 45 | ) 46 | "onStop" -> 47 | iMethodNotifyHooker( 48 | clazz = Clazz.Activity, 49 | method = "onStop", 50 | iClazz = IActivityHook::class.java, 51 | iMethodAfter = "onStop", 52 | needObject = true 53 | ) 54 | "onDestroy" -> 55 | iMethodNotifyHooker( 56 | clazz = Clazz.Activity, 57 | method = "onDestroy", 58 | iClazz = IActivityHook::class.java, 59 | iMethodAfter = "onDestroy", 60 | needObject = true 61 | ) 62 | else -> throw IllegalArgumentException("Unknown event: $event") 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/plugins/SharedEngine.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.plugins 2 | 3 | import com.ehook.core.HookCenter 4 | 5 | object SharedEngine { 6 | 7 | var hookCenters: List = listOf( 8 | ActivityHook 9 | // DatabaseHookers, 10 | // FileHookers 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/plugins/interfaces/IActivityHook.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.plugins.interfaces 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | 7 | interface IActivityHook { 8 | 9 | fun onCreate(activity: Activity, savedInstanceState: Bundle?) {} 10 | fun onStart(activity: Activity) {} 11 | fun onResume(activity: Activity) {} 12 | fun onPause(activity: Activity) {} 13 | fun onStop(activity: Activity) {} 14 | fun onDestroy(activity: Activity) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/utils/CmdUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.utils 2 | 3 | import android.app.ActivityManager 4 | import android.content.Context 5 | import android.os.UserHandle 6 | import java.io.BufferedReader 7 | import java.io.DataOutputStream 8 | import java.io.IOException 9 | import java.io.InputStreamReader 10 | import java.lang.reflect.Method 11 | 12 | object CmdUtil { 13 | 14 | private const val CMD_SU = "su" 15 | private const val CMD_SH = "sh" 16 | private const val CMD_EXIT = "exit\n" 17 | private const val CMD_LINE_END = "\n" 18 | 19 | data class Result(var result: Int, var successMsg: String? = null, var errorMsg: String? = null) 20 | 21 | val isRoot: Boolean 22 | get() = exec(command = "echo root", isNeedResultMsg = false).result == 0 23 | 24 | fun exec(command: String, isRoot: Boolean = true, isNeedResultMsg: Boolean = true): Result = 25 | exec(listOf(command), isRoot, isNeedResultMsg) 26 | 27 | fun exec( 28 | commands: List?, 29 | isRoot: Boolean = true, 30 | isNeedResultMsg: Boolean = true 31 | ): Result = 32 | exec((commands ?: listOf()).toTypedArray(), isRoot, isNeedResultMsg) 33 | 34 | fun exec(commands: Array?, isRoot: Boolean, isNeedResultMsg: Boolean = true): Result { 35 | var result = -1 36 | if (commands == null || commands.isEmpty()) { 37 | return Result(result, null, null) 38 | } 39 | var process: Process? = null 40 | var successResult: BufferedReader? = null 41 | var errorResult: BufferedReader? = null 42 | var successMsg: StringBuilder? = null 43 | var errorMsg: StringBuilder? = null 44 | var os: DataOutputStream? = null 45 | try { 46 | process = Runtime.getRuntime().exec(if (isRoot) CMD_SU else CMD_SH) 47 | os = DataOutputStream(process.outputStream) 48 | for (command in commands) { 49 | if (command == null) { 50 | continue 51 | } 52 | // donnot use os.writeBytes(commmand), avoid chinese charset error 53 | os.write(command.toByteArray()) 54 | os.writeBytes(CMD_LINE_END) 55 | os.flush() 56 | } 57 | os.writeBytes(CMD_EXIT) 58 | os.flush() 59 | result = process.waitFor() 60 | // get command result 61 | if (isNeedResultMsg) { 62 | successMsg = StringBuilder() 63 | errorMsg = StringBuilder() 64 | successResult = BufferedReader(InputStreamReader(process.inputStream)) 65 | errorResult = BufferedReader(InputStreamReader(process.errorStream)) 66 | var s: String? 67 | while (successResult.readLine().also { s = it } != null) { 68 | successMsg.append(s) 69 | } 70 | while (errorResult.readLine().also { s = it } != null) { 71 | errorMsg.append(s) 72 | } 73 | } 74 | } catch (e: IOException) { 75 | e.printStackTrace() 76 | } catch (e: Exception) { 77 | e.printStackTrace() 78 | } finally { 79 | try { 80 | os?.close() 81 | successResult?.close() 82 | errorResult?.close() 83 | } catch (e: IOException) { 84 | e.printStackTrace() 85 | } 86 | process?.destroy() 87 | } 88 | return Result( 89 | result, 90 | successMsg?.toString(), 91 | errorMsg?.toString() 92 | ) 93 | } 94 | 95 | fun killProcesses(context: Context, packageName: String) { 96 | var am: ActivityManager = 97 | context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 98 | am.killBackgroundProcesses(packageName) 99 | am.restartPackage(packageName) 100 | // val method = Class.forName("android.app.ActivityManager") 101 | // .getMethod("forceStopPackage", String.javaClass) 102 | // method.isAccessible = true 103 | // method.invoke(am, packageName) 104 | } 105 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/utils/FileUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.os.Environment 8 | import android.os.SystemClock 9 | import de.robv.android.xposed.XposedBridge 10 | import java.io.* 11 | import java.text.SimpleDateFormat 12 | import java.util.* 13 | 14 | object FileUtil { 15 | 16 | @JvmStatic 17 | private fun isAccessExternal(context: Context): Boolean = 18 | (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) 19 | && context.externalCacheDir != null) 20 | 21 | @JvmStatic 22 | fun getCacheDir(context: Context): String { 23 | val cacheDir = File(if (isAccessExternal(context)) context.externalCacheDir?.path else context.cacheDir?.path) 24 | if (!cacheDir.exists()) cacheDir.mkdirs() 25 | return cacheDir.path 26 | } 27 | 28 | @JvmStatic 29 | fun mkDirIfNotExists(context: Context, dirPath: String?): String { 30 | val cacheDir = getCacheDir(context) 31 | val file = File(cacheDir, dirPath) 32 | if (!file.exists()) file.mkdirs() 33 | return file.path 34 | } 35 | 36 | @JvmStatic 37 | fun writeBytesToDisk(path: String, content: ByteArray) { 38 | val file = File(path).also { it.parentFile.mkdirs() } 39 | val fout = FileOutputStream(file) 40 | BufferedOutputStream(fout).use { it.write(content) } 41 | } 42 | 43 | @JvmStatic 44 | fun readBytesFromDisk(path: String): ByteArray { 45 | val fin = FileInputStream(path) 46 | return BufferedInputStream(fin).use { it.readBytes() } 47 | } 48 | 49 | @JvmStatic 50 | fun writeObjectToDisk(path: String, obj: Serializable) { 51 | val out = ByteArrayOutputStream() 52 | ObjectOutputStream(out).use { 53 | it.writeObject(obj) 54 | } 55 | writeBytesToDisk(path, out.toByteArray()) 56 | } 57 | 58 | @JvmStatic 59 | fun readObjectFromDisk(path: String): Any? { 60 | val bytes = readBytesFromDisk(path) 61 | val ins = ByteArrayInputStream(bytes) 62 | return ObjectInputStream(ins).use { 63 | it.readObject() 64 | } 65 | } 66 | 67 | @JvmStatic 68 | fun writeInputStreamToDisk(path: String, ins: InputStream, bufferSize: Int = 8192) { 69 | val file = File(path) 70 | file.parentFile.mkdirs() 71 | val fout = FileOutputStream(file) 72 | BufferedOutputStream(fout).use { 73 | val buffer = ByteArray(bufferSize) 74 | var length = ins.read(buffer) 75 | while (length != -1) { 76 | it.write(buffer, 0, length) 77 | length = ins.read(buffer) 78 | } 79 | } 80 | } 81 | 82 | @JvmStatic 83 | fun writeBitmapToDisk(path: String, bitmap: Bitmap) { 84 | val out = ByteArrayOutputStream() 85 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) 86 | writeBytesToDisk(path, out.toByteArray()) 87 | } 88 | 89 | @JvmStatic 90 | inline fun writeOnce(path: String, writeCallback: (String) -> Unit) { 91 | val file = File(path) 92 | if (!file.exists()) { 93 | writeCallback(path) 94 | return 95 | } 96 | val bootAt = System.currentTimeMillis() - SystemClock.elapsedRealtime() 97 | val modifiedAt = file.lastModified() 98 | if (modifiedAt < bootAt) { 99 | writeCallback(path) 100 | } 101 | } 102 | 103 | @JvmStatic 104 | fun createTimeTag(): String { 105 | val formatter = SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.getDefault()) 106 | return formatter.format(Calendar.getInstance().time) 107 | } 108 | 109 | @JvmStatic 110 | fun notifyNewMediaFile(path: String, context: Context?) { 111 | val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) 112 | context?.sendBroadcast(intent.apply { 113 | data = Uri.fromFile(File(path)) 114 | }) 115 | } 116 | 117 | fun copyAssets(context: Context, appDir: String, dir: String, cover: Boolean = false) { 118 | val assetManager = context.assets 119 | var files: Array? = null 120 | try { 121 | files = assetManager.list(dir) 122 | } catch (e: IOException) { 123 | e.printStackTrace() 124 | } 125 | 126 | if (files != null) { 127 | File(appDir + File.separator + dir + File.separator).mkdirs() 128 | for (filename in files) { 129 | var `in`: InputStream? = null 130 | var out: OutputStream? = null 131 | try { 132 | `in` = assetManager.open(dir + File.separator + filename) 133 | val outFile = File(appDir + File.separator + dir + File.separator + filename) 134 | if (outFile.exists()) { 135 | if (!cover) { 136 | continue 137 | } else { 138 | outFile.delete() 139 | } 140 | } 141 | out = FileOutputStream(outFile) 142 | copyFile(`in`, out) 143 | } catch (e: IOException) { 144 | e.printStackTrace() 145 | } finally { 146 | if (`in` != null) { 147 | try { 148 | `in`!!.close() 149 | } catch (e: IOException) { 150 | e.printStackTrace() 151 | } 152 | 153 | } 154 | if (out != null) { 155 | try { 156 | out!!.close() 157 | } catch (e: IOException) { 158 | e.printStackTrace() 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | @Throws(IOException::class) 167 | fun copyFile(`in`: InputStream, out: OutputStream) { 168 | val buffer = ByteArray(1024) 169 | var read: Int 170 | do { 171 | read = `in`.read(buffer) 172 | if (read == -1) { 173 | break 174 | } 175 | out.write(buffer, 0, read) 176 | } while (true) 177 | } 178 | 179 | fun write(fileName: String, content: String, append: Boolean = false) { 180 | var writer: FileWriter? = null 181 | try { 182 | // 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件 183 | writer = FileWriter(fileName, append) 184 | writer.write(content) 185 | } catch (e: IOException) { 186 | XposedBridge.log(e) 187 | } finally { 188 | try { 189 | writer?.close() 190 | } catch (e: IOException) { 191 | e.printStackTrace() 192 | } 193 | } 194 | } 195 | 196 | @Throws(IOException::class) 197 | fun bytesToFile(bytes: ByteArray?, result: File?) { 198 | val bos = 199 | BufferedOutputStream(FileOutputStream(result)) 200 | bos.write(bytes) 201 | bos.flush() 202 | bos.close() 203 | } 204 | 205 | @Throws(IOException::class) 206 | fun toByteArray(input: InputStream): ByteArray { 207 | val output = ByteArrayOutputStream() 208 | copy(input, output) 209 | return output.toByteArray() 210 | } 211 | 212 | @Throws(IOException::class) 213 | fun copy(input: InputStream, output: OutputStream): Int { 214 | val count = copyLarge(input, output) 215 | return if (count > Int.MAX_VALUE) { 216 | -1 217 | } else count.toInt() 218 | } 219 | 220 | @Throws(IOException::class) 221 | fun copy(sourcePath: String, destPath: String): Int { 222 | val sourceFile = File(sourcePath) 223 | if (!sourceFile.exists()) return -1 224 | val destFile = File(destPath).also { if (it.parentFile?.exists() == false) it.parentFile?.mkdirs() } 225 | return copy(FileInputStream(sourceFile), FileOutputStream(destFile)) 226 | } 227 | 228 | private const val DEFAULT_BUFFER_SIZE = 1024 * 4 229 | @Throws(IOException::class) 230 | fun copyLarge(input: InputStream, output: OutputStream): Long { 231 | val buffer = 232 | ByteArray(DEFAULT_BUFFER_SIZE) 233 | var count: Long = 0 234 | var n = 0 235 | while (-1 != input.read(buffer).also { n = it }) { 236 | output.write(buffer, 0, n) 237 | count += n.toLong() 238 | } 239 | return count 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/utils/LogUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.utils 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * Created by wangcong on 14-12-26. 7 | * 在控制台打印Log,发布版本时在Application中设置答应Log 为false 8 | */ 9 | object LogUtil { 10 | 11 | private var isDebug = true 12 | 13 | private const val TAG = "easyhook" 14 | fun i(msg: String?) { 15 | if (isDebug) Log.i(TAG, msg) 16 | } 17 | 18 | fun d(msg: String?) { 19 | if (isDebug) Log.d(TAG, msg) 20 | } 21 | 22 | fun e(msg: String?) { 23 | if (isDebug) Log.e(TAG, msg) 24 | } 25 | 26 | fun v(msg: String?) { 27 | if (isDebug) Log.v(TAG, msg) 28 | } 29 | 30 | fun i(_class: Class<*>, msg: String?) { 31 | if (isDebug) Log.i(_class.getName(), msg) 32 | } 33 | 34 | fun d(_class: Class<*>, msg: String?) { 35 | if (isDebug) Log.d(_class.getName(), msg) 36 | } 37 | 38 | fun e(_class: Class<*>, msg: String?) { 39 | if (isDebug) Log.e(_class.getName(), msg) 40 | } 41 | 42 | fun v(_class: Class<*>, msg: String?) { 43 | if (isDebug) Log.v(_class.getName(), msg) 44 | } 45 | 46 | fun i(tag: String?, msg: String?) { 47 | if (isDebug) Log.i(tag, msg) 48 | } 49 | 50 | fun d(tag: String?, msg: String?) { 51 | if (isDebug) Log.d(tag, msg) 52 | } 53 | 54 | fun d(_class: Class<*>?, methodName: String?, msg: String?) { 55 | if (isDebug && (_class != null || methodName != null) && msg != null) Log.d(_class?.name + "--" + methodName, msg) 56 | } 57 | 58 | fun e(tag: String?, msg: String?) { 59 | if (isDebug) Log.e(tag, msg) 60 | } 61 | 62 | fun v(tag: String?, msg: String?) { 63 | if (isDebug) Log.v(tag, msg) 64 | } 65 | 66 | /** 67 | * 此方法用于框架内部调试 68 | * @param debug 69 | * @param clazz 70 | * @param method 71 | * @param msg 72 | */ 73 | fun d(debug: Boolean, clazz: Class<*>, method: String, msg: String?) { 74 | if (!isDebug) return 75 | if (debug && msg != null) Log.d(clazz.getName().toString() + " -- " + method, msg) 76 | } 77 | 78 | /** 79 | * 80 | * @param debug 81 | * @param clazz 82 | * @param msg 83 | */ 84 | fun d(debug: Boolean, clazz: Class<*>, msg: String?) { 85 | if (!isDebug) return 86 | if (debug && msg != null) Log.d(clazz.getName(), msg) 87 | } 88 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/utils/MirrorUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.utils 2 | 3 | object MirrorUtil { 4 | /** 5 | * 返回一个 Object 所声明的所有成员变量(不含基类成员) 6 | */ 7 | @JvmStatic fun collectFields(instance: Any): List> { 8 | return instance::class.java.declaredFields.filter { field -> 9 | field.name != "INSTANCE" && field.name != "\$\$delegatedProperties" 10 | }.map { field -> 11 | field.isAccessible = true 12 | val key = field.name.removeSuffix("\$delegate") 13 | val value = field.get(instance) 14 | key to value 15 | } 16 | } 17 | 18 | /** 19 | * 生成一份适配报告, 记录每个自动适配表达式最终指向了微信中的什么位置 20 | */ 21 | @JvmStatic fun generateReport(instances: List): List> { 22 | return instances.map { instance -> 23 | collectFields(instance).map { 24 | "${instance::class.java.canonicalName}.${it.first}" to it.second.toString() 25 | } 26 | }.flatten().sortedBy { it.first } 27 | } 28 | 29 | /** 30 | * 将一个用于单元测试的惰性求值对象还原到未求值的状态 31 | * 32 | * WARN: 仅供单元测试使用 33 | */ 34 | @JvmStatic fun clearUnitTestLazyFields(instance: Any) { 35 | instance::class.java.declaredFields.forEach { field -> 36 | if (Lazy::class.java.isAssignableFrom(field.type)) { 37 | field.isAccessible = true 38 | val lazyObject = field.get(instance) 39 | // if (lazyObject is MagicWxGlobal.UnitTestLazyImpl<*>) { 40 | // lazyObject.refresh() 41 | // } 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * 生成一份适配报告, 记录每个自动适配表达式最终指向了微信中的什么位置 48 | * 49 | * 如果某个自动适配表达式还没有进行求值的话, 该函数会强制进行一次求值 50 | * 51 | * WARN: 仅供单元测试使用 52 | */ 53 | @JvmStatic fun generateReportWithForceEval(instances: List): List> { 54 | return instances.map { instance -> 55 | collectFields(instance).map { 56 | val value = it.second 57 | if (value is Lazy<*>) { 58 | if (!value.isInitialized()) { 59 | value.value 60 | } 61 | } 62 | "${instance::class.java.canonicalName}.${it.first}" to it.second.toString() 63 | } 64 | }.flatten() // 为了 Benchmark 的准确性, 不对结果进行排序 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/utils/ParallelUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.utils 2 | 3 | import java.util.concurrent.ExecutorService 4 | import java.util.concurrent.Executors 5 | import java.util.concurrent.TimeUnit 6 | import kotlin.concurrent.thread 7 | import com.ehook.helper.TryHelper.tryVerbosely 8 | 9 | object ParallelUtil { 10 | 11 | val processors: Int = Runtime.getRuntime().availableProcessors() 12 | 13 | @JvmStatic 14 | fun createThreadPool(nThread: Int = processors): ExecutorService = 15 | Executors.newFixedThreadPool(nThread) 16 | 17 | @JvmStatic 18 | inline fun List.parallelMap(crossinline transform: (T) -> R): List { 19 | val sectionSize = size / processors 20 | 21 | val main = List(processors) { mutableListOf() } 22 | (0 until processors).map { section -> 23 | thread(start = true) { 24 | for (offset in 0 until sectionSize) { 25 | val idx = section * sectionSize + offset 26 | main[section].add(transform(this[idx])) 27 | } 28 | } 29 | }.forEach { it.join() } 30 | 31 | val rest = (0 until size % processors).map { offset -> 32 | val idx = processors * sectionSize + offset 33 | transform(this[idx]) 34 | } 35 | 36 | return main.flatten() + rest 37 | } 38 | 39 | @JvmStatic 40 | inline fun Iterable.parallelForEach(crossinline action: (T) -> Unit) { 41 | val pool = createThreadPool() 42 | val iterator = iterator() 43 | while (iterator.hasNext()) { 44 | val item = iterator.next() 45 | pool.execute { 46 | tryVerbosely { action(item) } // 避免进程崩溃 47 | } 48 | } 49 | pool.shutdown() 50 | pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS) 51 | } 52 | } -------------------------------------------------------------------------------- /ehook/src/main/java/com/ehook/utils/XposedUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ehook.utils 2 | 3 | import android.os.Build 4 | import android.os.Handler 5 | import android.os.HandlerThread 6 | import com.ehook.core.EHook 7 | import com.ehook.helper.TryHelper.tryVerbosely 8 | import com.ehook.helper.TryHelper.trySilently 9 | 10 | object XposedUtil { 11 | 12 | private val workerPool = ParallelUtil.createThreadPool() 13 | 14 | private val managerThread = HandlerThread("HookHandler").apply { start() } 15 | 16 | private val managerHandler: Handler = Handler(managerThread.looper) 17 | 18 | @JvmStatic 19 | private inline fun tryHook(crossinline hook: () -> Unit) { 20 | when { 21 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> { 22 | tryVerbosely(hook) 23 | } 24 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { 25 | workerPool.execute { tryVerbosely(hook) } 26 | } 27 | else -> { 28 | workerPool.execute { trySilently(hook) } 29 | } 30 | } 31 | } 32 | 33 | @JvmStatic 34 | fun postHooker(EHook: EHook) { 35 | managerHandler.post { 36 | EHook.hook() 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /ehook/src/main/jniLibs/arm64-v8a/libsilk.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/ehook/src/main/jniLibs/arm64-v8a/libsilk.so -------------------------------------------------------------------------------- /ehook/src/main/jniLibs/armeabi-v7a/libsilk.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwl568633995/xposed-douyin/d0e7f6fdc3e996be76bc03b7a3164824c81e979c/ehook/src/main/jniLibs/armeabi-v7a/libsilk.so -------------------------------------------------------------------------------- /ehook/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ehook 3 | 4 | -------------------------------------------------------------------------------- /ehook/src/test/java/com/easy/hooker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.ehook 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 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | # Automatically convert third-party libraries to use AndroidX 24 | android.enableJetifier=true 25 | 26 | # Kotlin code style for this project: "official" or "obsolete": 27 | kotlin.code.style=official 28 | 29 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Feb 01 00:21:08 CST 2020 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-5.5.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':ehook',':dy' 2 | 3 | 4 | --------------------------------------------------------------------------------