From 2cdc8258cd62fd021026b5c5109e2a18c997a053 Mon Sep 17 00:00:00 2001 From: fankesyooni Date: Thu, 29 Sep 2022 08:06:08 +0800 Subject: [PATCH] Modify move main configs function to Host App and refactored --- .../HookConst.kt => const/ConstFactory.kt} | 29 +- .../com/fankes/tsbattery/data/ConfigData.kt | 125 ++++ .../com/fankes/tsbattery/data/DataConst.kt | 35 - .../com/fankes/tsbattery/hook/HookEntry.kt | 699 +----------------- .../tsbattery/hook/entity/QQTIMHooker.kt | 562 ++++++++++++++ .../tsbattery/hook/entity/WeChatHooker.kt | 108 +++ .../hook/factory/BasicHookFactory.kt | 63 ++ .../tsbattery/ui/activity/MainActivity.kt | 106 +-- .../ui/activity/parasitic/ConfigActivity.kt | 107 +++ app/src/main/res/layout/activity_config.xml | 372 ++++++++++ app/src/main/res/layout/activity_main.xml | 255 +------ app/src/main/res/mipmap-xxhdpi/ic_back.png | Bin 0 -> 3633 bytes app/src/main/res/mipmap-xxhdpi/ic_bug.png | Bin 5428 -> 0 bytes app/src/main/res/mipmap-xxhdpi/ic_error.png | Bin 0 -> 7665 bytes 14 files changed, 1425 insertions(+), 1036 deletions(-) rename app/src/main/java/com/fankes/tsbattery/{hook/HookConst.kt => const/ConstFactory.kt} (65%) create mode 100644 app/src/main/java/com/fankes/tsbattery/data/ConfigData.kt delete mode 100644 app/src/main/java/com/fankes/tsbattery/data/DataConst.kt create mode 100644 app/src/main/java/com/fankes/tsbattery/hook/entity/QQTIMHooker.kt create mode 100644 app/src/main/java/com/fankes/tsbattery/hook/entity/WeChatHooker.kt create mode 100644 app/src/main/java/com/fankes/tsbattery/hook/factory/BasicHookFactory.kt create mode 100644 app/src/main/java/com/fankes/tsbattery/ui/activity/parasitic/ConfigActivity.kt create mode 100644 app/src/main/res/layout/activity_config.xml create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_back.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_bug.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_error.png diff --git a/app/src/main/java/com/fankes/tsbattery/hook/HookConst.kt b/app/src/main/java/com/fankes/tsbattery/const/ConstFactory.kt similarity index 65% rename from app/src/main/java/com/fankes/tsbattery/hook/HookConst.kt rename to app/src/main/java/com/fankes/tsbattery/const/ConstFactory.kt index 9a8bdc7..15b8786 100644 --- a/app/src/main/java/com/fankes/tsbattery/hook/HookConst.kt +++ b/app/src/main/java/com/fankes/tsbattery/const/ConstFactory.kt @@ -17,13 +17,30 @@ * and eula along with this software. If not, see * * - * This file is Created by fankes on 2021/11/9. + * This file is Created by fankes on 2022/9/29. */ -package com.fankes.tsbattery.hook +package com.fankes.tsbattery.const -object HookConst { +/** + * 包名常量定义类 + */ +object PackageName { - const val QQ_PACKAGE_NAME = "com.tencent.mobileqq" - const val TIM_PACKAGE_NAME = "com.tencent.tim" - const val WECHAT_PACKAGE_NAME = "com.tencent.mm" + /** QQ */ + const val QQ = "com.tencent.mobileqq" + + /** TIM */ + const val TIM = "com.tencent.tim" + + /** 微信 */ + const val WECHAT = "com.tencent.mm" +} + +/** + * 跳转常量定义类 + */ +object JumpEvent { + + /** 启动模块设置 */ + const val OPEN_MODULE_SETTING = "tsbattery_open_module_settings" } \ No newline at end of file diff --git a/app/src/main/java/com/fankes/tsbattery/data/ConfigData.kt b/app/src/main/java/com/fankes/tsbattery/data/ConfigData.kt new file mode 100644 index 0000000..3985da7 --- /dev/null +++ b/app/src/main/java/com/fankes/tsbattery/data/ConfigData.kt @@ -0,0 +1,125 @@ +/* + * TSBattery - A new way to save your battery avoid cancer apps hacker it. + * Copyright (C) 2019-2022 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 2022/9/28. + */ +package com.fankes.tsbattery.data + +import android.content.Context +import android.content.SharedPreferences +import android.widget.CompoundButton + +/** + * 全局配置存储控制类 + */ +object ConfigData { + + /** QQ、TIM 保守模式*/ + const val ENABLE_QQ_TIM_PROTECT_MODE = "enable_qq_tim_protect_mode" + + /** 自动关闭 QQ、TIM 的 CoreService */ + const val ENABLE_KILL_QQ_TIM_CORESERVICE = "enable_kill_qq_tim_core_service" + + /** 自动关闭 QQ、TIM 的 CoreService$KernelService */ + const val ENABLE_KILLE_QQ_TIM_CORESERVICE_CHILD = "enable_kill_qq_tim_core_service_child" + + /** 停用全部省电功能 (停用模块) */ + const val DISABLE_ALL_HOOK = "disable_all_hook" + + /** 当前的 [SharedPreferences] */ + private var sharePrefs: SharedPreferences? = null + + /** + * 读取 [SharedPreferences] + * @param key 键值名称 + * @param value 键值内容 + * @return [Boolean] + */ + private fun getBoolean(key: String, value: Boolean = false) = sharePrefs?.getBoolean(key, value) ?: value + + /** + * 存入 [SharedPreferences] + * @param key 键值名称 + * @param value 键值内容 + */ + private fun putBoolean(key: String, value: Boolean = false) = sharePrefs?.edit()?.putBoolean(key, value)?.apply() + + /** + * 初始化 [SharedPreferences] + * @param context 实例 + */ + fun init(context: Context) { + sharePrefs = context.getSharedPreferences("tsbattery_config", Context.MODE_PRIVATE) + } + + /** + * 绑定到 [CompoundButton] 自动设置选中状态 + * @param key 键值名称 + * @param onChange 当改变时回调 + */ + fun CompoundButton.bind(key: String, onChange: (Boolean) -> Unit = {}) { + isChecked = getBoolean(key) + setOnCheckedChangeListener { button, isChecked -> + if (button.isPressed) { + putBoolean(key, isChecked) + onChange(isChecked) + } + } + } + + /** + * 是否启用 QQ、TIM 保守模式 + * @return [Boolean] + */ + var isEnableQQTimProtectMode + get() = getBoolean(ENABLE_QQ_TIM_PROTECT_MODE) + set(value) { + putBoolean(ENABLE_QQ_TIM_PROTECT_MODE, value) + } + + /** + * 是否启用自动关闭 QQ、TIM 的 CoreService + * @return [Boolean] + */ + var isEnableKillQQTimCoreService + get() = getBoolean(ENABLE_KILL_QQ_TIM_CORESERVICE) + set(value) { + putBoolean(ENABLE_KILL_QQ_TIM_CORESERVICE, value) + } + + /** + * 是否启用自动关闭 QQ、TIM 的 CoreService$KernelService + * @return [Boolean] + */ + var isEnableKillQQTimCoreServiceChild + get() = getBoolean(ENABLE_KILLE_QQ_TIM_CORESERVICE_CHILD) + set(value) { + putBoolean(ENABLE_KILLE_QQ_TIM_CORESERVICE_CHILD, value) + } + + /** + * 是否停用全部省电功能 (停用模块) + * @return [Boolean] + */ + var isDisableAllHook + get() = getBoolean(DISABLE_ALL_HOOK) + set(value) { + putBoolean(DISABLE_ALL_HOOK, value) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/tsbattery/data/DataConst.kt b/app/src/main/java/com/fankes/tsbattery/data/DataConst.kt deleted file mode 100644 index 40b07e8..0000000 --- a/app/src/main/java/com/fankes/tsbattery/data/DataConst.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * TSBattery - A new way to save your battery avoid cancer apps hacker it. - * Copyright (C) 2019-2022 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 2022/3/28. - */ -package com.fankes.tsbattery.data - -import com.highcapable.yukihookapi.hook.xposed.prefs.data.PrefsData - -object DataConst { - - val ENABLE_RUN_INFO = PrefsData("_tip_run_info", false) - val ENABLE_NOTIFY_TIP = PrefsData("_tip_in_notify", true) - val ENABLE_SETTING_TIP = PrefsData("_tip_in_setting", true) - val ENABLE_QQTIM_WHITE_MODE = PrefsData("_qqtim_white_mode", false) - val ENABLE_QQTIM_CORESERVICE_BAN = PrefsData("_qqtim_core_service_ban", false) - val ENABLE_QQTIM_CORESERVICE_CHILD_BAN = PrefsData("_qqtim_core_service_child_ban", false) - val DISABLE_WECHAT_HOOK = PrefsData("_disable_wechat_hook", false) -} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/tsbattery/hook/HookEntry.kt b/app/src/main/java/com/fankes/tsbattery/hook/HookEntry.kt index 41cd25f..84a26cc 100644 --- a/app/src/main/java/com/fankes/tsbattery/hook/HookEntry.kt +++ b/app/src/main/java/com/fankes/tsbattery/hook/HookEntry.kt @@ -23,36 +23,16 @@ package com.fankes.tsbattery.hook -import android.app.Activity -import android.app.Service -import android.content.ComponentName -import android.content.Intent -import android.os.Build -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.view.ViewGroup.MarginLayoutParams -import android.widget.Toast -import androidx.core.app.ServiceCompat -import com.fankes.tsbattery.BuildConfig -import com.fankes.tsbattery.data.DataConst -import com.fankes.tsbattery.hook.HookConst.QQ_PACKAGE_NAME -import com.fankes.tsbattery.hook.HookConst.TIM_PACKAGE_NAME -import com.fankes.tsbattery.hook.HookConst.WECHAT_PACKAGE_NAME -import com.fankes.tsbattery.ui.activity.MainActivity -import com.fankes.tsbattery.utils.factory.dp -import com.fankes.tsbattery.utils.factory.showDialog -import com.fankes.tsbattery.utils.factory.versionCode +import com.fankes.tsbattery.const.PackageName +import com.fankes.tsbattery.data.ConfigData +import com.fankes.tsbattery.hook.entity.QQTIMHooker +import com.fankes.tsbattery.hook.entity.WeChatHooker import com.fankes.tsbattery.utils.factory.versionName import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed -import com.highcapable.yukihookapi.hook.bean.VariousClass -import com.highcapable.yukihookapi.hook.factory.* -import com.highcapable.yukihookapi.hook.log.loggerD -import com.highcapable.yukihookapi.hook.log.loggerE +import com.highcapable.yukihookapi.hook.factory.configs +import com.highcapable.yukihookapi.hook.factory.encase +import com.highcapable.yukihookapi.hook.factory.registerModuleAppActivities import com.highcapable.yukihookapi.hook.param.PackageParam -import com.highcapable.yukihookapi.hook.type.android.* -import com.highcapable.yukihookapi.hook.type.java.* import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit @InjectYukiHookWithXposed(isUsingResourcesHook = false) @@ -60,662 +40,41 @@ class HookEntry : IYukiHookXposedInit { companion object { - /** QQ、TIM 存在的类 */ - private const val SplashActivityClass = "$QQ_PACKAGE_NAME.activity.SplashActivity" - - /** QQ、TIM 存在的类 */ - private const val CoreServiceClass = "$QQ_PACKAGE_NAME.app.CoreService" - - /** QQ、TIM 存在的类 */ - private const val CoreService_KernelServiceClass = "$QQ_PACKAGE_NAME.app.CoreService\$KernelService" - - /** QQ、TIM 新版本存在的类 */ - private const val FormSimpleItemClass = "$QQ_PACKAGE_NAME.widget.FormSimpleItem" - - /** QQ、TIM 旧版本存在的类 */ - private const val FormCommonSingleLineItemClass = "$QQ_PACKAGE_NAME.widget.FormCommonSingleLineItem" - - /** 微信存在的类 */ - private const val LauncherUIClass = "$WECHAT_PACKAGE_NAME.ui.LauncherUI" - - /** 根据多个版本存的不同的类 */ - private val BaseChatPieClass = VariousClass( - "$QQ_PACKAGE_NAME.activity.aio.core.BaseChatPie", - "$QQ_PACKAGE_NAME.activity.BaseChatPie" - ) + /** 是否完全支持当前版本 */ + var isHookClientSupport = true } - /** 是否完全支持当前版本 */ - private var isHookClientSupport = true - - /** - * 这个类 QQ 的 BaseChatPie 是控制聊天界面的 - * - * 里面有两个随机混淆的方法 ⬇ - * - * remainScreenOn、cancelRemainScreenOn - * - * 这两个方法一个是挂起电源锁常驻亮屏 - * - * 一个是停止常驻亮屏 - * - * 不由分说每个版本混淆的方法名都会变 - * - * 所以说每个版本重新适配 - 也可以提交分支帮我适配 - * - * - ❗Hook 错了方法会造成闪退! - * @param version QQ 版本 - */ - private fun PackageParam.hookQQBaseChatPie(version: String) { - when (version) { - "8.0.0" -> { - interceptBaseChatPie(methodName = "bq") - interceptBaseChatPie(methodName = "aL") - } - "8.0.5", "8.0.7" -> { - interceptBaseChatPie(methodName = "bw") - interceptBaseChatPie(methodName = "aQ") - } - "8.1.0", "8.1.3" -> { - interceptBaseChatPie(methodName = "bE") - interceptBaseChatPie(methodName = "aT") - } - "8.1.5" -> { - interceptBaseChatPie(methodName = "bF") - interceptBaseChatPie(methodName = "aT") - } - "8.1.8", "8.2.0", "8.2.6" -> { - interceptBaseChatPie(methodName = "bC") - interceptBaseChatPie(methodName = "aT") - } - "8.2.7", "8.2.8", "8.2.11", "8.3.0" -> { - interceptBaseChatPie(methodName = "bE") - interceptBaseChatPie(methodName = "aV") - } - "8.3.5" -> { - interceptBaseChatPie(methodName = "bR") - interceptBaseChatPie(methodName = "aX") - } - "8.3.6" -> { - interceptBaseChatPie(methodName = "cp") - interceptBaseChatPie(methodName = "aX") - } - "8.3.9" -> { - interceptBaseChatPie(methodName = "cj") - interceptBaseChatPie(methodName = "aT") - } - "8.4.1", "8.4.5" -> { - interceptBaseChatPie(methodName = "ck") - interceptBaseChatPie(methodName = "aT") - } - "8.4.8", "8.4.10", "8.4.17", "8.4.18", "8.5.0" -> { - interceptBaseChatPie(methodName = "remainScreenOn") - interceptBaseChatPie(methodName = "cancelRemainScreenOn") - } - "8.5.5" -> { - interceptBaseChatPie(methodName = "bT") - interceptBaseChatPie(methodName = "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" -> { - interceptBaseChatPie(methodName = "ag") - interceptBaseChatPie(methodName = "ah") - } - "8.8.11", "8.8.12" -> { - interceptBaseChatPie(methodName = "bc") - interceptBaseChatPie(methodName = "bd") - } - "8.8.17", "8.8.20" -> { - interceptBaseChatPie(methodName = "bd") - interceptBaseChatPie(methodName = "be") - } - "8.8.23", "8.8.28" -> { - interceptBaseChatPie(methodName = "bf") - interceptBaseChatPie(methodName = "bg") - } - "8.8.33" -> { - interceptBaseChatPie(methodName = "bg") - interceptBaseChatPie(methodName = "bh") - } - "8.8.35", "8.8.38" -> { - interceptBaseChatPie(methodName = "bi") - interceptBaseChatPie(methodName = "bj") - } - "8.8.50" -> { - interceptBaseChatPie(methodName = "bj") - interceptBaseChatPie(methodName = "bk") - } - "8.8.55", "8.8.68", "8.8.80" -> { - interceptBaseChatPie(methodName = "bk") - interceptBaseChatPie(methodName = "bl") - } - "8.8.83", "8.8.85", "8.8.88", "8.8.90" -> { - interceptBaseChatPie(methodName = "bl") - interceptBaseChatPie(methodName = "bm") - } - "8.8.93", "8.8.95" -> { - interceptBaseChatPie(methodName = "J3") - interceptBaseChatPie(methodName = "S") - } - "8.8.98" -> { - interceptBaseChatPie(methodName = "M3") - interceptBaseChatPie(methodName = "S") - } - "8.9.0", "8.9.1", "8.9.2" -> { - interceptBaseChatPie(methodName = "N3") - interceptBaseChatPie(methodName = "S") - } - "8.9.3", "8.9.5" -> { - interceptBaseChatPie(methodName = "H3") - interceptBaseChatPie(methodName = "P") - } - "8.9.8", "8.9.10" -> { - interceptBaseChatPie(methodName = "H3") - interceptBaseChatPie(methodName = "N") - } - else -> { - isHookClientSupport = false - loggerD(msg = "$version not supported!") - } - } - } - - /** - * 拦截 [BaseChatPieClass] 的目标方法体封装 - * @param methodName 方法名 - */ - private fun PackageParam.interceptBaseChatPie(methodName: String) = - BaseChatPieClass.hook { - injectMember { - method { - name = methodName - emptyParam() - returnType = UnitType - } - intercept() - } - } - - /** Hook 系统电源锁 */ - private fun PackageParam.hookSystemWakeLock() = - PowerManager_WakeLockClass.hook { - injectMember { - method { - name = "acquireLocked" - emptyParam() - } - intercept() - } - } - - /** 增加通知栏文本显示守护状态 */ - private fun PackageParam.hookNotification() = - Notification_BuilderClass.hook { - injectMember { - method { - name = "setContentText" - param(CharSequenceType) - } - beforeHook { - if (prefs.get(DataConst.ENABLE_NOTIFY_TIP)) - when (args().first().cast()) { - "QQ正在后台运行" -> - args().first().set("QQ正在后台运行 - TSBattery 守护中") - "TIM正在后台运行" -> - args().first().set("TIM正在后台运行 - TSBattery 守护中") - } - } - } - } - - /** - * 提示模块运行信息 QQ、TIM、微信 - * @param isQQ 是否为 QQ - * @param isTIM 是否为 TIM - */ - private fun PackageParam.hookModuleRunningInfo(isQQ: Boolean = false, isTIM: Boolean = false) = - when { - isQQ || isTIM -> SplashActivityClass.hook { - /** - * Hook 启动界面的第一个 [Activity] - * QQ 和 TIM 都是一样的类 - * 在里面加入提示运行信息的对话框测试模块是否激活 - */ - injectMember { - method { - name = "doOnCreate" - param(BundleClass) - } - afterHook { - if (prefs.get(DataConst.ENABLE_RUN_INFO)) - instance().apply { - showDialog { - title = "TSBattery 已激活" - msg = "[提示模块运行信息功能已打开]\n\n" + - (if (isQQ && isHookClientSupport.not()) - "❎ 当前版本 $versionName($versionCode) 不在兼容列表,请自行测试是否生效~\n\n" - else "✅ 模块工作看起来一切正常,请自行测试是否能达到省电效果。\n\n") + - "已生效模块版本:${BuildConfig.VERSION_NAME}\n" + - "当前模式:${if (prefs.get(DataConst.ENABLE_QQTIM_WHITE_MODE)) "保守模式" else "完全模式"}" + - "\n\n包名:${packageName}\n版本:$versionName($versionCode)" + - "\n\n模块只对挂后台锁屏情况下有省电效果," + - "请不要将过多的群提醒,消息通知打开,这样子在使用过程时照样会极其耗电。\n\n" + - "如果你不想看到此提示。请在模块设置中关闭“提示模块运行信息”,此设置默认关闭。\n\n" + - "持续常驻使用 QQ、TIM 依然会耗电,任何软件都是如此," + - "模块是无法帮你做到前台不耗电的。\n\n" + - "开发者 酷安 @星夜不荟\n未经允许禁止修改或复制我的劳动成果。" - confirmButton(text = "我知道了") - noCancelable() - } - } - } - } - } - else -> LauncherUIClass.hook { - /** - * Hook 启动界面的第一个 [Activity] - * 在里面加入提示运行信息的对话框测试模块是否激活 - */ - injectMember { - method { - name = "onCreate" - param(BundleClass) - } - afterHook { - if (prefs.get(DataConst.ENABLE_RUN_INFO)) - instance().apply { - showDialog { - title = "TSBattery 已激活" - msg = "[提示模块运行信息功能已打开]\n\n" + - "模块工作看起来一切正常,请自行测试是否能达到省电效果。\n\n" + - "已生效模块版本:${BuildConfig.VERSION_NAME}\n" + - "当前模式:基础省电" + - "\n\n包名:${packageName}\n版本:$versionName($versionCode)" + - "\n\n当前只支持微信的基础省电,即系统电源锁,后续会继续适配微信相关的省电功能(在新建文件夹了)。\n\n" + - "如果你不想看到此提示。请在模块设置中关闭“提示模块运行信息”,此设置默认关闭。\n\n" + - "持续常驻使用微信依然会耗电,任何软件都是如此," + - "模块是无法帮你做到前台不耗电的。\n\n" + - "开发者 酷安 @星夜不荟\n未经允许禁止修改或复制我的劳动成果。" - confirmButton(text = "我知道了") { finish() } - noCancelable() - } - } - } - } - } - } - - /** - * Hook CoreService QQ、TIM - * @param isQQ 是否为 QQ - 单独处理 - */ - private fun PackageParam.hookCoreService(isQQ: Boolean) { - CoreServiceClass.hook { - if (isQQ) { - injectMember { - method { name = "startTempService" } - intercept() - }.ignoredNoSuchMemberFailure() - injectMember { - method { - name = "startCoreService" - param(BooleanType) - } - intercept() - }.ignoredNoSuchMemberFailure() - injectMember { - method { - name = "onStartCommand" - param(IntentClass, IntType, IntType) - } - replaceTo(any = 2) - }.ignoredNoSuchMemberFailure() - } - injectMember { - method { name = "onCreate" } - afterHook { - if (prefs.get(DataConst.ENABLE_QQTIM_CORESERVICE_BAN)) - instance().apply { - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - loggerD(msg = "Shutdown CoreService OK!") - } - } - } - } - CoreService_KernelServiceClass.hook { - injectMember { - method { name = "onCreate" } - afterHook { - if (prefs.get(DataConst.ENABLE_QQTIM_CORESERVICE_CHILD_BAN)) - instance().apply { - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - loggerD(msg = "Shutdown CoreService\$KernelService OK!") - } - } - } - injectMember { - method { - name = "onStartCommand" - param(IntentClass, IntType, IntType) - } - replaceTo(any = 2) - }.ignoredNoSuchMemberFailure() - } - } - - /** - * 将激活状态插入到设置页面 - * @param isQQ 是否为 QQ - 单独处理 - */ - private fun PackageParam.hookQQSettingsSettingActivity(isQQ: Boolean) = - findClass(name = "$QQ_PACKAGE_NAME.activity.QQSettingSettingActivity").hook { - injectMember { - method { - name = "doOnCreate" - param(BundleClass) - afterHook { - /** 是否启用 Hook */ - if (prefs.get(DataConst.ENABLE_SETTING_TIP).not()) return@afterHook - /** 当前的顶级 Item 实例 */ - val formItemRefRoot = field { - type(FormSimpleItemClass).index(num = 1) - }.ignored().get(instance).cast() ?: field { - type(FormCommonSingleLineItemClass).index(num = 1) - }.ignored().get(instance).cast() - /** 创建一个新的 Item */ - FormSimpleItemClass.toClass().buildOf(instance) { param(ContextClass) }?.current { - method { - name = "setLeftText" - param(CharSequenceType) - }.call("TSBattery") - method { - name = "setRightText" - param(CharSequenceType) - }.call("${if (isQQ && isHookClientSupport.not()) "❎" else "✅"} ${BuildConfig.VERSION_NAME}") - method { - name = "setBgType" - param(IntType) - }.call(if (isQQ) 0 else 2) - }?.apply { - setOnClickListener { - instance().apply { - showDialog { - title = "TSBattery 守护中" - msg = (if (isQQ && isHookClientSupport.not()) - "❎ 当前版本 $versionName($versionCode) 不在兼容列表,请自行测试是否生效~\n\n" else "") + - "已生效模块版本:${BuildConfig.VERSION_NAME}\n" + - "当前模式:${if (prefs.get(DataConst.ENABLE_QQTIM_WHITE_MODE)) "保守模式" else "完全模式"}" + - "\n\n包名:${packageName}\n版本:$versionName($versionCode)" + - "\n\n模块只对挂后台锁屏情况下有省电效果," + - "请不要将过多的群提醒,消息通知打开,这样子在使用过程时照样会极其耗电。\n\n" + - "持续常驻使用 QQ、TIM 依然会耗电,任何软件都是如此," + - "模块是无法帮你做到前台不耗电的。\n\n" + - "开发者 酷安 @星夜不荟\n未经允许禁止修改或复制我的劳动成果。" - confirmButton(text = "打开模块设置") { - runCatching { - startActivity(Intent().apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - component = ComponentName( - BuildConfig.APPLICATION_ID, - MainActivity::class.java.name - ) - }) - }.onFailure { Toast.makeText(context, "启动失败", Toast.LENGTH_SHORT).show() } - } - cancelButton(text = "关闭") - } - } - } - }?.also { item -> - var listGroup = formItemRefRoot?.parent as? ViewGroup? - val lparam = (if (listGroup?.childCount == 1) { - listGroup = listGroup.parent as? ViewGroup - (formItemRefRoot?.parent as? View?)?.layoutParams - } else formItemRefRoot?.layoutParams) ?: ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) - /** 设置圆角和间距 */ - if (isQQ) (lparam as? MarginLayoutParams?)?.setMargins(0, 15.dp(item.context), 0, 0) - /** 将 Item 添加到设置界面 */ - listGroup?.also { if (isQQ) it.addView(item, lparam) else it.addView(item, 0, lparam) } - } - } - } - } - } - override fun onInit() = configs { debugLog { tag = "TSBattery" } isDebug = false - isEnableModulePrefsCache = false isEnableDataChannel = false } + /** + * 装载对象是否为 QQ、TIM、微信 + * @return [Boolean] + */ + private fun PackageParam.isCurrentScope() = packageName.let { it == PackageName.QQ || it == PackageName.TIM || it == PackageName.WECHAT } + override fun onHook() = encase { - loadApp(QQ_PACKAGE_NAME) { - hookSystemWakeLock() - hookNotification() - hookCoreService(isQQ = true) - hookModuleRunningInfo(isQQ = true) - hookQQSettingsSettingActivity(isQQ = true) - if (prefs.get(DataConst.ENABLE_QQTIM_WHITE_MODE)) return@loadApp - /** 通过在生命周期里取到应用的版本号 */ - onAppLifecycle { onCreate { hookQQBaseChatPie(versionName) } } - /** - * 干掉消息收发功能的电源锁 - * 每个版本的差异暂未做排查 - * 旧版本理论上没有这个类 - */ - findClass(name = "$QQ_PACKAGE_NAME.msf.service.y").hook { - injectMember { - method { - name = "a" - param(StringType, LongType) - returnType = UnitType + loadApp { + if (isCurrentScope()) onAppLifecycle { + attachBaseContext { baseContext, hasCalledSuper -> + if (hasCalledSuper) return@attachBaseContext + ConfigData.init(baseContext) + when (baseContext.packageName) { + PackageName.QQ, PackageName.TIM -> loadHooker(QQTIMHooker.apply { appVersionName = baseContext.versionName }) + PackageName.WECHAT -> loadHooker(WeChatHooker) } - intercept() - }.onAllFailure { loggerE(msg = "Hook MsfService Failed $it") } - }.ignoredHookClassNotFoundFailure() - /** - * 干掉自动上传服务的电源锁 - * 每个版本的差异暂未做排查 - */ - findClass(name = "com.tencent.upload.impl.UploadServiceImpl").hook { - injectMember { - method { name = "acquireWakeLockIfNot" } - intercept() - }.onAllFailure { loggerE(msg = "Hook UploadServiceImpl Failed $it") } - }.ignoredHookClassNotFoundFailure() - /** - * Hook 掉一个一像素保活 [Activity] 真的我怎么都想不到讯哥的程序员做出这种事情 - * 这个东西经过测试会在锁屏的时候吊起来,解锁的时候自动 finish(),无限耍流氓耗电 - * 2022/1/25 后期查证:锁屏界面消息快速回复窗口的解锁后拉起保活界面,也是毒瘤 - */ - findClass(name = "$QQ_PACKAGE_NAME.activity.QQLSUnlockActivity").hook { - injectMember { - method { - name = "onCreate" - param(BundleClass) - } - var origDevice = "" - beforeHook { - /** 由于在 onCreate 里有一行判断只要型号是 xiaomi 的设备就开电源锁,所以说这里临时替换成菊花厂 */ - origDevice = Build.MANUFACTURER - if (Build.MANUFACTURER.lowercase() == "xiaomi") - BuildClass.field { name = "MANUFACTURER" }.get().set("HUAWEI") - } - afterHook { - instance().finish() - /** 这里再把型号替换回去 - 不影响应用变量等 Xposed 模块修改的型号 */ - BuildClass.field { name = "MANUFACTURER" }.get().set(origDevice) + } + onCreate { + when (packageName) { + PackageName.QQ, PackageName.TIM -> + registerModuleAppActivities(proxy = "${PackageName.QQ}.activity.QQSettingSettingActivity") + PackageName.WECHAT -> registerModuleAppActivities(proxy = "${PackageName.WECHAT}.plugin.welab.ui.WelabMainUI") } } } - /** - * 这个东西同上 - * 反正也是一个一像素保活的 [Activity] - * 讯哥的程序员真的有你的 - * 2022/1/25 后期查证:锁屏界面消息快速回复窗口 - */ - findClass("$QQ_PACKAGE_NAME.activity.QQLSActivity\$14", "ktq").hook { - injectMember { - method { name = "run" } - intercept() - }.ignoredAllFailure() - }.ignoredHookClassNotFoundFailure() - /** - * 这个是毒瘤核心类 - * WakeLockMonitor - * 这个名字真的起的特别诗情画意 - * 带给用户的却是 shit 一样的体验 - * 里面有各种使用 Handler 和 Timer 的各种耗时常驻后台耗电办法持续接收消息 - * 直接循环全部方法全部干掉 - * 👮🏻 经过排查 Play 版本没这个类...... Emmmm 不想说啥了 - */ - findClass(name = "com.tencent.qapmsdk.qqbattery.monitor.WakeLockMonitor").hook { - injectMember { - method { - name = "onHook" - param(StringType, AnyType, AnyArrayClass, AnyType) - } - intercept() - } - injectMember { - method { - name = "doReport" - param("com.tencent.qapmsdk.qqbattery.monitor.WakeLockMonitor\$WakeLockEntity", IntType) - } - intercept() - } - injectMember { - method { - name = "afterHookedMethod" - param("com.tencent.qapmsdk.qqbattery.monitor.MethodHookParam") - } - intercept() - } - injectMember { - method { - name = "beforeHookedMethod" - param("com.tencent.qapmsdk.qqbattery.monitor.MethodHookParam") - } - intercept() - } - injectMember { - method { name = "onAppBackground" } - intercept() - } - injectMember { - method { - name = "onOtherProcReport" - param(BundleClass) - } - intercept() - } - injectMember { - method { name = "onProcessRun30Min" } - intercept() - } - injectMember { - method { name = "onProcessBG5Min" } - intercept() - } - injectMember { - method { - name = "writeReport" - param(BooleanType) - } - intercept() - } - }.ignoredHookClassNotFoundFailure() - /** - * 这个是毒瘤核心操作类 - * 功能同上、全部拦截 - * 👮🏻 经过排查 Play 版本也没这个类...... Emmmm 不想说啥了 - */ - findClass(name = "com.tencent.qapmsdk.qqbattery.QQBatteryMonitor").hook { - injectMember { - method { name = "start" } - intercept() - } - injectMember { - method { name = "stop" } - intercept() - } - injectMember { - method { - name = "handleMessage" - param(MessageClass) - } - replaceToTrue() - } - injectMember { - method { name = "startMonitorInner" } - intercept() - } - injectMember { - method { name = "onAppBackground" } - intercept() - } - injectMember { - method { name = "onAppForeground" } - intercept() - } - injectMember { - method { - name = "setLogWhite" - paramCount = 2 - } - intercept() - } - injectMember { - method { - name = "setCmdWhite" - paramCount = 2 - } - intercept() - } - injectMember { - method { - name = "onWriteLog" - param(StringType, StringType) - } - intercept() - } - injectMember { - method { - name = "onCmdRequest" - param(StringType) - } - intercept() - } - injectMember { - method { - name = "addData" - paramCount = 4 - } - intercept() - } - injectMember { - method { - name = "onGpsScan" - paramCount = 2 - } - intercept() - } - }.ignoredHookClassNotFoundFailure() - } - loadApp(TIM_PACKAGE_NAME) { - hookSystemWakeLock() - hookNotification() - hookCoreService(isQQ = false) - hookModuleRunningInfo(isTIM = true) - hookQQSettingsSettingActivity(isQQ = false) - } - loadApp(WECHAT_PACKAGE_NAME) { - if (prefs.get(DataConst.DISABLE_WECHAT_HOOK)) return@loadApp - hookSystemWakeLock() - hookModuleRunningInfo() - loggerD(msg = "ウイチャット:それが機能するかどうかはわかりませんでした") } } } 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 new file mode 100644 index 0000000..bcc3cc4 --- /dev/null +++ b/app/src/main/java/com/fankes/tsbattery/hook/entity/QQTIMHooker.kt @@ -0,0 +1,562 @@ +/* + * TSBattery - A new way to save your battery avoid cancer apps hacker it. + * Copyright (C) 2019-2022 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 2022/9/29. + */ +package com.fankes.tsbattery.hook.entity + +import android.app.Activity +import android.app.Service +import android.os.Build +import android.view.View +import android.view.ViewGroup +import androidx.core.app.ServiceCompat +import com.fankes.tsbattery.BuildConfig +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.jumpToModuleSettings +import com.fankes.tsbattery.hook.factory.startModuleSettings +import com.fankes.tsbattery.utils.factory.dp +import com.highcapable.yukihookapi.hook.bean.VariousClass +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.factory.buildOf +import com.highcapable.yukihookapi.hook.factory.current +import com.highcapable.yukihookapi.hook.factory.field +import com.highcapable.yukihookapi.hook.log.loggerD +import com.highcapable.yukihookapi.hook.log.loggerE +import com.highcapable.yukihookapi.hook.type.android.* +import com.highcapable.yukihookapi.hook.type.java.* + +/** + * Hook QQ、TIM + */ +object QQTIMHooker : YukiBaseHooker() { + + /** QQ、TIM 存在的类 */ + const val JumpActivityClass = "${PackageName.QQ}.activity.JumpActivity" + + /** QQ、TIM 存在的类 */ + private const val QQSettingSettingActivityClass = "${PackageName.QQ}.activity.QQSettingSettingActivity" + + /** QQ、TIM 新版本存在的类 */ + private const val FormSimpleItemClass = "${PackageName.QQ}.widget.FormSimpleItem" + + /** QQ、TIM 旧版本存在的类 */ + private const val FormCommonSingleLineItemClass = "${PackageName.QQ}.widget.FormCommonSingleLineItem" + + /** QQ、TIM 存在的类 */ + private const val CoreServiceClass = "${PackageName.QQ}.app.CoreService" + + /** QQ、TIM 存在的类 */ + private const val CoreService_KernelServiceClass = "${PackageName.QQ}.app.CoreService\$KernelService" + + /** 根据多个版本存的不同的类 */ + private val BaseChatPieClass = + VariousClass("${PackageName.QQ}.activity.aio.core.BaseChatPie", "${PackageName.QQ}.activity.BaseChatPie") + + /** + * 当前是否为 QQ + * @return [Boolean] + */ + private val isQQ get() = packageName == PackageName.QQ + + /** 当前宿主的版本 */ + var appVersionName = "" + + /** + * 这个类 QQ 的 BaseChatPie 是控制聊天界面的 + * + * 里面有两个随机混淆的方法 ⬇ + * + * remainScreenOn、cancelRemainScreenOn + * + * 这两个方法一个是挂起电源锁常驻亮屏 + * + * 一个是停止常驻亮屏 + * + * 不由分说每个版本混淆的方法名都会变 + * + * 所以说每个版本重新适配 - 也可以提交分支帮我适配 + * + * - ❗Hook 错了方法会造成闪退! + */ + private fun hookQQBaseChatPie() { + if (isQQ) when (appVersionName) { + "8.0.0" -> { + hookBaseChatPie(methodName = "bq") + hookBaseChatPie(methodName = "aL") + } + "8.0.5", "8.0.7" -> { + hookBaseChatPie(methodName = "bw") + hookBaseChatPie(methodName = "aQ") + } + "8.1.0", "8.1.3" -> { + hookBaseChatPie(methodName = "bE") + hookBaseChatPie(methodName = "aT") + } + "8.1.5" -> { + hookBaseChatPie(methodName = "bF") + hookBaseChatPie(methodName = "aT") + } + "8.1.8", "8.2.0", "8.2.6" -> { + hookBaseChatPie(methodName = "bC") + hookBaseChatPie(methodName = "aT") + } + "8.2.7", "8.2.8", "8.2.11", "8.3.0" -> { + hookBaseChatPie(methodName = "bE") + hookBaseChatPie(methodName = "aV") + } + "8.3.5" -> { + hookBaseChatPie(methodName = "bR") + hookBaseChatPie(methodName = "aX") + } + "8.3.6" -> { + hookBaseChatPie(methodName = "cp") + hookBaseChatPie(methodName = "aX") + } + "8.3.9" -> { + hookBaseChatPie(methodName = "cj") + hookBaseChatPie(methodName = "aT") + } + "8.4.1", "8.4.5" -> { + hookBaseChatPie(methodName = "ck") + hookBaseChatPie(methodName = "aT") + } + "8.4.8", "8.4.10", "8.4.17", "8.4.18", "8.5.0" -> { + hookBaseChatPie(methodName = "remainScreenOn") + hookBaseChatPie(methodName = "cancelRemainScreenOn") + } + "8.5.5" -> { + hookBaseChatPie(methodName = "bT") + hookBaseChatPie(methodName = "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(methodName = "ag") + hookBaseChatPie(methodName = "ah") + } + "8.8.11", "8.8.12" -> { + hookBaseChatPie(methodName = "bc") + hookBaseChatPie(methodName = "bd") + } + "8.8.17", "8.8.20" -> { + hookBaseChatPie(methodName = "bd") + hookBaseChatPie(methodName = "be") + } + "8.8.23", "8.8.28" -> { + hookBaseChatPie(methodName = "bf") + hookBaseChatPie(methodName = "bg") + } + "8.8.33" -> { + hookBaseChatPie(methodName = "bg") + hookBaseChatPie(methodName = "bh") + } + "8.8.35", "8.8.38" -> { + hookBaseChatPie(methodName = "bi") + hookBaseChatPie(methodName = "bj") + } + "8.8.50" -> { + hookBaseChatPie(methodName = "bj") + hookBaseChatPie(methodName = "bk") + } + "8.8.55", "8.8.68", "8.8.80" -> { + hookBaseChatPie(methodName = "bk") + hookBaseChatPie(methodName = "bl") + } + "8.8.83", "8.8.85", "8.8.88", "8.8.90" -> { + hookBaseChatPie(methodName = "bl") + hookBaseChatPie(methodName = "bm") + } + "8.8.93", "8.8.95" -> { + hookBaseChatPie(methodName = "J3") + hookBaseChatPie(methodName = "S") + } + "8.8.98" -> { + hookBaseChatPie(methodName = "M3") + hookBaseChatPie(methodName = "S") + } + "8.9.0", "8.9.1", "8.9.2" -> { + hookBaseChatPie(methodName = "N3") + hookBaseChatPie(methodName = "S") + } + "8.9.3", "8.9.5" -> { + hookBaseChatPie(methodName = "H3") + hookBaseChatPie(methodName = "P") + } + "8.9.8", "8.9.10" -> { + hookBaseChatPie(methodName = "H3") + hookBaseChatPie(methodName = "N") + } + else -> { + HookEntry.isHookClientSupport = false + loggerD(msg = "$appVersionName not supported!") + } + } + } + + /** + * 拦截 [BaseChatPieClass] 的目标方法体封装 + * @param methodName 方法名 + */ + private fun hookBaseChatPie(methodName: String) { + BaseChatPieClass.hook { + injectMember { + method { + name = methodName + emptyParam() + returnType = UnitType + } + intercept() + } + } + } + + /** Hook CoreService QQ、TIM */ + private fun hookCoreService() { + CoreServiceClass.hook { + if (isQQ) { + injectMember { + method { name = "startTempService" } + intercept() + }.ignoredNoSuchMemberFailure() + injectMember { + method { + name = "startCoreService" + param(BooleanType) + } + intercept() + }.ignoredNoSuchMemberFailure() + injectMember { + method { + name = "onStartCommand" + param(IntentClass, IntType, IntType) + } + replaceTo(any = 2) + }.ignoredNoSuchMemberFailure() + } + injectMember { + method { name = "onCreate" } + afterHook { + if (ConfigData.isEnableKillQQTimCoreService) + instance().apply { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + loggerD(msg = "Shutdown CoreService OK!") + } + } + } + } + CoreService_KernelServiceClass.hook { + injectMember { + method { name = "onCreate" } + afterHook { + if (ConfigData.isEnableKillQQTimCoreServiceChild) + instance().apply { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + loggerD(msg = "Shutdown CoreService\$KernelService OK!") + } + } + } + injectMember { + method { + name = "onStartCommand" + param(IntentClass, IntType, IntType) + } + replaceTo(any = 2) + }.ignoredNoSuchMemberFailure() + } + } + + /** Hook QQ 不省电的功能 */ + private fun hookQQDisgusting() { + if (isQQ.not()) return + /** + * 干掉消息收发功能的电源锁 + * 每个版本的差异暂未做排查 + * 旧版本理论上没有这个类 + */ + findClass(name = "${PackageName.QQ}.msf.service.y").hook { + injectMember { + method { + name = "a" + param(StringType, LongType) + returnType = UnitType + } + intercept() + }.onAllFailure { loggerE(msg = "Hook MsfService Failed $it") } + }.ignoredHookClassNotFoundFailure() + /** + * 干掉自动上传服务的电源锁 + * 每个版本的差异暂未做排查 + */ + findClass(name = "com.tencent.upload.impl.UploadServiceImpl").hook { + injectMember { + method { name = "acquireWakeLockIfNot" } + intercept() + }.onAllFailure { loggerE(msg = "Hook UploadServiceImpl Failed $it") } + }.ignoredHookClassNotFoundFailure() + /** + * Hook 掉一个一像素保活 [Activity] 真的我怎么都想不到讯哥的程序员做出这种事情 + * 这个东西经过测试会在锁屏的时候吊起来,解锁的时候自动 finish(),无限耍流氓耗电 + * 2022/1/25 后期查证:锁屏界面消息快速回复窗口的解锁后拉起保活界面,也是毒瘤 + */ + findClass(name = "${PackageName.QQ}.activity.QQLSUnlockActivity").hook { + injectMember { + method { + name = "onCreate" + param(BundleClass) + } + var origDevice = "" + beforeHook { + /** 由于在 onCreate 里有一行判断只要型号是 xiaomi 的设备就开电源锁,所以说这里临时替换成菊花厂 */ + origDevice = Build.MANUFACTURER + if (Build.MANUFACTURER.lowercase() == "xiaomi") + BuildClass.field { name = "MANUFACTURER" }.get().set("HUAWEI") + } + afterHook { + instance().finish() + /** 这里再把型号替换回去 - 不影响应用变量等 Xposed 模块修改的型号 */ + BuildClass.field { name = "MANUFACTURER" }.get().set(origDevice) + } + } + } + /** + * 这个东西同上 + * 反正也是一个一像素保活的 [Activity] + * 讯哥的程序员真的有你的 + * 2022/1/25 后期查证:锁屏界面消息快速回复窗口 + */ + findClass("${PackageName.QQ}.activity.QQLSActivity\$14", "ktq").hook { + injectMember { + method { name = "run" } + intercept() + }.ignoredAllFailure() + }.ignoredHookClassNotFoundFailure() + /** + * 这个是毒瘤核心类 + * WakeLockMonitor + * 这个名字真的起的特别诗情画意 + * 带给用户的却是 shit 一样的体验 + * 里面有各种使用 Handler 和 Timer 的各种耗时常驻后台耗电办法持续接收消息 + * 直接循环全部方法全部干掉 + * 👮🏻 经过排查 Play 版本没这个类...... Emmmm 不想说啥了 + * ✅ 备注:8.9.x 版本已经基本移除了这个功能,没有再发现存在这个类 + */ + findClass(name = "com.tencent.qapmsdk.qqbattery.monitor.WakeLockMonitor").hook { + injectMember { + method { + name = "onHook" + param(StringType, AnyType, AnyArrayClass, AnyType) + } + intercept() + } + injectMember { + method { + name = "doReport" + param("com.tencent.qapmsdk.qqbattery.monitor.WakeLockMonitor\$WakeLockEntity", IntType) + } + intercept() + } + injectMember { + method { + name = "afterHookedMethod" + param("com.tencent.qapmsdk.qqbattery.monitor.MethodHookParam") + } + intercept() + } + injectMember { + method { + name = "beforeHookedMethod" + param("com.tencent.qapmsdk.qqbattery.monitor.MethodHookParam") + } + intercept() + } + injectMember { + method { name = "onAppBackground" } + intercept() + } + injectMember { + method { + name = "onOtherProcReport" + param(BundleClass) + } + intercept() + } + injectMember { + method { name = "onProcessRun30Min" } + intercept() + } + injectMember { + method { name = "onProcessBG5Min" } + intercept() + } + injectMember { + method { + name = "writeReport" + param(BooleanType) + } + intercept() + } + }.ignoredHookClassNotFoundFailure() + /** + * 这个是毒瘤核心操作类 + * 功能同上、全部拦截 + * 👮🏻 经过排查 Play 版本也没这个类...... Emmmm 不想说啥了 + * ✅ 备注:8.9.x 版本已经基本移除了这个功能,没有再发现存在这个类 + */ + findClass(name = "com.tencent.qapmsdk.qqbattery.QQBatteryMonitor").hook { + injectMember { + method { name = "start" } + intercept() + } + injectMember { + method { name = "stop" } + intercept() + } + injectMember { + method { + name = "handleMessage" + param(MessageClass) + } + replaceToTrue() + } + injectMember { + method { name = "startMonitorInner" } + intercept() + } + injectMember { + method { name = "onAppBackground" } + intercept() + } + injectMember { + method { name = "onAppForeground" } + intercept() + } + injectMember { + method { + name = "setLogWhite" + paramCount = 2 + } + intercept() + } + injectMember { + method { + name = "setCmdWhite" + paramCount = 2 + } + intercept() + } + injectMember { + method { + name = "onWriteLog" + param(StringType, StringType) + } + intercept() + } + injectMember { + method { + name = "onCmdRequest" + param(StringType) + } + intercept() + } + injectMember { + method { + name = "addData" + paramCount = 4 + } + intercept() + } + injectMember { + method { + name = "onGpsScan" + paramCount = 2 + } + intercept() + } + }.ignoredHookClassNotFoundFailure() + } + + override fun onHook() { + /** Hook 跳转事件 */ + JumpActivityClass.hook { + injectMember { + method { + name = "doOnCreate" + param(BundleClass) + } + afterHook { instance().jumpToModuleSettings() } + } + } + /** 将条目注入设置界面 */ + QQSettingSettingActivityClass.hook { + injectMember { + method { + name = "doOnCreate" + param(BundleClass) + } + afterHook { + /** 当前的顶级 Item 实例 */ + val formItemRefRoot = field { + type(FormSimpleItemClass).index(num = 1) + }.ignored().get(instance).cast() ?: field { + type(FormCommonSingleLineItemClass).index(num = 1) + }.ignored().get(instance).cast() + /** 创建一个新的 Item */ + FormSimpleItemClass.toClassOrNull()?.buildOf(instance) { param(ContextClass) }?.current { + method { + name = "setLeftText" + param(CharSequenceType) + }.call("TSBattery") + method { + name = "setRightText" + param(CharSequenceType) + }.call(BuildConfig.VERSION_NAME) + method { + name = "setBgType" + param(IntType) + }.call(if (isQQ) 0 else 2) + }?.apply { setOnClickListener { instance().startModuleSettings() } }?.also { item -> + var listGroup = formItemRefRoot?.parent as? ViewGroup? + val lparam = (if (listGroup?.childCount == 1) { + listGroup = listGroup.parent as? ViewGroup + (formItemRefRoot?.parent as? View?)?.layoutParams + } else formItemRefRoot?.layoutParams) + ?: ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + /** 设置圆角和间距 */ + if (isQQ) (lparam as? ViewGroup.MarginLayoutParams?)?.setMargins(0, 15.dp(item.context), 0, 0) + /** 将 Item 添加到设置界面 */ + listGroup?.also { if (isQQ) it.addView(item, lparam) else it.addView(item, 0, lparam) } + } + } + } + } + if (ConfigData.isDisableAllHook) return + /** Hook 系统电源锁 */ + hookSystemWakeLock() + /** Hook 聊天界面 */ + hookQQBaseChatPie() + /** Hook CoreService */ + hookCoreService() + /** Hook QQ 不省电的功能 */ + hookQQDisgusting() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/tsbattery/hook/entity/WeChatHooker.kt b/app/src/main/java/com/fankes/tsbattery/hook/entity/WeChatHooker.kt new file mode 100644 index 0000000..b3fbb77 --- /dev/null +++ b/app/src/main/java/com/fankes/tsbattery/hook/entity/WeChatHooker.kt @@ -0,0 +1,108 @@ +/* + * TSBattery - A new way to save your battery avoid cancer apps hacker it. + * Copyright (C) 2019-2022 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 2022/9/29. + */ +package com.fankes.tsbattery.hook.entity + +import android.app.Activity +import android.os.Build +import android.view.Gravity +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.core.content.res.ResourcesCompat +import com.fankes.tsbattery.R +import com.fankes.tsbattery.const.PackageName +import com.fankes.tsbattery.data.ConfigData +import com.fankes.tsbattery.hook.factory.hookSystemWakeLock +import com.fankes.tsbattery.hook.factory.jumpToModuleSettings +import com.fankes.tsbattery.hook.factory.startModuleSettings +import com.fankes.tsbattery.utils.factory.absoluteStatusBarHeight +import com.fankes.tsbattery.utils.factory.dp +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.factory.current +import com.highcapable.yukihookapi.hook.factory.injectModuleAppResources +import com.highcapable.yukihookapi.hook.log.loggerD +import com.highcapable.yukihookapi.hook.type.android.BundleClass + +/** + * Hook 微信 + * + * 具体功能还在画饼 + */ +object WeChatHooker : YukiBaseHooker() { + + /** 微信存在的类 - 未测试每个版本是否都存在 */ + const val LauncherUIClass = "${PackageName.WECHAT}.ui.LauncherUI" + + /** 微信存在的类 - 未测试每个版本是否都存在 */ + private const val SettingsUIClass = "${PackageName.WECHAT}.plugin.setting.ui.setting.SettingsUI" + + override fun onHook() { + /** Hook 跳转事件 */ + LauncherUIClass.hook { + injectMember { + method { + name = "onResume" + emptyParam() + } + afterHook { instance().jumpToModuleSettings(isFinish = false) } + } + } + /** 向设置界面右上角添加按钮 */ + SettingsUIClass.hook { + injectMember { + method { + name = "onCreate" + param(BundleClass) + } + afterHook { + method { + name = "get_fragment" + emptyParam() + superClass(isOnlySuperClass = true) + }.get(instance).call()?.current() + ?.field { name = "mController" } + ?.current()?.method { name = "getContentView" } + ?.invoke()?.addView(LinearLayout(instance()).apply { + context.injectModuleAppResources() + gravity = Gravity.END or Gravity.BOTTOM + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + addView(ImageView(context).apply { + layoutParams = ViewGroup.MarginLayoutParams(20.dp(context), 20.dp(context)).apply { + topMargin = context.absoluteStatusBarHeight + 15.dp(context) + rightMargin = 20.dp(context) + } + setColorFilter(ResourcesCompat.getColor(resources, R.color.colorTextGray, null)) + setImageResource(R.drawable.ic_icon) + if (Build.VERSION.SDK_INT >= 26) tooltipText = "TSBattery 设置" + setOnClickListener { context.startModuleSettings() } + }) + }) + } + } + } + if (ConfigData.isDisableAllHook) return + /** Hook 系统电源锁 */ + hookSystemWakeLock() + /** 日志省电大法 */ + loggerD(msg = "ウイチャット:それが機能するかどうかはわかりませんでした") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/tsbattery/hook/factory/BasicHookFactory.kt b/app/src/main/java/com/fankes/tsbattery/hook/factory/BasicHookFactory.kt new file mode 100644 index 0000000..d2ea3ab --- /dev/null +++ b/app/src/main/java/com/fankes/tsbattery/hook/factory/BasicHookFactory.kt @@ -0,0 +1,63 @@ +/* + * TSBattery - A new way to save your battery avoid cancer apps hacker it. + * Copyright (C) 2019-2022 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 2022/9/29. + */ +package com.fankes.tsbattery.hook.factory + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.fankes.tsbattery.const.JumpEvent +import com.fankes.tsbattery.ui.activity.parasitic.ConfigActivity +import com.highcapable.yukihookapi.YukiHookAPI +import com.highcapable.yukihookapi.hook.param.PackageParam +import com.highcapable.yukihookapi.hook.type.android.PowerManager_WakeLockClass +import kotlin.system.exitProcess + +/** 启动模块设置 [Activity] */ +fun Context.startModuleSettings() = startActivity(Intent(this, ConfigActivity::class.java)) + +/** + * 跳转模块设置 [Activity] + * @param isFinish 执行完成是否自动关闭当前活动 + */ +fun Activity.jumpToModuleSettings(isFinish: Boolean = true) { + if (intent.hasExtra(JumpEvent.OPEN_MODULE_SETTING)) { + /** 宿主版本不匹配的时候自动结束宿主进程 */ + if (intent.getLongExtra(JumpEvent.OPEN_MODULE_SETTING, 0) != YukiHookAPI.Status.compiledTimestamp) + exitProcess(status = 0) + intent.removeExtra(JumpEvent.OPEN_MODULE_SETTING) + startModuleSettings() + if (isFinish) finish() + } +} + +/** Hook 系统电源锁 */ +fun PackageParam.hookSystemWakeLock() { + PowerManager_WakeLockClass.hook { + injectMember { + method { + name = "acquireLocked" + emptyParam() + } + intercept() + } + } +} \ 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 08b9394..bc48b13 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 @@ -23,27 +23,27 @@ package com.fankes.tsbattery.ui.activity +import android.content.ComponentName +import android.content.Intent import android.view.HapticFeedbackConstants import androidx.core.view.isVisible import com.fankes.tsbattery.BuildConfig import com.fankes.tsbattery.R -import com.fankes.tsbattery.data.DataConst +import com.fankes.tsbattery.const.JumpEvent +import com.fankes.tsbattery.const.PackageName import com.fankes.tsbattery.databinding.ActivityMainBinding -import com.fankes.tsbattery.hook.HookConst.QQ_PACKAGE_NAME -import com.fankes.tsbattery.hook.HookConst.TIM_PACKAGE_NAME -import com.fankes.tsbattery.hook.HookConst.WECHAT_PACKAGE_NAME +import com.fankes.tsbattery.hook.entity.QQTIMHooker +import com.fankes.tsbattery.hook.entity.WeChatHooker import com.fankes.tsbattery.ui.activity.base.BaseActivity import com.fankes.tsbattery.utils.factory.* import com.fankes.tsbattery.utils.tool.GithubReleaseTool import com.fankes.tsbattery.utils.tool.YukiPromoteTool import com.highcapable.yukihookapi.YukiHookAPI -import com.highcapable.yukihookapi.hook.factory.modulePrefs class MainActivity : BaseActivity() { companion object { - private const val moduleVersion = BuildConfig.VERSION_NAME 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", @@ -71,7 +71,7 @@ class MainActivity : BaseActivity() { override fun onCreate() { /** 检查更新 */ - GithubReleaseTool.checkingForUpdate(context = this, moduleVersion) { version, function -> + GithubReleaseTool.checkingForUpdate(context = this, BuildConfig.VERSION_NAME) { version, function -> binding.mainTextReleaseVersion.apply { text = "点击更新 $version" isVisible = true @@ -90,38 +90,16 @@ class MainActivity : BaseActivity() { } else showDialog { title = "模块没有激活" - msg = "检测到模块没有激活,模块需要 Xposed 环境依赖," + - "同时需要系统拥有 Root 权限(太极阴可以免 Root)," + - "请自行查看本页面使用帮助与说明第三条。\n" + - "太极和第三方 Xposed 激活后" + - "可能不会提示激活,若想验证是否激活请打开“提示模块运行信息”自行检查," + - "或观察 QQ、TIM 的常驻通知是否有“TSBattery 守护中”字样”。\n\n" + - "如果生效就代表模块运行正常,若你在未 Root 情况下激活模块,这里的激活状态只是一个显示意义上的存在。\n" + - "太极(无极)在 MIUI 设备上会提示打开授权,请进行允许,然后再次打开本模块查看激活状态。" + msg = "检测到模块没有激活,若你正在使用免 Root 框架例如 LSPatch、太极或无极,你可以忽略此提示。" confirmButton(text = "我知道了") noCancelable() } - /** 推荐使用 LSPosed */ - if (YukiHookAPI.Status.isTaiChiModuleActive) - showDialog { - title = "兼容性提示" - msg = "若你的设备已 Root,推荐使用 LSPosed 激活模块,太极可能会出现模块设置无法保存的问题。" - confirmButton(text = "我知道了") - } - /** 检测应用转生 - 如果模块已激活就不再检测 */ - if (("com.bug.xposed").isInstall && YukiHookAPI.Status.isModuleActive.not()) - showDialog { - title = "环境异常" - msg = "检测到“应用转生”已被安装,为了保证模块的安全和稳定,请卸载更换其他 Hook 框架后才能继续使用。" - confirmButton(text = "退出") { finish() } - noCancelable() - } /** 设置安装状态 */ - binding.mainTextQqVer.text = if (QQ_PACKAGE_NAME.isInstall) version(QQ_PACKAGE_NAME) else "未安装" - binding.mainTextTimVer.text = if (TIM_PACKAGE_NAME.isInstall) version(TIM_PACKAGE_NAME) else "未安装" - binding.mainTextWechatVer.text = if (WECHAT_PACKAGE_NAME.isInstall) version(WECHAT_PACKAGE_NAME) else "未安装" + binding.mainTextQqVer.text = if (PackageName.QQ.isInstall) version(PackageName.QQ) else "未安装" + binding.mainTextTimVer.text = if (PackageName.TIM.isInstall) version(PackageName.TIM) else "未安装" + binding.mainTextWechatVer.text = if (PackageName.WECHAT.isInstall) version(PackageName.WECHAT) else "未安装" /** 设置文本 */ - binding.mainTextVersion.text = "模块版本:$moduleVersion $pendingFlag" + binding.mainTextVersion.text = "模块版本:${BuildConfig.VERSION_NAME} $pendingFlag" binding.mainQqItem.setOnClickListener { showDialog { title = "兼容的 QQ 版本" @@ -151,53 +129,16 @@ class MainActivity : BaseActivity() { } /** 获取 Sp 存储的信息 */ binding.hideIconInLauncherSwitch.isChecked = isLauncherIconShowing.not() - binding.qqtimProtectModeSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_QQTIM_WHITE_MODE) - binding.qqTimCoreServiceSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_QQTIM_CORESERVICE_BAN) - binding.qqTimCoreServiceKnSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_QQTIM_CORESERVICE_CHILD_BAN) - binding.wechatDisableHookSwitch.isChecked = modulePrefs.get(DataConst.DISABLE_WECHAT_HOOK) - binding.notifyModuleInfoSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_RUN_INFO) - binding.notifyNotifyTipSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_NOTIFY_TIP) - binding.settingModuleTipSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_SETTING_TIP) - binding.qqtimProtectModeSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.ENABLE_QQTIM_WHITE_MODE, b) - snake(msg = "修改需要重启 QQ 以生效") - } - binding.qqTimCoreServiceSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.ENABLE_QQTIM_CORESERVICE_BAN, b) - } - binding.qqTimCoreServiceKnSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.ENABLE_QQTIM_CORESERVICE_CHILD_BAN, b) - } - binding.wechatDisableHookSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.DISABLE_WECHAT_HOOK, b) - snake(msg = "修改需要重启微信以生效") - } binding.hideIconInLauncherSwitch.setOnCheckedChangeListener { btn, b -> if (btn.isPressed.not()) return@setOnCheckedChangeListener hideOrShowLauncherIcon(b) } - binding.notifyModuleInfoSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.ENABLE_RUN_INFO, b) - } - binding.notifyNotifyTipSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.ENABLE_NOTIFY_TIP, b) - } - binding.settingModuleTipSwitch.setOnCheckedChangeListener { btn, b -> - if (btn.isPressed.not()) return@setOnCheckedChangeListener - modulePrefs.put(DataConst.ENABLE_SETTING_TIP, b) - } /** 快捷操作 QQ */ - binding.quickQqButton.setOnClickListener { openSelfSetting(QQ_PACKAGE_NAME) } + binding.quickQqButton.setOnClickListener { startModuleSettings(PackageName.QQ) } /** 快捷操作 TIM */ - binding.quickTimButton.setOnClickListener { openSelfSetting(TIM_PACKAGE_NAME) } + binding.quickTimButton.setOnClickListener { startModuleSettings(PackageName.TIM) } /** 快捷操作微信 */ - binding.quickWechatButton.setOnClickListener { openSelfSetting(WECHAT_PACKAGE_NAME) } + binding.quickWechatButton.setOnClickListener { startModuleSettings(PackageName.WECHAT) } /** 项目地址按钮点击事件 */ binding.titleGithubIcon.setOnClickListener { openBrowser(url = "https://github.com/fankes/TSBattery") } /** 恰饭! */ @@ -206,6 +147,23 @@ class MainActivity : BaseActivity() { } } + /** + * 启动模块设置界面 + * @param packageName 包名 + */ + private fun startModuleSettings(packageName: String) { + if (packageName.isInstall) runCatching { + startActivity(Intent().apply { + component = ComponentName( + packageName, + if (packageName != PackageName.WECHAT) QQTIMHooker.JumpActivityClass else WeChatHooker.LauncherUIClass + ) + putExtra(JumpEvent.OPEN_MODULE_SETTING, YukiHookAPI.Status.compiledTimestamp) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) + }.onFailure { snake(msg = "启动模块设置失败\n$it") } else snake(msg = "你没有安装此应用") + } + /** 刷新模块激活使用的方式 */ private fun refreshActivateExecutor() { when { diff --git a/app/src/main/java/com/fankes/tsbattery/ui/activity/parasitic/ConfigActivity.kt b/app/src/main/java/com/fankes/tsbattery/ui/activity/parasitic/ConfigActivity.kt new file mode 100644 index 0000000..32becf5 --- /dev/null +++ b/app/src/main/java/com/fankes/tsbattery/ui/activity/parasitic/ConfigActivity.kt @@ -0,0 +1,107 @@ +/* + * TSBattery - A new way to save your battery avoid cancer apps hacker it. + * Copyright (C) 2019-2022 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 2022/9/28. + */ +@file:Suppress("SetTextI18n") + +package com.fankes.tsbattery.ui.activity.parasitic + +import android.widget.TextView +import androidx.core.view.isGone +import androidx.core.view.isVisible +import com.fankes.tsbattery.BuildConfig +import com.fankes.tsbattery.const.PackageName +import com.fankes.tsbattery.data.ConfigData +import com.fankes.tsbattery.data.ConfigData.bind +import com.fankes.tsbattery.databinding.ActivityConfigBinding +import com.fankes.tsbattery.hook.HookEntry +import com.fankes.tsbattery.ui.activity.base.BaseActivity +import com.fankes.tsbattery.utils.factory.* +import com.fankes.tsbattery.utils.tool.GithubReleaseTool +import kotlin.system.exitProcess + +class ConfigActivity : BaseActivity() { + + override fun onCreate() { + /** 检查更新 */ + GithubReleaseTool.checkingForUpdate(context = this, BuildConfig.VERSION_NAME) { version, function -> + binding.updateVersionText.apply { + text = "点击更新 $version" + isVisible = true + setOnClickListener { function() } + } + } + binding.titleBackIcon.setOnClickListener { finish() } + binding.titleNameText.text = "TSBattery 设置 (${appName.trim()})" + binding.appIcon.setImageDrawable(findAppIcon()) + binding.appName.text = appName.trim() + binding.appVersion.text = "${versionName}($versionCode)" + binding.moduleVersion.text = "${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})" + binding.activeModeIcon.isVisible = HookEntry.isHookClientSupport + binding.inactiveModeIcon.isGone = HookEntry.isHookClientSupport + binding.unsupportItem.isGone = HookEntry.isHookClientSupport + /** 刷新当前模式文本 */ + fun refreshCurrentModeText() { + binding.currentModeText.text = when { + ConfigData.isDisableAllHook -> "模块已停用" + packageName == PackageName.WECHAT -> "仅限基础省电模式" + ConfigData.isEnableQQTimProtectMode -> "已启用保守模式" + else -> "已启用完全模式" + } + } + refreshCurrentModeText() + /** 刷新配置条目显示隐藏状态 */ + fun refreshConfigItems() { + binding.itemQqTimConfig.isVisible = packageName != PackageName.WECHAT && ConfigData.isDisableAllHook.not() + } + refreshConfigItems() + binding.infoTipText.replaceToAppName() + binding.qqTimProtectTipText.replaceToAppName() + binding.disableAllHookSwitch.bind(ConfigData.DISABLE_ALL_HOOK) { refreshConfigItems(); refreshCurrentModeText(); showRestartDialog() } + binding.qqTimProtectModeSwitch.bind(ConfigData.ENABLE_QQ_TIM_PROTECT_MODE) { refreshCurrentModeText(); showRestartDialog() } + binding.qqTimCoreServiceSwitch.bind(ConfigData.ENABLE_KILL_QQ_TIM_CORESERVICE) { showRestartDialog() } + binding.qqTimCoreServiceChildSwitch.bind(ConfigData.ENABLE_KILLE_QQ_TIM_CORESERVICE_CHILD) { showRestartDialog() } + } + + /** 显示重新启动对话框 */ + private fun showRestartDialog() { + showDialog { + title = "需要重新启动" + msg = "你必须重新启动${appName}才能使当前更改生效,现在重新启动吗?" + confirmButton { + cancel() + finish() + exitProcess(status = 0) + } + cancelButton(text = "稍后再说") + } + } + + /** 替换占位符到当前 APP 名称 */ + private fun TextView.replaceToAppName() { + text = text.toString().replace(oldValue = "{APP_NAME}", appName) + } + + /** + * 获取当前 APP 名称 + * @return [String] + */ + private val appName by lazy { findAppName().let { if (packageName == PackageName.WECHAT) it else " $it " } } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_config.xml b/app/src/main/res/layout/activity_config.xml new file mode 100644 index 0000000..6b8f901 --- /dev/null +++ b/app/src/main/res/layout/activity_config.xml @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d8dc1d..f720032 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,7 +7,7 @@ android:background="@color/colorThemeBackground" android:orientation="vertical" tools:context=".ui.activity.MainActivity" - tools:ignore="HardcodedText,UseCompoundDrawables,ContentDescription,TooManyViews"> + tools:ignore="HardcodedText,UseCompoundDrawables,ContentDescription,UnusedAttribute"> + android:tint="@color/colorTextGray" + android:tooltipText="项目地址" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -741,16 +504,6 @@ android:textColor="@color/colorTextDark" android:textSize="12sp" /> - - 0CvrU!ok}3as!=utw z4}fUw8O7RB+kxq7NTJlvVYjNNGo4;7v;)1f5O57*OB~vCt8i8<@v3ptOURBn00Fvp z26@Z`Cen%a_~_SmD4a;+4Of;tM8gzNq^tlbjqQHJF&cPg+gaiBr`n8=*$M1Oan|f_ zNleqLYaRCRxUvmJ$}og}`ix_2G4Tcck-Vm-K<~0bve=EGXo(Np4}C>sP*1DOW`ABs zs}4xRy0lRQiM@662lbC}H7^!yr{pt@920V}EnM_C=9sq?W{6pY8c9Xhnr!7^U4Z(` zF%dn)d&>SKcj`o;smxb*;^q4y4?Mp)lYZGEkIwPGTGPcdw-PnKbDpgaZdR}ketQvn zz;9~Dj(P#&6W1YT>S>5_%QD^F{Q2#hU$KjOQ@m{U6}E!!Rp1>3OZA|-;yNkjc9BB8 z)!>OO0G*qOz7ie%%t%XX$ZuGr%>?fEVaHdjR)^s+%A55f%&dE1;bE3)-T6lUWL949 zt!kKNz%CL#j6zlYfZU&}FpzHWck1RrWh=2&D4{$l89b;D+XL%uZEZg*9Ez9RM{z8V-m1z1*=ClLmBKse@Br9+?MzIguUDzUkZg zSu9dRtM%4^f8)eu?cM%r6@rCab|72fM|u{KBON{JI~%n)yexiE>d_Mz{L%2v0pp2K zg3CRAq24?jJ~CQ)psbs-39O;3J}HkZuJhk0;i9U_K&e{Eg0uZzGt&H=nyrvS ziy~L_2sBhT?*X0+DA<{-Lq_d!qYffoH`>|Qe3=Y z`+LV!*l~ilE?mw(iQ>u)h-%@lPB=t);fF;|^B<7AxT}76L=w*+#~Hm3ZRu~uh3GZb z_Qu(zOoGf)2Exmne`2VvMUP#T_tv!e1>K;qa|3m`A2!x32BT3kcLDVW&AWz`wU@V? zQU*YN-O)JVWA!sv!yiQ|cPB?#s&d25fg~XTKAGg6p#lHM^7ogQE`of_6(H`PAOU0& zexkcg!7F0vM_Ht6b6u9&tRHyiDWQ5KG^##fKe6xP-D)TX|wA$zlr%J2%1JI zq&tHaJeLNNdYNC(>wVffv_D%ye+T0H3Qnls&P`@uJOqPOxpN?-Owz$0h86%S|2T4$D9ggpsWgC;dmDbgPs|tNq>CXffQt8Sl zT-@WIp2gmoEG~l5_ahKI^MfL3#pE2?6BLc&0(K^fMQxGh_Y@%6G4gCbLS zTG;dTACvvIwldQQaL=c+!VZJ{)Q4L_2H(bH|08Jk`M@ZrwRyPmS8x&~H}S}t{S{v1 zkNIMHjN9=KJ5~57t2EB9Cg*uy00;ePmYbm$~8q>1=>#s`ecR^s&L2AK9Qp?`w29fW%K;ux0add`R z`=|RJ=EyvlK*X6KPt$X2hl-MegO@Uhn<5`l_WvDWf9NXeUE`J~@AOosuM5>xqRv%0Au&y%;ZZT#doJd9N&>5t_d?BZ zY^T>AapeF(d%O`~oekB;#zbTnFYqij$NkUqk@K@U=L$gK#brNtH9_IH| zpbdqrC}21;_LfFF>2>*WNa;UMX0eK#{opNzMwHQCjEgnulgcXzW#=L9a?sTv2J{iY z%Fs%)0~+=j;q&MB`uGiZ%Ew$wXhSimYFqhHqWqo3cwRtM=)J5*TKQcQ=*hVl7o;Ww zmPwq_V`N&N^*j2dPabAs0lq$=uy-5HK&=sSMr!*o8p9uch2$6yuPlPrg?fiyWc?RR=t||kh zlR+r$D&6FgxXUf$0V9FU?fN>F{Z7#Iea#7w`^?M$afE|b5UC^pCa6c7Mf_XtboDXB zBF_q2wuA?g^1Wdl>Y&e-&QtC6e;{Y%aY3iAUK4t5jhotcnc-2>jJsJHbCqX>{}j2n zH0>a#^R_017aTDjZ_;@YYC||yFI5TGC$u&Q8rjMP4Ei$~?-1QIf=C`|unuk7BCrTd zA9K`Y6skN_e102LaYuH#22^ob0`YdX`x?>xN=@CDkHR&6@Ys+fJ?I1t7lukIIyH{Y=EeV!%5rHI8{#4h0eI@q=DrHpmcmjhd0OdWHFl<1Q9ycy1vrXM! zmtE!UfZ6c4*ro}FC&&Q$01%FdlnRwctvL2fbno<1hIeYYsv#*N%zuhbpoYUr3&#s2 z+gyuT4ykUEcvb(w(Me4A^Ic+FVq3O-=a$ehh_XUS_{%$eeSOiYC3m{4{K3*A3czIU zCj$z$!4T7%rwJu%B&YFrNp38SHTw1EpGv^yZ%^DTR$`9_^O)6(BF@utG#J5BV8WE8 zZ@0y`k^sNKofR4VbQa{9Zt;NpEvr{6Ldxz3_p7MTgSp*mU0L~d@`lh3a@xeaNjGz! z3Ub5Hkf3IqJ(vnsCffTW0kUo~_Ba5PM$6Jw-?GaV*EGYDIEU#kj(rwmA4aLllot?| z)w3ka4(*5TpTEU`|7Vgc(ZFWMCnJjl^9}`LBxf#Q1t2KiP++w961O}x z(~#4n$8ox^MA!+K=@jQ&Y(pu(v!bwLom*A$pWwvZ@;gquai_Byxtee_`0wk#CAS*M zWv4{h(sF8cRQ=mF@qZIKq7m|I23*im0Nk*vE#aEr~Jy*3Dcu!&Gj zObbcg$o3u)F14bcddqiz2k4d9uTm# Lbi~w}`=tFBNqyK~ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_bug.png b/app/src/main/res/mipmap-xxhdpi/ic_bug.png deleted file mode 100644 index 00bbd55ebe92fb8ed5bab19a5f5c650144198d05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5428 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 R|N2uFVQz2MVB&xN-vILE%yj?& literal 0 HcmV?d00001