Files
YukiHookAPI/yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHookAPI.kt

500 lines
21 KiB
Kotlin

/*
* YukiHookAPI - An efficient Hook API and Xposed Module solution built in Kotlin.
* Copyright (C) 2019-2023 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/2/2.
*/
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package com.highcapable.yukihookapi
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import com.highcapable.yukihookapi.YukiHookAPI.Configs.debugLog
import com.highcapable.yukihookapi.YukiHookAPI.configs
import com.highcapable.yukihookapi.YukiHookAPI.encase
import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed
import com.highcapable.yukihookapi.hook.core.api.compat.HookApiCategoryHelper
import com.highcapable.yukihookapi.hook.core.api.compat.HookApiProperty
import com.highcapable.yukihookapi.hook.core.api.compat.type.ExecutorType
import com.highcapable.yukihookapi.hook.core.finder.members.ConstructorFinder
import com.highcapable.yukihookapi.hook.core.finder.members.FieldFinder
import com.highcapable.yukihookapi.hook.core.finder.members.MethodFinder
import com.highcapable.yukihookapi.hook.core.finder.store.ReflectsCacheStore
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
import com.highcapable.yukihookapi.hook.factory.isTaiChiModuleActive
import com.highcapable.yukihookapi.hook.factory.processName
import com.highcapable.yukihookapi.hook.log.YukiHookLogger
import com.highcapable.yukihookapi.hook.log.yLoggerE
import com.highcapable.yukihookapi.hook.log.yLoggerI
import com.highcapable.yukihookapi.hook.param.PackageParam
import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper
import com.highcapable.yukihookapi.hook.xposed.application.ModuleApplication
import com.highcapable.yukihookapi.hook.xposed.bridge.YukiXposedModule
import com.highcapable.yukihookapi.hook.xposed.bridge.status.YukiXposedModuleStatus
import com.highcapable.yukihookapi.hook.xposed.bridge.type.HookEntryType
import com.highcapable.yukihookapi.hook.xposed.channel.YukiHookDataChannel
import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookPrefsBridge
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Member
import java.lang.reflect.Method
/**
* [YukiHookAPI] 的装载调用类
*
* 可以实现作为模块装载和自定义 Hook 装载两种方式
*
* Xposed 模块装载方式已经自动对接相关 API - 可直接调用 [encase] 完成操作
*
* 你可以调用 [configs] 对 [YukiHookAPI] 进行配置
*/
object YukiHookAPI {
/** 是否还未输出欢迎信息 */
private var isShowSplashLogOnceTime = true
/** 标识是否从自定义 Hook API 装载 */
internal var isLoadedFromBaseContext = false
/** 获取当前 [YukiHookAPI] 的版本 */
const val API_VERSION_NAME = BuildConfig.API_VERSION_NAME
/** 获取当前 [YukiHookAPI] 的版本号 */
const val API_VERSION_CODE = BuildConfig.API_VERSION_CODE
/**
* 当前 [YukiHookAPI] 的状态
*/
object Status {
/**
* 获取项目编译完成的时间戳 (当前本地时间)
* @return [Long]
*/
val compiledTimestamp get() = runCatching { YukiHookAPI_Impl.compiledTimestamp }.getOrNull() ?: 0L
/**
* 获取当前是否为 (Xposed) 宿主环境
* @return [Boolean]
*/
val isXposedEnvironment get() = YukiXposedModule.isXposedEnvironment
/**
* 获取当前 Hook Framework 名称
*
* - ❗此方法已弃用 - 在之后的版本中将直接被删除
*
* - ❗请现在转移到 [Executor.name]
* @return [String]
*/
@Deprecated(
message = "请使用新方式来实现此功能",
ReplaceWith("Executor.name", "com.highcapable.yukihookapi.YukiHookAPI.Status.Executor")
)
val executorName get() = Executor.name
/**
* 获取当前 Hook Framework 版本
*
* - ❗此方法已弃用 - 在之后的版本中将直接被删除
*
* - ❗请现在转移到 [Executor.apiLevel]、[Executor.versionName]、[Executor.versionCode]
* @return [Int]
*/
@Deprecated(
message = "请使用新方式来实现此功能",
ReplaceWith("Executor.apiLevel", "com.highcapable.yukihookapi.YukiHookAPI.Status.Executor")
)
val executorVersion get() = Executor.apiLevel
/**
* 判断模块是否在 Xposed 或太极、无极中激活
*
* - ❗在模块环境中你需要将 [Application] 继承于 [ModuleApplication]
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
*
* - ❗在 (Xposed) 宿主环境中仅返回非 [isTaiChiModuleActive] 的激活状态
* @return [Boolean] 是否激活
*/
val isModuleActive get() = isXposedEnvironment || YukiXposedModuleStatus.isActive || isTaiChiModuleActive
/**
* 仅判断模块是否在 Xposed 中激活
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
*
* - ❗在 (Xposed) 宿主环境中始终返回 true
* @return [Boolean] 是否激活
*/
val isXposedModuleActive get() = isXposedEnvironment || YukiXposedModuleStatus.isActive
/**
* 仅判断模块是否在太极、无极中激活
*
* - ❗在模块环境中你需要将 [Application] 继承于 [ModuleApplication]
*
* - ❗在 (Xposed) 宿主环境中始终返回 false
* @return [Boolean] 是否激活
*/
val isTaiChiModuleActive get() = isXposedEnvironment.not() && (ModuleApplication.currentContext?.isTaiChiModuleActive ?: false)
/**
* 判断当前 Hook Framework 是否支持资源钩子(Resources Hook)
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
*
* - ❗在 (Xposed) 宿主环境中可能会延迟等待事件回调后才会返回 true
*
* - ❗请注意你需要确保 [InjectYukiHookWithXposed.isUsingResourcesHook] 已启用 - 否则始终返回 false
* @return [Boolean] 是否支持
*/
val isSupportResourcesHook
get() = YukiXposedModule.isSupportResourcesHook.takeIf { isXposedEnvironment } ?: YukiXposedModuleStatus.isSupportResourcesHook
/**
* 当前 [YukiHookAPI] 使用的 Hook Framework 相关信息
*/
object Executor {
/**
* 获取当前 Hook Framework 名称
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
* @return [String] 无法获取会返回 unknown - 获取失败会返回 invalid
*/
val name
get() = HookApiProperty.name.takeIf { isXposedEnvironment } ?: when {
isXposedModuleActive -> YukiXposedModuleStatus.executorName
isTaiChiModuleActive -> HookApiProperty.TAICHI_XPOSED_NAME
else -> YukiXposedModuleStatus.executorName
}
/**
* 获取当前 Hook Framework 类型
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
* @return [ExecutorType]
*/
val type get() = HookApiProperty.type.takeIf { isXposedEnvironment } ?: HookApiProperty.type(YukiXposedModuleStatus.executorName)
/**
* 获取当前 Hook Framework 的 API 版本
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
* @return [Int] 无法获取会返回 -1
*/
val apiLevel get() = HookApiProperty.apiLevel.takeIf { isXposedEnvironment } ?: YukiXposedModuleStatus.executorApiLevel
/**
* 获取当前 Hook Framework 版本名称
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
* @return [String] 无法获取会返回 unknown - 不支持会返回 unsupported
*/
val versionName get() = HookApiProperty.versionName.takeIf { isXposedEnvironment } ?: YukiXposedModuleStatus.executorVersionName
/**
* 获取当前 Hook Framework 版本号
*
* - ❗在模块环境中需要启用 [Configs.isEnableHookModuleStatus]
* @return [Int] 无法获取会返回 -1 - 不支持会返回 0
*/
val versionCode get() = HookApiProperty.versionCode.takeIf { isXposedEnvironment } ?: YukiXposedModuleStatus.executorVersionCode
}
}
/**
* 配置 [YukiHookAPI]
*/
object Configs {
/**
* 配置 [YukiHookLogger.Configs] 相关参数
* @param initiate 方法体
*/
inline fun debugLog(initiate: YukiHookLogger.Configs.() -> Unit) = YukiHookLogger.Configs.apply(initiate).build()
/**
* 这是一个调试日志的全局标识
*
* - ❗此方法已弃用 - 在之后的版本中将直接被删除
*
* - ❗请现在转移到 [debugLog] 并使用 [YukiHookLogger.Configs.tag]
*/
@Deprecated(message = "请使用新方式来实现此功能")
var debugTag
get() = YukiHookLogger.Configs.tag
set(value) {
YukiHookLogger.Configs.tag = value
}
/**
* 是否开启调试模式 - 默认启用
*
* 启用后将交由日志输出管理器打印详细 Hook 日志到控制台
*
* 关闭后将只输出 Error 级别的日志
*
* 请过滤 [debugTag] 即可找到每条日志
*
* 当 [YukiHookLogger.Configs.isEnable] 关闭后 [isDebug] 也将同时关闭
*/
var isDebug = true
/**
* 是否启用调试日志的输出功能
*
* - ❗此方法已弃用 - 在之后的版本中将直接被删除
*
* - ❗请现在转移到 [debugLog] 并使用 [YukiHookLogger.Configs.isEnable]
*/
@Deprecated(message = "请使用新方式来实现此功能")
var isAllowPrintingLogs
get() = YukiHookLogger.Configs.isEnable
set(value) {
YukiHookLogger.Configs.isEnable = value
}
/**
* 是否启用 [YukiHookPrefsBridge] 的键值缓存功能
*
* - ❗此方法已弃用 - 在之后的版本中将直接被删除
*
* - ❗请现在转移到 [isEnablePrefsBridgeCache]
*/
@Deprecated(message = "请使用新的命名方法来实现此功能", ReplaceWith("isEnablePrefsBridgeCache"))
var isEnableModulePrefsCache
get() = isEnablePrefsBridgeCache
set(value) {
isEnablePrefsBridgeCache = value
}
/**
* 是否启用 [YukiHookPrefsBridge] 的键值缓存功能
*
* - 为防止内存复用过高问题 - 此功能默认启用
*
* 你可以手动在 [YukiHookPrefsBridge] 中自由开启和关闭缓存功能以及清除缓存
*/
var isEnablePrefsBridgeCache = true
/**
* 是否启用当前 Xposed 模块自身 [Resources] 缓存功能
*
* - 为防止内存复用过高问题 - 此功能默认启用
*
* - ❗关闭后每次使用 [PackageParam.moduleAppResources] 都会重新创建 - 可能会造成运行缓慢
*
* 你可以手动调用 [PackageParam.refreshModuleAppResources] 来刷新缓存
*/
var isEnableModuleAppResourcesCache = true
/**
* 是否启用 Hook Xposed 模块激活等状态功能
*
* - 为原生支持 Xposed 模块激活状态检测 - 此功能默认启用
*
* - ❗关闭后你将不能再在模块环境中使用 [YukiHookAPI.Status] 中的功能
*/
var isEnableHookModuleStatus = true
/**
* 是否启用 Hook [SharedPreferences]
*
* 启用后将在模块启动时强制将 [SharedPreferences] 文件权限调整为 [Context.MODE_WORLD_READABLE] (0664)
*
* - ❗这是一个可选的实验性功能 - 此功能默认不启用
*
* - 仅用于修复某些系统可能会出现在启用了 New XSharedPreferences 后依然出现文件权限错误问题 - 若你能正常使用 [YukiHookPrefsBridge] 就不建议启用此功能
*/
var isEnableHookSharedPreferences = false
/**
* 是否启用当前 Xposed 模块与宿主交互的 [YukiHookDataChannel] 功能
*
* 请确保 Xposed 模块的 [Application] 继承于 [ModuleApplication] 才能有效
*
* - 此功能默认启用 - 关闭后将不会在功能初始化的时候装载 [YukiHookDataChannel]
*/
var isEnableDataChannel = true
/**
* 是否启用 [Member] 缓存功能
*
* - 为防止 [Member] 复用过高造成的系统 GC 问题 - 此功能默认启用
*
* 启用后会缓存已经找到的 [Method]、[Constructor]、[Field]
*
* 缓存的 [Member] 都将处于 [ReflectsCacheStore] 的全局静态实例中
*
* 推荐使用 [MethodFinder]、[ConstructorFinder]、[FieldFinder] 来获取 [Member]
*
* 详情请参考 [API 文档](https://fankes.github.io/YukiHookAPI/zh-cn/api/home)
*
* For English version, see [API Document](https://fankes.github.io/YukiHookAPI/en/api/home)
*
* 除非缓存的 [Member] 发生了混淆的问题 - 例如使用 R8 混淆后的 APP 的目标 [Member] - 否则建议启用
*/
var isEnableMemberCache = true
}
/**
* 配置 [YukiHookAPI] 相关参数
*
* 详情请参考 [configs 方法](https://fankes.github.io/YukiHookAPI/zh-cn/config/api-example#configs-%E6%96%B9%E6%B3%95)
*
* For English version, see [configs Method](https://fankes.github.io/YukiHookAPI/en/config/api-example#configs-method)
* @param initiate 方法体
*/
inline fun configs(initiate: Configs.() -> Unit) {
Configs.apply(initiate)
}
/**
* 作为 Xposed 模块装载调用入口方法
*
* 用法请参考 [API 文档](https://fankes.github.io/YukiHookAPI/zh-cn/api/home)
*
* For English version, see [API Document](https://fankes.github.io/YukiHookAPI/en/api/home)
*
* 配置请参考 [通过 lambda 创建](https://fankes.github.io/YukiHookAPI/zh-cn/config/api-example#%E9%80%9A%E8%BF%87-lambda-%E5%88%9B%E5%BB%BA)
*
* For English version, see [Created by lambda](https://fankes.github.io/YukiHookAPI/en/config/api-example#created-by-lambda)
* @param initiate Hook 方法体
*/
fun encase(initiate: PackageParam.() -> Unit) {
isLoadedFromBaseContext = false
if (YukiXposedModule.isXposedEnvironment)
YukiXposedModule.packageParamCallback = initiate
else printNotFoundHookApiError()
}
/**
* 作为 Xposed 模块装载调用入口方法
*
* 用法请参考 [API 文档](https://fankes.github.io/YukiHookAPI/zh-cn/api/home)
*
* For English version, see [API Document](https://fankes.github.io/YukiHookAPI/en/api/home)
*
* 配置请参考 [通过自定义 Hooker 创建](https://fankes.github.io/YukiHookAPI/zh-cn/config/api-example#%E9%80%9A%E8%BF%87%E8%87%AA%E5%AE%9A%E4%B9%89-hooker-%E5%88%9B%E5%BB%BA)
*
* For English version, see [Created by Custom Hooker](https://fankes.github.io/YukiHookAPI/en/config/api-example#created-by-custom-hooker)
* @param hooker Hook 子类数组 - 必填不能为空
* @throws IllegalStateException 如果 [hooker] 是空的
*/
fun encase(vararg hooker: YukiBaseHooker) {
isLoadedFromBaseContext = false
if (YukiXposedModule.isXposedEnvironment)
YukiXposedModule.packageParamCallback = {
if (hooker.isNotEmpty())
hooker.forEach { it.assignInstance(packageParam = this) }
else yLoggerE(msg = "Failed to passing \"encase\" method because your hooker param is empty", isImplicit = true)
}
else printNotFoundHookApiError()
}
/**
* 作为 [Application] 装载调用入口方法
*
* 请在 [Application.attachBaseContext] 中实现 [YukiHookAPI] 的装载
*
* 详情请参考 [作为 Hook API 使用](https://fankes.github.io/YukiHookAPI/zh-cn/guide/quick-start#%E4%BD%9C%E4%B8%BA-hook-api-%E4%BD%BF%E7%94%A8)
*
* For English version, see [Use as Hook API](https://fankes.github.io/YukiHookAPI/en/guide/quick-start#use-as-hook-api)
*
* 用法请参考 [API 文档](https://fankes.github.io/YukiHookAPI/zh-cn/api/home)
*
* For English version, see [API Document](https://fankes.github.io/YukiHookAPI/en/api/home)
*
* 配置请参考 [通过 lambda 创建](https://fankes.github.io/YukiHookAPI/zh-cn/config/api-example#%E9%80%9A%E8%BF%87-lambda-%E5%88%9B%E5%BB%BA)
*
* For English version, see [Created by lambda](https://fankes.github.io/YukiHookAPI/en/config/api-example#created-by-lambda)
* @param baseContext attachBaseContext
* @param initiate Hook 方法体
*/
fun encase(baseContext: Context?, initiate: PackageParam.() -> Unit) {
isLoadedFromBaseContext = true
when {
HookApiCategoryHelper.hasAvailableHookApi && baseContext != null ->
initiate(baseContext.createPackageParam().apply { printSplashInfo() })
else -> printNotFoundHookApiError()
}
}
/**
* 作为 [Application] 装载调用入口方法
*
* 请在 [Application.attachBaseContext] 中实现 [YukiHookAPI] 的装载
*
* 详情请参考 [作为 Hook API 使用](https://fankes.github.io/YukiHookAPI/zh-cn/guide/quick-start#%E4%BD%9C%E4%B8%BA-hook-api-%E4%BD%BF%E7%94%A8)
*
* For English version, see [Use as Hook API](https://fankes.github.io/YukiHookAPI/en/guide/quick-start#use-as-hook-api)
*
* 用法请参考 [API 文档](https://fankes.github.io/YukiHookAPI/zh-cn/api/home)
*
* For English version, see [API Document](https://fankes.github.io/YukiHookAPI/en/api/home)
*
* 配置请参考 [通过自定义 Hooker 创建](https://fankes.github.io/YukiHookAPI/zh-cn/config/api-example#%E9%80%9A%E8%BF%87%E8%87%AA%E5%AE%9A%E4%B9%89-hooker-%E5%88%9B%E5%BB%BA)
*
* For English version, see [Created by Custom Hooker](https://fankes.github.io/YukiHookAPI/en/config/api-example#created-by-custom-hooker)
* @param baseContext attachBaseContext
* @param hooker Hook 子类数组 - 必填不能为空
* @throws IllegalStateException 如果 [hooker] 是空的
*/
fun encase(baseContext: Context?, vararg hooker: YukiBaseHooker) {
isLoadedFromBaseContext = true
if (HookApiCategoryHelper.hasAvailableHookApi)
(if (baseContext != null)
if (hooker.isNotEmpty()) {
printSplashInfo()
hooker.forEach { it.assignInstance(packageParam = baseContext.createPackageParam()) }
} else yLoggerE(msg = "Failed to passing \"encase\" method because your hooker param is empty", isImplicit = true))
else printNotFoundHookApiError()
}
/** 输出欢迎信息调试日志 */
internal fun printSplashInfo() {
if (Configs.isDebug.not() || isShowSplashLogOnceTime.not()) return
isShowSplashLogOnceTime = false
yLoggerI(
msg = "Welcome to YukiHookAPI $API_VERSION_NAME($API_VERSION_CODE)! Using ${Status.Executor.name} API ${Status.Executor.apiLevel}",
isImplicit = true
)
}
/** 输出找不到 Hook API 的错误日志 */
private fun printNotFoundHookApiError() =
yLoggerE(msg = "Could not found any available Hook APIs in current environment! Aborted", isImplicit = true)
/**
* 通过 baseContext 创建 Hook 入口类
* @return [PackageParam]
*/
private fun Context.createPackageParam() =
PackageParam(PackageParamWrapper(HookEntryType.PACKAGE, packageName, processName, classLoader, applicationInfo))
}