, Any?> = emptyArray()
112 | ) {
113 | try {
114 | getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run {
115 | isAccessible = true
116 | invoke(target, *paramsPairs.map { it.second }.toTypedArray())
117 | }
118 | } catch (e: Exception) {
119 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
120 | }
121 | }
122 |
123 | /**
124 | * 创建目标类对象
125 | *
126 | * @param paramsPairs 参数类型和参数值的键值对。示例:
127 | *
128 | * val context = ContextImpl::class.newInstance(
129 | * ActivityThread::class to ...,
130 | * LoadedApk::class to ...,
131 | * String::class to ...,
132 | * IBinder::class to ...,
133 | * )
134 | *
135 | * @return 目标对象新实例
136 | */
137 | fun Class<*>.newInstance(vararg paramsPairs: Pair, Any?> = emptyArray()) = try {
138 | if (paramsPairs.isEmpty()) newInstance()
139 | else getDeclaredConstructor(*paramsPairs.map { it.first.java }.toTypedArray()).run {
140 | isAccessible = true
141 | newInstance(*paramsPairs.map { it.second }.toTypedArray()) as? T?
142 | }
143 | } catch (e: Exception) {
144 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
145 | null
146 | }
147 |
148 | fun KClass<*>.invoke(
149 | target: Any?,
150 | methodName: String,
151 | vararg paramsPairs: Pair, Any?> = emptyArray()
152 | ) = try {
153 | java.run {
154 | getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run {
155 | isAccessible = true
156 | invoke(target, *paramsPairs.map { it.second }.toTypedArray()) as? T?
157 | }
158 | }
159 | } catch (e: Exception) {
160 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
161 | null
162 | }
163 |
164 | fun KClass<*>.invokeVoid(
165 | target: Any?,
166 | methodName: String,
167 | vararg paramsPairs: Pair, Any?> = emptyArray()
168 | ) {
169 | try {
170 | java.run {
171 | getDeclaredMethod(methodName, *paramsPairs.map { it.first.java }.toTypedArray()).run {
172 | isAccessible = true
173 | invoke(target, *paramsPairs.map { it.second }.toTypedArray())
174 | }
175 | }
176 | } catch (e: Exception) {
177 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
178 | }
179 | }
180 |
181 | fun KClass<*>.newInstance(vararg paramsPairs: Pair, Any?> = emptyArray()) = try {
182 | java.run {
183 | getDeclaredConstructor(*paramsPairs.map { it.first.java }.toTypedArray()).run {
184 | isAccessible = true
185 | newInstance(*paramsPairs.map { it.second }.toTypedArray()) as? T?
186 | }
187 | }
188 | } catch (e: Exception) {
189 | if (throwReflectException) throw e else Log.e(TAG, e.toString(), e)
190 | null
191 | }
192 |
193 | fun String.set(target: Any?, fieldName: String, value: Any?) =
194 | Class.forName(this).set(target, fieldName, value)
195 |
196 | fun String.get(target: Any?, fieldName: String) = Class.forName(this).get(target, fieldName)
197 |
198 | fun String.invoke(target: Any?, methodName: String, vararg paramsPairs: Pair, Any?>) =
199 | Class.forName(this).invoke(target, methodName, *paramsPairs)
200 |
201 | fun String.invokeVoid(target: Any?, methodName: String, vararg paramsPairs: Pair, Any?>) =
202 | Class.forName(this).invokeVoid(target, methodName, *paramsPairs)
203 |
204 | fun String.newInstance(vararg paramsPairs: Pair, Any?>) =
205 | Class.forName(this).newInstance(*paramsPairs)
206 |
207 | fun KClass<*>.set(target: Any?, fieldName: String, value: Any?) = java.set(target, fieldName, value)
208 |
209 | fun KClass<*>.get(target: Any?, fieldName: String) = java.get(target, fieldName)
--------------------------------------------------------------------------------
/HookwormForAndroid/src/main/cpp/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | #include "riru.h"
7 |
8 | static char *jstring2char(JNIEnv *env, jstring target) {
9 | char *result = nullptr;
10 | if (target) {
11 | const char *targetChar = env->GetStringUTFChars(target, nullptr);
12 | if (targetChar != nullptr) {
13 | int len = strlen(targetChar);
14 | result = (char *) malloc((len + 1) * sizeof(char));
15 | if (result != nullptr) {
16 | memset(result, 0, len + 1);
17 | memcpy(result, targetChar, len);
18 | }
19 | env->ReleaseStringUTFChars(target, targetChar);
20 | }
21 | }
22 | return result;
23 | }
24 |
25 | static bool equals(const char *target1, const char *target2) {
26 | if (target1 == nullptr && target2 == nullptr) {
27 | return true;
28 | } else {
29 | if (target1 != nullptr && target2 != nullptr) {
30 | return strcmp(target1, target2) == 0;
31 | } else {
32 | return false;
33 | }
34 | }
35 | }
36 |
37 | static bool shouldInject(const char *current_process_name) {
38 | const char *target_process_name[] = PROCESS_NAME_ARRAY;
39 | int target_process_size = PROCESS_NAME_ARRAY_SIZE;
40 | if (target_process_size == 0) {
41 | return true;
42 | }
43 | for (auto &i : target_process_name) {
44 | if (equals(i, current_process_name)) {
45 | return true;
46 | }
47 | }
48 | return false;
49 | }
50 |
51 | static void inject_dex(JNIEnv *env, const char *dexPath, const char *optimizedDirectory,
52 | const char *mainClassName, const char *processName) {
53 |
54 | //get class: ClassLoader
55 | jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
56 |
57 | //get method: ClassLoader.getSystemClassLoader()
58 | jmethodID getSystemClassLoaderMethodID = env->GetStaticMethodID(
59 | classLoaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
60 |
61 | //invoke method: ClassLoader.getSystemClassLoader(), got ClassLoader object
62 | jobject systemClassLoader = env->CallStaticObjectMethod(classLoaderClass,
63 | getSystemClassLoaderMethodID);
64 |
65 | //get class: DexClassLoader
66 | jclass dexClassLoaderClass = env->FindClass("dalvik/system/DexClassLoader");
67 |
68 | //get constructor: DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
69 | jmethodID dexClassLoaderConstructorID = env->GetMethodID(dexClassLoaderClass, "",
70 | "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
71 |
72 | //new instance: DexClassLoader(dexPath, optimizedDirectory, null, systemClassLoader), got DexClassLoader object
73 | jobject dexClassLoader = env->NewObject(dexClassLoaderClass, dexClassLoaderConstructorID,
74 | env->NewStringUTF(dexPath),
75 | env->NewStringUTF(optimizedDirectory), NULL,
76 | systemClassLoader);
77 |
78 | //get method: DexClassLoader.loadClass(String name)
79 | jmethodID loadClassMethodID = env->GetMethodID(
80 | dexClassLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
81 |
82 | //invoke method: DexClassLoader.loadClass(mainClassName), got Main class
83 | auto mainClass = (jclass) env->CallObjectMethod(
84 | dexClassLoader, loadClassMethodID, env->NewStringUTF(mainClassName));
85 |
86 | //get method: Main.main(String processName)
87 | jmethodID mainMethodID = env->GetStaticMethodID(
88 | mainClass, "main", "(Ljava/lang/String;)V");
89 |
90 | //invoke method: Main.main(processName)
91 | env->CallStaticVoidMethod(mainClass, mainMethodID, env->NewStringUTF(processName));
92 | }
93 |
94 | static char *process_name = nullptr;
95 | static char *app_data_dir = nullptr;
96 |
97 | static void forkAndSpecializePre(
98 | JNIEnv *env, jclass clazz, jint *_uid, jint *gid, jintArray *gids, jint *runtimeFlags,
99 | jobjectArray *rlimits, jint *mountExternal, jstring *seInfo, jstring *niceName,
100 | jintArray *fdsToClose, jintArray *fdsToIgnore, jboolean *is_child_zygote,
101 | jstring *instructionSet, jstring *appDataDir, jboolean *isTopApp,
102 | jobjectArray *pkgDataInfoList,
103 | jobjectArray *whitelistedDataInfoList, jboolean *bindMountAppDataDirs,
104 | jboolean *bindMountAppStorageDirs) {
105 | char *current_process_name = jstring2char(env, *niceName);
106 | if (shouldInject(current_process_name)) {
107 | process_name = current_process_name;
108 | app_data_dir = jstring2char(env, *appDataDir);
109 | } else {
110 | if (process_name) {
111 | free(process_name);
112 | process_name = nullptr;
113 | }
114 | }
115 | }
116 |
117 | static void forkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) {
118 | if (res == 0) {
119 | //normal process
120 | if (process_name && app_data_dir) {
121 | inject_dex(env, DEX_PATH, app_data_dir, MAIN_CLASS, process_name);
122 | free(app_data_dir);
123 | app_data_dir = nullptr;
124 | free(process_name);
125 | process_name = nullptr;
126 | }
127 | }
128 | }
129 |
130 | extern "C" {
131 |
132 | int riru_api_version;
133 | RiruApiV9 *riru_api_v9;
134 |
135 | void *init(void *arg) {
136 | static int step = 0;
137 | step += 1;
138 |
139 | static void *_module;
140 |
141 | switch (step) {
142 | case 1: {
143 | auto core_max_api_version = *(int *) arg;
144 | riru_api_version =
145 | core_max_api_version <= RIRU_MODULE_API_VERSION ? core_max_api_version
146 | : RIRU_MODULE_API_VERSION;
147 | return &riru_api_version;
148 | }
149 | case 2: {
150 | switch (riru_api_version) {
151 | // RiruApiV10 and RiruModuleInfoV10 are equal to V9
152 | case 10:
153 | case 9: {
154 | riru_api_v9 = (RiruApiV9 *) arg;
155 |
156 | auto module = (RiruModuleInfoV9 *) malloc(sizeof(RiruModuleInfoV9));
157 | memset(module, 0, sizeof(RiruModuleInfoV9));
158 | _module = module;
159 |
160 | module->supportHide = true;
161 |
162 | module->version = RIRU_MODULE_VERSION;
163 | module->versionName = RIRU_MODULE_VERSION_NAME;
164 | module->forkAndSpecializePre = forkAndSpecializePre;
165 | module->forkAndSpecializePost = forkAndSpecializePost;
166 | return module;
167 | }
168 | default: {
169 | return nullptr;
170 | }
171 | }
172 | }
173 | case 3: {
174 | free(_module);
175 | return nullptr;
176 | }
177 | default: {
178 | return nullptr;
179 | }
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/HookwormForAndroid/src/main/java/com/wuyr/hookworm/extensions/HookwormExtensions.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNCHECKED_CAST")
2 |
3 | package com.wuyr.hookworm.extensions
4 |
5 | import android.annotation.SuppressLint
6 | import android.app.Activity
7 | import android.view.MotionEvent
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.TextView
11 | import androidx.core.view.forEach
12 | import com.wuyr.hookworm.utils.get
13 | import com.wuyr.hookworm.utils.invoke
14 | import kotlin.reflect.KClass
15 |
16 | /**
17 | * @author wuyr
18 | * @github https://github.com/wuyr/HookwormForAndroid
19 | * @since 2020-09-22 上午10:29
20 | */
21 |
22 | /**
23 | * 根据类型来查找所有对应的View实例
24 | *
25 | * @return 对应的View集合
26 | */
27 | fun Activity.findAllViewsByClass(clazz: KClass): List {
28 | val result = ArrayList()
29 | fun fill(view: View) {
30 | if (clazz.isInstance(view)) {
31 | result += view
32 | }
33 | if (view is ViewGroup) {
34 | view.forEach { fill(it) }
35 | }
36 | }
37 | fill(window.decorView)
38 | return result
39 | }
40 |
41 | /**
42 | * 根据类型来查找所有对应的View实例
43 | *
44 | * @return 对应的View集合
45 | */
46 | fun View.findAllViewsByClass(clazz: KClass): List {
47 | val result = ArrayList()
48 | fun fill(view: View) {
49 | if (clazz.isInstance(view)) {
50 | result += view
51 | }
52 | if (view is ViewGroup) {
53 | view.forEach { fill(it) }
54 | }
55 | }
56 | fill(this)
57 | return result
58 | }
59 |
60 | /**
61 | * 根据资源id名来查找View实例
62 | *
63 | * @param idName id名数组(即:可同时匹配多个id名)
64 | * @return 对应的View,找不到即为null
65 | */
66 | fun Activity.findViewByIDName(vararg idName: String): V? =
67 | findAllViewsByIDName(*idName).let {
68 | if (it.isEmpty()) null else it[0] as V?
69 | }
70 |
71 | /**
72 | * 根据资源id名来查找所有对应的View实例
73 | *
74 | * @param idName id名数组(即:可同时匹配多个id名)
75 | * @return 对应的View集合
76 | */
77 | fun Activity.findAllViewsByIDName(vararg idName: String): List {
78 | val result = ArrayList()
79 | fun fill(view: View) {
80 | idName.forEach { name ->
81 | if (name == runCatching { resources.getResourceEntryName(view.id) }.getOrNull()) result += view
82 | }
83 | if (view is ViewGroup) {
84 | view.forEach { fill(it) }
85 | }
86 | }
87 | fill(window.decorView)
88 | return result
89 | }
90 |
91 | /**
92 | * 根据资源id名来查找View实例
93 | *
94 | * @param idName id名数组(即:可同时匹配多个id名)
95 | * @return 对应的View,找不到即为null
96 | */
97 | fun View.findViewByIDName(vararg idName: String): V? =
98 | findAllViewsByIDName(*idName).let {
99 | if (it.isEmpty()) null else it[0] as V?
100 | }
101 |
102 | /**
103 | * 根据资源id名来查找所有对应的View实例
104 | *
105 | * @param idName id名数组(即:可同时匹配多个id名)
106 | * @return 对应的View集合
107 | */
108 | fun View.findAllViewsByIDName(vararg idName: String): List {
109 | val result = ArrayList()
110 | fun fill(view: View) {
111 | idName.forEach { name ->
112 | if (name == runCatching { resources.getResourceEntryName(view.id) }.getOrNull()) result += view
113 | }
114 | if (view is ViewGroup) {
115 | view.forEach { fill(it) }
116 | }
117 | }
118 | fill(this)
119 | return result
120 | }
121 |
122 | /**
123 | * 根据显示的文本来查找View实例
124 | *
125 | * @param textList 文本数组(即:可同时匹配多个文本)
126 | * @return 对应的View,找不到即为null
127 | */
128 | fun Activity.findViewByText(vararg textList: String): V? {
129 | fun find(view: View): View? {
130 | if (view is TextView) {
131 | return if (textList.any { it == view.text.toString() }) view else null
132 | } else {
133 | val nodeText = view.createAccessibilityNodeInfo().text?.toString()
134 | if (textList.any { it == nodeText }) {
135 | return view
136 | }
137 | if (view is ViewGroup) {
138 | view.forEach { child -> find(child)?.let { return it } }
139 | }
140 | }
141 | return null
142 | }
143 | return find(window.decorView) as V
144 | }
145 |
146 | /**
147 | * 根据显示的文本来查找所有对应的View实例
148 | *
149 | * @param textList 文本数组(即:可同时匹配多个文本)
150 | * @return 对应的View集合
151 | */
152 | fun Activity.findAllViewsByText(vararg textList: String): List {
153 | val result = ArrayList()
154 | fun fill(view: View) {
155 | if (view is TextView) {
156 | if (textList.any { it == view.text.toString() }) result += view
157 | } else {
158 | val nodeText = view.createAccessibilityNodeInfo().text?.toString()
159 | if (textList.any { it == nodeText }) result += view
160 | if (view is ViewGroup) {
161 | view.forEach { fill(it) }
162 | }
163 | }
164 | }
165 | fill(window.decorView)
166 | return result
167 | }
168 |
169 | /**
170 | * 根据显示的文本来查找View实例
171 | *
172 | * @param textList 文本数组(即:可同时匹配多个文本)
173 | * @return 对应的View,找不到即为null
174 | */
175 | fun View.findViewByText(vararg textList: String): V? {
176 | fun find(view: View): View? {
177 | if (view is TextView) {
178 | return if (textList.any { it == view.text.toString() }) view else null
179 | } else {
180 | val nodeText = view.createAccessibilityNodeInfo().text?.toString()
181 | if (textList.any { it == nodeText }) {
182 | return view
183 | }
184 | if (view is ViewGroup) {
185 | view.forEach { child -> find(child)?.let { return it } }
186 | }
187 | }
188 | return null
189 | }
190 | return find(this) as V
191 | }
192 |
193 | /**
194 | * 根据显示的文本来查找所有对应的View实例
195 | *
196 | * @param textList 文本数组(即:可同时匹配多个文本)
197 | * @return 对应的View集合
198 | */
199 | fun View.findAllViewsByText(vararg textList: String): List {
200 | val result = ArrayList()
201 | fun fill(view: View) {
202 | if (view is TextView) {
203 | if (textList.any { it == view.text.toString() }) result += view
204 | } else {
205 | val nodeText = view.createAccessibilityNodeInfo().text?.toString()
206 | if (textList.any { it == nodeText }) result += view
207 | if (view is ViewGroup) {
208 | view.forEach { fill(it) }
209 | }
210 | }
211 | }
212 | fill(this)
213 | return result
214 | }
215 |
216 | /**
217 | * 检测目标View是否包含某些文本
218 | *
219 | * @param targetText 要检测的文本集合(可同时检测多个)
220 | * @param recursive 是否递归查找
221 | * @return 有找到则返回true,反之false
222 | */
223 | fun View.containsText(vararg targetText: String, recursive: Boolean = false): Boolean {
224 | val identifier = if (this is TextView) {
225 | if (text.isNullOrEmpty()) {
226 | if (hint.isNullOrEmpty()) "" else hint.toString()
227 | } else text.toString()
228 | } else createAccessibilityNodeInfo().text?.toString() ?: ""
229 |
230 | targetText.forEach { t -> if (identifier.contains(t)) return true }
231 | if (recursive && this is ViewGroup) {
232 | forEach { if (it.containsText(*targetText, recursive = true)) return true }
233 | }
234 | return false
235 | }
236 |
237 | /**
238 | * 设置目标View的点击代理
239 | *
240 | * @param proxyListener 点击回调lambda,参数oldListener即为原来的OnClickListener实例
241 | */
242 | fun View.setOnClickProxy(proxyListener: (view: View, oldListener: View.OnClickListener?) -> Unit) {
243 | if (!isClickable) isClickable = true
244 | val oldListener = View::class.invoke(this, "getListenerInfo")?.let {
245 | it::class.get(it, "mOnClickListener")
246 | }
247 | setOnClickListener { proxyListener(it, oldListener) }
248 | }
249 |
250 | /**
251 | * 设置目标View的长按代理
252 | *
253 | * @param proxyListener 长按回调lambda,参数oldListener即为原来的OnLongClickListener实例
254 | */
255 | fun View.setOnLongClickProxy(proxyListener: (view: View, oldListener: View.OnLongClickListener?) -> Boolean) {
256 | if (!isLongClickable) isLongClickable = true
257 | val oldListener = View::class.invoke(this, "getListenerInfo")?.let {
258 | it::class.get(it, "mOnLongClickListener")
259 | }
260 | setOnLongClickListener { proxyListener(it, oldListener) }
261 | }
262 |
263 | /**
264 | * 设置目标View的触摸代理
265 | *
266 | * @param proxyListener 触摸回调lambda,参数oldListener即为原来的OnTouchListener实例
267 | */
268 | @SuppressLint("ClickableViewAccessibility")
269 | fun View.setOnTouchProxy(proxyListener: (view: View, event: MotionEvent, oldListener: View.OnTouchListener?) -> Boolean) {
270 | val oldListener = View::class.invoke(this, "getListenerInfo")?.let {
271 | it::class.get(it, "mOnTouchListener")
272 | }
273 | setOnTouchListener { v, event ->
274 | proxyListener(v, event, oldListener)
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/HookwormForAndroid/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ExternalNativeCmakeOptions
2 | import javassist.ClassPool
3 | import javassist.CtMethod
4 | import org.apache.tools.ant.filters.FixCrLfFilter
5 | import org.gradle.internal.os.OperatingSystem
6 | import org.gradle.kotlin.dsl.support.zipTo
7 | import java.security.MessageDigest
8 | import java.util.*
9 | import kotlin.concurrent.thread
10 |
11 | plugins {
12 | id("com.android.library")
13 | kotlin("android")
14 | kotlin("android.extensions")
15 | }
16 |
17 | buildscript {
18 | dependencies {
19 | classpath("javassist:javassist:3.9.0.GA")
20 | }
21 | }
22 |
23 | if (!file("module.properties").exists()) {
24 | error("Please copy \"module.properties.sample\" and rename to \"module.properties\" and fill in the module information!")
25 | }
26 |
27 | val versionsProp =
28 | Properties().apply { load(file("src/main/resource/versions.properties").inputStream()) }
29 | val moduleCompileSdkVersion = versionsProp.getProperty("compileSdkVersion").toInt()
30 | val moduleMinSdkVersion = versionsProp.getProperty("minSdkVersion").toInt()
31 | val moduleTargetSdkVersion = versionsProp.getProperty("targetSdkVersion").toInt()
32 | val cmakeVersion: String = versionsProp.getProperty("cmakeVersion")
33 | val corektxVersion: String = versionsProp.getProperty("core-ktxVersion")
34 | val maxRiruApiVersionCode = versionsProp.getProperty("maxRiruApiVersionCode").toInt()
35 | val minRiruApiVersionCode = versionsProp.getProperty("minRiruApiVersionCode").toInt()
36 | val minRiruApiVersionName: String = versionsProp.getProperty("minRiruApiVersionName")
37 |
38 | val moduleProp =
39 | Properties().apply { load(file("module.properties").inputStream()) }
40 | val moduleId: String = moduleProp.getProperty("moduleId")
41 | val moduleName: String = moduleProp.getProperty("moduleName")
42 | val moduleAuthor: String = moduleProp.getProperty("moduleAuthor")
43 | val moduleDescription: String = moduleProp.getProperty("moduleDescription")
44 | val moduleVersionName: String = moduleProp.getProperty("moduleVersionName")
45 | val moduleVersionCode: String = moduleProp.getProperty("moduleVersionCode")
46 | val moduleMainClass: String = moduleProp.getProperty("moduleMainClass")
47 | val targetProcessName: String = moduleProp.getProperty("targetProcessName")
48 | val libraryPath: String =
49 | moduleProp.getProperty("libraryPath").let { if (it.isEmpty()) moduleId else it }
50 | val automaticInstallation: Boolean =
51 | moduleProp.getProperty("automaticInstallation").let { it == "1" || it == "true" }
52 | val debug: Boolean = moduleProp.getProperty("debug").let { it == "1" || it == "true" }
53 | val moduleDexPath: String = "${if (debug) "data/local/tmp" else "system/framework"}/$moduleId.dex"
54 | val hookwormMainClass = "com/wuyr/hookworm/core/Main"
55 | val targetProcessNameList =
56 | targetProcessName.split(";").filter { it.isNotBlank() && it.isNotEmpty() }
57 | val processNameArray = targetProcessNameList.joinToString("\", \"", "{\"", "\"}")
58 | val processNameArraySize = targetProcessNameList.size
59 | val platform: OperatingSystem = OperatingSystem.current()
60 |
61 | checkProperties()
62 | rootProject.allprojects.forEach { it.buildDir.deleteRecursively() }
63 |
64 | android {
65 | compileSdkVersion(moduleCompileSdkVersion)
66 | defaultConfig {
67 | minSdkVersion(moduleMinSdkVersion)
68 | targetSdkVersion(moduleTargetSdkVersion)
69 | externalNativeBuild { cmake { addDefinitions() } }
70 | }
71 | buildTypes {
72 | named("release") {
73 | isMinifyEnabled = true
74 | proguardFiles(
75 | getDefaultProguardFile("proguard-android-optimize.txt"),
76 | "proguard-rules.pro"
77 | )
78 | }
79 | }
80 | compileOptions {
81 | sourceCompatibility = JavaVersion.VERSION_1_8
82 | targetCompatibility = JavaVersion.VERSION_1_8
83 | }
84 | kotlinOptions {
85 | jvmTarget = "1.8"
86 | }
87 | externalNativeBuild {
88 | cmake {
89 | path = file("src/main/cpp/CMakeLists.txt")
90 | version = cmakeVersion
91 | }
92 | }
93 |
94 | libraryVariants.all {
95 | if (name == "release") {
96 | sdkDirectory.buildModule()
97 | }
98 | }
99 | }
100 | dependencies {
101 | implementation("androidx.core:core-ktx:$corektxVersion")
102 | }
103 |
104 | fun checkProperties() {
105 | if (moduleId.isEmpty()) {
106 | error("moduleId must be fill out!")
107 | }
108 | if (moduleMainClass.isEmpty()) {
109 | error("moduleMainClass must be fill out!")
110 | }
111 | }
112 |
113 | fun ExternalNativeCmakeOptions.addDefinitions() = arguments(
114 | "-DDEX_PATH=\"$moduleDexPath\"",
115 | "-DMAIN_CLASS=\"$hookwormMainClass\"",
116 | "-DPROCESS_NAME_ARRAY=$processNameArray",
117 | "-DPROCESS_NAME_ARRAY_SIZE=$processNameArraySize",
118 | "-DMODULE_NAME=riru_$moduleId",
119 | "-DRIRU_MODULE_API_VERSION=$maxRiruApiVersionCode",
120 | "-DRIRU_MODULE_VERSION=$moduleVersionCode",
121 | "-DRIRU_MODULE_VERSION_NAME=\"$moduleVersionName\""
122 | )
123 |
124 | var magiskDir = ""
125 |
126 | fun File.buildModule() {
127 | initModuleInfo()
128 | val task = rootProject.project("app").tasks.find { it.name == "assemble" }
129 | ?: error("Please dependent on to an app module!")
130 | val buildDir = task.project.buildDir.also { it.deleteRecursively() }
131 | task.doLast {
132 | val zipPath = buildDir.resolve("intermediates/magisk/").apply {
133 | deleteRecursively()
134 | magiskDir = absolutePath
135 | mkdirs()
136 | }
137 | copy {
138 | into(zipPath)
139 | processResource()
140 | processScript()
141 | zipTree(File(buildDir, "outputs/apk/release/app-release-unsigned.apk")
142 | .also { if (!it.exists()) error("${it.name} not found!") }).let { apkFileTree ->
143 | processLibs(apkFileTree)
144 | processDex(apkFileTree)
145 | }
146 | }
147 | zipPath.apply {
148 | val prop = "name=$moduleName\n" +
149 | "version=$moduleVersionName\n" +
150 | "versionCode=$moduleVersionCode\n" +
151 | "author=$moduleAuthor\n" +
152 | "description=$moduleDescription"
153 | resolve("module.prop").writeText(
154 | "id=$moduleId\n$prop\ntarget_process_name=$targetProcessName\nlibrary_path=$libraryPath"
155 | )
156 | resolve("riru").apply {
157 | mkdir()
158 | resolve("module.prop.new").writeText(
159 | "$prop\nminApi=$minRiruApiVersionCode"
160 | )
161 | }
162 | resolve("extras.files").run {
163 | if (debug) {
164 | createNewFile()
165 | } else {
166 | writeText("${moduleDexPath}\n")
167 | }
168 | }
169 | fixLineBreaks()
170 | generateSHA256Sum()
171 | }
172 | buildDir.resolve("outputs/module").also { it.mkdirs() }.run {
173 | val moduleFile = File(this, "${moduleId}_$moduleVersionName.zip")
174 | zipTo(moduleFile, zipPath)
175 | if (automaticInstallation) {
176 | if (isDeviceConnected(this@buildModule)) {
177 | if (installModuleFailed(this@buildModule, moduleFile, zipPath)) {
178 | openInGUI()
179 | error("Module installation failed!")
180 | }
181 | } else {
182 | openInGUI()
183 | error("Device not connected or connected more than one device!")
184 | }
185 | } else {
186 | openInGUI()
187 | }
188 | }
189 | }
190 | }
191 |
192 | fun initModuleInfo() = (project.tasks.find { it.name == "compileReleaseJavaWithJavac" }
193 | ?: error("Task 'compileReleaseJavaWithJavac' not found!"))
194 | .doLast {
195 | val classPool = ClassPool.getDefault()
196 | val moduleInfoClassPath =
197 | buildDir.resolve("intermediates/javac/release/classes").absolutePath
198 | classPool.insertClassPath(moduleInfoClassPath)
199 | classPool.getCtClass("com.wuyr.hookworm.core.ModuleInfo").run {
200 | getDeclaredMethod("getMainClass").name = "getMainClassOld"
201 | addMethod(
202 | CtMethod.make("static String getMainClass(){ return \"$moduleMainClass\"; }", this)
203 | )
204 | getDeclaredMethod("getDexPath").name = "getDexPathOld"
205 | addMethod(
206 | CtMethod.make("static String getDexPath(){ return \"$moduleDexPath\"; }", this)
207 | )
208 |
209 | if (debug) {
210 | getDeclaredMethod("isDebug").name = "isDebugOld"
211 | addMethod(CtMethod.make("static boolean isDebug(){ return true; }", this))
212 | }
213 |
214 | val hasSOFile =
215 | (rootProject.project("app").tasks.find { it.name == "mergeReleaseNativeLibs" }
216 | ?: error("Task 'mergeReleaseNativeLibs' not found!")).run {
217 | this is com.android.build.gradle.internal.tasks.MergeNativeLibsTask && externalLibNativeLibs.files.any {
218 | it.isDirectory && it.list()?.isNotEmpty() == true
219 | }
220 | }
221 | if (hasSOFile) {
222 | getDeclaredMethod("hasSOFile").name = "hasSOFileOld"
223 | addMethod(CtMethod.make("static boolean hasSOFile(){ return true; }", this))
224 |
225 | getDeclaredMethod("getSOPath").name = "getSOPathOld"
226 | addMethod(
227 | CtMethod.make(
228 | "static String getSOPath(){ return \"$libraryPath\"; }",
229 | this
230 | )
231 | )
232 | }
233 | writeFile(moduleInfoClassPath)
234 | detach()
235 | }
236 | }
237 |
238 | fun CopySpec.processResource() = from(file("src/main/resource")) {
239 | exclude("riru.sh", "versions.properties")
240 | }
241 |
242 | fun CopySpec.processScript() =
243 | from(file("src/main/resource/riru.sh")) {
244 | filter { line ->
245 | line.replace("%%%RIRU_MODULE_ID%%%", moduleId)
246 | .replace("%%%RIRU_MIN_API_VERSION%%%", minRiruApiVersionCode.toString())
247 | .replace("%%%RIRU_MIN_VERSION_NAME%%%", minRiruApiVersionName)
248 | }
249 | filter(FixCrLfFilter::class.java)
250 | }
251 |
252 | fun CopySpec.processLibs(apkFileTree: FileTree) = from(apkFileTree) {
253 | include("lib/**")
254 | eachFile {
255 | path = path.replace("lib/armeabi-v7a", "system/lib")
256 | .replace("lib/armeabi", "system/lib")
257 | .replace("lib/x86_64", "system_x86/lib64")
258 | .replace("lib/x86", "system_x86/lib")
259 | .replace("lib/arm64-v8a", "system/lib64")
260 | }
261 | }
262 |
263 | fun CopySpec.processDex(apkFileTree: FileTree) = from(apkFileTree) {
264 | include("classes.dex")
265 | eachFile { path = moduleDexPath }
266 | }
267 |
268 | fun File.fixLineBreaks() {
269 | val ignoreDirs = arrayOf("system", "system_x86")
270 | val ignoreSuffix = arrayOf("so", "dex")
271 | fun walk(file: File) {
272 | if (file.isDirectory) {
273 | if (!ignoreDirs.contains(file.name)) {
274 | file.listFiles()?.forEach { if (!ignoreDirs.contains(it.name)) walk(it) }
275 | }
276 | } else {
277 | if (ignoreSuffix.none { file.absolutePath.endsWith(it) }) {
278 | file.readText().run {
279 | if (contains("\r\n")) file.writeText(replace("\r\n", "\n"))
280 | }
281 | }
282 | }
283 | }
284 | walk(this)
285 | }
286 |
287 | fun File.generateSHA256Sum() = fileTree(this).matching {
288 | exclude("customize.sh", "verify.sh", "META-INF")
289 | }.filter { it.isFile }.forEach { file ->
290 | File(file.absolutePath + ".sha256sum").writeText(
291 | MessageDigest.getInstance("SHA-256").digest(file.readBytes()).run {
292 | joinToString("") { String.format("%02x", it.toInt() and 0xFF) }
293 | })
294 | }
295 |
296 | fun File.openInGUI() = platform.runCatching {
297 | Runtime.getRuntime().exec(
298 | "${
299 | when {
300 | isWindows -> "explorer"
301 | isLinux -> "nautilus"
302 | isMacOsX -> "open"
303 | else -> ""
304 | }
305 | } ${this@openInGUI}"
306 | )
307 | }.isSuccess
308 |
309 | val platformArgs: Array
310 | get() = if (platform.isWindows) arrayOf("cmd", "/C") else arrayOf("/bin/bash", "-c")
311 |
312 | val File.adb: String get() = if (platform.isWindows) "SET Path=$this/platform-tools&&adb" else "$this/platform-tools/adb"
313 | val File.adbWithoutSetup: String get() = if (platform.isWindows) "adb" else "$this/platform-tools/adb"
314 |
315 | fun isDeviceConnected(sdkDirectory: File) =
316 | exec("${sdkDirectory.adb} devices").count { it == '\n' } == 3
317 |
318 | fun exec(command: String) = runCatching {
319 | Runtime.getRuntime().exec(arrayOf(*platformArgs, command)).run {
320 | waitFor()
321 | inputStream.reader().readText().also { destroy() }
322 | }
323 | }.getOrDefault("")
324 |
325 | fun installModuleFailed(sdkDirectory: File, moduleFile: File, zipPath: File) =
326 | if (debug && (exec("${sdkDirectory.adb} shell ls $moduleDexPath&&echo 1").contains("1"))) {
327 | if (exec("${sdkDirectory.adb} push $magiskDir/$moduleDexPath /data/local/tmp/&&echo 1".also { it.p() })
328 | .contains("1")
329 | ) {
330 | targetProcessNameList.forEach { processName ->
331 | exec("adb shell su -c killall $processName".also { it.p() })
332 | }
333 | "*********************************".p()
334 | "Module installation is completed.".p()
335 | "*********************************".p()
336 | false
337 | } else {
338 | "*********************************".p()
339 | "Module installation failed! Please try again.".p()
340 | "*********************************".p()
341 | true
342 | }
343 | } else {
344 | exec("${sdkDirectory.adb} shell rm /data/local/tmp/$moduleId.dex".also { it.p() })
345 | if (debug) {
346 | exec("${sdkDirectory.adb} push $magiskDir/$moduleDexPath /data/local/tmp/").p()
347 | }
348 | "${sdkDirectory.adb} push $moduleFile /data/local/tmp/&&${sdkDirectory.adbWithoutSetup} push $zipPath/META-INF/com/google/android/update-binary /data/local/tmp/&&${sdkDirectory.adbWithoutSetup} shell su".runCatching {
349 | Runtime.getRuntime().exec(arrayOf(*platformArgs, this)).run {
350 | thread(isDaemon = true) { readContentSafely(inputStream) { it.p() } }
351 | thread(isDaemon = true) { readContentSafely(errorStream) { it.p() } }
352 | outputStream.run {
353 | write("cd /data/local/tmp&&BOOTMODE=true sh update-binary dummy 1 ${moduleFile.name}&&rm update-binary&&rm ${moduleFile.name}&&reboot\n".toByteArray())
354 | flush()
355 | close()
356 | }
357 | waitFor()
358 | destroy()
359 | }
360 | false
361 | }.getOrDefault(true)
362 | }
363 |
364 | fun Process.readContentSafely(inputStream: java.io.InputStream, onReadLine: (String) -> Unit) {
365 | runCatching {
366 | inputStream.bufferedReader().use { reader ->
367 | var line = ""
368 | while (runCatching { exitValue() }.isFailure && reader.readLine()
369 | ?.also { line = it } != null
370 | ) {
371 | onReadLine(line)
372 | }
373 | }
374 | }
375 | }
376 |
377 | fun Any?.p() = println(this)
--------------------------------------------------------------------------------
/HookwormForAndroid/src/main/java/com/wuyr/hookworm/core/Hookworm.kt:
--------------------------------------------------------------------------------
1 | package com.wuyr.hookworm.core
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.app.Application
6 | import android.content.Context
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.os.Handler
10 | import android.os.Looper
11 | import android.util.Log
12 | import android.view.ContextThemeWrapper
13 | import android.view.View
14 | import com.wuyr.hookworm.extensions.PhoneLayoutInflater
15 | import com.wuyr.hookworm.extensions.SimpleActivityLifecycleCallbacks
16 | import com.wuyr.hookworm.utils.*
17 | import dalvik.system.DexFile
18 | import java.io.File
19 | import java.lang.reflect.Proxy
20 | import kotlin.concurrent.thread
21 |
22 | /**
23 | * @author wuyr
24 | * @github https://github.com/wuyr/HookwormForAndroid
25 | * @since 2020-09-20 下午3:07
26 | */
27 | @Suppress("unused")
28 | object Hookworm {
29 |
30 | /**
31 | * 是否转接插件Dex的ClassLoader
32 | * 如果引用到了目标应用的一些自定义类或接口(或第三方库),则需要转接,否则会报 [ClassNotFoundException]
33 | */
34 | @JvmStatic
35 | var transferClassLoader = false
36 | set(value) {
37 | if (field != value) {
38 | field = value
39 | if (isApplicationInitialized && value) {
40 | application.initClassLoader()
41 | }
42 | }
43 | }
44 |
45 | /**
46 | * 是否劫持全局的LayoutInflater
47 | */
48 | @JvmStatic
49 | var hookGlobalLayoutInflater = false
50 | set(value) {
51 | if (field != value) {
52 | field = value
53 | if (isApplicationInitialized && value) {
54 | initGlobalLayoutInflater()
55 | }
56 | }
57 | }
58 | private var globalLayoutInflater: PhoneLayoutInflater? = null
59 |
60 | /**
61 | * 进程Application实例
62 | */
63 | @JvmStatic
64 | lateinit var application: Application
65 | private set
66 |
67 | /**
68 | * 进程存活Activity实例集合
69 | */
70 | @JvmStatic
71 | val activities = HashMap()
72 |
73 | /**
74 | * 监听Application初始化
75 | */
76 | @JvmStatic
77 | var onApplicationInitializedListener: ((Application) -> Unit)? = null
78 | private var isApplicationInitialized = false
79 |
80 | private val activityLifecycleCallbackList =
81 | HashMap()
82 |
83 | private var postInflateListenerList =
84 | HashMap View?)?>()
85 |
86 | private val tempActivityLifecycleCallbacksList =
87 | ArrayList()
88 |
89 | /**
90 | * 拦截LayoutInflater布局加载
91 | *
92 | * @param className 对应的Activity类名(完整类名),空字符串则表示拦截所有Activity的布局加载
93 | * @param postInflateListener 用来接收回调的lambda,需返回加载后的View(可在返回前对这个View做手脚)
94 | *
95 | * Lambda参数
96 | * resourceId:正在加载的xml ID
97 | * resourceName:正在加载的xml名称
98 | * rootView:加载完成后的View
99 | */
100 | @JvmStatic
101 | fun registerPostInflateListener(
102 | className: String,
103 | postInflateListener: (resourceId: Int, resourceName: String, rootView: View?) -> View?
104 | ) {
105 | postInflateListenerList[className] = postInflateListener
106 | if (className.isEmpty() && hookGlobalLayoutInflater) {
107 | globalLayoutInflater?.postInflateListener = postInflateListener
108 | } else {
109 | activities[className]?.hookLayoutInflater(postInflateListener)
110 | }
111 | }
112 |
113 | /**
114 | * 取消拦截LayoutInflater布局加载
115 | *
116 | * @param className 对应的Activity类名(完整类名)
117 | */
118 | @JvmStatic
119 | fun unregisterPostInflateListener(className: String) {
120 | if (className.isEmpty() && hookGlobalLayoutInflater) {
121 | globalLayoutInflater?.postInflateListener = null
122 | }
123 | postInflateListenerList.remove(className)
124 | activities[className]?.let { activity ->
125 | val oldInflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
126 | if (oldInflater is PhoneLayoutInflater) {
127 | oldInflater.postInflateListener = null
128 | }
129 | }
130 | }
131 |
132 | /**
133 | * 监听Activity的生命周期
134 | *
135 | * @param className 对应的Activity类名(完整类名),空字符串表示监听所有Activity
136 | * @param callback ActivityLifecycleCallbacks实例
137 | */
138 | @JvmStatic
139 | fun registerActivityLifecycleCallbacks(
140 | className: String, callback: Application.ActivityLifecycleCallbacks
141 | ) {
142 | activityLifecycleCallbackList[className] = callback
143 | }
144 |
145 | /**
146 | * 取消监听Activity的生命周期
147 | *
148 | * @param className 对应的Activity类名(完整类名)
149 | */
150 | @JvmStatic
151 | fun unregisterActivityLifecycleCallbacks(className: String) =
152 | activityLifecycleCallbackList.remove(className)
153 |
154 | /**
155 | * 根据完整类名查找Activity对象
156 | *
157 | * @param className 对应的Activity类名(完整类名)
158 | * @return 对应的Activity实例,找不到即为null
159 | */
160 | @JvmStatic
161 | fun findActivityByClassName(className: String) = activities[className]
162 |
163 | private var initialized = false
164 |
165 | @SuppressLint("PrivateApi", "DiscouragedPrivateApi")
166 | @Suppress("ControlFlowWithEmptyBody")
167 | @JvmStatic
168 | fun init() {
169 | if (initialized) return
170 | throwReflectException = true
171 | initialized = true
172 | thread(isDaemon = true) {
173 | try {
174 | while (Looper.getMainLooper() == null) {
175 | }
176 | val currentApplicationMethod = Class.forName("android.app.ActivityThread")
177 | .getDeclaredMethod("currentApplication").also { it.isAccessible = true }
178 | while (currentApplicationMethod.invoke(null) == null) {
179 | }
180 | "android.app.ActivityThread".invoke(null, "currentApplication")!!.run {
181 | application = this
182 | if (ModuleInfo.isDebug() && Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
183 | hiddenApiExemptions()
184 | }
185 | initGlobalLayoutInflater()
186 | initClassLoader()
187 | initLibrary()
188 | Handler(Looper.getMainLooper()).post {
189 | onApplicationInitializedListener?.invoke(this)
190 | }
191 | registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks())
192 | tempActivityLifecycleCallbacksList.forEach { callback ->
193 | registerActivityLifecycleCallbacks(callback)
194 | }
195 | tempActivityLifecycleCallbacksList.clear()
196 | isApplicationInitialized = true
197 | }
198 | } catch (e: Exception) {
199 | Log.e(Main.TAG, Log.getStackTraceString(e))
200 | }
201 | }
202 | }
203 |
204 | private fun hiddenApiExemptions() {
205 | try {
206 | Impactor.hiddenApiExemptions()
207 | } catch (t: Throwable) {
208 | try {
209 | DexFile(ModuleInfo.getDexPath()).apply {
210 | loadClass(Impactor::class.java.canonicalName, null)
211 | .invokeVoid(null, "hiddenApiExemptions")
212 | close()
213 | }
214 | } catch (t: Throwable) {
215 | Log.e(Main.TAG, t.toString(), t)
216 | }
217 | }
218 | }
219 |
220 | private fun hookLayoutInflater(className: String, activity: Activity) {
221 | if (postInflateListenerList.isNotEmpty()) {
222 | (postInflateListenerList[className]
223 | ?: if (hookGlobalLayoutInflater) null else postInflateListenerList[""])
224 | ?.also { activity.hookLayoutInflater(it) }
225 | }
226 | }
227 |
228 | @SuppressLint("PrivateApi")
229 | private fun Activity.hookLayoutInflater(
230 | postInflateListener: (resourceId: Int, resourceName: String, rootView: View?) -> View?
231 | ) {
232 | val oldInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE)
233 | if (oldInflater is PhoneLayoutInflater) {
234 | oldInflater.postInflateListener = postInflateListener
235 | } else {
236 | val inflater = PhoneLayoutInflater(this).also {
237 | it.postInflateListener = postInflateListener
238 | }
239 | try {
240 | ContextThemeWrapper::class.set(this, "mInflater", inflater)
241 | } catch (e: Exception) {
242 | Log.e(Main.TAG, "hookLayoutInflater", e)
243 | }
244 | }
245 | val oldWindowInflater = window.layoutInflater
246 | if (oldWindowInflater is PhoneLayoutInflater) {
247 | oldWindowInflater.postInflateListener = postInflateListener
248 | } else {
249 | try {
250 | val phoneWindowClass =
251 | Class.forName("com.android.internal.policy.PhoneWindow")
252 | if (phoneWindowClass.isInstance(window)) {
253 | val inflater = PhoneLayoutInflater(this).also {
254 | it.postInflateListener = postInflateListener
255 | }
256 | phoneWindowClass.set(window, "mLayoutInflater", inflater)
257 | }
258 | } catch (e: Exception) {
259 | Log.e(Main.TAG, "hookLayoutInflater", e)
260 | }
261 | }
262 | }
263 |
264 | private fun Application.initClassLoader() {
265 | try {
266 | if (transferClassLoader) {
267 | ClassLoader::class.set(
268 | Hookworm::class.java.classLoader, "parent", this::class.java.classLoader
269 | )
270 | }
271 | } catch (e: Exception) {
272 | Log.e(Main.TAG, "initClassLoader", e)
273 | }
274 | }
275 |
276 | private fun initLibrary() {
277 | try {
278 | @Suppress("ConstantConditionIf")
279 | if (ModuleInfo.hasSOFile()) {
280 | "dalvik.system.BaseDexClassLoader".get(
281 | Hookworm::class.java.classLoader, "pathList"
282 | )?.let { pathList ->
283 | pathList::class.run {
284 | val newDirectories = get>(
285 | pathList, "nativeLibraryDirectories"
286 | )!! + pathList::class.get>(
287 | pathList, "systemNativeLibraryDirectories"
288 | )!! + File(application.applicationInfo.dataDir, ModuleInfo.getSOPath())
289 | set(
290 | pathList, "nativeLibraryPathElements",
291 | invoke(
292 | pathList,
293 | "makePathElements",
294 | List::class to newDirectories
295 | )
296 | )
297 | }
298 | }
299 | }
300 | } catch (e: Exception) {
301 | Log.e(Main.TAG, "initLibrary", e)
302 | }
303 | }
304 |
305 | @SuppressLint("PrivateApi")
306 | private fun initGlobalLayoutInflater() {
307 | try {
308 | if (hookGlobalLayoutInflater && globalLayoutInflater == null) {
309 | "android.app.SystemServiceRegistry".get>(
310 | null, "SYSTEM_SERVICE_FETCHERS"
311 | )?.let { fetchers ->
312 | fetchers[Context.LAYOUT_INFLATER_SERVICE]?.let { layoutInflaterFetcher ->
313 | fetchers[Context.LAYOUT_INFLATER_SERVICE] = Proxy.newProxyInstance(
314 | ClassLoader.getSystemClassLoader(),
315 | arrayOf(Class.forName("android.app.SystemServiceRegistry\$ServiceFetcher"))
316 | ) { _, method, args ->
317 | if (method.name == "getService") {
318 | method.invoke(layoutInflaterFetcher, *args ?: arrayOf())
319 | globalLayoutInflater
320 | ?: PhoneLayoutInflater(args[0] as Context?).also {
321 | globalLayoutInflater = it
322 | it.postInflateListener = postInflateListenerList[""]
323 | }
324 | } else method.invoke(layoutInflaterFetcher, *args ?: arrayOf())
325 | }
326 | }
327 | }
328 | }
329 | } catch (e: Exception) {
330 | Log.e(Main.TAG, "initGlobalLayoutInflater", e)
331 | }
332 | }
333 |
334 | /**
335 | * 监听[Activity.onCreate]方法回调
336 | * @param className 对应的Activity类名(完整类名)
337 | * @param callback Callback lambda
338 | *
339 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
340 | */
341 | fun registerOnActivityCreated(
342 | className: String, callback: (Activity, Bundle?) -> Unit
343 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
344 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
345 | if (activity::class.java.name == className) {
346 | callback(activity, savedInstanceState)
347 | }
348 | }
349 | }.apply {
350 | if (::application.isInitialized) {
351 | application.registerActivityLifecycleCallbacks(this)
352 | } else {
353 | tempActivityLifecycleCallbacksList += this
354 | }
355 | }
356 |
357 | /**
358 | * 监听[Activity.onStart]方法回调
359 | * @param className 对应的Activity类名(完整类名)
360 | * @param callback Callback lambda
361 | *
362 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
363 | */
364 | fun registerOnActivityStarted(
365 | className: String, callback: (Activity) -> Unit
366 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
367 | override fun onActivityStarted(activity: Activity) {
368 | if (activity::class.java.name == className) {
369 | callback(activity)
370 | }
371 | }
372 | }.apply {
373 | if (::application.isInitialized) {
374 | application.registerActivityLifecycleCallbacks(this)
375 | } else {
376 | tempActivityLifecycleCallbacksList += this
377 | }
378 | }
379 |
380 | /**
381 | * 监听[Activity.onResume]方法回调
382 | * @param className 对应的Activity类名(完整类名)
383 | * @param callback Callback lambda
384 | *
385 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
386 | */
387 | fun registerOnActivityResumed(
388 | className: String, callback: (Activity) -> Unit
389 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
390 | override fun onActivityResumed(activity: Activity) {
391 | if (activity::class.java.name == className) {
392 | callback(activity)
393 | }
394 | }
395 | }.apply {
396 | if (::application.isInitialized) {
397 | application.registerActivityLifecycleCallbacks(this)
398 | } else {
399 | tempActivityLifecycleCallbacksList += this
400 | }
401 | }
402 |
403 | /**
404 | * 监听[Activity.onPause]方法回调
405 | * @param className 对应的Activity类名(完整类名)
406 | * @param callback Callback lambda
407 | *
408 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
409 | */
410 | fun registerOnActivityPaused(
411 | className: String, callback: (Activity) -> Unit
412 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
413 | override fun onActivityPaused(activity: Activity) {
414 | if (activity::class.java.name == className) {
415 | callback(activity)
416 | }
417 | }
418 | }.apply {
419 | if (::application.isInitialized) {
420 | application.registerActivityLifecycleCallbacks(this)
421 | } else {
422 | tempActivityLifecycleCallbacksList += this
423 | }
424 | }
425 |
426 | /**
427 | * 监听[Activity.onStop]方法回调
428 | * @param className 对应的Activity类名(完整类名)
429 | * @param callback Callback lambda
430 | *
431 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
432 | */
433 | fun registerOnActivityStopped(
434 | className: String, callback: (Activity) -> Unit
435 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
436 | override fun onActivityStopped(activity: Activity) {
437 | if (activity::class.java.name == className) {
438 | callback(activity)
439 | }
440 | }
441 | }.apply {
442 | if (::application.isInitialized) {
443 | application.registerActivityLifecycleCallbacks(this)
444 | } else {
445 | tempActivityLifecycleCallbacksList += this
446 | }
447 | }
448 |
449 | /**
450 | * 监听[Activity.onDestroy]方法回调
451 | * @param className 对应的Activity类名(完整类名)
452 | * @param callback Callback lambda
453 | *
454 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
455 | */
456 | fun registerOnActivityDestroyed(
457 | className: String, callback: (Activity) -> Unit
458 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
459 | override fun onActivityDestroyed(activity: Activity) {
460 | if (activity::class.java.name == className) {
461 | callback(activity)
462 | }
463 | }
464 | }.apply {
465 | if (::application.isInitialized) {
466 | application.registerActivityLifecycleCallbacks(this)
467 | } else {
468 | tempActivityLifecycleCallbacksList += this
469 | }
470 | }
471 |
472 | /**
473 | * 监听[Activity.onSaveInstanceState]方法回调
474 | * @param className 对应的Activity类名(完整类名)
475 | * @param callback Callback lambda
476 | *
477 | * @return [Application.ActivityLifecycleCallbacks]实例,可用来取消注册
478 | */
479 | fun registerOnActivitySaveInstanceState(
480 | className: String, callback: (Activity, Bundle) -> Unit
481 | ): Application.ActivityLifecycleCallbacks = object : SimpleActivityLifecycleCallbacks() {
482 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
483 | if (activity::class.java.name == className) {
484 | callback(activity, outState)
485 | }
486 | }
487 | }.apply {
488 | if (::application.isInitialized) {
489 | application.registerActivityLifecycleCallbacks(this)
490 | } else {
491 | tempActivityLifecycleCallbacksList += this
492 | }
493 | }
494 |
495 | private class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
496 |
497 | override fun onActivityCreated(
498 | activity: Activity, savedInstanceState: Bundle?
499 | ) {
500 | val className = activity::class.java.name
501 | hookLayoutInflater(className, activity)
502 | activities[className] = activity
503 | activityLifecycleCallbackList[className]
504 | ?.onActivityCreated(activity, savedInstanceState)
505 | activityLifecycleCallbackList[""]
506 | ?.onActivityCreated(activity, savedInstanceState)
507 | }
508 |
509 | override fun onActivityStarted(activity: Activity) {
510 | activityLifecycleCallbackList[activity::class.java.name]
511 | ?.onActivityStarted(activity)
512 | activityLifecycleCallbackList[""]?.onActivityStarted(activity)
513 | }
514 |
515 | override fun onActivityResumed(activity: Activity) {
516 | activityLifecycleCallbackList[activity::class.java.name]
517 | ?.onActivityResumed(activity)
518 | activityLifecycleCallbackList[""]?.onActivityResumed(activity)
519 | }
520 |
521 | override fun onActivityPaused(activity: Activity) {
522 | activityLifecycleCallbackList[activity::class.java.name]
523 | ?.onActivityPaused(activity)
524 | activityLifecycleCallbackList[""]?.onActivityPaused(activity)
525 | }
526 |
527 | override fun onActivityStopped(activity: Activity) {
528 | activityLifecycleCallbackList[activity::class.java.name]
529 | ?.onActivityStopped(activity)
530 | activityLifecycleCallbackList[""]?.onActivityStopped(activity)
531 | }
532 |
533 | override fun onActivityDestroyed(activity: Activity) {
534 | val className = activity::class.java.name
535 | activities.remove(className)
536 | activityLifecycleCallbackList[className]
537 | ?.onActivityDestroyed(activity)
538 | activityLifecycleCallbackList[""]?.onActivityDestroyed(activity)
539 | }
540 |
541 | override fun onActivitySaveInstanceState(
542 | activity: Activity, outState: Bundle
543 | ) {
544 | activityLifecycleCallbackList[activity::class.java.name]
545 | ?.onActivitySaveInstanceState(activity, outState)
546 | activityLifecycleCallbackList[""]
547 | ?.onActivitySaveInstanceState(activity, outState)
548 | }
549 | }
550 | }
551 |
--------------------------------------------------------------------------------