mirror of
https://github.com/HighCapable/YukiHookAPI.git
synced 2025-09-06 02:35:40 +08:00
Added YukiHookDataChannel function and PackageParam.onAppLifecycle function and refactor some code
This commit is contained in:
@@ -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] 缓存功能
|
||||
*
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
}
|
@@ -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] 方法 */
|
||||
|
@@ -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<String>()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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<String, ((String, Intent) -> 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<String>(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 <T> put(key: String, value: T) = pushReceiver(ChannelData(key, value))
|
||||
|
||||
/**
|
||||
* 发送键值数据
|
||||
* @param data 键值实例
|
||||
* @param value 键值数据 - 未指定为 [ChannelData.value]
|
||||
*/
|
||||
fun <T> put(data: ChannelData<T>, 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 <T> 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 <T> wait(data: ChannelData<T>, 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<String>(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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<T>(var key: String, var value: T? = null)
|
Reference in New Issue
Block a user