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" /> - -