diff --git a/docs/api/public/YukiHookDataChannel.md b/docs/api/public/YukiHookDataChannel.md index b419c848..6e294a75 100644 --- a/docs/api/public/YukiHookDataChannel.md +++ b/docs/api/public/YukiHookDataChannel.md @@ -21,13 +21,17 @@ class YukiHookDataChannel private constructor() ### NameSpace [class] ```kotlin -inner class NameSpace internal constructor(private val context: Context?, private val packageName: String) +inner class NameSpace internal constructor(private val context: Context?, private val packageName: String, private val isSecure: Boolean) ``` **变更记录** `v1.0.88` `新增` +`v1.0.90` `修改` + +新增 `isSecure` 参数 + **功能描述** > `YukiHookDataChannel` 命名空间。 @@ -85,17 +89,21 @@ fun put(key: String) #### wait [method] ```kotlin -fun wait(key: String, value: T?, result: (value: T) -> Unit) +fun wait(key: String, result: (value: T) -> Unit) ``` ```kotlin -fun wait(data: ChannelData, value: T?, result: (value: T) -> Unit) +fun wait(data: ChannelData, result: (value: T) -> Unit) ``` **变更记录** `v1.0.88` `新增` +`v1.0.90` `修改` + +移除默认值 `value` + **功能描述** > 获取键值数据。 diff --git a/docs/config/api-exception.md b/docs/config/api-exception.md index d9f06550..066271ae 100644 --- a/docs/config/api-exception.md +++ b/docs/config/api-exception.md @@ -120,7 +120,7 @@ Hook 目标方法、构造方法时发生错误。 **解决方案** -此问题通常由 Hook Framework 产生,请检查对应的日志内容,若问题持续出现请携带完整日志进行反馈。 +此问题通常由 Hook Framework 产生,请检查对应的日志内容,若问题持续出现请携带详细日志进行反馈。 !> `loggerE` Hooked Member with a finding error by **CLASS** @@ -432,6 +432,26 @@ Resources 的 Hook 并非类似方法的 Hook,其必须拥有完整的名称 这是一个异常汇总,请自行向下查看日志具体的异常是什么,例如找不到 Resources Id 的问题。 +!> `loggerE` Received action "**ACTION**" failed + +**异常原因** + +使用 `YukiHookDataChannel` 时回调广播事件异常。 + +**解决方案** + +一般情况下,此错误基本上不会发生,一旦发生错误,排除自身代码的问题后,请携带详细日志进行反馈。 + +!> `loggerE` Failed to sendBroadcast like "**KEY**", because got null context in "**PACKAGENAME**" + +**异常原因** + +使用 `YukiHookDataChannel` 时发送广播取到了空的上下文实例。 + +**解决方案** + +一般情况下,此错误基本上不会发生,在最新版本中已经修复宿主使用时可能发生的问题,若最新版本依然发生错误,排除自身代码的问题后,请携带详细日志进行反馈。 + ## 阻断异常 > 这些异常会直接导致 APP 停止运行(FC),同时会在控制台打印 `E` 级别的日志,还会造成 Hook 进程“死掉”。 @@ -524,6 +544,16 @@ class MyApplication : Application() { 你只能在 [作为 Xposed 模块使用](config/xposed-using) 时使用 `YukiHookDataChannel`。 +!> `IllegalStateException` YukiHookDataChannel only support used on an Activity, but this current context is "**CLASSNAME**" + +**异常原因** + +在模块的非 `Activity` 环境中使用了 `YukiHookDataChannel`。 + +**解决方案** + +你只能在 `Activity` 或 `Fragment` 中使用 `YukiHookDataChannel`。 + !> `IllegalStateException` Xposed modulePackageName load failed, please reset and rebuild it **异常原因** diff --git a/docs/guide/special-feature.md b/docs/guide/special-feature.md index 5f1319cd..f47739f0 100644 --- a/docs/guide/special-feature.md +++ b/docs/guide/special-feature.md @@ -1135,7 +1135,7 @@ dataChannel.checkingVersionEquals { isEquals -> 详情请参考 [YukiHookDataChannel](api/document?id=yukihookdatachannel-class)。 -### 重复创建回调事件的规则 +### 回调事件响应的规则 !> 在模块和宿主中,每一个 `dataChannel` 对应的 `key` 的回调事件**都不允许重复创建**,若重复,之前的回调事件会被新增加的回调事件替换,若在模块中使用,在同一个 `Activity` 中不可以重复,不同的 `Activity` 中相同的 `key` 允许重复。 @@ -1177,8 +1177,14 @@ class OtherActivity : AppCompatActivity() { 在上述示例中,回调事件 A 会被回调事件 B 替换掉,回调事件 C 的 `key` 不与其它重复,回调事件 D 在另一个 Activity 中,所以最终回调事件 B、C、D 都可被创建成功。 -## 宿主生命周期扩展功能 +!> 一个相同 `key` 的回调事件只会回调当前模块正在显示的 `Activity` 中注册的回调事件,例如上述中的 `test_key`,如果 `OtherActivity` 正在显示,那么 `MainActivity` 中的 `test_key` 就不会被回调。 +!> 请特别注意,相同的 `key` 在同一个 `Activity` 不同的 `Fragment` 中注册 `dataChannel`,它们依然会在当前 `Activity` 中同时被回调。 + +!> 在模块中,你只能使用 `Activity` 的 `Context` 注册 `dataChannel`,你不能在 `Application` 以及 `Service` 等地方使用 `dataChannel`,若要在 `Fragment` 中使用 `dataChannel`,请使用 `activity?.dataChannel(...)`。 + +## 宿主生命周期扩展功能 + > 这是一个自动 Hook 宿主 APP 生命周期的扩展功能。 ### 监听生命周期 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 index c4d887da..ab13e459 100644 --- 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 @@ -25,25 +25,30 @@ * * This file is Created by fankes on 2022/5/16. */ -@file:Suppress("StaticFieldLeak", "UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate") +@file:Suppress("StaticFieldLeak", "UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate", "DEPRECATION") package com.highcapable.yukihookapi.hook.xposed.channel import android.app.Activity +import android.app.ActivityManager import android.app.Application import android.content.BroadcastReceiver import android.content.Context +import android.content.Context.ACTIVITY_SERVICE 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.log.yLoggerW +import com.highcapable.yukihookapi.hook.log.loggerE +import com.highcapable.yukihookapi.hook.log.loggerW +import com.highcapable.yukihookapi.hook.log.yLoggerE 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 com.highcapable.yukihookapi.hook.xposed.helper.YukiHookAppHelper import java.io.Serializable +import java.util.concurrent.ConcurrentHashMap /** * 实现 Xposed 模块的数据通讯桥 @@ -82,8 +87,13 @@ class YukiHookDataChannel private constructor() { internal fun instance() = instance ?: YukiHookDataChannel().apply { instance = this } } + /** + * 键值回调的监听类型定义 + */ + private enum class CallbackKeyType { SINGLE, CDATA, VMFL } + /** 注册广播回调数组 */ - private var receiverCallbacks = HashMap Unit>>() + private var receiverCallbacks = ConcurrentHashMap Unit>>() /** 当前注册广播的 [Context] */ private var receiverContext: Context? = null @@ -94,15 +104,19 @@ class YukiHookDataChannel private constructor() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null) return intent.action?.also { action -> - receiverCallbacks.takeIf { it.isNotEmpty() }?.apply { - val destroyedCallbacks = arrayListOf() - forEach { (key, it) -> - if (it.first is Activity && (it.first as? Activity?)?.isDestroyed == true) - destroyedCallbacks.add(key) - else it.second(action, intent) + runCatching { + receiverCallbacks.takeIf { it.isNotEmpty() }?.apply { + arrayListOf().also { destroyedCallbacks -> + forEach { (key, it) -> + when { + (it.first as? Activity?)?.isDestroyed == true -> destroyedCallbacks.add(key) + isCurrentBroadcast(it.first) -> it.second(action, intent) + } + } + destroyedCallbacks.takeIf { it.isNotEmpty() }?.forEach { remove(it) } + } } - destroyedCallbacks.takeIf { it.isNotEmpty() }?.forEach { remove(it) } - } + }.onFailure { loggerE(msg = "Received action \"$action\" failed", e = it) } } } } @@ -115,6 +129,16 @@ class YukiHookDataChannel private constructor() { error("Xposed modulePackageName load failed, please reset and rebuild it") } + /** + * 是否为当前正在使用的广播回调事件 + * @param context 当前实例 + * @return [Boolean] + */ + private fun isCurrentBroadcast(context: Context?) = runCatching { + isXposedEnvironment || context?.javaClass?.name == ((context ?: receiverContext) + ?.getSystemService(ACTIVITY_SERVICE) as? ActivityManager?)?.getRunningTasks(1)?.get(0)?.topActivity?.className + }.getOrNull() ?: loggerW(msg = "Couldn't got current Activity status because a SecurityException blocked it").let { false } + /** * 获取宿主广播 Action 名称 * @param packageName 包名 @@ -146,8 +170,8 @@ class YukiHookDataChannel private constructor() { } ) /** 注册监听模块与宿主的版本是否匹配 */ - nameSpace(context, packageName).wait(GET_MODULE_GENERATED_VERSION) { fromPackageName -> - nameSpace(context, fromPackageName).put(RESULT_MODULE_GENERATED_VERSION, YukiHookBridge.moduleGeneratedVersion) + nameSpace(context, packageName, isSecure = false).wait(GET_MODULE_GENERATED_VERSION) { fromPackageName -> + nameSpace(context, fromPackageName, isSecure = false).put(RESULT_MODULE_GENERATED_VERSION, YukiHookBridge.moduleGeneratedVersion) } } @@ -155,11 +179,12 @@ class YukiHookDataChannel private constructor() { * 获取命名空间 * @param context 上下文实例 * @param packageName 目标 Hook APP (宿主) 的包名 + * @param isSecure 是否启用安全检查 - 默认是 * @return [NameSpace] */ - internal fun nameSpace(context: Context? = null, packageName: String): NameSpace { + internal fun nameSpace(context: Context? = null, packageName: String, isSecure: Boolean = true): NameSpace { checkApi() - return NameSpace(context = context ?: receiverContext ?: YukiHookAppHelper.currentApplication(), packageName) + return NameSpace(context = context ?: receiverContext, packageName, isSecure) } /** @@ -168,8 +193,16 @@ class YukiHookDataChannel private constructor() { * - ❗请使用 [nameSpace] 方法来获取 [NameSpace] * @param context 上下文实例 * @param packageName 目标 Hook APP (宿主) 的包名 + * @param isSecure 是否启用安全检查 */ - inner class NameSpace internal constructor(private val context: Context?, private val packageName: String) { + inner class NameSpace internal constructor(private val context: Context?, private val packageName: String, private val isSecure: Boolean) { + + /** + * 键值尾部名称 + * @param type 类型 + * @return [String] + */ + private fun keyShortName(type: CallbackKeyType) = "_${if (isXposedEnvironment) "X" else context?.javaClass?.name ?: "M"}_${type.ordinal}" /** * 创建一个调用空间 @@ -207,26 +240,24 @@ class YukiHookDataChannel private constructor() { /** * 获取键值数据 * @param key 键值名称 - * @param value 默认值 - 不填则在值为空的时候不回调 [result] * @param result 回调结果数据 */ - fun wait(key: String, value: T? = null, result: (value: T) -> Unit) { - receiverCallbacks[key + "_single_" + context?.javaClass?.name] = Pair(context) { action, intent -> + fun wait(key: String, result: (value: T) -> Unit) { + receiverCallbacks[key + keyShortName(CallbackKeyType.SINGLE)] = Pair(context) { 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) } } + (intent.extras?.get(key) as? T?)?.let { result(it) } } } /** * 获取键值数据 * @param data 键值实例 - * @param value 默认值 - 未指定为 [ChannelData.value] * @param result 回调结果数据 */ - fun wait(data: ChannelData, value: T? = data.value, result: (value: T) -> Unit) { - receiverCallbacks[data.key + "_cdata_" + context?.javaClass?.name] = Pair(context) { action, intent -> + fun wait(data: ChannelData, result: (value: T) -> Unit) { + receiverCallbacks[data.key + keyShortName(CallbackKeyType.CDATA)] = Pair(context) { 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) } } + (intent.extras?.get(data.key) as? T?)?.let { result(it) } } } @@ -238,7 +269,7 @@ class YukiHookDataChannel private constructor() { * @param result 回调结果 */ fun wait(key: String, result: () -> Unit) { - receiverCallbacks[key + "_vwfl_" + context?.javaClass?.name] = Pair(context) { action, intent -> + receiverCallbacks[key + keyShortName(CallbackKeyType.VMFL)] = Pair(context) { action, intent -> if (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) if (intent.getStringExtra(key) == VALUE_WAIT_FOR_LISTENER) result() } @@ -261,7 +292,11 @@ class YukiHookDataChannel private constructor() { */ private fun pushReceiver(vararg data: ChannelData<*>) { if (YukiHookAPI.Configs.isEnableDataChannel.not()) return - context?.sendBroadcast(Intent().apply { + /** 在 [isSecure] 启用的情况下 - 在模块环境中只能使用 [Activity] 发送广播 */ + if (isSecure && context != null) if (isXposedEnvironment.not() && context !is Activity) + error("YukiHookDataChannel only support used on an Activity, but this current context is \"${context.javaClass.name}\"") + /** 发送广播 */ + (context ?: YukiHookAppHelper.currentApplication())?.sendBroadcast(Intent().apply { action = if (isXposedEnvironment) moduleActionName() else hostActionName(packageName) data.takeIf { it.isNotEmpty() }?.forEach { when (it.value) { @@ -291,9 +326,9 @@ class YukiHookDataChannel private constructor() { else -> error("Key-Value type ${it.value?.javaClass?.name} is not allowed") } } - }) ?: yLoggerW( + }) ?: yLoggerE( msg = "Failed to sendBroadcast like \"${data.takeIf { it.isNotEmpty() }?.get(0)?.key ?: "unknown"}\", " + - "because got non-null context in \"$packageName\"" + "because got null context in \"$packageName\"" ) } } 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 index 304113a2..45f6d627 100644 --- 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 @@ -36,6 +36,6 @@ import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel * * - 详情请参考 [API 文档 - ChannelData](https://fankes.github.io/YukiHookAPI/#/api/document?id=channeldata-class) * @param key 键值 - * @param value 默认值 - 可空 + * @param value 键值数据 - 作为接收数据时可空 */ data class ChannelData(var key: String, var value: T? = null) \ No newline at end of file