Fix YukiHookDataChannel not in current Activity received broadcast bug and changed "wait" method params

This commit is contained in:
2022-05-27 01:24:23 +08:00
parent 1f132ff8bd
commit 960fd67cc3
5 changed files with 114 additions and 35 deletions

View File

@@ -21,13 +21,17 @@ class YukiHookDataChannel private constructor()
### NameSpace [class] ### NameSpace [class]
```kotlin ```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.88` `新增`
`v1.0.90` `修改`
新增 `isSecure` 参数
**功能描述** **功能描述**
> `YukiHookDataChannel` 命名空间。 > `YukiHookDataChannel` 命名空间。
@@ -85,17 +89,21 @@ fun put(key: String)
#### wait [method] #### wait [method]
```kotlin ```kotlin
fun <T> wait(key: String, value: T?, result: (value: T) -> Unit) fun <T> wait(key: String, result: (value: T) -> Unit)
``` ```
```kotlin ```kotlin
fun <T> wait(data: ChannelData<T>, value: T?, result: (value: T) -> Unit) fun <T> wait(data: ChannelData<T>, result: (value: T) -> Unit)
``` ```
**变更记录** **变更记录**
`v1.0.88` `新增` `v1.0.88` `新增`
`v1.0.90` `修改`
移除默认值 `value`
**功能描述** **功能描述**
> 获取键值数据。 > 获取键值数据。

View File

@@ -120,7 +120,7 @@ Hook 目标方法、构造方法时发生错误。
**解决方案** **解决方案**
此问题通常由 Hook Framework 产生,请检查对应的日志内容,若问题持续出现请携带完整日志进行反馈。 此问题通常由 Hook Framework 产生,请检查对应的日志内容,若问题持续出现请携带详细日志进行反馈。
!> `loggerE` Hooked Member with a finding error by **CLASS** !> `loggerE` Hooked Member with a finding error by **CLASS**
@@ -432,6 +432,26 @@ Resources 的 Hook 并非类似方法的 Hook其必须拥有完整的名称
这是一个异常汇总,请自行向下查看日志具体的异常是什么,例如找不到 Resources Id 的问题。 这是一个异常汇总,请自行向下查看日志具体的异常是什么,例如找不到 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 进程“死掉”。 > 这些异常会直接导致 APP 停止运行(FC),同时会在控制台打印 `E` 级别的日志,还会造成 Hook 进程“死掉”。
@@ -524,6 +544,16 @@ class MyApplication : Application() {
你只能在 [作为 Xposed 模块使用](config/xposed-using) 时使用 `YukiHookDataChannel` 你只能在 [作为 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 !> `IllegalStateException` Xposed modulePackageName load failed, please reset and rebuild it
**异常原因** **异常原因**

View File

@@ -1135,7 +1135,7 @@ dataChannel.checkingVersionEquals { isEquals ->
详情请参考 [YukiHookDataChannel](api/document?id=yukihookdatachannel-class)。 详情请参考 [YukiHookDataChannel](api/document?id=yukihookdatachannel-class)。
### 重复创建回调事件的规则 ### 回调事件响应的规则
!> 在模块和宿主中,每一个 `dataChannel` 对应的 `key` 的回调事件**都不允许重复创建**,若重复,之前的回调事件会被新增加的回调事件替换,若在模块中使用,在同一个 `Activity` 中不可以重复,不同的 `Activity` 中相同的 `key` 允许重复。 !> 在模块和宿主中,每一个 `dataChannel` 对应的 `key` 的回调事件**都不允许重复创建**,若重复,之前的回调事件会被新增加的回调事件替换,若在模块中使用,在同一个 `Activity` 中不可以重复,不同的 `Activity` 中相同的 `key` 允许重复。
@@ -1177,6 +1177,12 @@ class OtherActivity : AppCompatActivity() {
在上述示例中,回调事件 A 会被回调事件 B 替换掉,回调事件 C 的 `key` 不与其它重复,回调事件 D 在另一个 Activity 中,所以最终回调事件 B、C、D 都可被创建成功。 在上述示例中,回调事件 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 生命周期的扩展功能。 > 这是一个自动 Hook 宿主 APP 生命周期的扩展功能。

View File

@@ -25,25 +25,30 @@
* *
* This file is Created by fankes on 2022/5/16. * 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 package com.highcapable.yukihookapi.hook.xposed.channel
import android.app.Activity import android.app.Activity
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import com.highcapable.yukihookapi.YukiHookAPI 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.application.ModuleApplication
import com.highcapable.yukihookapi.hook.xposed.bridge.YukiHookBridge import com.highcapable.yukihookapi.hook.xposed.bridge.YukiHookBridge
import com.highcapable.yukihookapi.hook.xposed.channel.data.ChannelData import com.highcapable.yukihookapi.hook.xposed.channel.data.ChannelData
import com.highcapable.yukihookapi.hook.xposed.helper.YukiHookAppHelper import com.highcapable.yukihookapi.hook.xposed.helper.YukiHookAppHelper
import java.io.Serializable import java.io.Serializable
import java.util.concurrent.ConcurrentHashMap
/** /**
* 实现 Xposed 模块的数据通讯桥 * 实现 Xposed 模块的数据通讯桥
@@ -82,8 +87,13 @@ class YukiHookDataChannel private constructor() {
internal fun instance() = instance ?: YukiHookDataChannel().apply { instance = this } internal fun instance() = instance ?: YukiHookDataChannel().apply { instance = this }
} }
/**
* 键值回调的监听类型定义
*/
private enum class CallbackKeyType { SINGLE, CDATA, VMFL }
/** 注册广播回调数组 */ /** 注册广播回调数组 */
private var receiverCallbacks = HashMap<String, Pair<Context?, (String, Intent) -> Unit>>() private var receiverCallbacks = ConcurrentHashMap<String, Pair<Context?, (String, Intent) -> Unit>>()
/** 当前注册广播的 [Context] */ /** 当前注册广播的 [Context] */
private var receiverContext: Context? = null private var receiverContext: Context? = null
@@ -94,15 +104,19 @@ class YukiHookDataChannel private constructor() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) return if (intent == null) return
intent.action?.also { action -> intent.action?.also { action ->
receiverCallbacks.takeIf { it.isNotEmpty() }?.apply { runCatching {
val destroyedCallbacks = arrayListOf<String>() receiverCallbacks.takeIf { it.isNotEmpty() }?.apply {
forEach { (key, it) -> arrayListOf<String>().also { destroyedCallbacks ->
if (it.first is Activity && (it.first as? Activity?)?.isDestroyed == true) forEach { (key, it) ->
destroyedCallbacks.add(key) when {
else it.second(action, intent) (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") 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 名称 * 获取宿主广播 Action 名称
* @param packageName 包名 * @param packageName 包名
@@ -146,8 +170,8 @@ class YukiHookDataChannel private constructor() {
} }
) )
/** 注册监听模块与宿主的版本是否匹配 */ /** 注册监听模块与宿主的版本是否匹配 */
nameSpace(context, packageName).wait<String>(GET_MODULE_GENERATED_VERSION) { fromPackageName -> nameSpace(context, packageName, isSecure = false).wait<String>(GET_MODULE_GENERATED_VERSION) { fromPackageName ->
nameSpace(context, fromPackageName).put(RESULT_MODULE_GENERATED_VERSION, YukiHookBridge.moduleGeneratedVersion) nameSpace(context, fromPackageName, isSecure = false).put(RESULT_MODULE_GENERATED_VERSION, YukiHookBridge.moduleGeneratedVersion)
} }
} }
@@ -155,11 +179,12 @@ class YukiHookDataChannel private constructor() {
* 获取命名空间 * 获取命名空间
* @param context 上下文实例 * @param context 上下文实例
* @param packageName 目标 Hook APP (宿主) 的包名 * @param packageName 目标 Hook APP (宿主) 的包名
* @param isSecure 是否启用安全检查 - 默认是
* @return [NameSpace] * @return [NameSpace]
*/ */
internal fun nameSpace(context: Context? = null, packageName: String): NameSpace { internal fun nameSpace(context: Context? = null, packageName: String, isSecure: Boolean = true): NameSpace {
checkApi() 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] * - ❗请使用 [nameSpace] 方法来获取 [NameSpace]
* @param context 上下文实例 * @param context 上下文实例
* @param packageName 目标 Hook APP (宿主) 的包名 * @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 key 键值名称
* @param value 默认值 - 不填则在值为空的时候不回调 [result]
* @param result 回调结果数据 * @param result 回调结果数据
*/ */
fun <T> wait(key: String, value: T? = null, result: (value: T) -> Unit) { fun <T> wait(key: String, result: (value: T) -> Unit) {
receiverCallbacks[key + "_single_" + context?.javaClass?.name] = Pair(context) { action, intent -> receiverCallbacks[key + keyShortName(CallbackKeyType.SINGLE)] = Pair(context) { action, intent ->
if (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) 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 data 键值实例
* @param value 默认值 - 未指定为 [ChannelData.value]
* @param result 回调结果数据 * @param result 回调结果数据
*/ */
fun <T> wait(data: ChannelData<T>, value: T? = data.value, result: (value: T) -> Unit) { fun <T> wait(data: ChannelData<T>, result: (value: T) -> Unit) {
receiverCallbacks[data.key + "_cdata_" + context?.javaClass?.name] = Pair(context) { action, intent -> receiverCallbacks[data.key + keyShortName(CallbackKeyType.CDATA)] = Pair(context) { action, intent ->
if (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context)) 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 回调结果 * @param result 回调结果
*/ */
fun wait(key: String, result: () -> Unit) { 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 (action == if (isXposedEnvironment) hostActionName(packageName) else moduleActionName(context))
if (intent.getStringExtra(key) == VALUE_WAIT_FOR_LISTENER) result() if (intent.getStringExtra(key) == VALUE_WAIT_FOR_LISTENER) result()
} }
@@ -261,7 +292,11 @@ class YukiHookDataChannel private constructor() {
*/ */
private fun pushReceiver(vararg data: ChannelData<*>) { private fun pushReceiver(vararg data: ChannelData<*>) {
if (YukiHookAPI.Configs.isEnableDataChannel.not()) return 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) action = if (isXposedEnvironment) moduleActionName() else hostActionName(packageName)
data.takeIf { it.isNotEmpty() }?.forEach { data.takeIf { it.isNotEmpty() }?.forEach {
when (it.value) { when (it.value) {
@@ -291,9 +326,9 @@ class YukiHookDataChannel private constructor() {
else -> error("Key-Value type ${it.value?.javaClass?.name} is not allowed") 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"}\", " + 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\""
) )
} }
} }

View File

@@ -36,6 +36,6 @@ import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel
* *
* - 详情请参考 [API 文档 - ChannelData](https://fankes.github.io/YukiHookAPI/#/api/document?id=channeldata-class) * - 详情请参考 [API 文档 - ChannelData](https://fankes.github.io/YukiHookAPI/#/api/document?id=channeldata-class)
* @param key 键值 * @param key 键值
* @param value 默认值 - 可空 * @param value 键值数据 - 作为接收数据时可空
*/ */
data class ChannelData<T>(var key: String, var value: T? = null) data class ChannelData<T>(var key: String, var value: T? = null)