├── .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 | 
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 |
--------------------------------------------------------------------------------