From a879db99a17a2c858fefc8ad0b47383586f9d27a Mon Sep 17 00:00:00 2001 From: fankesyooni Date: Sat, 21 May 2022 01:49:19 +0800 Subject: [PATCH] Added YukiHookDataChannel function and PackageParam.onAppLifecycle function and refactor some code --- .../highcapable/yukihookapi/YukiHookAPI.kt | 21 ++ .../hook/factory/YukiHookFactory.kt | 26 +- .../yukihookapi/hook/param/PackageParam.kt | 97 +++++- .../xposed/application/ModuleApplication.kt | 4 + .../hook/xposed/bridge/YukiHookBridge.kt | 153 ++++++++-- .../xposed/channel/YukiHookDataChannel.kt | 285 ++++++++++++++++++ .../hook/xposed/channel/data/ChannelData.kt | 41 +++ 7 files changed, 585 insertions(+), 42 deletions(-) create mode 100644 yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/YukiHookDataChannel.kt create mode 100644 yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/data/ChannelData.kt diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt index 415661a0..6bbe7beb 100644 --- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/YukiHookAPI.kt @@ -44,7 +44,10 @@ import com.highcapable.yukihookapi.hook.param.PackageParam import com.highcapable.yukihookapi.hook.param.type.HookEntryType import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper import com.highcapable.yukihookapi.hook.store.MemberCacheStore +import com.highcapable.yukihookapi.hook.xposed.YukiHookModuleStatus +import com.highcapable.yukihookapi.hook.xposed.application.ModuleApplication import com.highcapable.yukihookapi.hook.xposed.bridge.YukiHookBridge +import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs import de.robv.android.xposed.XposedBridge import java.lang.reflect.Constructor @@ -151,6 +154,24 @@ object YukiHookAPI { */ var isEnableModuleAppResourcesCache = true + /** + * 是否启用 Hook Xposed 模块激活等状态功能 + * + * - 为原生支持 Xposed 模块激活状态检测 - 此功能默认启用 + * + * ❗关闭后你将不能再使用 [YukiHookModuleStatus] 中的功能 + */ + var isEnableHookModuleStatus = true + + /** + * 是否启用当前 Xposed 模块与宿主交互的 [YukiHookDataChannel] 功能 + * + * 请确保 Xposed 模块的 [Application] 继承于 [ModuleApplication] 才能有效 + * + * - 此功能默认启用 - 关闭后将不会在功能初始化的时候装载 [YukiHookDataChannel] + */ + var isEnableDataChannel = true + /** * 是否启用 [Member] 缓存功能 * diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt index 8e31c0a9..5ff3ffd7 100644 --- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt @@ -38,9 +38,9 @@ import com.highcapable.yukihookapi.YukiHookAPI import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker import com.highcapable.yukihookapi.hook.param.PackageParam import com.highcapable.yukihookapi.hook.xposed.YukiHookModuleStatus +import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit -import com.highcapable.yukihookapi.hook.xposed.proxy.YukiHookXposedInitProxy import java.io.BufferedReader import java.io.File import java.io.FileReader @@ -64,15 +64,6 @@ fun IYukiHookXposedInit.encase(initiate: PackageParam.() -> Unit) = YukiHookAPI. */ fun IYukiHookXposedInit.encase(vararg hooker: YukiBaseHooker) = YukiHookAPI.encase(hooker = hooker) -@Deprecated("请将接口转移到 IYukiHookXposedInit") -inline fun YukiHookXposedInitProxy.configs(initiate: YukiHookAPI.Configs.() -> Unit) = YukiHookAPI.configs(initiate) - -@Deprecated("请将接口转移到 IYukiHookXposedInit") -fun YukiHookXposedInitProxy.encase(initiate: PackageParam.() -> Unit) = YukiHookAPI.encase(initiate) - -@Deprecated("请将接口转移到 IYukiHookXposedInit") -fun YukiHookXposedInitProxy.encase(vararg hooker: YukiBaseHooker) = YukiHookAPI.encase(hooker = hooker) - /** * 获取模块的存取对象 * @return [YukiHookModulePrefs] @@ -84,23 +75,28 @@ val Context.modulePrefs get() = YukiHookModulePrefs.instance(context = this) * @param name 自定义 Sp 存储名称 * @return [YukiHookModulePrefs] */ -fun Context.modulePrefs(name: String) = YukiHookModulePrefs.instance(context = this).name(name) +fun Context.modulePrefs(name: String) = modulePrefs.name(name) + +/** + * 获取模块的数据通讯桥命名空间对象 + * @param packageName 目标 Hook APP (宿主) 包名 + * @return [YukiHookDataChannel.NameSpace] + */ +fun Context.dataChannel(packageName: String) = YukiHookDataChannel.instance().nameSpace(context = this, packageName) /** * 获取当前进程名称 * @return [String] */ val Context.processName - get() = try { + get() = runCatching { BufferedReader(FileReader(File("/proc/${Process.myPid()}/cmdline"))).let { buff -> buff.readLine().trim { it <= ' ' }.let { buff.close() it } } - } catch (_: Throwable) { - packageName ?: "" - } + }.getOrNull() ?: packageName ?: "" /** * 判断当前 Hook Framework 是否支持资源钩子(Resources Hook) diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt index a2cab925..42ff4cd1 100644 --- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/param/PackageParam.kt @@ -30,7 +30,9 @@ package com.highcapable.yukihookapi.hook.param import android.app.Application +import android.content.Context import android.content.pm.ApplicationInfo +import android.content.res.Configuration import android.content.res.Resources import com.highcapable.yukihookapi.YukiHookAPI import com.highcapable.yukihookapi.hook.bean.HookClass @@ -47,6 +49,7 @@ import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper import com.highcapable.yukihookapi.hook.xposed.bridge.YukiHookBridge import com.highcapable.yukihookapi.hook.xposed.bridge.dummy.YukiModuleResources import com.highcapable.yukihookapi.hook.xposed.bridge.dummy.YukiResources +import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel import com.highcapable.yukihookapi.hook.xposed.helper.YukiHookAppHelper import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs @@ -96,11 +99,11 @@ open class PackageParam internal constructor(@PublishedApi internal var wrapper: /** * 获取当前 Hook APP 的 [Application] 实例 * - * - ❗首次装载可能是空的 - 请延迟一段时间再获取 + * - ❗首次装载可能是空的 - 请延迟一段时间再获取或通过设置 [onAppLifecycle] 监听来完成 * @return [Application] * @throws IllegalStateException 如果 [Application] 是空的 */ - val appContext get() = YukiHookAppHelper.currentApplication() ?: error("PackageParam got null appContext") + val appContext get() = YukiHookBridge.hostApplication ?: YukiHookAppHelper.currentApplication() ?: error("PackageParam got null appContext") /** * 获取当前 Hook APP 的 Resources @@ -162,12 +165,16 @@ open class PackageParam internal constructor(@PublishedApi internal var wrapper: fun prefs(name: String) = prefs.name(name) /** - * 获得当前 Hook APP 的 [YukiResources] 对象 + * 获得当前使用的数据通讯桥命名空间对象 * - * 请调用 [HookResources.hook] 方法开始 Hook - * @return [HookResources] + * - ❗作为 Hook API 装载时无法使用 - 会抛出异常 + * @return [YukiHookDataChannel.NameSpace] + * @throws IllegalStateException 如果在 [HookEntryType.ZYGOTE] 装载 */ - fun resources() = HookResources(wrapper?.appResources) + val dataChannel + get() = if (wrapper?.type != HookEntryType.ZYGOTE) + YukiHookDataChannel.instance().nameSpace(packageName = packageName) + else error("YukiHookDataChannel cannot used in zygote") /** * 赋值并克隆另一个 [PackageParam] @@ -177,9 +184,25 @@ open class PackageParam internal constructor(@PublishedApi internal var wrapper: this.wrapper = anotherParam.wrapper } + /** + * 获得当前 Hook APP 的 [YukiResources] 对象 + * + * 请调用 [HookResources.hook] 方法开始 Hook + * @return [HookResources] + */ + fun resources() = HookResources(wrapper?.appResources) + /** 刷新当前 Xposed 模块自身 [Resources] */ fun refreshModuleAppResources() = YukiHookBridge.refreshModuleAppResources() + /** + * 监听当前 Hook APP 生命周期装载事件 + * + * - ❗在 [loadZygote] 中不会被装载 - 仅会在 [loadSystem]、[loadApp] 中装载 + * @param initiate 方法体 + */ + inline fun onAppLifecycle(initiate: AppLifecycle.() -> Unit) = AppLifecycle().apply(initiate).build() + /** * 装载并 Hook 指定、全部包名的 APP * @@ -378,5 +401,67 @@ open class PackageParam internal constructor(@PublishedApi internal var wrapper: HookClass(name = name, throwable = throwable ?: e) } + /** + * 当前 Hook APP 的生命周期实例处理类 + * + * - ❗请使用 [onAppLifecycle] 方法来获取 [AppLifecycle] + */ + inner class AppLifecycle @PublishedApi internal constructor() { + + /** + * 监听当前 Hook APP 装载 [Application.attachBaseContext] + * @param initiate 回调 - ([Context] baseContext,[Boolean] 是否已执行 super) + */ + fun attachBaseContext(initiate: (baseContext: Context, hasCalledSuper: Boolean) -> Unit) { + YukiHookBridge.AppLifecycleCallback.attachBaseContextCallback = initiate + } + + /** + * 监听当前 Hook APP 装载 [Application.onCreate] + * @param initiate 方法体 + */ + fun onCreate(initiate: Application.() -> Unit) { + YukiHookBridge.AppLifecycleCallback.onCreateCallback = initiate + } + + /** + * 监听当前 Hook APP 装载 [Application.onTerminate] + * @param initiate 方法体 + */ + fun onTerminate(initiate: Application.() -> Unit) { + YukiHookBridge.AppLifecycleCallback.onTerminateCallback = initiate + } + + /** + * 监听当前 Hook APP 装载 [Application.onLowMemory] + * @param initiate 方法体 + */ + fun onLowMemory(initiate: Application.() -> Unit) { + YukiHookBridge.AppLifecycleCallback.onLowMemoryCallback = initiate + } + + /** + * 监听当前 Hook APP 装载 [Application.onTrimMemory] + * @param initiate 回调 - ([Application] 当前实例,[Int] 类型) + */ + fun onTrimMemory(initiate: (self: Application, level: Int) -> Unit) { + YukiHookBridge.AppLifecycleCallback.onTrimMemoryCallback = initiate + } + + /** + * 监听当前 Hook APP 装载 [Application.onConfigurationChanged] + * @param initiate 回调 - ([Application] 当前实例,[Configuration] 配置实例) + */ + fun onConfigurationChanged(initiate: (self: Application, config: Configuration) -> Unit) { + YukiHookBridge.AppLifecycleCallback.onConfigurationChangedCallback = initiate + } + + /** 设置创建生命周期监听回调 */ + @PublishedApi + internal fun build() { + YukiHookBridge.AppLifecycleCallback.isCallbackSetUp = true + } + } + override fun toString() = "PackageParam by $wrapper" } \ No newline at end of file diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/application/ModuleApplication.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/application/ModuleApplication.kt index 4ca0a871..ddf12e1f 100644 --- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/application/ModuleApplication.kt +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/application/ModuleApplication.kt @@ -32,6 +32,7 @@ import android.content.Context import com.highcapable.yukihookapi.YukiHookAPI import com.highcapable.yukihookapi.hook.xposed.application.ModuleApplication.Companion.appContext import com.highcapable.yukihookapi.hook.xposed.application.inject.ModuleApplication_Injector +import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit import me.weishu.reflection.Reflection @@ -48,6 +49,8 @@ import me.weishu.reflection.Reflection * * - 在模块与宿主中装载 [YukiHookAPI.Configs] 以确保 [YukiHookAPI.Configs.debugTag] 不需要重复定义 * + * - 在模块与宿主中使用 [YukiHookDataChannel] 进行通讯 + * * - 在模块中使用系统隐藏 API - 核心技术引用了开源项目 [FreeReflection](https://github.com/tiann/FreeReflection) * * 详情请参考 [ModuleApplication](https://fankes.github.io/YukiHookAPI/#/api/document?id=moduleapplication-class) @@ -76,6 +79,7 @@ open class ModuleApplication : Application() { super.onCreate() currentContext = this callApiInit() + YukiHookDataChannel.instance().register(context = this) } /** 调用入口类的 [IYukiHookXposedInit.onInit] 方法 */ diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/bridge/YukiHookBridge.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/bridge/YukiHookBridge.kt index 69bf15b8..4ad3656a 100644 --- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/bridge/YukiHookBridge.kt +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/bridge/YukiHookBridge.kt @@ -29,7 +29,10 @@ package com.highcapable.yukihookapi.hook.xposed.bridge +import android.app.Application +import android.content.Context import android.content.pm.ApplicationInfo +import android.content.res.Configuration import android.content.res.Resources import com.highcapable.yukihookapi.YukiHookAPI import com.highcapable.yukihookapi.annotation.YukiGenerateApi @@ -38,10 +41,16 @@ import com.highcapable.yukihookapi.hook.param.PackageParam import com.highcapable.yukihookapi.hook.param.type.HookEntryType import com.highcapable.yukihookapi.hook.param.wrapper.HookParamWrapper import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper +import com.highcapable.yukihookapi.hook.type.android.ApplicationClass +import com.highcapable.yukihookapi.hook.type.android.ConfigurationClass +import com.highcapable.yukihookapi.hook.type.android.ContextClass +import com.highcapable.yukihookapi.hook.type.android.InstrumentationClass +import com.highcapable.yukihookapi.hook.type.java.IntType import com.highcapable.yukihookapi.hook.xposed.YukiHookModuleStatus import com.highcapable.yukihookapi.hook.xposed.bridge.dummy.YukiModuleResources import com.highcapable.yukihookapi.hook.xposed.bridge.dummy.YukiResources import com.highcapable.yukihookapi.hook.xposed.bridge.inject.YukiHookBridge_Injector +import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel import de.robv.android.xposed.* import de.robv.android.xposed.callbacks.XC_InitPackageResources import de.robv.android.xposed.callbacks.XC_LoadPackage @@ -66,6 +75,9 @@ object YukiHookBridge { /** Xposed 是否装载完成 */ private var isXposedInitialized = false + /** [YukiHookDataChannel] 是否已经注册 */ + private var isDataChannelRegister = false + /** 已在 [PackageParam] 中被装载的 APP 包名 */ private val loadedPackageNames = HashSet() @@ -75,6 +87,13 @@ object YukiHookBridge { /** 当前 [PackageParam] 方法体回调 */ internal var packageParamCallback: (PackageParam.() -> Unit)? = null + /** + * 当前 Hook APP (宿主) 的全局生命周期 [Application] + * + * 需要 [YukiHookAPI.Configs.isEnableDataChannel] 或 [AppLifecycleCallback.isCallbackSetUp] 才会生效 + */ + internal var hostApplication: Application? = null + /** 当前 Xposed 模块自身 APK 路径 */ internal var moduleAppFilePath = "" @@ -194,6 +213,67 @@ object YukiHookBridge { } } + /** + * 注入当前 Hook APP (宿主) 全局生命周期 + * @param packageName 包名 + */ + private fun registerToAppLifecycle(packageName: String) { + /** Hook [Application] 装载方法 */ + runCatching { + if (AppLifecycleCallback.isCallbackSetUp) { + Hooker.hookMethod(Hooker.findMethod(ApplicationClass, name = "attach", ContextClass), object : Hooker.YukiMemberHook() { + override fun beforeHookedMember(wrapper: HookParamWrapper) { + (wrapper.args?.get(0) as? Context?)?.also { AppLifecycleCallback.attachBaseContextCallback?.invoke(it, false) } + } + + override fun afterHookedMember(wrapper: HookParamWrapper) { + (wrapper.args?.get(0) as? Context?)?.also { AppLifecycleCallback.attachBaseContextCallback?.invoke(it, true) } + } + }) + Hooker.hookMethod(Hooker.findMethod(ApplicationClass, name = "onTerminate"), object : Hooker.YukiMemberHook() { + override fun afterHookedMember(wrapper: HookParamWrapper) { + (wrapper.instance as? Application?)?.also { AppLifecycleCallback.onTerminateCallback?.invoke(it) } + } + }) + Hooker.hookMethod(Hooker.findMethod(ApplicationClass, name = "onLowMemory"), object : Hooker.YukiMemberHook() { + override fun afterHookedMember(wrapper: HookParamWrapper) { + (wrapper.instance as? Application?)?.also { AppLifecycleCallback.onLowMemoryCallback?.invoke(it) } + } + }) + Hooker.hookMethod(Hooker.findMethod(ApplicationClass, name = "onTrimMemory", IntType), object : Hooker.YukiMemberHook() { + override fun afterHookedMember(wrapper: HookParamWrapper) { + val self = wrapper.instance as? Application? ?: return + val type = wrapper.args?.get(0) as? Int ?: return + AppLifecycleCallback.onTrimMemoryCallback?.invoke(self, type) + } + }) + Hooker.hookMethod( + Hooker.findMethod(ApplicationClass, name = "onConfigurationChanged", ConfigurationClass), + object : Hooker.YukiMemberHook() { + override fun afterHookedMember(wrapper: HookParamWrapper) { + val self = wrapper.instance as? Application? ?: return + val config = wrapper.args?.get(0) as? Configuration? ?: return + AppLifecycleCallback.onConfigurationChangedCallback?.invoke(self, config) + } + }) + } + if (YukiHookAPI.Configs.isEnableDataChannel || AppLifecycleCallback.isCallbackSetUp) + Hooker.hookMethod( + Hooker.findMethod(InstrumentationClass, name = "callApplicationOnCreate", ApplicationClass), + object : Hooker.YukiMemberHook() { + override fun afterHookedMember(wrapper: HookParamWrapper) { + (wrapper.args?.get(0) as? Application)?.also { + hostApplication = it + AppLifecycleCallback.onCreateCallback?.invoke(it) + if (isDataChannelRegister) return + isDataChannelRegister = true + runCatching { YukiHookDataChannel.instance().register(it, packageName) } + } + } + }) + } + } + /** 刷新当前 Xposed 模块自身 [Resources] */ internal fun refreshModuleAppResources() { dynamicModuleAppResources?.let { moduleAppResources = it } @@ -208,26 +288,27 @@ object YukiHookBridge { */ @YukiGenerateApi fun hookModuleAppStatus(classLoader: ClassLoader?, isHookResourcesStatus: Boolean = false) { - Hooker.findClass(classLoader, YukiHookModuleStatus::class.java).also { statusClass -> - if (isHookResourcesStatus.not()) { - Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.IS_ACTIVE_METHOD_NAME), - object : Hooker.YukiMemberReplacement() { - override fun replaceHookedMember(wrapper: HookParamWrapper) = true - }) - Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.GET_XPOSED_TAG_METHOD_NAME), - object : Hooker.YukiMemberReplacement() { - override fun replaceHookedMember(wrapper: HookParamWrapper) = executorName - }) - Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.GET_XPOSED_VERSION_METHOD_NAME), - object : Hooker.YukiMemberReplacement() { - override fun replaceHookedMember(wrapper: HookParamWrapper) = executorVersion - }) - } else - Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.HAS_RESOURCES_HOOK_METHOD_NAME), - object : Hooker.YukiMemberReplacement() { - override fun replaceHookedMember(wrapper: HookParamWrapper) = true - }) - } + if (YukiHookAPI.Configs.isEnableHookModuleStatus) + Hooker.findClass(classLoader, YukiHookModuleStatus::class.java).also { statusClass -> + if (isHookResourcesStatus.not()) { + Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.IS_ACTIVE_METHOD_NAME), + object : Hooker.YukiMemberReplacement() { + override fun replaceHookedMember(wrapper: HookParamWrapper) = true + }) + Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.GET_XPOSED_TAG_METHOD_NAME), + object : Hooker.YukiMemberReplacement() { + override fun replaceHookedMember(wrapper: HookParamWrapper) = executorName + }) + Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.GET_XPOSED_VERSION_METHOD_NAME), + object : Hooker.YukiMemberReplacement() { + override fun replaceHookedMember(wrapper: HookParamWrapper) = executorVersion + }) + } else + Hooker.hookMethod(Hooker.findMethod(statusClass, YukiHookModuleStatus.HAS_RESOURCES_HOOK_METHOD_NAME), + object : Hooker.YukiMemberReplacement() { + override fun replaceHookedMember(wrapper: HookParamWrapper) = true + }) + } } /** @@ -285,7 +366,37 @@ object YukiHookBridge { assignWrapper(HookEntryType.RESOURCES, resparam.packageName, appResources = YukiResources.createFromXResources(resparam.res)) else null else -> null - }?.also { YukiHookAPI.onXposedLoaded(it) } + }?.also { + YukiHookAPI.onXposedLoaded(it) + if (it.type == HookEntryType.PACKAGE) registerToAppLifecycle(it.packageName) + } + } + + /** + * 当前 Hook APP (宿主) 的生命周期回调处理类 + */ + internal object AppLifecycleCallback { + + /** 是否已设置回调 */ + internal var isCallbackSetUp = false + + /** [Application.attachBaseContext] 回调 */ + internal var attachBaseContextCallback: ((Context, Boolean) -> Unit)? = null + + /** [Application.onCreate] 回调 */ + internal var onCreateCallback: (Application.() -> Unit)? = null + + /** [Application.onTerminate] 回调 */ + internal var onTerminateCallback: (Application.() -> Unit)? = null + + /** [Application.onLowMemory] 回调 */ + internal var onLowMemoryCallback: (Application.() -> Unit)? = null + + /** [Application.onTrimMemory] 回调 */ + internal var onTrimMemoryCallback: ((Application, Int) -> Unit)? = null + + /** [Application.onConfigurationChanged] 回调 */ + internal var onConfigurationChangedCallback: ((Application, Configuration) -> Unit)? = null } /** diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/YukiHookDataChannel.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/YukiHookDataChannel.kt new file mode 100644 index 00000000..1178862b --- /dev/null +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/YukiHookDataChannel.kt @@ -0,0 +1,285 @@ +/* + * YukiHookAPI - An efficient Kotlin version of the Xposed Hook API. + * Copyright (C) 2019-2022 HighCapable + * https://github.com/fankes/YukiHookAPI + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This file is Created by fankes on 2022/5/16. + */ +@file:Suppress("StaticFieldLeak", "UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate") + +package com.highcapable.yukihookapi.hook.xposed.channel + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.os.Parcelable +import com.highcapable.yukihookapi.YukiHookAPI +import com.highcapable.yukihookapi.hook.utils.putIfAbsentCompat +import com.highcapable.yukihookapi.hook.xposed.application.ModuleApplication +import com.highcapable.yukihookapi.hook.xposed.bridge.YukiHookBridge +import com.highcapable.yukihookapi.hook.xposed.channel.data.ChannelData +import java.io.Serializable + +/** + * 实现 Xposed 模块的数据通讯桥 + * + * 通过模块与宿主相互注册 [BroadcastReceiver] 来实现数据的交互 + * + * 模块需要将 [Application] 继承于 [ModuleApplication] 来实现此功能 + * + * - ❗模块与宿主需要保持存活状态 - 否则无法建立通讯 + * + * - 详情请参考 [API 文档 - YukiHookDataChannel](https://fankes.github.io/YukiHookAPI/#/api/document?id=yukihookdatachannel-class) + */ +class YukiHookDataChannel private constructor() { + + internal companion object { + + /** 是否为 Xposed 环境 */ + private val isXposedEnvironment = YukiHookBridge.hasXposedBridge + + /** 模块构建版本号获取标签 */ + private const val GET_MODULE_GENERATED_VERSION = "module_generated_version_get" + + /** 模块构建版本号结果标签 */ + private const val RESULT_MODULE_GENERATED_VERSION = "module_generated_version_result" + + /** 仅监听结果键值 */ + private const val VALUE_WAIT_FOR_LISTENER = "wait_for_listener_value" + + /** 当前 [YukiHookDataChannel] 单例 */ + private var instance: YukiHookDataChannel? = null + + /** + * 获取 [YukiHookDataChannel] 单例 + * @return [YukiHookDataChannel] + */ + internal fun instance() = instance ?: YukiHookDataChannel().apply { instance = this } + } + + /** 注册广播回调数组 */ + private var receiverCallbacks = HashMap Unit)>() + + /** 当前注册广播的 [Context] */ + private var receiverContext: Context? = null + + /** 广播接收器 */ + private val handlerReceiver by lazy { + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null) return + intent.action?.also { action -> receiverCallbacks.takeIf { it.isNotEmpty() }?.forEach { (_, it) -> it(action, intent) } } + } + } + } + + /** 检查 API 装载状态 */ + private fun checkApi() { + if (YukiHookAPI.isLoadedFromBaseContext) error("YukiHookDataChannel not allowed in Custom Hook API") + if (YukiHookBridge.hasXposedBridge && YukiHookBridge.modulePackageName.isBlank()) + error("Xposed modulePackageName load failed, please reset and rebuild it") + } + + /** + * 获取宿主广播 Action 名称 + * @param packageName 包名 + * @return [String] + */ + private fun hostActionName(packageName: String) = "yukihookapi.intent.action.HOST_DATA_CHANNEL_${packageName.trim().hashCode()}" + + /** + * 获取模块广播 Action 名称 + * @param context 实例 - 默认空 + * @return [String] + */ + private fun moduleActionName(context: Context? = null) = "yukihookapi.intent.action.MODULE_DATA_CHANNEL_${ + YukiHookBridge.modulePackageName.ifBlank { context?.packageName ?: "" }.trim().hashCode() + }" + + /** + * 注册广播 + * @param context 目标 Hook APP (宿主) 或模块全局上下文实例 - 为空停止注册 + * @param packageName 包名 - 为空获取 [context] 的 [Context.getPackageName] + */ + internal fun register(context: Context?, packageName: String = context?.packageName ?: "") { + if (context == null) return + if (YukiHookAPI.Configs.isEnableDataChannel.not() || receiverContext != null) return + receiverContext = context + context.registerReceiver( + handlerReceiver, IntentFilter().apply { + addAction(if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) + } + ) + /** 注册监听模块与宿主的版本是否匹配 */ + nameSpace(context, packageName).wait(GET_MODULE_GENERATED_VERSION) { fromPackageName -> + nameSpace(context, fromPackageName).put(RESULT_MODULE_GENERATED_VERSION, YukiHookBridge.moduleGeneratedVersion) + } + } + + /** + * 获取命名空间 + * @param context 上下文实例 + * @param packageName 目标 Hook APP (宿主) 的包名 + * @return [NameSpace] + */ + internal fun nameSpace(context: Context? = null, packageName: String): NameSpace { + checkApi() + return NameSpace(context = context ?: receiverContext, packageName) + } + + /** + * [YukiHookDataChannel] 命名空间 + * + * - ❗请使用 [nameSpace] 方法来获取 [NameSpace] + * @param context 上下文实例 + * @param packageName 目标 Hook APP (宿主) 的包名 + */ + inner class NameSpace internal constructor(private val context: Context?, private val packageName: String) { + + /** + * 创建一个调用空间 + * @param initiate 方法体 + * @return [NameSpace] 可继续向下监听 + */ + inline fun with(initiate: NameSpace.() -> Unit) = apply(initiate) + + /** + * 发送键值数据 + * @param key 键值名称 + * @param value 键值数据 + */ + fun put(key: String, value: T) = pushReceiver(ChannelData(key, value)) + + /** + * 发送键值数据 + * @param data 键值实例 + * @param value 键值数据 - 未指定为 [ChannelData.value] + */ + fun put(data: ChannelData, value: T? = data.value) = pushReceiver(ChannelData(data.key, value)) + + /** + * 发送键值数据 + * @param data 键值实例 + */ + fun put(vararg data: ChannelData<*>) = data.takeIf { it.isNotEmpty() }?.let { pushReceiver(*it) } + + /** + * 仅发送键值监听 - 使用默认值 [VALUE_WAIT_FOR_LISTENER] 发送键值数据 + * @param key 键值名称 + */ + fun put(key: String) = pushReceiver(ChannelData(key, VALUE_WAIT_FOR_LISTENER)) + + /** + * 获取键值数据 + * @param key 键值名称 + * @param value 默认值 - 不填则在值为空的时候不回调 [result] + * @param result 回调结果数据 + */ + fun wait(key: String, value: T? = null, result: (value: T) -> Unit) { + receiverCallbacks.putIfAbsentCompat(key) { action, intent -> + if (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) + (intent.extras?.get(key) as? T?).also { if (it != null || value != null) (it ?: value)?.let { e -> result(e) } } + } + } + + /** + * 获取键值数据 + * @param data 键值实例 + * @param value 默认值 - 未指定为 [ChannelData.value] + * @param result 回调结果数据 + */ + fun wait(data: ChannelData, value: T? = data.value, result: (value: T) -> Unit) { + receiverCallbacks.putIfAbsentCompat(data.key) { action, intent -> + if (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) + (intent.extras?.get(data.key) as? T?).also { if (it != null || value != null) (it ?: value)?.let { e -> result(e) } } + } + } + + /** + * 仅获取监听结果 - 不获取键值数据 + * + * - ❗仅限使用 [VALUE_WAIT_FOR_LISTENER] 发送的监听才能被接收 + * @param key 键值名称 + * @param result 回调结果 + */ + fun wait(key: String, result: () -> Unit) { + receiverCallbacks.putIfAbsentCompat(key) { action, intent -> + if (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) + if (intent.getStringExtra(key) == VALUE_WAIT_FOR_LISTENER) result() + } + } + + /** + * 获取模块与宿主的版本是否匹配 + * + * 通过此方法可原生判断 Xposed 模块更新后宿主并未重新装载造成两者不匹配的情况 + * @param result 回调是否匹配 + */ + fun checkingVersionEquals(result: (Boolean) -> Unit) { + wait(RESULT_MODULE_GENERATED_VERSION) { result(it == YukiHookBridge.moduleGeneratedVersion) } + put(GET_MODULE_GENERATED_VERSION, packageName) + } + + /** + * 发送广播 + * @param data 键值数据 + */ + private fun pushReceiver(vararg data: ChannelData<*>) { + if (YukiHookAPI.Configs.isEnableDataChannel.not()) return + context?.sendBroadcast(Intent().apply { + action = if (isXposedEnvironment) moduleActionName() else hostActionName(packageName) + data.takeIf { it.isNotEmpty() }?.forEach { + when (it.value) { + null -> Unit + is Bundle -> putExtra(it.key, it.value as Bundle) + is Parcelable -> putExtra(it.key, it.value as Parcelable) + is Serializable -> putExtra(it.key, it.value as Serializable) + is Array<*> -> putExtra(it.key, it.value as Array<*>) + is Boolean -> putExtra(it.key, it.value as Boolean) + is BooleanArray -> putExtra(it.key, it.value as BooleanArray) + is Byte -> putExtra(it.key, it.value as Byte) + is ByteArray -> putExtra(it.key, it.value as ByteArray) + is Char -> putExtra(it.key, it.value as Char) + is CharArray -> putExtra(it.key, it.value as CharArray) + is CharSequence -> putExtra(it.key, it.value as CharSequence) + is Double -> putExtra(it.key, it.value as Double) + is DoubleArray -> putExtra(it.key, it.value as DoubleArray) + is Float -> putExtra(it.key, it.value as Float) + is FloatArray -> putExtra(it.key, it.value as FloatArray) + is Int -> putExtra(it.key, it.value as Int) + is IntArray -> putExtra(it.key, it.value as IntArray) + is Long -> putExtra(it.key, it.value as Long) + is LongArray -> putExtra(it.key, it.value as LongArray) + is Short -> putExtra(it.key, it.value as Short) + is ShortArray -> putExtra(it.key, it.value as ShortArray) + is String -> putExtra(it.key, it.value as String) + else -> error("Key-Value type ${it.value?.javaClass?.name} is not allowed") + } + } + }) + } + } +} \ No newline at end of file diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/data/ChannelData.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/data/ChannelData.kt new file mode 100644 index 00000000..304113a2 --- /dev/null +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/xposed/channel/data/ChannelData.kt @@ -0,0 +1,41 @@ +/* + * YukiHookAPI - An efficient Kotlin version of the Xposed Hook API. + * Copyright (C) 2019-2022 HighCapable + * https://github.com/fankes/YukiHookAPI + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This file is Created by fankes on 2022/5/16. + */ +package com.highcapable.yukihookapi.hook.xposed.channel.data + +import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel + +/** + * 数据通讯桥键值构造类 + * + * 这个类是对 [YukiHookDataChannel] 的一个扩展用法 + * + * - 详情请参考 [API 文档 - ChannelData](https://fankes.github.io/YukiHookAPI/#/api/document?id=channeldata-class) + * @param key 键值 + * @param value 默认值 - 可空 + */ +data class ChannelData(var key: String, var value: T? = null) \ No newline at end of file