├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── libs
│ └── mi_charge.jar
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ └── xposed_init
│ ├── kotlin
│ └── cn
│ │ └── buffcow
│ │ └── hypersc
│ │ ├── HookEntry.kt
│ │ ├── hook
│ │ └── ProtectFragmentHooker.kt
│ │ ├── utils
│ │ ├── ChargeProtectionUtils.kt
│ │ ├── ProtectNotificationHelper.kt
│ │ └── RemoteEventHelper.kt
│ │ └── view
│ │ └── ChargeValueSetDialogView.kt
│ ├── res
│ ├── values-zh-rCN
│ │ └── strings.xml
│ ├── values-zh-rTW
│ │ └── strings.xml
│ └── values
│ │ └── strings.xml
│ └── resources
│ └── META-INF
│ └── yukihookapi_init
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea
3 | .gradle
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 智能断充(HyperSmartCharge)
2 | 为支持**电池充电保护**的设备添加自定义断充阈值设置,妈妈再也不用担心我的手机过充啦!
3 |
4 | Add custom charge protect value setting for devices that support **battery charge protection**.
5 |
6 | > [!WARNING]
7 | > 本模块的使用所产生的所有后果,由使用者自行承担,项目组不承担任何责任,使用者应自行评估并承担相关风险。
8 | > 本项目不对任何任何衍生项目负责。
9 |
10 | ## 使用
11 | 1. 在 LSPosed 管理器中激活模块
12 | 2. 作用域勾选 安全服务(**`com.miui.securitycenter`**)
13 | 3. **重启手机**
14 | 4. 转到 **`省电与电池`** —— **`电池保护`** —— **`智能断充`** 设置阈值
15 |
16 | ## 注意
17 | 1. 需要设备本身支持电池保护功能并搭载 HyperOS 的系统
18 | 2. 支持调节范围为 20-100,推荐设置在 70-90 之间
19 | 3. 目前仅在 小米14(pro) 下测试通过
20 |
21 | ## 下载
22 | [LSPosed 仓库](https://github.com/Xposed-Modules-Repo/cn.buffcow.hypersc/releases)
23 |
24 | ## 无效
25 | 请先检查设备是否支持电池保护功能,模块是否正常激活,并且作用域是否勾选。
26 |
如果排查后仍有错误,请提交issue。或联系酷安[@buffcow](http://www.coolapk.com/u/1188320)(qingyu)
27 |
28 | ## 致谢
29 | 模块使用 [Yuki Hook API](https://github.com/fankes/YukiHookAPI) 构建
30 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import com.android.build.api.dsl.ApkSigningConfig
4 | import com.android.build.gradle.internal.api.BaseVariantOutputImpl
5 | import java.util.Properties
6 |
7 | plugins {
8 | alias(libs.plugins.agp.app)
9 | alias(libs.plugins.kotlin)
10 | alias(libs.plugins.ksp)
11 | alias(libs.plugins.kotlin.parcelize)
12 | }
13 |
14 | val localProp by lazy {
15 | Properties().apply {
16 | rootProject.file("local.properties").takeIf(File::exists)?.let {
17 | load(it.bufferedReader())
18 | }
19 | }
20 | }
21 | var releaseSigningCfg: ApkSigningConfig? = null
22 |
23 | android {
24 | localProp["sign.storeFile"]?.let(::file)?.takeIf { it.exists() }?.let { signFile ->
25 | signingConfigs {
26 | create("release") {
27 | enableV3Signing = true
28 | storeFile = signFile
29 | keyAlias = localProp.getProperty("sign.keyAlias")
30 | keyPassword = localProp.getProperty("sign.keyPassword")
31 | storePassword = localProp.getProperty("sign.storePassword")
32 | }.also { releaseSigningCfg = it }
33 | }
34 | }
35 |
36 | buildTypes {
37 | release {
38 | isMinifyEnabled = true
39 | isShrinkResources = true
40 | proguardFiles(
41 | getDefaultProguardFile("proguard-android-optimize.txt"),
42 | "proguard-rules.pro"
43 | )
44 | }
45 |
46 | all {
47 | signingConfig = releaseSigningCfg ?: signingConfigs["debug"]
48 | }
49 | }
50 |
51 | buildFeatures {
52 | buildConfig = true
53 | }
54 |
55 | applicationVariants.all {
56 | outputs.all {
57 | val appName = rootProject.name
58 | val newApkName = "$appName-${versionName}_$versionCode.apk"
59 | (this as BaseVariantOutputImpl).outputFileName = newApkName
60 | }
61 | }
62 | }
63 |
64 | dependencies {
65 | ksp(libs.ksp.yuki.xposed)
66 | implementation(libs.api.yuki)
67 | implementation(libs.androidx.annotation)
68 |
69 | compileOnly(libs.api.xposed)
70 | compileOnly(files("libs/mi_charge.jar"))
71 | }
72 |
--------------------------------------------------------------------------------
/app/libs/mi_charge.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buffcow/HyperSmartCharge/decc2615d727ebf581ad56875de87524e601836f/app/libs/mi_charge.jar
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | -keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | -renamesourcefileattribute ''
22 |
23 | -keep class cn.buffcow.hypersc.HookEntry
24 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
Create on 2023/12/20 22:46
15 | */ 16 | @InjectYukiHookWithXposed 17 | class HookEntry : IYukiHookXposedInit { 18 | 19 | override fun onHook() = encase { 20 | loadApp("com.miui.securitycenter") { 21 | withProcess(mainProcessName) { 22 | loadHooker(ProtectFragmentHooker) 23 | } 24 | withProcess("${mainProcessName}.remote") { 25 | onAppLifecycle { 26 | onCreate { 27 | ProtectNotificationHelper.registerBatteryReceiver(this) 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | override fun onInit() = configs { 35 | debugLog { isDebug = BuildConfig.DEBUG; tag = LOG_TAG } 36 | } 37 | 38 | companion object { 39 | private const val LOG_TAG = "HyperSmartCharge" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/buffcow/hypersc/hook/ProtectFragmentHooker.kt: -------------------------------------------------------------------------------- 1 | package cn.buffcow.hypersc.hook 2 | 3 | import android.app.AlertDialog 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.widget.Toast 7 | import cn.buffcow.hypersc.R 8 | import cn.buffcow.hypersc.utils.ChargeProtectionUtils 9 | import cn.buffcow.hypersc.utils.RemoteEventHelper 10 | import cn.buffcow.hypersc.view.ChargeValueSetDialogView 11 | import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker 12 | import com.highcapable.yukihookapi.hook.factory.method 13 | import com.highcapable.yukihookapi.hook.log.YLog 14 | import com.highcapable.yukihookapi.hook.type.android.BundleClass 15 | import com.highcapable.yukihookapi.hook.type.java.BooleanType 16 | import com.highcapable.yukihookapi.hook.type.java.StringClass 17 | import de.robv.android.xposed.XposedHelpers 18 | import de.robv.android.xposed.XposedHelpers.callMethod 19 | 20 | 21 | /** 22 | * Hooker for charge protect fragment. 23 | * 24 | * @author qingyu 25 | *Create on 2025/01/02 16:04
26 | */ 27 | object ProtectFragmentHooker : YukiBaseHooker() { 28 | 29 | private const val PREFERENCE_KEY_ALWAYS_PROTECT = "cb_always_charge_protect" 30 | private const val PREFERENCE_KEY_INTELLECT_PROTECT = "cb_intellect_charge_protect" 31 | private const val PREFERENCE_KEY_CATEGORY_PROTECT = "category_features_battery_protect" 32 | private const val PREFERENCE_KEY_SMART_CHARGE_VALUE_SET = "charge_protect_value_setting" 33 | 34 | override fun onHook() { 35 | "com.miui.powercenter.nightcharge.ChargeProtectFragment".toClass().method { 36 | name = "onCreatePreferences" 37 | paramCount = 2 38 | param(BundleClass, StringClass) 39 | }.hook().after { onCreatePreferences(instance) } 40 | } 41 | 42 | private fun onCreatePreferences(fragment: Any) { 43 | val smartChargeAvailable = checkSmartChargeAvailable(fragment) 44 | YLog.debug("onCreatePreferences() called with: fragment = $fragment, available=$smartChargeAvailable") 45 | if (smartChargeAvailable) { 46 | fragment.javaClass.apply { 47 | method { 48 | name = "onPreferenceClick" 49 | paramCount = 1 50 | returnType = BooleanType 51 | }.hook().after { onPreferenceClick(instance, args(0).any()) } 52 | 53 | method { 54 | name = "onPreferenceChange" 55 | paramCount = 2 56 | returnType = BooleanType 57 | }.hook().after { 58 | onPreferenceChange(instance, args(0).any(), args(1).any()) 59 | } 60 | } 61 | addSmartChargeTextPreference(fragment) 62 | } else { 63 | appContext?.let { RemoteEventHelper.sendEvent(it, RemoteEventHelper.Event.UnregisterBatteryReceiver) } 64 | } 65 | } 66 | 67 | private fun onPreferenceClick(fragment: Any, preference: Any?) { 68 | YLog.debug("onPreferenceClick() called with: preference = $preference") 69 | if (callMethod(preference, "getKey") == PREFERENCE_KEY_SMART_CHARGE_VALUE_SET) { 70 | val context = getContext(fragment) 71 | val dialogView = ChargeValueSetDialogView(context) { moduleAppResources } 72 | AlertDialog.Builder(context) 73 | .setTitle(moduleAppResources.getString(R.string.app_name)) 74 | .setView(dialogView) 75 | .setPositiveButton(android.R.string.ok, null) 76 | .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } 77 | .create().apply { 78 | show() 79 | getButton(Dialog.BUTTON_POSITIVE).setOnClickListener { 80 | val (suc, value) = dialogView.syncProtectValue() 81 | Toast.makeText(context, "$suc($value)", Toast.LENGTH_SHORT).show() 82 | callMethod(preference, "setText", getSmartChargeValueText(getContext(fragment), value)) 83 | appContext?.let { 84 | val pv = if (suc) value?.toString() else null 85 | RemoteEventHelper.sendEvent(it, RemoteEventHelper.Event.UpdateNotification(pv)) 86 | } 87 | dismiss() 88 | } 89 | } 90 | } 91 | } 92 | 93 | private fun onPreferenceChange(fragment: Any, preference: Any?, obj: Any?) { 94 | YLog.debug("onPreferenceChange() called with: preference = $preference, obj = $obj") 95 | preference ?: return 96 | getProtectCategory(fragment) 97 | ?.let { findPreference(it, PREFERENCE_KEY_SMART_CHARGE_VALUE_SET) } 98 | ?.takeIf { 99 | callMethod(preference, "getKey").let { k -> 100 | k == PREFERENCE_KEY_ALWAYS_PROTECT 101 | || k == PREFERENCE_KEY_INTELLECT_PROTECT 102 | } 103 | } 104 | ?.let { pref -> 105 | callMethod( 106 | pref, 107 | "setEnabled", 108 | (obj as? Boolean)?.not() ?: checkSmartChargeShouldEnable(fragment) 109 | ) 110 | } 111 | } 112 | 113 | private fun addSmartChargeTextPreference(fragment: Any) { 114 | XposedHelpers.newInstance("miuix.preference.TextPreference".toClass(), getContext(fragment)).apply { 115 | callMethod(this, "setOnPreferenceClickListener", fragment) 116 | callMethod(this, "setKey", PREFERENCE_KEY_SMART_CHARGE_VALUE_SET) 117 | callMethod(this, "setEnabled", checkSmartChargeShouldEnable(fragment)) 118 | callMethod(this, "setText", getSmartChargeValueText(getContext(fragment))) 119 | callMethod(this, "setTitle", moduleAppResources.getString(R.string.app_name)) 120 | callMethod(this, "setSummary", moduleAppResources.getString(R.string.smart_charge_pref_summary)) 121 | getProtectCategory(fragment)?.let { callMethod(it, "addPreference", this) } 122 | } 123 | } 124 | 125 | private fun getSmartChargeValueText( 126 | context: Context, 127 | value: Int? = ChargeProtectionUtils.getSmartChargePercentValue(context), 128 | ): String { 129 | return value?.takeIf(ChargeProtectionUtils::isSmartChargePercentValueValid)?.let { 130 | moduleAppResources.getString(R.string.smart_charge_value_per, it) 131 | } ?: moduleAppResources.getString(R.string.smart_charge_close) 132 | } 133 | 134 | private fun checkSmartChargeAvailable(fragment: Any) = getProtectCategory(fragment)?.let { 135 | findPreference(it, PREFERENCE_KEY_INTELLECT_PROTECT) 136 | } != null 137 | 138 | private fun checkSmartChargeShouldEnable(fragment: Any) = try { 139 | getProtectCategory(fragment)?.run { 140 | val always = findPreference(this, PREFERENCE_KEY_ALWAYS_PROTECT)?.let { 141 | callMethod(it, "isChecked") 142 | } 143 | val intellect = findPreference(this, PREFERENCE_KEY_INTELLECT_PROTECT)?.let { 144 | callMethod(it, "isChecked") 145 | } 146 | always != true && intellect != true 147 | } == true 148 | } catch (th: Throwable) { 149 | YLog.error("checkSmartChargeShouldEnable error:", th) 150 | false 151 | } 152 | 153 | private fun getProtectCategory(fragment: Any) = findPreference( 154 | fragment, 155 | PREFERENCE_KEY_CATEGORY_PROTECT 156 | ) 157 | 158 | private fun getContext(fragment: Any): Context { 159 | return callMethod(fragment, "requireContext") as Context 160 | } 161 | 162 | private fun findPreference(who: Any, key: String): Any? = callMethod(who, "findPreference", key) 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/buffcow/hypersc/utils/ChargeProtectionUtils.kt: -------------------------------------------------------------------------------- 1 | package cn.buffcow.hypersc.utils 2 | 3 | import android.content.Context 4 | import android.provider.Settings 5 | import com.highcapable.yukihookapi.hook.log.YLog 6 | import miui.util.IMiCharge 7 | 8 | /** 9 | * @author qingyu 10 | *Create on 2025/01/03 17:12
11 | */ 12 | object ChargeProtectionUtils { 13 | 14 | const val MIN_CHARGE_PERCENT_VALUE = 20 15 | const val MAX_CHARGE_PERCENT_VALUE = 100 16 | 17 | private const val KEY_SMART_CHARGE_PERCENT_VALUE = "smart_charge_percent_value" 18 | 19 | fun closeSmartCharge(): Boolean = setSmartChargeValue("0x10") 20 | 21 | fun openCommonProtectMode(value: Int): Boolean { 22 | val valueToSet = "0x${((value shl 16) or 17).toString(16)}" 23 | val res = setSmartChargeValue(valueToSet) 24 | YLog.debug("openCommonProtectMode:$res, setValue:$valueToSet") 25 | return res 26 | } 27 | 28 | private fun getSmartChargeValue(): String? = try { 29 | IMiCharge.getInstance().getMiChargePath("smart_chg").also { 30 | YLog.debug("getSmartChargeValue res:$it") 31 | } 32 | } catch (th: Throwable) { 33 | YLog.error("getSmartChargeValue error:", th) 34 | null 35 | } 36 | 37 | private fun setSmartChargeValue(value: String): Boolean = try { 38 | IMiCharge.getInstance().setMiChargePath("smart_chg", value).also { 39 | YLog.debug("setSmartChargeValue:$value, res:$it") 40 | } 41 | } catch (th: Throwable) { 42 | YLog.error("setSmartChargeValue error:", th) 43 | false 44 | } 45 | 46 | fun getSmartChargePercentValue(ctx: Context): Int? { 47 | val value = ctx.getPercentValue() ?: return null 48 | return when { 49 | !isSmartChargePercentValueValid(value) -> { 50 | YLog.warn("smart charge percent value invalid, remove now") 51 | ctx.putPercentValue(null) 52 | } 53 | 54 | (getSmartChargeValue()?.toIntOrNull() ?: 1) <= 0 -> { 55 | val res = openCommonProtectMode(value) 56 | YLog.warn("maybe reboot or smart_chg value changed by sys, retry set:$res") 57 | if (res) value else ctx.putPercentValue(null) 58 | } 59 | 60 | else -> value 61 | } 62 | } 63 | 64 | fun putSmartChargePercentValue(ctx: Context, value: Int?) { 65 | ctx.putPercentValue(value?.takeIf(ChargeProtectionUtils::isSmartChargePercentValueValid)) 66 | } 67 | 68 | fun isSmartChargePercentValueValid(perValue: Int): Boolean { 69 | return perValue in MIN_CHARGE_PERCENT_VALUE..MAX_CHARGE_PERCENT_VALUE 70 | } 71 | 72 | private fun Context.getPercentValue(): Int? { 73 | return Settings.System.getString(contentResolver, KEY_SMART_CHARGE_PERCENT_VALUE)?.toIntOrNull() 74 | } 75 | 76 | private fun Context.putPercentValue(value: Int?): Int? { 77 | return if (Settings.System.putString( 78 | contentResolver, 79 | KEY_SMART_CHARGE_PERCENT_VALUE, 80 | value?.toString() 81 | ) 82 | ) value else null 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/buffcow/hypersc/utils/ProtectNotificationHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.buffcow.hypersc.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.app.PendingIntent 8 | import android.content.BroadcastReceiver 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.content.IntentFilter 12 | import android.graphics.drawable.Icon 13 | import android.os.BatteryManager 14 | import com.highcapable.yukihookapi.hook.log.YLog 15 | import java.util.concurrent.atomic.AtomicBoolean 16 | 17 | /** 18 | * Helper for notification. 19 | * 20 | * @author qingyu 21 | *Create on 2025/01/06 19:25
22 | */ 23 | object ProtectNotificationHelper : RemoteEventHelper.EventListener { 24 | 25 | private const val NOTIFICATION_ID = 1008611 26 | private const val CHANNEL_ID = "com.miui.powercenter.low" 27 | 28 | private var notificationShowed = false 29 | private val batteryRegistered = AtomicBoolean(false) 30 | 31 | private val batteryReceiver = object : BroadcastReceiver() { 32 | override fun onReceive(context: Context, intent: Intent) { 33 | if (intent.action != Intent.ACTION_BATTERY_CHANGED) return 34 | 35 | val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 100) 36 | val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) 37 | val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) 38 | 39 | when (status) { 40 | BatteryManager.BATTERY_STATUS_FULL, 41 | BatteryManager.BATTERY_STATUS_CHARGING, 42 | -> if (level <= 100 && plugged != 0) createAndShowNotification(context) 43 | 44 | else -> removeNotification(context) 45 | } 46 | } 47 | } 48 | 49 | fun registerBatteryReceiver(context: Context) { 50 | if (batteryRegistered.compareAndSet(false, true)) { 51 | context.applicationContext.registerReceiver( 52 | batteryReceiver, 53 | IntentFilter(Intent.ACTION_BATTERY_CHANGED) 54 | ) 55 | RemoteEventHelper.register(context, this) 56 | YLog.debug("registered battery changed receiver.") 57 | } 58 | } 59 | 60 | override fun onReceive(context: Context, event: RemoteEventHelper.Event, intent: Intent) { 61 | YLog.debug("receive client event:$event, intent:$intent") 62 | when (event) { 63 | RemoteEventHelper.Event.UnregisterBatteryReceiver -> { 64 | unregisterBatteryReceiver(context) 65 | } 66 | 67 | is RemoteEventHelper.Event.UpdateNotification -> { 68 | event.percentValue?.let { value -> 69 | if (notificationShowed) { 70 | publishNotification(context, value) 71 | } else if (batteryRegistered.get()) { 72 | context.registerReceiver( 73 | null, 74 | IntentFilter(Intent.ACTION_BATTERY_CHANGED) 75 | )?.let { batteryReceiver.onReceive(context, it) } 76 | } 77 | } ?: removeNotification(context) 78 | } 79 | } 80 | } 81 | 82 | private fun unregisterBatteryReceiver(context: Context) { 83 | if (batteryRegistered.compareAndSet(true, false)) { 84 | context.applicationContext.unregisterReceiver(batteryReceiver) 85 | removeNotification(context) 86 | YLog.debug("unregistered battery changed receiver.") 87 | } 88 | } 89 | 90 | fun createAndShowNotification(context: Context) { 91 | if (notificationShowed) return 92 | val perChg = ChargeProtectionUtils.getSmartChargePercentValue(context) 93 | YLog.debug("showNotification-smart charge percent value:$perChg") 94 | perChg ?: return 95 | 96 | val notificationManager = context.getSystemService(NotificationManager::class.java) 97 | notificationManager.createNotificationChannel( 98 | NotificationChannel( 99 | CHANNEL_ID, 100 | context.getString("battery_and_property_ordinary_notify"), 101 | NotificationManager.IMPORTANCE_LOW 102 | ) 103 | ) 104 | 105 | publishNotification(context, "$perChg", notificationManager) 106 | 107 | notificationShowed = true 108 | } 109 | 110 | @SuppressLint("NotificationPermission") 111 | private fun publishNotification( 112 | context: Context, 113 | percentValue: String, 114 | notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java), 115 | ) { 116 | val icon = Icon.createWithResource( 117 | context, 118 | "ic_performance_notification".resolveAsId(context, "drawable") 119 | ) 120 | val intent = PendingIntent.getActivity( 121 | context, 0, 122 | Intent( 123 | context, 124 | context.classLoader.loadClass("com.miui.powercenter.nightcharge.ChargerProtectActivity") 125 | ), 126 | PendingIntent.FLAG_IMMUTABLE 127 | ) 128 | Notification.Builder(context, CHANNEL_ID) 129 | .setSmallIcon(icon) 130 | .setContentTitle(context.getString("pc_health_charge_protect_title")) 131 | .setContentText(context.getString("pc_health_charge_protect_noti_summary_title", "$percentValue%")) 132 | .setAutoCancel(true) 133 | .setContentIntent(intent) 134 | .setVisibility(Notification.VISIBILITY_PUBLIC) 135 | .build() 136 | .apply { notificationManager.notify(NOTIFICATION_ID, this) } 137 | } 138 | 139 | private fun removeNotification(context: Context) { 140 | context.getSystemService(NotificationManager::class.java).cancel(NOTIFICATION_ID) 141 | notificationShowed = false 142 | } 143 | 144 | private fun Context.getString(name: String, vararg args: String): String { 145 | return getString(name.resolveAsId(this, "string"), *args) 146 | } 147 | 148 | @SuppressLint("DiscouragedApi") 149 | private fun String.resolveAsId(context: Context, type: String): Int { 150 | return context.resources.getIdentifier( 151 | this, 152 | type, 153 | context.packageName 154 | ) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/buffcow/hypersc/utils/RemoteEventHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.buffcow.hypersc.utils 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.os.Parcelable 8 | import kotlinx.parcelize.Parcelize 9 | import java.util.concurrent.CopyOnWriteArrayList 10 | 11 | /** 12 | * Helper for cross process event。 13 | * 14 | * @author qingyu 15 | *Create on 2025/01/08 15:24
16 | */ 17 | object RemoteEventHelper { 18 | 19 | private val listeners = CopyOnWriteArrayListCreate on 2025/01/03 18:50
16 | */ 17 | class ChargeValueSetDialogView @JvmOverloads constructor( 18 | context: Context, 19 | private val moduleResourcesFetcher: () -> Resources = { context.resources }, 20 | ) : LinearLayout(context), SeekBar.OnSeekBarChangeListener { 21 | 22 | private val mSeekBar: SeekBar 23 | 24 | private val mTvSeekBarValue: TextView 25 | 26 | private val moduleResources get() = moduleResourcesFetcher.invoke() 27 | 28 | private val Int.realProgressValue 29 | get() = (this + MIN_PROGRESS_VALUE).takeIf(ChargeProtectionUtils::isSmartChargePercentValueValid) 30 | 31 | init { 32 | orientation = VERTICAL 33 | layoutDirection = LAYOUT_DIRECTION_LTR 34 | addViewInLayout( 35 | TextView(context).also { mTvSeekBarValue = it }, 36 | 0, 37 | LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { 38 | gravity = Gravity.CENTER 39 | setMargins(0, 10.dp2px(), 0, 0) 40 | } 41 | ) 42 | addViewInLayout( 43 | SeekBar(context).also { mSeekBar = it }, 44 | 1, 45 | LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { 46 | setMargins(12.dp2px(), 28.dp2px(), 12.dp2px(), 0) 47 | } 48 | ) 49 | addViewInLayout( 50 | TextView(context).apply { 51 | text = moduleResources.getString(R.string.smart_charge_set_note).trimIndent() 52 | setTextColor(Color.GRAY) 53 | }, 54 | 2, 55 | LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { 56 | setMargins(12.dp2px(), 30.dp2px(), 12.dp2px(), 0) 57 | } 58 | ) 59 | } 60 | 61 | private fun initView() { 62 | mSeekBar.apply { 63 | max = MAX_PROGRESS_VALUE - MIN_PROGRESS_VALUE 64 | ChargeProtectionUtils.getSmartChargePercentValue(context)?.let { 65 | progress = (it - MIN_PROGRESS_VALUE).coerceAtLeast(0) 66 | } 67 | invalidateSeekbarValueText(progress) 68 | setOnSeekBarChangeListener(this@ChargeValueSetDialogView) 69 | } 70 | mTvSeekBarValue.setTextColor(Color.BLACK) 71 | } 72 | 73 | private fun invalidateSeekbarValueText(progress: Int) { 74 | mTvSeekBarValue.text = progress.realProgressValue?.let { pv -> 75 | moduleResources.getString(R.string.smart_charge_value_per, pv) 76 | } ?: moduleResources.getString(R.string.smart_charge_close) 77 | } 78 | 79 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { 80 | if (fromUser) invalidateSeekbarValueText(progress) 81 | } 82 | 83 | override fun onStartTrackingTouch(seekBar: SeekBar?) { 84 | } 85 | 86 | override fun onStopTrackingTouch(seekBar: SeekBar?) { 87 | } 88 | 89 | override fun onAttachedToWindow() { 90 | super.onAttachedToWindow() 91 | initView() 92 | } 93 | 94 | fun syncProtectValue(): Pair