Added YukiHookDataChannel function and PackageParam.onAppLifecycle function and refactor some code

This commit is contained in:
2022-05-21 01:49:19 +08:00
parent ad9b53702c
commit a879db99a1
7 changed files with 585 additions and 42 deletions

View File

@@ -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] 缓存功能
*

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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] 方法 */

View File

@@ -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
}
/**

View File

@@ -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")
}
}
})
}
}
}

View File

@@ -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)