diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7a57d83..645e409 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(com.highcapable.yukihookapi.api) ksp(com.highcapable.yukihookapi.ksp.xposed) implementation(com.fankes.projectpromote.project.promote) + implementation(org.luckypray.dexkit) implementation(com.github.duanhong169.drawabletoolbox) implementation(com.squareup.okhttp3.okhttp) implementation(androidx.core.core.ktx) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e2019c2..4d50b85 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -37,6 +37,11 @@ # 排除注入的 Activity -keep class com.fankes.tsbattery.ui.activity.parasitic.ConfigActivity +# DexKit +-keep class org.luckypray.dexkit.DexKitBridge { + native ; +} + # 防止某些类被 R8 混淆 (可能是 BUG) # FIXME: 已知问题字符串类名 (即使是常量) 也会被 R8 处理为混淆后的类名 # FIXME: 所以目前只能把不允许 R8 处理的类 keep 掉,同时在当前模块中也不会被混淆就是了 diff --git a/app/src/main/java/com/fankes/tsbattery/hook/entity/QQTIMHooker.kt b/app/src/main/java/com/fankes/tsbattery/hook/entity/QQTIMHooker.kt index 78456e9..4ff4acf 100644 --- a/app/src/main/java/com/fankes/tsbattery/hook/entity/QQTIMHooker.kt +++ b/app/src/main/java/com/fankes/tsbattery/hook/entity/QQTIMHooker.kt @@ -36,11 +36,11 @@ import com.fankes.tsbattery.R import com.fankes.tsbattery.const.ModuleVersion import com.fankes.tsbattery.const.PackageName import com.fankes.tsbattery.data.ConfigData -import com.fankes.tsbattery.hook.HookEntry import com.fankes.tsbattery.hook.factory.hookSystemWakeLock import com.fankes.tsbattery.hook.factory.isQQNightMode import com.fankes.tsbattery.hook.factory.jumpToModuleSettings import com.fankes.tsbattery.hook.factory.startModuleSettings +import com.fankes.tsbattery.hook.helper.DexKitHelper import com.fankes.tsbattery.utils.factory.appVersionName import com.fankes.tsbattery.utils.factory.dp import com.highcapable.yukihookapi.hook.bean.VariousClass @@ -50,7 +50,6 @@ import com.highcapable.yukihookapi.hook.factory.current import com.highcapable.yukihookapi.hook.factory.field import com.highcapable.yukihookapi.hook.factory.injectModuleAppResources import com.highcapable.yukihookapi.hook.factory.method -import com.highcapable.yukihookapi.hook.factory.processName import com.highcapable.yukihookapi.hook.factory.registerModuleAppActivities import com.highcapable.yukihookapi.hook.log.YLog import com.highcapable.yukihookapi.hook.type.android.BuildClass @@ -67,6 +66,7 @@ import com.highcapable.yukihookapi.hook.type.java.ListClass import com.highcapable.yukihookapi.hook.type.java.LongType import com.highcapable.yukihookapi.hook.type.java.StringClass import com.highcapable.yukihookapi.hook.type.java.UnitType +import java.lang.reflect.Method import java.lang.reflect.Proxy /** @@ -118,6 +118,16 @@ object QQTIMHooker : YukiBaseHooker() { ) ) + /** + * DexKit 搜索结果数据实现类 + */ + private object DexKitData { + var BaseChatPie_RemainScreenOnMethod: Method? = null + var BaseChatPie_CancelRemainScreenOnMethod: Method? = null + var SimpleItemProcessorClass: Class<*>? = null + var SimpleItemProcessorClass_OnClickMethod: Method? = null + } + /** 一个内部进程的名称 (与 X5 浏览器内核有关) */ private val privilegedProcessName = "$packageName:privileged_process" @@ -147,6 +157,61 @@ object QQTIMHooker : YukiBaseHooker() { */ private fun Any.compatToActivity() = if (this is Activity) this else current().method { name = "getActivity"; superClass() }.invoke() + /** 使用 DexKit 进行搜索 */ + private fun searchUsingDexKit() { + val classLoader = appClassLoader ?: return + DexKitHelper.create(this) { + BaseChatPieClass?.name?.also { baseChatPieClassName -> + DexKitData.BaseChatPie_RemainScreenOnMethod = + findMethod { + matcher { + declaredClass(baseChatPieClassName) + usingStrings("remainScreenOn") + paramCount = 0 + returnType = UnitType.name + } + }.firstOrNull()?.getMethodInstance(classLoader) + DexKitData.BaseChatPie_CancelRemainScreenOnMethod = + findMethod { + matcher { + declaredClass(baseChatPieClassName) + usingStrings("cancelRemainScreenOn") + paramCount = 0 + returnType = UnitType.name + } + }.firstOrNull()?.getMethodInstance(classLoader) + } + val kotlinFunction0 = "kotlin.jvm.functions.Function0" + findClass { + searchPackages("${PackageName.QQ}.setting.processor") + matcher { + methods { + add { + name = "" + paramTypes(ContextClass.name, IntType.name, CharSequenceClass.name, IntType.name) + } + add { + paramTypes(kotlinFunction0) + returnType = UnitType.name + } + } + fields { count(6..Int.MAX_VALUE) } + } + }.firstOrNull()?.name?.also { className -> + DexKitData.SimpleItemProcessorClass = className.toClass() + DexKitData.SimpleItemProcessorClass_OnClickMethod = + findMethod { + matcher { + declaredClass = className + paramTypes(kotlinFunction0) + returnType = UnitType.name + usingNumbers(2) + } + }.firstOrNull()?.getMethodInstance(classLoader) + } + } + } + /** * 这个类 QQ 的 BaseChatPie 是控制聊天界面的 * @@ -154,175 +219,16 @@ object QQTIMHooker : YukiBaseHooker() { * * remainScreenOn、cancelRemainScreenOn * - * 这两个方法一个是挂起电源锁常驻亮屏 - * - * 一个是停止常驻亮屏 - * - * 不由分说每个版本混淆的方法名都会变 - * - * 所以说每个版本重新适配 - 也可以提交分支帮我适配 - * - * - ❗Hook 错了方法会造成闪退! + * 这两个方法一个是挂起电源锁常驻亮屏 - 一个是停止常驻亮屏 */ private fun hookQQBaseChatPie() { - if (isQQ) when (hostVersionName) { - "8.0.0" -> { - hookBaseChatPie("bq") - hookBaseChatPie("aL") - } - "8.0.5", "8.0.7" -> { - hookBaseChatPie("bw") - hookBaseChatPie("aQ") - } - "8.1.0", "8.1.3" -> { - hookBaseChatPie("bE") - hookBaseChatPie("aT") - } - "8.1.5" -> { - hookBaseChatPie("bF") - hookBaseChatPie("aT") - } - "8.1.8", "8.2.0", "8.2.6" -> { - hookBaseChatPie("bC") - hookBaseChatPie("aT") - } - "8.2.7", "8.2.8", "8.2.11", "8.3.0" -> { - hookBaseChatPie("bE") - hookBaseChatPie("aV") - } - "8.3.5" -> { - hookBaseChatPie("bR") - hookBaseChatPie("aX") - } - "8.3.6" -> { - hookBaseChatPie("cp") - hookBaseChatPie("aX") - } - "8.3.9" -> { - hookBaseChatPie("cj") - hookBaseChatPie("aT") - } - "8.4.1", "8.4.5" -> { - hookBaseChatPie("ck") - hookBaseChatPie("aT") - } - "8.4.8", "8.4.10", "8.4.17", "8.4.18", "8.5.0" -> { - hookBaseChatPie("remainScreenOn") - hookBaseChatPie("cancelRemainScreenOn") - } - "8.5.5" -> { - hookBaseChatPie("bT") - hookBaseChatPie("aN") - } - "8.6.0", "8.6.5", "8.7.0", "8.7.5", "8.7.8", "8.8.0", "8.8.3", "8.8.5" -> { - hookBaseChatPie("ag") - hookBaseChatPie("ah") - } - "8.8.11", "8.8.12" -> { - hookBaseChatPie("bc") - hookBaseChatPie("bd") - } - "8.8.17", "8.8.20" -> { - hookBaseChatPie("bd") - hookBaseChatPie("be") - } - "8.8.23", "8.8.28" -> { - hookBaseChatPie("bf") - hookBaseChatPie("bg") - } - "8.8.33" -> { - hookBaseChatPie("bg") - hookBaseChatPie("bh") - } - "8.8.35", "8.8.38" -> { - hookBaseChatPie("bi") - hookBaseChatPie("bj") - } - "8.8.50" -> { - hookBaseChatPie("bj") - hookBaseChatPie("bk") - } - "8.8.55", "8.8.68", "8.8.80" -> { - hookBaseChatPie("bk") - hookBaseChatPie("bl") - } - "8.8.83", "8.8.85", "8.8.88", "8.8.90" -> { - hookBaseChatPie("bl") - hookBaseChatPie("bm") - } - "8.8.93", "8.8.95" -> { - hookBaseChatPie("J3") - hookBaseChatPie("S") - } - "8.8.98" -> { - hookBaseChatPie("M3") - hookBaseChatPie("S") - } - "8.9.0", "8.9.1", "8.9.2" -> { - hookBaseChatPie("N3") - hookBaseChatPie("S") - } - "8.9.3", "8.9.5" -> { - hookBaseChatPie("H3") - hookBaseChatPie("P") - } - "8.9.8", "8.9.10" -> { - hookBaseChatPie("H3") - hookBaseChatPie("N") - } - "8.9.13" -> { - hookBaseChatPie("y3") - hookBaseChatPie("H") - } - "8.9.15", "8.9.18", "8.9.19", "8.9.20" -> { - hookBaseChatPie("w3") - hookBaseChatPie("H") - } - "8.9.23", "8.9.25" -> { - hookBaseChatPie("z3") - hookBaseChatPie("H") - } - "8.9.28", "8.9.30", "8.9.33" -> { - hookBaseChatPie("A3") - hookBaseChatPie("H") - } - "8.9.35", "8.9.38", "8.9.50" -> { - hookBaseChatPie("B3") - hookBaseChatPie("H") - } - "8.9.53", "8.9.55", "8.9.58" -> { - hookBaseChatPie("C3") - hookBaseChatPie("H") - } - "8.9.63", "8.9.68" -> { - hookBaseChatPie("t3") - hookBaseChatPie("J") - } - "8.9.70", "8.9.71", "8.9.73", "8.9.75", "8.9.76" -> { - hookBaseChatPie("u3") - hookBaseChatPie("J") - } - "8.9.78", "8.9.80", "8.9.83" -> { - hookBaseChatPie("v3") - hookBaseChatPie("I") - } - else -> { - HookEntry.isHookClientSupport = false - YLog.warn("$hostVersionName not supported!") - } - } - } - - /** - * 拦截 [BaseChatPieClass] 的目标方法体封装 - * @param methodName 方法名 - */ - private fun hookBaseChatPie(methodName: String) { - BaseChatPieClass?.method { - name = methodName - emptyParam() - returnType = UnitType - }?.hook()?.intercept() + /** + * 打印警告信息 + * @param index 序号 + */ + fun warn(index: Int) = YLog.warn("$hostVersionName [$index] not support!") + DexKitData.BaseChatPie_RemainScreenOnMethod?.hook()?.intercept() ?: warn(index = 0) + DexKitData.BaseChatPie_CancelRemainScreenOnMethod?.hook()?.intercept() ?: warn(index = 1) } /** Hook CoreService QQ、TIM */ @@ -527,16 +433,7 @@ object QQTIMHooker : YukiBaseHooker() { private fun hookQQSettingsUi() { if (MainSettingFragmentClass == null) return YLog.error("Could not found main setting class, hook aborted") val kotlinUnit = "kotlin.Unit" - val kotlinFunction0 = "kotlin.jvm.functions.Function0" - val simpleItemProcessorClass = searchClass { - from("${PackageName.QQ}.setting.processor").absolute() - constructor { param(ContextClass, IntType, CharSequenceClass, IntType) } - method { - param(kotlinFunction0) - returnType = UnitType - } - field().count { it >= 6 } - }.get() ?: return YLog.error("Could not found processor class, hook aborted") + val simpleItemProcessorClass = DexKitData.SimpleItemProcessorClass ?: return YLog.error("Could not found processor class, hook aborted") /** * 创建入口点条目 @@ -550,11 +447,7 @@ object QQTIMHooker : YukiBaseHooker() { return simpleItemProcessorClass.buildOf(context, R.id.tsbattery_qq_entry_item_id, "TSBattery", iconResId) { param(ContextClass, IntType, CharSequenceClass, IntType) }?.also { entryItem -> - val onClickMethod = simpleItemProcessorClass.method { - param { it[0].name == kotlinFunction0 } - paramCount = 1 - returnType = UnitType - }.giveAll().firstOrNull() ?: error("Could not found processor method") + val onClickMethod = DexKitData.SimpleItemProcessorClass_OnClickMethod ?: error("Could not found processor method") val proxyOnClick = Proxy.newProxyInstance(appClassLoader, arrayOf(onClickMethod.parameterTypes[0])) { any, method, args -> if (method.name == "invoke") { context.startModuleSettings() @@ -613,6 +506,7 @@ object QQTIMHooker : YukiBaseHooker() { } override fun onHook() { + searchUsingDexKit() onAppLifecycle(isOnFailureThrowToApp = false) { attachBaseContext { baseContext, hasCalledSuper -> if (hasCalledSuper.not()) baseConfiguration = baseContext.resources.configuration diff --git a/app/src/main/java/com/fankes/tsbattery/hook/helper/DexKitHelper.kt b/app/src/main/java/com/fankes/tsbattery/hook/helper/DexKitHelper.kt new file mode 100644 index 0000000..fe8cfac --- /dev/null +++ b/app/src/main/java/com/fankes/tsbattery/hook/helper/DexKitHelper.kt @@ -0,0 +1,56 @@ +/* + * TSBattery - A new way to save your battery avoid cancer apps hacker it. + * Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com) + * https://github.com/fankes/TSBattery + * + * This software is non-free but opensource software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * and eula along with this software. If not, see + * + * + * This file is created by fankes on 2023/10/8. + */ +@file:Suppress("MemberVisibilityCanBePrivate") + +package com.fankes.tsbattery.hook.helper + +import com.highcapable.yukihookapi.hook.log.YLog +import com.highcapable.yukihookapi.hook.param.PackageParam +import org.luckypray.dexkit.DexKitBridge + +/** + * DexKit 工具类 + */ +object DexKitHelper { + + /** 是否已装载 */ + private var isLoaded = false + + /** 装载 */ + fun load() { + if (isLoaded) return + runCatching { + System.loadLibrary("dexkit") + isLoaded = true + }.onFailure { YLog.error("Load DexKit failed!", it) } + } + + /** + * 创建 [DexKitBridge] + * @param param 当前实例 + * @param initiate 方法体 + */ + fun create(param: PackageParam, initiate: DexKitBridge.() -> Unit) { + load() + runCatching { DexKitBridge.create(param.appInfo.sourceDir)?.use { initiate(it) } } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/tsbattery/ui/activity/MainActivity.kt b/app/src/main/java/com/fankes/tsbattery/ui/activity/MainActivity.kt index d509112..8bf0185 100644 --- a/app/src/main/java/com/fankes/tsbattery/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/fankes/tsbattery/ui/activity/MainActivity.kt @@ -50,29 +50,7 @@ import com.highcapable.yukihookapi.YukiHookAPI class MainActivity : BaseActivity() { companion object { - - private val qqSupportVersions = arrayOf( - "8.0.0", "8.0.5", "8.0.7", "8.1.0", "8.1.3", "8.1.5", "8.1.8", - "8.2.0", "8.2.6", "8.2.7", "8.2.8", "8.2.11", "8.3.0", "8.3.5", - "8.3.6", "8.3.9", "8.4.1", "8.4.5", "8.4.8", "8.4.10", "8.4.17", - "8.4.18", "8.5.0", "8.5.5", "8.6.0", "8.6.5", "8.7.0", "8.7.5", - "8.7.8", "8.8.0", "8.8.3", "8.8.5", "8.8.11", "8.8.12", "8.8.17", - "8.8.20", "8.8.23", "8.8.28", "8.8.33", "8.8.35", "8.8.38", "8.8.50", - "8.8.55", "8.8.68", "8.8.80", "8.8.83", "8.8.85", "8.8.88", "8.8.90", - "8.8.93", "8.8.95", "8.8.98", "8.9.0", "8.9.1", "8.9.2", "8.9.3", - "8.9.5", "8.9.8", "8.9.10", "8.9.13", "8.9.15", "8.9.18", "8.9.19", - "8.9.20", "8.9.23", "8.9.25", "8.9.28", "8.9.30", "8.9.33", "8.9.35", - "8.9.38", "8.9.50", "8.9.53", "8.9.55", "8.9.58", "8.9.63", "8.9.68", - "8.9.70", "8.9.71", "8.9.73", "8.9.75", "8.9.76", "8.9.78", - "8.9.80", "8.9.83" - ) - private val qqSupportVersion by lazy { - if (qqSupportVersions.isNotEmpty()) { - var value = "" - qqSupportVersions.forEach { value += "$it、" } - "${value.trim().let { it.substring(0, it.lastIndex) }}\n\n其余版本请自行测试是否有效。" - } else "empty" - } + private const val QQ_SUPPORT_VERSION = "理论支持 8.0.0+ 及以上版本。" private const val TIM_SUPPORT_VERSION = "2+、3+ (并未完全测试每个版本)。" private const val WECHAT_SUPPORT_VERSION = "全版本仅支持基础省电,更多功能依然画饼。" } @@ -131,7 +109,7 @@ class MainActivity : BaseActivity() { binding.mainQqItem.setOnClickListener { showDialog { title = "兼容的 QQ 版本" - msg = qqSupportVersion + msg = QQ_SUPPORT_VERSION confirmButton(text = "我知道了") } /** 振动提醒 */ diff --git a/gradle/sweet-dependency/sweet-dependency-config.yaml b/gradle/sweet-dependency/sweet-dependency-config.yaml index 1a6185d..48d736e 100644 --- a/gradle/sweet-dependency/sweet-dependency-config.yaml +++ b/gradle/sweet-dependency/sweet-dependency-config.yaml @@ -46,6 +46,9 @@ libraries: version: 1.2.0 ksp-xposed: version-ref: ::api + org.luckypray: + dexkit: + version: 2.0.0-rc4 com.github.duanhong169: drawabletoolbox: version: 1.0.7