Compare commits

..

83 Commits

Author SHA1 Message Date
3ebf87c642 <powerbell>APK 15.14.41 release Publish. 2025-12-29 21:40:10 +08:00
347a4040cd <powerbell>Start New Stage Version. 2025-12-29 21:37:48 +08:00
6fd86a2742 添加TTS贴心服务,以免在充电时设置了服务提醒却不知道。 2025-12-29 21:36:34 +08:00
798357aedd 源码整理 2025-12-29 09:45:01 +08:00
9124303fd3 <powerbell>APK 15.14.40 release Publish. 2025-12-28 20:43:38 +08:00
414541093a <powerbell>Start New Stage Version. 2025-12-28 20:42:24 +08:00
13265be66e 添加点阵能量块风格。 2025-12-28 20:41:19 +08:00
ebd32adb68 <powerbell>APK 15.14.39 release Publish. 2025-12-28 13:54:20 +08:00
56a65cd10a 能量风格绘图方法切换回老式算法,修复能量条间隔缝隙问题。 2025-12-28 13:52:51 +08:00
e379684002 <powerbell>APK 15.14.38 release Publish. 2025-12-28 13:16:31 +08:00
4077ac18f6 <powerbell>Start New Stage Version. 2025-12-28 13:15:17 +08:00
278e690795 能量条绘图风格调试完成。 2025-12-28 13:14:24 +08:00
e81fc65b90 BatteryStyleView控件调试中。。。 2025-12-27 21:19:20 +08:00
abd956d7d0 添加BatteryStyleView控件,添加能量与斑马绘图风格。 2025-12-27 21:12:54 +08:00
fca17908b2 <powerbell>APK 15.14.37 release Publish. 2025-12-27 14:48:34 +08:00
39a3a5aeb0 更新黑白主题风格。 2025-12-27 14:45:13 +08:00
2e30f577b5 <powerbell>APK 15.14.36 release Publish. 2025-12-26 21:00:10 +08:00
dcb5355233 添加在切换主题时把主题基准色配置为背景图背景像素底色。 2025-12-26 20:58:31 +08:00
d1ced7ac63 <powerbell>APK 15.14.35 release Publish. 2025-12-26 20:13:09 +08:00
bf0cf23144 <powerbell>Start New Stage Version. 2025-12-26 20:11:34 +08:00
890b32ceae 修复像素拾取窗口菜单栏风格未统一问题。 2025-12-26 20:10:28 +08:00
c347d51c84 简化代码使用全局变量 2025-12-26 20:04:25 +08:00
7278e9f22f 源码整理 2025-12-26 19:47:06 +08:00
4e98c8d699 优化函数使用方式 2025-12-26 18:56:24 +08:00
3ec1bbe264 源码整理 2025-12-26 18:31:25 +08:00
f4a2a1585d 源码整理 2025-12-26 18:00:24 +08:00
35f4aa8730 源码整理 2025-12-26 17:42:30 +08:00
5b1d160dac <powerbell>APK 15.14.34 release Publish. 2025-12-26 02:58:19 +08:00
db87ba51e3 修复接收分享图片后的主界面更新问题。 2025-12-26 02:56:11 +08:00
fd6e852061 优化任务进程配置 2025-12-26 02:53:41 +08:00
8ff4b9e1f4 修复全局应用资源缓存不同步问题 2025-12-26 02:20:21 +08:00
78a2d85150 20251226_002506_076 正在调试设置窗口返回时的界面刷新。。。 2025-12-26 00:25:50 +08:00
fd2d1d17ed 注释调试码 2025-12-25 20:15:25 +08:00
37f3091103 <powerbell>APK 15.14.33 release Publish. 2025-12-25 20:10:33 +08:00
6dc5f05702 调整图片分享接收后的窗口切换逻辑。 2025-12-25 20:08:36 +08:00
77df2558b3 <powerbell>APK 15.14.32 release Publish. 2025-12-25 16:51:19 +08:00
315541f2d2 添加权限申请配置属性 2025-12-25 16:50:02 +08:00
f52bb54d22 <powerbell>APK 15.14.31 release Publish. 2025-12-25 16:31:07 +08:00
0742164f65 调整资源进程配置 2025-12-25 16:27:59 +08:00
fc785b9258 <powerbell>APK 15.14.30 release Publish. 2025-12-25 16:08:51 +08:00
0abeb982d2 增加自启成功概率 2025-12-25 16:04:23 +08:00
81f0e5c56e 视图缓存控件调试成功。 2025-12-25 16:00:17 +08:00
4fcce7edb3 20251225_153508_537 2025-12-25 15:35:12 +08:00
59f5eae136 <powerbell>APK 15.14.29 release Publish. 2025-12-24 21:23:33 +08:00
bf9479c53f 源码整理,最新像素背景位图缓存功能调试完成。 2025-12-24 21:21:38 +08:00
f4a485f1ff 线程提醒电量问题调试完成 2025-12-24 21:07:54 +08:00
80da7677f8 源码整理 2025-12-24 20:27:30 +08:00
e1c3c8f072 位图大小设置调试完成 2025-12-24 20:18:59 +08:00
2238d632f7 源码整理 2025-12-24 19:20:17 +08:00
558bc16013 位图平铺方式调试结束 2025-12-24 19:17:56 +08:00
7d50453e34 位图类处理模块重构 2025-12-24 17:28:55 +08:00
f771225830 <powerbell>APK 15.14.28 release Publish. 2025-12-24 12:22:06 +08:00
72794dc6e5 改进图片像素背景的绘图方法。 2025-12-24 12:19:48 +08:00
2ffdb26f69 图片加载逻辑优化,正在查询哪个类在使用背景像素。。。 2025-12-24 00:12:34 +08:00
3c6d269caf <powerbell>APK 15.14.27 release Publish. 2025-12-23 21:02:16 +08:00
2f7f6b62fd 设置应用初始背景颜色为白色,颜色清理按钮点击后,背景颜色改为主题风格的基准色。 2025-12-23 21:01:03 +08:00
db096e716c <powerbell>APK 15.14.26 release Publish. 2025-12-23 19:45:28 +08:00
1ce0a86b53 更新元素类库 2025-12-23 19:39:50 +08:00
1e04bd463f 改用jitpack.io库 2025-12-23 18:26:06 +08:00
7c0e723227 <powerbell>APK 15.14.25 release Publish. 2025-12-23 15:23:07 +08:00
accc276fd8 更新主题元素类库 2025-12-23 15:21:37 +08:00
16e581802f <powerbell>APK 15.14.24 release Publish. 2025-12-23 14:29:30 +08:00
3048963fa9 <powerbell>Start New Stage Version. 2025-12-23 14:28:13 +08:00
34082c49e9 源码整理 2025-12-23 14:16:58 +08:00
eff1822ee5 源码整理 2025-12-23 13:43:30 +08:00
4e84ff493b 源码整理 2025-12-23 13:12:17 +08:00
c2def0bb3b 源码整理 2025-12-22 23:19:50 +08:00
a086a47b2d 源码整理 2025-12-22 23:05:26 +08:00
c38b392e39 <powerbell>APK 15.14.23 release Publish. 2025-12-22 13:00:54 +08:00
0f736c2007 去掉位图压缩功能,尽量保持位图品质。 2025-12-22 12:59:50 +08:00
26b86cd715 <powerbell>APK 15.14.22 release Publish. 2025-12-22 11:21:24 +08:00
7888696e65 改进缓存策略。 2025-12-22 11:20:14 +08:00
8609c2f784 <powerbell>APK 15.14.21 release Publish. 2025-12-22 11:02:54 +08:00
863b743330 改进缓存策略,修复位图绘画时的异常。 2025-12-22 11:01:35 +08:00
61b7afa4b5 <powerbell>APK 15.14.20 release Publish. 2025-12-22 10:25:46 +08:00
8e4c7a6832 添加位图缓存 2025-12-22 10:24:29 +08:00
d29068b029 <powerbell>APK 15.14.19 release Publish. 2025-12-22 10:20:01 +08:00
51963f8e0f 优化启动界面显示 2025-12-22 10:18:28 +08:00
18a5762c15 <powerbell>APK 15.14.18 release Publish. 2025-12-22 10:04:49 +08:00
60de16ab45 <powerbell>Start New Stage Version. 2025-12-22 10:03:27 +08:00
6d60d71991 首页启动窗口改用视图控件缓存替代位图缓存。 2025-12-22 10:01:13 +08:00
0cfbc43acb MemoryCachedBackgroundView测试完成 2025-12-22 08:55:09 +08:00
20227e29ef 添加缓存视图控件类 2025-12-21 20:46:14 +08:00
72 changed files with 8166 additions and 4156 deletions

View File

@@ -83,11 +83,11 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0' //api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址 // WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libaes:15.12.0' //api 'cc.winboll.studio:libaes:15.12.7'
//api 'cc.winboll.studio:libappbase:15.12.2' //api 'cc.winboll.studio:libappbase:15.12.2'
// WinBoLL备用库 jitpack.io 地址 // WinBoLL备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:AES:aes-v15.12.3' api 'com.github.ZhanGSKen:AES:aes-v15.12.8'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1' api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
//api fileTree(dir: 'libs', include: ['*.aar']) //api fileTree(dir: 'libs', include: ['*.aar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sun Dec 21 19:13:50 HKT 2025 #Mon Dec 29 21:40:10 HKT 2025
stageCount=18 stageCount=42
libraryProject= libraryProject=
baseVersion=15.14 baseVersion=15.14
publishVersion=15.14.17 publishVersion=15.14.41
buildCount=0 buildCount=0
baseBetaVersion=15.14.18 baseBetaVersion=15.14.42

View File

@@ -4,6 +4,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell"> package="cc.winboll.studio.powerbell">
<!-- 此应用可显示在其他应用上方 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 运行前台服务 --> <!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -25,6 +28,9 @@
<!-- 计算应用存储空间 --> <!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/> <uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<!-- 请求忽略电池优化 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- 读取您共享存储空间中的内容 --> <!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
@@ -34,9 +40,6 @@
<!-- MANAGE_EXTERNAL_STORAGE --> <!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 请求忽略电池优化 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- 拍摄照片和视频 --> <!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
@@ -44,6 +47,24 @@
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS" android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false"/> android:required="false"/>
@@ -52,22 +73,6 @@
android:name="android.hardware.camera.autofocus" android:name="android.hardware.camera.autofocus"
android:required="false"/> android:required="false"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<queries> <queries>
<package android:name="com.miui.securitycenter"/> <package android:name="com.miui.securitycenter"/>
@@ -76,6 +81,7 @@
<application <application
android:name=".App" android:name=".App"
android:process=":main"
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -88,16 +94,11 @@
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity <activity
android:process=":main"
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:exported="true" android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask"/>
</activity>
<activity
android:name=".activities.CrashActivity"
android:exported="false"/>
<activity-alias <activity-alias
android:name=".MainActivityEN1" android:name=".MainActivityEN1"
@@ -166,12 +167,19 @@
</activity-alias> </activity-alias>
<activity <activity
android:process=":main"
android:name=".activities.CrashActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.ClearRecordActivity" android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity" android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:exported="false"/> android:exported="false"/>
<activity <activity
android:process=":main"
android:name=".activities.BackgroundSettingsActivity" android:name=".activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity" android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true" android:exported="true"
@@ -197,7 +205,49 @@
</activity> </activity>
<activity
android:process=":main"
android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.SettingsActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
android:exported="false"/>
<activity
android:process=":main"
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"/>
<receiver <receiver
android:process=":main"
android:name=".receivers.MainReceiver" android:name=".receivers.MainReceiver"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
@@ -220,7 +270,8 @@
android:priority="1000" android:priority="1000"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:process=".controlcenterservice" android:process=":main"
android:stopWithTask="false"
android:foregroundServiceType="dataSync"> android:foregroundServiceType="dataSync">
<property <property
@@ -233,7 +284,9 @@
android:name=".services.AssistantService" android:name=".services.AssistantService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:process=".assistantservice"> android:process=":assistant"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property <property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE" android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
@@ -241,39 +294,25 @@
</service> </service>
<meta-data <service
android:name="android.max_aspect" android:name=".services.TTSPlayService"
android:value="4.0"/> android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<activity <service android:name=".services.ThoughtfulService"
android:name=".activities.BatteryReporterActivity" android:enabled="true"
android:exported="false"/> android:exported="false"
android:process=":main"
<activity android:stopWithTask="false"/>
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:name=".activities.SettingsActivity"
android:exported="false"/>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true"
android:process=":main">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
@@ -281,13 +320,10 @@
</provider> </provider>
<activity <meta-data
android:name="com.yalantis.ucrop.UCropActivity" android:name="android.max_aspect"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" android:value="4.0"/>
android:exported="true">
</activity>
</application> </application>
</manifest> </manifest>

View File

@@ -1,10 +1,6 @@
package cc.winboll.studio.powerbell; package cc.winboll.studio.powerbell;
import android.content.ComponentCallbacks2;
import android.content.Context; import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
@@ -13,17 +9,26 @@ import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver; import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils; import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils; import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import java.io.File; import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
import java.util.concurrent.TimeUnit;
/** /**
* 应用全局入口类适配Android API 30基于Java 7编写 * 应用全局入口类适配Android API 30基于Java 7编写
* 核心策略:极致强制缓存 - 无论内存紧张程度永不自动清理任何缓存Bitmap/视图控件/路径记录)
*
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Version 1.0.0
* @Date 2025-12-29
*/ */
public class App extends GlobalApplication { public class App extends GlobalApplication {
// ===================== 常量定义区 =====================
public static final String TAG = "App"; // ==================== 常量定义 ====================
private static final String TAG = "App";
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
private static final int INVALID_BATTERY_VALUE = -1;
// 组件跳转常量 // 组件跳转常量
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1"; public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
@@ -35,304 +40,269 @@ public class App extends GlobalApplication {
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"; public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"; public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
// 内存紧张通知文案常量 // ==================== 静态属性 ====================
private static final String TRIM_MEMORY_NOTIFY_TITLE = "应用使用时内存紧张提醒";
private static final String TRIM_MEMORY_NOTIFY_CONTENT = "由于本应用使用时,系统通知内存紧张程度级别较高,图片缓存功能暂时不启用。";
// 定时任务间隔常量(分钟) private static App sApp;
private static final long TIMER_INTERVAL_MINUTES = 1;
// ===================== 静态属性区 ===================== // 配置与缓存
// 数据配置工具 public static AppConfigUtils sAppConfigUtils;
private static AppConfigUtils sAppConfigUtils;
private static AppCacheUtils sAppCacheUtils; private static AppCacheUtils sAppCacheUtils;
// 全局Bitmap缓存工具
// 资源与视图缓存(强制驻留)
public static BackgroundSourceUtils sBackgroundSourceUtils;
public static BitmapCacheUtils sBitmapCacheUtils; public static BitmapCacheUtils sBitmapCacheUtils;
// 临时文件夹路径 private static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
private static String sTempDirPath = "";
// 定时任务静态属性(全局唯一) // 状态
private static Handler sTimerHandler; public static volatile int sQuantityOfElectricity = INVALID_BATTERY_VALUE;
private static Runnable sTimerRunnable;
private static boolean sIsTimerRunning = false; // 系统工具
private static NotificationManagerUtils sNotificationManagerUtils;
// ==================== 成员属性 ====================
// ===================== 成员属性区 =====================
// 全局广播接收器
private GlobalApplicationReceiver mGlobalReceiver; private GlobalApplicationReceiver mGlobalReceiver;
// 通知管理工具
private NotificationManagerUtils mNotificationManager;
// ===================== 公共方法区 ===================== // ==================== 公共静态方法 (工具/单例) ====================
/** /**
* 获取临时文件夹路径 * 获取应用单例
*/ */
public static String getTempDirPath() { public static App getInstance() {
return sTempDirPath; LogUtils.d(TAG, "getInstance() called | Result: " + sApp);
return sApp;
} }
/** /**
* 获取应用配置工具实例 * 获取配置工具实例
*/ */
public static AppConfigUtils getAppConfigUtils(Context context) { public static AppConfigUtils getAppConfigUtils(Context context) {
LogUtils.d(TAG, "getAppConfigUtils() 调用传入Context" + context.getClass().getSimpleName()); String contextName = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "getAppConfigUtils() called with: context = [" + contextName + "]");
if (sAppConfigUtils == null) { if (sAppConfigUtils == null) {
sAppConfigUtils = AppConfigUtils.getInstance(context); sAppConfigUtils = AppConfigUtils.getInstance(context);
LogUtils.d(TAG, "getAppConfigUtils()AppConfigUtils实例已初始化"); LogUtils.d(TAG, "getAppConfigUtils: Initialized new instance");
} }
return sAppConfigUtils; return sAppConfigUtils;
} }
/** /**
* 获取应用缓存工具实例 * 获取缓存工具实例
*/ */
public static AppCacheUtils getAppCacheUtils(Context context) { public static AppCacheUtils getAppCacheUtils(Context context) {
LogUtils.d(TAG, "getAppCacheUtils() 调用传入Context" + context.getClass().getSimpleName()); String contextName = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "getAppCacheUtils() called with: context = [" + contextName + "]");
if (sAppCacheUtils == null) { if (sAppCacheUtils == null) {
sAppCacheUtils = AppCacheUtils.getInstance(context); sAppCacheUtils = AppCacheUtils.getInstance(context);
LogUtils.d(TAG, "getAppCacheUtils()AppCacheUtils实例已初始化"); LogUtils.d(TAG, "getAppCacheUtils: Initialized new instance");
} }
return sAppCacheUtils; return sAppCacheUtils;
} }
// ==================== 公共成员方法 (业务) ====================
/** /**
* 清除电池历史数据 * 清除电池历史数据
*/ */
public void clearBatteryHistory() { public void clearBatteryHistory() {
LogUtils.d(TAG, "clearBatteryHistory() 调用"); LogUtils.d(TAG, "clearBatteryHistory() called");
sAppCacheUtils.clearBatteryHistory(); if (sAppCacheUtils != null) {
sAppCacheUtils.clearBatteryHistory();
LogUtils.d(TAG, "clearBatteryHistory: Success");
} else {
LogUtils.w(TAG, "clearBatteryHistory: Failed, sAppCacheUtils is null");
}
} }
// ===================== 生命周期方法区 ===================== /**
* 手动清理所有缓存(仅主动调用生效)
*/
public static void manualClearAllCache() {
LogUtils.w(CACHE_PROTECT_TAG, "manualClearAllCache() called - Manual trigger only");
if (sBitmapCacheUtils != null) {
sBitmapCacheUtils.clearAllCache();
LogUtils.d(CACHE_PROTECT_TAG, "manualClearAllCache: Bitmap cache cleared");
}
// 仅置空引用,不销毁实例(符合极致缓存策略)
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(CACHE_PROTECT_TAG, "manualClearAllCache: View cache reference cleared");
sMemoryCachedBackgroundView = null;
}
LogUtils.w(CACHE_PROTECT_TAG, "manualClearAllCache: Manual cleanup finished");
}
/**
* 获取视图缓存实例
*/
public MemoryCachedBackgroundView getMemoryCachedBackgroundView() {
LogUtils.d(TAG, "getMemoryCachedBackgroundView() called | Current: " + sMemoryCachedBackgroundView);
return sMemoryCachedBackgroundView;
}
/**
* 发送通知消息
*/
public static void notifyMessage(String title, String content) {
LogUtils.d(TAG, "notifyMessage() called with: title = [" + title + "], content = [" + content + "]");
boolean canSend = isDebugging() && sApp != null && sNotificationManagerUtils != null;
if (canSend) {
NotificationMessage message = new NotificationMessage(title, content, "");
sNotificationManagerUtils.showMessageNotification(sApp, message);
LogUtils.d(TAG, "notifyMessage: Sent successfully");
} else {
LogUtils.d(TAG, "notifyMessage: Send failed, conditions not met");
}
}
// ==================== 生命周期方法 ====================
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
LogUtils.d(TAG, "onCreate() 应用启动,开始初始化"); LogUtils.d(TAG, "onCreate() called | Initializing application...");
// 初始化调试模式 sApp = this;
setIsDebugging(BuildConfig.DEBUG); setIsDebugging(BuildConfig.DEBUG);
LogUtils.d(TAG, "onCreate() 调试模式:" + BuildConfig.DEBUG); LogUtils.d(TAG, "onCreate: Debug mode = " + BuildConfig.DEBUG);
// 初始化基础工具 // 初始化核心组件
initBaseTools(); initBaseTools();
// 初始化临时文件夹
initTempDir();
// 初始化工具类实例
initUtils(); initUtils();
// 初始化广播接收器
initReceiver(); initReceiver();
// 启动定时任务
initTimerTask();
LogUtils.d(TAG, "onCreate() 应用初始化完成"); LogUtils.d(TAG, "onCreate: Application initialization completed. Force-cache strategy active.");
} }
@Override @Override
public void onTerminate() { public void onTerminate() {
super.onTerminate(); super.onTerminate();
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放资源"); LogUtils.d(TAG, "onTerminate() called | Releasing non-cache resources");
// 释放Toast工具 // 释放非缓存资源
ToastUtils.release(); ToastUtils.release();
// 释放通知工具
releaseNotificationManager(); releaseNotificationManager();
// 停止定时任务 releaseReceiver();
stopTimerTask();
LogUtils.d(TAG, "onTerminate() 应用资源释放完成"); // 核心策略:不清理缓存
LogUtils.w(CACHE_PROTECT_TAG, "onTerminate: Force-cache active, caches remain in memory");
LogUtils.d(TAG, "onTerminate: Non-cache resources released");
} }
@Override @Override
public void onTrimMemory(int level) { public void onTrimMemory(int level) {
super.onTrimMemory(level); super.onTrimMemory(level);
LogUtils.d(TAG, "onTrimMemory() 调用,内存等级level" + level); LogUtils.w(CACHE_PROTECT_TAG, "onTrimMemory() called with level: " + level + " | Ignoring, caches protected");
logDetailedCacheStatus();
// 初始化通知工具(若未初始化)
if (mNotificationManager == null) {
mNotificationManager = new NotificationManagerUtils(this);
LogUtils.d(TAG, "onTrimMemory()NotificationManagerUtils实例已初始化");
}
// 内存紧张等级判断
if (level > ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
sendTrimMemoryNotification(level);
} else {
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "onTrimMemory()Bitmap缓存已启用");
}
} }
// ===================== 私有初始化方法区 ===================== @Override
public void onLowMemory() {
super.onLowMemory();
LogUtils.w(CACHE_PROTECT_TAG, "onLowMemory() called | Force-cache active, no cleanup performed");
logDetailedCacheStatus();
}
// ==================== 私有初始化方法 ====================
/** /**
* 初始化基础工具Activity管理、Toast * 初始化基础工具
*/ */
private void initBaseTools() { private void initBaseTools() {
LogUtils.d(TAG, "initBaseTools() 开始初始化基础工具"); LogUtils.d(TAG, "initBaseTools: Starting...");
WinBoLLActivityManager.init(this); WinBoLLActivityManager.init(this);
ToastUtils.init(this); ToastUtils.init(this);
LogUtils.d(TAG, "initBaseTools() 基础工具初始化完成"); sNotificationManagerUtils = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initBaseTools: Completed");
} }
/** /**
* 初始化临时文件夹适配API 30外部存储访问 * 初始化核心工具与缓存(极致强制驻留
*/
private void initTempDir() {
LogUtils.d(TAG, "initTempDir() 开始初始化临时文件夹");
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File powerBellDir = new File(picturesDir, "PowerBell");
if (!powerBellDir.exists()) {
boolean isMkSuccess = powerBellDir.mkdirs();
LogUtils.d(TAG, "initTempDir() 文件夹创建结果:" + isMkSuccess);
}
sTempDirPath = powerBellDir.getAbsolutePath();
LogUtils.d(TAG, "initTempDir() 临时文件夹路径:" + sTempDirPath);
}
/**
* 初始化工具类实例
*/ */
private void initUtils() { private void initUtils() {
LogUtils.d(TAG, "initUtils() 开始初始化工具类"); LogUtils.d(TAG, "initUtils: Starting with force-cache strategy");
// 1. 配置与基础缓存
sAppConfigUtils = getAppConfigUtils(this); sAppConfigUtils = getAppConfigUtils(this);
sAppCacheUtils = getAppCacheUtils(this); sAppCacheUtils = getAppCacheUtils(this);
// 2. 资源与Bitmap缓存
sBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this);
sBackgroundSourceUtils.loadSettings();
sBitmapCacheUtils = BitmapCacheUtils.getInstance(); sBitmapCacheUtils = BitmapCacheUtils.getInstance();
mNotificationManager = new NotificationManagerUtils(this); LogUtils.d(TAG, "initUtils: Resource & Bitmap tools initialized (Permanent)");
LogUtils.d(TAG, "initUtils() 工具类初始化完成");
// 3. 视图缓存
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
if (sMemoryCachedBackgroundView == null) {
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, sBackgroundSourceUtils.getCurrentBackgroundBean(), true);
LogUtils.d(TAG, "initUtils: View cache - New instance created");
}
LogUtils.d(TAG, "initUtils: View cache initialized (Permanent)");
} }
/** /**
* 初始化广播接收器 * 注册全局广播接收器
*/ */
private void initReceiver() { private void initReceiver() {
LogUtils.d(TAG, "initReceiver() 开始初始化广播接收器"); LogUtils.d(TAG, "initReceiver: Starting...");
mGlobalReceiver = new GlobalApplicationReceiver(this); mGlobalReceiver = new GlobalApplicationReceiver(this);
mGlobalReceiver.registerAction(); mGlobalReceiver.registerAction();
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成"); LogUtils.d(TAG, "initReceiver: Completed");
} }
// ==================== 私有释放方法 ====================
/** /**
* 初始化定时任务(全局唯一实例) * 释放广播接收器
*/ */
private void initTimerTask() { private void releaseReceiver() {
LogUtils.d(TAG, "initTimerTask() 开始初始化定时任务,当前运行状态:" + sIsTimerRunning); LogUtils.d(TAG, "releaseReceiver: Starting...");
if (mGlobalReceiver != null) {
// 已运行则直接返回 mGlobalReceiver.unregisterAction();
if (sIsTimerRunning) { mGlobalReceiver = null;
LogUtils.d(TAG, "initTimerTask() 定时任务已在运行,无需重复启动"); LogUtils.d(TAG, "releaseReceiver: Completed");
return;
}
// 初始化Handler
if (sTimerHandler == null) {
sTimerHandler = new Handler(Looper.getMainLooper());
LogUtils.d(TAG, "initTimerTask() 定时任务Handler已初始化");
}
// 初始化Runnable
if (sTimerRunnable == null) {
sTimerRunnable = new Runnable() {
@Override
public void run() {
try {
LogUtils.d(TAG, "定时任务执行,间隔:" + TIMER_INTERVAL_MINUTES + "分钟");
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "定时任务Bitmap缓存已重新初始化");
} catch (Exception e) {
LogUtils.e(TAG, "定时任务执行异常:" + e.getMessage());
} finally {
if (sIsTimerRunning) {
long delayMillis = TimeUnit.MINUTES.toMillis(TIMER_INTERVAL_MINUTES);
sTimerHandler.postDelayed(this, delayMillis);
LogUtils.d(TAG, "定时任务已预约下次执行,延迟:" + delayMillis + "ms");
}
}
}
};
LogUtils.d(TAG, "initTimerTask() 定时任务Runnable已初始化");
}
// 启动任务
sTimerHandler.post(sTimerRunnable);
sIsTimerRunning = true;
LogUtils.d(TAG, "initTimerTask() 定时任务已启动,间隔:" + TIMER_INTERVAL_MINUTES + "分钟");
}
// ===================== 私有工具方法区 =====================
/**
* 停止定时任务
*/
private void stopTimerTask() {
LogUtils.d(TAG, "stopTimerTask() 开始停止定时任务");
if (sTimerHandler != null && sTimerRunnable != null) {
sTimerHandler.removeCallbacks(sTimerRunnable);
sIsTimerRunning = false;
LogUtils.d(TAG, "stopTimerTask() 定时任务已停止运行状态重置为false");
} else {
LogUtils.d(TAG, "stopTimerTask() 定时任务未初始化,无需停止");
} }
} }
/** /**
* 释放通知管理工具资源 * 释放通知管理
*/ */
private void releaseNotificationManager() { private void releaseNotificationManager() {
LogUtils.d(TAG, "releaseNotificationManager() 开始释放通知工具"); LogUtils.d(TAG, "releaseNotificationManager: Starting...");
if (mNotificationManager != null) { if (sNotificationManagerUtils != null) {
mNotificationManager.release(); sNotificationManagerUtils.release();
mNotificationManager = null; sNotificationManagerUtils = null;
LogUtils.d(TAG, "releaseNotificationManager() 通知工具资源已释放"); LogUtils.d(TAG, "releaseNotificationManager: Completed");
} else {
LogUtils.d(TAG, "releaseNotificationManager() 通知工具未初始化,无需释放");
} }
} }
/** // ==================== 私有辅助方法 ====================
* 发送内存紧张通知
*/
private void sendTrimMemoryNotification(int level) {
LogUtils.d(TAG, "sendTrimMemoryNotification() 调用内存等级level" + level);
NotificationMessage message = new NotificationMessage();
message.setTitle(TRIM_MEMORY_NOTIFY_TITLE);
String content = String.format("%s [ 缓存紧张级别描述: Level %d | %s ]",
TRIM_MEMORY_NOTIFY_CONTENT, level, getTrimMemoryLevelDesc(level));
message.setContent(content);
mNotificationManager.showConfigNotification(this, message);
LogUtils.d(TAG, "sendTrimMemoryNotification() 内存紧张通知已发送,内容:" + content);
}
/** /**
* 转换内存等级为可读描述 * 记录当前缓存详细状态(用于调试监控)
*/ */
private String getTrimMemoryLevelDesc(int level) { private void logDetailedCacheStatus() {
LogUtils.d(TAG, "getTrimMemoryLevelDesc() 调用传入level" + level); LogUtils.d(TAG, "logDetailedCacheStatus: Reporting cache state");
String desc;
switch (level) { if (sBitmapCacheUtils != null) {
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: LogUtils.d(CACHE_PROTECT_TAG, "Cache Status: BitmapCache [Valid]");
desc = "TRIM_MEMORY_COMPLETE应用内存完全紧张"; try {
break; LogUtils.d(CACHE_PROTECT_TAG, "Cache Detail: Bitmap Count = " + sBitmapCacheUtils.getCacheCount());
case ComponentCallbacks2.TRIM_MEMORY_MODERATE: } catch (Exception e) {
desc = "MODERATE中等内存紧张"; LogUtils.d(CACHE_PROTECT_TAG, "Cache Detail: Failed to get bitmap count - " + e.getMessage());
break; }
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: }
desc = "BACKGROUND应用进入后台";
break; if (sMemoryCachedBackgroundView != null) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: LogUtils.d(CACHE_PROTECT_TAG, "Cache Status: ViewCache [Valid]");
desc = "BACKGROUND应用UI隐藏"; LogUtils.d(CACHE_PROTECT_TAG, "Cache Detail: View Instance Count = " + MemoryCachedBackgroundView.getInstanceCount());
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
desc = "RUNNING_CRITICAL应用运行关键级紧张";
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
desc = "RUNNING_LOW应用运行低内存";
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
desc = "RUNNING_MODERATE应用运行中等内存紧张";
break;
default:
desc = "UNKNOWN(" + level + ")";
break;
} }
LogUtils.d(TAG, "getTrimMemoryLevelDesc() 内存等级描述结果:" + desc);
return desc;
} }
} }

View File

@@ -26,53 +26,63 @@ import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
import cc.winboll.studio.powerbell.activities.SettingsActivity; import cc.winboll.studio.powerbell.activities.SettingsActivity;
import cc.winboll.studio.powerbell.activities.WinBoLLActivity; import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.models.BatteryStyle;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean; import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity;
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity; import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils; import cc.winboll.studio.powerbell.utils.PermissionUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils; import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.views.BatteryStyleView;
import cc.winboll.studio.powerbell.views.MainContentView; import cc.winboll.studio.powerbell.views.MainContentView;
/** /**
* 应用核心主活动 * 应用核心主活动
* 功能:管理电池监控、背景设置、服务启停、权限申请等核心功能 * 功能:管理电池监控、背景设置、服务启停、权限申请等核心功能
* 适配Java7 | API30 | 内存泄漏防护 | UI与服务状态实时同步 * 适配Java7 | API30 | 内存泄漏防护 | UI与服务状态实时同步
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/ */
public class MainActivity extends WinBoLLActivity implements MainContentView.OnViewActionListener { public class MainActivity extends WinBoLLActivity implements MainContentView.OnViewActionListener {
// ======================== 静态常量(置顶统一,抽离魔法值======================== // ======================== 静态常量区(抽离魔法值,按功能分类========================
public static final String TAG = "MainActivity"; public static final String TAG = "MainActivity";
private static final int REQUEST_READ_MEDIA_IMAGES = 1001; private static final int REQUEST_BACKGROUND_SETTINGS_ACTIVITY = 1001;
public static final String EXTRA_ISRELOAD_BACKGROUNDVIEW = "EXTRA_ISRELOAD_BACKGROUNDVIEW";
public static final String EXTRA_ISRELOAD_ACCENTCOLOR = "EXTRA_ISRELOAD_ACCENTCOLOR";
private static final long DELAY_LOAD_NON_CRITICAL = 500L; private static final long DELAY_LOAD_NON_CRITICAL = 500L;
// Handler 消息常量
public static final int MSG_RELOAD_APPCONFIG = 0; public static final int MSG_RELOAD_APPCONFIG = 0;
public static final int MSG_CURRENTVALUEBATTERY = 1; public static final int MSG_CURRENTVALUEBATTERY = 1;
public static final int MSG_LOAD_BACKGROUND = 2; public static final int MSG_LOAD_BACKGROUND = 2;
private static final int MSG_UPDATE_SERVICE_SWITCH = 3; private static final int MSG_UPDATE_SERVICE_SWITCH = 3;
private static final int MSG_UPDATE_BATTERYDRAWABLE = 4;
// ======================== 静态成员(全局共享,严格管控生命周期)======================== // ======================== 静态成员(全局共享,管控生命周期)========================
private static MainActivity sMainActivity; private static MainActivity sMainActivity;
private static Handler sGlobalHandler; private static Handler sGlobalHandler;
// ======================== 工具类实例(单例,避免重复初始化)======================== // ======================== 工具类实例(单例,避免重复初始化)========================
private PermissionUtils mPermissionUtils; private PermissionUtils mPermissionUtils;
private AppConfigUtils mAppConfigUtils; private AppConfigUtils mAppConfigUtils;
private BackgroundSourceUtils mBgSourceUtils; private BackgroundSourceUtils mBgSourceUtils;
// ======================== 应用核心实例 ========================= // ======================== 应用核心实例 =========================
private App mApplication; private App mApplication;
private MainContentView mMainContentView; private MainContentView mMainContentView;
private ControlCenterServiceBean mServiceControlBean; private ControlCenterServiceBean mServiceControlBean;
// ======================== 基础视图组件 ========================= // ======================== 基础视图组件 =========================
private Toolbar mToolbar; private Toolbar mToolbar;
private ViewStub mAdsViewStub; private ViewStub mAdsViewStub;
private ADsBannerView mADsBannerView; private ADsBannerView mADsBannerView;
private Drawable mFrameDrawable; private Drawable mFrameDrawable;
private Menu mMenu; private Menu mMenu;
// ======================== 生命周期方法(按系统调用顺序排列)======================== // ======================== 生命周期方法(按系统调用顺序排列)========================
@Override @Override
public Activity getActivity() { public Activity getActivity() {
return this; return this;
@@ -86,7 +96,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate() | savedInstanceState=" + savedInstanceState); LogUtils.d(TAG, "onCreate() 调用 | savedInstanceState: " + savedInstanceState);
initGlobalHandler(); initGlobalHandler();
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
@@ -95,61 +105,75 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
initCriticalView(); initCriticalView();
initCoreUtilsAsync(); initCoreUtilsAsync();
loadNonCriticalViewDelayed(); loadNonCriticalViewDelayed();
// 处理首次启动参数
handleReloadBackgroundParam(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
LogUtils.d(TAG, "onNewIntent() 调用 | intent: " + intent);
// 关键更新Activity持有的Intent确保后续获取最新值
setIntent(intent);
// 统一处理刷新背景参数
handleReloadBackgroundParam(intent);
} }
@Override @Override
protected void onPostCreate(Bundle savedInstanceState) { protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "onPostCreate() | savedInstanceState=" + savedInstanceState); LogUtils.d(TAG, "onPostCreate() 调用 | savedInstanceState: " + savedInstanceState);
mPermissionUtils.startPermissionRequest(this); mPermissionUtils.startPermissionRequest(this);
LogUtils.d(TAG, "onPostCreate: 发起权限申请");
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
LogUtils.d(TAG, "onResume()"); LogUtils.d(TAG, "onResume() 调用");
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
}
if (mADsBannerView != null) { if (mADsBannerView != null) {
mADsBannerView.resumeADs(this); mADsBannerView.resumeADs(this);
LogUtils.d(TAG, "onResume: 广告视图已恢复");
} }
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
LogUtils.d(TAG, "onPause()"); LogUtils.d(TAG, "onPause() 调用");
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
LogUtils.d(TAG, "onDestroy()"); LogUtils.d(TAG, "onDestroy() 调用");
// 释放广告资源 // 释放广告资源
if (mADsBannerView != null) { if (mADsBannerView != null) {
mADsBannerView.releaseAdResources(); mADsBannerView.releaseAdResources();
mADsBannerView = null; mADsBannerView = null;
LogUtils.d(TAG, "onDestroy: 广告资源已释放");
} }
// 释放核心视图 // 释放核心视图
if (mMainContentView != null) { if (mMainContentView != null) {
mMainContentView.releaseResources(); mMainContentView.releaseResources();
mMainContentView = null; mMainContentView = null;
LogUtils.d(TAG, "onDestroy: 核心视图资源已释放");
} }
// 销毁Handler防止内存泄漏 // 销毁Handler防止内存泄漏
if (sGlobalHandler != null) { if (sGlobalHandler != null) {
sGlobalHandler.removeCallbacksAndMessages(null); sGlobalHandler.removeCallbacksAndMessages(null);
sGlobalHandler = null; sGlobalHandler = null;
LogUtils.d(TAG, "onDestroy: 全局Handler已销毁");
} }
// 释放Drawable // 释放Drawable
if (mFrameDrawable != null) { if (mFrameDrawable != null) {
mFrameDrawable.setCallback(null); mFrameDrawable.setCallback(null);
mFrameDrawable = null; mFrameDrawable = null;
LogUtils.d(TAG, "onDestroy: 框架Drawable已释放");
} }
// 置空所有引用 // 置空所有引用,消除内存泄漏风险
sMainActivity = null; sMainActivity = null;
mPermissionUtils = null; mPermissionUtils = null;
mAppConfigUtils = null; mAppConfigUtils = null;
@@ -164,22 +188,27 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "onActivityResult() | requestCode=" + requestCode + " | resultCode=" + resultCode + " | data=" + data); LogUtils.d(TAG, "onActivityResult() 调用 | requestCode: " + requestCode + " | resultCode: " + resultCode + " | data: " + data);
mPermissionUtils.handlePermissionRequest(this, requestCode, resultCode, data); mPermissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
if (requestCode == REQUEST_READ_MEDIA_IMAGES && sGlobalHandler != null) {
if (requestCode == REQUEST_BACKGROUND_SETTINGS_ACTIVITY && sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND); sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
LogUtils.d(TAG, "onActivityResult: 发送背景加载消息");
} }
} }
// ======================== 菜单与导航方法 ======================== // ======================== 菜单与导航方法 ========================
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
LogUtils.d(TAG, "onCreateOptionsMenu() | menu=" + menu); LogUtils.d(TAG, "onCreateOptionsMenu() 调用 | menu: " + menu);
mMenu = menu; mMenu = menu;
AESThemeUtil.inflateMenu(this, menu); AESThemeUtil.inflateMenu(this, menu);
// 调试模式加载测试菜单
if (App.isDebugging()) { if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu); DevelopUtils.inflateMenu(this, menu);
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu); getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
LogUtils.d(TAG, "onCreateOptionsMenu: 已加载测试菜单");
} }
getMenuInflater().inflate(R.menu.toolbar_main, mMenu); getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
return true; return true;
@@ -187,14 +216,21 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
LogUtils.d(TAG, "onOptionsItemSelected() | itemId=" + item.getItemId()); LogUtils.d(TAG, "onOptionsItemSelected() 调用 | itemId: " + item.getItemId());
// 主题切换处理
if (AESThemeUtil.onAppThemeItemSelected(this, item)) { if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
recreate(); recreate();
Intent mainIntent = new Intent(MainActivity.this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_ACCENTCOLOR, true);
startActivity(mainIntent);
return true; return true;
} }
// 开发者功能处理
if (DevelopUtils.onDevelopItemSelected(this, item)) { if (DevelopUtils.onDevelopItemSelected(this, item)) {
return true; return true;
} }
// 菜单点击事件分发
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_settings: case R.id.action_settings:
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
@@ -206,11 +242,14 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
startActivity(new Intent(this, ClearRecordActivity.class)); startActivity(new Intent(this, ClearRecordActivity.class));
break; break;
case R.id.action_changepicture: case R.id.action_changepicture:
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_READ_MEDIA_IMAGES); startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_BACKGROUND_SETTINGS_ACTIVITY);
break; break;
case R.id.action_unittestactivity: case R.id.action_unittestactivity:
startActivity(new Intent(this, MainUnitTestActivity.class)); startActivity(new Intent(this, MainUnitTestActivity.class));
break; break;
case R.id.action_unittest2activity:
startActivity(new Intent(this, MainUnitTest2Activity.class));
break;
case R.id.action_about: case R.id.action_about:
startAboutActivity(); startAboutActivity();
break; break;
@@ -223,41 +262,46 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override @Override
public void setupToolbar() { public void setupToolbar() {
super.setupToolbar(); super.setupToolbar();
LogUtils.d(TAG, "setupToolbar()"); LogUtils.d(TAG, "setupToolbar() 调用");
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setDisplayHomeAsUpEnabled(false);
LogUtils.d(TAG, "setupToolbar: 已隐藏返回按钮");
} }
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
LogUtils.d(TAG, "onBackPressed()"); LogUtils.d(TAG, "onBackPressed() 调用");
moveTaskToBack(true); moveTaskToBack(true);
LogUtils.d(TAG, "onBackPressed: 应用已退至后台");
} }
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
LogUtils.d(TAG, "dispatchKeyEvent() | event=" + event); LogUtils.d(TAG, "dispatchKeyEvent() 调用 | event: " + event);
return super.dispatchKeyEvent(event); return super.dispatchKeyEvent(event);
} }
// ======================== 核心初始化方法 ======================== // ======================== 核心初始化方法 ========================
private void initPermissionUtils() { private void initPermissionUtils() {
LogUtils.d(TAG, "initPermissionUtils()"); LogUtils.d(TAG, "initPermissionUtils() 调用");
mPermissionUtils = PermissionUtils.getInstance(); mPermissionUtils = PermissionUtils.getInstance();
LogUtils.d(TAG, "initPermissionUtils: 权限工具类已初始化");
} }
private void initGlobalHandler() { private void initGlobalHandler() {
LogUtils.d(TAG, "initGlobalHandler()"); LogUtils.d(TAG, "initGlobalHandler() 调用");
if (sGlobalHandler == null) { if (sGlobalHandler == null) {
sGlobalHandler = new Handler() { sGlobalHandler = new Handler() {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
// Activity已销毁则跳过消息处理
if (sMainActivity == null || sMainActivity.isFinishing() || sMainActivity.isDestroyed()) { if (sMainActivity == null || sMainActivity.isFinishing() || sMainActivity.isDestroyed()) {
LogUtils.w(TAG, "handleMessage: Activity已销毁跳过消息 | what=" + msg.what); LogUtils.w(TAG, "handleMessage: Activity已销毁跳过消息 | what: " + msg.what);
return; return;
} }
LogUtils.d(TAG, "handleMessage() | what=" + msg.what); LogUtils.d(TAG, "handleMessage() 调用 | what: " + msg.what);
switch (msg.what) { switch (msg.what) {
case MSG_RELOAD_APPCONFIG: case MSG_RELOAD_APPCONFIG:
sMainActivity.updateViewData(); sMainActivity.updateViewData();
@@ -265,45 +309,53 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
case MSG_CURRENTVALUEBATTERY: case MSG_CURRENTVALUEBATTERY:
if (sMainActivity.mMainContentView != null) { if (sMainActivity.mMainContentView != null) {
sMainActivity.mMainContentView.updateCurrentBattery(msg.arg1); sMainActivity.mMainContentView.updateCurrentBattery(msg.arg1);
LogUtils.d(TAG, "handleMessage: 更新当前电量 | value=" + msg.arg1); LogUtils.d(TAG, "handleMessage: 更新当前电量 | value: " + msg.arg1);
} }
break; break;
case MSG_LOAD_BACKGROUND: case MSG_LOAD_BACKGROUND:
sMainActivity.reloadBackground(); sMainActivity.reloadBackground();
sMainActivity.setMainLayoutBackgroundColor();
break; break;
case MSG_UPDATE_SERVICE_SWITCH: case MSG_UPDATE_SERVICE_SWITCH:
sMainActivity.updateServiceSwitchUI(); sMainActivity.updateServiceSwitchUI();
break;
case MSG_UPDATE_BATTERYDRAWABLE:
sMainActivity.updateBatteryDrawable();
break; break;
} }
} }
}; };
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已创建");
} else {
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已存在无需重复创建");
} }
} }
private void initMainContentView() { private void initMainContentView() {
LogUtils.d(TAG, "initMainContentView()"); LogUtils.d(TAG, "initMainContentView() 调用");
View rootView = findViewById(android.R.id.content); View rootView = findViewById(android.R.id.content);
mMainContentView = new MainContentView(this, rootView, this); mMainContentView = new MainContentView(this, rootView, this);
LogUtils.d(TAG, "initMainContentView: 核心内容视图已初始化");
} }
private void initCriticalView() { private void initCriticalView() {
LogUtils.d(TAG, "initCriticalView()"); LogUtils.d(TAG, "initCriticalView() 调用");
sMainActivity = this; sMainActivity = this;
mToolbar = findViewById(R.id.toolbar); mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar); setSupportActionBar(mToolbar);
if (mToolbar != null) { if (mToolbar != null) {
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
LogUtils.d(TAG, "initCriticalView: 工具栏已设置标题样式");
} }
mAdsViewStub = findViewById(R.id.stub_ads_banner); mAdsViewStub = findViewById(R.id.stub_ads_banner);
LogUtils.d(TAG, "initCriticalView: 广告ViewStub已获取");
} }
private void initCoreUtilsAsync() { private void initCoreUtilsAsync() {
LogUtils.d(TAG, "initCoreUtilsAsync()"); LogUtils.d(TAG, "initCoreUtilsAsync() 调用");
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId=" + Thread.currentThread().getId()); LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId: " + Thread.currentThread().getId());
mApplication = (App) getApplication(); mApplication = (App) getApplication();
mAppConfigUtils = AppConfigUtils.getInstance(getApplicationContext()); mAppConfigUtils = AppConfigUtils.getInstance(getApplicationContext());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity()); mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
@@ -313,17 +365,20 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
if (mServiceControlBean == null) { if (mServiceControlBean == null) {
mServiceControlBean = new ControlCenterServiceBean(false); mServiceControlBean = new ControlCenterServiceBean(false);
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean); ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置不存在,已创建默认配置");
} }
// 根据配置启停服务 // 根据配置启停服务
final boolean isServiceEnable = mServiceControlBean.isEnableService(); final boolean isServiceEnable = mServiceControlBean.isEnableService();
final boolean isServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName()); final boolean isServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置状态 | isServiceEnable=" + isServiceEnable + " | isServiceAlive=" + isServiceAlive); LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置状态 | isServiceEnable: " + isServiceEnable + " | isServiceAlive: " + isServiceAlive);
if (isServiceEnable && !isServiceAlive) { if (isServiceEnable && !isServiceAlive) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
ControlCenterService.startControlCenterService(getApplicationContext()); ControlCenterService.startControlCenterService(getApplicationContext());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已启动");
} }
}); });
} else if (!isServiceEnable && isServiceAlive) { } else if (!isServiceEnable && isServiceAlive) {
@@ -331,6 +386,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override @Override
public void run() { public void run() {
ControlCenterService.stopControlCenterService(getApplicationContext()); ControlCenterService.stopControlCenterService(getApplicationContext());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已停止");
} }
}); });
} }
@@ -343,7 +399,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁跳过UI更新"); LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁跳过UI更新");
return; return;
} }
// 加载框架背景适配API23+ // 适配API30兼容低版本Drawable加载
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame, getTheme()); mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame, getTheme());
} else { } else {
@@ -352,6 +408,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
updateViewData(); updateViewData();
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND); sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH); sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
LogUtils.d(TAG, "initCoreUtilsAsync: UI更新消息已发送");
} }
}); });
} }
@@ -359,7 +416,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
} }
private void loadNonCriticalViewDelayed() { private void loadNonCriticalViewDelayed() {
LogUtils.d(TAG, "loadNonCriticalViewDelayed() | 延迟时长=" + DELAY_LOAD_NON_CRITICAL + "ms"); LogUtils.d(TAG, "loadNonCriticalViewDelayed() 调用 | 延迟时长: " + DELAY_LOAD_NON_CRITICAL + "ms");
new Handler().postDelayed(new Runnable() { new Handler().postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -372,9 +429,34 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
}, DELAY_LOAD_NON_CRITICAL); }, DELAY_LOAD_NON_CRITICAL);
} }
// ======================== 视图操作方法 ======================== // ======================== 视图操作方法 ========================
private void handleReloadBackgroundParam(Intent intent) {
LogUtils.d(TAG, "handleReloadBackgroundParam() 调用 | intent: " + intent);
if (intent == null) {
LogUtils.d(TAG, "handleReloadBackgroundParam: Intent 为空");
return;
}
boolean isReloadAccentColor = intent.getBooleanExtra(EXTRA_ISRELOAD_ACCENTCOLOR, false);
if (isReloadAccentColor) {
App.sBackgroundSourceUtils.getCurrentBackgroundBean().setPixelColor(ImageUtils.getColorAccent(this));
App.sBackgroundSourceUtils.saveSettings();
}
boolean isReloadBackgroundView = intent.getBooleanExtra(EXTRA_ISRELOAD_BACKGROUNDVIEW, false);
if (isReloadBackgroundView) {
LogUtils.d(TAG, "handleReloadBackgroundParam: 接收到刷新背景视图指令");
reloadBackgroundView();
}
}
private void reloadBackgroundView() {
LogUtils.d(TAG, "reloadBackgroundView() 调用");
mMainContentView.reloadBackgroundView();
}
private void loadAdsView() { private void loadAdsView() {
LogUtils.d(TAG, "loadAdsView()"); LogUtils.d(TAG, "loadAdsView() 调用");
if (mAdsViewStub == null) { if (mAdsViewStub == null) {
LogUtils.e(TAG, "loadAdsView: 广告ViewStub为空加载失败"); LogUtils.e(TAG, "loadAdsView: 广告ViewStub为空加载失败");
return; return;
@@ -382,47 +464,51 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
if (mADsBannerView == null) { if (mADsBannerView == null) {
View adsView = mAdsViewStub.inflate(); View adsView = mAdsViewStub.inflate();
mADsBannerView = adsView.findViewById(R.id.adsbanner); mADsBannerView = adsView.findViewById(R.id.adsbanner);
LogUtils.d(TAG, "loadAdsView: 广告视图已加载");
} else {
LogUtils.d(TAG, "loadAdsView: 广告视图已存在,无需重复加载");
} }
} }
private void updateViewData() { private void updateViewData() {
LogUtils.d(TAG, "updateViewData()"); LogUtils.d(TAG, "updateViewData() 调用");
if (mMainContentView == null || mFrameDrawable == null) { if (mMainContentView == null || mFrameDrawable == null) {
LogUtils.e(TAG, "updateViewData: 核心视图或框架背景为空,更新失败"); LogUtils.e(TAG, "updateViewData: 核心视图或框架背景为空,更新失败");
return; return;
} }
mMainContentView.updateViewData(mFrameDrawable); mMainContentView.updateViewData(mFrameDrawable);
LogUtils.d(TAG, "updateViewData: 视图数据已更新");
} }
void updateBatteryDrawable() {
BatteryStyle batteryStyle = BatteryStyleView.getSavedBatteryStyle(this);
mMainContentView.updateBatteryDrawable(batteryStyle);
}
public static void sendUpdateBatteryDrawableMessage() {
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_BATTERYDRAWABLE);
}
}
private void reloadBackground() { private void reloadBackground() {
LogUtils.d(TAG, "reloadBackground()"); LogUtils.d(TAG, "reloadBackground() 调用");
if (mMainContentView == null || mBgSourceUtils == null) { if (mMainContentView == null || mBgSourceUtils == null) {
LogUtils.e(TAG, "reloadBackground: 核心视图或背景工具类为空,加载失败"); LogUtils.e(TAG, "reloadBackground: 核心视图或背景工具类为空,加载失败");
return; return;
} }
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean(); BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
if (currentBgBean != null) { if (currentBgBean != null) {
mMainContentView.backgroundView.loadBackgroundBean(currentBgBean); mMainContentView.backgroundView.loadByBackgroundBean(currentBgBean, true);
LogUtils.d(TAG, "reloadBackground: 已加载自定义背景");
} else { } else {
mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background); mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background);
} LogUtils.d(TAG, "reloadBackground: 已加载默认背景");
}
private void setMainLayoutBackgroundColor() {
LogUtils.d(TAG, "setMainLayoutBackgroundColor()");
if (isFinishing() || isDestroyed() || mMainContentView == null || mBgSourceUtils == null) {
LogUtils.e(TAG, "setMainLayoutBackgroundColor: 上下文无效,设置失败");
return;
}
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
if (currentBgBean != null) {
mMainContentView.mainLayout.setBackgroundColor(currentBgBean.getPixelColor());
LogUtils.d(TAG, "setMainLayoutBackgroundColor: 主布局背景色设置完成 | color=" + currentBgBean.getPixelColor());
} }
} }
private void updateServiceSwitchUI() { private void updateServiceSwitchUI() {
LogUtils.d(TAG, "updateServiceSwitchUI()"); LogUtils.d(TAG, "updateServiceSwitchUI() 调用");
if (mMainContentView == null || mServiceControlBean == null) { if (mMainContentView == null || mServiceControlBean == null) {
LogUtils.e(TAG, "updateServiceSwitchUI: 核心视图或服务配置为空,更新失败"); LogUtils.e(TAG, "updateServiceSwitchUI: 核心视图或服务配置为空,更新失败");
return; return;
@@ -431,69 +517,77 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
mMainContentView.setServiceSwitchEnabled(false); mMainContentView.setServiceSwitchEnabled(false);
mMainContentView.setServiceSwitchChecked(configEnabled); mMainContentView.setServiceSwitchChecked(configEnabled);
mMainContentView.setServiceSwitchEnabled(true); mMainContentView.setServiceSwitchEnabled(true);
LogUtils.d(TAG, "updateServiceSwitchUI: 服务开关已更新 | 状态: " + configEnabled);
} }
// ======================== 服务与线程管理方法 ======================== // ======================== 服务与线程管理方法 ========================
private void toggleServiceEnableState(boolean isEnable) { private void toggleServiceEnableState(boolean isEnable) {
LogUtils.d(TAG, "toggleServiceEnableState() | 目标状态=" + isEnable); LogUtils.d(TAG, "toggleServiceEnableState() 调用 | 目标状态: " + isEnable);
if (mServiceControlBean == null) { if (mServiceControlBean == null) {
LogUtils.e(TAG, "toggleServiceEnableState: 服务配置为空,切换失败"); LogUtils.e(TAG, "toggleServiceEnableState: 服务配置为空,切换失败");
return; return;
} }
mServiceControlBean.setIsEnableService(isEnable); mServiceControlBean.setIsEnableService(isEnable);
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean); ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
LogUtils.d(TAG, "toggleServiceEnableState: 服务配置已保存");
// UI开关联动服务启停 // UI开关联动服务启停
if (isEnable) { if (isEnable) {
if (!ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName())) { if (!ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName())) {
ControlCenterService.startControlCenterService(getApplicationContext()); ControlCenterService.startControlCenterService(getApplicationContext());
LogUtils.d(TAG, "toggleServiceEnableState: 服务已启动");
} }
} else { } else {
ControlCenterService.stopControlCenterService(getApplicationContext()); ControlCenterService.stopControlCenterService(getApplicationContext());
LogUtils.d(TAG, "toggleServiceEnableState: 服务已停止");
} }
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH); sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
} }
// ======================== 页面跳转方法 ======================== // ======================== 页面跳转方法 ========================
private void startAboutActivity() { private void startAboutActivity() {
LogUtils.d(TAG, "startAboutActivity()"); LogUtils.d(TAG, "startAboutActivity() 调用");
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class); Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
APPInfo appInfo = genDefaultAppInfo(); APPInfo appInfo = genDefaultAppInfo();
aboutIntent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo); aboutIntent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo);
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class); WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class);
LogUtils.d(TAG, "startAboutActivity: 关于页面已启动");
} }
// ======================== 消息发送方法 ======================== // ======================== 消息发送方法 ========================
private void notifyServiceAppConfigChange() { private void notifyServiceAppConfigChange() {
LogUtils.d(TAG, "notifyServiceAppConfigChange()"); LogUtils.d(TAG, "notifyServiceAppConfigChange() 调用");
ControlCenterService.sendAppConfigStatusUpdateMessage(this); ControlCenterService.sendAppConfigStatusUpdateMessage(this);
reloadAppConfig(); reloadAppConfig();
LogUtils.d(TAG, "notifyServiceAppConfigChange: 服务配置已通知更新");
} }
public static void reloadAppConfig() { public static void reloadAppConfig() {
LogUtils.d(TAG, "reloadAppConfig()"); LogUtils.d(TAG, "reloadAppConfig() 调用");
if (sGlobalHandler != null) { if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_RELOAD_APPCONFIG); sGlobalHandler.sendEmptyMessage(MSG_RELOAD_APPCONFIG);
LogUtils.d(TAG, "reloadAppConfig: 配置重载消息已发送");
} else { } else {
LogUtils.w(TAG, "reloadAppConfig: 全局Handler为空消息发送失败"); LogUtils.w(TAG, "reloadAppConfig: 全局Handler为空消息发送失败");
} }
} }
public static void sendCurrentBatteryValueMessage(int value) { public static void sendCurrentBatteryValueMessage(int value) {
LogUtils.d(TAG, "sendCurrentBatteryValueMessage() | 电量=" + value); LogUtils.d(TAG, "sendCurrentBatteryValueMessage() 调用 | 电量: " + value);
if (sGlobalHandler != null) { if (sGlobalHandler != null) {
Message msg = sGlobalHandler.obtainMessage(MSG_CURRENTVALUEBATTERY); Message msg = sGlobalHandler.obtainMessage(MSG_CURRENTVALUEBATTERY);
msg.arg1 = value; msg.arg1 = value;
sGlobalHandler.sendMessage(msg); sGlobalHandler.sendMessage(msg);
LogUtils.d(TAG, "sendCurrentBatteryValueMessage: 电量消息已发送");
} else { } else {
LogUtils.w(TAG, "sendCurrentBatteryValueMessage: 全局Handler为空消息发送失败"); LogUtils.w(TAG, "sendCurrentBatteryValueMessage: 全局Handler为空消息发送失败");
} }
} }
// ======================== 辅助工具方法 ======================== // ======================== 辅助工具方法 ========================
private APPInfo genDefaultAppInfo() { private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo()"); LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "powerbell"; String branchName = "powerbell";
APPInfo appInfo = new APPInfo(); APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name)); appInfo.setAppName(getString(R.string.app_name));
@@ -506,37 +600,38 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell"); appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
appInfo.setAppAPKName("PowerBell"); appInfo.setAppAPKName("PowerBell");
appInfo.setAppAPKFolderName("PowerBell"); appInfo.setAppAPKFolderName("PowerBell");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo; return appInfo;
} }
// ======================== MainContentView 事件回调 ======================== // ======================== MainContentView 事件回调 ========================
@Override @Override
public void onChargeReminderSwitchChanged(boolean isChecked) { public void onChargeReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onChargeReminderSwitchChanged() | isChecked=" + isChecked); LogUtils.d(TAG, "onChargeReminderSwitchChanged() 调用 | isChecked: " + isChecked);
notifyServiceAppConfigChange(); notifyServiceAppConfigChange();
} }
@Override @Override
public void onUsageReminderSwitchChanged(boolean isChecked) { public void onUsageReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onUsageReminderSwitchChanged() | isChecked=" + isChecked); LogUtils.d(TAG, "onUsageReminderSwitchChanged() 调用 | isChecked: " + isChecked);
notifyServiceAppConfigChange(); notifyServiceAppConfigChange();
} }
@Override @Override
public void onServiceSwitchChanged(boolean isChecked) { public void onServiceSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onServiceSwitchChanged() | isChecked=" + isChecked); LogUtils.d(TAG, "onServiceSwitchChanged() 调用 | isChecked: " + isChecked);
toggleServiceEnableState(isChecked); toggleServiceEnableState(isChecked);
} }
@Override @Override
public void onChargeReminderProgressChanged(int progress) { public void onChargeReminderProgressChanged(int progress) {
LogUtils.d(TAG, "onChargeReminderProgressChanged() | progress=" + progress); LogUtils.d(TAG, "onChargeReminderProgressChanged() 调用 | progress: " + progress);
notifyServiceAppConfigChange(); notifyServiceAppConfigChange();
} }
@Override @Override
public void onUsageReminderProgressChanged(int progress) { public void onUsageReminderProgressChanged(int progress) {
LogUtils.d(TAG, "onUsageReminderProgressChanged() | progress=" + progress); LogUtils.d(TAG, "onUsageReminderProgressChanged() 调用 | progress: " + progress);
notifyServiceAppConfigChange(); notifyServiceAppConfigChange();
} }
} }

View File

@@ -15,40 +15,51 @@ import android.provider.MediaStore;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog; import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog; import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils; import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.FileUtils; import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils; import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import cc.winboll.studio.powerbell.utils.UriUtils; import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File; import java.io.File;
/**
* 背景设置页面(支持图片选择、拍照、裁剪、像素拾取、调色板等功能)
* 核心:基于强制缓存策略,支持预览与设置提交分离,保留操作状态
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class BackgroundSettingsActivity extends WinBoLLActivity { public class BackgroundSettingsActivity extends WinBoLLActivity {
// ====================== 常量定义(按功能分类排序)======================
// ====================== 常量定义(按功能分类置顶)======================
public static final String TAG = "BackgroundSettingsActivity"; public static final String TAG = "BackgroundSettingsActivity";
// 系统版本常量 // 系统版本常量
private static final int SDK_VERSION_TIRAMISU = 33; private static final int SDK_VERSION_TIRAMISU = 33;
// 请求码(按功能分组)
// 请求码(按功能分组,从小到大排序)
public static final int REQUEST_SELECT_PICTURE = 0; public static final int REQUEST_SELECT_PICTURE = 0;
public static final int REQUEST_TAKE_PHOTO = 1; public static final int REQUEST_TAKE_PHOTO = 1;
public static final int REQUEST_CROP_IMAGE = 2; public static final int REQUEST_CROP_IMAGE = 2;
private static final int REQUEST_PIXELPICKER = 1001; private static final int REQUEST_PIXELPICKER = 1001;
private static final int REQUEST_CAMERA_PERMISSION = 1004; private static final int REQUEST_CAMERA_PERMISSION = 1004;
// Bitmap解析常量 // Bitmap解析常量
private static final int BITMAP_MAX_SIZE = 2048; private static final int BITMAP_MAX_SIZE = 2048;
private static final int BITMAP_MAX_SAMPLE_SIZE = 16; private static final int BITMAP_MAX_SAMPLE_SIZE = 16;
@@ -57,9 +68,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
// 工具类实例 // 工具类实例
private BackgroundSourceUtils mBgSourceUtils; private BackgroundSourceUtils mBgSourceUtils;
private BitmapCacheUtils mBitmapCache; private BitmapCacheUtils mBitmapCache;
// 视图组件 // 视图组件
private Toolbar mToolbar; private Toolbar mToolbar;
private BackgroundView mBackgroundView; private BackgroundView mBackgroundView;
// 状态标记volatile保证多线程可见性 // 状态标记volatile保证多线程可见性
private volatile boolean isCommitSettings = false; private volatile boolean isCommitSettings = false;
private volatile boolean isPreviewBackgroundChanged = false; private volatile boolean isPreviewBackgroundChanged = false;
@@ -78,52 +91,81 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
LogUtils.d(TAG, "【生命周期】onCreate 开始初始化"); LogUtils.d(TAG, "onCreate() 开始初始化");
setContentView(R.layout.activity_background_settings); setContentView(R.layout.activity_background_settings);
// 初始化核心组件 // 初始化核心组件
initCoreComponents(); initCoreComponents();
// 初始化界面与事件 // 初始化Toolbar与点击事件
initToolbar(); initToolbar();
initClickListeners(); initClickListeners();
LogUtils.d(TAG, "【初始化】界面与事件绑定完成"); LogUtils.d(TAG, "onCreate() 视图与事件绑定完成");
// 处理分享意图或初始化预览 // 处理分享意图或初始化预览
handleIntentOrPreview(); handleIntentOrPreview();
// 初始化预览环境并刷新 // 初始化预览环境并刷新
initPreviewEnvironment(); initPreviewEnvironment();
LogUtils.d(TAG, "【生命周期】onCreate 初始化完成");
LogUtils.d(TAG, "onCreate() 初始化完成");
} }
@Override @Override
protected void onPostCreate(Bundle savedInstanceState) { protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "【生命周期】onPostCreate 执行双重刷新预览"); LogUtils.d(TAG, "onPostCreate() 执行双重刷新预览");
doubleRefreshPreview();
// 监听视图布局完成事件
mBackgroundView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除监听,避免重复回调
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mBackgroundView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mBackgroundView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// 此时已获取真实宽高
int width = mBackgroundView.getWidth();
int height = mBackgroundView.getHeight();
LogUtils.d(TAG, String.format("onPostCreate() 获取视图尺寸 | width=%d | height=%d", width, height));
if (width > 0 && height > 0) {
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(BackgroundSettingsActivity.this);
appConfigUtils.loadAppConfig();
appConfigUtils.mAppConfigBean.setDefaultFrameWidth(width);
appConfigUtils.mAppConfigBean.setDefaultFrameHeight(height);
appConfigUtils.saveAppConfig();
LogUtils.d(TAG, "onPostCreate() 保存默认相框尺寸成功");
doubleRefreshPreview();
}
}
});
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【回调触发】requestCode" + requestCode + "resultCode" + resultCode); LogUtils.d(TAG, String.format("onActivityResult() | requestCode=%d | resultCode=%d | data=%s",
requestCode, resultCode, data != null ? data.toString() : "null"));
try { try {
if (resultCode != RESULT_OK) { if (resultCode != RESULT_OK) {
LogUtils.d(TAG, "【回调处理】结果非RESULT_OK执行取消逻辑"); LogUtils.d(TAG, String.format("onActivityResult() 操作取消 | requestCode=%d", requestCode));
handleOperationCancelOrFail(); handleOperationCancelOrFail();
return; return;
} }
handleActivityResult(requestCode, data); handleActivityResult(requestCode, data);
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【回调异常】requestCode" + requestCode + "异常信息" + e.getMessage()); LogUtils.e(TAG, String.format("onActivityResult() 异常 | requestCode=%d | 异常信息=%s",
requestCode, e.getMessage()));
ToastUtils.show("操作失败"); ToastUtils.show("操作失败");
} }
} }
@Override @Override
public void finish() { public void finish() {
LogUtils.d(TAG, "【生命周期】finish 触发isCommitSettings" + isCommitSettings + "isPreviewBackgroundChanged" + isPreviewBackgroundChanged); LogUtils.d(TAG, String.format("finish() | isCommitSettings=%b | isPreviewBackgroundChanged=%b",
isCommitSettings, isPreviewBackgroundChanged));
if (isCommitSettings) { if (isCommitSettings) {
super.finish(); super.finish();
} else { } else {
@@ -135,7 +177,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, "【权限回调】requestCode" + requestCode + ",权限数量:" + permissions.length); LogUtils.d(TAG, String.format("onRequestPermissionsResult() | requestCode=%d | 权限数量=%d | 结果数量=%d",
requestCode, permissions.length, grantResults.length));
if (requestCode == REQUEST_CAMERA_PERMISSION) { if (requestCode == REQUEST_CAMERA_PERMISSION) {
handleCameraPermissionResult(grantResults); handleCameraPermissionResult(grantResults);
} }
@@ -143,9 +186,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
// ====================== 界面初始化方法Toolbar + 点击事件)====================== // ====================== 界面初始化方法Toolbar + 点击事件)======================
private void initToolbar() { private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar); mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) { if (mToolbar == null) {
LogUtils.e(TAG, "【初始化异常】Toolbar未找到"); LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return; return;
} }
setSupportActionBar(mToolbar); setSupportActionBar(mToolbar);
@@ -155,15 +199,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mToolbar.setNavigationOnClickListener(new View.OnClickListener() { mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "导航栏点击返回按钮"); LogUtils.d(TAG, "导航栏 点击返回按钮");
finish(); finish();
} }
}); });
LogUtils.d(TAG, "【界面初始化】Toolbar 配置完成"); LogUtils.d(TAG, "initToolbar() 配置完成");
} }
private void initClickListeners() { private void initClickListeners() {
LogUtils.d(TAG, "【界面初始化】开始绑定按钮点击事件"); LogUtils.d(TAG, "initClickListeners() 开始绑定按钮点击事件");
// 绑定所有按钮点击事件 // 绑定所有按钮点击事件
bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener); bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener); bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener);
@@ -175,16 +219,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener); bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener); bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener); bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener);
LogUtils.d(TAG, "【界面初始化】按钮点击事件绑定完成"); LogUtils.d(TAG, "initClickListeners() 按钮点击事件绑定完成");
} }
// 通用按钮绑定工具方法 // 通用按钮绑定工具方法
private void bindClickListener(int resId, View.OnClickListener listener) { private void bindClickListener(int resId, View.OnClickListener listener) {
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d", resId));
View view = findViewById(resId); View view = findViewById(resId);
if (view != null) { if (view != null) {
view.setOnClickListener(listener); view.setOnClickListener(listener);
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d 绑定成功", resId));
} else { } else {
LogUtils.e(TAG, "【绑定异常】未找到视图:" + resId); LogUtils.e(TAG, String.format("bindClickListener() | 未找到视图:%d", resId));
} }
} }
@@ -192,10 +238,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() { private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】取消背景图片"); LogUtils.d(TAG, "onOriginNullClickListener() | 取消背景图片");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空"); LogUtils.e(TAG, "onOriginNullClickListener() | 预览Bean为空");
return; return;
} }
previewBean.setIsUseBackgroundFile(false); previewBean.setIsUseBackgroundFile(false);
@@ -208,17 +254,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() { private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】选择图片"); LogUtils.d(TAG, "onSelectPictureClickListener() | 选择图片");
launchImageSelector(); launchImageSelector();
} }
}; };
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() { private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener(){ LogUtils.d(TAG, "onNetworkBackgroundDialog() | 打开网络背景对话框");
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener() {
@Override @Override
public void onConfirm(String szConfirmFilePath) { public void onConfirm(String szConfirmFilePath) {
LogUtils.d(TAG, String.format("网络背景确认 onConfirm() | 文件路径=%s", szConfirmFilePath));
// 拷贝文件到预览数据并启动裁剪 // 拷贝文件到预览数据并启动裁剪
if (putUriFileToPreviewSource(new File(szConfirmFilePath))) { if (putUriFileToPreviewSource(new File(szConfirmFilePath))) {
startImageCrop(false); startImageCrop(false);
@@ -227,16 +275,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override @Override
public void onCancel() { public void onCancel() {
LogUtils.d(TAG, "网络背景取消 onCancel()");
} }
}); });
networkBackgroundDialog.show(); networkBackgroundDialog.show();
} }
}; };
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】固定比例裁剪"); LogUtils.d(TAG, "onCropPictureClickListener() | 固定比例裁剪");
startImageCrop(false); startImageCrop(false);
} }
}; };
@@ -244,7 +293,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】自由裁剪"); LogUtils.d(TAG, "onCropFreePictureClickListener() | 自由裁剪");
startImageCrop(true); startImageCrop(true);
} }
}; };
@@ -252,11 +301,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】拍照"); LogUtils.d(TAG, "onTakePhotoClickListener() | 拍照");
// 动态申请相机权限 // 动态申请相机权限
if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA) if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "拍照准备相机权限未授予,发起申请"); LogUtils.d(TAG, "拍照准备 | 相机权限未授予,发起申请");
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
BackgroundSettingsActivity.this, BackgroundSettingsActivity.this,
new String[]{Manifest.permission.CAMERA}, new String[]{Manifest.permission.CAMERA},
@@ -270,10 +319,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】恢复收到的图片"); LogUtils.d(TAG, "onReceivedPictureClickListener() | 恢复收到的图片");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空"); LogUtils.e(TAG, "onReceivedPictureClickListener() | 预览Bean为空");
return; return;
} }
previewBean.setIsUseBackgroundFile(true); previewBean.setIsUseBackgroundFile(true);
@@ -286,10 +335,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】像素拾取"); LogUtils.d(TAG, "onPixelPickerClickListener() | 像素拾取");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空"); LogUtils.e(TAG, "onPixelPickerClickListener() | 预览Bean为空");
ToastUtils.show("无有效图片可拾取像素"); ToastUtils.show("无有效图片可拾取像素");
return; return;
} }
@@ -297,46 +346,46 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
File targetFile = new File(targetImagePath); File targetFile = new File(targetImagePath);
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
ToastUtils.show("无有效图片可拾取像素"); ToastUtils.show("无有效图片可拾取像素");
LogUtils.e(TAG, "像素拾取失败文件无效:" + targetImagePath); LogUtils.e(TAG, String.format("像素拾取失败 | 文件无效:%s", targetImagePath));
return; return;
} }
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class); Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", targetImagePath); intent.putExtra("imagePath", targetImagePath);
startActivityForResult(intent, REQUEST_PIXELPICKER); startActivityForResult(intent, REQUEST_PIXELPICKER);
LogUtils.d(TAG, "像素拾取启动路径:" + targetImagePath); LogUtils.d(TAG, String.format("像素拾取启动 | 路径:%s", targetImagePath));
} }
}; };
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() { private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】清空像素颜色"); LogUtils.d(TAG, "onCleanPixelClickListener() | 清空像素颜色");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空"); LogUtils.e(TAG, "onCleanPixelClickListener() | 预览Bean为空");
return; return;
} }
int oldColor = previewBean.getPixelColor(); int oldColor = previewBean.getPixelColor();
previewBean.setPixelColor(0xFF000000); previewBean.setPixelColor(ImageUtils.getColorAccent(BackgroundSettingsActivity.this));
mBgSourceUtils.saveSettings(); mBgSourceUtils.saveSettings();
doubleRefreshPreview(); doubleRefreshPreview();
isPreviewBackgroundChanged = true; isPreviewBackgroundChanged = true;
ToastUtils.show("像素颜色已清空"); ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, "【像素清空】旧颜色:" + String.format("#%08X", oldColor)); LogUtils.d(TAG, String.format("像素清空 | 旧颜色:#%08X", oldColor));
} }
}; };
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() { private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】调色板按钮"); LogUtils.d(TAG, "onColorPaletteClickListener() | 调色板按钮");
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空"); LogUtils.e(TAG, "onColorPaletteClickListener() | 预览Bean为空");
return; return;
} }
int initialColor = previewBean.getPixelColor(); int initialColor = previewBean.getPixelColor();
LogUtils.d(TAG, "【调色板】初始颜色:" + String.format("#%08X", initialColor)); LogUtils.d(TAG, String.format("调色板 | 初始颜色:#%08X", initialColor));
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() { ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
@Override @Override
public void onColorSelected(int color) { public void onColorSelected(int color) {
@@ -344,11 +393,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mBgSourceUtils.saveSettings(); mBgSourceUtils.saveSettings();
doubleRefreshPreview(); doubleRefreshPreview();
isPreviewBackgroundChanged = true; isPreviewBackgroundChanged = true;
LogUtils.d(TAG, "【颜色选择】选中颜色:" + String.format("#%08X", color)); LogUtils.d(TAG, String.format("颜色选择 | 选中颜色:#%08X", color));
} }
}); });
dialog.show(); dialog.show();
LogUtils.d(TAG, "调色板对话框已显示"); LogUtils.d(TAG, "调色板 | 对话框已显示");
} }
}; };
@@ -359,9 +408,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 适配后的Uri失败返回null * @return 适配后的Uri失败返回null
*/ */
public Uri getFileProviderUri(File file) { public Uri getFileProviderUri(File file) {
LogUtils.d(TAG, "【工具方法】生成FileProvider Uri文件路径:" + (file != null ? file.getAbsolutePath() : "null")); LogUtils.d(TAG, String.format("getFileProviderUri() | 文件路径:%s", (file != null ? file.getAbsolutePath() : "null")));
if (file == null) { if (file == null) {
LogUtils.e(TAG, "【工具异常】文件为空"); LogUtils.e(TAG, "getFileProviderUri() | 文件为空");
return null; return null;
} }
try { try {
@@ -372,7 +421,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
return Uri.fromFile(file); return Uri.fromFile(file);
} }
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【工具异常】生成Uri失败" + e.getMessage()); LogUtils.e(TAG, String.format("getFileProviderUri() | 生成Uri失败%s", e.getMessage()));
return null; return null;
} }
} }
@@ -384,7 +433,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
*/ */
private boolean isBitmapValid(Bitmap bitmap) { private boolean isBitmapValid(Bitmap bitmap) {
boolean isValid = bitmap != null && !bitmap.isRecycled(); boolean isValid = bitmap != null && !bitmap.isRecycled();
LogUtils.d(TAG, "【工具方法】Bitmap有效性校验" + isValid); LogUtils.d(TAG, String.format("isBitmapValid() | Bitmap有效性校验%b", isValid));
return isValid; return isValid;
} }
@@ -392,9 +441,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 双重刷新预览,确保背景加载最新数据 * 双重刷新预览,确保背景加载最新数据
*/ */
private void doubleRefreshPreview() { private void doubleRefreshPreview() {
LogUtils.d(TAG, "【工具方法】开始双重刷新预览"); LogUtils.d(TAG, "doubleRefreshPreview() 开始双重刷新预览");
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) { if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
LogUtils.w(TAG, "双重刷新跳过对象为空或Activity已结束"); LogUtils.w(TAG, "双重刷新 跳过对象为空或Activity已结束");
return; return;
} }
@@ -402,11 +451,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
try { try {
mBgSourceUtils.loadSettings(); mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBackgroundView.loadBackgroundBean(previewBean, true); mBackgroundView.loadByBackgroundBean(previewBean, true);
mBackgroundView.setBackgroundColor(previewBean.getPixelColor()); LogUtils.d(TAG, "双重刷新 第一重完成");
LogUtils.d(TAG, "【双重刷新】第一重完成");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "双重刷新第一重异常:" + e.getMessage()); LogUtils.e(TAG, String.format("双重刷新 第一重异常:%s", e.getMessage()));
return; return;
} }
@@ -418,11 +466,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
try { try {
mBgSourceUtils.loadSettings(); mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBackgroundView.loadBackgroundBean(previewBean, true); mBackgroundView.loadByBackgroundBean(previewBean, true);
mBackgroundView.setBackgroundColor(previewBean.getPixelColor()); LogUtils.d(TAG, "双重刷新 第二重完成");
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "双重刷新第二重异常:" + e.getMessage()); LogUtils.e(TAG, String.format("双重刷新 第二重异常:%s", e.getMessage()));
} }
} }
} }
@@ -434,27 +481,30 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 初始化核心组件(工具类+视图) * 初始化核心组件(工具类+视图)
*/ */
private void initCoreComponents() { private void initCoreComponents() {
LogUtils.d(TAG, "initCoreComponents() 开始初始化");
// 初始化视图 // 初始化视图
mBackgroundView = findViewById(R.id.background_view); mBackgroundView = findViewById(R.id.background_view);
if (mBackgroundView == null) { if (mBackgroundView == null) {
LogUtils.e(TAG, "【初始化异常】BackgroundView未找到"); LogUtils.e(TAG, "initCoreComponents() | BackgroundView未找到");
} }
// 初始化工具类 // 初始化工具类
mBgSourceUtils = BackgroundSourceUtils.getInstance(this); mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings(); mBgSourceUtils.loadSettings();
mBitmapCache = BitmapCacheUtils.getInstance(); mBitmapCache = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "【初始化】视图与工具类加载完成"); LogUtils.d(TAG, "initCoreComponents() 视图与工具类加载完成");
} }
/** /**
* 处理意图或初始化预览 * 处理意图或初始化预览
*/ */
private void handleIntentOrPreview() { private void handleIntentOrPreview() {
LogUtils.d(TAG, "handleIntentOrPreview() 开始处理");
if (handleShareIntent()) { if (handleShareIntent()) {
ToastUtils.show("已接收分享图片"); ToastUtils.show("已接收分享图片");
LogUtils.d(TAG, "handleIntentOrPreview() | 处理分享意图成功");
} else { } else {
mBgSourceUtils.setCurrentSourceToPreview(); mBgSourceUtils.setCurrentSourceToPreview();
LogUtils.d(TAG, "【预览初始化】加载当前背景配置"); LogUtils.d(TAG, "handleIntentOrPreview() | 加载当前背景配置");
} }
} }
@@ -462,9 +512,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 初始化预览环境 * 初始化预览环境
*/ */
private void initPreviewEnvironment() { private void initPreviewEnvironment() {
LogUtils.d(TAG, "initPreviewEnvironment() 开始初始化");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean); mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean);
doubleRefreshPreview(); doubleRefreshPreview();
LogUtils.d(TAG, "initPreviewEnvironment() 初始化完成");
} }
/** /**
@@ -472,11 +524,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 处理成功返回true否则false * @return 处理成功返回true否则false
*/ */
private boolean handleShareIntent() { private boolean handleShareIntent() {
LogUtils.d(TAG, "handleShareIntent() 开始处理");
Intent intent = getIntent(); Intent intent = getIntent();
if (intent != null) { if (intent != null) {
String action = intent.getAction(); String action = intent.getAction();
String type = intent.getType(); String type = intent.getType();
LogUtils.d(TAG, "分享处理action" + action + "type" + type); LogUtils.d(TAG, String.format("分享处理 | action%stype%s", action, type));
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
showSharePreviewDialog(); showSharePreviewDialog();
return true; return true;
@@ -489,17 +542,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 显示分享图片预览对话框 * 显示分享图片预览对话框
*/ */
private void showSharePreviewDialog() { private void showSharePreviewDialog() {
LogUtils.d(TAG, "showSharePreviewDialog()"); LogUtils.d(TAG, "showSharePreviewDialog() 开始显示");
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener() { BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener() {
@Override @Override
public void onAcceptRecivedPicture(Uri uriRecivedPicture) { public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
LogUtils.d(TAG, String.format("分享确认 | Uri%s", uriRecivedPicture.toString()));
if (putUriFileToPreviewSource(uriRecivedPicture)) { if (putUriFileToPreviewSource(uriRecivedPicture)) {
startImageCrop(false); startImageCrop(false);
} }
} }
}); });
dlg.show(); dlg.show();
LogUtils.d(TAG, "分享处理显示图片预览对话框"); LogUtils.d(TAG, "分享处理 | 显示图片预览对话框");
} }
/** /**
@@ -508,19 +562,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 是图片返回true否则false * @return 是图片返回true否则false
*/ */
private boolean isImageType(String mimeType) { private boolean isImageType(String mimeType) {
if (mimeType == null) { if (mimeType == null) {
return false; return false;
} }
String lowerMimeType = mimeType.toLowerCase(); String lowerMimeType = mimeType.toLowerCase();
LogUtils.d("isImageType", "mimeType: " + mimeType + ", lowerMimeType: " + lowerMimeType); LogUtils.d(TAG, String.format("isImageType() | mimeType: %s, lowerMimeType: %s", mimeType, lowerMimeType));
return lowerMimeType.startsWith("image/"); return lowerMimeType.startsWith("image/");
} }
/** /**
* 启动图片选择器 * 启动图片选择器
*/ */
private void launchImageSelector() { private void launchImageSelector() {
LogUtils.d(TAG, "【业务逻辑】启动图片选择器"); LogUtils.d(TAG, "launchImageSelector() 启动图片选择器");
Intent[] intents = createImageSelectorIntents(); Intent[] intents = createImageSelectorIntents();
Intent validIntent = findValidIntent(intents); Intent validIntent = findValidIntent(intents);
@@ -536,6 +590,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 意图数组 * @return 意图数组
*/ */
private Intent[] createImageSelectorIntents() { private Intent[] createImageSelectorIntents() {
LogUtils.d(TAG, "createImageSelectorIntents() 开始创建");
Intent[] intents = new Intent[3]; Intent[] intents = new Intent[3];
// ACTION_GET_CONTENT // ACTION_GET_CONTENT
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
@@ -558,6 +613,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intents[2] = openDocIntent; intents[2] = openDocIntent;
} }
LogUtils.d(TAG, "createImageSelectorIntents() 意图数组创建完成");
return intents; return intents;
} }
@@ -567,11 +623,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 有效意图无则返回null * @return 有效意图无则返回null
*/ */
private Intent findValidIntent(Intent[] intents) { private Intent findValidIntent(Intent[] intents) {
LogUtils.d(TAG, "findValidIntent() 开始查找");
for (Intent intent : intents) { for (Intent intent : intents) {
if (intent != null && intent.resolveActivity(getPackageManager()) != null) { if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
LogUtils.d(TAG, "findValidIntent() | 找到有效意图");
return intent; return intent;
} }
} }
LogUtils.d(TAG, "findValidIntent() | 无有效意图");
return null; return null;
} }
@@ -580,17 +639,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param validIntent 有效意图 * @param validIntent 有效意图
*/ */
private void launchImageChooser(Intent validIntent) { private void launchImageChooser(Intent validIntent) {
LogUtils.d(TAG, "launchImageChooser() 启动选择器");
Intent chooser = Intent.createChooser(validIntent, "选择图片"); Intent chooser = Intent.createChooser(validIntent, "选择图片");
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(chooser, REQUEST_SELECT_PICTURE); startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
LogUtils.d(TAG, "【选图意图】启动图片选择"); LogUtils.d(TAG, "launchImageChooser() | 启动图片选择");
} }
/** /**
* 显示无相册应用提示对话框 * 显示无相册应用提示对话框
*/ */
private void showNoGalleryDialog() { private void showNoGalleryDialog() {
LogUtils.d(TAG, "【选图意图】无相册应用"); LogUtils.d(TAG, "showNoGalleryDialog() | 无相册应用");
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -614,12 +674,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 启动应用商店下载相册 * 启动应用商店下载相册
*/ */
private void launchGalleryMarket() { private void launchGalleryMarket() {
LogUtils.d(TAG, "launchGalleryMarket() 启动应用商店");
Intent marketIntent = new Intent(Intent.ACTION_VIEW); Intent marketIntent = new Intent(Intent.ACTION_VIEW);
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
if (marketIntent.resolveActivity(getPackageManager()) != null) { if (marketIntent.resolveActivity(getPackageManager()) != null) {
startActivity(marketIntent); startActivity(marketIntent);
LogUtils.d(TAG, "launchGalleryMarket() | 启动成功");
} else { } else {
ToastUtils.show("无法打开应用商店"); ToastUtils.show("无法打开应用商店");
LogUtils.e(TAG, "launchGalleryMarket() | 启动失败");
} }
} }
@@ -627,8 +690,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 处理操作取消或失败 * 处理操作取消或失败
*/ */
private void handleOperationCancelOrFail() { private void handleOperationCancelOrFail() {
LogUtils.d(TAG, "handleOperationCancelOrFail() 操作取消或失败");
mBgSourceUtils.setCurrentSourceToPreview(); mBgSourceUtils.setCurrentSourceToPreview();
LogUtils.d(TAG, "【业务逻辑】操作取消或失败,恢复预览");
ToastUtils.show("操作取消或失败"); ToastUtils.show("操作取消或失败");
doubleRefreshPreview(); doubleRefreshPreview();
} }
@@ -637,10 +700,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 处理拍照逻辑(权限通过后执行) * 处理拍照逻辑(权限通过后执行)
*/ */
void handleTakePhoto() { void handleTakePhoto() {
LogUtils.d(TAG, "【业务逻辑】开始处理拍照"); LogUtils.d(TAG, "handleTakePhoto() 开始处理拍照");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【拍照失败】预览Bean为空"); LogUtils.e(TAG, "handleTakePhoto() | 预览Bean为空");
ToastUtils.show("拍照文件创建失败"); ToastUtils.show("拍照文件创建失败");
return; return;
} }
@@ -648,7 +711,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
File takePhotoFile = new File(previewBean.getBackgroundFilePath()); File takePhotoFile = new File(previewBean.getBackgroundFilePath());
if (!takePhotoFile.exists()) { if (!takePhotoFile.exists()) {
ToastUtils.show("拍照文件创建失败"); ToastUtils.show("拍照文件创建失败");
LogUtils.e(TAG, "【拍照失败】文件不存在:" + takePhotoFile.getAbsolutePath()); LogUtils.e(TAG, String.format("handleTakePhoto() | 文件不存在:%s", takePhotoFile.getAbsolutePath()));
return; return;
} }
@@ -660,11 +723,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
} }
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
LogUtils.d(TAG, "【拍照启动】Uri" + photoUri.toString()); LogUtils.d(TAG, String.format("handleTakePhoto() | Uri%s", photoUri.toString()));
} catch (Exception e) { } catch (Exception e) {
String errMsg = "拍照启动异常:" + e.getMessage(); String errMsg = "拍照启动异常:" + e.getMessage();
ToastUtils.show(errMsg.substring(0, 20)); ToastUtils.show(errMsg.substring(0, 20));
LogUtils.e(TAG, "【拍照失败】" + e.getMessage()); LogUtils.e(TAG, String.format("handleTakePhoto() | %s", e.getMessage()));
} }
} }
@@ -674,6 +737,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据 * @param data 回调数据
*/ */
private void handleActivityResult(int requestCode, Intent data) { private void handleActivityResult(int requestCode, Intent data) {
LogUtils.d(TAG, String.format("handleActivityResult() | 处理请求码:%d", requestCode));
switch (requestCode) { switch (requestCode) {
case REQUEST_SELECT_PICTURE: case REQUEST_SELECT_PICTURE:
handleSelectPictureResult(data); handleSelectPictureResult(data);
@@ -688,7 +752,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
handlePixelPickerResult(); handlePixelPickerResult();
break; break;
default: default:
LogUtils.d(TAG, "【回调忽略】未知requestCode" + requestCode); LogUtils.d(TAG, String.format("handleActivityResult() | 未知requestCode%d", requestCode));
break; break;
} }
} }
@@ -698,10 +762,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据 * @param data 回调数据
*/ */
private void handleTakePhotoResult(Intent data) { private void handleTakePhotoResult(Intent data) {
LogUtils.d(TAG, "【业务逻辑】处理拍照结果"); LogUtils.d(TAG, "handleTakePhotoResult() 处理拍照结果");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【拍照结果处理】预览Bean为空"); LogUtils.e(TAG, "handleTakePhotoResult() | 预览Bean为空");
return; return;
} }
@@ -711,7 +775,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
doubleRefreshPreview(); doubleRefreshPreview();
startImageCrop(false); startImageCrop(false);
LogUtils.d(TAG, "【拍照完成】已启动裁剪"); LogUtils.d(TAG, "handleTakePhotoResult() | 已启动裁剪");
} }
/** /**
@@ -719,30 +783,30 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据 * @param data 回调数据
*/ */
private void handleSelectPictureResult(Intent data) { private void handleSelectPictureResult(Intent data) {
LogUtils.d(TAG, "【业务逻辑】处理选图结果"); LogUtils.d(TAG, "handleSelectPictureResult() 处理选图结果");
Uri selectedImage = data.getData(); Uri selectedImage = data.getData();
if (selectedImage == null) { if (selectedImage == null) {
ToastUtils.show("图片Uri为空"); ToastUtils.show("图片Uri为空");
LogUtils.e(TAG, "【选图结果】Uri为空"); LogUtils.e(TAG, "handleSelectPictureResult() | Uri为空");
return; return;
} }
LogUtils.d(TAG, "【选图回调】系统返回Uri : " + selectedImage.toString()); LogUtils.d(TAG, String.format("handleSelectPictureResult() | 系统返回Uri : %s", selectedImage.toString()));
// 申请持久化权限API33+ // 申请持久化权限API33+
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) { if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
getContentResolver().takePersistableUriPermission( getContentResolver().takePersistableUriPermission(
selectedImage, selectedImage,
Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent.FLAG_GRANT_READ_URI_PERMISSION);
LogUtils.d(TAG, "【选图权限】已添加持久化权限"); LogUtils.d(TAG, "handleSelectPictureResult() | 已添加持久化权限");
} }
// 同步文件并启动裁剪 // 同步文件并启动裁剪
if (putUriFileToPreviewSource(selectedImage)) { if (putUriFileToPreviewSource(selectedImage)) {
LogUtils.d(TAG, "【选图同步】路径绑定完成"); LogUtils.d(TAG, "handleSelectPictureResult() | 路径绑定完成");
startImageCrop(false); startImageCrop(false);
} else { } else {
ToastUtils.show("图片同步失败"); ToastUtils.show("图片同步失败");
LogUtils.e(TAG, "【选图同步】文件复制失败"); LogUtils.e(TAG, "handleSelectPictureResult() | 文件复制失败");
} }
} }
@@ -752,9 +816,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 同步成功返回true否则false * @return 同步成功返回true否则false
*/ */
private boolean putUriFileToPreviewSource(Uri srcUriFile) { private boolean putUriFileToPreviewSource(Uri srcUriFile) {
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源Uri%s", srcUriFile.toString()));
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile); String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
if (TextUtils.isEmpty(filePath)) { if (TextUtils.isEmpty(filePath)) {
LogUtils.e(TAG, "【选图同步】Uri解析路径为空"); LogUtils.e(TAG, "putUriFileToPreviewSource() | Uri解析路径为空");
return false; return false;
} }
File srcFile = new File(filePath); File srcFile = new File(filePath);
@@ -767,16 +832,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 同步成功返回true否则false * @return 同步成功返回true否则false
*/ */
private boolean putUriFileToPreviewSource(File srcFile) { private boolean putUriFileToPreviewSource(File srcFile) {
LogUtils.d(TAG, "【选图同步】源文件:" + srcFile.getAbsolutePath()); LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源文件:%s", srcFile.getAbsolutePath()));
mBgSourceUtils.loadSettings(); mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
File dstFile = new File(previewBean.getBackgroundFilePath()); File dstFile = new File(previewBean.getBackgroundFilePath());
LogUtils.d(TAG, "【选图同步】目标文件:" + dstFile.getAbsolutePath()); LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 目标文件:%s", dstFile.getAbsolutePath()));
if (FileUtils.copyFile(srcFile, dstFile)) { if (FileUtils.copyFile(srcFile, dstFile)) {
LogUtils.d(TAG, "【选图同步】文件拷贝成功"); LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件拷贝成功");
return true; return true;
} }
LogUtils.d(TAG, "【选图同步】文件无法拷贝"); LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件无法拷贝");
return false; return false;
} }
@@ -785,10 +850,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据 * @param data 回调数据
*/ */
private void handleCropImageResult(Intent data) { private void handleCropImageResult(Intent data) {
LogUtils.d(TAG, "【业务逻辑】处理裁剪结果"); LogUtils.d(TAG, "handleCropImageResult() 处理裁剪结果");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【裁剪结果处理】预览Bean为空"); LogUtils.e(TAG, "handleCropImageResult() | 预览Bean为空");
handleOperationCancelOrFail(); handleOperationCancelOrFail();
return; return;
} }
@@ -812,8 +877,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param fileSize 文件大小 * @param fileSize 文件大小
*/ */
private void handleCropSuccess(BackgroundBean previewBean, long fileSize) { private void handleCropSuccess(BackgroundBean previewBean, long fileSize) {
LogUtils.d(TAG, String.format("handleCropSuccess() | 裁剪成功,文件大小:%d", fileSize));
isPreviewBackgroundChanged = true; isPreviewBackgroundChanged = true;
LogUtils.d(TAG, "【裁剪结果】裁剪成功,文件大小:" + fileSize);
previewBean.setIsUseBackgroundFile(true); previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true); previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings(); mBgSourceUtils.saveSettings();
@@ -827,15 +892,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param fileSize 文件大小 * @param fileSize 文件大小
*/ */
private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) { private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) {
LogUtils.e(TAG, String.format("handleCropFailure() | 裁剪失败,文件状态:存在=%b可读=%b大小=%d",
isFileExist, isFileReadable, fileSize));
handleOperationCancelOrFail(); handleOperationCancelOrFail();
LogUtils.e(TAG, "【裁剪结果】裁剪失败,文件状态:存在=" + isFileExist + ",可读=" + isFileReadable + ",大小=" + fileSize);
} }
/** /**
* 处理像素拾取结果 * 处理像素拾取结果
*/ */
private void handlePixelPickerResult() { private void handlePixelPickerResult() {
LogUtils.d(TAG, "【业务逻辑】处理像素拾取结果"); LogUtils.d(TAG, "handlePixelPickerResult() 处理像素拾取结果");
doubleRefreshPreview(); doubleRefreshPreview();
isPreviewBackgroundChanged = true; isPreviewBackgroundChanged = true;
} }
@@ -845,11 +911,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param grantResults 权限结果数组 * @param grantResults 权限结果数组
*/ */
private void handleCameraPermissionResult(int[] grantResults) { private void handleCameraPermissionResult(int[] grantResults) {
LogUtils.d(TAG, "handleCameraPermissionResult() 处理相机权限结果");
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "【权限申请】相机权限授予成功"); LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予成功");
handleTakePhoto(); handleTakePhoto();
} else { } else {
LogUtils.d(TAG, "【权限申请】相机权限授予失败"); LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予失败");
ToastUtils.show("相机权限被拒绝,无法拍照"); ToastUtils.show("相机权限被拒绝,无法拍照");
// 引导用户到设置页面开启权限(用户选择不再询问时) // 引导用户到设置页面开启权限(用户选择不再询问时)
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
@@ -862,18 +929,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 启动应用设置页面 * 启动应用设置页面
*/ */
private void launchAppSettings() { private void launchAppSettings() {
LogUtils.d(TAG, "launchAppSettings() 启动应用设置页面");
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null); Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri); intent.setData(uri);
startActivity(intent); startActivity(intent);
ToastUtils.show("请在设置中开启相机权限"); ToastUtils.show("请在设置中开启相机权限");
LogUtils.d(TAG, "【权限引导】启动应用设置页面");
} }
/** /**
* 处理Finish确认对话框 * 处理Finish确认对话框
*/ */
private void handleFinishConfirmation() { private void handleFinishConfirmation() {
LogUtils.d(TAG, "handleFinishConfirmation() 处理Finish确认");
if (isPreviewBackgroundChanged) { if (isPreviewBackgroundChanged) {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() { YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override @Override
@@ -881,12 +949,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mBgSourceUtils.commitPreviewSourceToCurrent(); mBgSourceUtils.commitPreviewSourceToCurrent();
isCommitSettings = true; isCommitSettings = true;
finish(); finish();
Intent mainIntent = new Intent(BackgroundSettingsActivity.this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
startActivity(mainIntent);
LogUtils.d(TAG, "handleFinishConfirmation() | 确认设置启动MainActivity并刷新背景");
} }
@Override @Override
public void onNo() { public void onNo() {
isCommitSettings = true; isCommitSettings = true;
finish(); finish();
LogUtils.d(TAG, "handleFinishConfirmation() | 取消设置,关闭页面");
} }
}); });
} else { } else {
@@ -900,9 +973,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param isFreeCrop 是否自由裁剪 * @param isFreeCrop 是否自由裁剪
*/ */
private void startImageCrop(boolean isFreeCrop) { private void startImageCrop(boolean isFreeCrop) {
LogUtils.d(TAG, String.format("startImageCrop() | 是否自由裁剪:%b", isFreeCrop));
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) { if (previewBean == null) {
LogUtils.e(TAG, "【裁剪启动】预览Bean为空"); LogUtils.e(TAG, "startImageCrop() | 预览Bean为空");
ToastUtils.show("裁剪失败:无有效图片"); ToastUtils.show("裁剪失败:无有效图片");
return; return;
} }
@@ -914,7 +988,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
height, height,
isFreeCrop, isFreeCrop,
REQUEST_CROP_IMAGE); REQUEST_CROP_IMAGE);
LogUtils.d(TAG, "【裁剪启动】是否自由裁剪:" + isFreeCrop + ",目标尺寸:" + width + "x" + height); LogUtils.d(TAG, String.format("startImageCrop() | 目标尺寸:%dx%d", width, height));
} }
} }

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.powerbell.activities; package cc.winboll.studio.powerbell.activities;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/22 13:21
* @Describe BatteryReportActivity
*/
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
@@ -36,111 +31,90 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 电池报告页面统计应用24小时运行时长与电池消耗情况
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity { public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量(按功能分类) =========================
public static final String TAG = "BatteryReportActivity"; public static final String TAG = "BatteryReportActivity";
private static final long ONE_DAY_MS = 24 * 3600 * 1000; // 24小时毫秒数
private static final long ONE_MINUTE_MS = 60 * 1000; // 1分钟毫秒数
private Toolbar mToolbar; // ======================== 成员变量(按依赖优先级+功能分类) =========================
// UI组件
private Toolbar mToolbar;
private RecyclerView rvBatteryReport; private RecyclerView rvBatteryReport;
private BatteryReportAdapter adapter;
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
private BroadcastReceiver batteryReceiver;
private int batteryCapacity = 5400; // 电池容量mAh
private float lastBatteryPercent = 100.0f;
private long lastCheckTime = System.currentTimeMillis();
private EditText etSearch; private EditText etSearch;
private Map<String, Long> appRunTimeCache = new HashMap<String, Long>();
private Map<String, String> packageToAppNameCache = new HashMap<String, String>(); // 数据与适配器
private BatteryReportAdapter adapter;
private List<AppBatteryModel> dataList = new ArrayList<>();
private List<AppBatteryModel> filteredList = new ArrayList<>();
// 电池相关
private BroadcastReceiver batteryReceiver;
private int batteryCapacity = 5400; // 电池容量mAh
private float lastBatteryPercent = 100.0f; // 上次电池百分比
private long lastCheckTime = System.currentTimeMillis(); // 上次检查时间戳
// 缓存相关
private Map<String, Long> appRunTimeCache = new HashMap<>();
private Map<String, String> packageToAppNameCache = new HashMap<>();
private PackageManager mPackageManager; private PackageManager mPackageManager;
@Override // ======================== 接口实现方法 =========================
public Activity getActivity() { @Override
return this; public Activity getActivity() {
} return this;
}
@Override @Override
public String getTag() { public String getTag() {
return TAG; return TAG;
} }
// ======================== 生命周期方法(按执行顺序排列) =========================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_battery_report); setContentView(R.layout.activity_battery_report);
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始");
mToolbar = findViewById(R.id.toolbar); // 初始化UI组件
setSupportActionBar(mToolbar); initView();
mToolbar.setSubtitle(getTag()); // 初始化PackageManager
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
mPackageManager = getPackageManager(); mPackageManager = getPackageManager();
LogUtils.d(TAG, "【onCreate】基础组件初始化完成");
// 权限检查Java7 传统条件判断) // 权限检查Java7 传统条件判断)
if (!hasUsageStatsPermission(this)) { if (!hasUsageStatsPermission(this)) {
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show(); Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)); startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限引导用户开启");
return; return;
} }
etSearch = (EditText) findViewById(R.id.et_search); // 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
// 初始化流程新增“加载24小时累计耗电”步骤
loadAllAppPackage(); loadAllAppPackage();
preCacheAllAppNames(); preCacheAllAppNames();
appRunTimeCache = getAppRunTime(); appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel(); updateAppRunTimeToModel();
calculateInitial24hTotalConsumption(); // 初始化时计算24小时累计耗电 calculateInitial24hTotalConsumption();
filteredList.addAll(dataList); filteredList.addAll(dataList);
LogUtils.d(TAG, "【onCreate】数据初始化完成原始数据量" + dataList.size());
// 初始化适配器
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache); adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
rvBatteryReport.setAdapter(adapter); rvBatteryReport.setAdapter(adapter);
LogUtils.d(TAG, "【onCreate】适配器初始化完成过滤后数据量" + filteredList.size());
// 搜索监听(不变) // 绑定搜索监听 + 注册电池广播
etSearch.addTextChangedListener(new TextWatcher() { bindSearchListener();
@Override registerBatteryReceiver();
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
public void onTextChanged(CharSequence s, int start, int before, int count) {
filterAppsByPackageAndName(s.toString());
}
@Override
public void afterTextChanged(Editable s) {}
});
// 电池广播:调用修改后的“单次耗电计算+累计累加”方法
batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra("level", 100);
int scale = intent.getIntExtra("scale", 100);
float currentPercent = (float) level / scale * 100;
LogUtils.d(TAG, "电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
if (currentPercent < lastBatteryPercent) {
float dropPercent = lastBatteryPercent - currentPercent;
long duration = System.currentTimeMillis() - lastCheckTime;
LogUtils.d(TAG, "电池消耗:" + dropPercent + "%,时长:" + duration + "ms");
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache); // 单次+累计逻辑
}
lastBatteryPercent = currentPercent;
lastCheckTime = System.currentTimeMillis();
}
};
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
} }
@Override @Override
@@ -149,82 +123,213 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
// Java7 显式非空判断 // Java7 显式非空判断
if (batteryReceiver != null) { if (batteryReceiver != null) {
unregisterReceiver(batteryReceiver); unregisterReceiver(batteryReceiver);
LogUtils.d(TAG, "【onDestroy】电池广播已注销");
} }
LogUtils.d(TAG, "【onDestroy】BatteryReportActivity 销毁完成");
} }
// ======================== UI初始化方法 =========================
private void initView() {
// 初始化Toolbar
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
// 初始化RecyclerView与搜索框
etSearch = (EditText) findViewById(R.id.et_search);
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
// ======================== 搜索监听绑定方法 =========================
private void bindSearchListener() {
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String keyword = s.toString().trim();
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化" + keyword);
filterAppsByPackageAndName(keyword);
}
@Override
public void afterTextChanged(Editable s) {}
});
LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成");
}
// ======================== 电池广播注册方法 =========================
private void registerBatteryReceiver() {
batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra("level", 100);
int scale = intent.getIntExtra("scale", 100);
float currentPercent = (float) level / scale * 100;
LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
if (currentPercent < lastBatteryPercent) {
float dropPercent = lastBatteryPercent - currentPercent;
long duration = System.currentTimeMillis() - lastCheckTime;
LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration));
// 更新运行时长并计算耗电
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache);
}
// 刷新记录
lastBatteryPercent = currentPercent;
lastCheckTime = System.currentTimeMillis();
}
};
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成");
}
// ======================== 权限检查方法 =========================
/** /**
* 加载所有应用仅获取包名初始化模型时单次耗电、累计耗电均设为0 * 检查是否拥有使用情况访问权限
* @param context 上下文
* @return 拥有权限返回true否则返回false
*/
private boolean hasUsageStatsPermission(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LogUtils.w(TAG, "【hasUsageStatsPermission】系统版本低于LOLLIPOP不支持使用情况访问权限");
return false;
}
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
if (manager == null) {
LogUtils.e(TAG, "【hasUsageStatsPermission】获取UsageStatsManager失败");
return false;
}
long endTime = System.currentTimeMillis();
long startTime = endTime - ONE_MINUTE_MS;
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
boolean hasPermission = statsList != null && !statsList.isEmpty();
LogUtils.d(TAG, "【hasUsageStatsPermission】使用情况访问权限检查结果" + hasPermission);
return hasPermission;
}
// ======================== 数据加载与缓存方法 =========================
/**
* 加载所有应用包名,初始化数据模型
*/ */
private void loadAllAppPackage() { private void loadAllAppPackage() {
List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA); List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
dataList.clear(); dataList.clear();
LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表共找到" + appList.size() + "个应用");
LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用");
for (ApplicationInfo appInfo : appList) { for (ApplicationInfo appInfo : appList) {
String packageName = appInfo.packageName; String packageName = appInfo.packageName;
// 初始化单次耗电consumption=0累计耗电totalConsumption=0运行时长=0
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0)); dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
} }
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成共添加" + dataList.size() + "个包名");
LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名。");
} }
/** /**
* 预缓存应用名称(逻辑不变) * 预缓存所有应用名称减少PackageManager重复调用
*/ */
private void preCacheAllAppNames() { private void preCacheAllAppNames() {
packageToAppNameCache.clear(); packageToAppNameCache.clear();
LogUtils.d(TAG, "开始预缓存包名-应用名称映射"); LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
for (AppBatteryModel model : dataList) { for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName(); String packageName = model.getPackageName();
String appName = getAppNameByPackage(packageName); String appName = getAppNameByPackage(packageName);
packageToAppNameCache.put(packageName, appName); packageToAppNameCache.put(packageName, appName);
} }
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成共缓存" + packageToAppNameCache.size() + "个应用名称");
LogUtils.d(TAG, "预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
} }
/** /**
* 通过包名获取应用名称(逻辑不变) * 通过包名获取应用名称,带异常处理
* @param packageName 应用包名
* @return 应用名称,获取失败返回包名
*/ */
private String getAppNameByPackage(String packageName) { private String getAppNameByPackage(String packageName) {
LogUtils.v(TAG, "【getAppNameByPackage】查询包名" + packageName);
try { try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0); ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
return mPackageManager.getApplicationLabel(appInfo).toString(); return mPackageManager.getApplicationLabel(appInfo).toString();
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "包名" + packageName + "对应的应用未找到:" + e.getMessage()); LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
return packageName; return packageName;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "查询应用名称失败(包名:" + packageName + "" + e.getMessage()); LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "" + e.getMessage());
return packageName; return packageName;
} }
} }
/** /**
* 更新运行时长到模型(逻辑不变) * 更新运行时长到数据模型
*/ */
private void updateAppRunTimeToModel() { private void updateAppRunTimeToModel() {
int nCount = 0; int updateCount = 0;
for (AppBatteryModel model : dataList) { for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName(); String packageName = model.getPackageName();
Long runTime; Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
if (appRunTimeCache.containsKey(packageName)) {
runTime = appRunTimeCache.get(packageName);
LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName));
nCount++;
} else {
runTime = 0L;
}
model.setRunTime(runTime); model.setRunTime(runTime);
if (runTime > 0) {
updateCount++;
}
} }
LogUtils.d(TAG, String.format("dataList.size() %d appRunTimeCache.size() %d。", dataList.size(), appRunTimeCache.size())); LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成数据量" + dataList.size() + ",更新运行时长应用数:" + updateCount);
LogUtils.d(TAG, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount));
} }
/** /**
* 【新增】初始化时计算24小时累计耗电赋值给totalConsumption * 获取应用24小时运行时长
* @return 应用包名-运行时长ms映射
*/
private Map<String, Long> getAppRunTime() {
Map<String, Long> runTimeMap = new HashMap<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
long endTime = System.currentTimeMillis();
long startTime = endTime - ONE_DAY_MS; // 近24小时
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
for (android.app.usage.UsageStats stats : statsList) {
long runTimeMs = stats.getTotalTimeInForeground();
String packageName = stats.getPackageName();
runTimeMap.put(packageName, runTimeMs);
LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长" + formatRunTime(runTimeMs));
if (packageName.equals("aidepro.top")) {
LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果");
}
}
} catch (Exception e) {
LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败" + e.getMessage());
}
}
LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量" + runTimeMap.size());
return runTimeMap;
}
// ======================== 核心计算方法 =========================
/**
* 初始化时计算24小时累计耗电赋值给totalConsumption
* 逻辑基于24小时运行时长占比分配当前电池容量的理论24小时消耗 * 逻辑基于24小时运行时长占比分配当前电池容量的理论24小时消耗
*/ */
private void calculateInitial24hTotalConsumption() { private void calculateInitial24hTotalConsumption() {
@@ -233,53 +338,54 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) { for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
total24hRunTime += entry.getValue(); total24hRunTime += entry.getValue();
} }
LogUtils.d(TAG, "24小时内所有应用总运行时长" + formatRunTime(total24hRunTime)); LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长" + formatRunTime(total24hRunTime));
// 2. 按运行时长占比分配24小时累计耗电假设电池满电循环用总容量近似24小时总消耗 // 2. 按运行时长占比分配24小时累计耗电
for (AppBatteryModel model : dataList) { for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName(); String packageName = model.getPackageName();
Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L); Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
// 计算占比与累计耗电
float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0; float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
float initialTotalConsumption = batteryCapacity * ratio; // 用电池容量近似24小时总消耗 float initialTotalConsumption = batteryCapacity * ratio;
model.setTotalConsumption(initialTotalConsumption); // 初始化累计耗电 model.setTotalConsumption(initialTotalConsumption);
LogUtils.d(TAG, String.format("应用包 %s 24小时累计耗电初始化%.1f mAh", packageName, initialTotalConsumption)); LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化" + initialTotalConsumption + " mAh");
} }
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
} }
/** /**
* 【核心修改】计算单次耗电赋值给consumption+ 累加至累计耗电totalConsumption = totalConsumption + consumption * 计算单次耗电赋值给consumption+ 累加至累计耗电
* @param dropPercent 电池下降百分比
* @param runTimeMap 应用运行时长映射
*/ */
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) { private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】开始计算电池下降百分比" + dropPercent);
long totalSingleRunTime = 0; long totalSingleRunTime = 0;
// 1. 计算本次电池下降期间的总运行时长 // 1. 计算本次电池下降期间的总运行时长
for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) { for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
totalSingleRunTime += entry.getValue(); totalSingleRunTime += entry.getValue();
} }
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长" + formatRunTime(totalSingleRunTime));
// 2. 遍历计算每个应用的单次耗电”并“累加至累计” // 2. 遍历计算每个应用的单次耗电并累加
for (AppBatteryModel model : dataList) { for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName(); String packageName = model.getPackageName();
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L); Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
// 步骤1计算本次单次耗电赋值给consumption
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0; float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; // 单次消耗 float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
model.setConsumption(singleConsumption); // 存储单次耗电 model.setConsumption(singleConsumption);
// 步骤2累加单次耗电到累计耗电totalConsumption = 原有累计 + 本次单次) // 累加至累计耗电
float newTotalConsumption = model.getTotalConsumption() + singleConsumption; float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
model.setTotalConsumption(newTotalConsumption); // 更新累计耗电 model.setTotalConsumption(newTotalConsumption);
// 同步运行时长
model.setRunTime(appSingleRunTime); model.setRunTime(appSingleRunTime);
LogUtils.d(TAG, String.format("应用包 %s单次耗电%.1f mAh累计耗电%.1f mAh", LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s单次耗电%.1f mAh累计耗电%.1f mAh",
packageName, singleConsumption, newTotalConsumption)); packageName, singleConsumption, newTotalConsumption));
} }
// 3. 按累计耗电排序(从高到低) // 3. 按累计耗电降序排序
Collections.sort(dataList, new Comparator<AppBatteryModel>() { Collections.sort(dataList, new Comparator<AppBatteryModel>() {
@Override @Override
public int compare(AppBatteryModel m1, AppBatteryModel m2) { public int compare(AppBatteryModel m1, AppBatteryModel m2) {
@@ -287,71 +393,43 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
} }
}); });
// 4. 重新应用过滤并刷新列表 // 4. 重新过滤并刷新列表
filterAppsByPackageAndName(etSearch.getText().toString()); filterAppsByPackageAndName(etSearch.getText().toString().trim());
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成列表已刷新");
} }
/** /**
* 双维度过滤(逻辑不变 * 双维度过滤(包名+应用名
* @param keyword 搜索关键词
*/ */
private void filterAppsByPackageAndName(String keyword) { private void filterAppsByPackageAndName(String keyword) {
filteredList.clear(); filteredList.clear();
if (keyword == null || keyword.isEmpty()) { if (keyword == null || keyword.isEmpty()) {
filteredList.addAll(dataList); filteredList.addAll(dataList);
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空显示全部应用数量" + filteredList.size());
} else { } else {
String lowerKeyword = keyword.toLowerCase(); String lowerKeyword = keyword.toLowerCase();
for (AppBatteryModel model : dataList) { for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName(); String packageName = model.getPackageName();
String packageNameLower = packageName.toLowerCase(); String packageNameLower = packageName.toLowerCase();
String appName = packageToAppNameCache.get(packageName); String appName = packageToAppNameCache.get(packageName);
String appNameLower = appName.toLowerCase(); String appNameLower = appName.toLowerCase();
boolean isMatched = packageNameLower.contains(lowerKeyword) boolean isMatched = packageNameLower.contains(lowerKeyword) || appNameLower.contains(lowerKeyword);
|| appNameLower.contains(lowerKeyword);
if (isMatched) { if (isMatched) {
filteredList.add(model); filteredList.add(model);
} }
} }
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词" + keyword + ",匹配应用数量:" + filteredList.size());
} }
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
// ======================== 工具方法 =========================
/** /**
* 获取应用运行时长逻辑不变返回24小时运行时长 * 格式化运行时长
*/ * @param runTimeMs 运行时长ms
private Map<String, Long> getAppRunTime() { * @return 格式化后的运行时长字符串
Map<String, Long> runTimeMap = new HashMap<String, Long>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
long endTime = System.currentTimeMillis();
long startTime = endTime - 24 * 3600 * 1000; // 近24小时
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
for (android.app.usage.UsageStats stats : statsList) {
long runTimeMs = stats.getTotalTimeInForeground();
String packageName = stats.getPackageName();
LogUtils.d(TAG, "包名" + packageName + "24小时运行时长" + formatRunTime(runTimeMs));
runTimeMap.put(packageName, runTimeMs);
if (packageName.equals("aidepro.top")) {
LogUtils.d(TAG, String.format("runTimeMap.put(packageName, runTimeMs) 特殊查询 %s 查询有结果。", packageName));
}
}
} catch (Exception e) {
LogUtils.e(TAG, "获取应用运行时长失败:" + e.getMessage());
}
}
LogUtils.d(TAG, String.format("应用运行时长列表数量%d。", runTimeMap.size()));
return runTimeMap;
}
/**
* 格式化运行时长(逻辑不变)
*/ */
private String formatRunTime(long runTimeMs) { private String formatRunTime(long runTimeMs) {
if (runTimeMs <= 0) { if (runTimeMs <= 0) {
@@ -371,66 +449,46 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
} }
} }
// ======================== 内部类:数据模型 =========================
/** /**
* 权限检查(逻辑不变) * 应用电池数据模型
*/ * - consumption单次耗电两次电池广播间的消耗
private boolean hasUsageStatsPermission(Context context) { * - totalConsumption累计耗电24小时初始化值+后续单次累加)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { * - runTime运行时长ms
return false;
}
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
if (manager == null) {
return false;
}
long endTime = System.currentTimeMillis();
long startTime = endTime - 1000 * 60;
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
return statsList != null && !statsList.isEmpty();
}
/**
* 【核心修改】数据模型:明确字段含义
* - consumption单次耗电两次电池广播间的消耗float类型便于计算
* - totalConsumption累计耗电24小时初始化值+后续单次累加,显示用)
*/ */
public static class AppBatteryModel { public static class AppBatteryModel {
private String packageName; // 应用包名(核心标识) private String packageName; // 应用包名(核心标识)
private float consumption; // 单次耗电mAhfloat类型 private float consumption; // 单次耗电mAh
private float totalConsumption;// 累计耗电mAh,显示+排序用 private float totalConsumption;// 累计耗电mAh
private long runTime; // 运行时长ms private long runTime; // 运行时长ms
// Java7 显式构造初始化单次耗电、累计耗电为0 // Java7 显式构造
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) { public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
this.packageName = packageName; this.packageName = packageName;
this.consumption = consumption; // 单次耗电初始为0 this.consumption = consumption;
this.totalConsumption = totalConsumption; // 累计耗电初始为0后续初始化时赋值 this.totalConsumption = totalConsumption;
this.runTime = runTime; this.runTime = runTime;
} }
// Getter/Setter:覆盖所有字段,确保数据操作正常 // Getter/Setter
public String getPackageName() { public String getPackageName() {
return packageName; return packageName;
} }
public float getConsumption() { public float getConsumption() {
return consumption; // 获取单次耗电 return consumption;
} }
public void setConsumption(float consumption) { public void setConsumption(float consumption) {
this.consumption = consumption; // 设置单次耗电 this.consumption = consumption;
} }
public float getTotalConsumption() { public float getTotalConsumption() {
return totalConsumption; // 获取累计耗电(显示用) return totalConsumption;
} }
public void setTotalConsumption(float totalConsumption) { public void setTotalConsumption(float totalConsumption) {
this.totalConsumption = totalConsumption; // 设置累计耗电(初始化/累加用) this.totalConsumption = totalConsumption;
} }
public long getRunTime() { public long getRunTime() {
@@ -442,8 +500,9 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
} }
} }
// ======================== 内部类RecyclerView适配器 =========================
/** /**
* RecyclerView 适配器仅显示累计耗电totalConsumption逻辑适配模型修改 * 电池报告列表适配器,显示应用名称、累计耗电、运行时长
*/ */
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> { public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
private Context mContext; private Context mContext;
@@ -451,29 +510,30 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
private PackageManager mPm; private PackageManager mPm;
private Map<String, String> mPackageToNameCache; private Map<String, String> mPackageToNameCache;
// Java7 显式构造:接收名称缓存,确保显示时高效获取应用名 // Java7 显式构造
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList, public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
PackageManager pm, Map<String, String> packageToNameCache) { PackageManager pm, Map<String, String> packageToNameCache) {
this.mContext = context; this.mContext = context;
this.mDataList = dataList; this.mDataList = dataList;
this.mPm = pm; this.mPm = pm;
this.mPackageToNameCache = packageToNameCache; this.mPackageToNameCache = packageToNameCache;
LogUtils.d(TAG, "【BatteryReportAdapter】适配器构造完成数据量" + dataList.size());
} }
@Override @Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 加载系统列表项布局text1显示应用名text2显示累计耗电+时长)
View itemView = LayoutInflater.from(mContext) View itemView = LayoutInflater.from(mContext)
.inflate(android.R.layout.simple_list_item_2, parent, false); .inflate(android.R.layout.simple_list_item_2, parent, false);
return new ViewHolder(itemView); return new ViewHolder(itemView);
} }
@Override @Override
public void onBindViewHolder(ViewHolder holder, int position) { public void onBindViewHolder(ViewHolder holder, int position) {
// Java7 显式非空判断:避免空指针异常 // Java7 显式非空判断
if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) { if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
holder.tvAppName.setText("未知应用"); holder.tvAppName.setText("未知应用");
holder.tvConsumption.setText("累计耗电0.0 mAh | 运行时长0秒"); holder.tvConsumption.setText("累计耗电0.0 mAh | 运行时长0秒");
LogUtils.w(TAG, "【onBindViewHolder】数据异常位置" + position);
return; return;
} }
@@ -481,11 +541,11 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
String packageName = model.getPackageName(); String packageName = model.getPackageName();
String appName = ""; String appName = "";
// 优先从缓存获取应用名减少PackageManager调用提升性能 // 优先从缓存获取应用名
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) { if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
appName = mPackageToNameCache.get(packageName); appName = mPackageToNameCache.get(packageName);
} else { } else {
// 缓存无数据时兜底查询,并同步更新缓存 // 缓存无数据时兜底查询
try { try {
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0); ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
appName = mPm.getApplicationLabel(appInfo).toString(); appName = mPm.getApplicationLabel(appInfo).toString();
@@ -493,45 +553,40 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
mPackageToNameCache.put(packageName, appName); mPackageToNameCache.put(packageName, appName);
} }
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
appName = packageName; // 包名不存在时用包名兜底 appName = packageName;
LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage()); LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
} catch (Exception e) { } catch (Exception e) {
appName = packageName; // 其他异常时用包名兜底 appName = packageName;
LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "" + e.getMessage()); LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "" + e.getMessage());
} }
} }
// 显示逻辑:仅展示累计耗电totalConsumption隐藏单次耗电 // 显示逻辑:应用名称 + 累计耗电 + 运行时长
holder.tvAppName.setText(appName); holder.tvAppName.setText(appName);
// 格式化运行时长 + 累计耗电保留1位小数提升可读性
String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime()); String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s", String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
model.getTotalConsumption(), runTimeStr); model.getTotalConsumption(), runTimeStr);
holder.tvConsumption.setText(totalConsumptionText); holder.tvConsumption.setText(totalConsumptionText);
// 显示优化:文字颜色区分(避免所有应用均标蓝,仅示例可按需修改) // 显示优化
holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black)); holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray)); holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
// 调整文字大小:适配手机屏幕,提升可读性
holder.tvAppName.setTextSize(16); holder.tvAppName.setTextSize(16);
holder.tvConsumption.setTextSize(14); holder.tvConsumption.setTextSize(14);
} }
// 获取列表长度Java7 三元运算符判断空值,避免空指针
@Override @Override
public int getItemCount() { public int getItemCount() {
return mDataList == null ? 0 : mDataList.size(); return mDataList == null ? 0 : mDataList.size();
} }
/** /**
* ViewHolder绑定系统布局控件,与显示逻辑对应 * ViewHolder绑定系统布局控件
*/ */
public static class ViewHolder extends RecyclerView.ViewHolder { public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvAppName; // 显示应用名称 TextView tvAppName; // 应用名称
TextView tvConsumption; // 显示累计耗电 + 运行时长 TextView tvConsumption; // 累计耗电 + 运行时长
// Java7 显式构造绑定控件ID系统布局固定IDtext1、text2
public ViewHolder(View itemView) { public ViewHolder(View itemView) {
super(itemView); super(itemView);
tvAppName = (TextView) itemView.findViewById(android.R.id.text1); tvAppName = (TextView) itemView.findViewById(android.R.id.text1);

View File

@@ -6,8 +6,9 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.Switch; import android.widget.Switch;
import android.widget.TextView; import android.widget.TextView;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
@@ -17,90 +18,149 @@ import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.StringUtils; import cc.winboll.studio.powerbell.utils.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
/**
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
* 适配 API30基于 Java7 开发
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity { public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量(按功能分类) =========================
public static final String TAG = "ClearRecordActivity"; public static final String TAG = "ClearRecordActivity";
private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned.";
// ======================== 成员变量(按依赖优先级+功能分类) =========================
// UI组件
private Toolbar mToolbar; private Toolbar mToolbar;
TextView mtvRecordText; private TextView mtvRecordText;
App mApplication; private TextView tvAOHPCTCSeekBarMSG;
boolean mIsShowRecordWithEnter = false; private AOHPCTCSeekBar aOHPCTCSeekBar;
@Override // 应用与配置
public Activity getActivity() { private App mApplication;
return this; private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
}
@Override // ======================== 接口实现方法 =========================
public String getTag() { @Override
return TAG; public Activity getActivity() {
} return this;
}
@Override
public String getTag() {
return TAG;
}
// ======================== 生命周期方法(按执行顺序排列) =========================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clearrecord); setContentView(R.layout.activity_clearrecord);
mApplication = (App) getApplication(); LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
// 初始化工具栏 // 初始化应用实例
mToolbar = findViewById(R.id.toolbar); mApplication = (App) getApplication();
LogUtils.d(TAG, "【onCreate】应用实例初始化完成");
// 初始化核心逻辑
initView();
initSeekBar();
initRecordText();
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
}
// ======================== UI初始化方法 =========================
/**
* 初始化Toolbar与显示文本组件
*/
private void initView() {
// 初始化Toolbar
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar); setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag()); mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() { mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回"); LogUtils.d(TAG, "【导航栏】点击返回按钮,关闭当前页面");
finish(); finish();
} }
}); });
// 设置滑动清理控 // 初始化显示文本组
// tvAOHPCTCSeekBarMSG = (TextView) findViewById(R.id.activityclearrecordTextView1);
// 初始化发送拉动控件 mtvRecordText = (TextView) findViewById(R.id.activityclearrecordTextView2);
final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1); tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
/**
* 初始化滑动清理控件,设置回调监听
*/
private void initSeekBar() {
aOHPCTCSeekBar = (AOHPCTCSeekBar) findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer)); aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
aOHPCTCSeekBar.setThumbOffset(0); aOHPCTCSeekBar.setThumbOffset(0);
aOHPCTCSeekBar.setOnOHPCListener( aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
new AOHPCTCSeekBar.OnOHPCListener(){ @Override
public void onOHPCommit() {
LogUtils.d(TAG, "【onOHPCommit】滑动清理触发开始执行记录清理逻辑");
// 清理电池历史记录
mApplication.clearBatteryHistory();
// 发送广播更新前台通知
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
// 刷新记录显示
initRecordText();
// 提示清理成功
ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS);
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成已发送前台通知更新广播");
}
});
@Override LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成回调监听已绑定");
public void onOHPCommit() {
mApplication.clearBatteryHistory();
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
initRecordText();
String szMSG = "The APP battery record is cleaned.";
LogUtils.d(TAG, szMSG);
ToastUtils.show(szMSG);
}
});
// 初始化提示框
TextView tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
initRecordText();
} }
// ======================== 业务逻辑方法 =========================
/**
* 初始化记录显示文本,根据配置切换带换行/不带换行格式
*/
void initRecordText() { void initRecordText() {
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo(); ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
if (mIsShowRecordWithEnter) { String szRecordText;
String szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
mtvRecordText.setText(szRecordText); // 判空处理:避免空列表导致异常
} else { if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
String szRecordText = StringUtils.formatPCMListString(listBatteryInfo); szRecordText = getString(R.string.msg_no_battery_record);
mtvRecordText.setText(szRecordText); LogUtils.d(TAG, "【initRecordText】无电池记录数据显示空记录提示文本");
} } else {
// 根据配置切换显示格式
if (mIsShowRecordWithEnter) {
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
LogUtils.d(TAG, String.format("【initRecordText】使用带换行格式显示记录记录数量%d", listBatteryInfo.size()));
} else {
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
LogUtils.d(TAG, String.format("【initRecordText】使用无换行格式显示记录记录数量%d", listBatteryInfo.size()));
}
}
mtvRecordText.setText(szRecordText);
LogUtils.d(TAG, "【initRecordText】记录显示文本刷新完成");
} }
public void onShowRecordWithEnter(View view) { // ======================== 事件回调方法 =========================
Switch swShowRecordWithEnter = (Switch)view; /**
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked(); * 切换记录显示格式(带换行/不带换行)
initRecordText(); * @param view 触发事件的Switch控件
} */
public void onShowRecordWithEnter(View view) {
Switch swShowRecordWithEnter = (Switch) view;
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
LogUtils.d(TAG, String.format("【onShowRecordWithEnter】记录显示格式切换带换行显示%b", mIsShowRecordWithEnter));
// 刷新记录显示
initRecordText();
}
} }

View File

@@ -1,9 +1,5 @@
package cc.winboll.studio.powerbell.activities; package cc.winboll.studio.powerbell.activities;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/06/22 14:15
*/
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Intent; import android.content.Intent;
@@ -20,162 +16,200 @@ import android.widget.ImageView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
/**
* 像素拾取页面,支持加载图片并拾取指定位置像素颜色,同步至背景配置
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/06/22 14:15
*/
public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActivity { public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量 =========================
public static final String TAG = "PixelPickerActivity";
public static final String EXTRA_IMAGE_PATH = "imagePath"; // 图片路径传递键
// 提示文本常量
private static final String MSG_IMAGE_LOADED = "图片已加载,点击获取像素值";
private static final String MSG_NO_IMAGE_PATH = "未找到图片路径";
private static final String MSG_IMAGE_LOAD_FAILED = "图片加载失败";
private static final String MSG_FILE_NOT_EXIST = "图片文件不存在";
private static final String MSG_FILE_NOT_FOUND = "图片文件未找到";
private static final String MSG_PIXEL_OUT_OF_RANGE = "像素坐标超出范围";
private static final String MSG_TOUCH_OUT_OF_IMAGE = "点击位置超出图片显示范围";
private static final String MSG_PIXEL_CALC_FAILED = "计算像素位置失败";
private static final String MSG_PIXEL_RECORDED = "已记录像素值";
public static final String TAG = "PixelPickerActivity"; // ======================== 成员变量 =========================
// UI组件
private Toolbar mToolbar;
private ImageView imageView;
private TextView infoText;
private ViewGroup imageContainer;
private RelativeLayout mainLayout;
// 图片与像素数据
private Bitmap originalBitmap; // 原始图片Bitmap用于像素拾取
@Override // ======================== 接口实现方法 =========================
public Activity getActivity() { @Override
return this; public Activity getActivity() {
} return this;
}
@Override @Override
public String getTag() { public String getTag() {
return TAG; return TAG;
} }
private AToolbar mAToolbar;
private ImageView imageView;
private Bitmap originalBitmap;
private TextView infoText;
private ViewGroup imageContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pixelpicker);
// ======================== 生命周期方法 =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pixelpicker);
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化开始");
// 初始化UI组件
initView();
// 初始化工具栏 // 初始化工具栏
mAToolbar = (AToolbar) findViewById(R.id.toolbar); initToolbar();
setActionBar(mAToolbar); // 加载传递的图片
mAToolbar.setSubtitle(R.string.subtitle_activity_pixelpicker); loadImageFromIntent();
getActionBar().setDisplayHomeAsUpEnabled(true); // 绑定图片触摸事件
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { bindImageTouchListener();
@Override
public void onClick(View v) {
finish();
}
});
imageView = findViewById(R.id.imageView); LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化完成");
infoText = findViewById(R.id.infoText); }
imageContainer = findViewById(R.id.imageContainer);
// 从Intent获取图片路径并加载 @Override
String imagePath = getIntent().getStringExtra("imagePath"); protected void onResume() {
if (imagePath != null) { super.onResume();
loadImage(imagePath); LogUtils.d(TAG, "【onResume】PixelPickerActivity 恢复显示");
} else { // 同步背景颜色
infoText.setText("未找到图片路径"); setBackgroundColor();
} }
// 设置图片点击事件 @Override
imageContainer.setOnTouchListener(new View.OnTouchListener() { protected void onDestroy() {
super.onDestroy();
// 回收Bitmap资源避免内存泄漏
if (originalBitmap != null && !originalBitmap.isRecycled()) {
originalBitmap.recycle();
originalBitmap = null;
LogUtils.d(TAG, "【onDestroy】原始图片Bitmap资源已回收");
}
LogUtils.d(TAG, "【onDestroy】PixelPickerActivity 销毁完成");
}
// ======================== UI初始化方法 =========================
/**
* 初始化所有UI组件
*/
private void initView() {
imageView = findViewById(R.id.imageView);
infoText = findViewById(R.id.infoText);
imageContainer = findViewById(R.id.imageContainer);
mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
/**
* 初始化工具栏,设置导航与标题
*/
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override @Override
public boolean onTouch(View v, MotionEvent event) { public void onClick(View v) {
if (event.getAction() == MotionEvent.ACTION_DOWN && originalBitmap != null) { LogUtils.d(TAG, "导航栏 点击返回按钮");
// 计算点击位置在图片上的实际坐标 finish();
float touchX = event.getX();
float touchY = event.getY();
int pixelX = -1, pixelY = -1;
try {
// 获取图片在容器中的实际位置和尺寸
int[] imageLocation = new int[2];
imageView.getLocationInWindow(imageLocation);
int imageWidth = imageView.getWidth();
int imageHeight = imageView.getHeight();
// 计算缩放比例
float scaleX = (float) originalBitmap.getWidth() / imageWidth;
float scaleY = (float) originalBitmap.getHeight() / imageHeight;
// 调整触摸坐标到图片坐标系
float adjustedX = touchX - imageLocation[0];
float adjustedY = touchY - imageLocation[1];
// 检查是否在图片范围内
if (adjustedX >= 0 && adjustedX <= imageWidth && adjustedY >= 0 && adjustedY <= imageHeight) {
// 计算实际像素坐标
pixelX = (int) (adjustedX * scaleX);
pixelY = (int) (adjustedY * scaleY);
// 再次检查像素坐标是否在有效范围内
if (pixelX >= 0 && pixelX < originalBitmap.getWidth() &&
pixelY >= 0 && pixelY < originalBitmap.getHeight()) {
int pixelColor = originalBitmap.getPixel(pixelX, pixelY);
showPixelDialog(pixelColor, pixelX, pixelY);
} else {
Toast.makeText(PixelPickerActivity.this, "像素坐标超出范围", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(PixelPickerActivity.this, "点击位置超出图片显示范围", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(PixelPickerActivity.this, "计算像素位置失败", Toast.LENGTH_SHORT).show();
}
}
return true;
} }
}); });
} LogUtils.d(TAG, "initToolbar() 配置完成");
}
/** // ======================== 业务逻辑方法 =========================
* 加载图片 /**
*/ * 从Intent中获取图片路径并加载图片
private void loadImage(String imagePath) { */
try { private void loadImageFromIntent() {
File file = new File(imagePath); String imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
if (file.exists()) { LogUtils.d(TAG, "【loadImageFromIntent】获取到图片路径" + imagePath);
// 解码图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1; // 加载原图
originalBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
if (originalBitmap != null) { if (imagePath != null) {
imageView.setImageBitmap(originalBitmap); loadImage(imagePath);
infoText.setText("图片已加载,点击获取像素值"); } else {
} else { infoText.setText(MSG_NO_IMAGE_PATH);
infoText.setText("图片加载失败"); LogUtils.w(TAG, "【loadImageFromIntent】未获取到图片路径");
} }
} else { }
infoText.setText("图片文件不存在");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
infoText.setText("图片文件未找到");
}
}
/** /**
* 显示像素对话框 * 加载指定路径的图片
*/ * @param imagePath 图片文件路径
private void showPixelDialog(final int pixelColor, int x, int y) { */
final Dialog dialog = new Dialog(this); private void loadImage(String imagePath) {
dialog.setContentView(R.layout.dialog_pixel); try {
dialog.setCancelable(true); File file = new File(imagePath);
if (file.exists()) {
// 解码图片(加载原图)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
originalBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
// 设置像素颜色视图背景 if (originalBitmap != null) {
TextView colorView = dialog.findViewById(R.id.pixelColorView); imageView.setImageBitmap(originalBitmap);
colorView.setBackgroundColor(pixelColor); infoText.setText(MSG_IMAGE_LOADED);
LogUtils.d(TAG, "【loadImage】图片加载成功尺寸" + originalBitmap.getWidth() + "x" + originalBitmap.getHeight());
} else {
infoText.setText(MSG_IMAGE_LOAD_FAILED);
LogUtils.e(TAG, "【loadImage】图片解码失败");
}
} else {
infoText.setText(MSG_FILE_NOT_EXIST);
LogUtils.w(TAG, "【loadImage】图片文件不存在" + imagePath);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
infoText.setText(MSG_FILE_NOT_FOUND);
LogUtils.e(TAG, "【loadImage】图片文件未找到" + e.getMessage());
}
}
// 显示颜色信息 /**
TextView infoText = dialog.findViewById(R.id.colorInfoText); * 显示像素颜色信息对话框
String colorInfo = String.format( * @param pixelColor 拾取的像素颜色ARGB
* @param x 像素X坐标
* @param y 像素Y坐标
*/
private void showPixelDialog(final int pixelColor, int x, int y) {
final Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.dialog_pixel);
dialog.setCancelable(true);
// 设置颜色预览与信息展示
TextView colorView = dialog.findViewById(R.id.pixelColorView);
TextView infoTextView = dialog.findViewById(R.id.colorInfoText);
colorView.setBackgroundColor(pixelColor);
String colorInfo = String.format(
"RGB: (%d, %d, %d)\n" + "RGB: (%d, %d, %d)\n" +
"ARGB: #%08X\n" + "ARGB: #%08X\n" +
"实际像素位置: (%d, %d)", "实际像素位置: (%d, %d)",
@@ -184,77 +218,129 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
Color.blue(pixelColor), Color.blue(pixelColor),
pixelColor, pixelColor,
x, y); x, y);
infoText.setText(colorInfo); infoTextView.setText(colorInfo);
LogUtils.d(TAG, "【showPixelDialog】显示像素信息" + colorInfo);
// 设置确定按钮点击事件 // 确定按钮点击事件
Button confirmButton = dialog.findViewById(R.id.confirmButton); Button confirmButton = dialog.findViewById(R.id.confirmButton);
confirmButton.setOnClickListener(new View.OnClickListener() { confirmButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
dialog.dismiss(); dialog.dismiss();
// 可以在这里添加确定后的回调逻辑 // 保存像素颜色到背景配置
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this); savePixelColor(pixelColor);
BackgroundBean bean = utils.getPreviewBackgroundBean(); Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_RECORDED, Toast.LENGTH_SHORT).show();
bean.setPixelColor(pixelColor); // 同步背景颜色
utils.saveSettings();
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
setBackgroundColor(); setBackgroundColor();
} }
}); });
dialog.show(); dialog.show();
} LogUtils.d(TAG, "【showPixelDialog】像素对话框已显示");
}
@Override /**
protected void onDestroy() { * 保存拾取的像素颜色到背景配置
super.onDestroy(); * @param pixelColor 拾取的像素颜色ARGB
// 回收Bitmap资源 */
if (originalBitmap != null && !originalBitmap.isRecycled()) { private void savePixelColor(int pixelColor) {
originalBitmap.recycle(); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
originalBitmap = null; BackgroundBean bean = utils.getPreviewBackgroundBean();
} bean.setPixelColor(pixelColor);
} utils.saveSettings();
LogUtils.d(TAG, "【savePixelColor】像素颜色已保存#" + Integer.toHexString(pixelColor));
}
/**
* 同步背景颜色为拾取的像素颜色
*/
void setBackgroundColor() {
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
BackgroundBean bean = utils.getPreviewBackgroundBean();
int pixelColor = bean.getPixelColor();
mainLayout.setBackgroundColor(pixelColor);
LogUtils.d(TAG, "【setBackgroundColor】背景颜色已同步#" + Integer.toHexString(pixelColor));
}
void setBackgroundColor() { // ======================== 事件回调方法 =========================
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this); /**
BackgroundBean bean = utils.getPreviewBackgroundBean(); * 绑定图片容器的触摸事件,处理像素拾取逻辑
int nPixelColor = bean.getPixelColor(); */
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1); private void bindImageTouchListener() {
mainLayout.setBackgroundColor(nPixelColor); imageContainer.setOnTouchListener(new View.OnTouchListener() {
} @Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN && originalBitmap != null) {
float touchX = event.getX();
float touchY = event.getY();
LogUtils.v(TAG, "【onTouch】触摸坐标(" + touchX + ", " + touchY + ")");
@Override try {
protected void onResume() { // 获取图片在窗口中的位置与尺寸
super.onResume(); int[] imageLocation = new int[2];
setBackgroundColor(); imageView.getLocationInWindow(imageLocation);
} int imageWidth = imageView.getWidth();
int imageHeight = imageView.getHeight();
LogUtils.v(TAG, "【onTouch】图片显示尺寸" + imageWidth + "x" + imageHeight + ",位置:(" + imageLocation[0] + ", " + imageLocation[1] + ")");
// 计算缩放比例
float scaleX = (float) originalBitmap.getWidth() / imageWidth;
float scaleY = (float) originalBitmap.getHeight() / imageHeight;
LogUtils.v(TAG, "【onTouch】图片缩放比例X=" + scaleX + "Y=" + scaleY);
// 调整触摸坐标到图片显示区域坐标系
float adjustedX = touchX - imageLocation[0];
float adjustedY = touchY - imageLocation[1];
LogUtils.v(TAG, "【onTouch】调整后触摸坐标(" + adjustedX + ", " + adjustedY + ")");
// 检查是否在图片显示范围内
if (adjustedX >= 0 && adjustedX <= imageWidth && adjustedY >= 0 && adjustedY <= imageHeight) {
// 计算原始图片的像素坐标
int pixelX = (int) (adjustedX * scaleX);
int pixelY = (int) (adjustedY * scaleY);
LogUtils.v(TAG, "【onTouch】计算后像素坐标(" + pixelX + ", " + pixelY + ")");
// 检查像素坐标是否在原始图片范围内
if (pixelX >= 0 && pixelX < originalBitmap.getWidth() && pixelY >= 0 && pixelY < originalBitmap.getHeight()) {
int pixelColor = originalBitmap.getPixel(pixelX, pixelY);
showPixelDialog(pixelColor, pixelX, pixelY);
} else {
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_OUT_OF_RANGE, Toast.LENGTH_SHORT).show();
LogUtils.w(TAG, "【onTouch】像素坐标超出原始图片范围");
}
} else {
Toast.makeText(PixelPickerActivity.this, MSG_TOUCH_OUT_OF_IMAGE, Toast.LENGTH_SHORT).show();
LogUtils.w(TAG, "【onTouch】触摸位置超出图片显示范围");
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_CALC_FAILED, Toast.LENGTH_SHORT).show();
LogUtils.e(TAG, "【onTouch】计算像素位置失败" + e.getMessage());
}
}
return true;
}
});
LogUtils.d(TAG, "【bindImageTouchListener】图片触摸事件已绑定");
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
Intent intent = new Intent(); LogUtils.d(TAG, "【onOptionsItemSelected】点击返回菜单");
intent.setClass(this, BackgroundSettingsActivity.class); Intent intent = new Intent(this, BackgroundSettingsActivity.class);
startActivity(intent); startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), );
return true; return true;
} }
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
super.onBackPressed(); super.onBackPressed();
setResult(RESULT_OK); setResult(RESULT_OK);
finish(); finish();
// Intent intent = new Intent(); LogUtils.d(TAG, "【onBackPressed】返回键触发页面关闭");
// intent.setClass(this, BackgroundSettingsActivity.class); }
// startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
}
} }

View File

@@ -1,45 +1,85 @@
package cc.winboll.studio.powerbell.activities; package cc.winboll.studio.powerbell.activities;
import android.app.Activity; import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings;
import android.view.View; import android.view.View;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import java.lang.reflect.Field;
/** /**
* 应用设置窗口,提供应用配置项的统一入口
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/27 14:26 * @Date 2025/11/27 14:26
* @Describe 应用设置窗口 * @Describe 应用设置窗口
*/ */
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity { public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量 =========================
public static final String TAG = "SettingsActivity"; public static final String TAG = "SettingsActivity";
// 权限请求常量(为后续读取媒体图片权限预留)
private static final int REQUEST_READ_MEDIA_IMAGES = 1001; private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
private Toolbar mToolbar; // ======================== 成员变量 =========================
private Toolbar mToolbar; // 顶部工具栏
@Override // ======================== 接口实现方法 =========================
public Activity getActivity() { @Override
return this; public Activity getActivity() {
} return this;
}
@Override @Override
public String getTag() { public String getTag() {
return TAG; return TAG;
} }
// ======================== 生命周期方法 =========================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings); setContentView(R.layout.activity_settings);
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化开始");
mToolbar = findViewById(R.id.toolbar); // 初始化工具栏
initToolbar();
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
((CheckBox)findViewById(R.id.activitysettingsCheckBox1)).setChecked(thoughtfulServiceBean.isEnableUsePowerTts());
((CheckBox)findViewById(R.id.activitysettingsCheckBox2)).setChecked(thoughtfulServiceBean.isEnableChargeTts());
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
}
// ======================== UI初始化方法 =========================
/**
* 初始化顶部工具栏,设置导航返回与样式
*/
private void initToolbar() {
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar); setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag()); // 设置工具栏副标题与标题样式
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 显示返回按钮
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// 绑定导航点击事件
mToolbar.setNavigationOnClickListener(new View.OnClickListener() { mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -47,5 +87,98 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
finish(); finish();
} }
}); });
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
} }
public void onCheckTTSDrawOverlaysPermission(View view) {
canDrawOverlays();
}
public void onEnableChargeTts(View view) {
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
thoughtfulServiceBean.setIsEnableChargeTts(((CheckBox)view).isChecked());
ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
}
public void onEnableUsePowerTts(View view) {
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
thoughtfulServiceBean.setIsEnableUsePowerTts(((CheckBox)view).isChecked());
ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
}
/**
* 悬浮窗权限检查与请求
*/
void canDrawOverlays() {
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
// API6.0+校验权限
if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
showDrawOverlayRequestDialog();
} else {
ToastUtils.show("悬浮窗权限已开启");
}
}
/**
* 显示悬浮窗权限请求对话框
*/
private void showDrawOverlayRequestDialog() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
jumpToDrawOverlaySettings();
}
})
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
// 解决对话框焦点问题
if (dialog.getWindow() != null) {
dialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
dialog.show();
}
/**
* 跳转悬浮窗权限设置页面(反射适配低版本)
*/
private void jumpToDrawOverlaySettings() {
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
try {
// 反射获取设置页面Action避免高版本API依赖
Class<?> settingsClazz = Settings.class;
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
String action = (String) actionField.get(null);
// 跳转当前应用权限设置页
Intent intent = new Intent(action);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception e) {
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
}
}
} }

View File

@@ -3,47 +3,73 @@ package cc.winboll.studio.powerbell.activities;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.APPPlusUtils; import cc.winboll.studio.powerbell.utils.APPPlusUtils;
import cc.winboll.studio.powerbell.App;
/** /**
* 应用快捷方式活动类,处理应用图标快捷菜单的切换请求
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/15 13:45 * @Date 2025/11/15 13:45
* @Describe 应用快捷方式活动类 * @Describe 应用快捷方式活动类
*/ */
public class ShortcutActionActivity extends Activity { public class ShortcutActionActivity extends Activity {
// ======================== 静态常量 =========================
public static final String TAG = "ShortcutActionActivity"; public static final String TAG = "ShortcutActionActivity";
// 快捷指令常量
private static final String ACTION_SWITCH_TO_EN1 = "switchto_en1";
private static final String ACTION_SWITCH_TO_CN1 = "switchto_cn1";
private static final String ACTION_SWITCH_TO_CN2 = "switchto_cn2";
@Override // ======================== 生命周期方法 =========================
protected void onCreate(Bundle savedInstanceState) { @Override
super.onCreate(savedInstanceState); protected void onCreate(Bundle savedInstanceState) {
// 处理应用级别的切换请求 super.onCreate(savedInstanceState);
LogUtils.d(TAG, "【onCreate】ShortcutActionActivity 启动,开始处理快捷方式请求");
// 处理应用图标快捷菜单的切换请求
handleSwitchRequest(); handleSwitchRequest();
finish();
}
/** LogUtils.d(TAG, "【onCreate】快捷方式请求处理完成关闭活动");
* 处理应用图标快捷菜单的请求 finish();
}
// ======================== 业务逻辑方法 =========================
/**
* 处理应用图标快捷菜单的请求,根据意图数据切换应用启动组件
*/ */
private void handleSwitchRequest() { private void handleSwitchRequest() {
Intent intent = getIntent(); Intent intent = getIntent();
if (intent != null && "switchto_en1".equals(intent.getDataString())) { if (intent == null) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1); LogUtils.w(TAG, "【handleSwitchRequest】意图为空无法处理快捷方式请求");
ToastUtils.show("切换至" + getString(R.string.app_name) + "图标"); return;
//moveTaskToBack(true);
} }
if (intent != null && "switchto_cn1".equals(intent.getDataString())) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1); String dataString = intent.getDataString();
ToastUtils.show("切换至" + getString(R.string.app_name_cn1) + "图标"); LogUtils.d(TAG, "【handleSwitchRequest】获取到快捷指令" + dataString);
//moveTaskToBack(true);
} // 匹配快捷指令并切换组件
if (intent != null && "switchto_cn2".equals(intent.getDataString())) { if (ACTION_SWITCH_TO_EN1.equals(dataString)) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2); APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
ToastUtils.show("切换至" + getString(R.string.app_name_cn2) + "图标"); String toastMsg = "切换至" + getString(R.string.app_name) + "图标";
//moveTaskToBack(true); ToastUtils.show(toastMsg);
LogUtils.d(TAG, "【handleSwitchRequest】已切换至EN1组件" + App.COMPONENT_EN1);
} else if (ACTION_SWITCH_TO_CN1.equals(dataString)) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
String toastMsg = "切换至" + getString(R.string.app_name_cn1) + "图标";
ToastUtils.show(toastMsg);
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN1组件" + App.COMPONENT_CN1);
} else if (ACTION_SWITCH_TO_CN2.equals(dataString)) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
String toastMsg = "切换至" + getString(R.string.app_name_cn2) + "图标";
ToastUtils.show(toastMsg);
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN2组件" + App.COMPONENT_CN2);
} else {
LogUtils.w(TAG, "【handleSwitchRequest】未匹配到有效快捷指令" + dataString);
} }
} }
} }

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.powerbell.activities; package cc.winboll.studio.powerbell.activities;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/06/19 20:35
* @Describe 应用窗口基类
*/
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.graphics.Color; import android.graphics.Color;
@@ -24,107 +19,187 @@ import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean; import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil; import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.BuildConfig; import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
@SuppressLint("SetTextI18n") /**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/06/19 20:35
* @Describe 应用窗口基类提供主题设置、Activity 管理、工具栏配置、全屏切换、版本标签显示等通用功能
* 适配 API30基于 Java7 开发,所有子类需继承此类实现统一窗口行为
*/
public abstract class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity { public abstract class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
// ======================== 静态常量 =========================
public static final String TAG = "WinBoLLActivity"; public static final String TAG = "WinBoLLActivity";
private static final String VERSION_TAG_TEXT = "MIMO SDK V%s"; // 版本标签文本格式
private static final float VERSION_TAG_TEXT_SIZE = 10f; // 版本标签字体大小sp
protected volatile AESThemeBean.ThemeType mThemeType; // ======================== 成员变量 =========================
protected TextView mTagView; protected volatile AESThemeBean.ThemeType mThemeType; // 当前主题类型
protected TextView mTagView; // 版本标签显示控件
// ======================== 接口实现 & 抽象方法 =========================
@Override
public abstract Activity getActivity();
@Override
public abstract String getTag();
// ======================== 生命周期方法 =========================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化开始", getTag()));
// 初始化主题
mThemeType = getThemeType(); mThemeType = getThemeType();
setThemeStyle(); setThemeStyle();
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化完成当前主题%s", getTag(), mThemeType));
AESThemeBean.ThemeType getThemeType() {
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
void setThemeStyle() {
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
} }
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
LogUtils.d(TAG, String.format("【%s-onStart】添加版本标签到页面", getTag()));
// 添加版本标签
addVersionNameToContentView(); addVersionNameToContentView();
} }
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// 注册到Activity管理器
WinBoLLActivityManager.getInstance().add(this);
LogUtils.d(TAG, String.format("【%s-onPostCreate】已注册到Activity管理器", getTag()));
}
@Override
protected void onDestroy() {
super.onDestroy();
// 从Activity管理器移除
WinBoLLActivityManager.getInstance().registeRemove(this);
LogUtils.d(TAG, String.format("【%s-onDestroy】已从Activity管理器移除", getTag()));
}
// ======================== 主题相关方法 =========================
/**
* 获取当前主题类型
* @return 主题类型枚举
*/
AESThemeBean.ThemeType getThemeType() {
int themeId = AESThemeUtil.getThemeTypeID(getApplicationContext());
AESThemeBean.ThemeType themeType = AESThemeBean.getThemeStyleType(themeId);
LogUtils.d(TAG, String.format("【%s-getThemeType】获取主题类型ID%d类型%s", getTag(), themeId, themeType));
return themeType;
}
/**
* 设置主题样式
*/
void setThemeStyle() {
int themeId = AESThemeUtil.getThemeTypeID(getApplicationContext());
setTheme(themeId);
LogUtils.d(TAG, String.format("【%s-setThemeStyle】应用主题样式ID%d", getTag(), themeId));
}
// ======================== UI 配置方法 =========================
/**
* 添加版本标签到页面底部
*/
protected void addVersionNameToContentView() { protected void addVersionNameToContentView() {
if (!isTagViewVisible()) { if (!isTagViewVisible()) {
LogUtils.d(TAG, String.format("【%s-addVersionNameToContentView】版本标签不可见跳过添加", getTag()));
return; return;
} }
if (mTagView == null) { if (mTagView == null) {
mTagView = new TextView(this); mTagView = new TextView(this);
// 配置版本标签样式
mTagView.setTextColor(Color.GRAY); mTagView.setTextColor(Color.GRAY);
mTagView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); mTagView.setTextSize(TypedValue.COMPLEX_UNIT_SP, VERSION_TAG_TEXT_SIZE);
mTagView.setText("MIMO SDK V" + BuildConfig.VERSION_NAME); mTagView.setText(String.format(VERSION_TAG_TEXT, BuildConfig.VERSION_NAME));
// 配置布局参数
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT); ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
// 添加到根布局
FrameLayout frameLayout = findViewById(android.R.id.content); FrameLayout frameLayout = findViewById(android.R.id.content);
frameLayout.addView(mTagView, params); if (frameLayout != null) {
frameLayout.addView(mTagView, params);
LogUtils.d(TAG, String.format("【%s-addVersionNameToContentView】版本标签添加完成版本%s", getTag(), BuildConfig.VERSION_NAME));
} else {
LogUtils.w(TAG, String.format("【%s-addVersionNameToContentView】根布局为空无法添加版本标签", getTag()));
}
} }
} }
protected boolean isTagViewVisible() { /**
return true; * 配置工具栏,显示返回按钮
} */
public void setupToolbar() { public void setupToolbar() {
Toolbar mToolbar = findViewById(R.id.toolbar); Toolbar mToolbar = findViewById(R.id.toolbar);
if (mToolbar != null) { if (mToolbar != null) {
setSupportActionBar(mToolbar); setSupportActionBar(mToolbar);
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
LogUtils.d(TAG, String.format("【%s-setupToolbar】工具栏配置完成已显示返回按钮", getTag()));
} else {
LogUtils.w(TAG, String.format("【%s-setupToolbar】ActionBar为空无法显示返回按钮", getTag()));
} }
} else {
LogUtils.w(TAG, String.format("【%s-setupToolbar】未找到工具栏控件IDtoolbar", getTag()));
} }
} }
@Override /**
protected void onPostCreate(Bundle savedInstanceState) { * 版本标签是否可见
super.onPostCreate(savedInstanceState); * @return 默认为true子类可重写修改
WinBoLLActivityManager.getInstance().add(this); */
} protected boolean isTagViewVisible() {
return true;
@Override
protected void onDestroy() {
super.onDestroy();
WinBoLLActivityManager.getInstance().registeRemove(this);
} }
// ======================== 菜单 & 返回键处理 =========================
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class); LogUtils.d(TAG, String.format("【%s-onOptionsItemSelected】点击返回菜单", getTag()));
return true; return true;
} }
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
super.onBackPressed(); super.onBackPressed();
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class); LogUtils.d(TAG, String.format("【%s-onBackPressed】触发返回键", getTag()));
} }
public void changeFullScreen(Activity activity) { // ======================== 工具方法 =========================
Window window = activity.getWindow(); /**
if (window == null){ * 切换至全屏模式,隐藏状态栏与导航栏
* @param activity 目标Activity
*/
public void changeFullScreen(Activity activity) {
if (activity == null) {
LogUtils.w(TAG, String.format("【%s-changeFullScreen】目标Activity为空无法切换全屏", getTag()));
return; return;
} }
Window window = activity.getWindow();
if (window == null) {
LogUtils.w(TAG, String.format("【%s-changeFullScreen】窗口为空无法切换全屏", getTag()));
return;
}
View decorView = window.getDecorView(); View decorView = window.getDecorView();
if (decorView == null){ if (decorView == null) {
LogUtils.w(TAG, String.format("【%s-changeFullScreen】DecorView为空无法切换全屏", getTag()));
return; return;
} }
// 配置全屏标志位
int flag = decorView.getSystemUiVisibility(); int flag = decorView.getSystemUiVisibility();
flag |= View.SYSTEM_UI_FLAG_FULLSCREEN; flag |= View.SYSTEM_UI_FLAG_FULLSCREEN;
flag |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; flag |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
@@ -132,6 +207,9 @@ public abstract class WinBoLLActivity extends AppCompatActivity implements IWinB
flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
decorView.setSystemUiVisibility(flag); decorView.setSystemUiVisibility(flag);
// 配置窗口标志位
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
LogUtils.d(TAG, String.format("【%s-changeFullScreen】已切换至全屏模式", getTag()));
} }
} }

View File

@@ -1,60 +1,106 @@
package cc.winboll.studio.powerbell.adapters; package cc.winboll.studio.powerbell.adapters;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 14:38:55
* @Describe 电池报告数据适配器
*/
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
import cc.winboll.studio.powerbell.models.BatteryData; import cc.winboll.studio.powerbell.models.BatteryData;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* 电池报告数据适配器用于RecyclerView展示电池电量、充放电时间数据
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 14:38:55
* @Describe 电池报告数据适配器
*/
public class BatteryAdapter extends RecyclerView.Adapter<BatteryAdapter.ViewHolder> { public class BatteryAdapter extends RecyclerView.Adapter<BatteryAdapter.ViewHolder> {
// ======================== 静态常量 =========================
public static final String TAG = "BatteryAdapter"; public static final String TAG = "BatteryAdapter";
private List<BatteryData> dataList = new ArrayList<>(); private static final String FORMAT_BATTERY_LEVEL = "%d%%"; // 电量显示格式
private static final String PREFIX_DISCHARGE_TIME = "使用时间: "; // 放电时间前缀
private static final String PREFIX_CHARGE_TIME = "充电时间: "; // 充电时间前缀
public void updateData(List<BatteryData> newData) { // ======================== 成员变量 =========================
dataList = newData; private List<BatteryData> dataList = new ArrayList<>(); // 电池数据列表
notifyDataSetChanged();
// ======================== 构造方法 =========================
public BatteryAdapter() {
LogUtils.d(TAG, "【BatteryAdapter】适配器初始化初始数据列表为空");
} }
// ======================== 数据操作方法 =========================
/**
* 更新适配器数据并刷新列表
* @param newData 新的电池数据列表
*/
public void updateData(List<BatteryData> newData) {
LogUtils.d(TAG, "【updateData】开始更新数据新数据列表是否为空" + (newData == null));
// 判空处理,避免空指针
if (newData != null) {
dataList = newData;
LogUtils.d(TAG, "【updateData】数据更新完成当前数据量" + dataList.size());
} else {
dataList.clear();
LogUtils.w(TAG, "【updateData】新数据列表为空已清空本地数据");
}
notifyDataSetChanged();
LogUtils.d(TAG, "【updateData】已通知列表刷新");
}
// ======================== RecyclerView 重写方法 =========================
@Override @Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LogUtils.d(TAG, "【onCreateViewHolder】创建ViewHolder父容器" + parent.getContext().getClass().getSimpleName());
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_battery_report, parent, false); .inflate(R.layout.item_battery_report, parent, false);
return new ViewHolder(view); ViewHolder viewHolder = new ViewHolder(view);
LogUtils.d(TAG, "【onCreateViewHolder】ViewHolder创建完成");
return viewHolder;
} }
@Override @Override
public void onBindViewHolder(ViewHolder holder, int position) { public void onBindViewHolder(ViewHolder holder, int position) {
LogUtils.d(TAG, "【onBindViewHolder】绑定ViewHolder位置" + position);
// 判空与越界校验
if (dataList == null || dataList.isEmpty() || position >= dataList.size()) {
LogUtils.w(TAG, "【onBindViewHolder】数据异常无法绑定视图位置" + position);
return;
}
BatteryData item = dataList.get(position); BatteryData item = dataList.get(position);
holder.tvLevel.setText(String.format("%d%%", item.getCurrentLevel())); // 绑定数据到视图
holder.tvDischargeTime.setText("使用时间: " + item.getDischargeTime()); holder.tvLevel.setText(String.format(FORMAT_BATTERY_LEVEL, item.getCurrentLevel()));
holder.tvChargeTime.setText("充电时间: " + item.getChargeTime()); holder.tvDischargeTime.setText(PREFIX_DISCHARGE_TIME + item.getDischargeTime());
holder.tvChargeTime.setText(PREFIX_CHARGE_TIME + item.getChargeTime());
LogUtils.d(TAG, "【onBindViewHolder】视图绑定完成位置" + position + ",电量:" + item.getCurrentLevel() + "%");
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return dataList.size(); int count = dataList.size();
LogUtils.d(TAG, "【getItemCount】获取条目数量" + count);
return count;
} }
// ======================== ViewHolder 内部类 =========================
static class ViewHolder extends RecyclerView.ViewHolder { static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvLevel; TextView tvLevel; // 电量显示
TextView tvDischargeTime; TextView tvDischargeTime; // 放电时间显示
TextView tvChargeTime; TextView tvChargeTime; // 充电时间显示
ViewHolder(View itemView) { ViewHolder(View itemView) {
super(itemView); super(itemView);
// 初始化视图控件
tvLevel = itemView.findViewById(R.id.tvLevel); tvLevel = itemView.findViewById(R.id.tvLevel);
tvDischargeTime = itemView.findViewById(R.id.tvDischargeTime); tvDischargeTime = itemView.findViewById(R.id.tvDischargeTime);
tvChargeTime = itemView.findViewById(R.id.tvChargeTime); tvChargeTime = itemView.findViewById(R.id.tvChargeTime);
LogUtils.d(TAG, "【ViewHolder】控件初始化完成");
} }
} }
} }

View File

@@ -1,4 +1,5 @@
package cc.winboll.studio.powerbell.dialogs; package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -8,133 +9,145 @@ import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.Toast; import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity; import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.UriUtils; import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
/** /**
* 背景图片的接收分享文件后的预览对话框
* 适配 API30基于 Java7 开发支持分享图片的Uri解析、预览与确认选择
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/25 16:27:53 * @Date 2024/04/25 16:27:53
* @Describe 背景图片的接收分享文件后的预览对话框 * @Describe 背景图片的接收分享文件后的预览对话框
*/ */
public class BackgroundPicturePreviewDialog extends Dialog { public class BackgroundPicturePreviewDialog extends Dialog {
// ======================== 静态常量 =========================
public static final String TAG = "BackgroundPicturePreviewDialog"; public static final String TAG = "BackgroundPicturePreviewDialog";
private static final String TOAST_MSG_EMPTY_FILE = "接收到的文件为空。"; // 空文件提示文本
Context mContext; // ======================== 成员变量 =========================
//BackgroundSourceUtils mBackgroundPictureUtils; private Context mContext; // 上下文对象
Button dialogbackgroundpicturepreviewButton1; private IOnRecivedPictureListener mIOnRecivedPictureListener; // 图片接收监听
Button dialogbackgroundpicturepreviewButton2; private Uri mUriRecivedPicture; // 接收的图片Uri
//String mszPreReceivedFileName; // 控件对象
IOnRecivedPictureListener mIOnRecivedPictureListener; private BackgroundView mBackgroundView; // 背景预览视图
Uri mUriRecivedPicture; private Button dialogbackgroundpicturepreviewButton1; // 取消按钮
BackgroundView mBackgroundView; private Button dialogbackgroundpicturepreviewButton2; // 确认按钮
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
super(context);
setContentView(R.layout.dialog_backgroundpicturepreview);
mIOnRecivedPictureListener = iOnRecivedPictureListener;
//initEnv();
mContext = context;
//mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
mBackgroundView = findViewById(R.id.backgroundview);
previewRecivedPicture();
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 不使用分享到的图片
// 跳转到主窗口
Intent i = new Intent(mContext, MainActivity.class);
mContext.startActivity(i);
dismiss();
}
});
dialogbackgroundpicturepreviewButton2 = findViewById(R.id.dialogbackgroundpicturepreviewButton2);
dialogbackgroundpicturepreviewButton2.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
// 使用分享到的图片
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
// 关闭对话框
dismiss();
}
});
}
// void initEnv() {
// LogUtils.d(TAG, "initEnv()");
// mszPreReceivedFileName = "PreReceived.data";
// }
void previewRecivedPicture() {
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
//取出文件uri
mUriRecivedPicture = activity.getIntent().getData();
if (mUriRecivedPicture == null) {
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
//获取文件真实地址
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
if (TextUtils.isEmpty(szSrcImage)) {
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
mBackgroundView.loadImage(szSrcImage);
//
// File fSrcImage = new File(szSrcImage);
// //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
// File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
// // 复制源图片到剪裁文件
// try {
// FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
// LogUtils.d(TAG, "copyFileUsingFileChannels");
// Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
// imageView.setBackground(drawable);
// //LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
// } catch (IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// }
}
//
// 创建图片背景图片目录
//
// boolean createBackgroundFolder2(String szBackgroundFolder) {
// // 文件路径参数为空值或无效值时返回false.
// if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
// return false;
// }
//
// LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
// File f = new File(szBackgroundFolder);
// if (f.exists()) {
// if (f.isDirectory()) {
// return true;
// } else {
// // 工作路径不是一个目录
// LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
// return false;
// }
// } else {
// return f.mkdirs();
// }
// }
// ======================== 接口定义 =========================
/**
* 图片接收监听接口用于通知确认选择的图片Uri
*/
public interface IOnRecivedPictureListener { public interface IOnRecivedPictureListener {
void onAcceptRecivedPicture(Uri uriRecivedPicture); void onAcceptRecivedPicture(Uri uriRecivedPicture);
} }
// ======================== 构造方法 =========================
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
super(context);
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化开始");
// 初始化成员变量
mContext = context;
mIOnRecivedPictureListener = iOnRecivedPictureListener;
// 设置布局与控件
setContentView(R.layout.dialog_backgroundpicturepreview);
initViews();
bindButtonClickEvents();
// 预览接收的图片
previewRecivedPicture();
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化完成");
}
// ======================== 视图初始化方法 =========================
/**
* 初始化对话框内所有控件
*/
private void initViews() {
mBackgroundView = findViewById(R.id.backgroundview);
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
dialogbackgroundpicturepreviewButton2 = findViewById(R.id.dialogbackgroundpicturepreviewButton2);
LogUtils.d(TAG, "【initViews】对话框控件初始化完成");
}
// ======================== 事件绑定方法 =========================
/**
* 绑定按钮点击事件
*/
private void bindButtonClickEvents() {
// 取消按钮:跳转到主页面并关闭对话框
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LogUtils.d(TAG, "【onClick】点击取消按钮跳转到主页面");
Intent intent = new Intent(mContext, MainActivity.class);
mContext.startActivity(intent);
dismiss();
LogUtils.d(TAG, "【onClick】对话框已关闭");
}
});
// 确认按钮:通知监听并关闭对话框
dialogbackgroundpicturepreviewButton2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【onClick】点击确认按钮通知接收图片");
if (mIOnRecivedPictureListener != null && mUriRecivedPicture != null) {
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
LogUtils.d(TAG, "【onClick】已通知监听图片Uri" + mUriRecivedPicture);
} else {
LogUtils.w(TAG, "【onClick】监听为空或图片Uri无效无法通知");
}
dismiss();
LogUtils.d(TAG, "【onClick】对话框已关闭");
}
});
LogUtils.d(TAG, "【bindButtonClickEvents】按钮点击事件绑定完成");
}
// ======================== 业务逻辑方法 =========================
/**
* 预览接收的分享图片
*/
private void previewRecivedPicture() {
LogUtils.d(TAG, "【previewRecivedPicture】开始预览接收的图片");
// 校验上下文类型
if (!(mContext instanceof BackgroundSettingsActivity)) {
LogUtils.e(TAG, "【previewRecivedPicture】上下文不是BackgroundSettingsActivity无法获取图片Uri");
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
dismiss();
return;
}
BackgroundSettingsActivity activity = (BackgroundSettingsActivity) mContext;
// 从Intent中获取图片Uri优先getData其次EXTRA_STREAM
mUriRecivedPicture = activity.getIntent().getData();
if (mUriRecivedPicture == null) {
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
LogUtils.d(TAG, "【previewRecivedPicture】从EXTRA_STREAM获取Uri" + mUriRecivedPicture);
} else {
LogUtils.d(TAG, "【previewRecivedPicture】从getData获取Uri" + mUriRecivedPicture);
}
// 解析Uri为文件路径
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
//App.notifyMessage(TAG, "szSrcImage : " + szSrcImage);
if (TextUtils.isEmpty(szSrcImage)) {
LogUtils.w(TAG, "【previewRecivedPicture】解析的文件路径为空");
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
dismiss();
return;
}
// 加载图片到预览视图
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
mBackgroundView.loadImage(nCurrentPixelColor, szSrcImage, true);
LogUtils.d(TAG, "【previewRecivedPicture】图片预览完成文件路径" + szSrcImage);
}
} }

View File

@@ -23,14 +23,17 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import com.a4455jkjh.colorpicker.ColorPickerDialog; import com.a4455jkjh.colorpicker.ColorPickerDialog;
import com.a4455jkjh.colorpicker.view.OnColorChangedListener;
/** /**
* 调色板对话框支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
* 适配 API30基于 Java7 开发,返回 0xAARRGGBB 格式颜色(含透明度)
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/16 11:47 * @Date 2025/12/16 11:47
* @Describe 调色板对话框支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型) * @Describe 调色板对话框支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
*/ */
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener { public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
// ====================== 常量定义(首屏可见,统一管理) ====================== // ====================== 静态常量(首屏可见,统一管理) ======================
public static final String TAG = "ColorPaletteDialog"; public static final String TAG = "ColorPaletteDialog";
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值0-255 private static final int MAX_RGB_VALUE = 255; // RGB分量最大值0-255
private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比100%,无调节) private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比100%,无调节)
@@ -39,6 +42,8 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
private static final int MAX_BRIGHTNESS = 200; // 亮度最大值200%,避免过曝失真) private static final int MAX_BRIGHTNESS = 200; // 亮度最大值200%,避免过曝失真)
private static final int MAX_ALPHA_PERCENT = 100; // 透明度最大值100%=不透明) private static final int MAX_ALPHA_PERCENT = 100; // 透明度最大值100%=不透明)
private static final int MIN_ALPHA_PERCENT = 0; // 透明度最小值0%=完全透明) private static final int MIN_ALPHA_PERCENT = 0; // 透明度最小值0%=完全透明)
private static final String FORMAT_COLOR_HEX = "#%08X"; // 颜色值格式化AARRGGBB
private static final String FORMAT_PERCENT = "%d%%"; // 百分比格式化X%
// ====================== 回调接口(紧跟常量,逻辑关联) ====================== // ====================== 回调接口(紧跟常量,逻辑关联) ======================
public interface OnColorSelectedListener { public interface OnColorSelectedListener {
@@ -66,9 +71,9 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发) // 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
private static volatile boolean isAppSelfUpdatingColor = false; private static volatile boolean isAppSelfUpdatingColor = false;
// 控件引用(新增透明度进度条+文本) // 控件引用
private ImageView ivColorPicker; // 颜色预览拾取框 private ImageView ivColorPicker; // 颜色预览拾取框
private ImageView ivColorScaler; // 颜色渐变拾取框 private ImageView ivColorScaler; // 颜色渐变拾取框
private EditText etR; // R分量输入框显示实时调节值 private EditText etR; // R分量输入框显示实时调节值
private EditText etG; // G分量输入框显示实时调节值 private EditText etG; // G分量输入框显示实时调节值
private EditText etB; // B分量输入框显示实时调节值 private EditText etB; // B分量输入框显示实时调节值
@@ -93,12 +98,11 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
} }
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节) // 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
// 透明度初始颜色的alpha0-255转百分比0-100%
this.mOriginalAlpha = Color.alpha(initialColor); this.mOriginalAlpha = Color.alpha(initialColor);
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha); this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
this.mCurrentAlpha = mOriginalAlpha; this.mCurrentAlpha = mOriginalAlpha;
this.mCurrentAlphaPercent = mOriginalAlphaPercent; this.mCurrentAlphaPercent = mOriginalAlphaPercent;
// RGB初始颜色的RGB分量
this.mOriginalR = Color.red(initialColor); this.mOriginalR = Color.red(initialColor);
this.mOriginalG = Color.green(initialColor); this.mOriginalG = Color.green(initialColor);
this.mOriginalB = Color.blue(initialColor); this.mOriginalB = Color.blue(initialColor);
@@ -110,10 +114,11 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS; this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
this.mCurrentColor = initialColor; this.mCurrentColor = initialColor;
LogUtils.d(TAG, "init dialog success | 初始颜色:" + String.format("#%08X", initialColor) LogUtils.d(TAG, String.format("init dialog success | 初始颜色:%s | 原始RGB%d,%d,%d | 原始透明度:%s | 初始亮度:%s",
+ " | 原始RGB" + mOriginalR + "," + mOriginalG + "," + mOriginalB String.format(FORMAT_COLOR_HEX, initialColor),
+ " | 原始透明度:" + mOriginalAlphaPercent + "%" mOriginalR, mOriginalG, mOriginalB,
+ " | 初始亮度:" + mCurrentBrightnessPercent + "%"); String.format(FORMAT_PERCENT, mOriginalAlphaPercent),
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
} }
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ====================== // ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
@@ -142,11 +147,11 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
// ====================== 初始化核心方法(职责单一,便于维护) ====================== // ====================== 初始化核心方法(职责单一,便于维护) ======================
/** /**
* 控件绑定(新增透明度进度条+文本绑定) * 控件绑定
*/ */
private void initViewBind(View view) { private void initViewBind(View view) {
ivColorPicker = view.findViewById(R.id.iv_color_picker); ivColorPicker = view.findViewById(R.id.iv_color_picker);
ivColorScaler = view.findViewById(R.id.iv_color_scaler); ivColorScaler = view.findViewById(R.id.iv_color_scaler);
etR = view.findViewById(R.id.et_r); etR = view.findViewById(R.id.et_r);
etG = view.findViewById(R.id.et_g); etG = view.findViewById(R.id.et_g);
etB = view.findViewById(R.id.et_b); etB = view.findViewById(R.id.et_b);
@@ -161,9 +166,9 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
// 控件非空校验(小米低版本容错,绑定失败直接关闭对话框) // 控件非空校验(小米低版本容错,绑定失败直接关闭对话框)
if (ivColorPicker == null || ivColorScaler == null || etR == null || etG == null || etB == null || etColorValue == null if (ivColorPicker == null || ivColorScaler == null || etR == null || etG == null || etB == null || etColorValue == null
|| sbAlpha == null || tvAlphaValue == null || sbAlpha == null || tvAlphaValue == null
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null || tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|| tvConfirm == null || tvCancel == null) { || tvConfirm == null || tvCancel == null) {
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确"); LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确");
dismiss(); dismiss();
return; return;
@@ -183,22 +188,23 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
etG.setText(String.valueOf(mCurrentG)); etG.setText(String.valueOf(mCurrentG));
etB.setText(String.valueOf(mCurrentB)); etB.setText(String.valueOf(mCurrentB));
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB,大写更规范 // 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB
etColorValue.setText(String.format("#%08X", mCurrentColor)); etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
// 4. 透明度控件(进度条+文本,初始=原始透明度) // 4. 透明度控件(进度条+文本,初始=原始透明度)
sbAlpha.setProgress(mCurrentAlphaPercent); sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(mCurrentAlphaPercent + "%"); tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
// 5. 亮度控件显示默认100%,初始化按钮状态) // 5. 亮度控件显示默认100%,初始化按钮状态)
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%"); tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
updateBrightnessBtnStatus(); // 禁用边界值按钮初始100%,都可用) updateBrightnessBtnStatus(); // 禁用边界值按钮
LogUtils.d(TAG, "init data complete | 原始透明度:" + mOriginalAlphaPercent + "%"); LogUtils.d(TAG, String.format("init data complete | 原始透明度:%s",
String.format(FORMAT_PERCENT, mOriginalAlphaPercent)));
} }
/** /**
* 监听初始化(新增透明度进度条监听) * 监听初始化
*/ */
private void initListener() { private void initListener() {
// 点击监听(按钮+颜色拾取框) // 点击监听(按钮+颜色拾取框)
@@ -225,10 +231,10 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
// 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸) // 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸)
lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8); lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8);
lp.height = WindowManager.LayoutParams.WRAP_CONTENT; lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 软键盘适配:小米虚拟导航栏兼容,避免输入框被遮挡 // 软键盘适配:小米虚拟导航栏兼容
window.setAttributes(lp); window.setAttributes(lp);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘"); LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘");
} }
} }
@@ -261,7 +267,7 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
}); });
} }
// ====================== 透明度进度条监听实现(核心新增) ====================== // ====================== 透明度进度条监听实现 ======================
@Override @Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// 仅处理用户手动拖动进度条(避免应用自身更新时触发) // 仅处理用户手动拖动进度条(避免应用自身更新时触发)
@@ -277,7 +283,7 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
public void onStopTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {}
/** /**
* 拖动透明度进度条更新颜色(核心新增逻辑) * 拖动透明度进度条更新颜色
*/ */
private synchronized void updateAlphaBySeekBar(int alphaPercent) { private synchronized void updateAlphaBySeekBar(int alphaPercent) {
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
@@ -290,18 +296,17 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
calculateBrightnessAndUpdate(); calculateBrightnessAndUpdate();
// 同步所有控件 // 同步所有控件
updateAllViews(); updateAllViews();
LogUtils.d(TAG, "update alpha by seekbar | 透明度:" + mCurrentAlphaPercent + "%"); LogUtils.d(TAG, String.format("update alpha by seekbar | 透明度:%s",
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
} finally { } finally {
// 直接释放标记,避免卡顿 isAppSelfUpdatingColor = false; // 释放标记
isAppSelfUpdatingColor = false;
} }
} }
} }
// ====================== 颜色核心逻辑(新增透明度参数,全功能兼容) ====================== // ====================== 颜色核心逻辑 ======================
/** /**
* 核心计算基于原始RGB+当前亮度+当前透明度计算实时RGB+最终颜色 * 核心计算基于原始RGB+当前亮度+当前透明度计算实时RGB+最终颜色
* 逻辑亮度百分比→调节系数→原始RGB×系数→限制0-255→拼接透明度→最终颜色
*/ */
private void calculateBrightnessAndUpdate() { private void calculateBrightnessAndUpdate() {
// 亮度百分比转调节系数10%→0.1100%→1.0200%→2.0 // 亮度百分比转调节系数10%→0.1100%→1.0200%→2.0
@@ -317,98 +322,93 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
} }
/** /**
* 亮度减少每次减5%最低10%,防止过暗 * 亮度减少每次减5%最低10%
*/ */
private void decreaseBrightness() { private void decreaseBrightness() {
changeBrightness(false); changeBrightness(false);
} }
/** /**
* 亮度增加每次加5%最高200%,防止过曝 * 亮度增加每次加5%最高200%
*/ */
private void increaseBrightness() { private void increaseBrightness() {
changeBrightness(true); changeBrightness(true);
} }
/** /**
* 亮度调节核心方法(统一逻辑,加并发控制,同步所有控件 * 亮度调节核心方法(统一逻辑,加并发控制)
*/ */
private synchronized void changeBrightness(boolean isIncrease) { private synchronized void changeBrightness(boolean isIncrease) {
// 关键:判断非应用自身更新,才执行调节(避免重复触发/循环)
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新 isAppSelfUpdatingColor = true;
try { try {
if (isIncrease) { if (isIncrease) {
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return; // 达到最大值,不处理 if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return;
mCurrentBrightnessPercent += BRIGHTNESS_STEP; // 增加步长 mCurrentBrightnessPercent += BRIGHTNESS_STEP;
} else { } else {
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return; // 达到最小值,不处理 if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return;
mCurrentBrightnessPercent -= BRIGHTNESS_STEP; // 减少步长 mCurrentBrightnessPercent -= BRIGHTNESS_STEP;
} }
// 计算亮度调节后的实时RGB+最终颜色(含当前透明度) // 计算亮度调节后的实时RGB+最终颜色
calculateBrightnessAndUpdate(); calculateBrightnessAndUpdate();
// 同步所有控件 // 同步所有控件
updateAllViews(); updateAllViews();
LogUtils.d(TAG, (isIncrease ? "increase" : "decrease") + " brightness | " LogUtils.d(TAG, String.format("%s brightness | 亮度:%s | 实时RGB%d,%d,%d",
+ "亮度:" + mCurrentBrightnessPercent + "% | 实时RGB" + mCurrentR + "," + mCurrentG + "," + mCurrentB); isIncrease ? "increase" : "decrease",
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent),
mCurrentR, mCurrentG, mCurrentB));
} finally { } finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false; isAppSelfUpdatingColor = false;
} }
} }
} }
/** /**
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB容错处理,更新原始基准值+实时值) * 解析颜色字符串(支持#RRGGBB/#AARRGGBB更新原始基准值+实时值)
* 新增:解析颜色的透明度,同步更新透明度进度条
*/ */
private void parseColorFromStr(String colorStr, int triggerViewId) { private void parseColorFromStr(String colorStr, int triggerViewId) {
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新 isAppSelfUpdatingColor = true;
try { try {
if (TextUtils.isEmpty(colorStr)) return; if (TextUtils.isEmpty(colorStr)) return;
// 补全#前缀(兼容用户输入习惯如直接输AARRGGBB // 补全#前缀(兼容用户输入习惯)
if (!colorStr.startsWith("#")) { if (!colorStr.startsWith("#")) {
colorStr = "#" + colorStr; colorStr = "#" + colorStr;
} }
// 格式校验仅支持6位RRGGBB/8位AARRGGBB,避免非法格式 // 格式校验仅支持6位RRGGBB/8位AARRGGBB
if (colorStr.length() != 7 && colorStr.length() != 9) { if (colorStr.length() != 7 && colorStr.length() != 9) {
LogUtils.e(TAG, "parse color failed | 格式错误(需#RRGGBB/#AARRGGBB输入" + colorStr); LogUtils.e(TAG, String.format("parse color failed | 格式错误(需#RRGGBB/#AARRGGBB输入%s", colorStr));
return; return;
} }
// 解析颜色系统API安全可靠 // 解析颜色
int parsedColor = Color.parseColor(colorStr); int parsedColor = Color.parseColor(colorStr);
// 更新原始基准值(用户输入颜色,重置基准) // 更新原始基准值与实时值
// 透明度解析颜色的alpha0-255转百分比0-100%
mOriginalAlpha = Color.alpha(parsedColor); mOriginalAlpha = Color.alpha(parsedColor);
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha); mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
// RGB解析颜色的RGB分量
mOriginalR = Color.red(parsedColor); mOriginalR = Color.red(parsedColor);
mOriginalG = Color.green(parsedColor); mOriginalG = Color.green(parsedColor);
mOriginalB = Color.blue(parsedColor); mOriginalB = Color.blue(parsedColor);
// 更新实时值(原始值=实时值,无调节)
mCurrentAlpha = mOriginalAlpha; mCurrentAlpha = mOriginalAlpha;
mCurrentAlphaPercent = mOriginalAlphaPercent; mCurrentAlphaPercent = mOriginalAlphaPercent;
mCurrentR = mOriginalR; mCurrentR = mOriginalR;
mCurrentG = mOriginalG; mCurrentG = mOriginalG;
mCurrentB = mOriginalB; mCurrentB = mOriginalB;
// 重置亮度为100%
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS; mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = parsedColor; mCurrentColor = parsedColor;
// 同步所有控件 // 同步所有控件
updateAllViews(); updateAllViews();
LogUtils.d(TAG, "parse color success | 解析颜色:" + String.format("#%08X", parsedColor) LogUtils.d(TAG, String.format("parse color success | 解析颜色:%s | 透明度:%s | 重置亮度:%s",
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%"); String.format(FORMAT_COLOR_HEX, parsedColor),
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LogUtils.e(TAG, "parse color failed | 非法颜色格式,输入:" + colorStr, e); LogUtils.e(TAG, String.format("parse color failed | 非法颜色格式,输入:%s", colorStr), e);
} finally { } finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false; isAppSelfUpdatingColor = false;
} }
} }
@@ -416,77 +416,74 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
/** /**
* 通过RGB输入框更新颜色用户输入后更新原始基准值+实时值重置亮度为100% * 通过RGB输入框更新颜色用户输入后更新原始基准值+实时值重置亮度为100%
* 新增透明度基准值保持不变仅更新RGB
*/ */
private synchronized void updateColorByRGB(int triggerViewId) { private synchronized void updateColorByRGB(int triggerViewId) {
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新 isAppSelfUpdatingColor = true;
try { try {
// 解析用户输入的RGB值限制0-255非法输入设为0 // 解析用户输入的RGB值限制0-255非法输入设为0
int inputR = parseInputValue(etR.getText().toString()); int inputR = parseInputValue(etR.getText().toString());
int inputG = parseInputValue(etG.getText().toString()); int inputG = parseInputValue(etG.getText().toString());
int inputB = parseInputValue(etB.getText().toString()); int inputB = parseInputValue(etB.getText().toString());
// 更新原始基准值(用户手动输入,作为新的调节基准) // 更新原始基准值与实时值
mOriginalR = inputR; mOriginalR = inputR;
mOriginalG = inputG; mOriginalG = inputG;
mOriginalB = inputB; mOriginalB = inputB;
// 更新实时值(输入值=实时值,无亮度调节)
mCurrentR = inputR; mCurrentR = inputR;
mCurrentG = inputG; mCurrentG = inputG;
mCurrentB = inputB; mCurrentB = inputB;
// 重置亮度为100%(透明度保持当前值不变)
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS; mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
// 计算最终颜色(无亮度调节,拼接当前透明度)
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB); mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
// 同步所有控件 // 同步所有控件
updateAllViews(); updateAllViews();
LogUtils.d(TAG, "update color by RGB | 新原始RGB" + mOriginalR + "," + mOriginalG + "," + mOriginalB LogUtils.d(TAG, String.format("update color by RGB | 新原始RGB%d,%d,%d | 透明度:%s | 重置亮度:%s",
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%"); mOriginalR, mOriginalG, mOriginalB,
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "update color by RGB failed", e); LogUtils.e(TAG, "update color by RGB failed", e);
} finally { } finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false; isAppSelfUpdatingColor = false;
} }
} }
} }
/** /**
* 核心同步:更新所有控件显示(新增透明度控件同步,统一方法) * 核心同步:更新所有控件显示
*/ */
private void updateAllViews() { private void updateAllViews() {
// 1. 同步颜色预览(显示最终颜色,含透明度+亮度) // 1. 同步颜色预览
ivColorPicker.setBackgroundColor(mCurrentColor); ivColorPicker.setBackgroundColor(mCurrentColor);
// 2. 同步RGB输入框(显示实时调节值) // 2. 同步RGB输入框
etR.setText(String.valueOf(mCurrentR)); etR.setText(String.valueOf(mCurrentR));
etG.setText(String.valueOf(mCurrentG)); etG.setText(String.valueOf(mCurrentG));
etB.setText(String.valueOf(mCurrentB)); etB.setText(String.valueOf(mCurrentB));
// 3. 同步颜色值输入框(显示最终颜色,含透明度,格式#AARRGGBB // 3. 同步颜色值输入框
etColorValue.setText(String.format("#%08X", mCurrentColor)); etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
// 4. 同步透明度控件(进度条+文本,显示实时透明度) // 4. 同步透明度控件
sbAlpha.setProgress(mCurrentAlphaPercent); sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(mCurrentAlphaPercent + "%"); tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
// 5. 同步亮度控件(数值+按钮状态) // 5. 同步亮度控件
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%"); tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
updateBrightnessBtnStatus(); updateBrightnessBtnStatus();
LogUtils.d(TAG, "sync all views complete | 最终颜色:" + String.format("#%08X", mCurrentColor) LogUtils.d(TAG, String.format("sync all views complete | 最终颜色:%s | 实时RGB%d,%d,%d | 透明度:%s | 亮度:%s",
+ " | 实时RGB" + mCurrentR + "," + mCurrentG + "," + mCurrentB String.format(FORMAT_COLOR_HEX, mCurrentColor),
+ " | 透明度:" + mCurrentAlphaPercent + "% | 亮度:" + mCurrentBrightnessPercent + "%"); mCurrentR, mCurrentG, mCurrentB,
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
} }
/** /**
* 更新亮度按钮状态(边界值禁用,提升交互体验) * 更新亮度按钮状态(边界值禁用,提升交互体验)
*/ */
private void updateBrightnessBtnStatus() { private void updateBrightnessBtnStatus() {
// 亮度≤10%禁用减号文字变浅灰≥200%:禁用加号(文字变浅灰)
boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS; boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS;
boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS; boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS;
@@ -496,37 +493,37 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC")); tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC"));
} }
// ====================== 工具方法(新增透明度转换工具,通用复用) ====================== // ====================== 工具方法 ======================
/** /**
* 透明度0-255 → 0-100%(颜色计算值转用户直观百分比) * 透明度0-255 → 0-100%
*/ */
private int alpha2Percent(int alpha) { private int alpha2Percent(int alpha) {
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT); return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
} }
/** /**
* 透明度0-100% → 0-255(用户操作百分比转颜色计算值) * 透明度0-100% → 0-255
*/ */
private int percent2Alpha(int percent) { private int percent2Alpha(int percent) {
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE); return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
} }
/** /**
* 解析输入值限制0-255非法输入返回0,容错处理 * 解析输入值限制0-255非法输入返回0
*/ */
private int parseInputValue(String input) { private int parseInputValue(String input) {
if (TextUtils.isEmpty(input)) return 0; if (TextUtils.isEmpty(input)) return 0;
try { try {
int value = Integer.parseInt(input); int value = Integer.parseInt(input);
return Math.min(Math.max(value, 0), MAX_RGB_VALUE); // 限制范围,避免溢出 return Math.min(Math.max(value, 0), MAX_RGB_VALUE);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
LogUtils.e(TAG, "parse input failed | 非法数字,输入:" + input, e); LogUtils.e(TAG, String.format("parse input failed | 非法数字,输入:%s", input), e);
return 0; return 0;
} }
} }
/** /**
* RGB输入框监听复用(减少冗余代码,统一逻辑) * RGB输入框监听复用
*/ */
private void setEditTextWatcher(EditText editText, final int viewId) { private void setEditTextWatcher(EditText editText, final int viewId) {
editText.addTextChangedListener(new TextWatcher() { editText.addTextChangedListener(new TextWatcher() {
@@ -538,90 +535,77 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
@Override @Override
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable s) {
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
updateColorByRGB(viewId); // 输入变化后更新颜色 updateColorByRGB(viewId);
} }
} }
}); });
} }
/** /**
* dp转px适配小米不同分辨率,避免尺寸错乱,通用工具 * dp转px适配小米不同分辨率
*/ */
private int dp2px(float dp) { private int dp2px(float dp) {
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f); return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
} }
/** /**
* 显示系统颜色选择器兼容API29-30无高版本依赖小米机型适配 * 显示系统颜色选择器兼容API29-30无高版本依赖小米机型适配
* 核心调整:新增「水平滚动容器+颜色排列容器」二级结构内置圆形按钮无额外drawable依赖 */
*/ private void showSystemColorPicker() {
private void showSystemColorPicker() { LogUtils.d(TAG, "show system color picker | 兼容小米API29-30支持横向滚动");
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30支持横向滚动"); final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext()); builder.setTitle("选择基础颜色");
builder.setTitle("选择基础颜色");
// 50种常用颜色「红→橙→黄→绿→青→蓝→紫→粉→棕→灰→黑白」彩虹光谱顺序排列 // 50种常用颜色按彩虹光谱顺序排列
final int[] systemColors = { final int[] systemColors = {
// 红色系6种深红→大红→浅红→玫红→暗红→橘红 0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500, 0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
// 橙色系5种深橙→橙→浅橙→橙黄→橘橙 0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623, 0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
// 黄色系5种深黄→黄→浅黄→鹅黄→金黄 0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700, 0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
// 绿色系7种深绿→绿→浅绿→草绿→薄荷绿→翠绿→墨绿 0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300, 0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
// 青色系5种深青→青→浅青→蓝绿→青绿 0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0, 0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
// 蓝色系8种深蓝→藏蓝→蓝→浅蓝→天蓝→宝蓝→湖蓝→靛蓝 0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
// 紫色系6种深紫→紫→浅紫→紫罗兰→紫红→蓝紫
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
// 粉色系5种深粉→粉→浅粉→嫩粉→桃粉
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
// 棕色系4种深棕→棕→浅棕→棕黄
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
// 灰色系6种深灰→灰→浅灰→银灰→淡灰→浅银灰
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
// 黑白系3种黑→白→米白
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
}; };
// 1. 第一级:水平滚动容器 // 1. 第一级:水平滚动容器
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext()); HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
horizontalScrollView.setHorizontalScrollBarEnabled(true); horizontalScrollView.setHorizontalScrollBarEnabled(true);
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5)); horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
// 2. 第二级:颜色排列容器(横向) // 2. 第二级:颜色排列容器(横向)
LinearLayout colorLayout = new LinearLayout(getContext()); LinearLayout colorLayout = new LinearLayout(getContext());
colorLayout.setOrientation(LinearLayout.HORIZONTAL); colorLayout.setOrientation(LinearLayout.HORIZONTAL);
colorLayout.setGravity(Gravity.CENTER_VERTICAL); colorLayout.setGravity(Gravity.CENTER_VERTICAL);
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10)); colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
// 3. 循环添加颜色按钮(内置圆形效果,无额外依赖 // 3. 循环添加颜色按钮(内置圆形效果)
for (int i = 0; i < systemColors.length; i++) { for (int i = 0; i < systemColors.length; i++) {
final int color = systemColors[i]; final int color = systemColors[i];
ImageView colorBtn = new ImageView(getContext()); ImageView colorBtn = new ImageView(getContext());
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40)); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
if (i != systemColors.length - 1) { if (i != systemColors.length - 1) {
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距 lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
} }
colorBtn.setLayoutParams(lp); colorBtn.setLayoutParams(lp);
// 核心:内置圆形背景(白色边框+圆形形状无需drawable文件 // 内置圆形背景(白色边框+圆形形状)
GradientDrawable circleBg = new GradientDrawable(); GradientDrawable circleBg = new GradientDrawable();
circleBg.setShape(GradientDrawable.OVAL); // 圆形 circleBg.setShape(GradientDrawable.OVAL);
circleBg.setColor(color); // 按钮颜色 circleBg.setColor(color);
circleBg.setStroke(dp2px(2), Color.WHITE); // 白色边框2dp宽区分颜色 circleBg.setStroke(dp2px(2), Color.WHITE);
colorBtn.setBackground(circleBg); // 设置圆形背景 colorBtn.setBackground(circleBg);
colorBtn.setClickable(true); colorBtn.setClickable(true);
colorBtn.setFocusable(true); colorBtn.setFocusable(true);
// 点击事件(逻辑不变) // 点击事件
colorBtn.setOnClickListener(new View.OnClickListener() { colorBtn.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
@@ -641,57 +625,59 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
mCurrentColor = color; mCurrentColor = color;
updateAllViews(); updateAllViews();
builder.create().dismiss(); builder.create().dismiss();
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color) LogUtils.d(TAG, String.format("select system color | 选择颜色:%s | 透明度:%s",
+ " | 透明度:" + mCurrentAlphaPercent + "%"); String.format(FORMAT_COLOR_HEX, color),
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
} finally { } finally {
isAppSelfUpdatingColor = false; isAppSelfUpdatingColor = false;
} }
} }
} }
}); });
colorLayout.addView(colorBtn); colorLayout.addView(colorBtn);
} }
// 层级嵌套(滚动容器→颜色容器) // 层级嵌套
horizontalScrollView.addView(colorLayout); horizontalScrollView.addView(colorLayout);
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show(); builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
} }
// ====================== 点击事件实现(统一处理,逻辑清晰) ====================== // ====================== 点击事件实现 ======================
@Override @Override
public void onClick(View v) { public void onClick(View v) {
//ToastUtils.show("onClick");
int id = v.getId(); int id = v.getId();
// 关键:所有点击事件均加判断(避免并发冲突/重复触发) // 所有点击事件均加并发判断
if (!isAppSelfUpdatingColor) { if (!isAppSelfUpdatingColor) {
if (id == R.id.iv_color_picker) { if (id == R.id.iv_color_picker) {
showSystemColorPicker(); // 打开系统颜色选择器 showSystemColorPicker();
} if (id == R.id.iv_color_scaler) { } else if (id == R.id.iv_color_scaler) {
//ToastUtils.show("iv_color_scale"); openColorScalerDialog(mCurrentColor);
openColorScalerDialog(mCurrentColor); // 打开系统颜色选择器
} else if (id == R.id.tv_confirm) { } else if (id == R.id.tv_confirm) {
mListener.onColorSelected(mCurrentColor); // 确认选择,回调颜色 mListener.onColorSelected(mCurrentColor);
LogUtils.d(TAG, "confirm color | 回调颜色:" + String.format("#%08X", mCurrentColor)); LogUtils.d(TAG, String.format("confirm color | 回调颜色:%s",
String.format(FORMAT_COLOR_HEX, mCurrentColor)));
dismiss(); dismiss();
} else if (id == R.id.tv_cancel) { } else if (id == R.id.tv_cancel) {
dismiss(); // 取消,关闭对话框 dismiss();
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框"); LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
} else if (id == R.id.tv_brightness_minus) { } else if (id == R.id.tv_brightness_minus) {
decreaseBrightness(); // 减少亮度 decreaseBrightness();
} else if (id == R.id.tv_brightness_plus) { } else if (id == R.id.tv_brightness_plus) {
increaseBrightness(); // 增加亮度 increaseBrightness();
} }
} }
} }
void openColorScalerDialog(int nColor) { /**
//ToastUtils.show("openColorPickerDialog"); * 打开颜色渐变选择器
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor); */
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() { void openColorScalerDialog(int nColor) {
LogUtils.d(TAG, String.format("openColorScalerDialog | 初始颜色:%s",
String.format(FORMAT_COLOR_HEX, nColor)));
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
dlg.setOnColorChangedListener(new OnColorChangedListener() {
@Override @Override
public void beforeColorChanged() { public void beforeColorChanged() {}
}
@Override @Override
public void onColorChanged(int color) { public void onColorChanged(int color) {
@@ -699,49 +685,49 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
} }
@Override @Override
public void afterColorChanged() { public void afterColorChanged() {}
}
}); });
dlg.show(); dlg.show();
} }
// ====================== 内部类 ======================
class ColorScalerDialog extends ColorPickerDialog { class ColorScalerDialog extends ColorPickerDialog {
public int currentColorScalerDialogColor = 0; public int currentColorScalerDialogColor = 0;
public ColorScalerDialog(Context context, int p) {
super(context, p);
}
@Override public ColorScalerDialog(Context context, int p) {
public void dismiss() { super(context, p);
super.dismiss(); this.currentColorScalerDialogColor = p;
int color = currentColorScalerDialogColor; }
ToastUtils.show(String.format("dismiss color %d", color));
if (!isAppSelfUpdatingColor) { @Override
isAppSelfUpdatingColor = true; public void dismiss() {
try { super.dismiss();
mOriginalAlpha = Color.alpha(color); int color = currentColorScalerDialogColor;
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha); ToastUtils.show(String.format("选择颜色:%s", String.format(FORMAT_COLOR_HEX, color)));
mOriginalR = Color.red(color); if (!isAppSelfUpdatingColor) {
mOriginalG = Color.green(color); isAppSelfUpdatingColor = true;
mOriginalB = Color.blue(color); try {
mCurrentAlpha = mOriginalAlpha; mOriginalAlpha = Color.alpha(color);
mCurrentAlphaPercent = mOriginalAlphaPercent; mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
mCurrentR = mOriginalR; mOriginalR = Color.red(color);
mCurrentG = mOriginalG; mOriginalG = Color.green(color);
mCurrentB = mOriginalB; mOriginalB = Color.blue(color);
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS; mCurrentAlpha = mOriginalAlpha;
mCurrentColor = color; mCurrentAlphaPercent = mOriginalAlphaPercent;
updateAllViews(); mCurrentR = mOriginalR;
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color) mCurrentG = mOriginalG;
+ " | 透明度:" + mCurrentAlphaPercent + "%"); mCurrentB = mOriginalB;
} finally { mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
isAppSelfUpdatingColor = false; mCurrentColor = color;
} updateAllViews();
} LogUtils.d(TAG, String.format("select scaler color | 选择颜色:%s | 透明度:%s",
} String.format(FORMAT_COLOR_HEX, color),
} String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
} finally {
isAppSelfUpdatingColor = false;
}
}
}
}
} }

View File

@@ -1,10 +1,10 @@
package cc.winboll.studio.powerbell.dialogs; package cc.winboll.studio.powerbell.dialogs;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
@@ -15,69 +15,82 @@ import androidx.appcompat.app.AlertDialog;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ImageDownloader;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import cc.winboll.studio.powerbell.utils.ImageDownloader;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import android.text.TextUtils;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 20:11 * @Date 2025/11/19 20:11
* @Describe 网络后台使用提示对话框 * @Describe 网络背景使用提示对话框
* 继承 AndroidX AlertDialog绑定自定义布局 dialog_networkbackground.xml * 继承 AndroidX AlertDialog绑定自定义布局 dialog_networkbackground.xml
* 适配 API30基于 Java7 开发,支持网络图片下载、预览与回调
*/ */
public class NetworkBackgroundDialog extends AlertDialog { public class NetworkBackgroundDialog extends AlertDialog {
// ====================== 静态常量(首屏可见,统一管理) ======================
public static final String TAG = "NetworkBackgroundDialog"; public static final String TAG = "NetworkBackgroundDialog";
// 消息标识:图片加载成功 private static final int MSG_IMAGE_LOAD_SUCCESS = 1001; // 图片加载成功消息标识
private static final int MSG_IMAGE_LOAD_SUCCESS = 1001; private static final int MSG_IMAGE_LOAD_FAILED = 1002; // 图片加载失败消息标识
// 消息标识:图片加载失败
private static final int MSG_IMAGE_LOAD_FAILED = 1002;
// 控件引用 // ====================== 回调接口(紧跟常量,逻辑关联) ======================
private TextView tvTitle; /**
private TextView tvContent; * 按钮点击回调接口Java7 接口实现)
private Button btnCancel; */
private Button btnConfirm;
private Button btnPreview;
private EditText etURL;
BackgroundView mBackgroundView;
Context mContext;
// 主线程 Handler用于接收子线程消息并更新 UI
private Handler mUiHandler;
String mPreviewFilePath;
String mPreviewFileUrl;
String mDownloadSavedPath;
// 按钮点击回调接口Java7 接口实现)
public interface OnDialogClickListener { public interface OnDialogClickListener {
void onConfirm(String szConfirmFilePath); // 确认按钮点击 void onConfirm(String szConfirmFilePath); // 确认按钮点击,返回图片路径
void onCancel(); // 取消按钮点击 void onCancel(); // 取消按钮点击
} }
private OnDialogClickListener listener; // ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
// 核心数据
private OnDialogClickListener listener; // 按钮点击回调
private Context mContext; // 上下文对象
private Handler mUiHandler; // 主线程 Handler用于接收子线程消息更新 UI
private String mPreviewFilePath; // 预览图片文件路径
private String mPreviewFileUrl; // 预览图片网络 URL
private String mDownloadSavedPath; // 下载图片保存路径
// 控件引用
private TextView tvTitle; // 对话框标题
private TextView tvContent; // 对话框内容
private Button btnCancel; // 取消按钮
private Button btnConfirm; // 确认按钮
private Button btnPreview; // 预览按钮
private EditText etURL; // URL 输入框
private BackgroundView mBackgroundView; // 背景预览视图
// Java7 显式构造(必须传入 Context // ====================== 构造方法Java7 显式构造,按参数重载排序) ======================
/**
* 基础构造(仅传入 Context
* @param context 上下文
*/
public NetworkBackgroundDialog(@NonNull Context context) { public NetworkBackgroundDialog(@NonNull Context context) {
super(context); super(context);
initHandler(); // 初始化 Handler LogUtils.d(TAG, "NetworkBackgroundDialog: 基础构造初始化");
initView(); // 初始化布局和控件 initHandler();
setDismissListener(); // 设置对话框消失监听
}
// 带回调的构造(便于外部处理点击事件)
public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) {
super(context);
this.listener = listener;
initHandler(); // 初始化 Handler
initView(); initView();
setDismissListener(); // 设置对话框消失监听 setDismissListener();
} }
/** /**
* 初始化主线程 Handler用于更新 UI * 带回调的构造(便于外部处理点击事件)
* @param context 上下文
* @param listener 按钮点击回调
*/
public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) {
super(context);
this.listener = listener;
LogUtils.d(TAG, "NetworkBackgroundDialog: 带回调构造初始化");
initHandler();
initView();
setDismissListener();
}
// ====================== 生命周期相关方法对话框消失监听、Handler 初始化) ======================
/**
* 初始化主线程 Handler用于接收子线程消息并更新 UI
*/ */
private void initHandler() { private void initHandler() {
mUiHandler = new Handler() { mUiHandler = new Handler() {
@@ -86,22 +99,30 @@ public class NetworkBackgroundDialog extends AlertDialog {
super.handleMessage(msg); super.handleMessage(msg);
// 对话框已消失时,不再处理 UI 消息 // 对话框已消失时,不再处理 UI 消息
if (!isShowing()) { if (!isShowing()) {
LogUtils.d(TAG, "handleMessage: 对话框已消失,忽略消息");
return; return;
} }
switch (msg.what) { switch (msg.what) {
case MSG_IMAGE_LOAD_SUCCESS: case MSG_IMAGE_LOAD_SUCCESS:
// 图片加载成功,获取文件路径并设置背景 // 图片加载成功,获取文件路径并设置背景
mDownloadSavedPath = (String) msg.obj; mDownloadSavedPath = (String) msg.obj;
mBackgroundView.loadImage(mDownloadSavedPath); LogUtils.d(TAG, String.format("handleMessage: 图片加载成功,保存路径:%s", mDownloadSavedPath));
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
mBackgroundView.loadImage(nCurrentPixelColor, mDownloadSavedPath, true);
break; break;
case MSG_IMAGE_LOAD_FAILED: case MSG_IMAGE_LOAD_FAILED:
// 图片加载失败,设置默认背景 // 图片加载失败,设置默认背景
LogUtils.e(TAG, "handleMessage: 图片加载失败");
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
ToastUtils.show("图片预览失败,请检查链接"); ToastUtils.show("图片预览失败,请检查链接");
break; break;
default:
break;
} }
} }
}; };
LogUtils.d(TAG, "initHandler: 主线程 Handler 初始化完成");
} }
/** /**
@@ -114,20 +135,22 @@ public class NetworkBackgroundDialog extends AlertDialog {
// 对话框消失时,移除所有未处理的消息和回调 // 对话框消失时,移除所有未处理的消息和回调
if (mUiHandler != null) { if (mUiHandler != null) {
mUiHandler.removeCallbacksAndMessages(null); mUiHandler.removeCallbacksAndMessages(null);
LogUtils.d(TAG, "onDismiss: Handler 消息已清理");
} }
LogUtils.d(TAG, "对话框已消失Handler 消息已清理"); LogUtils.d(TAG, "onDismiss: 对话框已消失");
} }
}); });
LogUtils.d(TAG, "setDismissListener: 对话框消失监听已设置");
} }
// ====================== 初始化方法(布局、控件、点击事件) ======================
/** /**
* 初始化布局和控件 * 初始化布局和控件
*/ */
private void initView() { private void initView() {
mContext = this.getContext(); mContext = this.getContext();
// 加载自定义布局 // 加载自定义布局
View dialogView = LayoutInflater.from(getContext()) View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_networkbackground, null);
.inflate(R.layout.dialog_networkbackground, null);
// 设置对话框内容视图 // 设置对话框内容视图
setView(dialogView); setView(dialogView);
@@ -139,10 +162,21 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview); btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
etURL = (EditText) dialogView.findViewById(R.id.et_url); etURL = (EditText) dialogView.findViewById(R.id.et_url);
mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview); mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
// 加载初始图片
mBackgroundView.setBackgroundResource(R.drawable.blank100x100); // 控件非空校验
if (tvTitle == null || tvContent == null || btnCancel == null || btnConfirm == null || btnPreview == null
|| etURL == null || mBackgroundView == null) {
LogUtils.e(TAG, "initView: 控件绑定失败请检查布局ID是否正确");
dismiss();
return;
}
// 加载初始图片
mBackgroundView.setBackgroundResource(R.drawable.blank100x100);
// 设置按钮点击事件 // 设置按钮点击事件
setButtonClickListeners(); setButtonClickListeners();
LogUtils.d(TAG, "initView: 布局和控件初始化完成");
} }
/** /**
@@ -153,13 +187,14 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnCancel.setOnClickListener(new View.OnClickListener() { btnCancel.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击"); LogUtils.d(TAG, "onClick: 取消按钮点击");
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.setCurrentSourceToPreview(); utils.setCurrentSourceToPreview();
dismiss(); // 关闭对话框 dismiss(); // 关闭对话框
if (listener != null) { if (listener != null) {
listener.onCancel(); listener.onCancel();
LogUtils.d(TAG, "onClick: 取消回调已执行");
} }
} }
}); });
@@ -168,14 +203,16 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnConfirm.setOnClickListener(new View.OnClickListener() { btnConfirm.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击"); LogUtils.d(TAG, "onClick: 确认按钮点击");
dismiss(); // 关闭对话框 dismiss(); // 关闭对话框
if(TextUtils.isEmpty(mDownloadSavedPath)) { if (TextUtils.isEmpty(mDownloadSavedPath)) {
ToastUtils.show("未下载图片。"); ToastUtils.show("未下载图片。");
LogUtils.w(TAG, "onClick: 确认失败,未下载图片");
return; return;
} }
if (listener != null) { if (listener != null) {
listener.onConfirm(mDownloadSavedPath); listener.onConfirm(mDownloadSavedPath);
LogUtils.d(TAG, String.format("onClick: 确认回调已执行,图片路径:%s", mDownloadSavedPath));
} }
} }
}); });
@@ -184,90 +221,120 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnPreview.setOnClickListener(new View.OnClickListener() { btnPreview.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "onClick: 预览按钮点击");
downloadImageToAlbumAndPreview(); downloadImageToAlbumAndPreview();
} }
}); });
LogUtils.d(TAG, "setButtonClickListeners: 按钮点击监听已设置");
}
// ====================== 业务逻辑方法(图片下载、预览) ======================
/**
* 下载网络图片并预览
*/
void downloadImageToAlbumAndPreview() {
mPreviewFileUrl = etURL.getText().toString().trim();
if (TextUtils.isEmpty(mPreviewFileUrl)) {
ToastUtils.show("请输入图片URL");
LogUtils.w(TAG, "downloadImageToAlbumAndPreview: 图片URL为空");
return;
}
LogUtils.d(TAG, String.format("downloadImageToAlbumAndPreview: 开始下载图片URL%s", mPreviewFileUrl));
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback() {
@Override
public void onSuccess(String savePath) {
LogUtils.d(TAG, String.format("onSuccess: 图片下载成功,保存路径:%s", savePath));
// 发送消息到主线程,携带图片路径
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
mUiHandler.sendMessage(successMsg);
}
@Override
public void onFailure(String errorMsg) {
LogUtils.e(TAG, String.format("onFailure: 图片下载失败,错误信息:%s", errorMsg));
ToastUtils.show("下载失败:" + errorMsg);
// 发送图片加载失败消息
Message failMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_FAILED);
mUiHandler.sendMessage(failMsg);
}
});
} }
/** /**
* 根据文件路径设置 BackgroundView 背景(主线程调用) * 根据文件路径设置 BackgroundView 背景(主线程调用)
* @param filePath 图片文件路径 * @param previewFilePath 图片文件路径
*/ */
private void previewBackground(String previewFilePath) { private void previewBackground(String previewFilePath) {
if (TextUtils.isEmpty(previewFilePath)) {
LogUtils.w(TAG, "previewBackground: 预览文件路径为空");
return;
}
FileInputStream fis = null; FileInputStream fis = null;
try { try {
File imageFile = new File(previewFilePath); File imageFile = new File(previewFilePath);
if (!imageFile.exists()) { if (!imageFile.exists()) {
ToastUtils.show("图片文件不存在:" + previewFilePath); ToastUtils.show("图片文件不存在:" + previewFilePath);
LogUtils.e(TAG, "图片文件不存在:" + previewFilePath); LogUtils.e(TAG, String.format("previewBackground: 图片文件不存在,路径:%s", previewFilePath));
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
return; return;
} }
// 预览背景 // 预览背景
mPreviewFilePath = previewFilePath; mPreviewFilePath = previewFilePath;
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl); utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
mBackgroundView.loadBackgroundBean(utils.getPreviewBackgroundBean()); mBackgroundView.loadByBackgroundBean(utils.getPreviewBackgroundBean());
LogUtils.d(TAG, String.format("previewBackground: 图片预览成功,路径:%s", previewFilePath));
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LogUtils.e(TAG, String.format("previewBackground: 图片预览失败,错误信息:%s", e.getMessage()), e);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
LogUtils.e(TAG, "图片预览失败:" + e.getMessage());
} finally { } finally {
// Java7 手动关闭流,避免资源泄漏 // Java7 手动关闭流,避免资源泄漏
if (fis != null) { if (fis != null) {
try { try {
fis.close(); fis.close();
LogUtils.d(TAG, "previewBackground: 文件输入流已关闭");
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LogUtils.e(TAG, String.format("previewBackground: 关闭文件输入流失败,错误信息:%s", e.getMessage()), e);
} }
} }
} }
} }
// ====================== 对外提供方法(灵活适配不同场景) ======================
/** /**
* 对外提供方法:修改对话框标题(灵活适配不同场景) * 对外提供方法:修改对话框标题
* @param title 标题文本
*/ */
public void setTitle(String title) { public void setTitle(String title) {
if (tvTitle != null) { if (tvTitle != null && !TextUtils.isEmpty(title)) {
tvTitle.setText(title); tvTitle.setText(title);
LogUtils.d(TAG, String.format("setTitle: 对话框标题已修改为:%s", title));
} }
} }
/** /**
* 对外提供方法:修改对话框内容(灵活适配不同场景) * 对外提供方法:修改对话框内容
* @param content 内容文本
*/ */
public void setContent(String content) { public void setContent(String content) {
if (tvContent != null) { if (tvContent != null && !TextUtils.isEmpty(content)) {
tvContent.setText(content); tvContent.setText(content);
LogUtils.d(TAG, String.format("setContent: 对话框内容已修改为:%s", content));
} }
} }
/** /**
* 对外提供方法:设置按钮点击回调(替代带参构造) * 对外提供方法:设置按钮点击回调(替代带参构造)
* @param listener 按钮点击回调
*/ */
public void setOnDialogClickListener(OnDialogClickListener listener) { public void setOnDialogClickListener(OnDialogClickListener listener) {
this.listener = listener; this.listener = listener;
LogUtils.d(TAG, "setOnDialogClickListener: 按钮点击回调已设置");
} }
void downloadImageToAlbumAndPreview() {
//String previewFileUrl = "https://example.com/test.jpg";
mPreviewFileUrl = etURL.getText().toString();
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback(){
@Override
public void onSuccess(String savePath) {
// 发送消息到主线程,携带图片路径
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
mUiHandler.sendMessage(successMsg);
}
@Override
public void onFailure(String errorMsg) {
ToastUtils.show("下载失败:" + errorMsg);
}
});
}
} }

View File

@@ -1,59 +0,0 @@
package cc.winboll.studio.powerbell.dialogs;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/10 19:32:55
* @Describe 用户确定与否选择框
*/
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
public class YesNoAlertDialog {
public static final String TAG = "YesNoAlertDialog";
public static void show(Context context, String szTitle, String szMessage, final OnDialogResultListener listener) {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
context);
// set title
alertDialogBuilder.setTitle(szTitle);
// set dialog message
alertDialogBuilder
.setMessage(szMessage)
.setCancelable(true)
.setOnCancelListener(new DialogInterface.OnCancelListener(){
@Override
public void onCancel(DialogInterface dialog) {
listener.onNo();
}
})
.setPositiveButton("YES", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// if this button is clicked, close
// current activity
listener.onYes();
}
})
.setNegativeButton("NO", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// if this button is clicked, just close
// the dialog box and do nothing
dialog.cancel();
}
});
// create alert dialog
AlertDialog alertDialog = alertDialogBuilder.create();
// show it
alertDialog.show();
}
public interface OnDialogResultListener {
abstract void onYes();
abstract void onNo();
}
}

View File

@@ -1,84 +1,81 @@
package cc.winboll.studio.powerbell.models; package cc.winboll.studio.powerbell.models;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类
*/
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.JsonWriter; import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean; import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
// 核心修正:新增 Parcelable 接口实现API30 持久化/Intent 传递必备) /**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类
* 适配 API30支持 Serializable 持久化、Parcelable Intent 传递、JSON 序列化/反序列化
* 包含耗电提醒、充电提醒、电量检测、铃声提醒、相框尺寸等核心配置
*/
public class AppConfigBean extends BaseBean implements Serializable, Parcelable { public class AppConfigBean extends BaseBean implements Serializable, Parcelable {
// ====================== 静态常量区(首屏可见,统一管理) ======================
// 序列化版本号Serializable 必备,避免反序列化失败) // 序列化版本号Serializable 必备,避免反序列化失败)
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// 日志标签(全局统一)
transient public static final String TAG = "AppConfigBean"; transient public static final String TAG = "AppConfigBean";
// 字段校验常量(统一阈值,避免硬编码)
private static final int MIN_INTERVAL = 500; // 最小检测间隔ms
private static final int MIN_REMIND_INTERVAL = 1000;// 最小提醒间隔ms
private static final int BATTERY_MIN = 0; // 电量最小值
private static final int BATTERY_MAX = 100; // 电量最大值
private static final int INVALID_BATTERY = -1; // 无效电量标识
private static final int DEFAULT_FRAME_WIDTH = 500; // 默认相框宽度px
private static final int DEFAULT_FRAME_HEIGHT = 500;// 默认相框高度px
// 核心配置字段(保留原有字段,统一状态字段命名) // ====================== 成员变量区(按功能分类:提醒配置→电量状态→检测配置→相框配置) ======================
// 耗电提醒配置
boolean isEnableUsageReminder = false; // 耗电提醒开关 boolean isEnableUsageReminder = false; // 耗电提醒开关
int usageReminderValue = 45; // 耗电提醒阈值0-100 int usageReminderValue = 45; // 耗电提醒阈值0-100
// 充电提醒配置
boolean isEnableChargeReminder = false;// 充电提醒开关 boolean isEnableChargeReminder = false;// 充电提醒开关
int chargeReminderValue = 100; // 充电提醒阈值0-100 int chargeReminderValue = 100; // 充电提醒阈值0-100
int reminderIntervalTime = 5000; // 铃声提醒间隔ms原有 // 铃声提醒配置
boolean isCharging = false; // 是否充电(状态字段,原有 int reminderIntervalTime = 5000; // 铃声提醒间隔ms
int currentBatteryValue = -1; // 修正统一命名为「currentBatteryValue」原 currentValue // 电量状态
int batteryDetectInterval = 2000; // 新增电量检测间隔ms适配 RemindThread boolean isCharging = false; // 是否充电
// 电量检测配置
int batteryDetectInterval = 2000; // 电量检测间隔ms适配 RemindThread
// 相框配置
int defaultFrameWidth = DEFAULT_FRAME_WIDTH; // 默认相框宽度px
int defaultFrameHeight = DEFAULT_FRAME_HEIGHT;// 默认相框高度px
// 构造方法初始化默认配置(同步修正字段名,统一默认值) // ====================== 构造方法初始化默认配置,强化默认值校验) ======================
public AppConfigBean() { public AppConfigBean() {
setChargeReminderValue(100); setChargeReminderValue(100);
setEnableChargeReminder(false); setEnableChargeReminder(false);
setUsageReminderValue(10); setUsageReminderValue(10);
setEnableUsageReminder(false); setEnableUsageReminder(false);
setReminderIntervalTime(5000); setReminderIntervalTime(5000);
setBatteryDetectInterval(1000); // 新增默认检测间隔1秒 setBatteryDetectInterval(1000);
setCurrentBatteryValue(-1); // 修正:初始化当前电量字段 setDefaultFrameWidth(DEFAULT_FRAME_WIDTH);
setDefaultFrameHeight(DEFAULT_FRAME_HEIGHT);
LogUtils.d(TAG, "AppConfigBean() 构造器执行 | 默认配置初始化完成");
} }
// ====================== 核心修复:补全缺失方法(适配 RemindThread/Receiver 调用 ====================== // ====================== 核心业务方法Setter/Getter按字段功能分类补充调试日志 ======================
/** // --------------- 充电状态相关 ---------------
* 设置当前电池电量Receiver 监听电池变化时调用,与 RemindThread 字段对齐) public void setIsCharging(boolean isCharging) {
*/
public void setCurrentBatteryValue(int currentBatteryValue) {
// 强化校验:电量范围限制在 0-100异常值置为 -1标识无效
this.currentBatteryValue = (currentBatteryValue >= 0 && currentBatteryValue <= 100)
? currentBatteryValue : -1;
}
/**
* 获取当前电池电量RemindThread 同步配置时调用,与 set 方法对应)
*/
public int getCurrentBatteryValue() {
return currentBatteryValue;
}
// ====================== 原有字段 Setter/Getter修正命名强化校验 ======================
public void setReminderIntervalTime(int reminderIntervalTime) {
// 校验:提醒间隔不小于 1000ms避免频繁提醒
this.reminderIntervalTime = Math.max(reminderIntervalTime, 1000);
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
public void setIsCharging(boolean isCharging) { // 修正:方法名与字段名统一(原 setCharging
this.isCharging = isCharging; this.isCharging = isCharging;
LogUtils.d(TAG, String.format("setIsCharging() 执行 | 充电状态=%b", isCharging));
} }
public boolean isCharging() { public boolean isCharging() {
return isCharging; return isCharging;
} }
// --------------- 耗电提醒配置相关 ---------------
public void setEnableUsageReminder(boolean isEnableUsageReminder) { public void setEnableUsageReminder(boolean isEnableUsageReminder) {
this.isEnableUsageReminder = isEnableUsageReminder; this.isEnableUsageReminder = isEnableUsageReminder;
LogUtils.d(TAG, String.format("setEnableUsageReminder() 执行 | 耗电提醒开关=%b", isEnableUsageReminder));
} }
public boolean isEnableUsageReminder() { public boolean isEnableUsageReminder() {
@@ -86,16 +83,18 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
} }
public void setUsageReminderValue(int usageReminderValue) { public void setUsageReminderValue(int usageReminderValue) {
// 校验:阈值范围 0-100 this.usageReminderValue = Math.min(Math.max(usageReminderValue, BATTERY_MIN), BATTERY_MAX);
this.usageReminderValue = Math.min(Math.max(usageReminderValue, 0), 100); LogUtils.d(TAG, String.format("setUsageReminderValue() 执行 | 最终阈值=%d | 输入值=%d", this.usageReminderValue, usageReminderValue));
} }
public int getUsageReminderValue() { public int getUsageReminderValue() {
return usageReminderValue; return usageReminderValue;
} }
// --------------- 充电提醒配置相关 ---------------
public void setEnableChargeReminder(boolean isEnableChargeReminder) { public void setEnableChargeReminder(boolean isEnableChargeReminder) {
this.isEnableChargeReminder = isEnableChargeReminder; this.isEnableChargeReminder = isEnableChargeReminder;
LogUtils.d(TAG, String.format("setEnableChargeReminder() 执行 | 充电提醒开关=%b", isEnableChargeReminder));
} }
public boolean isEnableChargeReminder() { public boolean isEnableChargeReminder() {
@@ -103,25 +102,54 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
} }
public void setChargeReminderValue(int chargeReminderValue) { public void setChargeReminderValue(int chargeReminderValue) {
// 校验:阈值范围 0-100 this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, BATTERY_MIN), BATTERY_MAX);
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, 0), 100); LogUtils.d(TAG, String.format("setChargeReminderValue() 执行 | 最终阈值=%d | 输入值=%d", this.chargeReminderValue, chargeReminderValue));
} }
public int getChargeReminderValue() { public int getChargeReminderValue() {
return chargeReminderValue; return chargeReminderValue;
} }
// ====================== 电量检测间隔 Setter/Getter适配 RemindThread ====================== // --------------- 铃声提醒配置相关 ---------------
public void setReminderIntervalTime(int reminderIntervalTime) {
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
LogUtils.d(TAG, String.format("setReminderIntervalTime() 执行 | 最终间隔=%dms | 输入值=%dms", this.reminderIntervalTime, reminderIntervalTime));
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
// --------------- 电量检测配置相关 ---------------
public void setBatteryDetectInterval(int batteryDetectInterval) {
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 执行 | 最终间隔=%dms | 输入值=%dms", this.batteryDetectInterval, batteryDetectInterval));
}
public int getBatteryDetectInterval() { public int getBatteryDetectInterval() {
return batteryDetectInterval; return batteryDetectInterval;
} }
// 强化校验检测间隔不小于500ms避免 CPU 高占用,与 RemindThread 最小休眠一致) // --------------- 相框配置相关 ---------------
public void setBatteryDetectInterval(int batteryDetectInterval) { public void setDefaultFrameWidth(int defaultFrameWidth) {
this.batteryDetectInterval = Math.max(batteryDetectInterval, 500); this.defaultFrameWidth = defaultFrameWidth;
LogUtils.d(TAG, String.format("setDefaultFrameWidth() 执行 | 最终宽度=%dpx | 输入值=%dpx", this.defaultFrameWidth, defaultFrameWidth));
} }
// ====================== JSON 序列化/反序列化(兼容旧配置,同步修正字段) ====================== public int getDefaultFrameWidth() {
return defaultFrameWidth;
}
public void setDefaultFrameHeight(int defaultFrameHeight) {
this.defaultFrameHeight = defaultFrameHeight;
LogUtils.d(TAG, String.format("setDefaultFrameHeight() 执行 | 最终高度=%dpx | 输入值=%dpx", this.defaultFrameHeight, defaultFrameHeight));
}
public int getDefaultFrameHeight() {
return defaultFrameHeight;
}
// ====================== 父类重写方法JSON 序列化/反序列化,兼容旧配置) ======================
@Override @Override
public String getName() { public String getName() {
return AppConfigBean.class.getName(); return AppConfigBean.class.getName();
@@ -130,91 +158,110 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
@Override @Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter); super.writeThisToJsonWriter(jsonWriter);
AppConfigBean bean = this; LogUtils.d(TAG, "writeThisToJsonWriter() 执行 | 开始JSON序列化");
// 原有字段序列化(保留拼写兼容,同步修正字段名)
jsonWriter.name("isEnableUsageReminder").value(bean.isEnableUsageReminder()); // 原有字段序列化
jsonWriter.name("usageReminderValue").value(bean.getUsageReminderValue()); jsonWriter.name("isEnableUsageReminder").value(isEnableUsageReminder());
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder()); jsonWriter.name("usageReminderValue").value(getUsageReminderValue());
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue()); jsonWriter.name("isEnableChargeReminder").value(isEnableChargeReminder());
jsonWriter.name("reminderIntervalTime").value(bean.getReminderIntervalTime()); jsonWriter.name("chargeReminderValue").value(getChargeReminderValue());
jsonWriter.name("isCharging").value(bean.isCharging()); jsonWriter.name("reminderIntervalTime").value(getReminderIntervalTime());
// 修正:序列化新字段名 currentBatteryValue兼容旧字段 currentValue jsonWriter.name("isCharging").value(isCharging());
jsonWriter.name("currentBatteryValue").value(bean.getCurrentBatteryValue()); // 新增字段序列化(检测配置)
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue()); // 兼容旧配置,避免数据丢失 jsonWriter.name("batteryDetectInterval").value(getBatteryDetectInterval());
// 新增字段序列化:电量检测间隔 // 新增字段序列化(相框配置)
jsonWriter.name("batteryDetectInterval").value(bean.getBatteryDetectInterval()); jsonWriter.name("defaultFrameWidth").value(getDefaultFrameWidth());
jsonWriter.name("defaultFrameHeight").value(getDefaultFrameHeight());
LogUtils.d(TAG, "writeThisToJsonWriter() 完成 | JSON序列化成功");
} }
@Override @Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader() 执行 | 开始JSON反序列化");
AppConfigBean bean = new AppConfigBean(); AppConfigBean bean = new AppConfigBean();
jsonReader.beginObject(); jsonReader.beginObject();
while (jsonReader.hasNext()) { while (jsonReader.hasNext()) {
String name = jsonReader.nextName(); String name = jsonReader.nextName();
// 原有字段反序列化(兼容旧 Key 拼写,同步修正字段 // 兼容拼写错误字段isEnableUsegeReminder → isEnableUsageReminder
if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) { if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) {
bean.setEnableUsageReminder(jsonReader.nextBoolean()); bean.setEnableUsageReminder(jsonReader.nextBoolean());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isEnableUsageReminder()));
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) { } else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
bean.setUsageReminderValue(jsonReader.nextInt()); bean.setUsageReminderValue(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getUsageReminderValue()));
} else if (name.equals("isEnableChargeReminder")) { } else if (name.equals("isEnableChargeReminder")) {
bean.setEnableChargeReminder(jsonReader.nextBoolean()); bean.setEnableChargeReminder(jsonReader.nextBoolean());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isEnableChargeReminder()));
} else if (name.equals("chargeReminderValue")) { } else if (name.equals("chargeReminderValue")) {
bean.setChargeReminderValue(jsonReader.nextInt()); bean.setChargeReminderValue(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getChargeReminderValue()));
} else if (name.equals("reminderIntervalTime")) { } else if (name.equals("reminderIntervalTime")) {
bean.setReminderIntervalTime(jsonReader.nextInt()); bean.setReminderIntervalTime(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getReminderIntervalTime()));
} else if (name.equals("isCharging")) { } else if (name.equals("isCharging")) {
bean.setIsCharging(jsonReader.nextBoolean()); // 修正:调用新方法名 bean.setIsCharging(jsonReader.nextBoolean());
} LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isCharging()));
// 核心兼容:优先读取旧字段 currentValue再读取新字段 currentBatteryValue新字段覆盖旧字段 } else if (name.equals("batteryDetectInterval")) {
else if (name.equals("currentValue")) {
bean.setCurrentBatteryValue(jsonReader.nextInt());
} else if (name.equals("currentBatteryValue")) {
bean.setCurrentBatteryValue(jsonReader.nextInt());
}
// 新增字段反序列化兼容无此字段的旧配置用默认值1000ms
else if (name.equals("batteryDetectInterval")) {
bean.setBatteryDetectInterval(jsonReader.nextInt()); bean.setBatteryDetectInterval(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getBatteryDetectInterval()));
} else if (name.equals("defaultFrameWidth")) {
bean.setDefaultFrameWidth(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getDefaultFrameWidth()));
} else if (name.equals("defaultFrameHeight")) {
bean.setDefaultFrameHeight(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getDefaultFrameHeight()));
} else { } else {
jsonReader.skipValue(); jsonReader.skipValue();
LogUtils.w(TAG, String.format("readBeanFromJsonReader() 跳过未知字段 | %s", name));
} }
} }
jsonReader.endObject(); jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader() 完成 | JSON反序列化成功");
return bean; return bean;
} }
// ====================== Parcelable 接口实现(同步修正字段,确保 Intent 传递正常 ====================== // ====================== Parcelable 接口实现(API30 Intent 传递必备 ======================
@Override @Override
public int describeContents() { public int describeContents() {
return 0; // 无特殊内容描述固定返回0 return 0; // 无特殊内容描述固定返回0
} }
// 序列化:将所有字段写入 Parcel同步修正字段名Java7 适配)
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0)); // boolean → byte LogUtils.d(TAG, "writeToParcel() 执行 | 开始Parcel序列化");
// 按成员变量顺序写入boolean 转 byte 存储
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0));
dest.writeInt(usageReminderValue); dest.writeInt(usageReminderValue);
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0)); // boolean → byte dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0));
dest.writeInt(chargeReminderValue); dest.writeInt(chargeReminderValue);
dest.writeInt(reminderIntervalTime); dest.writeInt(reminderIntervalTime);
dest.writeByte((byte) (isCharging ? 1 : 0)); // boolean → byte dest.writeByte((byte) (isCharging ? 1 : 0));
dest.writeInt(currentBatteryValue); // 修正:序列化新字段名
dest.writeInt(batteryDetectInterval); dest.writeInt(batteryDetectInterval);
dest.writeInt(defaultFrameWidth);
dest.writeInt(defaultFrameHeight);
LogUtils.d(TAG, "writeToParcel() 完成 | Parcel序列化成功");
} }
// 反序列化:从 Parcel 读取字段,创建对象(必须 public static final 修饰) // 反序列化 Creator(必须 public static final 修饰Java7 适配
public static final Parcelable.Creator<AppConfigBean> CREATOR = new Parcelable.Creator<AppConfigBean>() { public static final Parcelable.Creator<AppConfigBean> CREATOR = new Parcelable.Creator<AppConfigBean>() {
@Override @Override
public AppConfigBean createFromParcel(Parcel source) { public AppConfigBean createFromParcel(Parcel source) {
LogUtils.d(TAG, "createFromParcel() 执行 | 开始Parcel反序列化");
AppConfigBean bean = new AppConfigBean(); AppConfigBean bean = new AppConfigBean();
// 按 writeToParcel 顺序读取,同步修正字段 // 按 writeToParcel 顺序读取
bean.isEnableUsageReminder = source.readByte() != 0; bean.isEnableUsageReminder = source.readByte() != 0;
bean.usageReminderValue = source.readInt(); bean.usageReminderValue = source.readInt();
bean.isEnableChargeReminder = source.readByte() != 0; bean.isEnableChargeReminder = source.readByte() != 0;
bean.chargeReminderValue = source.readInt(); bean.chargeReminderValue = source.readInt();
bean.reminderIntervalTime = source.readInt(); bean.reminderIntervalTime = source.readInt();
bean.isCharging = source.readByte() != 0; bean.isCharging = source.readByte() != 0;
bean.currentBatteryValue = source.readInt(); // 修正:读取新字段名
bean.batteryDetectInterval = source.readInt(); bean.batteryDetectInterval = source.readInt();
bean.defaultFrameWidth = source.readInt();
bean.defaultFrameHeight = source.readInt();
LogUtils.d(TAG, "createFromParcel() 完成 | Parcel反序列化成功");
return bean; return bean;
} }

View File

@@ -3,51 +3,61 @@ package cc.winboll.studio.powerbell.models;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.JsonWriter; import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean; import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 11:52:28 * @Date 2024/07/18 11:52:28
* @Describe 应用背景图片数据类(存储正式/预览背景配置支持JSON序列化/反序列化) * @Describe 应用背景图片数据类
* 适配 API30支持 Serializable 持久化、JSON 序列化/反序列化
* 存储正式/预览背景配置,包含原图、压缩图、裁剪比例、像素颜色等核心字段
*/ */
public class BackgroundBean extends BaseBean implements Serializable { public class BackgroundBean extends BaseBean implements Serializable {
// ====================== 静态常量(首屏可见,统一管理) ======================
// 日志标签(全局统一,替换 Log 为 LogUtils
public static final String TAG = "BackgroundBean";
// 兼容旧字段常量(统一管理,避免硬编码)
private static final String OLD_FIELD_USE_SCALED_COMPRESS = "isUseScaledCompress";
// 字段默认值常量(统一管理,避免魔法值)
private static final int DEFAULT_DIMENSION = 100; // 默认宽高
private static final int MIN_DIMENSION = 1; // 最小宽高
public static final String TAG = "BackgroundPictureBean"; // ====================== 成员变量(按功能分类:原图配置→压缩图配置→控制字段→裁剪配置→像素颜色) ======================
// 原图配置
// 核心字段背景图片文件名对应应用私有目录下的图片文件与BackgroundSettingsActivity的_mSourceCroppedFile匹配 private String backgroundFileName = ""; // 背景图片文件名
private String backgroundFileName = ""; private String backgroundFilePath = ""; // 背景图片完整路径
// 核心字段背景图片完整路径解决仅存文件名导致的路径拼接错误与backgroundScaledCompressFilePath对应 private String backgroundFileInfo = ""; // 图片信息Uri、网络地址等
private String backgroundFilePath = ""; // 压缩图配置
// 附加字段图片信息如Uri、网络地址等仅作备注不参与路径生成 private String backgroundScaledCompressFileName = ""; // 压缩后背景图片文件名
private String backgroundFileInfo = ""; private String backgroundScaledCompressFilePath = ""; // 压缩后背景图片完整路径
// 控制字段是否启用背景图片true-显示背景图false-显示透明背景) // 控制字段
private boolean isUseBackgroundFile = false; private boolean isUseBackgroundFile = false; // 是否启用背景图片
// 核心字段:压缩背景图片文件名对应应用私有目录下的压缩图片与saveCropBitmap的压缩图匹配 private boolean isUseBackgroundScaledCompressFile = false; // 是否启用压缩背景图重命名原isUseScaledCompress
private String backgroundScaledCompressFileName = ""; // 裁剪配置
// 核心字段压缩后背景图片完整路径解决仅存文件名导致的路径拼接错误适配BackgroundSettingsActivity的私有目录 private int backgroundWidth = DEFAULT_DIMENSION; // 背景图宽度
private String backgroundScaledCompressFilePath = ""; private int backgroundHeight = DEFAULT_DIMENSION; // 背景图高度
// 重命名字段是否启用压缩背景图原isUseScaledCompress → 新isUseBackgroundScaledCompressFile语义更清晰 // 像素颜色
private boolean isUseBackgroundScaledCompressFile = false; private int pixelColor = 0xFFFFFFFF; // 拾取的像素颜色(纯色背景用)
// 裁剪比例字段背景图宽高比默认1:1用于固定比例裁剪
private int backgroundWidth = 100;
private int backgroundHeight = 100;
// 像素拾取字段:拾取的像素颜色(用于纯色背景)
private int pixelColor = 0;
// ====================== 构造方法无参构造JSON反序列化必备 ======================
/** /**
* 无参构造器必须JSON反序列化时需默认构造器 * 无参构造器必须JSON反序列化时需默认构造器
*/ */
public BackgroundBean() { public BackgroundBean() {
LogUtils.d(TAG, "BackgroundBean: 无参构造初始化完成");
} }
// ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)====================================== // ====================== Getter/Setter 方法(按功能分类,补充调试日志,强化校验) ======================
// --------------- 原图配置相关 ---------------
public String getBackgroundFileName() { public String getBackgroundFileName() {
return backgroundFileName; return backgroundFileName;
} }
public void setBackgroundFileName(String backgroundFileName) { public void setBackgroundFileName(String backgroundFileName) {
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName; // 防null避免空指针 this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName;
LogUtils.d(TAG, String.format("setBackgroundFileName: 背景文件名设置为 %s", this.backgroundFileName));
} }
public String getBackgroundFilePath() { public String getBackgroundFilePath() {
@@ -55,7 +65,8 @@ public class BackgroundBean extends BaseBean implements Serializable {
} }
public void setBackgroundFilePath(String backgroundFilePath) { public void setBackgroundFilePath(String backgroundFilePath) {
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath; // 防null避免路径拼接错误 this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath;
LogUtils.d(TAG, String.format("setBackgroundFilePath: 背景文件路径设置为 %s", this.backgroundFilePath));
} }
public String getBackgroundFileInfo() { public String getBackgroundFileInfo() {
@@ -63,23 +74,28 @@ public class BackgroundBean extends BaseBean implements Serializable {
} }
public void setBackgroundFileInfo(String backgroundFileInfo) { public void setBackgroundFileInfo(String backgroundFileInfo) {
this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo; // 防null避免空指针 this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo;
LogUtils.d(TAG, String.format("setBackgroundFileInfo: 背景文件信息设置为 %s", this.backgroundFileInfo));
} }
// --------------- 控制字段相关 ---------------
public boolean isUseBackgroundFile() { public boolean isUseBackgroundFile() {
return isUseBackgroundFile; return isUseBackgroundFile;
} }
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) { public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
this.isUseBackgroundFile = isUseBackgroundFile; this.isUseBackgroundFile = isUseBackgroundFile;
LogUtils.d(TAG, String.format("setIsUseBackgroundFile: 是否启用背景图设置为 %b", isUseBackgroundFile));
} }
// --------------- 压缩图配置相关 ---------------
public String getBackgroundScaledCompressFileName() { public String getBackgroundScaledCompressFileName() {
return backgroundScaledCompressFileName; return backgroundScaledCompressFileName;
} }
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) { public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName;
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFileName: 压缩背景文件名设置为 %s", this.backgroundScaledCompressFileName));
} }
public String getBackgroundScaledCompressFilePath() { public String getBackgroundScaledCompressFilePath() {
@@ -87,7 +103,8 @@ public class BackgroundBean extends BaseBean implements Serializable {
} }
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) { public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null避免路径错误 this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath;
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFilePath: 压缩背景文件路径设置为 %s", this.backgroundScaledCompressFilePath));
} }
/** /**
@@ -100,14 +117,17 @@ public class BackgroundBean extends BaseBean implements Serializable {
public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) { public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) {
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile; this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
LogUtils.d(TAG, String.format("setIsUseBackgroundScaledCompressFile: 是否启用压缩背景图设置为 %b", isUseBackgroundScaledCompressFile));
} }
// --------------- 裁剪配置相关 ---------------
public int getBackgroundWidth() { public int getBackgroundWidth() {
return backgroundWidth; return backgroundWidth;
} }
public void setBackgroundWidth(int backgroundWidth) { public void setBackgroundWidth(int backgroundWidth) {
this.backgroundWidth = backgroundWidth <= 0 ? 100 : backgroundWidth; // 防无效值,确保宽高比有效 this.backgroundWidth = backgroundWidth < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundWidth;
LogUtils.d(TAG, String.format("setBackgroundWidth: 背景宽度设置为 %d输入值%d", this.backgroundWidth, backgroundWidth));
} }
public int getBackgroundHeight() { public int getBackgroundHeight() {
@@ -115,44 +135,54 @@ public class BackgroundBean extends BaseBean implements Serializable {
} }
public void setBackgroundHeight(int backgroundHeight) { public void setBackgroundHeight(int backgroundHeight) {
this.backgroundHeight = backgroundHeight <= 0 ? 100 : backgroundHeight; // 防无效值,确保宽高比有效 this.backgroundHeight = backgroundHeight < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundHeight;
LogUtils.d(TAG, String.format("setBackgroundHeight: 背景高度设置为 %d输入值%d", this.backgroundHeight, backgroundHeight));
} }
// --------------- 像素颜色相关 ---------------
public int getPixelColor() { public int getPixelColor() {
return pixelColor; return pixelColor;
} }
public void setPixelColor(int pixelColor) { public void setPixelColor(int pixelColor) {
this.pixelColor = pixelColor; this.pixelColor = pixelColor;
LogUtils.d(TAG, String.format("setPixelColor: 像素颜色设置为 0x%08X", pixelColor));
} }
// ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本====================================== // ====================== 序列化/反序列化方法(适配重命名字段,兼容旧版本,补充调试日志) ======================
@Override @Override
public String getName() { public String getName() {
return BackgroundBean.class.getName(); // 必须重写BaseBean序列化时需类名标识 String className = BackgroundBean.class.getName();
LogUtils.d(TAG, String.format("getName: 类名标识为 %s", className));
return className;
} }
/** /**
* 序列化同步重命名字段原isUseScaledCompress → 新isUseBackgroundScaledCompressFile * 序列化同步重命名字段原isUseScaledCompress → 新isUseBackgroundScaledCompressFile
* 确保新字段能正常持久化同时兼容旧版本JSON可选:保留旧字段写入,避免旧版本读取异常) * 确保新字段能正常持久化同时兼容旧版本JSON保留旧字段写入避免旧版本读取异常
*/ */
@Override @Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter); super.writeThisToJsonWriter(jsonWriter);
BackgroundBean bean = this; BackgroundBean bean = this;
// 原图配置序列化
jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName()); jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName());
jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath()); // 新增字段:背景原图完整路径 jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath());
jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo()); jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo());
// 控制字段序列化
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile()); jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
// 压缩图配置序列化
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName()); jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath()); jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath());
// 关键:新字段序列化(核心) // 关键:新字段序列化(核心)
jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile()); jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile());
// 兼容旧版本:保留旧字段名写入(可选,避免旧版本Bean读取时缺失字段 // 兼容旧版本保留旧字段名写入避免旧版本Bean读取时缺失字段
jsonWriter.name("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile()); jsonWriter.name(OLD_FIELD_USE_SCALED_COMPRESS).value(bean.isUseBackgroundScaledCompressFile());
// 裁剪配置与像素颜色序列化
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth()); jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight()); jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
jsonWriter.name("pixelColor").value(bean.getPixelColor()); jsonWriter.name("pixelColor").value(bean.getPixelColor());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成,已兼容旧字段");
} }
/** /**
@@ -172,7 +202,7 @@ public class BackgroundBean extends BaseBean implements Serializable {
bean.setBackgroundFileName(jsonReader.nextString()); bean.setBackgroundFileName(jsonReader.nextString());
break; break;
case "backgroundFilePath": case "backgroundFilePath":
bean.setBackgroundFilePath(jsonReader.nextString()); // 新增字段:读取背景原图完整路径 bean.setBackgroundFilePath(jsonReader.nextString());
break; break;
case "backgroundFileInfo": case "backgroundFileInfo":
bean.setBackgroundFileInfo(jsonReader.nextString()); bean.setBackgroundFileInfo(jsonReader.nextString());
@@ -186,13 +216,15 @@ public class BackgroundBean extends BaseBean implements Serializable {
case "backgroundScaledCompressFilePath": case "backgroundScaledCompressFilePath":
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString()); bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
break; break;
// 关键:读取新字段(优先)
case "isUseBackgroundScaledCompressFile": case "isUseBackgroundScaledCompressFile":
// 关键:读取新字段(优先)
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean()); bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 isUseBackgroundScaledCompressFile 完成");
break; break;
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值) case OLD_FIELD_USE_SCALED_COMPRESS:
case "isUseScaledCompress": // 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
tempUseScaledCompress = jsonReader.nextBoolean(); tempUseScaledCompress = jsonReader.nextBoolean();
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 isUseScaledCompress 完成");
break; break;
case "backgroundWidth": case "backgroundWidth":
bean.setBackgroundWidth(jsonReader.nextInt()); bean.setBackgroundWidth(jsonReader.nextInt());
@@ -204,32 +236,36 @@ public class BackgroundBean extends BaseBean implements Serializable {
bean.setPixelColor(jsonReader.nextInt()); bean.setPixelColor(jsonReader.nextInt());
break; break;
default: default:
jsonReader.skipValue(); // 跳过未知字段兼容旧版本Bean避免崩溃 jsonReader.skipValue();
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
break; break;
} }
} }
jsonReader.endObject(); jsonReader.endObject();
// 兼容逻辑若新字段未被赋值旧版本JSON无此字段则用旧字段值填充 // 兼容逻辑若新字段未被赋值旧版本JSON无此字段则用旧字段值填充
if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) { if (!bean.isUseBackgroundScaledCompressFile()) {
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress); bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
LogUtils.d(TAG, "readBeanFromJsonReader: 旧字段值已填充到新字段");
} }
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
return bean; return bean;
} }
// ====================================== 辅助方法(同步更新重命名字段)====================================== // ====================== 辅助方法(重置配置、配置校验,补充调试日志) ======================
/** /**
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段) * 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
*/ */
public void resetBackgroundConfig() { public void resetBackgroundConfig() {
this.backgroundFileName = ""; this.backgroundFileName = "";
this.backgroundFilePath = ""; // 新增:重置背景原图完整路径 this.backgroundFilePath = "";
this.backgroundScaledCompressFileName = ""; this.backgroundScaledCompressFileName = "";
this.backgroundScaledCompressFilePath = ""; this.backgroundScaledCompressFilePath = "";
this.backgroundFileInfo = ""; this.backgroundFileInfo = "";
this.isUseBackgroundFile = false; this.isUseBackgroundFile = false;
this.isUseBackgroundScaledCompressFile = false; // 重命名字段重置为false this.isUseBackgroundScaledCompressFile = false;
this.backgroundWidth = 100; this.backgroundWidth = DEFAULT_DIMENSION;
this.backgroundHeight = 100; this.backgroundHeight = DEFAULT_DIMENSION;
LogUtils.d(TAG, "resetBackgroundConfig: 背景配置已重置为默认值");
} }
/** /**
@@ -240,16 +276,21 @@ public class BackgroundBean extends BaseBean implements Serializable {
public boolean isBackgroundConfigValid() { public boolean isBackgroundConfigValid() {
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空 // 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
if (!isUseBackgroundFile) { if (!isUseBackgroundFile) {
LogUtils.d(TAG, "isBackgroundConfigValid: 未启用背景图,配置无效");
return false; return false;
} }
// 原图校验:路径非空 或 文件名非空 // 原图校验:路径非空 或 文件名非空
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty(); boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
// 压缩图校验:启用压缩图时,路径/文件名需非空 // 压缩图校验:启用压缩图时,路径/文件名需非空
boolean isCompressValid = true; boolean isCompressValid = true;
if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图 if (isUseBackgroundScaledCompressFile()) {
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty(); isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
} }
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效 // 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
return isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid; boolean isValid = isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
LogUtils.d(TAG, String.format("isBackgroundConfigValid: 背景配置有效性为 %b启用压缩图%b原图有效%b压缩图有效%b",
isValid, isUseBackgroundScaledCompressFile(), isOriginalValid, isCompressValid));
return isValid;
} }
} }

View File

@@ -1,26 +1,82 @@
package cc.winboll.studio.powerbell.models; package cc.winboll.studio.powerbell.models;
import cc.winboll.studio.libappbase.LogUtils;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 14:30:51 * @Date 2025/03/22 14:30:51
* @Describe 电池报告数据模型 * @Describe 电池报告数据模型
* 适配 API30存储当前电量、放电时间、充电时间核心数据
* 支持参数校验与调试日志输出
*/ */
public class BatteryData { public class BatteryData {
// ====================== 静态常量(首屏可见,统一管理) ======================
public static final String TAG = "BatteryData"; public static final String TAG = "BatteryData";
// 字段校验常量(避免硬编码,统一管理)
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
private static final String EMPTY_TIME = "00:00:00";
private int currentLevel; // ====================== 成员变量(按功能分类:电量→时间) ======================
private String dischargeTime; private int currentLevel; // 当前电池电量0-100
private String chargeTime; private String dischargeTime; // 放电时间
private String chargeTime; // 充电时间
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) { // ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
this.currentLevel = currentLevel; /**
this.dischargeTime = dischargeTime; * 无参构造器(适配 JSON 反序列化、反射实例化场景)
this.chargeTime = chargeTime; */
public BatteryData() {
this.currentLevel = BATTERY_MIN;
this.dischargeTime = EMPTY_TIME;
this.chargeTime = EMPTY_TIME;
LogUtils.d(TAG, "BatteryData: 无参构造初始化完成,默认值已设置");
} }
public int getCurrentLevel() { return currentLevel; } /**
public String getDischargeTime() { return dischargeTime; } * 带参构造器(核心构造,初始化所有字段)
public String getChargeTime() { return chargeTime; } * @param currentLevel 当前电量0-100
* @param dischargeTime 放电时间
* @param chargeTime 充电时间
*/
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) {
// 电量范围校验0-100异常值置为0
this.currentLevel = currentLevel >= BATTERY_MIN && currentLevel <= BATTERY_MAX
? currentLevel : BATTERY_MIN;
// 时间字段防 null空值置为默认空时间
this.dischargeTime = dischargeTime == null ? EMPTY_TIME : dischargeTime;
this.chargeTime = chargeTime == null ? EMPTY_TIME : chargeTime;
// 调试日志:输出入参与最终赋值结果
LogUtils.d(TAG, String.format("BatteryData: 带参构造初始化完成 | 当前电量:%d输入%d| 放电时间:%s输入%s| 充电时间:%s输入%s",
this.currentLevel, currentLevel,
this.dischargeTime, dischargeTime,
this.chargeTime, chargeTime));
}
// ====================== Getter 方法(按成员变量顺序排列,补充日志可选) ======================
/**
* 获取当前电池电量
* @return 当前电量0-100
*/
public int getCurrentLevel() {
return currentLevel;
}
/**
* 获取放电时间
* @return 放电时间
*/
public String getDischargeTime() {
return dischargeTime;
}
/**
* 获取充电时间
* @return 充电时间
*/
public String getChargeTime() {
return chargeTime;
}
} }

View File

@@ -3,47 +3,88 @@ package cc.winboll.studio.powerbell.models;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.JsonWriter; import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean; import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Describe 电池信息数据模型
* 适配 API30存储电量时间戳与电量值支持 JSON 序列化/反序列化
* 修复字段拼写错误,补充数据校验与调试日志
*/
public class BatteryInfoBean extends BaseBean implements Serializable { public class BatteryInfoBean extends BaseBean implements Serializable {
// ====================== 静态常量(首屏可见,统一管理) ======================
public static final String TAG = "BatteryInfoBean"; public static final String TAG = "BatteryInfoBean";
// 字段校验常量(避免硬编码,统一管理)
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
private static final long DEFAULT_TIMESTAMP = 0L;
private static final int DEFAULT_BATTERY_VALUE = 0;
// 记录电量的时间戳 // ====================== 成员变量修复拼写错误battetyValue → batteryValue ======================
long timeStamp; private long timeStamp; // 记录电量的时间戳
// 电量值 private int batteryValue; // 电量值0-100
int battetyValue;
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
/**
* 无参构造器JSON 反序列化、反射实例化必备)
*/
public BatteryInfoBean() { public BatteryInfoBean() {
this.timeStamp = 0; this.timeStamp = DEFAULT_TIMESTAMP;
this.battetyValue = 0; this.batteryValue = DEFAULT_BATTERY_VALUE;
LogUtils.d(TAG, "BatteryInfoBean: 无参构造初始化完成,默认时间戳:" + timeStamp + ",默认电量:" + batteryValue);
} }
public BatteryInfoBean(long timeStamp, int battetyValue) { /**
* 带参构造器(核心构造,初始化所有字段)
* @param timeStamp 电量记录时间戳
* @param batteryValue 电量值0-100
*/
public BatteryInfoBean(long timeStamp, int batteryValue) {
this.timeStamp = timeStamp; this.timeStamp = timeStamp;
this.battetyValue = battetyValue; // 电量范围校验0-100异常值置为默认值
this.batteryValue = batteryValue >= BATTERY_MIN && batteryValue <= BATTERY_MAX
? batteryValue : DEFAULT_BATTERY_VALUE;
LogUtils.d(TAG, String.format("BatteryInfoBean: 带参构造初始化完成 | 时间戳:%d | 电量:%d输入%d",
this.timeStamp, this.batteryValue, batteryValue));
} }
// ====================== Setter/Getter 方法(按成员变量顺序排列,修复拼写错误,补充日志) ======================
/**
* 设置电量记录时间戳
* @param timeStamp 时间戳
*/
public void setTimeStamp(long timeStamp) { public void setTimeStamp(long timeStamp) {
this.timeStamp = timeStamp; this.timeStamp = timeStamp;
LogUtils.d(TAG, "setTimeStamp: 时间戳设置为 " + timeStamp);
} }
public long getTimeStamp() { public long getTimeStamp() {
return timeStamp; return timeStamp;
} }
public void setBattetyValue(int battetyValue) { /**
this.battetyValue = battetyValue; * 设置电量值(修复拼写错误:battetyValuebatteryValue
* @param batteryValue 电量值0-100
*/
public void setBatteryValue(int batteryValue) {
this.batteryValue = batteryValue >= BATTERY_MIN && batteryValue <= BATTERY_MAX
? batteryValue : DEFAULT_BATTERY_VALUE;
LogUtils.d(TAG, String.format("setBatteryValue: 电量设置为 %d输入%d",
this.batteryValue, batteryValue));
} }
public int getBattetyValue() { public int getBatteryValue() {
return battetyValue; return batteryValue;
} }
// ====================== JSON 序列化/反序列化方法(修复字段拼写错误,补充调试日志) ======================
@Override @Override
public String getName() { public String getName() {
return BatteryInfoBean.class.getName(); String className = BatteryInfoBean.class.getName();
LogUtils.d(TAG, "getName: 类名标识为 " + className);
return className;
} }
@Override @Override
@@ -51,7 +92,9 @@ public class BatteryInfoBean extends BaseBean implements Serializable {
super.writeThisToJsonWriter(jsonWriter); super.writeThisToJsonWriter(jsonWriter);
BatteryInfoBean bean = this; BatteryInfoBean bean = this;
jsonWriter.name("timeStamp").value(bean.getTimeStamp()); jsonWriter.name("timeStamp").value(bean.getTimeStamp());
jsonWriter.name("battetyValue").value(bean.getBattetyValue()); // 修复 JSON 字段名拼写错误battetyValue → batteryValue
jsonWriter.name("batteryValue").value(bean.getBatteryValue());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
} }
@Override @Override
@@ -60,16 +103,28 @@ public class BatteryInfoBean extends BaseBean implements Serializable {
jsonReader.beginObject(); jsonReader.beginObject();
while (jsonReader.hasNext()) { while (jsonReader.hasNext()) {
String name = jsonReader.nextName(); String name = jsonReader.nextName();
if (name.equals("timeStamp")) { switch (name) {
bean.setTimeStamp(jsonReader.nextLong()); case "timeStamp":
} else if (name.equals("battetyValue")) { bean.setTimeStamp(jsonReader.nextLong());
bean.setBattetyValue(jsonReader.nextInt()); break;
} else { case "batteryValue":
jsonReader.skipValue(); bean.setBatteryValue(jsonReader.nextInt());
break;
// 兼容旧字段名battetyValue避免旧配置解析失败
case "battetyValue":
int oldBatteryValue = jsonReader.nextInt();
bean.setBatteryValue(oldBatteryValue);
LogUtils.w(TAG, "readBeanFromJsonReader: 读取旧字段 battetyValue已兼容为 batteryValue" + oldBatteryValue);
break;
default:
jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知字段 " + name);
break;
} }
} }
// 结束 JSON 对象
jsonReader.endObject(); jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
return bean; return bean;
} }
} }

View File

@@ -0,0 +1,12 @@
package cc.winboll.studio.powerbell.models;
/**
* 电池绘制样式枚举 (单选选项)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public enum BatteryStyle {
ENERGY_STYLE, // 能量样式
ZEBRA_STYLE, // 条纹样式
POINT_STYLE // 点阵样式
}

View File

@@ -4,48 +4,44 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.JsonWriter; import android.util.JsonWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import cc.winboll.studio.libappbase.BaseBean; import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 15:55 * @Date 2025/12/17 15:55
* @Describe 服务控制参数模型管理服务启用状态支持序列化、Parcel传递、JSON解析 * @Describe 服务控制参数模型
* 适配 API30管理服务启用状态支持 Serializable 持久化、Parcelable 组件传递、JSON 序列化解析
*/ */
public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable { public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable {
// ================================== 静态常量(置顶统一管理,避免魔法值)================================= // ====================== 静态常量(置顶统一管理,避免魔法值) ======================
private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容 //private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
private static final String TAG = "ControlCenterServiceBean"; private static final String TAG = "ControlCenterServiceBean";
// JSON 字段常量,避免硬编码,减少拼写错误 private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService"; // JSON 字段常量,避免硬编码
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService";
// ================================== 核心成员变量(私有封装,规范命名)================================= // ====================== 核心成员变量(私有封装,规范命名) ======================
private boolean isEnableService = false; // 服务启用状态true=启用false=禁用 private boolean isEnableService = false; // 服务启用状态true=启用false=禁用
// ================================== Parcelable 静态创建器(必须 public static final适配 API30 传递)================================= // ====================== Parcelable 静态创建器(必须 public static final适配 API30 组件传递) ======================
public static final Parcelable.Creator<ControlCenterServiceBean> CREATOR = new Parcelable.Creator<ControlCenterServiceBean>() { public static final Parcelable.Creator<ControlCenterServiceBean> CREATOR = new Parcelable.Creator<ControlCenterServiceBean>() {
@Override @Override
public ControlCenterServiceBean createFromParcel(Parcel source) { public ControlCenterServiceBean createFromParcel(Parcel source) {
LogUtils.d(TAG, "Parcelable createFromParcel: 从Parcel反序列化对象");
// Java7 + API30 适配Parcel 无直接 writeBoolean用 byte 存储/读取
boolean isEnable = source.readByte() != 0; boolean isEnable = source.readByte() != 0;
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnable); ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnable);
LogUtils.d(TAG, "Parcelable createFromParcel: 反序列化完成isEnableService=" + isEnable); LogUtils.d(TAG, String.format("createFromParcel: 反序列化完成isEnableService=%b", isEnable));
return bean; return bean;
} }
@Override @Override
public ControlCenterServiceBean[] newArray(int size) { public ControlCenterServiceBean[] newArray(int size) {
LogUtils.d(TAG, "Parcelable newArray: 创建数组,长度=" + size); LogUtils.d(TAG, String.format("newArray: 创建数组,长度=%d", size));
return new ControlCenterServiceBean[size]; return new ControlCenterServiceBean[size];
} }
}; };
// ================================== 构造方法(无参+有参,满足不同初始化场景)================================= // ====================== 构造方法(无参+有参,满足不同初始化场景) ======================
/** /**
* 无参构造JSON解析、反射创建必备 * 无参构造JSON解析、反射创建必备
*/ */
@@ -60,25 +56,25 @@ public class ControlCenterServiceBean extends BaseBean implements Parcelable, Se
*/ */
public ControlCenterServiceBean(boolean isEnableService) { public ControlCenterServiceBean(boolean isEnableService) {
this.isEnableService = isEnableService; this.isEnableService = isEnableService;
LogUtils.d(TAG, "有参构造初始化服务状态isEnableService=" + isEnableService); LogUtils.d(TAG, String.format("有参构造初始化服务状态isEnableService=%b", isEnableService));
} }
// ================================== Getter/Setter 方法(封装成员变量,控制访问)================================= // ====================== Getter/Setter 方法(封装成员变量,控制访问) ======================
public boolean isEnableService() { public boolean isEnableService() {
LogUtils.d(TAG, "get isEnableService: 当前状态=" + isEnableService); LogUtils.d(TAG, String.format("isEnableService: 当前状态=%b", isEnableService));
return isEnableService; return isEnableService;
} }
public void setIsEnableService(boolean isEnableService) { public void setIsEnableService(boolean isEnableService) {
LogUtils.d(TAG, "set isEnableService: 旧状态=" + this.isEnableService + ",新状态=" + isEnableService); LogUtils.d(TAG, String.format("setIsEnableService: 旧状态=%b新状态=%b", this.isEnableService, isEnableService));
this.isEnableService = isEnableService; this.isEnableService = isEnableService;
} }
// ================================== 父类 BaseBean 方法重写(核心业务逻辑================================= // ====================== 父类 BaseBean 方法重写(核心业务逻辑JSON 序列化/反序列化) ======================
@Override @Override
public String getName() { public String getName() {
String className = ControlCenterServiceBean.class.getName(); String className = ControlCenterServiceBean.class.getName();
LogUtils.d(TAG, "getName: 返回类名=" + className); LogUtils.d(TAG, String.format("getName: 返回类名=%s", className));
return className; return className;
} }
@@ -87,11 +83,9 @@ public class ControlCenterServiceBean extends BaseBean implements Parcelable, Se
*/ */
@Override @Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将对象序列化到JSON");
super.writeThisToJsonWriter(jsonWriter); super.writeThisToJsonWriter(jsonWriter);
// 写入服务启用状态字段
jsonWriter.name(JSON_FIELD_IS_ENABLE_SERVICE).value(this.isEnableService); jsonWriter.name(JSON_FIELD_IS_ENABLE_SERVICE).value(this.isEnableService);
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成,字段=" + JSON_FIELD_IS_ENABLE_SERVICE + ",值=" + this.isEnableService); LogUtils.d(TAG, String.format("writeThisToJsonWriter: 序列化完成,%s=%b", JSON_FIELD_IS_ENABLE_SERVICE, this.isEnableService));
} }
/** /**
@@ -99,44 +93,39 @@ public class ControlCenterServiceBean extends BaseBean implements Parcelable, Se
*/ */
@Override @Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON反序列化对象");
ControlCenterServiceBean bean = new ControlCenterServiceBean(); ControlCenterServiceBean bean = new ControlCenterServiceBean();
jsonReader.beginObject(); jsonReader.beginObject();
while (jsonReader.hasNext()) { while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName(); String fieldName = jsonReader.nextName();
if (JSON_FIELD_IS_ENABLE_SERVICE.equals(fieldName)) { if (JSON_FIELD_IS_ENABLE_SERVICE.equals(fieldName)) {
// 读取并设置服务启用状态
boolean isEnable = jsonReader.nextBoolean(); boolean isEnable = jsonReader.nextBoolean();
bean.setIsEnableService(isEnable); bean.setIsEnableService(isEnable);
LogUtils.d(TAG, "readBeanFromJsonReader: 读取JSON字段" + fieldName + "=" + isEnable); LogUtils.d(TAG, String.format("readBeanFromJsonReader: 读取字段,%s=%b", fieldName, isEnable));
} else { } else {
// 跳过未知字段,避免解析异常
jsonReader.skipValue(); jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段=" + fieldName); LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段=%s", fieldName));
} }
} }
jsonReader.endObject(); jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成"); LogUtils.d(TAG, "readBeanFromJsonReader: 反序列化完成");
return bean; return bean;
} }
// ================================== Parcelable 接口方法实现(适配 Intent 组件间传递================================= // ====================== Parcelable 接口方法实现(适配 Intent 组件间传递Java7 适配) ======================
@Override @Override
public int describeContents() { public int describeContents() {
// 无特殊内容如文件描述符返回0即可API30 标准实现)
LogUtils.d(TAG, "describeContents: 返回内容描述符=0"); LogUtils.d(TAG, "describeContents: 返回内容描述符=0");
return 0; return 0; // 无特殊内容如文件描述符返回0即可API30 标准实现)
} }
/** /**
* 序列化对象到 ParcelIntent 传递必备Java7 适配) * 序列化对象到 ParcelIntent 传递必备Java7 适配:用 byte 存储 boolean
*/ */
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
LogUtils.d(TAG, "writeToParcel: 开始将对象序列化到Parcelflags=" + flags); byte flag = (byte) (this.isEnableService ? 1 : 0);
// Java7 + API30 适配Parcel 无 writeBoolean 方法,用 byte 存储1=true0=false dest.writeByte(flag);
dest.writeByte((byte) (this.isEnableService ? 1 : 0)); LogUtils.d(TAG, String.format("writeToParcel: 序列化完成isEnableService=%b存储为byte=%d", this.isEnableService, flag));
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成isEnableService=" + this.isEnableService + "存储为byte=" + (this.isEnableService ? 1 : 0) + "");
} }
} }

View File

@@ -1,36 +1,75 @@
package cc.winboll.studio.powerbell.models; package cc.winboll.studio.powerbell.models;
import cc.winboll.studio.libappbase.LogUtils;
/** /**
* 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递 * 通知数据模型
* 适配 API30统一存储通知标题、内容、标识信息支持各组件数据传递
* @Author ZhanGSKen<zhangsken@qq.com>
* @Describe 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递
*/ */
public class NotificationMessage { public class NotificationMessage {
// ====================== 静态常量(统一管理) ======================
private static final String TAG = "NotificationMessage";
private static final String EMPTY_STRING = "";
// ====================== 核心成员变量(按业务逻辑排序) ======================
private String title; // 通知标题 private String title; // 通知标题
private String content; // 通知内容 private String content; // 通知内容
private String remindMSG; // 通知标识(区分服务运行/充电/耗电) private String remindMSG; // 通知标识(区分服务运行/充电/耗电)
// ====================== Setter/Getter 方法 ====================== // ====================== 构造方法(无参+全参,满足不同初始化场景) ======================
public String getTitle() { /**
return title; * 无参构造器反射实例化、JSON反序列化必备
*/
public NotificationMessage() {
this.title = EMPTY_STRING;
this.content = EMPTY_STRING;
this.remindMSG = EMPTY_STRING;
LogUtils.d(TAG, "无参构造:初始化通知数据模型,默认值为空字符串");
} }
/**
* 全参构造器(直接传参创建实例,简化调用)
* @param title 通知标题
* @param content 通知内容
* @param remindMSG 通知标识
*/
public NotificationMessage(String title, String content, String remindMSG) {
this.title = title == null ? EMPTY_STRING : title;
this.content = content == null ? EMPTY_STRING : content;
this.remindMSG = remindMSG == null ? EMPTY_STRING : remindMSG;
LogUtils.d(TAG, String.format("全参构造:初始化完成 | 标题:%s | 内容:%s | 标识:%s",
this.title, this.content, this.remindMSG));
}
// ====================== Setter 方法(补充空值防护与调试日志) ======================
public void setTitle(String title) { public void setTitle(String title) {
this.title = title; this.title = title == null ? EMPTY_STRING : title;
LogUtils.d(TAG, String.format("setTitle通知标题设置为「%s」", this.title));
}
public void setContent(String content) {
this.content = content == null ? EMPTY_STRING : content;
LogUtils.d(TAG, String.format("setContent通知内容设置为「%s」", this.content));
}
public void setRemindMSG(String remindMSG) {
this.remindMSG = remindMSG == null ? EMPTY_STRING : remindMSG;
LogUtils.d(TAG, String.format("setRemindMSG通知标识设置为「%s」", this.remindMSG));
}
// ====================== Getter 方法(按成员变量顺序排列) ======================
public String getTitle() {
return title;
} }
public String getContent() { public String getContent() {
return content; return content;
} }
public void setContent(String content) {
this.content = content;
}
public String getRemindMSG() { public String getRemindMSG() {
return remindMSG; return remindMSG;
} }
public void setRemindMSG(String remindMSG) {
this.remindMSG = remindMSG;
}
} }

View File

@@ -0,0 +1,47 @@
package cc.winboll.studio.powerbell.models;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.Serializable;
/**
* TTS 语音播放文本内容实体类
* 适配Java7 语法规范 | Android API30 系统版本
* 特性:实现序列化接口,支持跨页面/进程传递,属性默认值初始化
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:13
*/
public class TTSSpeakTextBean implements Serializable {
// ====================================== 常量区 - 置顶排序 ======================================
/** 日志TAG 瞬态修饰,不参与序列化,减少序列化体积 */
transient public static final String TAG = "TTSSpeakTextBean";
// ====================================== 成员属性区 - 业务属性排序 ======================================
/** 延迟播放时长 单位毫秒默认值0无延迟播放 */
public int mnDelay = 0;
/** TTS语音播放文本内容默认值空字符串防止空指针 */
public String mszSpeakContent = "";
// ====================================== 构造方法区 - 无参+有参 完整实现 ======================================
/**
* 无参构造方法
* Java7序列化规范必备 + 兼容反射实例化场景
*/
public TTSSpeakTextBean() {
LogUtils.d(TAG, "【无参构造】TTSSpeakTextBean 实例化,使用默认值 | 延迟:" + mnDelay + " | 文本:" + mszSpeakContent);
}
/**
* 有参构造方法【主构造】
* @param nDelay 延迟播放时长(ms)
* @param szSpeakContent 语音播放文本内容
*/
public TTSSpeakTextBean(int nDelay, String szSpeakContent) {
LogUtils.d(TAG, "【有参构造】TTSSpeakTextBean 实例化,入参 | 延迟:" + nDelay + " | 文本:" + szSpeakContent);
this.mnDelay = nDelay;
this.mszSpeakContent = szSpeakContent;
LogUtils.d(TAG, "【有参构造】赋值完成 | 最终延迟:" + this.mnDelay + " | 最终文本:" + this.mszSpeakContent);
}
}

View File

@@ -0,0 +1,156 @@
package cc.winboll.studio.powerbell.models;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
import java.io.Serializable;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 20:59
* @Describe 贴心服务配置实体类 (适配API30 / Java7)
*/
public class ThoughtfulServiceBean extends BaseBean implements Parcelable, Serializable {
// ====================== 常量区 - 置顶统一管理 ======================
public static final String TAG = ThoughtfulServiceBean.class.getSimpleName();
private static final long serialVersionUID = 1L; // Serializable 序列化兼容必备
// JSON序列化字段常量 杜绝硬编码
public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS = "isEnableChargeTts";
public static final String JSON_FIELD_IS_ENABLE_USE_POWER_TTS = "isEnableUsePowerTts";
// ====================== 核心成员变量 - 私有封装 ======================
private boolean isEnableChargeTts = false; // 是否启用 充电TTS贴心语音服务
private boolean isEnableUsePowerTts = false; // 是否启用 用电TTS贴心语音服务
// ====================== Parcelable 静态创建器 (API30标准写法 必须public static final) ======================
public static final Creator<ThoughtfulServiceBean> CREATOR = new Creator<ThoughtfulServiceBean>() {
@Override
public ThoughtfulServiceBean createFromParcel(Parcel source) {
return new ThoughtfulServiceBean(source);
}
@Override
public ThoughtfulServiceBean[] newArray(int size) {
LogUtils.d(TAG, "newArray: 初始化数组size = " + size);
return new ThoughtfulServiceBean[size];
}
};
// ====================== 构造方法区 (无参+有参+Parcel构造 全覆盖) ======================
/**
* 无参构造 - JSON解析/反射实例化 必备
*/
public ThoughtfulServiceBean() {
LogUtils.d(TAG, "ThoughtfulServiceBean: 无参构造初始化默认禁用所有TTS服务");
}
/**
* 全参构造 - 手动配置所有服务状态
* @param isEnableChargeTts 充电TTS服务开关
* @param isEnableUsePowerTts 用电TTS服务开关
*/
public ThoughtfulServiceBean(boolean isEnableChargeTts, boolean isEnableUsePowerTts) {
this.isEnableChargeTts = isEnableChargeTts;
this.isEnableUsePowerTts = isEnableUsePowerTts;
LogUtils.d(TAG, "ThoughtfulServiceBean: 全参构造 | isEnableChargeTts=" + isEnableChargeTts + " | isEnableUsePowerTts=" + isEnableUsePowerTts);
}
/**
* Parcel反序列化构造 - Parcelable必备 私有私有化
*/
private ThoughtfulServiceBean(Parcel in) {
this.isEnableChargeTts = in.readByte() != 0;
this.isEnableUsePowerTts = in.readByte() != 0;
LogUtils.d(TAG, "ThoughtfulServiceBean: Parcel构造解析完成 | isEnableChargeTts=" + isEnableChargeTts + " | isEnableUsePowerTts=" + isEnableUsePowerTts);
}
// ====================== Getter/Setter 方法区 (封装成员变量 统一访问) ======================
public boolean isEnableChargeTts() {
return isEnableChargeTts;
}
public void setIsEnableChargeTts(boolean isEnableChargeTts) {
LogUtils.d(TAG, "setIsEnableChargeTts: 旧值=" + this.isEnableChargeTts + " 新值=" + isEnableChargeTts);
this.isEnableChargeTts = isEnableChargeTts;
}
public boolean isEnableUsePowerTts() {
return isEnableUsePowerTts;
}
public void setIsEnableUsePowerTts(boolean isEnableUsePowerTts) {
LogUtils.d(TAG, "setIsEnableUsePowerTts: 旧值=" + this.isEnableUsePowerTts + " 新值=" + isEnableUsePowerTts);
this.isEnableUsePowerTts = isEnableUsePowerTts;
}
// ====================== 重写父类 BaseBean 核心方法 (JSON序列化/反序列化 业务核心) ======================
@Override
public String getName() {
String className = ThoughtfulServiceBean.class.getName();
LogUtils.d(TAG, "getName: 返回当前实体类名 = " + className);
return className;
}
/**
* JSON序列化 - 写入所有字段 适配持久化/网络传输
*/
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS).value(this.isEnableChargeTts);
jsonWriter.name(JSON_FIELD_IS_ENABLE_USE_POWER_TTS).value(this.isEnableUsePowerTts);
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成所有TTS服务状态已写入");
}
/**
* JSON反序列化 - 读取字段生成实体 适配数据恢复
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
ThoughtfulServiceBean bean = new ThoughtfulServiceBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
switch (fieldName) {
case JSON_FIELD_IS_ENABLE_CHARGE_TTS:
bean.setIsEnableChargeTts(jsonReader.nextBoolean());
break;
case JSON_FIELD_IS_ENABLE_USE_POWER_TTS:
bean.setIsEnableUsePowerTts(jsonReader.nextBoolean());
break;
default:
jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段 = " + fieldName);
break;
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成生成实体对象");
return bean;
}
// ====================== 实现 Parcelable 接口方法 (组件间Intent传递必备 API30/Java7完美适配) ======================
@Override
public int describeContents() {
return 0; // 无文件描述符等特殊内容固定返回0即可
}
/**
* Parcel序列化 - boolean用byte存储(Java7/API30标准写法 避免兼容性问题)
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (isEnableChargeTts ? 1 : 0));
dest.writeByte((byte) (isEnableUsePowerTts ? 1 : 0));
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成所有TTS服务状态已写入");
}
}

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.models.AppConfigBean; import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.NotificationMessage; import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
@@ -12,16 +13,18 @@ import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils; import cc.winboll.studio.powerbell.utils.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils; import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import cc.winboll.studio.powerbell.services.ThoughtfulService;
/** /**
* 控制中心广播接收器 * 控制中心广播接收器
* 功能:监听电池状态变化、前台通知更新、配置变更指令 * 功能:监听电池状态变化、前台通知更新、配置变更指令
* 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步 * 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/19 20:23 * @Date 2025/12/19 20:23
* @Describe 统一处理系统与应用内广播,同步电池状态与配置,保障多线程数据一致性
*/ */
public class ControlCenterServiceReceiver extends BroadcastReceiver { public class ControlCenterServiceReceiver extends BroadcastReceiver {
// ================================== 静态常量区(置顶归类,消除魔法值)================================= // ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "ControlCenterServiceReceiver"; public static final String TAG = "ControlCenterServiceReceiver";
// 广播Action常量带包名前缀防冲突 // 广播Action常量带包名前缀防冲突
@@ -34,41 +37,42 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
private static final int BATTERY_LEVEL_MIN = 0; private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100; private static final int BATTERY_LEVEL_MAX = 100;
// ================================== 静态状态标记volatile保证多线程可见性================================= // ====================== 静态状态标记volatile保证多线程可见性 ======================
private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见) private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见)
private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见) private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见)
// ================================== 成员变量区(弱引用防泄漏,按功能分层)================================= // ====================== 成员变量区(弱引用防泄漏,按功能分层) ======================
private WeakReference<ControlCenterService> mwrControlCenterService; private WeakReference<ControlCenterService> mwrControlCenterService;
private boolean isRegistered = false; // 新增:标记广播注册状态,避免冗余操作 private boolean isRegistered = false; // 标记广播注册状态,避免冗余操作
// ================================== 构造方法(初始化弱引用,避免服务强引用泄漏)================================= // ====================== 构造方法(初始化弱引用,避免服务强引用泄漏) ======================
public ControlCenterServiceReceiver(ControlCenterService service) { public ControlCenterServiceReceiver(ControlCenterService service) {
LogUtils.d(TAG, "构造接收器 | service=" + (service != null ? service.getClass().getSimpleName() : "null")); LogUtils.d(TAG, String.format("ControlCenterServiceReceiver() 构造 | 服务实例:%s",
this.mwrControlCenterService = new WeakReference<>(service); service != null ? service.getClass().getSimpleName() : "null"));
this.mwrControlCenterService = new WeakReference<ControlCenterService>(service);
} }
// ================================== 广播核心接收逻辑入口方法分Action分发处理================================= // ====================== 广播核心接收逻辑入口方法分Action分发处理 ======================
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
LogUtils.d(TAG, "onReceive: 接收广播 | action=" + (intent != null ? intent.getAction() : "null")); String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onReceive() 执行 | 接收广播 Action%s", action));
// 基础参数校验 // 基础参数校验
if (context == null || intent == null || intent.getAction() == null) { if (context == null || intent == null || action == null) {
LogUtils.e(TAG, "onReceive: 参数无效context=" + context + " | intent=" + intent + ",终止处理"); LogUtils.e(TAG, "onReceive() 终止 | 参数无效context=" + context + " | intent=" + intent + "");
return; return;
} }
// 弱引用获取服务,双重校验服务有效性 // 弱引用获取服务,双重校验服务有效性
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null; ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
if (service == null || service.isDestroyed()) { if (service == null || service.isDestroyed()) {
LogUtils.e(TAG, "onReceive: 服务已销毁或为空service=" + service + "),注销广播"); LogUtils.e(TAG, "onReceive() 终止 | 服务已销毁或为空,执行注销");
unregisterAction(context); unregisterAction(context);
return; return;
} }
// 分Action处理业务逻辑 // 分Action处理业务逻辑
String action = intent.getAction();
switch (action) { switch (action) {
case Intent.ACTION_BATTERY_CHANGED: case Intent.ACTION_BATTERY_CHANGED:
handleBatteryStateChanged(service, intent); handleBatteryStateChanged(service, intent);
@@ -77,46 +81,57 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
handleUpdateForegroundNotification(service); handleUpdateForegroundNotification(service);
break; break;
case ACTION_APPCONFIG_CHANGED: case ACTION_APPCONFIG_CHANGED:
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播"); // 新增:标记配置广播处理起点 LogUtils.d(TAG, "onReceive() 分发 | 处理配置更新广播");
handleNotifyAppConfigUpdate(service); handleNotifyAppConfigUpdate(service);
break; break;
default: default:
LogUtils.w(TAG, "onReceive: 未知Action=" + action); LogUtils.w(TAG, String.format("onReceive() 警告 | 未知Action=%s", action));
} }
LogUtils.d(TAG, "onReceive: 广播处理完成"); LogUtils.d(TAG, "onReceive() 完成 | 广播处理结束");
} }
// ================================== 业务处理方法(按功能拆分,强化容错与日志)================================= // ====================== 业务处理方法(按功能拆分,强化容错与日志) ======================
/** /**
* 处理电池状态变化广播 * 处理电池状态变化广播
* @param service 控制中心服务实例 * @param service 控制中心服务实例
* @param intent 电池状态广播意图 * @param intent 电池状态广播意图
*/ */
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) { private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态 | service=" + service + " | intent=" + intent); LogUtils.d(TAG, "handleBatteryStateChanged() 执行 | 解析电池状态");
try { try {
// 1. 解析并校验当前电池状态 // 1. 解析并校验当前电池状态
boolean currentCharging = BatteryUtils.isCharging(intent); boolean currentCharging = BatteryUtils.isCharging(intent);
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent); int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX); currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
LogUtils.d(TAG, "handleBatteryStateChanged: 当前状态 | 充电=" + currentCharging + " | 电量=" + currentBatteryLevel + "%"); LogUtils.d(TAG, String.format("handleBatteryStateChanged() 解析 | 充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
// 2. 状态无变化则跳过,减少无效运算 // 2. 状态无变化则跳过,减少无效运算
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) { if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态无变化,跳过处理"); LogUtils.d(TAG, "handleBatteryStateChanged() 跳过 | 电池状态无变化");
return; return;
} }
// 在插拔充电线时,执行贴心服务
if(currentCharging != sIsCharging) {
if(currentCharging) {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.CHARGE_STATE);
} else {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.DISCHARGE_STATE);
}
}
// 4. 更新静态缓存状态,保证多线程可见 // 3. 更新静态缓存状态,保证多线程可见
sIsCharging = currentCharging; sIsCharging = currentCharging;
sLastBatteryLevel = currentBatteryLevel; sLastBatteryLevel = currentBatteryLevel;
handleNotifyAppConfigUpdate(service);
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态处理成功 | 缓存电量=" + sLastBatteryLevel + "% | 缓存充电状态=" + sIsCharging); // 4. 同步缓存状态到配置
handleNotifyAppConfigUpdate(service);
LogUtils.d(TAG, String.format("handleBatteryStateChanged() 完成 | 缓存电量=%d%% | 缓存充电状态=%b",
sLastBatteryLevel, sIsCharging));
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e); LogUtils.e(TAG, "handleBatteryStateChanged() 失败", e);
} }
} }
@@ -125,25 +140,26 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param service 控制中心服务实例 * @param service 控制中心服务实例
*/ */
private void handleNotifyAppConfigUpdate(ControlCenterService service) { private void handleNotifyAppConfigUpdate(ControlCenterService service) {
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置 | service=" + service); LogUtils.d(TAG, "handleNotifyAppConfigUpdate() 执行 | 同步缓存状态到配置");
try { try {
// 加载最新配置 // 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig(); AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
if (latestConfig == null) { // 新增:配置空指针防护 if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 最新配置为空,终止处理"); LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
return; return;
} }
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=" + latestConfig.getChargeReminderValue() + " | 耗电阈值=" + latestConfig.getUsageReminderValue()); LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate() 加载 | 充电阈值=%d | 耗电阈值=%d",
latestConfig.getChargeReminderValue(), latestConfig.getUsageReminderValue()));
// 同步缓存的电池状态到配置 // 同步缓存的电池状态到配置
latestConfig.setCurrentBatteryValue(sLastBatteryLevel); App.sQuantityOfElectricity = sLastBatteryLevel;
latestConfig.setIsCharging(sIsCharging); latestConfig.setIsCharging(sIsCharging);
service.notifyAppConfigUpdate(latestConfig); service.notifyAppConfigUpdate(latestConfig);
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=" + sLastBatteryLevel + "% | 充电状态=" + sIsCharging); LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate() 完成 | 缓存电量=%d%% | 充电状态=%b",
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成"); // 新增:标记配置广播处理终点 sLastBatteryLevel, sIsCharging));
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 处理失败", e); LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 失败", e);
} }
} }
@@ -152,33 +168,34 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param service 控制中心服务实例 * @param service 控制中心服务实例
*/ */
private void handleUpdateForegroundNotification(ControlCenterService service) { private void handleUpdateForegroundNotification(ControlCenterService service) {
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知 | service=" + service); LogUtils.d(TAG, "handleUpdateForegroundNotification() 执行 | 更新前台通知");
try { try {
NotificationManagerUtils notifyUtils = service.getNotificationManager(); NotificationManagerUtils notifyUtils = service.getNotificationManager();
NotificationMessage notifyMsg = service.getForegroundNotifyMsg(); NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
// 非空校验,避免空指针 // 非空校验,避免空指针
if (notifyUtils == null || notifyMsg == null) { if (notifyUtils == null || notifyMsg == null) {
LogUtils.e(TAG, "handleUpdateForegroundNotification: 通知工具类或消息为空notifyUtils=" + notifyUtils + " | notifyMsg=" + notifyMsg + ""); LogUtils.e(TAG, String.format("handleUpdateForegroundNotification() 终止 | 通知工具类或消息为空notifyUtils=%s | notifyMsg=%s",
notifyUtils, notifyMsg));
return; return;
} }
notifyUtils.updateForegroundServiceNotify(notifyMsg); notifyUtils.updateForegroundServiceNotify(notifyMsg);
LogUtils.d(TAG, "handleUpdateForegroundNotification: 前台通知更新成功 | 通知标题=" + notifyMsg.getTitle()); LogUtils.d(TAG, String.format("handleUpdateForegroundNotification() 完成 | 标题=%s", notifyMsg.getTitle()));
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "handleUpdateForegroundNotification: 处理失败", e); LogUtils.e(TAG, "handleUpdateForegroundNotification() 失败", e);
} }
} }
// ================================== 广播注册/注销(强化容错,避免重复操作)================================= // ====================== 广播注册/注销(强化容错,避免重复操作) ======================
/** /**
* 注册广播接收器 * 注册广播接收器
* @param context 上下文 * @param context 上下文
*/ */
public void registerAction(Context context) { public void registerAction(Context context) {
LogUtils.d(TAG, "registerAction: 注册广播接收器 | context=" + context); LogUtils.d(TAG, "registerAction() 执行 | 注册广播接收器");
if (context == null || isRegistered) { // 新增:已注册则跳过 if (context == null || isRegistered) {
LogUtils.e(TAG, "registerAction: 上下文为空或已注册,注册失败"); LogUtils.e(TAG, "registerAction() 失败 | 上下文为空或已注册");
return; return;
} }
@@ -190,10 +207,10 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
filter.setPriority(BROADCAST_PRIORITY); filter.setPriority(BROADCAST_PRIORITY);
context.registerReceiver(this, filter); context.registerReceiver(this, filter);
isRegistered = true; // 标记为已注册 isRegistered = true;
LogUtils.d(TAG, "registerAction: 广播注册成功 | 优先级=" + BROADCAST_PRIORITY); LogUtils.d(TAG, String.format("registerAction() 完成 | 优先级=%d", BROADCAST_PRIORITY));
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "registerAction: 注册失败", e); LogUtils.e(TAG, "registerAction() 失败", e);
} }
} }
@@ -202,39 +219,39 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param context 上下文 * @param context 上下文
*/ */
public void unregisterAction(Context context) { public void unregisterAction(Context context) {
LogUtils.d(TAG, "unregisterAction: 注销广播接收器 | context=" + context); LogUtils.d(TAG, "unregisterAction() 执行 | 注销广播接收器");
if (context == null || !isRegistered) { // 新增:未注册则跳过 if (context == null || !isRegistered) {
LogUtils.e(TAG, "unregisterAction: 上下文为空或未注册,注销失败"); LogUtils.e(TAG, "unregisterAction() 失败 | 上下文为空或未注册");
return; return;
} }
try { try {
context.unregisterReceiver(this); context.unregisterReceiver(this);
isRegistered = false; // 标记为未注册 isRegistered = false;
LogUtils.d(TAG, "unregisterAction: 广播注销成功"); LogUtils.d(TAG, "unregisterAction() 完成 | 广播注销成功");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销"); LogUtils.w(TAG, "unregisterAction() 警告 | 广播未注册,跳过注销");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "unregisterAction: 注销失败", e); LogUtils.e(TAG, "unregisterAction() 失败", e);
} }
} }
// ================================== 资源释放与Getter方法按需开放防泄漏================================= // ====================== 资源释放与Getter方法按需开放防泄漏 ======================
/** /**
* 主动释放资源,避免内存泄漏 * 主动释放资源,避免内存泄漏
*/ */
public void release() { public void release() {
LogUtils.d(TAG, "release: 释放广播接收器资源"); LogUtils.d(TAG, "release() 执行 | 释放广播接收器资源");
// 清空弱引用帮助GC回收 // 清空弱引用帮助GC回收
if (mwrControlCenterService != null) { if (mwrControlCenterService != null) {
mwrControlCenterService.clear(); mwrControlCenterService.clear();
mwrControlCenterService = null; mwrControlCenterService = null;
LogUtils.d(TAG, "release: 弱引用已清空"); LogUtils.d(TAG, "release() 步骤 | 弱引用已清空");
} }
// 重置静态状态缓存 // 重置静态状态缓存
sLastBatteryLevel = -1; sLastBatteryLevel = -1;
sIsCharging = false; sIsCharging = false;
LogUtils.d(TAG, "release: 静态状态缓存已重置"); LogUtils.d(TAG, "release() 完成 | 静态状态缓存已重置");
} }
/** /**
@@ -253,3 +270,4 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
return sIsCharging; return sIsCharging;
} }
} }

View File

@@ -18,110 +18,113 @@ import cc.winboll.studio.powerbell.utils.BatteryUtils;
* 适配Java7 | API30 | 内存泄漏防护 * 适配Java7 | API30 | 内存泄漏防护
*/ */
public class GlobalApplicationReceiver extends BroadcastReceiver { public class GlobalApplicationReceiver extends BroadcastReceiver {
// ================================== 静态常量区(置顶归类,消除魔法值)================================= // ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "GlobalApplicationReceiver"; public static final String TAG = "GlobalApplicationReceiver";
private static final int BATTERY_LEVEL_MIN = 0; private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100; private static final int BATTERY_LEVEL_MAX = 100;
// ================================== 静态成员变量(线程安全,volatile保证多线程可见性================================= // ====================== 静态状态标记(volatile保证多线程可见性 ======================
private static volatile int sLastBatteryLevel = -1; // 历史电量0-100 private static volatile int sLastBatteryLevel = -1; // 历史电量0-100
private static volatile boolean sLastIsCharging = false; // 历史充电状态 private static volatile boolean sLastIsCharging = false; // 历史充电状态
// ================================== 成员变量区(按功能分层)================================= // ====================== 成员变量区按功能分层移除冗余的mCurrentReceiver ======================
private App mGlobalApplication; private App mGlobalApplication;
private AppConfigUtils mAppConfigUtils; private AppConfigUtils mAppConfigUtils;
private GlobalApplicationReceiver mCurrentReceiver;
// ================================== 构造方法(强化参数校验,初始化核心依赖)================================= // ====================== 构造方法(强化参数校验,初始化核心依赖) ======================
public GlobalApplicationReceiver(App globalApplication) { public GlobalApplicationReceiver(App globalApplication) {
LogUtils.d(TAG, "构造接收器 | App=" + globalApplication); LogUtils.d(TAG, String.format("构造接收器 | App实例:%s", globalApplication));
if (globalApplication == null) { if (globalApplication == null) {
LogUtils.e(TAG, "构造失败App实例为空"); LogUtils.e(TAG, "构造失败App实例为空");
throw new IllegalArgumentException("App cannot be null"); throw new IllegalArgumentException("App cannot be null");
} }
this.mCurrentReceiver = this;
this.mGlobalApplication = globalApplication; this.mGlobalApplication = globalApplication;
this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication); this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
LogUtils.d(TAG, "构造完成AppConfigUtils=" + mAppConfigUtils); LogUtils.d(TAG, String.format("构造完成 | AppConfigUtils%s", mAppConfigUtils));
} }
// ================================== 广播核心接收逻辑(入口方法,过滤电池状态广播)================================= // ====================== 广播核心接收逻辑(入口方法,过滤电池状态广播) ======================
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
LogUtils.d(TAG, "onReceive: 接收广播 | context=" + context + " | intent=" + intent + " | action=" + (intent != null ? intent.getAction() : "null")); String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onReceive: 接收广播 | 上下文:%s | Action%s", context, action));
// 基础参数校验 // 基础参数校验
if (context == null || intent == null || intent.getAction() == null) { if (context == null || intent == null || action == null) {
LogUtils.e(TAG, "onReceive: 参数无效,终止处理"); LogUtils.e(TAG, "onReceive: 参数无效,终止处理");
return; return;
} }
// 仅处理电池状态变化广播 // 仅处理电池状态变化广播
if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) { if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
handleBatteryStateChanged(context, intent); handleBatteryStateChanged(context, intent);
} }
LogUtils.d(TAG, "onReceive: 广播处理完成"); LogUtils.d(TAG, "onReceive: 广播处理完成");
} }
// ================================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面)================================= // ====================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面) ======================
/** /**
* 处理电池状态变化广播 * 处理电池状态变化广播
* @param context 上下文 * @param context 上下文
* @param intent 电池状态广播意图 * @param intent 电池状态广播意图
*/ */
private void handleBatteryStateChanged(Context context, Intent intent) { private void handleBatteryStateChanged(Context context, Intent intent) {
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态 | intent=" + intent); LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
// 1. 解析当前电池状态(复用工具类,二次校验电量范围) try {
boolean currentIsCharging = BatteryUtils.isCharging(intent); // 1. 解析当前电池状态(复用工具类,二次校验电量范围)
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent); boolean currentIsCharging = BatteryUtils.isCharging(intent);
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX); int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
LogUtils.d(TAG, "handleBatteryStateChanged: 当前状态 | 充电=" + currentIsCharging + " | 电量=" + currentBatteryLevel + "%"); currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentIsCharging, currentBatteryLevel));
// 2. 状态无变化则跳过,减少无效运算 // 2. 状态无变化则跳过,减少无效运算
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) { if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理"); LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
return; return;
}
// 3. 同步最新状态到配置工具类
if (mAppConfigUtils != null) {
if (currentIsCharging != sLastIsCharging) {
mAppConfigUtils.setCharging(currentIsCharging);
LogUtils.d(TAG, "handleBatteryStateChanged: 同步充电状态 | " + currentIsCharging);
} }
if (currentBatteryLevel != sLastBatteryLevel) {
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel); // 3. 同步最新状态到配置工具类
LogUtils.d(TAG, "handleBatteryStateChanged: 同步电量 | " + currentBatteryLevel + "%"); if (mAppConfigUtils != null) {
if (currentIsCharging != sLastIsCharging) {
mAppConfigUtils.setCharging(currentIsCharging);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步充电状态 | %b", currentIsCharging));
}
if (currentBatteryLevel != sLastBatteryLevel) {
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步电量 | %d%%", currentBatteryLevel));
}
} else {
LogUtils.e(TAG, "handleBatteryStateChanged: AppConfigUtils为空同步失败");
} }
} else {
LogUtils.e(TAG, "handleBatteryStateChanged: AppConfigUtils为空同步失败");
}
// 4. 执行状态变化后的业务逻辑 // 4. 执行状态变化后的业务逻辑
// 记录电量变化时间 // 记录电量变化时间
if (App.getAppCacheUtils(context) != null) { if (App.getAppCacheUtils(context) != null) {
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel); App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间"); LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
} }
// 通知MainActivity更新电量 // 通知MainActivity更新电量
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel); MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
LogUtils.d(TAG, "handleBatteryStateChanged: 发送电量更新消息到MainActivity"); LogUtils.d(TAG, String.format("handleBatteryStateChanged: 发送电量更新消息到MainActivity | %d%%", currentBatteryLevel));
// 5. 更新历史状态缓存 // 5. 更新历史状态缓存
sLastIsCharging = currentIsCharging; sLastIsCharging = currentIsCharging;
sLastBatteryLevel = currentBatteryLevel; sLastBatteryLevel = currentBatteryLevel;
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成"); LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
} catch (Exception e) {
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
}
} }
// ================================== 广播注册/注销(强化容错,避免重复操作)================================= // ====================== 广播注册/注销(强化容错,避免重复操作) ======================
/** /**
* 注册广播接收器 * 注册广播接收器
*/ */
public void registerAction() { public void registerAction() {
LogUtils.d(TAG, "registerAction: 注册广播"); LogUtils.d(TAG, "registerAction: 注册广播");
if (mGlobalApplication == null || mCurrentReceiver == null) { if (mGlobalApplication == null) {
LogUtils.e(TAG, "注册失败App或Receiver实例为空"); LogUtils.e(TAG, "注册失败App实例为空");
return; return;
} }
@@ -130,7 +133,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
unregisterAction(); unregisterAction();
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED); filter.addAction(Intent.ACTION_BATTERY_CHANGED);
mGlobalApplication.registerReceiver(mCurrentReceiver, filter); mGlobalApplication.registerReceiver(this, filter);
LogUtils.d(TAG, "registerAction: 广播注册成功"); LogUtils.d(TAG, "registerAction: 广播注册成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "registerAction: 注册失败", e); LogUtils.e(TAG, "registerAction: 注册失败", e);
@@ -142,13 +145,13 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
*/ */
public void unregisterAction() { public void unregisterAction() {
LogUtils.d(TAG, "unregisterAction: 注销广播"); LogUtils.d(TAG, "unregisterAction: 注销广播");
if (mGlobalApplication == null || mCurrentReceiver == null) { if (mGlobalApplication == null) {
LogUtils.e(TAG, "注销失败App或Receiver实例为空"); LogUtils.e(TAG, "注销失败App实例为空");
return; return;
} }
try { try {
mGlobalApplication.unregisterReceiver(mCurrentReceiver); mGlobalApplication.unregisterReceiver(this);
LogUtils.d(TAG, "unregisterAction: 广播注销成功"); LogUtils.d(TAG, "unregisterAction: 广播注销成功");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销"); LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
@@ -157,7 +160,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
} }
} }
// ================================== 资源释放方法(主动释放,彻底避免内存泄漏)================================= // ====================== 资源释放方法(主动释放,彻底避免内存泄漏) ======================
/** /**
* 释放接收器资源供App销毁时调用 * 释放接收器资源供App销毁时调用
*/ */
@@ -168,7 +171,6 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
// 置空引用帮助GC回收 // 置空引用帮助GC回收
mGlobalApplication = null; mGlobalApplication = null;
mAppConfigUtils = null; mAppConfigUtils = null;
mCurrentReceiver = null;
// 重置静态状态缓存 // 重置静态状态缓存
sLastBatteryLevel = -1; sLastBatteryLevel = -1;
sLastIsCharging = false; sLastIsCharging = false;

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.powerbell.receivers; package cc.winboll.studio.powerbell.receivers;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/06 15:01:39
* @Describe 应用广播消息接收类
*/
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -14,30 +9,84 @@ import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.ServiceUtils; import cc.winboll.studio.powerbell.utils.ServiceUtils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/06 15:01:39
* @Describe 应用核心广播接收器
* 功能:监听开机完成广播,实现服务开机自启
* 适配Java7 | API30 | 服务启动兼容性处理
*/
public class MainReceiver extends BroadcastReceiver { public class MainReceiver extends BroadcastReceiver {
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "MainReceiver"; public static final String TAG = "MainReceiver";
// 系统广播Action常量
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
// API版本常量适配前台服务启动要求
private static final int API_LEVEL_26 = 26;
static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED"; // ====================== 静态状态标记volatile保证多线程可见性 ======================
// 存储电量指示值, // 历史电量值,用于校验电量变化(暂未使用,保留扩展能力)
// 用于校验电量消息时的电量变化 private static volatile int sLastBatteryLevel = -1;
static volatile int _mnTheQuantityOfElectricityOld = -1;
// ====================== 广播核心接收逻辑入口方法分Action处理 ======================
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String szAction = intent.getAction(); // 基础参数校验
if (szAction.equals(ACTION_BOOT_COMPLETED)) { if (context == null || intent == null) {
boolean isEnableService = App.getAppConfigUtils(context).isServiceEnabled(); LogUtils.e(TAG, "onReceive: 上下文或意图为空,终止处理");
if (isEnableService) { return;
if (ServiceUtils.isServiceAlive(context.getApplicationContext(), ControlCenterService.class.getName()) == false) { }
LogUtils.d(TAG, "wakeupAndBindMain() Wakeup... ControlCenterService");
if (Build.VERSION.SDK_INT >= 26) { String action = intent.getAction();
context.startForegroundService(new Intent(context, ControlCenterService.class)); LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action%s", action));
} else {
context.startService(new Intent(context, ControlCenterService.class)); // 仅处理开机完成广播
} if (ACTION_BOOT_COMPLETED.equals(action)) {
} handleBootCompleted(context);
} else {
LogUtils.w(TAG, String.format("onReceive: 忽略未知Action%s", action));
}
}
// ====================== 业务处理方法(处理开机完成广播,实现服务自启) ======================
/**
* 处理开机完成广播,自动启动控制中心服务
* @param context 上下文
*/
private void handleBootCompleted(Context context) {
LogUtils.d(TAG, "handleBootCompleted: 开始处理开机完成广播");
try {
// 1. 校验服务启用状态
boolean isServiceEnabled = App.getAppConfigUtils(context).isServiceEnabled();
LogUtils.d(TAG, String.format("handleBootCompleted: 服务启用状态:%b", isServiceEnabled));
if (!isServiceEnabled) {
LogUtils.d(TAG, "handleBootCompleted: 服务未启用,跳过自启");
return;
} }
// 2. 校验服务是否已运行
String serviceClassName = ControlCenterService.class.getName();
boolean isServiceAlive = ServiceUtils.isServiceAlive(context.getApplicationContext(), serviceClassName);
LogUtils.d(TAG, String.format("handleBootCompleted: 服务运行状态:%b", isServiceAlive));
if (isServiceAlive) {
LogUtils.d(TAG, "handleBootCompleted: 服务已运行,无需重复启动");
return;
}
// 3. 按API版本启动服务适配前台服务要求
Intent serviceIntent = new Intent(context, ControlCenterService.class);
if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
context.startForegroundService(serviceIntent);
LogUtils.d(TAG, "handleBootCompleted: 启动前台服务API >= 26");
} else {
context.startService(serviceIntent);
LogUtils.d(TAG, "handleBootCompleted: 启动普通服务API < 26");
}
LogUtils.d(TAG, "handleBootCompleted: 服务自启处理完成");
} catch (Exception e) {
LogUtils.e(TAG, "handleBootCompleted: 服务自启失败", e);
} }
} }
} }

View File

@@ -16,21 +16,25 @@ import cc.winboll.studio.powerbell.utils.ServiceUtils;
* 电池提醒核心服务进程守护类 * 电池提醒核心服务进程守护类
* 功能:监听主服务 {@link ControlCenterService} 存活状态,异常断开时自动重启并绑定 * 功能:监听主服务 {@link ControlCenterService} 存活状态,异常断开时自动重启并绑定
* 适配Java7 | API30 | 前台服务启动规则 | 服务绑定稳定性保障 * 适配Java7 | API30 | 前台服务启动规则 | 服务绑定稳定性保障
* @Author ZhanGSKen<zhangsken@qq.com>
* @Describe 守护服务保障ControlCenterService持续运行
*/ */
public class AssistantService extends Service { public class AssistantService extends Service {
// ================================== 静态常量区(置顶归类,消除魔法值)================================= // ====================== 静态常量区(置顶归类,消除魔法值) ======================
private static final String TAG = "AssistantService"; private static final String TAG = "AssistantService";
// 服务返回策略常量(统一定义,避免魔法值) // 服务返回策略常量
private static final int SERVICE_RETURN_STICKY = START_STICKY; private static final int SERVICE_RETURN_STICKY = START_STICKY;
// 服务绑定标记常量 // 服务绑定标记常量
private static final int BIND_FLAG = Context.BIND_IMPORTANT; private static final int BIND_FLAG = Context.BIND_IMPORTANT;
// API版本常量适配前台服务启动要求
private static final int API_LEVEL_26 = Build.VERSION_CODES.O;
// ================================== 成员变量区按功能分层volatile保证多线程可见性================================= // ====================== 成员变量区按功能分层volatile保证多线程可见性 ======================
private AppConfigUtils mAppConfigUtils; private AppConfigUtils mAppConfigUtils;
private MyServiceConnection mMyServiceConnection; private MyServiceConnection mMyServiceConnection;
private volatile boolean mIsThreadAlive; private volatile boolean mIsThreadAlive;
// ================================== 内部类(服务连接状态监听,前置定义便于引用)================================= // ====================== 内部类(服务连接状态监听,前置定义便于引用) ======================
/** /**
* 服务连接状态监听器 * 服务连接状态监听器
* 主服务连接成功时记录状态,断开时自动重连 * 主服务连接成功时记录状态,断开时自动重连
@@ -38,12 +42,14 @@ public class AssistantService extends Service {
private class MyServiceConnection implements ServiceConnection { private class MyServiceConnection implements ServiceConnection {
@Override @Override
public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceConnected(ComponentName name, IBinder service) {
LogUtils.d(TAG, "onServiceConnected: 主服务连接成功 | 组件名=" + name.getClassName() + " | Binder=" + service); String className = name != null ? name.getClassName() : "null";
LogUtils.d(TAG, String.format("onServiceConnected: 主服务连接成功 | 组件名=%s | Binder=%s", className, service));
} }
@Override @Override
public void onServiceDisconnected(ComponentName name) { public void onServiceDisconnected(ComponentName name) {
LogUtils.d(TAG, "onServiceDisconnected: 主服务连接断开 | 组件名=" + name.getClassName()); String className = name != null ? name.getClassName() : "null";
LogUtils.d(TAG, String.format("onServiceDisconnected: 主服务连接断开 | 组件名=%s", className));
// 主服务断开且配置启用时,重新唤醒绑定 // 主服务断开且配置启用时,重新唤醒绑定
if (mAppConfigUtils != null && mAppConfigUtils.isServiceEnabled()) { if (mAppConfigUtils != null && mAppConfigUtils.isServiceEnabled()) {
LogUtils.d(TAG, "onServiceDisconnected: 配置启用,尝试重新唤醒并绑定主服务"); LogUtils.d(TAG, "onServiceDisconnected: 配置启用,尝试重新唤醒并绑定主服务");
@@ -52,11 +58,11 @@ public class AssistantService extends Service {
} }
} }
// ================================== 服务生命周期方法按执行顺序排列onCreate→onStartCommand→onBind→onDestroy================================= // ====================== 服务生命周期方法按执行顺序排列onCreate→onStartCommand→onBind→onDestroy ======================
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
LogUtils.d(TAG, "onCreate: 守护服务启动 | 进程ID=" + android.os.Process.myPid()); LogUtils.d(TAG, String.format("onCreate: 守护服务启动 | 进程ID=%d", android.os.Process.myPid()));
// 初始化配置工具类,添加空指针防护 // 初始化配置工具类,添加空指针防护
mAppConfigUtils = App.getAppConfigUtils(this); mAppConfigUtils = App.getAppConfigUtils(this);
@@ -75,12 +81,12 @@ public class AssistantService extends Service {
// 初始化运行状态,执行核心守护逻辑 // 初始化运行状态,执行核心守护逻辑
mIsThreadAlive = false; mIsThreadAlive = false;
run(); run();
LogUtils.d(TAG, "onCreate: 守护服务初始化完成 | 服务启用状态=" + mAppConfigUtils.isServiceEnabled()); LogUtils.d(TAG, String.format("onCreate: 守护服务初始化完成 | 服务启用状态=%b", mAppConfigUtils.isServiceEnabled()));
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "onStartCommand: 守护服务触发重启 | flags=" + flags + " | startId=" + startId); LogUtils.d(TAG, String.format("onStartCommand: 守护服务触发重启 | flags=%d | startId=%d", flags, startId));
// 配置工具类为空时,直接返回非粘性策略 // 配置工具类为空时,直接返回非粘性策略
if (mAppConfigUtils == null) { if (mAppConfigUtils == null) {
LogUtils.e(TAG, "onStartCommand: AppConfigUtils未初始化终止服务"); LogUtils.e(TAG, "onStartCommand: AppConfigUtils未初始化终止服务");
@@ -90,13 +96,13 @@ public class AssistantService extends Service {
run(); run();
int returnFlag = mAppConfigUtils.isServiceEnabled() ? SERVICE_RETURN_STICKY : super.onStartCommand(intent, flags, startId); int returnFlag = mAppConfigUtils.isServiceEnabled() ? SERVICE_RETURN_STICKY : super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "onStartCommand: 处理完成 | 返回策略=" + (returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT")); LogUtils.d(TAG, String.format("onStartCommand: 处理完成 | 返回策略=%s", returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT"));
return returnFlag; return returnFlag;
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind: 服务绑定请求 | intent=" + intent); LogUtils.d(TAG, String.format("onBind: 服务绑定请求 | intent=%s", intent));
return null; return null;
} }
@@ -116,14 +122,15 @@ public class AssistantService extends Service {
LogUtils.d(TAG, "onDestroy: 守护服务销毁完成"); LogUtils.d(TAG, "onDestroy: 守护服务销毁完成");
} }
// ================================== 核心业务逻辑(守护主服务存活)================================= // ====================== 核心业务逻辑(守护主服务存活) ======================
/** /**
* 执行守护逻辑:检查主服务状态,按需唤醒并绑定 * 执行守护逻辑:检查主服务状态,按需唤醒并绑定
* 前置条件mAppConfigUtils 必须初始化完成 * 前置条件mAppConfigUtils 必须初始化完成
*/ */
private void run() { private void run() {
LogUtils.d(TAG, "run: 执行守护逻辑 | 配置启用=" + mAppConfigUtils.isServiceEnabled() + " | 线程存活=" + mIsThreadAlive); boolean isServiceEnabled = mAppConfigUtils.isServiceEnabled();
if (mAppConfigUtils.isServiceEnabled()) { LogUtils.d(TAG, String.format("run: 执行守护逻辑 | 配置启用=%b | 线程存活=%b", isServiceEnabled, mIsThreadAlive));
if (isServiceEnabled) {
if (!mIsThreadAlive) { if (!mIsThreadAlive) {
mIsThreadAlive = true; mIsThreadAlive = true;
wakeupAndBindMain(); wakeupAndBindMain();
@@ -141,13 +148,14 @@ public class AssistantService extends Service {
*/ */
private void wakeupAndBindMain() { private void wakeupAndBindMain() {
// 检查主服务存活状态 // 检查主服务存活状态
boolean isMainServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName()); String mainServiceName = ControlCenterService.class.getName();
LogUtils.d(TAG, "wakeupAndBindMain: 主服务存活状态=" + isMainServiceAlive); boolean isMainServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), mainServiceName);
LogUtils.d(TAG, String.format("wakeupAndBindMain: 主服务存活状态=%b", isMainServiceAlive));
// 主服务未存活时按需启动区分API版本 // 主服务未存活时按需启动区分API版本
if (!isMainServiceAlive) { if (!isMainServiceAlive) {
Intent mainServiceIntent = new Intent(AssistantService.this, ControlCenterService.class); Intent mainServiceIntent = new Intent(AssistantService.this, ControlCenterService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
startForegroundService(mainServiceIntent); startForegroundService(mainServiceIntent);
LogUtils.d(TAG, "wakeupAndBindMain: API26+ 以前台服务方式启动主服务"); LogUtils.d(TAG, "wakeupAndBindMain: API26+ 以前台服务方式启动主服务");
} else { } else {
@@ -159,10 +167,10 @@ public class AssistantService extends Service {
// 绑定主服务,监听连接状态,添加结果日志 // 绑定主服务,监听连接状态,添加结果日志
Intent bindIntent = new Intent(AssistantService.this, ControlCenterService.class); Intent bindIntent = new Intent(AssistantService.this, ControlCenterService.class);
boolean bindResult = bindService(bindIntent, mMyServiceConnection, BIND_FLAG); boolean bindResult = bindService(bindIntent, mMyServiceConnection, BIND_FLAG);
LogUtils.d(TAG, "wakeupAndBindMain: 绑定主服务结果=" + bindResult + " | 绑定标记=BIND_IMPORTANT"); LogUtils.d(TAG, String.format("wakeupAndBindMain: 绑定主服务结果=%b | 绑定标记=BIND_IMPORTANT", bindResult));
} }
// ================================== 辅助工具方法(拆分独立逻辑,提高可维护性)================================= // ====================== 辅助工具方法(拆分独立逻辑,提高可维护性) ======================
/** /**
* 解绑主服务,包含异常捕获与状态日志 * 解绑主服务,包含异常捕获与状态日志
*/ */
@@ -172,7 +180,7 @@ public class AssistantService extends Service {
unbindService(mMyServiceConnection); unbindService(mMyServiceConnection);
LogUtils.d(TAG, "unbindMainService: 已成功解绑ControlCenterService"); LogUtils.d(TAG, "unbindMainService: 已成功解绑ControlCenterService");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unbindMainService: 解绑服务失败,服务未绑定 | " + e.getMessage()); LogUtils.w(TAG, String.format("unbindMainService: 解绑服务失败,服务未绑定 | %s", e.getMessage()));
} }
mMyServiceConnection = null; mMyServiceConnection = null;
} }

View File

@@ -24,22 +24,30 @@ import java.util.List;
* 电池提醒核心服务 * 电池提醒核心服务
* 功能:管理前台服务生命周期、控制提醒线程启停、处理配置更新 * 功能:管理前台服务生命周期、控制提醒线程启停、处理配置更新
* 适配Java7 | API30 | 前台服务超时防护 | 电池优化忽略引导 * 适配Java7 | API30 | 前台服务超时防护 | 电池优化忽略引导
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 核心服务:实现电池监测、提醒控制与前台服务保活
*/ */
public class ControlCenterService extends Service { public class ControlCenterService extends Service {
// ================================== 静态常量区(置顶归类,消除魔法值)================================= // ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "ControlCenterService"; public static final String TAG = "ControlCenterService";
// 线程与服务常量
private static final long THREAD_STOP_TIMEOUT = 1000L; private static final long THREAD_STOP_TIMEOUT = 1000L;
private static final int SERVICE_RETURN_STICKY = START_STICKY; private static final int SERVICE_RETURN_STICKY = START_STICKY;
private static final int RUNNING_SERVICE_LIST_LIMIT = 100;
// 默认配置常量
private static final int DEFAULT_CHARGE_REMINDER_VALUE = 80; private static final int DEFAULT_CHARGE_REMINDER_VALUE = 80;
private static final int DEFAULT_USAGE_REMINDER_VALUE = 20; private static final int DEFAULT_USAGE_REMINDER_VALUE = 20;
private static final int DEFAULT_BATTERY_DETECT_INTERVAL = 1000; private static final int DEFAULT_BATTERY_DETECT_INTERVAL = 1000;
private static final int RUNNING_SERVICE_LIST_LIMIT = 100; // API版本常量
private static final int API_LEVEL_26 = Build.VERSION_CODES.O;
private static final int API_LEVEL_30 = Build.VERSION_CODES.R;
private static final int API_LEVEL_23 = Build.VERSION_CODES.M;
// ================================== 静态状态标记volatile保证多线程可见性================================= // ====================== 静态状态标记volatile保证多线程可见性 ======================
private static volatile boolean isServiceRunning = false; private static volatile boolean isServiceRunning = false;
private static volatile boolean mIsDestroyed = true; private static volatile boolean mIsDestroyed = true;
// ================================== 成员变量区(按功能分层:配置→核心组件→通知相关)================================= // ====================== 成员变量区(按功能分层:配置→核心组件→通知相关) ======================
// 服务控制配置 // 服务控制配置
private ControlCenterServiceBean mServiceControlBean; private ControlCenterServiceBean mServiceControlBean;
private AppConfigBean mCurrentConfigBean; private AppConfigBean mCurrentConfigBean;
@@ -50,37 +58,39 @@ public class ControlCenterService extends Service {
private NotificationManagerUtils mNotificationManager; private NotificationManagerUtils mNotificationManager;
private NotificationMessage mForegroundNotifyMsg; private NotificationMessage mForegroundNotifyMsg;
// ================================== 服务生命周期方法按执行顺序onCreate→onStartCommand→onBind→onDestroy================================= // ====================== 服务生命周期方法按执行顺序onCreate→onStartCommand→onBind→onDestroy ======================
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
LogUtils.d(TAG, "onCreate执行 | 线程=" + Thread.currentThread().getName() + " | 进程ID=" + android.os.Process.myPid()); LogUtils.d(TAG, String.format("onCreate() 执行 | 线程=%s | 进程ID=%d", Thread.currentThread().getName(), android.os.Process.myPid()));
runCoreServiceLogic(); runCoreServiceLogic();
LogUtils.d(TAG, "onCreate完成 | 前台状态=" + isServiceRunning + " | 服务启用=" + (mServiceControlBean != null && mServiceControlBean.isEnableService())); boolean serviceEnabled = mServiceControlBean != null && mServiceControlBean.isEnableService();
LogUtils.d(TAG, String.format("onCreate() 完成 | 前台状态=%b | 服务启用=%b", isServiceRunning, serviceEnabled));
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "onStartCommand执行 | startId=" + startId + " | action=" + (intent != null ? intent.getAction() : "null")); String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onStartCommand() 执行 | startId=%d | action=%s", startId, action));
loadLatestServiceControlConfig(); loadLatestServiceControlConfig();
runCoreServiceLogic(); runCoreServiceLogic();
int returnFlag = (mServiceControlBean != null && mServiceControlBean.isEnableService()) int returnFlag = (mServiceControlBean != null && mServiceControlBean.isEnableService())
? SERVICE_RETURN_STICKY ? SERVICE_RETURN_STICKY
: super.onStartCommand(intent, flags, startId); : super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "onStartCommand完成 | 返回策略=" + (returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT")); LogUtils.d(TAG, String.format("onStartCommand() 完成 | 返回策略=%s", returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT"));
return returnFlag; return returnFlag;
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind执行 | intent=" + intent); LogUtils.d(TAG, String.format("onBind() 执行 | intent=%s", intent));
return null; return null;
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
LogUtils.d(TAG, "onDestroy执行服务销毁流程启动"); LogUtils.d(TAG, "onDestroy() 执行:服务销毁流程启动");
super.onDestroy(); super.onDestroy();
// 资源释放顺序:前台服务 → 线程 → 广播接收器 → Handler → 通知 → 引用(避免内存泄漏) // 资源释放顺序:前台服务 → 线程 → 广播接收器 → Handler → 通知 → 引用(避免内存泄漏)
@@ -98,20 +108,20 @@ public class ControlCenterService extends Service {
isServiceRunning = false; isServiceRunning = false;
mIsDestroyed = true; mIsDestroyed = true;
LogUtils.d(TAG, "onDestroy完成服务销毁完成"); LogUtils.d(TAG, "onDestroy() 完成:服务销毁完成");
} }
// ================================== 核心业务逻辑(独立抽取,统一调用)================================= // ====================== 核心业务逻辑(独立抽取,统一调用) ======================
/** /**
* 服务核心运行逻辑在onCreate/onStartCommand复用 * 服务核心运行逻辑在onCreate/onStartCommand复用
* 避免重复初始化,保证前台服务优先启动 * 避免重复初始化,保证前台服务优先启动
*/ */
private synchronized void runCoreServiceLogic() { private synchronized void runCoreServiceLogic() {
LogUtils.d(TAG, "runCoreServiceLogic执行"); LogUtils.d(TAG, "runCoreServiceLogic() 执行");
loadLatestServiceControlConfig(); loadLatestServiceControlConfig();
boolean serviceEnabled = mServiceControlBean != null && mServiceControlBean.isEnableService(); boolean serviceEnabled = mServiceControlBean != null && mServiceControlBean.isEnableService();
LogUtils.d(TAG, "runCoreServiceLogic:服务启用=" + serviceEnabled + " | 已运行=" + isServiceRunning + " | 已销毁=" + mIsDestroyed); LogUtils.d(TAG, String.format("runCoreServiceLogic() | 服务启用=%b | 已运行=%b | 已销毁=%b", serviceEnabled, isServiceRunning, mIsDestroyed));
if (serviceEnabled && !isServiceRunning) { if (serviceEnabled && !isServiceRunning) {
isServiceRunning = true; isServiceRunning = true;
@@ -120,28 +130,28 @@ public class ControlCenterService extends Service {
if (initForegroundNotificationImmediately()) { if (initForegroundNotificationImmediately()) {
loadDefaultConfig(); loadDefaultConfig();
initServiceBusinessLogic(); initServiceBusinessLogic();
LogUtils.d(TAG, "runCoreServiceLogic核心组件初始化成功"); LogUtils.d(TAG, "runCoreServiceLogic() | 核心组件初始化成功");
} else { } else {
LogUtils.e(TAG, "runCoreServiceLogic前台通知初始化失败,终止业务"); LogUtils.e(TAG, "runCoreServiceLogic() | 前台通知初始化失败,终止业务");
stopForegroundService(); stopForegroundService();
isServiceRunning = false; isServiceRunning = false;
} }
} else { } else {
LogUtils.d(TAG, "runCoreServiceLogic无需执行核心逻辑"); LogUtils.d(TAG, "runCoreServiceLogic() | 无需执行核心逻辑");
} }
} }
// ================================== 前台通知管理优先执行防止API26+前台服务5秒超时================================= // ====================== 前台通知管理优先执行防止API26+前台服务5秒超时 ======================
/** /**
* 立即初始化前台通知防止API26+前台服务超时异常 * 立即初始化前台通知防止API26+前台服务超时异常
* @return true=成功 false=失败 * @return true=成功 false=失败
*/ */
private boolean initForegroundNotificationImmediately() { private boolean initForegroundNotificationImmediately() {
LogUtils.d(TAG, "initForegroundNotificationImmediately执行"); LogUtils.d(TAG, "initForegroundNotificationImmediately() 执行");
try { try {
if (mNotificationManager == null) { if (mNotificationManager == null) {
mNotificationManager = new NotificationManagerUtils(this); mNotificationManager = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initForegroundNotificationImmediately通知工具类初始化完成"); LogUtils.d(TAG, "initForegroundNotificationImmediately() | 通知工具类初始化完成");
} }
if (mForegroundNotifyMsg == null) { if (mForegroundNotifyMsg == null) {
@@ -149,15 +159,15 @@ public class ControlCenterService extends Service {
mForegroundNotifyMsg.setTitle("电池监测服务"); mForegroundNotifyMsg.setTitle("电池监测服务");
mForegroundNotifyMsg.setContent("后台运行中"); mForegroundNotifyMsg.setContent("后台运行中");
mForegroundNotifyMsg.setRemindMSG("service_running"); mForegroundNotifyMsg.setRemindMSG("service_running");
LogUtils.d(TAG, "initForegroundNotificationImmediately通知消息构建完成"); LogUtils.d(TAG, "initForegroundNotificationImmediately() | 通知消息构建完成");
} }
mNotificationManager.startForegroundServiceNotify(this, mForegroundNotifyMsg); mNotificationManager.startForegroundServiceNotify(this, mForegroundNotifyMsg);
ToastUtils.show("电池监测服务已启动"); ToastUtils.show("电池监测服务已启动");
LogUtils.d(TAG, "initForegroundNotificationImmediately前台通知发送成功 | ID=" + NotificationManagerUtils.NOTIFY_ID_FOREGROUND_SERVICE); LogUtils.d(TAG, String.format("initForegroundNotificationImmediately() | 前台通知发送成功 | ID=%d", NotificationManagerUtils.NOTIFY_ID_FOREGROUND_SERVICE));
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "initForegroundNotificationImmediately通知初始化异常", e); LogUtils.e(TAG, "initForegroundNotificationImmediately() | 通知初始化异常", e);
return false; return false;
} }
} }
@@ -166,27 +176,27 @@ public class ControlCenterService extends Service {
* 停止前台服务并取消通知 * 停止前台服务并取消通知
*/ */
private void stopForegroundService() { private void stopForegroundService() {
LogUtils.d(TAG, "stopForegroundService执行"); LogUtils.d(TAG, "stopForegroundService() 执行");
try { try {
stopForeground(true); stopForeground(true);
LogUtils.d(TAG, "stopForegroundService前台服务已停止,通知已取消"); LogUtils.d(TAG, "stopForegroundService() | 前台服务已停止,通知已取消");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "stopForegroundService停止异常", e); LogUtils.e(TAG, "stopForegroundService() | 停止异常", e);
} }
} }
// ================================== 配置管理(本地持久化+内存同步)================================= // ====================== 配置管理(本地持久化+内存同步) ======================
/** /**
* 加载本地最新服务控制配置 * 加载本地最新服务控制配置
*/ */
private void loadLatestServiceControlConfig() { private void loadLatestServiceControlConfig() {
LogUtils.d(TAG, "loadLatestServiceControlConfig执行"); LogUtils.d(TAG, "loadLatestServiceControlConfig() 执行");
ControlCenterServiceBean latestBean = ControlCenterServiceBean.loadBean(this, ControlCenterServiceBean.class); ControlCenterServiceBean latestBean = ControlCenterServiceBean.loadBean(this, ControlCenterServiceBean.class);
if (latestBean != null) { if (latestBean != null) {
mServiceControlBean = latestBean; mServiceControlBean = latestBean;
LogUtils.d(TAG, "loadLatestServiceControlConfig配置读取成功 | 启用=" + mServiceControlBean.isEnableService()); LogUtils.d(TAG, String.format("loadLatestServiceControlConfig() | 配置读取成功 | 启用=%b", mServiceControlBean.isEnableService()));
} else { } else {
LogUtils.w(TAG, "loadLatestServiceControlConfig本地无配置,沿用内存配置"); LogUtils.w(TAG, "loadLatestServiceControlConfig() | 本地无配置,沿用内存配置");
} }
} }
@@ -194,7 +204,7 @@ public class ControlCenterService extends Service {
* 加载默认业务配置(首次启动兜底) * 加载默认业务配置(首次启动兜底)
*/ */
private void loadDefaultConfig() { private void loadDefaultConfig() {
LogUtils.d(TAG, "loadDefaultConfig执行"); LogUtils.d(TAG, "loadDefaultConfig() 执行");
if (mCurrentConfigBean == null) { if (mCurrentConfigBean == null) {
mCurrentConfigBean = new AppConfigBean(); mCurrentConfigBean = new AppConfigBean();
mCurrentConfigBean.setEnableChargeReminder(true); mCurrentConfigBean.setEnableChargeReminder(true);
@@ -202,32 +212,33 @@ public class ControlCenterService extends Service {
mCurrentConfigBean.setEnableUsageReminder(true); mCurrentConfigBean.setEnableUsageReminder(true);
mCurrentConfigBean.setUsageReminderValue(DEFAULT_USAGE_REMINDER_VALUE); mCurrentConfigBean.setUsageReminderValue(DEFAULT_USAGE_REMINDER_VALUE);
mCurrentConfigBean.setBatteryDetectInterval(DEFAULT_BATTERY_DETECT_INTERVAL); mCurrentConfigBean.setBatteryDetectInterval(DEFAULT_BATTERY_DETECT_INTERVAL);
LogUtils.d(TAG, "loadDefaultConfig默认配置加载完成 | 充电阈值=" + DEFAULT_CHARGE_REMINDER_VALUE + " | 耗电阈值=" + DEFAULT_USAGE_REMINDER_VALUE + " | 检测间隔=" + DEFAULT_BATTERY_DETECT_INTERVAL + "ms"); LogUtils.d(TAG, String.format("loadDefaultConfig() | 默认配置加载完成 | 充电阈值=%d | 耗电阈值=%d | 检测间隔=%dms",
DEFAULT_CHARGE_REMINDER_VALUE, DEFAULT_USAGE_REMINDER_VALUE, DEFAULT_BATTERY_DETECT_INTERVAL));
} else { } else {
LogUtils.d(TAG, "loadDefaultConfig内存已有配置,无需加载"); LogUtils.d(TAG, "loadDefaultConfig() | 内存已有配置,无需加载");
} }
} }
// ================================== 业务组件初始化与销毁Handler/广播/线程等)================================= // ====================== 业务组件初始化与销毁Handler/广播/线程等) ======================
/** /**
* 初始化Handler等核心业务组件 * 初始化Handler等核心业务组件
*/ */
private void initServiceBusinessLogic() { private void initServiceBusinessLogic() {
LogUtils.d(TAG, "initServiceBusinessLogic执行"); LogUtils.d(TAG, "initServiceBusinessLogic() 执行");
// 初始化Handler // 初始化Handler
if (mServiceHandler == null) { if (mServiceHandler == null) {
mServiceHandler = new ControlCenterServiceHandler(this); mServiceHandler = new ControlCenterServiceHandler(this);
LogUtils.d(TAG, "initServiceBusinessLogicHandler初始化完成"); LogUtils.d(TAG, "initServiceBusinessLogic() | Handler初始化完成");
} else { } else {
LogUtils.d(TAG, "initServiceBusinessLogicHandler已存在"); LogUtils.d(TAG, "initServiceBusinessLogic() | Handler已存在");
} }
// 初始化广播接收器 // 初始化广播接收器
if (mControlCenterServiceReceiver == null) { if (mControlCenterServiceReceiver == null) {
mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this); mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this);
mControlCenterServiceReceiver.registerAction(this); mControlCenterServiceReceiver.registerAction(this);
LogUtils.d(TAG, "initServiceBusinessLogic广播接收器初始化并注册完成 | 接收器=" + mControlCenterServiceReceiver); LogUtils.d(TAG, "initServiceBusinessLogic() | 广播接收器初始化并注册完成");
} else { } else {
LogUtils.d(TAG, "initServiceBusinessLogic广播接收器已存在"); LogUtils.d(TAG, "initServiceBusinessLogic() | 广播接收器已存在");
} }
} }
@@ -235,13 +246,13 @@ public class ControlCenterService extends Service {
* 释放广播接收器资源 * 释放广播接收器资源
*/ */
private void releaseBroadcastReceiver() { private void releaseBroadcastReceiver() {
LogUtils.d(TAG, "releaseBroadcastReceiver执行"); LogUtils.d(TAG, "releaseBroadcastReceiver() 执行");
if (mControlCenterServiceReceiver != null) { if (mControlCenterServiceReceiver != null) {
mControlCenterServiceReceiver.release(); mControlCenterServiceReceiver.release();
mControlCenterServiceReceiver = null; mControlCenterServiceReceiver = null;
LogUtils.d(TAG, "releaseBroadcastReceiver广播接收器已释放"); LogUtils.d(TAG, "releaseBroadcastReceiver() | 广播接收器已释放");
} else { } else {
LogUtils.w(TAG, "releaseBroadcastReceiver广播接收器实例为空"); LogUtils.w(TAG, "releaseBroadcastReceiver() | 广播接收器实例为空");
} }
} }
@@ -249,13 +260,13 @@ public class ControlCenterService extends Service {
* 销毁Handler移除所有消息和回调防止内存泄漏 * 销毁Handler移除所有消息和回调防止内存泄漏
*/ */
private void destroyHandler() { private void destroyHandler() {
LogUtils.d(TAG, "destroyHandler执行"); LogUtils.d(TAG, "destroyHandler() 执行");
if (mServiceHandler != null) { if (mServiceHandler != null) {
mServiceHandler.removeCallbacksAndMessages(null); mServiceHandler.removeCallbacksAndMessages(null);
mServiceHandler = null; mServiceHandler = null;
LogUtils.d(TAG, "destroyHandlerHandler已销毁"); LogUtils.d(TAG, "destroyHandler() | Handler已销毁");
} else { } else {
LogUtils.w(TAG, "destroyHandlerHandler实例为空"); LogUtils.w(TAG, "destroyHandler() | Handler实例为空");
} }
} }
@@ -263,13 +274,13 @@ public class ControlCenterService extends Service {
* 释放通知工具类资源 * 释放通知工具类资源
*/ */
private void releaseNotificationResource() { private void releaseNotificationResource() {
LogUtils.d(TAG, "releaseNotificationResource执行"); LogUtils.d(TAG, "releaseNotificationResource() 执行");
if (mNotificationManager != null) { if (mNotificationManager != null) {
mNotificationManager.release(); mNotificationManager.release();
mNotificationManager = null; mNotificationManager = null;
LogUtils.d(TAG, "releaseNotificationResource通知资源已释放"); LogUtils.d(TAG, "releaseNotificationResource() | 通知资源已释放");
} else { } else {
LogUtils.w(TAG, "releaseNotificationResource通知工具类实例为空"); LogUtils.w(TAG, "releaseNotificationResource() | 通知工具类实例为空");
} }
} }
@@ -277,37 +288,37 @@ public class ControlCenterService extends Service {
* 置空所有引用,防止内存泄漏 * 置空所有引用,防止内存泄漏
*/ */
private void clearAllReferences() { private void clearAllReferences() {
LogUtils.d(TAG, "clearAllReferences执行"); LogUtils.d(TAG, "clearAllReferences() 执行");
mForegroundNotifyMsg = null; mForegroundNotifyMsg = null;
mServiceControlBean = null; mServiceControlBean = null;
LogUtils.d(TAG, "clearAllReferences引用清理完成"); LogUtils.d(TAG, "clearAllReferences() | 引用清理完成");
} }
// ================================== 外部调用接口(静态方法,提供服务启停/配置更新入口)================================= // ====================== 外部调用接口(静态方法,提供服务启停/配置更新入口) ======================
/** /**
* 外部启动服务的统一入口 * 外部启动服务的统一入口
* @param context 上下文 * @param context 上下文
*/ */
public static void startControlCenterService(Context context) { public static void startControlCenterService(Context context) {
LogUtils.d(TAG, "startControlCenterService执行 | context=" + context); LogUtils.d(TAG, String.format("startControlCenterService() 执行 | context=%s", context));
if (context == null) { if (context == null) {
LogUtils.e(TAG, "startControlCenterServiceContext为空启动失败"); LogUtils.e(TAG, "startControlCenterService() | Context为空启动失败");
return; return;
} }
// 保存启用配置 // 保存启用配置
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(true); ControlCenterServiceBean controlBean = new ControlCenterServiceBean(true);
ControlCenterServiceBean.saveBean(context, controlBean); ControlCenterServiceBean.saveBean(context, controlBean);
LogUtils.d(TAG, "startControlCenterService服务启用配置已保存 | 配置=" + controlBean); LogUtils.d(TAG, "startControlCenterService() | 服务启用配置已保存");
// 启动服务区分API版本 // 启动服务区分API版本
Intent intent = new Intent(context, ControlCenterService.class); Intent intent = new Intent(context, ControlCenterService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
context.startForegroundService(intent); context.startForegroundService(intent);
LogUtils.d(TAG, "startControlCenterService以前台服务方式启动API26+"); LogUtils.d(TAG, "startControlCenterService() | 以前台服务方式启动API26+");
} else { } else {
context.startService(intent); context.startService(intent);
LogUtils.d(TAG, "startControlCenterService以普通服务方式启动API26-"); LogUtils.d(TAG, "startControlCenterService() | 以普通服务方式启动API26-");
} }
} }
@@ -316,69 +327,67 @@ public class ControlCenterService extends Service {
* @param context 上下文 * @param context 上下文
*/ */
public static void stopControlCenterService(Context context) { public static void stopControlCenterService(Context context) {
LogUtils.d(TAG, "stopControlCenterService执行 | context=" + context); LogUtils.d(TAG, String.format("stopControlCenterService() 执行 | context=%s", context));
if (context == null) { if (context == null) {
LogUtils.e(TAG, "stopControlCenterServiceContext为空停止失败"); LogUtils.e(TAG, "stopControlCenterService() | Context为空停止失败");
return; return;
} }
// 保存停用配置 // 保存停用配置
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(false); ControlCenterServiceBean controlBean = new ControlCenterServiceBean(false);
ControlCenterServiceBean.saveBean(context, controlBean); ControlCenterServiceBean.saveBean(context, controlBean);
LogUtils.d(TAG, "stopControlCenterService服务停用配置已保存 | 配置=" + controlBean); LogUtils.d(TAG, "stopControlCenterService() | 服务停用配置已保存");
// 停止服务 // 停止服务
Intent intent = new Intent(context, ControlCenterService.class); Intent intent = new Intent(context, ControlCenterService.class);
context.stopService(intent); context.stopService(intent);
LogUtils.d(TAG, "stopControlCenterService停止指令已发送"); LogUtils.d(TAG, "stopControlCenterService() | 停止指令已发送");
} }
/** /**
* 外部更新配置并触发线程重启 * 外部更新配置并触发线程重启
* @param context 上下文 * @param context 上下文
*/ */
public static void sendAppConfigStatusUpdateMessage(Context context) { public static void sendAppConfigStatusUpdateMessage(Context context) {
LogUtils.d(TAG, "sendAppConfigStatusUpdateMessage执行 | context=" + context); LogUtils.d(TAG, String.format("sendAppConfigStatusUpdateMessage() 执行 | context=%s", context));
if (context == null) { if (context == null) {
LogUtils.e(TAG, "sendAppConfigStatusUpdateMessage参数为空,更新失败"); LogUtils.e(TAG, "sendAppConfigStatusUpdateMessage() | 参数为空,更新失败");
return; return;
} }
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED); Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
// 新增:发送广播并记录结果 context.sendBroadcast(intent);
context.sendBroadcast(intent); LogUtils.d(TAG, String.format("sendAppConfigStatusUpdateMessage() | 配置更新广播发送 | action=%s", ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED));
LogUtils.d(TAG, "sendAppConfigStatusUpdateMessage配置更新广播发送 action=" + ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED); }
}
/** /**
* 检查并引导用户开启忽略电池优化API23+ * 检查并引导用户开启忽略电池优化API23+
* @param context 上下文 * @param context 上下文
*/ */
public static void checkIgnoreBatteryOptimization(Context context) { public static void checkIgnoreBatteryOptimization(Context context) {
LogUtils.d(TAG, "checkIgnoreBatteryOptimization执行 | context=" + context); LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization() 执行 | context=%s", context));
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (context == null || Build.VERSION.SDK_INT < API_LEVEL_23) {
LogUtils.w(TAG, "checkIgnoreBatteryOptimization无需检查Context为空或API<23"); LogUtils.w(TAG, "checkIgnoreBatteryOptimization() | 无需检查Context为空或API<23");
return; return;
} }
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (powerManager == null) { if (powerManager == null) {
LogUtils.e(TAG, "checkIgnoreBatteryOptimizationPowerManager获取失败"); LogUtils.e(TAG, "checkIgnoreBatteryOptimization() | PowerManager获取失败");
return; return;
} }
String packageName = context.getPackageName(); String packageName = context.getPackageName();
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(packageName); boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(packageName);
LogUtils.d(TAG, "checkIgnoreBatteryOptimization已忽略电池优化=" + isIgnored); LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization() | 已忽略电池优化=%b", isIgnored));
if (!isIgnored) { if (!isIgnored) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName)); intent.setData(Uri.parse("package:" + packageName));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent); context.startActivity(intent);
LogUtils.d(TAG, "checkIgnoreBatteryOptimization已跳转至系统设置页 | package=" + packageName); LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization() | 已跳转至系统设置页 | package=%s", packageName));
} }
} }
@@ -389,15 +398,15 @@ public class ControlCenterService extends Service {
* @return true=运行中 false=未运行 * @return true=运行中 false=未运行
*/ */
private static boolean isServiceRunning(Context context, Class<?> serviceClass) { private static boolean isServiceRunning(Context context, Class<?> serviceClass) {
LogUtils.d(TAG, "isServiceRunning执行 | context=" + context + " | service=" + (serviceClass != null ? serviceClass.getName() : "null")); LogUtils.d(TAG, String.format("isServiceRunning() 执行 | context=%s | service=%s", context, serviceClass != null ? serviceClass.getName() : "null"));
if (context == null || serviceClass == null) { if (context == null || serviceClass == null) {
LogUtils.e(TAG, "isServiceRunning参数为空"); LogUtils.e(TAG, "isServiceRunning() | 参数为空");
return false; return false;
} }
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) { if (am == null) {
LogUtils.e(TAG, "isServiceRunningActivityManager获取失败"); LogUtils.e(TAG, "isServiceRunning() | ActivityManager获取失败");
return false; return false;
} }
@@ -405,7 +414,7 @@ public class ControlCenterService extends Service {
String packageName = context.getPackageName(); String packageName = context.getPackageName();
String serviceClassName = serviceClass.getName(); String serviceClassName = serviceClass.getName();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= API_LEVEL_30) {
// API30+ 禁止获取其他应用服务,通过进程状态判断 // API30+ 禁止获取其他应用服务,通过进程状态判断
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses(); List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes != null) { if (processes != null) {
@@ -418,7 +427,7 @@ public class ControlCenterService extends Service {
} }
} }
} }
LogUtils.d(TAG, "isServiceRunningAPI30+ 判断结果=" + isRunning); LogUtils.d(TAG, String.format("isServiceRunning() | API30+ 判断结果=%b", isRunning));
} else { } else {
// API30- 通过服务列表判断 // API30- 通过服务列表判断
List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(RUNNING_SERVICE_LIST_LIMIT); List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(RUNNING_SERVICE_LIST_LIMIT);
@@ -430,13 +439,13 @@ public class ControlCenterService extends Service {
} }
} }
} }
LogUtils.d(TAG, "isServiceRunningAPI30- 判断结果=" + isRunning); LogUtils.d(TAG, String.format("isServiceRunning() | API30- 判断结果=%b", isRunning));
} }
// 兜底判断:配置启用状态 // 兜底判断:配置启用状态
if (!isRunning) { if (!isRunning) {
isRunning = isServiceStarted(context, serviceClass); isRunning = isServiceStarted(context, serviceClass);
LogUtils.d(TAG, "isServiceRunning兜底判断结果=" + isRunning); LogUtils.d(TAG, String.format("isServiceRunning() | 兜底判断结果=%b", isRunning));
} }
return isRunning; return isRunning;
} }
@@ -445,33 +454,35 @@ public class ControlCenterService extends Service {
* 兜底判断服务是否已启动(通过配置文件) * 兜底判断服务是否已启动(通过配置文件)
*/ */
private static boolean isServiceStarted(Context context, Class<?> serviceClass) { private static boolean isServiceStarted(Context context, Class<?> serviceClass) {
LogUtils.d(TAG, "isServiceStarted执行"); LogUtils.d(TAG, "isServiceStarted() 执行");
try { try {
ControlCenterServiceBean controlBean = ControlCenterServiceBean.loadBean(context, ControlCenterServiceBean.class); ControlCenterServiceBean controlBean = ControlCenterServiceBean.loadBean(context, ControlCenterServiceBean.class);
return controlBean != null && controlBean.isEnableService(); return controlBean != null && controlBean.isEnableService();
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "isServiceStarted兜底判断异常", e); LogUtils.e(TAG, "isServiceStarted() | 兜底判断异常", e);
return false; return false;
} }
} }
// ================================== 业务方法(配置更新/电池状态回调)================================= // ====================== 业务方法(配置更新/电池状态回调) ======================
/** /**
* 接收外部配置更新,同步到提醒线程 * 接收外部配置更新,同步到提醒线程
* @param latestConfig 最新配置 * @param latestConfig 最新配置
*/ */
public void notifyAppConfigUpdate(AppConfigBean latestConfig) { public void notifyAppConfigUpdate(AppConfigBean latestConfig) {
LogUtils.d(TAG, "notifyAppConfigUpdate执行 | 充电阈值=" + (latestConfig != null ? latestConfig.getChargeReminderValue() : null) + " | 耗电阈值=" + (latestConfig != null ? latestConfig.getUsageReminderValue() : null)); int chargeThreshold = latestConfig != null ? latestConfig.getChargeReminderValue() : -1;
int usageThreshold = latestConfig != null ? latestConfig.getUsageReminderValue() : -1;
LogUtils.d(TAG, String.format("notifyAppConfigUpdate() 执行 | 充电阈值=%d | 耗电阈值=%d", chargeThreshold, usageThreshold));
if (latestConfig != null && mServiceHandler != null) { if (latestConfig != null && mServiceHandler != null) {
mCurrentConfigBean = latestConfig; mCurrentConfigBean = latestConfig;
RemindThread.startRemindThreadWithAppConfig(this, mServiceHandler, latestConfig); RemindThread.startRemindThreadWithAppConfig(this, mServiceHandler, latestConfig);
LogUtils.d(TAG, "notifyAppConfigUpdate配置已同步到提醒线程"); LogUtils.d(TAG, "notifyAppConfigUpdate() | 配置已同步到提醒线程");
} else { } else {
LogUtils.e(TAG, "notifyAppConfigUpdate参数为空,同步失败 | latestConfig=" + latestConfig + " | mServiceHandler=" + mServiceHandler); LogUtils.e(TAG, String.format("notifyAppConfigUpdate() | 参数为空,同步失败 | latestConfig=%s | mServiceHandler=%s", latestConfig, mServiceHandler));
} }
} }
// ================================== Getter 方法按需开放避免冗余Setter================================= // ====================== Getter 方法按需开放避免冗余Setter ======================
public ControlCenterServiceBean getServiceControlBean() { public ControlCenterServiceBean getServiceControlBean() {
return mServiceControlBean; return mServiceControlBean;
} }

View File

@@ -0,0 +1,83 @@
package cc.winboll.studio.powerbell.services;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.TTSSpeakTextBean;
import cc.winboll.studio.powerbell.utils.TextToSpeechUtils;
import java.util.ArrayList;
/**
* TTS 语音播放后台服务组件
* 适配Java7 语法规范 | Android API30 系统版本
* 功能后台承载TTS语音播放解耦页面生命周期避免页面销毁中断播放
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:12
*/
public class TTSPlayService extends Service {
// ====================================== 常量区 - 静态全局常量 置顶排序 ======================================
public static final String TAG = "TTSPlayService";
public static final String EXTRA_SPEAKDATA = "EXTRA_SPEAKDATA";
// ====================================== 对外公开静态快捷调用方法【新增核心】======================================
/**
* 公开静态方法一键启动TTS播放服务播放指定文本内容
* @param context 上下文对象
* @param speakText 需要播放的语音文本内容
*/
public static void startPlayTTS(Context context, String speakText) {
LogUtils.d(TAG, "【startPlayTTS】静态快捷调用方法 | 入参Context=" + context + " | 播放文本=" + speakText);
if (context != null && speakText != null && !speakText.isEmpty()) {
// 初始化播放数据集合
ArrayList<TTSSpeakTextBean> ttsBeanList = new ArrayList<>();
// 添加播放文本延迟时间为0无延迟立即播放
ttsBeanList.add(new TTSSpeakTextBean(0, speakText));
LogUtils.d(TAG, "【startPlayTTS】封装播放数据完成创建启动服务意图");
// 创建意图并封装序列化参数
Intent intent = new Intent(context, TTSPlayService.class);
intent.putExtra(EXTRA_SPEAKDATA, ttsBeanList);
// 启动当前服务
context.startService(intent);
LogUtils.d(TAG, "【startPlayTTS】已调用startServiceTTS播放服务启动成功");
} else {
LogUtils.d(TAG, "【startPlayTTS】上下文为空 或 播放文本为空/空字符串,跳过启动服务");
}
}
// ====================================== 生命周期方法 - 绑定服务 (无绑定逻辑) ======================================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "【onBind】服务绑定方法调用入参Intent" + intent);
return null;
}
// ====================================== 生命周期方法 - 启动服务【核心方法】 ======================================
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "【onStartCommand】服务启动方法调用 | 入参Intent" + intent + " | flags" + flags + " | startId" + startId);
// 解析播放数据并执行播放
if (intent != null) {
LogUtils.d(TAG, "【onStartCommand】Intent不为空开始解析序列化播放数据");
ArrayList<TTSSpeakTextBean> listTTSSpeakTextBean = (ArrayList<TTSSpeakTextBean>) intent.getSerializableExtra(EXTRA_SPEAKDATA);
if (listTTSSpeakTextBean != null && listTTSSpeakTextBean.size() > 0) {
LogUtils.d(TAG, "【onStartCommand】解析播放数据成功队列长度" + listTTSSpeakTextBean.size() + "调用TTS播放工具类");
TextToSpeechUtils.getInstance(this).speekTTSList(listTTSSpeakTextBean);
} else {
LogUtils.d(TAG, "【onStartCommand】播放数据为空/长度0跳过语音播放逻辑");
}
} else {
LogUtils.d(TAG, "【onStartCommand】Intent为空无播放数据可解析");
}
// 返回默认值,保持原服务启动策略不变
int result = super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "【onStartCommand】方法执行完成返回值" + result);
return result;
}
}

View File

@@ -0,0 +1,164 @@
package cc.winboll.studio.powerbell.services;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/**
* 智能电池服务(充电/放电状态处理)
* 适配Java7 语法规范 | Android API30 系统版本
* 功能:接收充电/放电状态指令,根据不同状态执行对应业务任务
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:29
*/
public class ThoughtfulService extends Service {
// ====================================== 常量区 - 置顶排序 ======================================
public static final String TAG = "ThoughtfulService";
/** Intent传递 服务类型 的Key值 */
public static final String EXTRA_SERVICE_TYPE = "EXTRA_SERVICE_TYPE";
// ====================================== 枚举类 - 服务类型 充电/放电状态 ======================================
/**
* 服务执行类型枚举
* CHARGE_STATE : 充电状态服务
* DISCHARGE_STATE : 放电(耗电)状态服务
*/
public enum ServiceType {
CHARGE_STATE, //充电状态服务
DISCHARGE_STATE //放电状态服务
}
// ====================================== 对外公开静态启动函数【新增核心】入参Context + 枚举 ======================================
/**
* 公开静态方法:传入上下文+服务类型枚举,一键构建意图并启动当前服务
* @param context 上下文对象
* @param serviceType 服务类型枚举【充电/放电】
*/
public static void startServiceWithType(Context context, ServiceType serviceType) {
LogUtils.d(TAG, "【startServiceWithType】静态启动方法调用 | Context=" + context + " | ServiceType=" + (serviceType == null ? "null" : serviceType.name()));
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(context, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
// 对应TTS服务提醒没有启用就退出
if((serviceType == ServiceType.CHARGE_STATE && !thoughtfulServiceBean.isEnableChargeTts())
||(serviceType == ServiceType.DISCHARGE_STATE && !thoughtfulServiceBean.isEnableUsePowerTts())){
return;
}
// 判空健壮性校验
if (context != null && serviceType != null) {
// 构建意图 + 封装枚举参数
Intent intent = new Intent(context, ThoughtfulService.class);
intent.putExtra(EXTRA_SERVICE_TYPE, serviceType);
// 启动服务
context.startService(intent);
LogUtils.d(TAG, "【startServiceWithType】服务启动成功执行[" + serviceType.name() + "]任务");
} else {
LogUtils.d(TAG, "【startServiceWithType】上下文为空 或 服务类型枚举为空,跳过启动服务");
}
}
// ====================================== 生命周期方法 - 绑定服务 (原逻辑保留) ======================================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "【onBind】服务绑定方法调用入参Intent" + intent);
return null;
}
// ====================================== 生命周期方法 - 启动服务【核心逻辑】接收枚举+分支执行任务 ======================================
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "【onStartCommand】服务启动方法调用 | intent=" + intent + " | flags=" + flags + " | startId=" + startId);
// 判断意图非空,解析服务类型参数
if (intent != null) {
LogUtils.d(TAG, "【onStartCommand】Intent不为空开始解析服务类型枚举参数");
// 获取传递的服务类型枚举
ServiceType serviceType = (ServiceType) intent.getSerializableExtra(EXTRA_SERVICE_TYPE);
// 根据服务类型,执行对应任务
if (serviceType != null) {
LogUtils.d(TAG, "【onStartCommand】解析到服务类型" + serviceType.name());
switch (serviceType) {
case CHARGE_STATE:
// 执行【充电状态】对应的业务任务
executeChargeStateTask();
break;
case DISCHARGE_STATE:
// 执行【放电状态】对应的业务任务
executeDischargeStateTask();
break;
default:
LogUtils.d(TAG, "【onStartCommand】未知的服务类型不执行任何任务");
break;
}
} else {
LogUtils.d(TAG, "【onStartCommand】未解析到有效服务类型参数参数为空");
}
} else {
LogUtils.d(TAG, "【onStartCommand】启动服务的Intent为空直接返回");
}
// 返回默认策略,与原生逻辑一致
int result = super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "【onStartCommand】服务执行完成返回值" + result);
return result;
}
// ====================================== 私有业务方法 充电/放电 分任务执行 ======================================
/**
* 执行【充电状态】的业务任务
* 可在此方法内编写 充电时的逻辑(语音提醒/电量监控/弹窗等)
*/
private void executeChargeStateTask() {
LogUtils.d(TAG, "【executeChargeStateTask】执行【充电状态】业务任务 >>> ");
//ToastUtils.show("【executeChargeStateTask】执行【充电状态】业务任务 >>> ");
// TODO 此处添加充电状态需要执行的业务逻辑代码
// 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(this).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
return;
}
if (latestConfig.isEnableChargeReminder()) {
int nChargeReminderValue = latestConfig.getChargeReminderValue();
String szRemind = String.format("额定充电提醒已启用,额定值为百分之%d。", nChargeReminderValue);
szRemind = szRemind + szRemind + szRemind;
TTSPlayService.startPlayTTS(this, szRemind);
}
}
/**
* 执行【放电(耗电)状态】的业务任务
* 可在此方法内编写 放电时的逻辑(语音提醒/电量监控/弹窗等)
*/
private void executeDischargeStateTask() {
LogUtils.d(TAG, "【executeDischargeStateTask】执行【放电状态】业务任务 >>> ");
//ToastUtils.show("【executeDischargeStateTask】执行【放电状态】业务任务 >>> ");
// TODO 此处添加放电状态需要执行的业务逻辑代码
// 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(this).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
return;
}
if (latestConfig.isEnableUsageReminder()) {
int nUsageReminderValue = latestConfig.getUsageReminderValue();
String szRemind = String.format("电量不足提醒已启用,低电值为百分之%d。", nUsageReminderValue);
//szRemind = szRemind + szRemind + szRemind;
TTSPlayService.startPlayTTS(this, szRemind);
}
}
}

View File

@@ -3,20 +3,23 @@ package cc.winboll.studio.powerbell.threads;
import android.content.Context; import android.content.Context;
import android.os.Message; import android.os.Message;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler; import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean; import cc.winboll.studio.powerbell.models.AppConfigBean;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
* 提醒线程(多实例列表管理) * 电量通知提醒线程(多实例列表管理)
* 功能:管理充电/耗电提醒逻辑触发条件时向Handler发送提醒消息 * 功能:管理充电/耗电提醒逻辑触发条件时向Handler发送提醒消息
* 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步 * 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步
* 对外接口:{@link #startRemindThreadWithAppConfig(Context, ControlCenterServiceHandler, AppConfigBean)}、{ * 对外接口:{@link #startRemindThreadWithAppConfig(Context, ControlCenterServiceHandler, AppConfigBean)}、
* @link #startRemindThreadWithBatteryInfo(Context, ControlCenterServiceHandler, boolean, int)}、{@link #stopRemindThread()} * {@link #startRemindThreadWithBatteryInfo(Context, ControlCenterServiceHandler, boolean, int)}、{@link #stopRemindThread()}
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 电量通知提醒线程
*/ */
public class RemindThread extends Thread { public class RemindThread extends Thread {
// ================================== 静态常量区(置顶归类,消除魔法值)================================= // ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "RemindThread"; public static final String TAG = "RemindThread";
// 时间常量 (ms) // 时间常量 (ms)
@@ -24,7 +27,6 @@ public class RemindThread extends Thread {
private static final long THREAD_JOIN_TIMEOUT = 1000L; private static final long THREAD_JOIN_TIMEOUT = 1000L;
// 状态常量 // 状态常量
private static final int INVALID_BATTERY_VALUE = -1;
private static final int BATTERY_LEVEL_MIN = 0; private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100; private static final int BATTERY_LEVEL_MAX = 100;
@@ -32,10 +34,10 @@ public class RemindThread extends Thread {
private static final String REMIND_TYPE_CHARGE = "+"; private static final String REMIND_TYPE_CHARGE = "+";
private static final String REMIND_TYPE_USAGE = "-"; private static final String REMIND_TYPE_USAGE = "-";
// ================================== 静态成员(多实例列表管理)================================= // ====================== 静态成员(多实例列表管理) ======================
private static volatile ArrayList<RemindThread> sRemindThreadList; private static volatile ArrayList<RemindThread> sRemindThreadList;
// ================================== 成员变量区按功能分层volatile保证多线程可见性================================= // ====================== 成员变量区按功能分层volatile保证多线程可见性 ======================
// 并发安全锁(保护线程状态变更) // 并发安全锁(保护线程状态变更)
private final Object mRemindLock = new Object(); private final Object mRemindLock = new Object();
@@ -53,19 +55,18 @@ public class RemindThread extends Thread {
private volatile long sleepTime; private volatile long sleepTime;
private volatile int chargeReminderValue; private volatile int chargeReminderValue;
private volatile int usageReminderValue; private volatile int usageReminderValue;
private volatile int quantityOfElectricity;
private volatile boolean isCharging; private volatile boolean isCharging;
// ================================== 私有构造器(禁止外部实例化)================================= // ====================== 私有构造器(禁止外部实例化) ======================
private RemindThread(Context context, ControlCenterServiceHandler handler) { private RemindThread(Context context, ControlCenterServiceHandler handler) {
LogUtils.d(TAG, "构造器调用 | context=" + context + " | handler=" + handler); LogUtils.d(TAG, String.format("RemindThread() 构造器调用 | context=%s | handler=%s", context, handler));
this.mContext = context.getApplicationContext(); this.mContext = context.getApplicationContext();
this.mwrControlCenterServiceHandler = new WeakReference<>(handler); this.mwrControlCenterServiceHandler = new WeakReference<>(handler);
resetThreadStateInternal(); resetThreadStateInternal();
LogUtils.d(TAG, "构造完成 | threadId=" + getId() + " | 初始状态重置成功"); LogUtils.d(TAG, String.format("RemindThread() 构造完成 | threadId=%d | 初始状态重置成功", getId()));
} }
// ================================== 对外公开静态接口(多实例列表管理)================================= // ====================== 对外公开静态接口(多实例列表管理) ======================
/** /**
* 启动提醒线程,同步最新配置 * 启动提醒线程,同步最新配置
* 逻辑:停止所有旧线程 → 创建新线程 → 加入列表管理 * 逻辑:停止所有旧线程 → 创建新线程 → 加入列表管理
@@ -75,15 +76,15 @@ public class RemindThread extends Thread {
* @return true: 启动成功false: 入参非法 * @return true: 启动成功false: 入参非法
*/ */
public static boolean startRemindThreadWithAppConfig(Context context, ControlCenterServiceHandler handler, AppConfigBean config) { public static boolean startRemindThreadWithAppConfig(Context context, ControlCenterServiceHandler handler, AppConfigBean config) {
LogUtils.d(TAG, "startRemindThreadWithAppConfig调用 | context=" + context + " | handler=" + handler + " | config=" + config); LogUtils.d(TAG, String.format("startRemindThreadWithAppConfig() 调用 | context=%s | handler=%s | config=%s", context, handler, config));
// 入参严格校验 // 入参严格校验
if (context == null || handler == null || config == null) { if (context == null || handler == null || config == null) {
LogUtils.e(TAG, "启动失败:入参为空 | context=" + context + " | handler=" + handler + " | config=" + config); LogUtils.e(TAG, String.format("启动失败:入参为空 | context=%s | handler=%s | config=%s", context, handler, config));
return false; return false;
} }
// 初始化线程列表 // 初始化线程列表(双重校验锁)
if (sRemindThreadList == null) { if (sRemindThreadList == null) {
synchronized (RemindThread.class) { synchronized (RemindThread.class) {
if (sRemindThreadList == null) { if (sRemindThreadList == null) {
@@ -102,50 +103,7 @@ public class RemindThread extends Thread {
newRemindThread.isExist = false; newRemindThread.isExist = false;
newRemindThread.start(); newRemindThread.start();
sRemindThreadList.add(newRemindThread); sRemindThreadList.add(newRemindThread);
LogUtils.d(TAG, "新线程启动成功 | threadId=" + newRemindThread.getId() + " | 列表大小=" + sRemindThreadList.size()); LogUtils.d(TAG, String.format("新线程启动成功 | threadId=%d | 列表大小=%d", newRemindThread.getId(), sRemindThreadList.size()));
return true;
}
/**
* 启动提醒线程,同步电池状态信息
* 逻辑:停止所有旧线程 → 创建新线程 → 同步电池状态 → 加入列表管理
* @param context 上下文(非空)
* @param handler 服务处理器(非空)
* @param isCharging 充电状态
* @param batteryLevel 当前电量
* @return true: 启动成功false: 入参非法
*/
public static boolean startRemindThreadWithBatteryInfo(Context context, ControlCenterServiceHandler handler, boolean isCharging, int batteryLevel) {
LogUtils.d(TAG, "startRemindThreadWithBatteryInfo调用 | context=" + context + " | handler=" + handler + " | isCharging=" + isCharging + " | batteryLevel=" + batteryLevel);
// 入参严格校验
if (context == null || handler == null) {
LogUtils.e(TAG, "启动失败:入参为空 | context=" + context + " | handler=" + handler);
return false;
}
// 初始化线程列表
if (sRemindThreadList == null) {
synchronized (RemindThread.class) {
if (sRemindThreadList == null) {
sRemindThreadList = new ArrayList<RemindThread>();
LogUtils.d(TAG, "线程列表初始化完成");
}
}
}
// 停止所有旧线程
stopAllOldThreadsInternal();
// 创建并启动新线程
RemindThread newRemindThread = new RemindThread(context, handler);
// 同步电池状态
newRemindThread.isCharging = isCharging;
newRemindThread.quantityOfElectricity = Math.min(Math.max(batteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
newRemindThread.isExist = false;
newRemindThread.start();
sRemindThreadList.add(newRemindThread);
LogUtils.d(TAG, "新线程启动成功 | threadId=" + newRemindThread.getId() + " | 电池状态同步完成");
return true; return true;
} }
@@ -153,7 +111,8 @@ public class RemindThread extends Thread {
* 安全停止所有线程,清空列表 * 安全停止所有线程,清空列表
*/ */
public static void stopRemindThread() { public static void stopRemindThread() {
LogUtils.d(TAG, "stopRemindThread调用 | 列表存在=" + (sRemindThreadList != null) + " | 列表大小=" + (sRemindThreadList != null ? sRemindThreadList.size() : 0)); int listSize = sRemindThreadList != null ? sRemindThreadList.size() : 0;
LogUtils.d(TAG, String.format("stopRemindThread() 调用 | 列表存在=%b | 列表大小=%d", sRemindThreadList != null, listSize));
if (sRemindThreadList == null || sRemindThreadList.isEmpty()) { if (sRemindThreadList == null || sRemindThreadList.isEmpty()) {
LogUtils.w(TAG, "停止失败:线程列表为空"); LogUtils.w(TAG, "停止失败:线程列表为空");
return; return;
@@ -162,14 +121,14 @@ public class RemindThread extends Thread {
// 标记所有线程退出 // 标记所有线程退出
for (RemindThread remindThread : sRemindThreadList) { for (RemindThread remindThread : sRemindThreadList) {
remindThread.isExist = true; remindThread.isExist = true;
LogUtils.d(TAG, "标记线程退出 | threadId=" + remindThread.getId()); LogUtils.d(TAG, String.format("标记线程退出 | threadId=%d", remindThread.getId()));
} }
// 清空列表 // 清空列表
sRemindThreadList.clear(); sRemindThreadList.clear();
LogUtils.d(TAG, "所有线程已标记退出,列表已清空"); LogUtils.d(TAG, "所有线程已标记退出,列表已清空");
} }
// ================================== 私有静态辅助方法(多实例管理)================================= // ====================== 私有静态辅助方法(多实例管理) ======================
/** /**
* 停止所有旧线程并清空列表 * 停止所有旧线程并清空列表
*/ */
@@ -181,67 +140,70 @@ public class RemindThread extends Thread {
// 标记所有旧线程退出 // 标记所有旧线程退出
for (RemindThread remindThread : sRemindThreadList) { for (RemindThread remindThread : sRemindThreadList) {
remindThread.isExist = true; remindThread.isExist = true;
LogUtils.d(TAG, "标记旧线程退出 | threadId=" + remindThread.getId()); LogUtils.d(TAG, String.format("标记旧线程退出 | threadId=%d", remindThread.getId()));
} }
// 清空旧线程列表 // 清空旧线程列表
sRemindThreadList.clear(); sRemindThreadList.clear();
LogUtils.d(TAG, "旧线程已全部标记退出,列表已清空"); LogUtils.d(TAG, "旧线程已全部标记退出,列表已清空");
} }
// ================================== 线程核心运行逻辑================================= // ====================== 线程核心运行逻辑 ======================
@Override @Override
public void run() { public void run() {
LogUtils.d(TAG, "run执行 | threadId=" + getId() + " | 状态=" + getState()); LogUtils.d(TAG, String.format("run() 执行 | threadId=%d | 状态=%s", getId(), getState()));
// 初始化提醒状态(加锁保护,避免多线程竞争) // 初始化提醒状态(加锁保护,避免多线程竞争)
synchronized (mRemindLock) { synchronized (mRemindLock) {
if (isReminding) { if (isReminding) {
LogUtils.w(TAG, "线程已在提醒状态,退出运行 | threadId=" + getId()); LogUtils.w(TAG, String.format("线程已在提醒状态,退出运行 | threadId=%d", getId()));
return; return;
} }
isReminding = true; isReminding = true;
} }
// 核心电量检测循环 // 核心电量检测循环
LogUtils.d(TAG, "进入电量检测循环 | 休眠时间=" + sleepTime + "ms | threadId=" + getId()); LogUtils.d(TAG, String.format("进入电量检测循环 | 休眠时间=%dms | threadId=%d", sleepTime, getId()));
while (!isExist) { while (!isExist) {
try { try {
// 快速退出判断 // 快速退出判断
if (isExist) break; if (isExist) break;
// 电量有效性校验非0-100视为无效退出电量提醒线程 // 电量有效性校验非0-100视为无效退出电量提醒线程
if (quantityOfElectricity < BATTERY_LEVEL_MIN || quantityOfElectricity > BATTERY_LEVEL_MAX) { if (App.sQuantityOfElectricity < BATTERY_LEVEL_MIN || App.sQuantityOfElectricity > BATTERY_LEVEL_MAX) {
LogUtils.w(TAG, "电量无效,退出电量提醒线程 | 当前电量=" + quantityOfElectricity + " | threadId=" + getId()); LogUtils.w(TAG, String.format("电量无效,退出电量提醒线程 | 当前电量=%d | threadId=%d", App.sQuantityOfElectricity, getId()));
break; break;
} }
// 充电/耗电提醒触发逻辑 // 充电/耗电提醒触发逻辑
if (isCharging && isEnableChargeReminder && quantityOfElectricity >= chargeReminderValue) { boolean chargeRemindTrigger = isCharging && isEnableChargeReminder && App.sQuantityOfElectricity >= chargeReminderValue;
LogUtils.d(TAG, "触发充电提醒 | 当前电量=" + quantityOfElectricity + " ≥ 阈值=" + chargeReminderValue + " | threadId=" + getId()); boolean usageRemindTrigger = !isCharging && isEnableUsageReminder && App.sQuantityOfElectricity <= usageReminderValue;
sendNotificationMessageInternal(REMIND_TYPE_CHARGE, quantityOfElectricity, isCharging);
} else if (!isCharging && isEnableUsageReminder && quantityOfElectricity <= usageReminderValue) { if (chargeRemindTrigger) {
LogUtils.d(TAG, "触发电提醒 | 当前电量=" + quantityOfElectricity + " ≤ 阈值=" + usageReminderValue + " | threadId=" + getId()); LogUtils.d(TAG, String.format("触发电提醒 | 当前电量=%d ≥ 阈值=%d | threadId=%d", App.sQuantityOfElectricity, chargeReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_USAGE, quantityOfElectricity, isCharging); sendNotificationMessageInternal(REMIND_TYPE_CHARGE, App.sQuantityOfElectricity, isCharging);
} else if (usageRemindTrigger) {
LogUtils.d(TAG, String.format("触发耗电提醒 | 当前电量=%d ≤ 阈值=%d | threadId=%d", App.sQuantityOfElectricity, usageReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_USAGE, App.sQuantityOfElectricity, isCharging);
} else { } else {
// 未有合适类型提醒,退出提醒线程 LogUtils.d(TAG, String.format("未有合适类型提醒,退出提醒线程 | threadId=%d", getId()));
LogUtils.d(TAG, "未有合适类型提醒,退出提醒线程"); break;
break; }
}
// 安全休眠,保留中断标记 // 安全休眠,保留中断标记
safeSleepInternal(sleepTime); safeSleepInternal(sleepTime);
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "循环运行异常,退出电量提醒线程 | 当前电量=" + quantityOfElectricity + " | threadId=" + getId(), e); LogUtils.e(TAG, String.format("循环运行异常,退出电量提醒线程 | 当前电量=%d | threadId=%d", App.sQuantityOfElectricity, getId()), e);
break; break;
} }
} }
// 循环退出,清理状态 // 循环退出,清理状态
cleanThreadStateInternal(); cleanThreadStateInternal();
LogUtils.d(TAG, "run结束 | threadId=" + getId()); LogUtils.d(TAG, String.format("run() 结束 | threadId=%d", getId()));
} }
// ================================== 内部业务辅助方法================================= // ====================== 内部业务辅助方法 ======================
/** /**
* 发送提醒消息到Handler弱引用避免内存泄漏 * 发送提醒消息到Handler弱引用避免内存泄漏
* @param type 提醒类型:+充电/-耗电 * @param type 提醒类型:+充电/-耗电
@@ -249,20 +211,21 @@ public class RemindThread extends Thread {
* @param isCharging 充电状态 * @param isCharging 充电状态
*/ */
private void sendNotificationMessageInternal(String type, int battery, boolean isCharging) { private void sendNotificationMessageInternal(String type, int battery, boolean isCharging) {
LogUtils.d(TAG, "sendNotificationMessageInternal调用 | 类型=" + type + " | 电量=" + battery + " | isCharging=" + isCharging + " | threadId=" + getId()); LogUtils.d(TAG, String.format("sendNotificationMessageInternal() 调用 | 类型=%s | 电量=%d | isCharging=%b | threadId=%d", type, battery, isCharging, getId()));
// 前置状态校验 // 前置状态校验
if (isExist || !isReminding) { if (isExist || !isReminding) {
LogUtils.d(TAG, "消息发送跳过:线程已退出或提醒关闭 | threadId=" + getId()); LogUtils.d(TAG, String.format("消息发送跳过:线程已退出或提醒关闭 | threadId=%d", getId()));
return; return;
} }
// 获取弱引用的Handler // 获取弱引用的Handler(校验有效性)
ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get(); ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get();
if (handler == null) { if (handler == null) {
LogUtils.w(TAG, "消息发送失败Handler已被回收 | threadId=" + getId()); LogUtils.w(TAG, String.format("消息发送失败Handler已被回收 | threadId=%d", getId()));
return; return;
} }
// 构建并发送消息
Message message = Message.obtain(handler, ControlCenterServiceHandler.MSG_REMIND_TEXT); Message message = Message.obtain(handler, ControlCenterServiceHandler.MSG_REMIND_TEXT);
message.obj = type; message.obj = type;
message.arg1 = battery; message.arg1 = battery;
@@ -270,9 +233,9 @@ public class RemindThread extends Thread {
try { try {
handler.sendMessage(message); handler.sendMessage(message);
LogUtils.d(TAG, "提醒消息发送成功 | 类型=" + type + " | 电量=" + battery + " | threadId=" + getId()); LogUtils.d(TAG, String.format("提醒消息发送成功 | 类型=%s | 电量=%d | threadId=%d", type, battery, getId()));
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "消息发送异常 | threadId=" + getId(), e); LogUtils.e(TAG, String.format("消息发送异常 | threadId=%d", getId()), e);
// 异常时回收Message避免内存泄漏 // 异常时回收Message避免内存泄漏
if (message != null) { if (message != null) {
message.recycle(); message.recycle();
@@ -285,12 +248,12 @@ public class RemindThread extends Thread {
* @param millis 休眠时长(ms) * @param millis 休眠时长(ms)
*/ */
private void safeSleepInternal(long millis) { private void safeSleepInternal(long millis) {
LogUtils.d(TAG, "safeSleepInternal调用 | 休眠时长=" + millis + "ms | threadId=" + getId()); LogUtils.d(TAG, String.format("safeSleepInternal() 调用 | 休眠时长=%dms | threadId=%d", millis, getId()));
try { try {
Thread.sleep(millis); Thread.sleep(millis);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LogUtils.w(TAG, "休眠被中断,线程准备退出 | threadId=" + getId()); LogUtils.w(TAG, String.format("休眠被中断,线程准备退出 | threadId=%d", getId()));
} }
} }
@@ -298,7 +261,7 @@ public class RemindThread extends Thread {
* 重置线程初始状态(构造器专用) * 重置线程初始状态(构造器专用)
*/ */
private void resetThreadStateInternal() { private void resetThreadStateInternal() {
LogUtils.d(TAG, "resetThreadStateInternal调用 | threadId=" + getId()); LogUtils.d(TAG, String.format("resetThreadStateInternal() 调用 | threadId=%d", getId()));
// 状态标记初始化 // 状态标记初始化
isExist = false; isExist = false;
isReminding = false; isReminding = false;
@@ -308,24 +271,23 @@ public class RemindThread extends Thread {
sleepTime = MIN_SLEEP_TIME; sleepTime = MIN_SLEEP_TIME;
chargeReminderValue = -1; chargeReminderValue = -1;
usageReminderValue = -1; usageReminderValue = -1;
quantityOfElectricity = INVALID_BATTERY_VALUE;
isCharging = false; isCharging = false;
LogUtils.d(TAG, "线程初始状态重置完成 | threadId=" + getId()); LogUtils.d(TAG, String.format("线程初始状态重置完成 | threadId=%d", getId()));
} }
/** /**
* 清理线程运行状态(循环退出时调用) * 清理线程运行状态(循环退出时调用)
*/ */
private void cleanThreadStateInternal() { private void cleanThreadStateInternal() {
LogUtils.d(TAG, "cleanThreadStateInternal调用 | threadId=" + getId()); LogUtils.d(TAG, String.format("cleanThreadStateInternal() 调用 | threadId=%d", getId()));
isReminding = false; isReminding = false;
isExist = true; isExist = true;
quantityOfElectricity = INVALID_BATTERY_VALUE;
// 中断当前线程(如果存活) // 中断当前线程(如果存活)
if (isAlive()) { if (isAlive()) {
interrupt(); interrupt();
LogUtils.d(TAG, String.format("线程已中断 | threadId=%d", getId()));
} }
LogUtils.d(TAG, "线程运行状态清理完成 | threadId=" + getId()); LogUtils.d(TAG, String.format("线程运行状态清理完成 | threadId=%d", getId()));
} }
/** /**
@@ -333,10 +295,9 @@ public class RemindThread extends Thread {
* @param config 应用配置Bean * @param config 应用配置Bean
*/ */
public void setAppConfigBean(AppConfigBean config) { public void setAppConfigBean(AppConfigBean config) {
LogUtils.d(TAG, "setAppConfigBean调用 | config=" + config + " | threadId=" + getId()); LogUtils.d(TAG, String.format("setAppConfigBean() 调用 | config=%s | threadId=%d", config, getId()));
if (config == null) { if (config == null) {
LogUtils.e(TAG, "配置同步失败配置Bean为空 | threadId=" + getId()); LogUtils.e(TAG, String.format("配置同步失败配置Bean为空 | threadId=%d", getId()));
quantityOfElectricity = INVALID_BATTERY_VALUE;
return; return;
} }
@@ -346,11 +307,12 @@ public class RemindThread extends Thread {
chargeReminderValue = Math.min(Math.max(config.getChargeReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX); chargeReminderValue = Math.min(Math.max(config.getChargeReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
usageReminderValue = Math.min(Math.max(config.getUsageReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX); usageReminderValue = Math.min(Math.max(config.getUsageReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
sleepTime = Math.max(config.getBatteryDetectInterval(), MIN_SLEEP_TIME); sleepTime = Math.max(config.getBatteryDetectInterval(), MIN_SLEEP_TIME);
quantityOfElectricity = (config.getCurrentBatteryValue() >= BATTERY_LEVEL_MIN && config.getCurrentBatteryValue() <= BATTERY_LEVEL_MAX) // sQuantityOfElectricity = (config.getCurrentBatteryValue() >= BATTERY_LEVEL_MIN && config.getCurrentBatteryValue() <= BATTERY_LEVEL_MAX)
? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE; // ? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE;
isCharging = config.isCharging(); isCharging = config.isCharging();
LogUtils.d(TAG, "配置同步完成 | 休眠时间=" + sleepTime + "ms | 提醒开启=" + isReminding + " | 当前电量=" + quantityOfElectricity + " | 充电阈值=" + chargeReminderValue + " | 耗电阈值=" + usageReminderValue + " | threadId=" + getId()); LogUtils.d(TAG, String.format("配置同步完成 | 休眠时间=%dms | 充电提醒=%b | 耗电提醒=%b | 当前电量=%d | 充电阈值=%d | 耗电阈值=%d | threadId=%d",
sleepTime, isEnableChargeReminder, isEnableUsageReminder, App.sQuantityOfElectricity, chargeReminderValue, usageReminderValue, getId()));
} }
/** /**
@@ -359,13 +321,13 @@ public class RemindThread extends Thread {
*/ */
private boolean isRunning() { private boolean isRunning() {
boolean running = !isExist && isAlive(); boolean running = !isExist && isAlive();
LogUtils.d(TAG, "isRunning调用 | 运行中=" + running + " | 退出标记=" + isExist + " | 存活=" + isAlive() + " | threadId=" + getId()); LogUtils.d(TAG, String.format("isRunning() 调用 | 运行中=%b | 退出标记=%b | 存活=%b | threadId=%d", running, isExist, isAlive(), getId()));
return running; return running;
} }
// ================================== Getter/Setter按需开放================================= // ====================== Getter/Setter按需开放 ======================
public void setIsExist(boolean isExist) { public void setIsExist(boolean isExist) {
LogUtils.d(TAG, "setIsExist调用 | isExist=" + isExist + " | threadId=" + getId()); LogUtils.d(TAG, String.format("setIsExist() 调用 | isExist=%b | threadId=%d", isExist, getId()));
this.isExist = isExist; this.isExist = isExist;
} }
@@ -373,20 +335,20 @@ public class RemindThread extends Thread {
return isExist; return isExist;
} }
// ================================== 调试辅助方法================================= // ====================== 调试辅助方法 ======================
@Override @Override
public String toString() { public String toString() {
return "RemindThread{" + return "RemindThread{" +
"threadId=" + getId() + "threadId=" + getId() +
", threadName='" + getName() + '\'' + ", threadName='" + getName() + '\'' +
", isRunning=" + isRunning() + ", isRunning=" + isRunning() +
", isReminding=" + isReminding + ", isReminding=" + isReminding +
", chargeThreshold=" + chargeReminderValue + ", chargeThreshold=" + chargeReminderValue +
", usageThreshold=" + usageReminderValue + ", usageThreshold=" + usageReminderValue +
", currentBattery=" + quantityOfElectricity + ", currentBattery=" + App.sQuantityOfElectricity +
", isCharging=" + isCharging + ", isCharging=" + isCharging +
", sleepTime=" + sleepTime + "ms" + ", sleepTime=" + sleepTime + "ms" +
'}'; '}';
} }
} }

View File

@@ -0,0 +1,285 @@
package cc.winboll.studio.powerbell.unittest;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* 单元测试页面2内存缓存背景视图专用
* 功能测试MemoryCachedBackgroundView加载、图片裁剪、双重刷新预览等功能
* 适配Java7 | API30 | 私有目录文件操作 | 无Uri冲突 | 内存缓存视图
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 单元测试页2验证带内存缓存的背景视图相关逻辑
*/
public class MainUnitTest2Activity extends AppCompatActivity {
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "MainUnitTest2Activity";
public static final int REQUEST_CROP_IMAGE = 0;
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
private static final long FILE_MIN_SIZE = 100L;
private static final long DOUBLE_REFRESH_DELAY = 200L;
// ====================== 成员变量区按功能分层移除所有Uri相关 ======================
private MemoryCachedBackgroundView mMemoryCachedBackgroundView;
private LinearLayout mllBackgroundView;
private String mAppPrivateDirPath;
private File mPrivateTestImageFile;
private File mPrivateCropImageFile;
private BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法按执行顺序onCreate→onActivityResult ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
initBaseParams();
initViewAndEvent();
copyAssetsTestImageToPrivateDir();
initBackgroundBean();
doubleRefreshPreview();
ToastUtils.show("单元测试页面2启动完成");
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, String.format("=== onActivityResult 回调 | requestCode=%d | resultCode=%d ===", requestCode, resultCode));
if (requestCode == REQUEST_CROP_IMAGE) {
handleCropResult(resultCode);
}
}
// ====================== 初始化相关方法基础参数→视图→背景Bean ======================
/**
* 初始化基础参数:私有目录、测试文件
*/
private void initBaseParams() {
LogUtils.d(TAG, "initBaseParams初始化基础参数");
// 初始化私有目录无需权限无UID冲突
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
File privateDir = new File(mAppPrivateDirPath);
if (!privateDir.exists()) {
boolean isDirCreated = privateDir.mkdirs();
LogUtils.d(TAG, String.format("initBaseParams创建私有目录 | 路径=%s | 结果=%b", mAppPrivateDirPath, isDirCreated));
}
// 初始化测试文件与裁剪文件无Uri
File refFile = new File(ASSETS_TEST_IMAGE_PATH);
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
LogUtils.d(TAG, String.format("initBaseParams测试图路径=%s", mPrivateTestImageFile.getAbsolutePath()));
LogUtils.d(TAG, String.format("initBaseParams裁剪图路径=%s", mPrivateCropImageFile.getAbsolutePath()));
}
/**
* 初始化布局与控件事件(含单例视图创建)
*/
private void initViewAndEvent() {
LogUtils.d(TAG, "initViewAndEvent初始化布局与控件事件");
setContentView(R.layout.activity_mainunittest2);
mllBackgroundView = (LinearLayout) findViewById(R.id.ll_backgroundview);
// 创建MemoryCachedBackgroundView单例并添加到布局
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(this).getCurrentBackgroundBean().getPixelColor();
mMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
mllBackgroundView.addView(mMemoryCachedBackgroundView);
LogUtils.d(TAG, "initViewAndEvent内存缓存背景视图实例创建并添加完成");
// 跳转主页面按钮
Button btnMain = (Button) findViewById(R.id.btn_main_activity);
btnMain.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "initViewAndEvent点击按钮→跳转主页面");
startActivity(new Intent(MainUnitTest2Activity.this, MainActivity.class));
}
});
// 裁剪按钮直接用File路径启动无Uri
Button btnCrop = (Button) findViewById(R.id.btn_test_cropimage);
btnCrop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "initViewAndEvent点击按钮→启动裁剪File路径版");
ToastUtils.show("准备启动图片裁剪");
if (isFileValid(mPrivateTestImageFile)) {
startCropTestByFile();
} else {
ToastUtils.show("测试图片未准备好,重新拷贝");
copyAssetsTestImageToPrivateDir();
}
}
});
}
/**
* 初始化背景Bean
*/
private void initBackgroundBean() {
LogUtils.d(TAG, "initBackgroundBean初始化背景Bean");
mPreviewBackgroundBean = new BackgroundBean();
mPreviewBackgroundBean.setPixelColor(ImageUtils.getColorAccent(this));
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
LogUtils.d(TAG, "initBackgroundBean背景Bean初始化完成");
}
// ====================== 核心业务方法(文件拷贝→裁剪→结果处理→预览刷新) ======================
/**
* 从assets拷贝图片到私有目录
*/
private void copyAssetsTestImageToPrivateDir() {
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir开始拷贝assets图片到私有目录");
if (isFileValid(mPrivateTestImageFile)) {
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir图片已存在无需拷贝");
return;
}
InputStream inputStream = null;
try {
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
LogUtils.d(TAG, String.format("copyAssetsTestImageToPrivateDir图片拷贝成功 | 大小=%d字节", mPrivateTestImageFile.length()));
} catch (IOException e) {
LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir图片拷贝失败 | %s", e.getMessage()), e);
ToastUtils.show("图片准备失败");
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir关闭流失败 | %s", e.getMessage()));
}
}
}
}
/**
* 直接用File启动裁剪关键调用ImageCropUtils的File重载方法
*/
private void startCropTestByFile() {
LogUtils.d(TAG, String.format("startCropTestByFile启动裁剪 | 原图=%s", mPrivateTestImageFile.getAbsolutePath()));
// 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) {
boolean isDirCreated = cropParent.mkdirs();
LogUtils.d(TAG, String.format("startCropTestByFile创建裁剪目录 | 路径=%s | 结果=%b", cropParent.getAbsolutePath(), isDirCreated));
}
// 调用ImageCropUtils的File参数方法核心绕开Uri
ImageCropUtils.startImageCrop(
this,
mPrivateTestImageFile,
mPrivateCropImageFile,
0,
0,
true,
REQUEST_CROP_IMAGE
);
LogUtils.d(TAG, String.format("startCropTestByFile裁剪请求已发送 | 输出路径=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("已启动图片裁剪");
}
/**
* 处理裁剪结果直接校验输出File
* @param resultCode 裁剪结果码
*/
private void handleCropResult(int resultCode) {
LogUtils.d(TAG, String.format("handleCropResult裁剪回调处理 | resultCode=%d", resultCode));
if (resultCode == RESULT_OK) {
if (isFileValid(mPrivateCropImageFile)) {
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(this).getCurrentBackgroundBean().getPixelColor();
mMemoryCachedBackgroundView.loadImage(nCurrentPixelColor, mPrivateCropImageFile.getAbsolutePath(), true);
LogUtils.d(TAG, String.format("handleCropResult裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview();
} else {
LogUtils.e(TAG, "handleCropResult裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效");
}
} else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "handleCropResult裁剪取消");
ToastUtils.show("裁剪已取消");
} else {
LogUtils.e(TAG, String.format("handleCropResult裁剪失败 | resultCode异常=%d", resultCode));
ToastUtils.show("裁剪失败");
}
}
/**
* 双重刷新预览,确保背景加载最新数据
*/
private void doubleRefreshPreview() {
LogUtils.d(TAG, "doubleRefreshPreview执行双重刷新预览");
// 第一重刷新
try {
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
//mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第一重异常 | %s", e.getMessage()));
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mMemoryCachedBackgroundView != null && !isFinishing()) {
try {
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
//mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第二重异常 | %s", e.getMessage()));
}
}
}
}, DOUBLE_REFRESH_DELAY);
}
// ====================== 工具辅助方法(文件校验) ======================
/**
* 校验文件是否有效(存在且大小达标)
* @param file 待校验文件
* @return true=有效 false=无效
*/
private boolean isFileValid(File file) {
boolean isValid = file != null && file.exists() && file.length() > FILE_MIN_SIZE;
LogUtils.d(TAG, String.format("isFileValid文件校验 | 路径=%s | 结果=%b", file != null ? file.getAbsolutePath() : "null", isValid));
return isValid;
}
}

View File

@@ -12,32 +12,38 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.FileUtils; import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils; import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import cc.winboll.studio.powerbell.models.BackgroundBean;
/** /**
* 终极修复版放弃FileProvider直接用私有目录File路径彻底解决UID冲突 * 单元测试页面
* 功能:测试背景图加载、图片裁剪、双重刷新预览等功能
* 适配Java7 | API30 | 私有目录文件操作 | 无Uri冲突
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 单元测试页:验证图片处理与背景预览相关逻辑
*/ */
public class MainUnitTestActivity extends AppCompatActivity { public class MainUnitTestActivity extends AppCompatActivity {
// ====================== 常量定义 ====================== // ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "MainUnitTestActivity"; public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0; public static final int REQUEST_CROP_IMAGE = 0;
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png"; private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
private static final long FILE_MIN_SIZE = 100L;
private static final long DOUBLE_REFRESH_DELAY = 200L;
// ====================== 成员变量移除所有Uri相关 ====================== // ====================== 成员变量区(按功能分层,移除所有Uri相关 ======================
private BackgroundView mBackgroundView; private BackgroundView mBackgroundView;
private String mAppPrivateDirPath; private String mAppPrivateDirPath;
private File mPrivateTestImageFile; // 仅用File不用Uri private File mPrivateTestImageFile; // 仅用File不用Uri
private File mPrivateCropImageFile; private File mPrivateCropImageFile;
BackgroundBean mPreviewBackgroundBean; private BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法 ====================== // ====================== 生命周期方法按执行顺序onCreate→onActivityResult ======================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -46,15 +52,9 @@ public class MainUnitTestActivity extends AppCompatActivity {
initBaseParams(); initBaseParams();
initViewAndEvent(); initViewAndEvent();
copyAssetsTestImageToPrivateDir(); copyAssetsTestImageToPrivateDir();
//loadBackgroundByFile(); // 直接用File加载 initBackgroundBean();
mPreviewBackgroundBean = new BackgroundBean(); doubleRefreshPreview();
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
doubleRefreshPreview();
ToastUtils.show("单元测试页面启动完成"); ToastUtils.show("单元测试页面启动完成");
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ==="); LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
} }
@@ -62,36 +62,42 @@ public class MainUnitTestActivity extends AppCompatActivity {
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "=== onActivityResult 回调 ==="); LogUtils.d(TAG, String.format("=== onActivityResult 回调 | requestCode=%d | resultCode=%d ===", requestCode, resultCode));
if (requestCode == REQUEST_CROP_IMAGE) { if (requestCode == REQUEST_CROP_IMAGE) {
handleCropResult(resultCode); handleCropResult(resultCode);
} }
} }
// ====================== 初始化相关方法 ====================== // ====================== 初始化相关方法基础参数→视图→背景Bean ======================
/**
* 初始化基础参数:私有目录、测试文件
*/
private void initBaseParams() { private void initBaseParams() {
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File"); LogUtils.d(TAG, "initBaseParams初始化基础参数");
// 初始化私有目录无需权限无UID冲突
// 私有目录无需权限无UID冲突
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/"; mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
File privateDir = new File(mAppPrivateDirPath); File privateDir = new File(mAppPrivateDirPath);
if (!privateDir.exists()) { if (!privateDir.exists()) {
privateDir.mkdirs(); boolean isDirCreated = privateDir.mkdirs();
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath); LogUtils.d(TAG, String.format("initBaseParams创建私有目录 | 路径=%s | 结果=%b", mAppPrivateDirPath, isDirCreated));
} }
// 初始化File无Uri // 初始化测试文件与裁剪文件无Uri
File refFile = new File(ASSETS_TEST_IMAGE_PATH); File refFile = new File(ASSETS_TEST_IMAGE_PATH);
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png"; String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png"); String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName); mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName); mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
LogUtils.d(TAG, "测试图File路径" + mPrivateTestImageFile.getAbsolutePath()); LogUtils.d(TAG, String.format("initBaseParams测试图路径=%s", mPrivateTestImageFile.getAbsolutePath()));
LogUtils.d(TAG, String.format("initBaseParams裁剪图路径=%s", mPrivateCropImageFile.getAbsolutePath()));
} }
/**
* 初始化布局与控件事件
*/
private void initViewAndEvent() { private void initViewAndEvent() {
LogUtils.d(TAG, "初始化布局与控件事件"); LogUtils.d(TAG, "initViewAndEvent初始化布局与控件事件");
setContentView(R.layout.activity_mainunittest); setContentView(R.layout.activity_mainunittest);
mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview); mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
@@ -100,7 +106,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
btnMain.setOnClickListener(new View.OnClickListener() { btnMain.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "点击按钮跳转主页面"); LogUtils.d(TAG, "initViewAndEvent点击按钮跳转主页面");
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class)); startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
} }
}); });
@@ -110,11 +116,11 @@ public class MainUnitTestActivity extends AppCompatActivity {
btnCrop.setOnClickListener(new View.OnClickListener() { btnCrop.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "点击按钮启动裁剪File路径版"); LogUtils.d(TAG, "initViewAndEvent点击按钮启动裁剪File路径版");
ToastUtils.show("准备启动图片裁剪"); ToastUtils.show("准备启动图片裁剪");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) { if (isFileValid(mPrivateTestImageFile)) {
startCropTestByFile(); // 直接传File startCropTestByFile();
} else { } else {
ToastUtils.show("测试图片未准备好,重新拷贝"); ToastUtils.show("测试图片未准备好,重新拷贝");
copyAssetsTestImageToPrivateDir(); copyAssetsTestImageToPrivateDir();
@@ -123,11 +129,29 @@ public class MainUnitTestActivity extends AppCompatActivity {
}); });
} }
// 从assets拷贝图片不变确保File存在 /**
* 初始化背景Bean
*/
private void initBackgroundBean() {
LogUtils.d(TAG, "initBackgroundBean初始化背景Bean");
mPreviewBackgroundBean = new BackgroundBean();
mPreviewBackgroundBean.setPixelColor(ImageUtils.getColorAccent(this));
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
LogUtils.d(TAG, "initBackgroundBean背景Bean初始化完成");
}
// ====================== 核心业务方法(文件拷贝→裁剪→结果处理→预览刷新) ======================
/**
* 从assets拷贝图片到私有目录
*/
private void copyAssetsTestImageToPrivateDir() { private void copyAssetsTestImageToPrivateDir() {
LogUtils.d(TAG, "开始拷贝assets图片到私有目录"); LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir开始拷贝assets图片到私有目录");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) { if (isFileValid(mPrivateTestImageFile)) {
LogUtils.d(TAG, "图片已存在,无需拷贝"); LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir图片已存在,无需拷贝");
return; return;
} }
@@ -135,97 +159,87 @@ public class MainUnitTestActivity extends AppCompatActivity {
try { try {
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH); inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile); FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节"); LogUtils.d(TAG, String.format("copyAssetsTestImageToPrivateDir图片拷贝成功 | 大小=%d字节", mPrivateTestImageFile.length()));
} catch (IOException e) { } catch (IOException e) {
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), e); LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir图片拷贝失败 | %s", e.getMessage()), e);
ToastUtils.show("图片准备失败"); ToastUtils.show("图片准备失败");
} finally { } finally {
if (inputStream != null) { if (inputStream != null) {
try { try {
inputStream.close(); inputStream.close();
} catch (IOException e) { } catch (IOException e) {
LogUtils.e(TAG, "关闭流失败:" + e.getMessage()); LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir关闭流失败 | %s", e.getMessage()));
} }
} }
} }
} }
// ====================== 核心业务方法全改为File路径 ====================== /**
/** 直接用File路径加载背景图无Uri无冲突 */ * 直接用File启动裁剪关键调用ImageCropUtils的File重载方法
// private void loadBackgroundByFile() { */
// LogUtils.d(TAG, "开始加载背景图File路径版");
// if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
// mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
// LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
// ToastUtils.show("背景图加载成功");
// } else {
// LogUtils.e(TAG, "背景图加载失败:文件无效");
// ToastUtils.show("背景图加载失败");
// }
// }
/** 直接用File启动裁剪关键调用ImageCropUtils的File重载方法 */
private void startCropTestByFile() { private void startCropTestByFile() {
LogUtils.d(TAG, "启动裁剪File路径版原图" + mPrivateTestImageFile.getAbsolutePath()); LogUtils.d(TAG, String.format("startCropTestByFile启动裁剪 | 原图=%s", mPrivateTestImageFile.getAbsolutePath()));
// 确保输出目录存在 // 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile(); File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) { if (!cropParent.exists()) {
cropParent.mkdirs(); boolean isDirCreated = cropParent.mkdirs();
LogUtils.d(TAG, String.format("startCropTestByFile创建裁剪目录 | 路径=%s | 结果=%b", cropParent.getAbsolutePath(), isDirCreated));
} }
// 调用ImageCropUtils的File参数方法核心绕开Uri // 调用ImageCropUtils的File参数方法核心绕开Uri
ImageCropUtils.startImageCrop( ImageCropUtils.startImageCrop(
this, this,
mPrivateTestImageFile, // 原图File mPrivateTestImageFile, // 原图File
mPrivateCropImageFile, // 输出File mPrivateCropImageFile, // 输出File
0, 0,
0, 0,
true, true,
REQUEST_CROP_IMAGE REQUEST_CROP_IMAGE
); );
LogUtils.d(TAG, "裁剪请求已发送输出路径" + mPrivateCropImageFile.getAbsolutePath()); LogUtils.d(TAG, String.format("startCropTestByFile裁剪请求已发送 | 输出路径=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("已启动图片裁剪"); ToastUtils.show("已启动图片裁剪");
} }
/** 处理裁剪结果直接校验输出File */ /**
* 处理裁剪结果直接校验输出File
* @param resultCode 裁剪结果码
*/
private void handleCropResult(int resultCode) { private void handleCropResult(int resultCode) {
LogUtils.d(TAG, "裁剪回调处理resultCode=" + resultCode); // LogUtils.d(TAG, String.format("handleCropResult裁剪回调处理 | resultCode=%d", resultCode));
if (resultCode == RESULT_OK) { // if (resultCode == RESULT_OK) {
if (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) { // if (isFileValid(mPrivateCropImageFile)) {
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath()); // mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
LogUtils.d(TAG, "裁剪成功加载裁剪图" + mPrivateCropImageFile.getAbsolutePath()); // LogUtils.d(TAG, String.format("handleCropResult裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("裁剪成功"); // ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true); // mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview(); // doubleRefreshPreview();
} else { // } else {
LogUtils.e(TAG, "裁剪成功但输出文件无效"); // LogUtils.e(TAG, "handleCropResult裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效"); // ToastUtils.show("裁剪失败:输出文件无效");
} // }
} else if (resultCode == RESULT_CANCELED) { // } else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "裁剪取消"); // LogUtils.d(TAG, "handleCropResult裁剪取消");
ToastUtils.show("裁剪已取消"); // ToastUtils.show("裁剪已取消");
} else { // } else {
LogUtils.e(TAG, "裁剪失败resultCode异常"); // LogUtils.e(TAG, String.format("handleCropResult裁剪失败 | resultCode异常=%d", resultCode));
ToastUtils.show("裁剪失败"); // ToastUtils.show("裁剪失败");
} // }
} }
/** /**
* 双重刷新预览,确保背景加载最新数据 * 双重刷新预览,确保背景加载最新数据
* 移除:缓存清空逻辑
*/ */
private void doubleRefreshPreview() { private void doubleRefreshPreview() {
LogUtils.d(TAG, "doubleRefreshPreview执行双重刷新预览");
// 第一重刷新 // 第一重刷新
try { try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true); mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor()); mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成"); LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第一重完成");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage()); LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第一重异常 | %s", e.getMessage()));
return; return;
} }
@@ -235,15 +249,27 @@ public class MainUnitTestActivity extends AppCompatActivity {
public void run() { public void run() {
if (mBackgroundView != null && !isFinishing()) { if (mBackgroundView != null && !isFinishing()) {
try { try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true); mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor()); mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成"); LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第二重完成");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage()); LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第二重异常 | %s", e.getMessage()));
} }
} }
} }
}, 200); }, DOUBLE_REFRESH_DELAY);
}
// ====================== 工具辅助方法(文件校验) ======================
/**
* 校验文件是否有效(存在且大小达标)
* @param file 待校验文件
* @return true=有效 false=无效
*/
private boolean isFileValid(File file) {
boolean isValid = file != null && file.exists() && file.length() > FILE_MIN_SIZE;
LogUtils.d(TAG, String.format("isFileValid文件校验 | 路径=%s | 结果=%b", file != null ? file.getAbsolutePath() : "null", isValid));
return isValid;
} }
} }

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 15:54
* @Describe 应用图标切换工具类(启用组件时创建对应快捷方式)
*/
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -13,75 +8,143 @@ import android.os.Build;
import android.widget.Toast; import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
/**
* 应用图标切换工具类(启用组件时创建对应快捷方式)
* 适配Java7 | API30 | 高低版本快捷方式创建兼容
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 应用启动器组件切换与桌面快捷方式创建工具,支持多组件管理与版本兼容
*/
public class APPPlusUtils { public class APPPlusUtils {
// ======================== 静态常量区(魔法值与标签管理)========================
public static final String TAG = "APPPlusUtils"; public static final String TAG = "APPPlusUtils";
private static final int SHORTCUT_ICON_DEFAULT = R.drawable.ic_launcher; // 默认快捷方式图标
private static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; // 旧版快捷方式广播Action
// 快捷方式配置(名称+图标,需与实际资源匹配) // ======================== 公共业务方法区(对外核心接口)========================
// private static final String PLUS_SHORTCUT_NAME = "位置服务-Laojun";
// private static final int PLUS_SHORTCUT_ICON = R.mipmap.ic_launcher; // Laojun 图标资源
/** /**
* 添加Plus组件与图标 * 切换应用启动器组件(禁用其他组件,启用目标组件)
* @param context 上下文
* @param componentName 目标组件完整类名
* @return 切换是否成功
*/ */
public static boolean switchAppLauncherToComponent(Context context, String componentName) { public static boolean switchAppLauncherToComponent(Context context, String componentName) {
LogUtils.d(TAG, String.format("switchAppLauncherToComponent调用 | 传入组件名=%s", componentName));
// 参数校验
if (context == null) { if (context == null) {
LogUtils.d(TAG, "切换失败:上下文为空"); LogUtils.e(TAG, "switchAppLauncherToComponent失败:上下文为空");
Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败", Toast.LENGTH_SHORT).show(); return false;
}
if (componentName == null || componentName.isEmpty()) {
LogUtils.e(TAG, "switchAppLauncherToComponent失败组件名为空");
return false; return false;
} }
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
ComponentName targetComponent = new ComponentName(context, componentName);
ComponentName plusComponentSwitchTo = new ComponentName(context, componentName); ComponentName en1Component = new ComponentName(context, App.COMPONENT_EN1);
ComponentName plusComponentEN1 = new ComponentName(context, App.COMPONENT_EN1); ComponentName cn1Component = new ComponentName(context, App.COMPONENT_CN1);
ComponentName plusComponentCN1 = new ComponentName(context, App.COMPONENT_CN1); ComponentName cn2Component = new ComponentName(context, App.COMPONENT_CN2);
ComponentName plusComponentCN2 = new ComponentName(context, App.COMPONENT_CN2);
try { try {
disableComponent(pm, plusComponentEN1); // 禁用所有其他启动器组件
disableComponent(pm, plusComponentCN1); disableComponent(pm, en1Component);
disableComponent(pm, plusComponentCN2); disableComponent(pm, cn1Component);
enableComponent(pm, plusComponentSwitchTo); disableComponent(pm, cn2Component);
// 启用目标组件
enableComponent(pm, targetComponent);
LogUtils.d(TAG, String.format("switchAppLauncherToComponent成功 | 目标组件=%s", componentName));
Toast.makeText(context, context.getString(R.string.app_name) + "图标切换成功", Toast.LENGTH_SHORT).show();
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "图标切换失败:" + e.getMessage()); LogUtils.e(TAG, String.format("switchAppLauncherToComponent失败 | 异常信息=%s", e.getMessage()), e);
Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show(); Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
return false; return false;
} }
} }
// ======================== 私有辅助方法区(组件状态控制)========================
/**
* 启用组件(带状态检查,避免重复操作)
* @param pm 包管理器
* @param component 目标组件
*/
private static void enableComponent(PackageManager pm, ComponentName component) {
int currentState = pm.getComponentEnabledSetting(component);
String componentName = component.getClassName();
if (currentState != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
);
LogUtils.d(TAG, String.format("enableComponent成功 | 组件=%s", componentName));
} else {
LogUtils.d(TAG, String.format("enableComponent无需操作 | 组件已启用=%s", componentName));
}
}
/**
* 禁用组件(带状态检查,避免重复操作)
* @param pm 包管理器
* @param component 目标组件
*/
private static void disableComponent(PackageManager pm, ComponentName component) {
int currentState = pm.getComponentEnabledSetting(component);
String componentName = component.getClassName();
if (currentState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
);
LogUtils.d(TAG, String.format("disableComponent成功 | 组件=%s", componentName));
} else {
LogUtils.d(TAG, String.format("disableComponent无需操作 | 组件已禁用=%s", componentName));
}
}
// ======================== 私有辅助方法区(快捷方式创建)========================
/** /**
* 创建指定组件的桌面快捷方式(自动去重,兼容 Android 8.0+ * 创建指定组件的桌面快捷方式(自动去重,兼容 Android 8.0+
* @param component 目标组件(如 LAOJUN_ACTIVITY * @param context 上下文
* @param component 目标组件
* @param name 快捷方式名称 * @param name 快捷方式名称
* @param iconRes 快捷方式图标资源ID * @param iconRes 快捷方式图标资源ID
* @return 是否创建成功 * @return 是否创建成功
*/ */
private static boolean createComponentShortcut(Context context, ComponentName component, String name, int iconRes) { private static boolean createComponentShortcut(Context context, ComponentName component, String name, int iconRes) {
if (context == null || component == null || name == null || iconRes == 0) { // 参数校验
LogUtils.d(TAG, "快捷方式创建失败:参数为空"); String componentName = component != null ? component.getClassName() : "null";
LogUtils.d(TAG, String.format("createComponentShortcut调用 | 组件=%s | 名称=%s", componentName, name));
if (context == null || component == null || name == null || name.isEmpty()) {
LogUtils.e(TAG, "createComponentShortcut失败上下文、组件或名称为空");
return false; return false;
} }
// 图标资源默认值补全
int finalIconRes = iconRes != 0 ? iconRes : SHORTCUT_ICON_DEFAULT;
// Android 8.0+API 26+):使用 ShortcutManager系统推荐 // Android 8.0+API 26+):使用 ShortcutManager系统推荐
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try { try {
PackageManager pm = context.getPackageManager();
android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class); android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class);
if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) { if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) {
LogUtils.d(TAG, "系统不支持创建快捷方式"); LogUtils.w(TAG, "createComponentShortcut系统不支持创建快捷方式");
return false; return false;
} }
// 检查是否已存在该组件的快捷方式(去重) // 检查是否已存在该组件的快捷方式(去重)
for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) { for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) {
if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) { if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) {
LogUtils.d(TAG, "快捷方式已存在" + component.getClassName()); LogUtils.d(TAG, String.format("createComponentShortcut快捷方式已存在=%s", componentName));
return true; return true;
} }
} }
@@ -96,16 +159,17 @@ public class APPPlusUtils {
android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName()) android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName())
.setShortLabel(name) .setShortLabel(name)
.setLongLabel(name) .setLongLabel(name)
.setIcon(android.graphics.drawable.Icon.createWithResource(context, iconRes)) .setIcon(android.graphics.drawable.Icon.createWithResource(context, finalIconRes))
.setIntent(launchIntent) .setIntent(launchIntent)
.build(); .build();
// 请求创建快捷方式(需用户确认) // 请求创建快捷方式(需用户确认)
shortcutManager.requestPinShortcut(shortcutInfo, null); shortcutManager.requestPinShortcut(shortcutInfo, null);
LogUtils.d(TAG, "createComponentShortcutAndroid O+ 快捷方式创建请求已发送");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LogUtils.d(TAG, "Android O+ 快捷方式创建失败:" + e.getMessage()); LogUtils.e(TAG, String.format("createComponentShortcut失败 | Android O+ 异常=%s", e.getMessage()), e);
return false; return false;
} }
} else { } else {
@@ -118,47 +182,22 @@ public class APPPlusUtils {
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 构建创建快捷方式的广播意图 // 构建创建快捷方式的广播意图
Intent installIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); Intent installIntent = new Intent(ACTION_INSTALL_SHORTCUT);
installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent); installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(context, iconRes)); Intent.ShortcutIconResource.fromContext(context, finalIconRes));
installIntent.putExtra("duplicate", false); // 禁止重复创建 installIntent.putExtra("duplicate", false); // 禁止重复创建
context.sendBroadcast(installIntent); context.sendBroadcast(installIntent);
LogUtils.d(TAG, "createComponentShortcutAndroid O- 快捷方式创建广播已发送");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LogUtils.d(TAG, "Android O- 快捷方式创建失败:" + e.getMessage()); LogUtils.e(TAG, String.format("createComponentShortcut失败 | Android O- 异常=%s", e.getMessage()), e);
return false; return false;
} }
} }
} }
/**
* 启用组件(带状态检查,避免重复操作)
*/
private static void enableComponent(PackageManager pm, ComponentName component) {
if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
);
}
}
/**
* 禁用组件(带状态检查,避免重复操作)
*/
private static void disableComponent(PackageManager pm, ComponentName component) {
if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
);
}
}
} }

View File

@@ -5,81 +5,139 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BatteryInfoBean; import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList; import java.util.ArrayList;
/**
* 应用缓存工具类适配Android API 30基于Java 7编写
* 负责电池信息的缓存、持久化与管理
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 电池信息缓存工具:实现电量变化记录、持久化存储与缓存限制
*/
public class AppCacheUtils { public class AppCacheUtils {
// ===================== 静态常量区(置顶归类,消除魔法值) =====================
public static final String TAG = "AppCacheUtils"; public static final String TAG = "AppCacheUtils";
private static final int MAX_BATTERY_RECORD_COUNT = 180; // 电池记录最大条数限制
// 保存唯一配置实例 // ===================== 静态成员区(单例相关) =====================
static AppCacheUtils _mAppCacheUtils; private static AppCacheUtils sInstance;
// 配置实例引用的上下文环境
Context mContext;
// 配置实例的数据的存储文件路径
//volatile String mAppCacheDataFilePath = null;
ArrayList<BatteryInfoBean> mlBatteryInfo;
// 私有实例构造方法 // ===================== 成员变量区(按功能分层) =====================
// private Context mContext; // ApplicationContext避免内存泄漏
AppCacheUtils(Context context) { private ArrayList<BatteryInfoBean> mBatteryInfoList; // 电池信息缓存列表
mContext = context;
//mAppCacheDataFilePath = context.getExternalFilesDir(TAG) + File.separator + "mlBatteryInfo.dat";
mlBatteryInfo = new ArrayList<BatteryInfoBean>();
loadAppCacheData();
}
// 返回唯一实例 // ===================== 单例方法区(线程安全) =====================
// /**
* 获取单例实例
* @param context 上下文内部会转换为ApplicationContext
* @return 唯一AppCacheUtils实例
*/
public static synchronized AppCacheUtils getInstance(Context context) { public static synchronized AppCacheUtils getInstance(Context context) {
if (_mAppCacheUtils == null) { String contextType = context != null ? context.getClass().getSimpleName() : "null";
_mAppCacheUtils = new AppCacheUtils(context); LogUtils.d(TAG, String.format("getInstance调用 | 传入Context类型=%s", contextType));
if (sInstance == null) {
if (context == null) {
LogUtils.e(TAG, "getInstance失败传入Context为null");
throw new IllegalArgumentException("Context cannot be null");
}
sInstance = new AppCacheUtils(context.getApplicationContext());
LogUtils.d(TAG, "getInstance单例实例初始化完成");
} }
return _mAppCacheUtils; return sInstance;
} }
// 添加电量改变时间 // ===================== 私有构造方法区(禁止外部实例化) =====================
// /**
public void addChangingTime(int nBattetyValue) { * 私有构造方法,初始化缓存列表并加载持久化数据
if (mlBatteryInfo.size() == 0) { * @param context ApplicationContext
addChangingTimeToList(nBattetyValue); */
//LogUtils.d(TAG, "nBattetyValue is "+Integer.toString(nBattetyValue)); private AppCacheUtils(Context context) {
LogUtils.d(TAG, "AppCacheUtils构造方法调用");
mContext = context;
mBatteryInfoList = new ArrayList<BatteryInfoBean>();
loadAppCacheData();
LogUtils.d(TAG, String.format("AppCacheUtils构造完成 | 初始电池信息数量=%d", mBatteryInfoList.size()));
}
// ===================== 公共业务方法区(对外暴露接口) =====================
/**
* 添加电池电量变化记录(仅当电量变化时添加)
* @param batteryValue 电池电量值
*/
public void addChangingTime(int batteryValue) {
LogUtils.d(TAG, String.format("addChangingTime调用 | 传入电量值=%d", batteryValue));
if (mBatteryInfoList.isEmpty()) {
addChangingTimeToList(batteryValue);
LogUtils.d(TAG, "addChangingTime缓存列表为空直接添加记录");
return; return;
} }
if (mlBatteryInfo.get(mlBatteryInfo.size() - 1).getBattetyValue() != nBattetyValue) {
addChangingTimeToList(nBattetyValue);
//LogUtils.d(TAG, "nBattetyValue is "+Integer.toString(nBattetyValue));
// 对比最后一条记录的电量值,避免重复添加
int lastBatteryValue = mBatteryInfoList.get(mBatteryInfoList.size() - 1).getBatteryValue();
if (lastBatteryValue != batteryValue) {
addChangingTimeToList(batteryValue);
LogUtils.d(TAG, String.format("addChangingTime电量变化添加新记录 | 原电量=%d | 新电量=%d", lastBatteryValue, batteryValue));
} else {
LogUtils.d(TAG, "addChangingTime电量未变化跳过添加");
} }
} }
void addChangingTimeToList(int nBattetyValue) { /**
if (mlBatteryInfo.size() > 180) { * 获取电池信息缓存列表
mlBatteryInfo.remove(0); * @return 完整的电池信息列表
} */
BatteryInfoBean batteryInfo = new BatteryInfoBean(System.currentTimeMillis(), nBattetyValue);
LogUtils.d(TAG, "getBattetyValue is " + Integer.toString(batteryInfo.getBattetyValue()));
LogUtils.d(TAG, "getTimeStamp is " + Long.toString(batteryInfo.getTimeStamp()));
mlBatteryInfo.add(batteryInfo);
saveAppCacheData();
}
public ArrayList<BatteryInfoBean> getArrayListBatteryInfo() { public ArrayList<BatteryInfoBean> getArrayListBatteryInfo() {
LogUtils.d(TAG, String.format("getArrayListBatteryInfo调用 | 当前缓存数量=%d", mBatteryInfoList.size()));
loadAppCacheData(); loadAppCacheData();
return mlBatteryInfo; return mBatteryInfoList;
}
// 读取文件存储的数据
//
void saveAppCacheData() {
BatteryInfoBean.saveBeanList(mContext, mlBatteryInfo, BatteryInfoBean.class);
}
// 保存数据到文件
//
void loadAppCacheData() {
mlBatteryInfo.clear();
BatteryInfoBean.loadBeanList(mContext, mlBatteryInfo, BatteryInfoBean.class);
} }
/**
* 清除所有电池历史记录
*/
public void clearBatteryHistory() { public void clearBatteryHistory() {
mlBatteryInfo.clear(); LogUtils.d(TAG, String.format("clearBatteryHistory调用 | 清除前缓存数量=%d", mBatteryInfoList.size()));
mBatteryInfoList.clear();
saveAppCacheData(); saveAppCacheData();
LogUtils.d(TAG, "clearBatteryHistory完成 | 缓存已清空");
}
// ===================== 私有辅助方法区(内部业务逻辑) =====================
/**
* 内部方法:添加电量记录到列表并持久化
* @param batteryValue 电池电量值
*/
private void addChangingTimeToList(int batteryValue) {
LogUtils.d(TAG, String.format("addChangingTimeToList调用 | 传入电量值=%d", batteryValue));
// 限制列表最大长度,避免内存溢出
if (mBatteryInfoList.size() >= MAX_BATTERY_RECORD_COUNT) {
mBatteryInfoList.remove(0);
LogUtils.d(TAG, String.format("addChangingTimeToList列表超过%d条移除最旧记录", MAX_BATTERY_RECORD_COUNT));
}
BatteryInfoBean batteryInfo = new BatteryInfoBean(System.currentTimeMillis(), batteryValue);
mBatteryInfoList.add(batteryInfo);
LogUtils.d(TAG, String.format("addChangingTimeToList添加新记录 | 电量=%d | 时间戳=%d", batteryInfo.getBatteryValue(), batteryInfo.getTimeStamp()));
saveAppCacheData();
}
/**
* 从文件加载缓存数据
*/
private void loadAppCacheData() {
LogUtils.d(TAG, "loadAppCacheData调用 | 开始加载持久化数据");
mBatteryInfoList.clear();
BatteryInfoBean.loadBeanList(mContext, mBatteryInfoList, BatteryInfoBean.class);
LogUtils.d(TAG, String.format("loadAppCacheData完成 | 加载数据数量=%d", mBatteryInfoList.size()));
}
/**
* 保存缓存数据到文件
*/
private void saveAppCacheData() {
LogUtils.d(TAG, String.format("saveAppCacheData调用 | 保存数据数量=%d", mBatteryInfoList.size()));
BatteryInfoBean.saveBeanList(mContext, mBatteryInfoList, BatteryInfoBean.class);
LogUtils.d(TAG, "saveAppCacheData完成 | 数据已持久化");
} }
} }

View File

@@ -1,22 +1,20 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.models.AppConfigBean; import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean; import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.threads.RemindThread;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
* @Date 2025/12/17 13:59
* @Describe 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
* 适配Java7 | API30 | 小米手机,单例模式,线程安全,配置持久化 * 适配Java7 | API30 | 小米手机,单例模式,线程安全,配置持久化
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 应用配置全量管理工具,支持配置持久化、自动校准、线程安全访问
*/ */
public class AppConfigUtils { public class AppConfigUtils {
// ======================== 静态常量(顶部统一管理,抽离魔法值======================== // ======================== 静态常量区(魔法值统一管理========================
public static final String TAG = "AppConfigUtils"; public static final String TAG = "AppConfigUtils";
public static final String BACKGROUND_DIR = "Background"; // 背景图片存储目录 public static final String BACKGROUND_DIR = "Background"; // 背景图片存储目录
private static final int MIN_REMINDER_VALUE = 0; // 提醒阈值最小值 private static final int MIN_REMINDER_VALUE = 0; // 提醒阈值最小值
@@ -24,90 +22,102 @@ public class AppConfigUtils {
private static final int MIN_INTERVAL_TIME = 1000; // 最小提醒间隔ms private static final int MIN_INTERVAL_TIME = 1000; // 最小提醒间隔ms
private static final int MIN_DETECT_INTERVAL = 500; // 最小电量检测间隔ms private static final int MIN_DETECT_INTERVAL = 500; // 最小电量检测间隔ms
// ======================== 静态成员(单例实例,严格控制初始化======================== // ======================== 静态成员(单例实例)========================
private static AppConfigUtils sInstance; // 单例实例(私有,禁止外部直接创建 private static volatile AppConfigUtils sInstance; // 单例实例(volatile保障双重校验锁有效性
// ======================== 核心依赖属性优先排列final保障安全)======================== // ======================== 成员变量区按依赖优先级排序final/volatile保障线程安全)========================
private final Context mContext; // 应用上下文(避免内存泄漏) private final Context mContext; // 应用上下文(ApplicationContext避免内存泄漏)
private App mApplication; // 应用Application实例 private final App mApplication; // 应用Application实例final保障不可变
public volatile AppConfigBean mAppConfigBean; // 应用配置Bean持久化核心volatile保障线程安全
private volatile boolean mIsServiceEnabled = false; // 服务开关缓存状态减少Bean读取次数
// ======================== 配置Bean属性持久化核心volatile保障线程安全======================== // ======================== 单例相关方法区(双重校验锁+构造方法========================
public volatile AppConfigBean mAppConfigBean; // 应用配置Bean /**
* 双重校验锁单例获取方法,线程安全
// ======================== 缓存状态属性减少Bean读取次数提升性能======================== * @param context 上下文不可为null
private volatile boolean mIsServiceEnabled = false; // 服务开关缓存状态 * @return 单例实例
*/
// ======================== 单例构造方法(私有,禁止外部实例化)========================
private AppConfigUtils(Context context) {
LogUtils.d(TAG, "初始化配置工具类");
this.mContext = context.getApplicationContext(); // 强制取应用上下文,杜绝内存泄漏
this.mApplication = (App) context.getApplicationContext();
// 初始化配置Bean
mAppConfigBean = new AppConfigBean();
// 加载持久化配置
loadAppConfig();
LogUtils.d(TAG, "配置工具类初始化完成");
}
// ======================== 单例获取方法(双重校验锁,线程安全,适配多线程)========================
public static AppConfigUtils getInstance(Context context) { public static AppConfigUtils getInstance(Context context) {
String contextType = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, String.format("getInstance() 调用 | 传入Context类型=%s", contextType));
if (context == null) { if (context == null) {
LogUtils.e(TAG, "getInstance: Context不能为空,获取实例失败"); LogUtils.e(TAG, "getInstance() 失败:Context不能为空");
throw new IllegalArgumentException("Context cannot be null"); throw new IllegalArgumentException("Context cannot be null");
} }
if (sInstance == null) { if (sInstance == null) {
synchronized (AppConfigUtils.class) { synchronized (AppConfigUtils.class) {
if (sInstance == null) { if (sInstance == null) {
sInstance = new AppConfigUtils(context); sInstance = new AppConfigUtils(context);
LogUtils.d(TAG, "getInstance: 单例实例创建成功"); LogUtils.d(TAG, "getInstance()单例实例创建成功");
} }
} }
} }
LogUtils.d(TAG, "getInstance():单例实例获取成功");
return sInstance; return sInstance;
} }
// ======================== 核心配置加载/保存方法(内部核心逻辑,优先排列)========================
/** /**
* 加载所有配置(应用配置+服务配置,统一入口,初始化/重载通用) * 私有构造方法,禁止外部实例化
* @param context 上下文内部转换为ApplicationContext
*/
private AppConfigUtils(Context context) {
LogUtils.d(TAG, "AppConfigUtils() 构造方法调用");
this.mContext = context.getApplicationContext();
this.mApplication = (App) context.getApplicationContext();
mAppConfigBean = new AppConfigBean();
loadAppConfig(); // 加载持久化配置
LogUtils.d(TAG, "AppConfigUtils() 构造完成,配置初始化成功");
}
// ======================== 核心配置持久化方法区(加载+保存)========================
/**
* 加载应用配置(初始化/重载通用入口)
* @return 加载后的应用配置Bean
*/ */
public AppConfigBean loadAppConfig() { public AppConfigBean loadAppConfig() {
LogUtils.d(TAG, "loadAllConfig: 开始加载所有配置"); LogUtils.d(TAG, "loadAppConfig() 调用 | 开始加载应用配置");
// 加载应用配置
AppConfigBean savedAppBean = (AppConfigBean) AppConfigBean.loadBean(mContext, AppConfigBean.class); AppConfigBean savedAppBean = (AppConfigBean) AppConfigBean.loadBean(mContext, AppConfigBean.class);
if (savedAppBean != null) { if (savedAppBean != null) {
mAppConfigBean = savedAppBean; mAppConfigBean = savedAppBean;
LogUtils.d(TAG, "loadAllConfig: 应用配置加载成功"); LogUtils.d(TAG, String.format("loadAppConfig() 成功 | 充电阈值=%d%% | 耗电阈值=%d%%",
mAppConfigBean.getChargeReminderValue(), mAppConfigBean.getUsageReminderValue()));
} else { } else {
mAppConfigBean = new AppConfigBean(); mAppConfigBean = new AppConfigBean();
AppConfigBean.saveBean(mContext, mAppConfigBean); AppConfigBean.saveBean(mContext, mAppConfigBean);
LogUtils.d(TAG, "loadAllConfig: 无已保存应用配置,使用默认值并持久化"); LogUtils.d(TAG, "loadAppConfig()无已保存配置,使用默认值并持久化");
} }
return mAppConfigBean;
return mAppConfigBean;
} }
/** /**
* 保存应用配置(内部核心方法,直接持久化,同步通知服务+Activity * 保存应用配置(内部核心方法,直接持久化)
*/ */
private void saveAppConfig() { public void saveAppConfig() {
AppConfigBean.saveBean(mContext, mAppConfigBean); AppConfigBean.saveBean(mContext, mAppConfigBean);
LogUtils.d(TAG, "saveAppConfig: 应用配置保存成功已同步服务和Activity"); LogUtils.d(TAG, "saveAppConfig()应用配置保存成功");
} }
// ======================== 充电提醒配置方法(单独归类,逻辑聚焦======================== // ======================== 充电提醒配置方法区(开关+阈值========================
/** /**
* 设置充电提醒开关状态(直接生效,无弹窗) * 设置充电提醒开关状态
* @param isEnabled 目标状态true=开启false=关闭) * @param isEnabled 目标状态true=开启false=关闭)
*/ */
public void setChargeReminderEnabled(final boolean isEnabled) { public void setChargeReminderEnabled(final boolean isEnabled) {
LogUtils.d(TAG, String.format("setChargeReminderEnabled() 调用 | 传入状态=%b", isEnabled));
if (isEnabled == mAppConfigBean.isEnableChargeReminder()) { if (isEnabled == mAppConfigBean.isEnableChargeReminder()) {
LogUtils.d(TAG, "setChargeReminderEnabled: 充电提醒状态无变化,无需操作"); LogUtils.d(TAG, "setChargeReminderEnabled()充电提醒状态无变化,无需操作");
return; return;
} }
mAppConfigBean.setEnableChargeReminder(isEnabled); mAppConfigBean.setEnableChargeReminder(isEnabled);
saveAppConfig(); saveAppConfig();
LogUtils.d(TAG, "setChargeReminderEnabled: 充电提醒状态更新为=" + (isEnabled ? "开启" : "关闭")); LogUtils.d(TAG, String.format("setChargeReminderEnabled() 成功 | 充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
} }
/** /**
@@ -116,23 +126,26 @@ public class AppConfigUtils {
*/ */
public boolean isChargeReminderEnabled() { public boolean isChargeReminderEnabled() {
boolean isEnabled = mAppConfigBean.isEnableChargeReminder(); boolean isEnabled = mAppConfigBean.isEnableChargeReminder();
LogUtils.d(TAG, "isChargeReminderEnabled: 获取充电提醒状态=" + (isEnabled ? "开启" : "关闭")); LogUtils.d(TAG, String.format("isChargeReminderEnabled()获取充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
return isEnabled; return isEnabled;
} }
/** /**
* 设置充电提醒阈值(直接生效无弹窗自动校准范围适配API30数据安全 * 设置充电提醒阈值(自动校准0-100
* @param value 目标阈值自动校准0-100 * @param value 目标阈值
*/ */
public void setChargeReminderValue(final int value) { public void setChargeReminderValue(final int value) {
LogUtils.d(TAG, String.format("setChargeReminderValue() 调用 | 传入阈值=%d", value));
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE); final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getChargeReminderValue()) { if (calibratedValue == mAppConfigBean.getChargeReminderValue()) {
LogUtils.d(TAG, "setChargeReminderValue: 充电提醒阈值无变化,无需操作"); LogUtils.d(TAG, "setChargeReminderValue()充电提醒阈值无变化,无需操作");
return; return;
} }
mAppConfigBean.setChargeReminderValue(calibratedValue); mAppConfigBean.setChargeReminderValue(calibratedValue);
saveAppConfig(); saveAppConfig();
LogUtils.d(TAG, "setChargeReminderValue: 充电提醒阈值更新为=" + calibratedValue + "%"); LogUtils.d(TAG, String.format("setChargeReminderValue() 成功 | 充电提醒阈值=%d%%", calibratedValue));
} }
/** /**
@@ -141,24 +154,26 @@ public class AppConfigUtils {
*/ */
public int getChargeReminderValue() { public int getChargeReminderValue() {
int value = mAppConfigBean.getChargeReminderValue(); int value = mAppConfigBean.getChargeReminderValue();
LogUtils.d(TAG, "getChargeReminderValue: 获取充电提醒阈值=" + value + "%"); LogUtils.d(TAG, String.format("getChargeReminderValue()获取充电提醒阈值=%d%%", value));
return value; return value;
} }
// ======================== 耗电提醒配置方法区(开关+阈值)========================
// ======================== 耗电提醒配置方法(单独归类,逻辑聚焦)========================
/** /**
* 设置耗电提醒开关状态(直接生效,无弹窗) * 设置耗电提醒开关状态
* @param isEnabled 目标状态true=开启false=关闭) * @param isEnabled 目标状态true=开启false=关闭)
*/ */
public void setUsageReminderEnabled(final boolean isEnabled) { public void setUsageReminderEnabled(final boolean isEnabled) {
LogUtils.d(TAG, String.format("setUsageReminderEnabled() 调用 | 传入状态=%b", isEnabled));
if (isEnabled == mAppConfigBean.isEnableUsageReminder()) { if (isEnabled == mAppConfigBean.isEnableUsageReminder()) {
LogUtils.d(TAG, "setUsageReminderEnabled: 耗电提醒状态无变化,无需操作"); LogUtils.d(TAG, "setUsageReminderEnabled()耗电提醒状态无变化,无需操作");
return; return;
} }
mAppConfigBean.setEnableUsageReminder(isEnabled); mAppConfigBean.setEnableUsageReminder(isEnabled);
saveAppConfig(); saveAppConfig();
LogUtils.d(TAG, "setUsageReminderEnabled: 耗电提醒状态更新为=" + (isEnabled ? "开启" : "关闭")); LogUtils.d(TAG, String.format("setUsageReminderEnabled() 成功 | 耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
} }
/** /**
@@ -167,23 +182,26 @@ public class AppConfigUtils {
*/ */
public boolean isUsageReminderEnabled() { public boolean isUsageReminderEnabled() {
boolean isEnabled = mAppConfigBean.isEnableUsageReminder(); boolean isEnabled = mAppConfigBean.isEnableUsageReminder();
LogUtils.d(TAG, "isUsageReminderEnabled: 获取耗电提醒状态=" + (isEnabled ? "开启" : "关闭")); LogUtils.d(TAG, String.format("isUsageReminderEnabled()获取耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
return isEnabled; return isEnabled;
} }
/** /**
* 设置耗电提醒阈值(直接生效,无弹窗,自动校准范围,适配小米手机电量跳变 * 设置耗电提醒阈值(自动校准0-100
* @param value 目标阈值自动校准0-100 * @param value 目标阈值
*/ */
public void setUsageReminderValue(final int value) { public void setUsageReminderValue(final int value) {
LogUtils.d(TAG, String.format("setUsageReminderValue() 调用 | 传入阈值=%d", value));
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE); final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getUsageReminderValue()) { if (calibratedValue == mAppConfigBean.getUsageReminderValue()) {
LogUtils.d(TAG, "setUsageReminderValue: 耗电提醒阈值无变化,无需操作"); LogUtils.d(TAG, "setUsageReminderValue()耗电提醒阈值无变化,无需操作");
return; return;
} }
mAppConfigBean.setUsageReminderValue(calibratedValue); mAppConfigBean.setUsageReminderValue(calibratedValue);
saveAppConfig(); saveAppConfig();
LogUtils.d(TAG, "setUsageReminderValue: 耗电提醒阈值更新为=" + calibratedValue + "%"); LogUtils.d(TAG, String.format("setUsageReminderValue() 成功 | 耗电提醒阈值=%d%%", calibratedValue));
} }
/** /**
@@ -192,23 +210,25 @@ public class AppConfigUtils {
*/ */
public int getUsageReminderValue() { public int getUsageReminderValue() {
int value = mAppConfigBean.getUsageReminderValue(); int value = mAppConfigBean.getUsageReminderValue();
LogUtils.d(TAG, "getUsageReminderValue: 获取耗电提醒阈值=" + value + "%"); LogUtils.d(TAG, String.format("getUsageReminderValue()获取耗电提醒阈值=%d%%", value));
return value; return value;
} }
// ======================== 实时电池状态配置方法区(内存缓存,不持久化)========================
// ======================== 实时电池状态配置方法(临时缓存,不持久化,无需弹窗)========================
/** /**
* 设置当前充电状态(仅内存缓存,不持久化 * 设置当前充电状态(仅内存缓存)
* @param isCharging 充电状态true=充电中false=未充电) * @param isCharging 充电状态true=充电中false=未充电)
*/ */
public void setCharging(boolean isCharging) { public void setCharging(boolean isCharging) {
LogUtils.d(TAG, String.format("setCharging() 调用 | 传入状态=%b", isCharging));
if (isCharging == mAppConfigBean.isCharging()) { if (isCharging == mAppConfigBean.isCharging()) {
LogUtils.d(TAG, "setCharging: 充电状态无变化,无需操作"); LogUtils.d(TAG, "setCharging()充电状态无变化,无需操作");
return; return;
} }
mAppConfigBean.setIsCharging(isCharging); mAppConfigBean.setIsCharging(isCharging);
LogUtils.d(TAG, "setCharging: 充电状态更新为=" + (isCharging ? "充电中" : "未充电")); LogUtils.d(TAG, String.format("setCharging() 成功 | 充电状态=%s", isCharging ? "充电中" : "未充电"));
} }
/** /**
@@ -217,22 +237,25 @@ public class AppConfigUtils {
*/ */
public boolean isCharging() { public boolean isCharging() {
boolean isCharging = mAppConfigBean.isCharging(); boolean isCharging = mAppConfigBean.isCharging();
LogUtils.d(TAG, "isCharging: 获取充电状态=" + (isCharging ? "充电中" : "未充电")); LogUtils.d(TAG, String.format("isCharging()获取充电状态=%s", isCharging ? "充电中" : "未充电"));
return isCharging; return isCharging;
} }
/** /**
* 设置当前电池电量(仅内存缓存,不持久化,自动校准范围 * 设置当前电池电量(仅内存缓存,自动校准0-100
* @param value 当前电量自动校准0-100 * @param value 当前电量
*/ */
public void setCurrentBatteryValue(int value) { public void setCurrentBatteryValue(int value) {
LogUtils.d(TAG, String.format("setCurrentBatteryValue() 调用 | 传入电量=%d", value));
int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE); int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getCurrentBatteryValue()) {
LogUtils.d(TAG, "setCurrentBatteryValue: 电池电量无变化,无需操作"); if (calibratedValue == App.sQuantityOfElectricity) {
LogUtils.d(TAG, "setCurrentBatteryValue():电池电量无变化,无需操作");
return; return;
} }
mAppConfigBean.setCurrentBatteryValue(calibratedValue);
LogUtils.d(TAG, "setCurrentBatteryValue: 电池电量更新为=" + calibratedValue + "%"); App.sQuantityOfElectricity = calibratedValue;
LogUtils.d(TAG, String.format("setCurrentBatteryValue() 成功 | 电池电量=%d%%", calibratedValue));
} }
/** /**
@@ -240,26 +263,28 @@ public class AppConfigUtils {
* @return 当前电池电量0-100 * @return 当前电池电量0-100
*/ */
public int getCurrentBatteryValue() { public int getCurrentBatteryValue() {
int value = mAppConfigBean.getCurrentBatteryValue(); int value = App.sQuantityOfElectricity;
LogUtils.d(TAG, "getCurrentBatteryValue: 获取电池电量=" + value + "%"); LogUtils.d(TAG, String.format("getCurrentBatteryValue()获取电池电量=%d%%", value));
return value; return value;
} }
// ======================== 间隔配置方法区(持久化)========================
// ======================== 间隔配置方法(持久化存储,直接生效,无弹窗)========================
/** /**
* 设置提醒间隔时间(直接生效,无弹窗,自动校准最小1000ms * 设置提醒间隔时间自动校准最小1000ms
* @param interval 目标间隔单位ms * @param interval 目标间隔单位ms
*/ */
public void setReminderIntervalTime(final int interval) { public void setReminderIntervalTime(final int interval) {
LogUtils.d(TAG, String.format("setReminderIntervalTime() 调用 | 传入间隔=%dms", interval));
final int calibratedInterval = Math.max(interval, MIN_INTERVAL_TIME); final int calibratedInterval = Math.max(interval, MIN_INTERVAL_TIME);
if (calibratedInterval == mAppConfigBean.getReminderIntervalTime()) { if (calibratedInterval == mAppConfigBean.getReminderIntervalTime()) {
LogUtils.d(TAG, "setReminderIntervalTime: 提醒间隔无变化,无需操作"); LogUtils.d(TAG, "setReminderIntervalTime()提醒间隔无变化,无需操作");
return; return;
} }
mAppConfigBean.setReminderIntervalTime(calibratedInterval); mAppConfigBean.setReminderIntervalTime(calibratedInterval);
saveAppConfig(); saveAppConfig();
LogUtils.d(TAG, "setReminderIntervalTime: 提醒间隔更新为=" + calibratedInterval + "ms"); LogUtils.d(TAG, String.format("setReminderIntervalTime() 成功 | 提醒间隔=%dms", calibratedInterval));
} }
/** /**
@@ -268,23 +293,26 @@ public class AppConfigUtils {
*/ */
public int getReminderIntervalTime() { public int getReminderIntervalTime() {
int interval = mAppConfigBean.getReminderIntervalTime(); int interval = mAppConfigBean.getReminderIntervalTime();
LogUtils.d(TAG, "getReminderIntervalTime: 获取提醒间隔=" + interval + "ms"); LogUtils.d(TAG, String.format("getReminderIntervalTime()获取提醒间隔=%dms", interval));
return interval; return interval;
} }
/** /**
* 设置电量检测间隔(直接生效,无弹窗,自动校准最小500ms与RemindThread同步 * 设置电量检测间隔自动校准最小500ms
* @param interval 目标间隔单位ms * @param interval 目标间隔单位ms
*/ */
public void setBatteryDetectInterval(final int interval) { public void setBatteryDetectInterval(final int interval) {
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 调用 | 传入间隔=%dms", interval));
final int calibratedInterval = Math.max(interval, MIN_DETECT_INTERVAL); final int calibratedInterval = Math.max(interval, MIN_DETECT_INTERVAL);
if (calibratedInterval == mAppConfigBean.getBatteryDetectInterval()) { if (calibratedInterval == mAppConfigBean.getBatteryDetectInterval()) {
LogUtils.d(TAG, "setBatteryDetectInterval: 检测间隔无变化,无需操作"); LogUtils.d(TAG, "setBatteryDetectInterval()检测间隔无变化,无需操作");
return; return;
} }
mAppConfigBean.setBatteryDetectInterval(calibratedInterval); mAppConfigBean.setBatteryDetectInterval(calibratedInterval);
saveAppConfig(); saveAppConfig();
LogUtils.d(TAG, "setBatteryDetectInterval: 电量检测间隔更新为=" + calibratedInterval + "ms"); LogUtils.d(TAG, String.format("setBatteryDetectInterval() 成功 | 电量检测间隔=%dms", calibratedInterval));
} }
/** /**
@@ -293,23 +321,38 @@ public class AppConfigUtils {
*/ */
public int getBatteryDetectInterval() { public int getBatteryDetectInterval() {
int interval = mAppConfigBean.getBatteryDetectInterval(); int interval = mAppConfigBean.getBatteryDetectInterval();
LogUtils.d(TAG, "getBatteryDetectInterval: 获取电量检测间隔=" + interval + "ms"); LogUtils.d(TAG, String.format("getBatteryDetectInterval()获取电量检测间隔=%dms", interval));
return interval; return interval;
} }
public boolean isServiceEnabled() { // ======================== 服务开关配置方法区独立Bean========================
// 加载服务配置 /**
* 获取服务开关状态
* @return 服务开关状态true=开启false=关闭)
*/
public boolean isServiceEnabled() {
LogUtils.d(TAG, "isServiceEnabled() 调用 | 开始获取服务开关状态");
ControlCenterServiceBean savedServiceBean = (ControlCenterServiceBean) ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class); ControlCenterServiceBean savedServiceBean = (ControlCenterServiceBean) ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
if (savedServiceBean != null) { if (savedServiceBean != null) {
return savedServiceBean.isEnableService(); boolean isEnabled = savedServiceBean.isEnableService();
LogUtils.d(TAG, String.format("isServiceEnabled():服务开关状态=%b", isEnabled));
return isEnabled;
} else { } else {
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false)); ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false));
LogUtils.d(TAG, "isServiceEnabled():无已保存服务配置,默认关闭并持久化");
return false; return false;
} }
} }
public void setIsServiceEnabled(boolean isServiceEnabled) { /**
* 设置服务开关状态
* @param isServiceEnabled 目标状态true=开启false=关闭)
*/
public void setIsServiceEnabled(boolean isServiceEnabled) {
LogUtils.d(TAG, String.format("setIsServiceEnabled() 调用 | 传入状态=%b", isServiceEnabled));
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(isServiceEnabled)); ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(isServiceEnabled));
} LogUtils.d(TAG, String.format("setIsServiceEnabled() 成功 | 服务开关状态=%b", isServiceEnabled));
}
} }

View File

@@ -1,23 +1,26 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import android.content.Context; import android.content.Context;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import cc.winboll.studio.libappbase.LogUtils;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 09:14 * @Date 2025/12/11 09:14
* @Describe Assets 目录拷贝工具类 * @Describe Assets 目录拷贝工具类
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径 * 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
* 适配Java7 | API30 | 递归拷贝 | 覆盖写入
*/ */
public class AssetsCopyUtils { public class AssetsCopyUtils {
// ======================== 静态常量区 ========================
public static final String TAG = "AssetsCopyUtils"; public static final String TAG = "AssetsCopyUtils";
private static final int BUFFER_SIZE = 1024 * 8; private static final int BUFFER_SIZE = 1024 * 8; // 8KB 缓冲区,平衡性能与内存占用
// ======================== 公共快捷方法区(对外入口) ========================
/** /**
* 拷贝 assets/images/ 目录到指定目标目录 * 拷贝 assets/images/ 目录到指定目标目录
* @param context 上下文 * @param context 上下文
@@ -25,10 +28,14 @@ public class AssetsCopyUtils {
* @return 拷贝是否成功 * @return 拷贝是否成功
*/ */
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) { public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
LogUtils.d(TAG, "copyAssetsImagesToDir() 调用,目标路径:" + targetDirPath);
// 拷贝 assets/images 根目录 // 拷贝 assets/images 根目录
return copyAssetsDirToDir(context, "images", targetDirPath); boolean result = copyAssetsDirToDir(context, "images", targetDirPath);
LogUtils.d(TAG, "copyAssetsImagesToDir() 执行完成,结果:" + result);
return result;
} }
// ======================== 公共核心方法区(递归拷贝目录) ========================
/** /**
* 递归拷贝 assets 下指定目录到目标目录 * 递归拷贝 assets 下指定目录到目标目录
* @param context 上下文 * @param context 上下文
@@ -37,10 +44,16 @@ public class AssetsCopyUtils {
* @return 拷贝是否成功 * @return 拷贝是否成功
*/ */
public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) { public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) {
LogUtils.d(TAG, "copyAssetsDirToDir() 调用,源目录:" + assetsDir + ",目标路径:" + targetDirPath);
if (context == null) {
LogUtils.e(TAG, "copyAssetsDirToDir() 拷贝失败:上下文为空");
return false;
}
File targetDir = new File(targetDirPath); File targetDir = new File(targetDirPath);
// 创建目标目录(含多级父目录) // 创建目标目录(含多级父目录)
if (!targetDir.exists() && !targetDir.mkdirs()) { if (!targetDir.exists() && !targetDir.mkdirs()) {
Log.e(TAG, "创建目标目录失败:" + targetDirPath); LogUtils.e(TAG, "copyAssetsDirToDir() 创建目标目录失败:" + targetDirPath);
return false; return false;
} }
@@ -48,7 +61,7 @@ public class AssetsCopyUtils {
// 获取 assets 目录下的文件/子目录列表 // 获取 assets 目录下的文件/子目录列表
String[] fileList = context.getAssets().list(assetsDir); String[] fileList = context.getAssets().list(assetsDir);
if (fileList == null || fileList.length == 0) { if (fileList == null || fileList.length == 0) {
Log.d(TAG, "assets 目录为空:" + assetsDir); LogUtils.d(TAG, "copyAssetsDirToDir() assets 目录为空:" + assetsDir);
return true; return true;
} }
@@ -61,23 +74,26 @@ public class AssetsCopyUtils {
if (subFileList != null && subFileList.length > 0) { if (subFileList != null && subFileList.length > 0) {
// 是子目录,递归拷贝 // 是子目录,递归拷贝
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) { if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
LogUtils.e(TAG, "copyAssetsDirToDir() 递归拷贝子目录失败:" + assetsFilePath);
return false; return false;
} }
} else { } else {
// 是文件,直接拷贝 // 是文件,直接拷贝
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) { if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
LogUtils.e(TAG, "copyAssetsDirToDir() 拷贝文件失败:" + assetsFilePath);
return false; return false;
} }
} }
} }
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath); LogUtils.d(TAG, "copyAssetsDirToDir() assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage()); LogUtils.e(TAG, "copyAssetsDirToDir() 拷贝 assets 目录异常:" + e.getMessage(), e);
return false; return false;
} }
} }
// ======================== 私有辅助方法区(单个文件拷贝) ========================
/** /**
* 拷贝 assets 下单个文件到指定路径 * 拷贝 assets 下单个文件到指定路径
* @param context 上下文 * @param context 上下文
@@ -86,14 +102,16 @@ public class AssetsCopyUtils {
* @return 拷贝是否成功 * @return 拷贝是否成功
*/ */
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) { public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) {
LogUtils.d(TAG, "copyAssetsFileToDir() 调用,源文件:" + assetsFilePath + ",目标文件:" + targetFilePath);
InputStream inputStream = null; InputStream inputStream = null;
OutputStream outputStream = null; OutputStream outputStream = null;
try { try {
inputStream = context.getAssets().open(assetsFilePath); inputStream = context.getAssets().open(assetsFilePath);
File targetFile = new File(targetFilePath); File targetFile = new File(targetFilePath);
// 覆盖已存在的文件 // 覆盖已存在的文件
if (targetFile.exists() && !targetFile.delete()) { if (targetFile.exists() && !targetFile.delete()) {
Log.w(TAG, "覆盖目标文件失败,跳过:" + targetFilePath); LogUtils.w(TAG, "copyAssetsFileToDir() 覆盖目标文件失败,跳过:" + targetFilePath);
return true; return true;
} }
@@ -103,18 +121,22 @@ public class AssetsCopyUtils {
while ((length = inputStream.read(buffer)) != -1) { while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length); outputStream.write(buffer, 0, length);
} }
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath); LogUtils.d(TAG, "copyAssetsFileToDir() 文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage()); LogUtils.e(TAG, "copyAssetsFileToDir() 拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage(), e);
return false; return false;
} finally { } finally {
// 关闭流 // 关闭流
try { try {
if (inputStream != null) inputStream.close(); if (inputStream != null) {
if (outputStream != null) outputStream.close(); inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "关闭流异常:" + e.getMessage()); LogUtils.e(TAG, "copyAssetsFileToDir() 关闭流异常:" + e.getMessage(), e);
} }
} }
} }

View File

@@ -16,9 +16,10 @@ public class BatteryUtils {
public static final String TAG = "BatteryUtils"; public static final String TAG = "BatteryUtils";
// 电池电量计算常量 // 电池电量计算常量
private static final int BATTERY_SCALE_DEFAULT = 100; private static final int BATTERY_SCALE_DEFAULT = 100; // 电量刻度默认值
private static final int BATTERY_LEVEL_MIN = 0; private static final int BATTERY_LEVEL_MIN = 0; // 电量百分比最小值
private static final int BATTERY_LEVEL_MAX = 100; private static final int BATTERY_LEVEL_MAX = 100; // 电量百分比最大值
private static final int EXTRA_STATUS_DEFAULT = -1; // 电池状态默认值
// ================================== 工具方法(静态方法,无状态设计)================================= // ================================== 工具方法(静态方法,无状态设计)=================================
/** /**
@@ -27,16 +28,21 @@ public class BatteryUtils {
* @return true=充电中/已充满false=未充电 * @return true=充电中/已充满false=未充电
*/ */
public static boolean isCharging(Intent intent) { public static boolean isCharging(Intent intent) {
LogUtils.d(TAG, "isCharging: 调用 | intent=" + intent); LogUtils.d(TAG, "isCharging】调用开始");
// 入参非空校验 // 入参非空校验
if (intent == null) { if (intent == null) {
LogUtils.e(TAG, "isCharging: intent为空返回false"); LogUtils.e(TAG, "isCharging】入参异常:intent为空返回false");
return false; return false;
} }
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); // 解析电池状态
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, EXTRA_STATUS_DEFAULT);
LogUtils.d(TAG, "isCharging: 解析完成 | status=" + status + " | result=" + isCharging); LogUtils.d(TAG, "isCharging】解析电池状态:status=" + status);
// 判断充电状态(充电中/已充满均视为充电状态)
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING
|| status == BatteryManager.BATTERY_STATUS_FULL;
LogUtils.d(TAG, "【isCharging】调用结束 | 充电状态=" + isCharging);
return isCharging; return isCharging;
} }
@@ -46,29 +52,30 @@ public class BatteryUtils {
* @return 电量百分比异常返回0 * @return 电量百分比异常返回0
*/ */
public static int getCurrentBatteryLevel(Intent intent) { public static int getCurrentBatteryLevel(Intent intent) {
LogUtils.d(TAG, "getCurrentBatteryLevel: 调用 | intent=" + intent); LogUtils.d(TAG, "getCurrentBatteryLevel】调用开始");
// 入参非空校验 // 入参非空校验
if (intent == null) { if (intent == null) {
LogUtils.e(TAG, "getCurrentBatteryLevel: intent为空返回0"); LogUtils.e(TAG, "getCurrentBatteryLevel】入参异常:intent为空返回0");
return BATTERY_LEVEL_MIN; return BATTERY_LEVEL_MIN;
} }
// 解析电量原始值与刻度值 // 解析电量原始值与刻度值
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN); int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE_DEFAULT); int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE_DEFAULT);
LogUtils.d(TAG, "getCurrentBatteryLevel: 原始值 | level=" + level + " | scale=" + scale); LogUtils.d(TAG, "getCurrentBatteryLevel】解析原始数据 | level=" + level + " | scale=" + scale);
// 计算并校验电量百分比避免除以0或数值越界 // 计算并校验电量百分比避免除以0或数值越界
int batteryLevel; int batteryLevel;
if (scale <= 0) { if (scale <= 0) {
LogUtils.w(TAG, "【getCurrentBatteryLevel】刻度值无效scale=" + scale + "直接使用level值");
batteryLevel = level; batteryLevel = level;
LogUtils.w(TAG, "getCurrentBatteryLevel: scale无效直接使用level值");
} else { } else {
batteryLevel = level * BATTERY_SCALE_DEFAULT / scale; batteryLevel = level * BATTERY_SCALE_DEFAULT / scale;
} }
// 确保电量值在0-100范围内 // 确保电量值在0-100范围内
batteryLevel = Math.max(BATTERY_LEVEL_MIN, Math.min(batteryLevel, BATTERY_LEVEL_MAX)); batteryLevel = Math.max(BATTERY_LEVEL_MIN, Math.min(batteryLevel, BATTERY_LEVEL_MAX));
LogUtils.d(TAG, "getCurrentBatteryLevel: 计算完成 | batteryLevel=" + batteryLevel + "%"); LogUtils.d(TAG, "getCurrentBatteryLevel】调用结束 | 电量百分比=" + batteryLevel + "%");
return batteryLevel; return batteryLevel;
} }
} }

View File

@@ -2,46 +2,66 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 01:57 * @Date 2025/12/11 01:57
* @Describe 单例 Bitmap 缓存工具类Java 7 兼容) * @Describe 单例 Bitmap 缓存工具类Java 7 兼容)- 极致强制缓存版(无图片压缩)
* 功能:内存缓存 Bitmap支持路径关联缓存、全局获取、缓存清空、SP 持久化最后缓存路径、构造时预加载 * 功能:内存缓存 Bitmap支持路径关联缓存、全局获取、缓存清空、SP 持久化最后缓存路径、构造时预加载
* 特点1. 单例模式 2. 压缩加载避免OOM 3. 路径-Bitmap 映射 4. 线程安全 5. SP 持久化最后缓存路径 6. 构造时预加载 * 特点1. 单例模式 2. 硬引用唯一缓存(极致强制保持,任何情况不自动回收) 3. 路径-Bitmap 映射 4. 线程安全
* 5. SP 持久化最后缓存路径 6. 构造时预加载 7. 引用计数防误回收 8. 无图片压缩,保留原始品质
* 核心策略无论内存如何紧张强制保持已缓存的Bitmap保留图片原始品质永不自动清理
*/ */
public class BitmapCacheUtils { public class BitmapCacheUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "BitmapCacheUtils"; public static final String TAG = "BitmapCacheUtils";
// 最大图片尺寸适配1080P屏幕可根据需求调整
private static final int MAX_WIDTH = 1080;
private static final int MAX_HEIGHT = 1920;
// SP 相关常量 // SP 相关常量
private static final String SP_NAME = "BitmapCacheSP"; private static final String SP_NAME = "BitmapCacheSP";
private static final String SP_KEY_LAST_CACHE_PATH = "last_cache_image_path"; private static final String SP_KEY_LAST_CACHE_PATH = "last_cache_image_path";
// 单例实例volatile 保证多线程可见性) // Bitmap 解码常量
private static final int BITMAP_SAMPLE_SIZE_ORIGINAL = 1; // 无压缩采样率
private static final Bitmap.Config BITMAP_CONFIG_DEFAULT = Bitmap.Config.ARGB_8888; // 全彩品质配置
// ================================== 成员变量按功能分类volatile 保证多线程可见性)=================================
// 单例实例
private static volatile BitmapCacheUtils sInstance; private static volatile BitmapCacheUtils sInstance;
// 路径-Bitmap 缓存容器(内存缓存 // 路径-Bitmap 硬引用缓存(极致强制保持,永不自动回收
private final Map<String, Bitmap> mBitmapCacheMap; private final Map<String, Bitmap> mHardCacheMap;
// 路径-引用计数 映射(仅统计,不影响缓存生命周期)
private final Map<String, Integer> mRefCountMap;
// SP 实例(用于持久化最后缓存路径) // SP 实例(用于持久化最后缓存路径)
private final SharedPreferences mSp; private final SharedPreferences mSp;
// 私有构造器(单例模式) // ================================== 单例方法(双重校验锁,线程安全)=================================
/**
* 私有构造器(单例模式)
*/
private BitmapCacheUtils() { private BitmapCacheUtils() {
mBitmapCacheMap = new HashMap<>(); LogUtils.d(TAG, "【BitmapCacheUtils】单例构造开始");
//App.notifyMessage(TAG, "【BitmapCacheUtils】单例构造开始");
// 使用 ConcurrentHashMap 保证线程安全,避免手动同步
mHardCacheMap = new ConcurrentHashMap<>();
mRefCountMap = new ConcurrentHashMap<>();
// 初始化 SP使用 App 全局上下文,避免内存泄漏) // 初始化 SP使用 App 全局上下文,避免内存泄漏)
mSp = App.getInstance().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); mSp = App.getInstance().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
// 构造时自动预加载 SP 中保存的最后一次缓存路径的图片 // 构造时自动预加载 SP 中保存的最后一次缓存路径的图片
preloadLastCachedBitmap(); preloadLastCachedBitmap();
// 注册内存状态监听(仅记录日志,不清理缓存)
registerMemoryStatusListener();
LogUtils.d(TAG, "【BitmapCacheUtils】单例构造完成极致强制缓存策略已启用");
} }
/** /**
@@ -58,180 +78,321 @@ public class BitmapCacheUtils {
return sInstance; return sInstance;
} }
// ================================== 对外监控接口App 类调用专用)=================================
/** /**
* 核心接口:根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP * 获取当前缓存 Bitmap 数量
* @return 缓存的 Bitmap 数量
*/
public int getCacheCount() {
int count = mHardCacheMap.size();
LogUtils.d(TAG, "【getCacheCount】当前缓存 Bitmap 数量 - " + count);
return count;
}
/**
* 获取当前缓存的所有图片路径集合
* @return 路径集合
*/
public Set<String> getCachedPaths() {
Set<String> paths = mHardCacheMap.keySet();
LogUtils.d(TAG, "【getCachedPaths】当前缓存路径数量 - " + paths.size());
return paths;
}
/**
* 估算当前缓存的总内存占用(单位:字节)
* @return 总内存占用
*/
public long getTotalCacheSize() {
long totalSize = 0;
for (Bitmap bitmap : mHardCacheMap.values()) {
if (isBitmapValid(bitmap)) {
if (Build.VERSION.SDK_INT >= 12) {
totalSize += bitmap.getByteCount();
} else {
totalSize += bitmap.getRowBytes() * bitmap.getHeight();
}
}
}
LogUtils.d(TAG, "【getTotalCacheSize】当前缓存总内存占用 - " + totalSize + " 字节");
return totalSize;
}
// ================================== 对外核心接口:缓存操作(无压缩)=================================
/**
* 直接缓存已解码的 Bitmap适配 BackgroundView 改进需求)
* @param imagePath 图片绝对路径
* @param bitmap 已解码的有效 Bitmap
* @return 缓存后的 Bitmap / null参数无效
*/
public Bitmap cacheBitmap(String imagePath, Bitmap bitmap) {
LogUtils.d(TAG, "【cacheBitmap】调用开始直接缓存已解码 Bitmap| 路径=" + imagePath);
// 入参非空校验
if (TextUtils.isEmpty(imagePath) || !isBitmapValid(bitmap)) {
LogUtils.e(TAG, "【cacheBitmap】入参异常路径为空或 Bitmap 无效");
return null;
}
// 极致强制:直接存入硬引用缓存,覆盖旧值(若存在)
mHardCacheMap.put(imagePath, bitmap);
// 初始化引用计数为1若不存在
mRefCountMap.putIfAbsent(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "【cacheBitmap】调用成功直接缓存已解码 Bitmap| 路径=" + imagePath);
return bitmap;
}
/**
* 根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP
* @param imagePath 图片绝对路径 * @param imagePath 图片绝对路径
* @return 缓存成功的 Bitmap / null路径无效/文件不存在/解码失败) * @return 缓存成功的 Bitmap / null路径无效/文件不存在/解码失败)
*/ */
public Bitmap cacheBitmap(String imagePath) { public Bitmap cacheBitmap(String imagePath) {
LogUtils.d(TAG, "【cacheBitmap】调用开始路径缓存| 路径=" + imagePath);
// 入参非空校验
if (TextUtils.isEmpty(imagePath)) { if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "cacheBitmap: 图片路径为空"); LogUtils.e(TAG, "cacheBitmap】入参异常:图片路径为空");
return null; return null;
} }
// 文件有效性校验
File imageFile = new File(imagePath); File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "cacheBitmap: 图片文件无效不存在/非文件/空文件 - " + imagePath); LogUtils.e(TAG, "cacheBitmap文件无效不存在/非文件/空文件 | 路径=" + imagePath);
return null; return null;
} }
// 已缓存则直接返回,避免重复加载 // 已缓存则直接返回,避免重复加载
if (mBitmapCacheMap.containsKey(imagePath)) { Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
Bitmap cachedBitmap = mBitmapCacheMap.get(imagePath); if (isBitmapValid(hardCacheBitmap)) {
// 额外校验缓存的Bitmap是否有效 LogUtils.d(TAG, "【cacheBitmap】硬引用缓存命中引用计数+1 | 路径=" + imagePath);
if (cachedBitmap != null && !cachedBitmap.isRecycled()) { // 引用计数+1
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath); increaseRefCount(imagePath);
// 持久化当前路径到 SP(更新最后缓存路径) // 持久化当前路径到 SP
saveLastCachePathToSp(imagePath); saveLastCachePathToSp(imagePath);
return cachedBitmap; LogUtils.d(TAG, "cacheBitmap】调用成功(缓存命中)| 路径=" + imagePath);
} else { return hardCacheBitmap;
// 缓存的Bitmap已失效移除后重新加载
mBitmapCacheMap.remove(imagePath);
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效移除后重新加载 - " + imagePath);
}
} }
// 压缩加载 Bitmap避免OOM // 压缩解码 Bitmap保留原始品质
Bitmap bitmap = decodeCompressedBitmap(imagePath); Bitmap bitmap = decodeOriginalBitmap(imagePath);
if (bitmap != null) { if (bitmap != null) {
// 存入缓存容器 // 极致强制:存入硬引用缓存,永不自动回收
mBitmapCacheMap.put(imagePath, bitmap); mHardCacheMap.put(imagePath, bitmap);
// 持久化当前路径到 SP更新最后缓存路径 // 初始化引用计数为1
mRefCountMap.put(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath); saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径 - " + imagePath); LogUtils.d(TAG, "cacheBitmap】调用成功(新缓存)| 路径=" + imagePath);
} else { } else {
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath); LogUtils.e(TAG, "cacheBitmap】调用失败:图片解码失败 | 路径=" + imagePath);
} }
return bitmap; return bitmap;
} }
/** /**
* 核心接口:根据路径获取缓存的 Bitmap * 根据路径获取缓存的 Bitmap
* @param imagePath 图片绝对路径 * @param imagePath 图片绝对路径
* @return 缓存的有效 Bitmap / null未缓存/已回收) * @return 缓存的有效 Bitmap / null未缓存/已回收)
*/ */
public Bitmap getCachedBitmap(String imagePath) { public Bitmap getCachedBitmap(String imagePath) {
LogUtils.d(TAG, "【getCachedBitmap】调用开始 | 路径=" + imagePath);
// 入参非空校验
if (TextUtils.isEmpty(imagePath)) { if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【getCachedBitmap】入参异常图片路径为空");
return null; return null;
} }
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
// 校验Bitmap是否有效 // 仅从硬引用缓存获取,无任何 fallback
if (bitmap != null && bitmap.isRecycled()) { Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
mBitmapCacheMap.remove(imagePath); if (isBitmapValid(hardCacheBitmap)) {
return null; LogUtils.d(TAG, "【getCachedBitmap】调用成功缓存命中| 路径=" + imagePath);
return hardCacheBitmap;
}
// 缓存未命中或 Bitmap 已失效(极致强制策略下,理论上不会出现已回收情况)
LogUtils.w(TAG, "【getCachedBitmap】调用失败缓存未命中或 Bitmap 已失效 | 路径=" + imagePath);
return null;
}
// ================================== 对外接口:引用计数管理(仅统计,不影响缓存)=================================
/**
* 增加指定路径 Bitmap 的引用计数
* @param imagePath 图片绝对路径
*/
public void increaseRefCount(String imagePath) {
LogUtils.d(TAG, "【increaseRefCount】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【increaseRefCount】入参异常图片路径为空");
return;
}
synchronized (mRefCountMap) {
Integer count = mRefCountMap.get(imagePath);
if (count == null) {
mRefCountMap.put(imagePath, 1);
} else {
mRefCountMap.put(imagePath, count + 1);
}
int newCount = mRefCountMap.get(imagePath);
LogUtils.d(TAG, "【increaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数=" + newCount);
} }
return bitmap;
} }
/** /**
* 清空所有 Bitmap 缓存(释放内存),并清空 SP 中保存的最后缓存路径 * 减少指定路径 Bitmap 的引用计数计数为0时仅标记不回收极致强制缓存策略
* @param imagePath 图片绝对路径
*/
public void decreaseRefCount(String imagePath) {
LogUtils.d(TAG, "【decreaseRefCount】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【decreaseRefCount】入参异常图片路径为空");
return;
}
synchronized (mRefCountMap) {
Integer count = mRefCountMap.get(imagePath);
if (count == null || count <= 0) {
LogUtils.w(TAG, "【decreaseRefCount】引用计数无效路径=" + imagePath);
return;
}
int newCount = count - 1;
if (newCount <= 0) {
// 极致强制缓存策略引用计数为0时仅移除计数绝对不回收 Bitmap
mRefCountMap.remove(imagePath);
LogUtils.d(TAG, "【decreaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数为0极致强制保持 Bitmap");
} else {
mRefCountMap.put(imagePath, newCount);
LogUtils.d(TAG, "【decreaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数=" + newCount);
}
}
}
// ================================== 对外接口:缓存清理(仅手动调用,永不自动执行)=================================
/**
* 清空所有 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行)
*/ */
public void clearAllCache() { public void clearAllCache() {
synchronized (mBitmapCacheMap) { LogUtils.w(TAG, "【clearAllCache】调用开始极致强制缓存策略下需谨慎使用");
for (Bitmap bitmap : mBitmapCacheMap.values()) {
if (bitmap != null && !bitmap.isRecycled()) { // 清空硬引用缓存并回收 Bitmap
bitmap.recycle(); // 主动回收 Bitmap for (Bitmap bitmap : mHardCacheMap.values()) {
} if (isBitmapValid(bitmap)) {
bitmap.recycle();
} }
mBitmapCacheMap.clear();
} }
mHardCacheMap.clear();
// 清空引用计数
mRefCountMap.clear();
// 清空 SP 中保存的最后缓存路径 // 清空 SP 中保存的最后缓存路径
clearLastCachePathInSp(); clearLastCachePathInSp();
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空SP 路径已清除");
LogUtils.d(TAG, "【clearAllCache】调用成功所有 Bitmap 缓存已清空");
} }
/** /**
* 移除指定路径的 Bitmap 缓存 * 移除指定路径的 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行)
* @param imagePath 图片绝对路径 * @param imagePath 图片绝对路径
*/ */
public void removeCachedBitmap(String imagePath) { public void removeCachedBitmap(String imagePath) {
LogUtils.d(TAG, "【removeCachedBitmap】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) { if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【removeCachedBitmap】入参异常图片路径为空");
return; return;
} }
synchronized (mBitmapCacheMap) {
Bitmap bitmap = mBitmapCacheMap.remove(imagePath); synchronized (mRefCountMap) {
if (bitmap != null && !bitmap.isRecycled()) { // 手动移除时才回收 Bitmap
bitmap.recycle(); Bitmap hardBitmap = mHardCacheMap.remove(imagePath);
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath); if (isBitmapValid(hardBitmap)) {
hardBitmap.recycle();
LogUtils.d(TAG, "【removeCachedBitmap】手动回收硬引用缓存 | 路径=" + imagePath);
} }
mRefCountMap.remove(imagePath);
// 若移除的是最后缓存的路径,清空 SP // 若移除的是最后缓存的路径,清空 SP
String lastPath = getLastCachePathFromSp(); String lastPath = getLastCachePathFromSp();
if (imagePath.equals(lastPath)) { if (imagePath.equals(lastPath)) {
clearLastCachePathInSp(); clearLastCachePathInSp();
LogUtils.d(TAG, "removeCachedBitmap: 移除的是最后缓存路径,已清空 SP"); LogUtils.d(TAG, "removeCachedBitmap】移除最后缓存路径,已清空 SP");
} }
} }
LogUtils.d(TAG, "【removeCachedBitmap】调用成功 | 路径=" + imagePath);
} }
// ================================== 内部工具方法(无压缩解码 + Bitmap 有效性判断)=================================
/** /**
* 压缩解码 Bitmap按最大尺寸缩放避免OOM * 压缩解码 Bitmap保留原始品质
* @param imagePath 图片绝对路径 * @param imagePath 图片绝对路径
* @return 解码后的 Bitmap / null文件无效/解码失败) * @return 解码后的 Bitmap / null文件无效/解码失败)
*/ */
private Bitmap decodeCompressedBitmap(String imagePath) { private Bitmap decodeOriginalBitmap(String imagePath) {
LogUtils.d(TAG, "【decodeOriginalBitmap】调用开始 | 路径=" + imagePath);
// 前置校验:确保文件有效 // 前置校验:确保文件有效
File imageFile = new File(imagePath); File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + imagePath); LogUtils.e(TAG, "decodeOriginalBitmap文件无效,跳过解码 | 路径=" + imagePath);
return null; return null;
} }
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
// 第一步:只获取图片尺寸,不加载像素 // 仅获取尺寸用于日志记录,不参与解码逻辑
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, options); BitmapFactory.decodeFile(imagePath, options);
// 校验尺寸是否有效 // 校验尺寸是否有效
if (options.outWidth <= 0 || options.outHeight <= 0) { if (options.outWidth <= 0 || options.outHeight <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 图片尺寸无效 - " + imagePath); LogUtils.e(TAG, "decodeOriginalBitmap图片尺寸无效 | 路径=" + imagePath);
return null; return null;
} }
// 计算缩放比例 LogUtils.d(TAG, "【decodeOriginalBitmap】图片原始尺寸 | 宽=" + options.outWidth + " | 高=" + options.outHeight);
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
// 第二步:加载压缩后的 Bitmap // 无压缩解码配置
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize; options.inSampleSize = BITMAP_SAMPLE_SIZE_ORIGINAL; // 不缩放采样率为1
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存比ARGB_8888少一半内存 options.inPreferredConfig = BITMAP_CONFIG_DEFAULT; // 保留全彩品质
options.inPurgeable = true; options.inPurgeable = false; // 关闭可清除标志,极致强制保持内存
options.inInputShareable = true; options.inInputShareable = false;
options.inDither = true; // 开启抖动,保证色彩还原
options.inScaled = false; // 关闭自动缩放,保留原始尺寸
try { try {
return BitmapFactory.decodeFile(imagePath, options); Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options);
LogUtils.d(TAG, "【decodeOriginalBitmap】解码" + (bitmap != null ? "成功" : "失败") + " | 路径=" + imagePath);
return bitmap;
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath); LogUtils.e(TAG, "decodeOriginalBitmapOOM 异常(无压缩,图片尺寸过大)| 路径=" + imagePath);
// 极致强制缓存策略OOM 时仅放弃当前解码,绝对不清理已缓存的 Bitmap
return null; return null;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e); LogUtils.e(TAG, "decodeOriginalBitmap解码异常 | 路径=" + imagePath, e);
return null; return null;
} }
} }
/** /**
* 计算 Bitmap 缩放比例 * 判断 Bitmap 是否有效(非空且未被回收)
*/ */
private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) { private boolean isBitmapValid(Bitmap bitmap) {
int rawWidth = options.outWidth; boolean isValid = bitmap != null && !bitmap.isRecycled();
int rawHeight = options.outHeight; if (!isValid) {
int inSampleSize = 1; LogUtils.w(TAG, "【isBitmapValid】Bitmap 无效:空或已回收");
if (rawWidth > maxWidth || rawHeight > maxHeight) {
int halfWidth = rawWidth / 2;
int halfHeight = rawHeight / 2;
while ((halfWidth / inSampleSize) >= maxWidth && (halfHeight / inSampleSize) >= maxHeight) {
inSampleSize *= 2;
}
} }
return inSampleSize; return isValid;
} }
// ================================== 内部工具方法SP 持久化相关 ==================================
/** /**
* 从 SP 中获取最后一次缓存的图片路径 * 从 SP 中获取最后一次缓存的图片路径
* @return 最后缓存的路径 / null未保存 * @return 最后缓存的路径 / null未保存
*/ */
private String getLastCachePathFromSp() { private String getLastCachePathFromSp() {
return mSp.getString(SP_KEY_LAST_CACHE_PATH, null); String path = mSp.getString(SP_KEY_LAST_CACHE_PATH, null);
LogUtils.d(TAG, "【getLastCachePathFromSp】获取最后缓存路径 | 路径=" + path);
return path;
} }
/** /**
@@ -239,11 +400,13 @@ public class BitmapCacheUtils {
* @param imagePath 图片绝对路径 * @param imagePath 图片绝对路径
*/ */
private void saveLastCachePathToSp(String imagePath) { private void saveLastCachePathToSp(String imagePath) {
LogUtils.d(TAG, "【saveLastCachePathToSp】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) { if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【saveLastCachePathToSp】入参异常图片路径为空");
return; return;
} }
mSp.edit().putString(SP_KEY_LAST_CACHE_PATH, imagePath).commit(); // Java 7 兼容,使用 commit 而非 apply mSp.edit().putString(SP_KEY_LAST_CACHE_PATH, imagePath).commit(); // Java 7 兼容,使用 commit 而非 apply
LogUtils.d(TAG, "saveLastCachePathToSp: 持久化最后缓存路径 - " + imagePath); LogUtils.d(TAG, "saveLastCachePathToSp】调用成功 | 路径=" + imagePath);
} }
/** /**
@@ -251,27 +414,78 @@ public class BitmapCacheUtils {
*/ */
private void clearLastCachePathInSp() { private void clearLastCachePathInSp() {
mSp.edit().remove(SP_KEY_LAST_CACHE_PATH).commit(); mSp.edit().remove(SP_KEY_LAST_CACHE_PATH).commit();
LogUtils.d(TAG, "clearLastCachePathInSp: SP 中最后缓存路径已清空"); LogUtils.d(TAG, "clearLastCachePathInSp】调用成功:SP 中最后缓存路径已清空");
} }
// ================================== 内部工具方法:预加载相关 ==================================
/** /**
* 构造时预加载 SP 中保存的最后一次缓存路径的图片 * 构造时预加载 SP 中保存的最后一次缓存路径的图片
*/ */
private void preloadLastCachedBitmap() { private void preloadLastCachedBitmap() {
LogUtils.d(TAG, "【preloadLastCachedBitmap】调用开始");
String lastPath = getLastCachePathFromSp(); String lastPath = getLastCachePathFromSp();
if (TextUtils.isEmpty(lastPath)) { if (TextUtils.isEmpty(lastPath)) {
LogUtils.d(TAG, "preloadLastCachedBitmap: SP 中无保存的缓存路径,跳过预加载"); LogUtils.d(TAG, "preloadLastCachedBitmapSP 中无保存的缓存路径,跳过预加载");
return; return;
} }
// 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断) // 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断)
Bitmap bitmap = cacheBitmap(lastPath); Bitmap bitmap = cacheBitmap(lastPath);
if (bitmap != null) { if (bitmap != null) {
LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功 - " + lastPath); LogUtils.d(TAG, "preloadLastCachedBitmap预加载成功 | 路径=" + lastPath);
} else { } else {
LogUtils.w(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径失败,清空无效路径 - " + lastPath); LogUtils.w(TAG, "preloadLastCachedBitmap预加载失败,清空无效路径 | 路径=" + lastPath);
// 预加载失败,清空 SP 中无效路径 // 预加载失败,清空 SP 中无效路径
clearLastCachePathInSp(); clearLastCachePathInSp();
} }
} }
// ================================== 内部工具方法:内存状态监听(仅记录日志)=================================
/**
* 注册内存状态监听(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private void registerMemoryStatusListener() {
LogUtils.d(TAG, "【registerMemoryStatusListener】调用开始");
if (Build.VERSION.SDK_INT >= 14) {
App.getInstance().registerComponentCallbacks(new MemoryStatusCallback());
LogUtils.d(TAG, "【registerMemoryStatusListener】内存状态监听已注册仅记录日志不清理缓存");
} else {
LogUtils.w(TAG, "【registerMemoryStatusListener】API 版本低于14不支持内存状态监听");
}
}
/**
* 记录当前缓存状态(用于内存紧张时的调试)
*/
private void logCurrentCacheStatus() {
LogUtils.d(TAG, "【logCurrentCacheStatus】缓存数量 - " + getCacheCount() + ",总内存占用 - " + getTotalCacheSize() + " 字节");
LogUtils.d(TAG, "【logCurrentCacheStatus】缓存路径 - " + getCachedPaths().toString());
}
// ================================== 内部类:内存状态回调(仅记录日志)=================================
/**
* 内存状态回调(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private class MemoryStatusCallback implements android.content.ComponentCallbacks2 {
@Override
public void onTrimMemory(int level) {
// 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "【onTrimMemory】内存紧张级别 - " + level + ",极致强制保持所有 Bitmap 缓存(无压缩)");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onLowMemory() {
// 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "【onLowMemory】系统低内存极致强制保持所有 Bitmap 缓存(无压缩)");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
// 配置变化时无需处理
}
}
} }

View File

@@ -1,16 +1,37 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Locale;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/24
* @Describe 日期时间工具类Java 7 兼容 | API 30 适配)
* 功能:提供当前时间的格式化字符串获取功能
*/
public class DateUtils { public class DateUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
// 获取当前时间的格式化字符串 public static final String TAG = "DateUtils";
private static final String DATE_FORMAT_PATTERN = "yyyyMMdd_HHmmssSSS"; // 修正年份格式为小写yyyy毫秒为SSS
private static final Locale DEFAULT_LOCALE = Locale.getDefault();
// ================================== 工具方法(静态方法,无状态设计)=================================
/**
* 获取当前时间的格式化字符串
* 格式yyyyMMdd_HHmmssSSS年-月-日_时-分-秒-毫秒)
* @return 格式化后的当前时间字符串
*/
public static String getDateNowString() { public static String getDateNowString() {
// 日期类转化成字符串类的工具 LogUtils.d(TAG, "【getDateNowString】调用开始");
SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("YYYYMMdd_HHmmssmmm", java.util.Locale.getDefault()); // 初始化日期格式化工具Java 7 兼容使用小写yyyy避免周基年问题
// 读取当前时间 SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_PATTERN, DEFAULT_LOCALE);
long nTimeNow = System.currentTimeMillis(); // 读取当前时间戳
return mSimpleDateFormat.format(nTimeNow); long currentTime = System.currentTimeMillis();
// 格式化时间
String formattedTime = sdf.format(currentTime);
LogUtils.d(TAG, "【getDateNowString】调用成功 | 格式化时间=" + formattedTime);
return formattedTime;
} }
} }

View File

@@ -3,12 +3,10 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import cc.winboll.studio.libappbase.LogUtils;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -17,85 +15,111 @@ import java.io.IOException;
* 适配 PowerBell 项目支持指定保存路径、自动创建目录、处理PNG图片压缩 * 适配 PowerBell 项目支持指定保存路径、自动创建目录、处理PNG图片压缩
*/ */
public class DrawableToFileUtils { public class DrawableToFileUtils {
private static final String TAG = "DrawableToFileUtils"; // ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "DrawableToFileUtils";
private static final String IMAGE_FORMAT_PNG = ".png"; // 目标图片格式
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.PNG; // 压缩格式
private static final int COMPRESS_QUALITY = 100; // PNG无损压缩质量
private static final long MIN_FILE_SIZE = 100; // 有效文件最小字节数
// ================================== 核心工具方法(基础版:指定文件路径)=================================
/** /**
* 核心方法:将 R.drawable 图片保存为 File 对象 * 核心方法:将 R.drawable 图片保存为 File 对象
* @param context 上下文(用于获取 Resources * @param context 上下文(用于获取 Resources
* @param drawableResId 图片资源ID如 R.drawable.ic_test_png * @param drawableResId 图片资源ID如 R.drawable.ic_test_png
* @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.png" * @param filePath 保存的文件路径(可带/不带.png后缀
* @return 保存成功返回 File 对象,失败返回 null * @return 保存成功返回 File 对象,失败返回 null
*/ */
public static File saveDrawableToFile(Context context, int drawableResId, String filePath) { public static File saveDrawableToFile(Context context, int drawableResId, String filePath) {
// 1. 校验参数(避免空指针/无效参数) LogUtils.d(TAG, "【saveDrawableToFile】调用开始 | 资源ID=" + drawableResId + " | 目标路径=" + filePath);
if (context == null || drawableResId == 0 || filePath == null || filePath.isEmpty()) { // 1. 校验核心参数(避免空指针/无效参数)
LogUtils.e(TAG, "【保存失败】参数无效context为空/资源ID为0/文件名为空)"); if (context == null) {
LogUtils.e(TAG, "【saveDrawableToFile】参数异常context为空");
return null; return null;
} }
if (!filePath.endsWith(".png")) { if (drawableResId == 0) {
filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确 LogUtils.e(TAG, "【saveDrawableToFile】参数异常drawableResId为0");
LogUtils.d(TAG, "【格式适配】自动添加.png后缀最终文件名" + filePath); return null;
}
if (filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "【saveDrawableToFile】参数异常filePath为空");
return null;
} }
// 3. 构建目标 File 对象(最终保存的文件路径 // 2. 格式化文件路径(强制添加.png后缀
File targetFile = new File(filePath); String targetFilePath = filePath.endsWith(IMAGE_FORMAT_PNG) ? filePath : filePath + IMAGE_FORMAT_PNG;
LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath()); if (!filePath.equals(targetFilePath)) {
LogUtils.d(TAG, "【saveDrawableToFile】格式适配自动添加.png后缀 | 最终路径=" + targetFilePath);
}
// 4. 读取 drawable 资源为 Bitmap处理高清图/缩放问题) // 3. 构建目标File对象并创建父目录
Bitmap bitmap = null; File targetFile = new File(targetFilePath);
try { File parentDir = targetFile.getParentFile();
// 读取 drawable 资源(适配不同分辨率的图片,避免变形) if (parentDir != null && !parentDir.exists()) {
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId); boolean isDirCreated = parentDir.mkdirs();
if (bitmap == null) { if (!isDirCreated) {
LogUtils.e(TAG, "读取失败】无法读取drawable资源资源ID" + drawableResId + ""); LogUtils.e(TAG, "saveDrawableToFile】目录创建失败" + parentDir.getAbsolutePath());
return null; return null;
} }
LogUtils.d(TAG, "读取成功】drawable资源转Bitmap成功" + bitmap.getWidth() + ",高:" + bitmap.getHeight() + ""); LogUtils.d(TAG, "saveDrawableToFile】目录创建成功" + parentDir.getAbsolutePath());
}
LogUtils.d(TAG, "【saveDrawableToFile】目标文件路径" + targetFile.getAbsolutePath());
// 5. 将 Bitmap 写入 FilePNG格式无损保存 // 4. 读取drawable资源为Bitmap
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
if (bitmap == null) {
LogUtils.e(TAG, "【saveDrawableToFile】读取失败无法解析drawable资源资源ID=" + drawableResId + "");
return null;
}
LogUtils.d(TAG, "【saveDrawableToFile】读取成功Bitmap尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight());
// 5. 将Bitmap写入FilePNG无损保存
FileOutputStream fos = new FileOutputStream(targetFile); FileOutputStream fos = new FileOutputStream(targetFile);
// 压缩参数PNG格式质量100无损写入输出流 boolean isSaved = bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, fos);
boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); fos.flush();
fos.flush(); // 刷新输出流 fos.close();
fos.close(); // 关闭输出流
// 6. 校验保存结果(文件是否存在且有效) // 6. 校验保存结果
if (isSaved && targetFile.exists() && targetFile.length() > 100) { if (isSaved && targetFile.exists() && targetFile.length() > MIN_FILE_SIZE) {
LogUtils.d(TAG, "保存成功】drawable图片保存为File" + targetFile.getAbsolutePath()); LogUtils.d(TAG, "saveDrawableToFile】保存成功" + targetFile.getAbsolutePath());
return targetFile; // 保存成功返回File对象 return targetFile;
} else { } else {
LogUtils.e(TAG, "保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)"); LogUtils.e(TAG, "saveDrawableToFile】保存失败文件无效存在=" + targetFile.exists() + " | 大小=" + targetFile.length() + "字节)");
// 保存失败,删除无效文件 // 清理无效文件
if (targetFile.exists()) { if (targetFile.exists()) {
targetFile.delete(); targetFile.delete();
LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath()); LogUtils.d(TAG, "saveDrawableToFile】清理无效文件:" + targetFile.getAbsolutePath());
} }
return null; return null;
} }
} catch (IOException e) { } catch (IOException e) {
LogUtils.e(TAG, "【保存异常】写入文件时出错" + e.getMessage()); LogUtils.e(TAG, "saveDrawableToFile】保存异常:" + e.getMessage());
LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e));
return null; return null;
} finally { } finally {
// 回收Bitmap资源避免内存溢出 // 回收Bitmap资源避免内存溢出
if (bitmap != null && !bitmap.isRecycled()) { if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle(); bitmap.recycle();
LogUtils.d(TAG, "【资源回收Bitmap资源已回收"); LogUtils.d(TAG, "saveDrawableToFile】资源回收Bitmap已回收");
} }
} }
} }
// ================================== 重载工具方法(扩展版:指定目录+文件名)=================================
/** /**
* 重载方法:自定义保存路径(灵活适配不同场景) * 重载方法:自定义保存路径(灵活适配不同场景)
* @param context 上下文 * @param context 上下文
* @param drawableResId 图片资源ID * @param drawableResId 图片资源ID
* @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/" * @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/"
* @param fileName 保存的文件名(带.png后缀 * @param fileName 保存的文件名(可带/不带.png后缀
* @return 保存成功返回File对象失败返回null * @return 保存成功返回File对象失败返回null
*/ */
public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) { public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) {
File filePath = new File(saveDirPath, fileName); LogUtils.d(TAG, "【saveDrawableToFile】重载方法调用开始 | 资源ID=" + drawableResId + " | 目录=" + saveDirPath + " | 文件名=" + fileName);
return saveDrawableToFile(context, drawableResId, filePath.getAbsolutePath()); // 构建完整文件路径
File targetFile = new File(saveDirPath, fileName);
return saveDrawableToFile(context, drawableResId, targetFile.getAbsolutePath());
} }
} }

View File

@@ -4,33 +4,37 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import java.io.BufferedInputStream;
import java.io.*; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID; import java.util.UUID;
import android.content.Context;
import android.net.Uri;
/** /**
* 文件操作工具类 * 文件操作工具类
* 功能:文件读写、复制、图片转换、文件名处理等常用文件操作 * 功能:文件读写、复制、图片转换、文件名处理等常用文件操作
* 适配Java 7+,支持Android全版本 * 适配Java 7 + Android API 30
* 注意调用文件操作前需确保已获取存储权限Android 6.0+ 需动态申请) * 注意调用文件操作前需确保已获取存储权限Android 6.0+ 需动态申请)
*/ */
public class FileUtils { public class FileUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
/** 日志标签 */
public static final String TAG = "FileUtils"; public static final String TAG = "FileUtils";
/** 读取文件默认缓冲区大小10KB */ /** 读取文件默认缓冲区大小10KB */
private static final int BUFFER_SIZE = 10240; private static final int BUFFER_SIZE = 10240;
/** 最大读取文件大小1GB防止OOM */ /** 最大读取文件大小1GB防止OOM */
private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024; private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024;
/** 最大文件后缀长度(避免异常文件名) */
private static final int MAX_SUFFIX_LENGTH = 5;
/** 缓冲区大小(流复制专用) */
private static final int STREAM_BUFFER_SIZE = 1024;
// ====================================== 文件读取相关 ====================================== // ================================== 文件读取相关(字符串 + 字节数组)=================================
/** /**
* 读取文件内容并转为字符串 * 读取文件内容并转为字符串
* @param filePath 文件绝对路径(非空) * @param filePath 文件绝对路径(非空)
@@ -38,25 +42,34 @@ public class FileUtils {
* @throws IOException 异常:文件不存在、文件过大、读取失败等 * @throws IOException 异常:文件不存在、文件过大、读取失败等
*/ */
public static String readFileAsString(String filePath) throws IOException { public static String readFileAsString(String filePath) throws IOException {
LogUtils.d(TAG, "【readFileAsString】调用开始 | 文件路径=" + filePath);
// 1. 校验文件合法性 // 1. 校验文件合法性
File file = new File(filePath); File file = new File(filePath);
if (!file.exists()) { if (!file.exists()) {
LogUtils.e(TAG, "【readFileAsString】文件不存在" + filePath);
throw new FileNotFoundException("文件不存在:" + filePath); throw new FileNotFoundException("文件不存在:" + filePath);
} }
if (file.length() > MAX_READ_FILE_SIZE) { if (file.length() > MAX_READ_FILE_SIZE) {
LogUtils.e(TAG, "【readFileAsString】文件过大超过1GB" + filePath);
throw new IOException("文件过大超过1GB禁止读取" + filePath); throw new IOException("文件过大超过1GB禁止读取" + filePath);
} }
// 2. 读取文件内容使用StringBuilder高效拼接 // 2. 读取文件内容使用StringBuilder高效拼接
StringBuilder sb = new StringBuilder((int) file.length()); StringBuilder sb = new StringBuilder((int) file.length());
try (FileInputStream fis = new FileInputStream(file)) { FileInputStream fis = null;
try {
fis = new FileInputStream(file);
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
int readLen; int readLen;
// 循环读取缓冲区避免一次性读取大文件导致OOM
while ((readLen = fis.read(buffer)) > 0) { while ((readLen = fis.read(buffer)) > 0) {
sb.append(new String(buffer, 0, readLen)); sb.append(new String(buffer, 0, readLen));
} }
} finally {
if (fis != null) {
fis.close();
}
} }
LogUtils.d(TAG, "【readFileAsString】读取成功 | 文件大小=" + file.length() + "字节");
return sb.toString(); return sb.toString();
} }
@@ -67,28 +80,39 @@ public class FileUtils {
* @throws IOException 异常:文件不存在、读取失败等 * @throws IOException 异常:文件不存在、读取失败等
*/ */
public static byte[] readFileByBytes(String filePath) throws IOException { public static byte[] readFileByBytes(String filePath) throws IOException {
LogUtils.d(TAG, "【readFileByBytes】调用开始 | 文件路径=" + filePath);
// 1. 校验文件合法性 // 1. 校验文件合法性
File file = new File(filePath); File file = new File(filePath);
if (!file.exists()) { if (!file.exists()) {
LogUtils.e(TAG, "【readFileByBytes】文件不存在" + filePath);
throw new FileNotFoundException("文件不存在:" + filePath); throw new FileNotFoundException("文件不存在:" + filePath);
} }
// 2. 缓冲流读取高效减少IO次数 // 2. 缓冲流读取高效减少IO次数
try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length()); ByteArrayOutputStream bos = null;
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { BufferedInputStream bis = null;
try {
bos = new ByteArrayOutputStream((int) file.length());
bis = new BufferedInputStream(new FileInputStream(file));
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
int readLen; int readLen;
while ((readLen = bis.read(buffer)) != -1) { while ((readLen = bis.read(buffer)) != -1) {
bos.write(buffer, 0, readLen); bos.write(buffer, 0, readLen);
} }
bos.flush(); bos.flush();
LogUtils.d(TAG, "【readFileByBytes】读取成功 | 文件大小=" + file.length() + "字节");
return bos.toByteArray(); return bos.toByteArray();
} finally {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
} }
} }
// ====================================== 文件复制相关 ====================================== // ================================== 文件复制相关FileChannel + 简化版 + 流复制)=================================
/** /**
* 基于FileChannel复制文件高效适用于大文件复制 * 基于FileChannel复制文件高效适用于大文件复制
* @param source 源文件(非空,必须存在) * @param source 源文件(非空,必须存在)
@@ -96,62 +120,117 @@ public class FileUtils {
* @throws IOException 异常:源文件不存在、复制失败等 * @throws IOException 异常:源文件不存在、复制失败等
*/ */
public static void copyFileUsingFileChannels(File source, File dest) throws IOException { public static void copyFileUsingFileChannels(File source, File dest) throws IOException {
LogUtils.d(TAG, "【copyFileUsingFileChannels】调用开始 | 源文件=" + source.getAbsolutePath() + " | 目标文件=" + dest.getAbsolutePath());
// 1. 校验源文件合法性 // 1. 校验源文件合法性
if (!source.exists() || !source.isFile()) { if (!source.exists() || !source.isFile()) {
LogUtils.e(TAG, "【copyFileUsingFileChannels】源文件无效" + source.getAbsolutePath());
throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath()); throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath());
} }
// 2. 创建目标文件父目录 // 2. 创建目标文件父目录
if (!dest.getParentFile().exists()) { if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs(); dest.getParentFile().mkdirs();
LogUtils.d(TAG, "【copyFileUsingFileChannels】创建父目录" + dest.getParentFile().getAbsolutePath());
} }
// 3. 通道复制(try-with-resources 自动关闭通道,无需手动关闭) // 3. 通道复制(手动关闭兼容Java 7
try (FileChannel inputChannel = new FileInputStream(source).getChannel(); FileChannel inputChannel = null;
FileChannel outputChannel = new FileOutputStream(dest).getChannel()) { FileChannel outputChannel = null;
// 从输入通道复制到输出通道(高效,底层优化) try {
inputChannel = new FileInputStream(source).getChannel();
outputChannel = new FileOutputStream(dest).getChannel();
outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
LogUtils.d(TAG, "文件复制成功FileChannel" + source.getAbsolutePath() + "" + dest.getAbsolutePath()); LogUtils.d(TAG, "【copyFileUsingFileChannels】复制成功");
} finally {
if (inputChannel != null) {
inputChannel.close();
}
if (outputChannel != null) {
outputChannel.close();
}
} }
} }
/** /**
* 简化版文件复制(基于NIO Files工具类代码简洁,适用于中小文件) * 简化版文件复制(基于传统IO兼容全版本,适用于中小文件)
* @param oldFile 源文件(非空,必须存在) * @param oldFile 源文件(非空,必须存在)
* @param newFile 目标文件(非空,父目录会自动创建) * @param newFile 目标文件(非空,父目录会自动创建)
* @return 复制结果true-成功false-失败 * @return 复制结果true-成功false-失败
*/ */
public static boolean copyFile(File oldFile, File newFile) { public static boolean copyFile(File oldFile, File newFile) {
LogUtils.d(TAG, "【copyFile】调用开始 | 源文件=" + (oldFile != null ? oldFile.getAbsolutePath() : "null") + " | 目标文件=" + (newFile != null ? newFile.getAbsolutePath() : "null"));
// 1. 校验源文件合法性 // 1. 校验源文件合法性
if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) { if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) {
LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null")); LogUtils.e(TAG, "【copyFile】源文件无效");
return false; return false;
} }
// 2. 创建目标文件父目录 // 2. 创建目标文件父目录
if (!newFile.getParentFile().exists()) { if (!newFile.getParentFile().exists()) {
newFile.getParentFile().mkdirs(); newFile.getParentFile().mkdirs();
LogUtils.d(TAG, "【copyFile】创建父目录" + newFile.getParentFile().getAbsolutePath());
} }
// 3. 复制文件(覆盖已有目标文件) // 3. 复制文件(覆盖已有目标文件)
if (newFile.exists()) {
newFile.delete();
LogUtils.d(TAG, "【copyFile】删除已有目标文件" + newFile.getAbsolutePath());
}
try { try {
Path sourcePath = Paths.get(oldFile.getPath()); copyFileUsingFileChannels(oldFile, newFile);
Path destPath = Paths.get(newFile.getPath());
// 先删除已有目标文件(避免覆盖失败)
if (newFile.exists()) {
newFile.delete();
}
Files.copy(sourcePath, destPath);
LogUtils.d(TAG, "文件复制成功Files" + oldFile.getAbsolutePath() + "" + newFile.getAbsolutePath());
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e); LogUtils.e(TAG, "【copyFile】复制失败:" + e.getMessage(), e);
return false; return false;
} }
} }
// ====================================== 图片文件相关 ====================================== /**
* 复制输入流到文件兼容Uri解析失败场景
* @param inputStream 输入流(非空)
* @param file 目标文件(非空)
* @throws IOException 异常:流关闭失败、目录创建失败等
*/
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException {
LogUtils.d(TAG, "【copyStreamToFile】调用开始 | 目标文件=" + file.getAbsolutePath());
// 1. 校验参数合法性
if (inputStream == null || file == null) {
LogUtils.e(TAG, "【copyStreamToFile】参数为空InputStream=" + (inputStream == null) + " | File=" + (file == null));
throw new IllegalArgumentException("InputStream或File不能为空");
}
// 2. 创建目标文件父目录
File parentDir = file.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
LogUtils.e(TAG, "【copyStreamToFile】无法创建父目录" + parentDir.getAbsolutePath());
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
}
// 3. 流复制手动关闭流兼容Java 7
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
byte[] buffer = new byte[STREAM_BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
LogUtils.d(TAG, "【copyStreamToFile】复制成功");
} finally {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【copyStreamToFile】关闭输入流失败" + e.getMessage());
}
if (outputStream != null) {
outputStream.close();
}
}
}
// ================================== 图片文件相关BitmapDrawable 获取)=================================
/** /**
* 从文件路径获取BitmapDrawable适用于Android图片显示 * 从文件路径获取BitmapDrawable适用于Android图片显示
* @param path 图片文件绝对路径(非空) * @param path 图片文件绝对路径(非空)
@@ -159,40 +238,50 @@ public class FileUtils {
* @throws IOException 异常文件读取IO错误 * @throws IOException 异常文件读取IO错误
*/ */
public static BitmapDrawable getImageDrawable(String path) throws IOException { public static BitmapDrawable getImageDrawable(String path) throws IOException {
LogUtils.d(TAG, "【getImageDrawable】调用开始 | 图片路径=" + path);
// 1. 校验文件合法性 // 1. 校验文件合法性
File file = new File(path); File file = new File(path);
if (!file.exists() || !file.isFile()) { if (!file.exists() || !file.isFile()) {
LogUtils.e(TAG, "图片文件不存在" + path); LogUtils.e(TAG, "【getImageDrawable】图片文件无效" + path);
return null; return null;
} }
// 2. 读取文件并转为BitmapDrawable缓冲流读取减少内存占用 // 2. 读取文件并转为BitmapDrawable缓冲流读取减少内存占用
try (InputStream is = new FileInputStream(file); InputStream is = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream()) { ByteArrayOutputStream bos = null;
try {
is = new FileInputStream(file);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
int readLen; int readLen;
while ((readLen = is.read(buffer)) != -1) { while ((readLen = is.read(buffer)) != -1) {
bos.write(buffer, 0, readLen); bos.write(buffer, 0, readLen);
} }
// 3. 生成Bitmap并包装为BitmapDrawable
byte[] imageBytes = bos.toByteArray(); byte[] imageBytes = bos.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
LogUtils.d(TAG, "【getImageDrawable】转换成功 | 图片尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight());
return new BitmapDrawable(bitmap); return new BitmapDrawable(bitmap);
} finally {
if (is != null) {
is.close();
}
if (bos != null) {
bos.close();
}
} }
} }
// ====================================== 文件名处理相关 ====================================== // ================================== 文件名处理相关(后缀截取 + 唯一文件名)=================================
/** /**
* 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png" * 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png"
* @param file 目标文件可为null * @param file 目标文件可为null
* @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串 * @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串
*/ */
public static String getFileSuffixWithMultiDot(File file) { public static String getFileSuffixWithMultiDot(File file) {
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null"));
// 1. 校验文件合法性 // 1. 校验文件合法性
if (file == null || !file.isFile()) { if (file == null || !file.isFile()) {
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】文件无效返回空后缀");
return ""; return "";
} }
@@ -201,14 +290,39 @@ public class FileUtils {
int lastDotIndex = fileName.lastIndexOf("."); int lastDotIndex = fileName.lastIndexOf(".");
// 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件) // 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件)
if (lastDotIndex == -1 // 无 "." if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1 || (fileName.length() - lastDotIndex) > MAX_SUFFIX_LENGTH) {
|| lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore" LogUtils.d(TAG, "【getFileSuffixWithMultiDot】无有效后缀 | 文件名=" + fileName);
|| (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5异常文件名
return ""; return "";
} }
// 4. 返回小写后缀(统一格式,避免大小写不一致问题) // 4. 返回小写后缀(统一格式,避免大小写不一致问题)
return fileName.substring(lastDotIndex).toLowerCase(); String suffix = fileName.substring(lastDotIndex).toLowerCase();
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】获取成功 | 后缀=" + suffix);
return suffix;
}
/**
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
* @param file 目标文件
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
*/
public static String getFileSuffix(File file) {
LogUtils.d(TAG, "【getFileSuffix】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null"));
if (file == null || file.getName().isEmpty()) {
LogUtils.d(TAG, "【getFileSuffix】文件无效返回空后缀");
return "";
}
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
LogUtils.d(TAG, "【getFileSuffix】无有效后缀 | 文件名=" + fileName);
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
String suffix = fileName.substring(lastDotIndex + 1).toLowerCase();
LogUtils.d(TAG, "【getFileSuffix】获取成功 | 后缀=" + suffix);
return suffix;
} }
/** /**
@@ -218,73 +332,35 @@ public class FileUtils {
* @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点) * @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点)
*/ */
public static String createUniqueFileName(File refFile) { public static String createUniqueFileName(File refFile) {
LogUtils.d(TAG, "【createUniqueFileName】调用开始 | 参考文件=" + (refFile != null ? refFile.getAbsolutePath() : "null"));
// 1. 获取参考文件的后缀名自动容错null/无效文件) // 1. 获取参考文件的后缀名自动容错null/无效文件)
String suffix = getFileSuffixWithMultiDot(refFile); String suffix = getFileSuffixWithMultiDot(refFile);
// 2. 生成唯一标识UUID确保全局唯一时间戳进一步降低重复概率 // 2. 生成唯一标识UUID确保全局唯一时间戳进一步降低重复概率
String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名 String uniqueId = UUID.randomUUID().toString().replace("-", "");
long timeStamp = System.currentTimeMillis(); long timeStamp = System.currentTimeMillis();
// 3. 拼接文件名(分场景处理,避免多余点) // 3. 拼接文件名(分场景处理,避免多余点)
String fileName;
if (suffix.isEmpty()) { if (suffix.isEmpty()) {
// 无后缀唯一ID + 时间戳 fileName = String.format("%s_%d", uniqueId, timeStamp);
return String.format("%s_%d", uniqueId, timeStamp);
} else { } else {
// 有后缀唯一ID + 时间戳 + 后缀(无多余点) fileName = String.format("%s_%d%s", uniqueId, timeStamp, suffix);
return String.format("%s_%d%s", uniqueId, timeStamp, suffix);
} }
LogUtils.d(TAG, "【createUniqueFileName】生成成功 | 文件名=" + fileName);
return fileName;
} }
/** // ================================== 工具辅助方法(文件存在性判断)=================================
* 复制输入流到文件兼容Uri解析失败场景 /**
*/ * 判断文件是否存在
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException { * @param path 文件绝对路径
if (inputStream == null || file == null) { * @return true-存在false-不存在
throw new IllegalArgumentException("InputStream或File不能为空"); */
} public static boolean isFileExists(String path) {
File parentDir = file.getParentFile(); LogUtils.d(TAG, "【isFileExists】调用开始 | 文件路径=" + path);
if (!parentDir.exists() && !parentDir.mkdirs()) { File file = new File(path);
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath()); boolean exists = file.exists();
} LogUtils.d(TAG, "【isFileExists】判断结果 | 路径=" + path + " | 存在=" + exists);
try { return exists;
OutputStream outputStream = new FileOutputStream(file); }
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
} finally {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage());
}
}
}
public static boolean isFileExists(String path) {
File file = new File(path);
return file.exists();
}
/**
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
* @param file 目标文件
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
*/
public static String getFileSuffix(File file) {
if (file == null || file.getName().isEmpty()) {
return ""; // 空文件/空文件名,返回空
}
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
} }

View File

@@ -2,27 +2,35 @@ package cc.winboll.studio.powerbell.utils;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.widget.Toast;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import com.yalantis.ucrop.UCrop; import com.yalantis.ucrop.UCrop;
import java.io.File; import java.io.File;
import java.util.regex.Pattern;
/** /**
* 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File传参) * 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File/BackgroundBean 多传参)
* 适配Java 7 + Android API 30
* 核心策略:强制 PNG 输出,保留透明通道,统一裁剪配置
*/ */
public class ImageCropUtils { public class ImageCropUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "ImageCropUtils"; public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与 AndroidManifest 配置一致) // FileProvider 授权(与 AndroidManifest 配置一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 强制输出格式:固定为 PNG保留透明通道 // 强制输出格式:固定为 PNG保留透明通道
private static final String FORCE_OUTPUT_SUFFIX = "png"; private static final String FORCE_OUTPUT_SUFFIX = "png";
private static final android.graphics.Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = android.graphics.Bitmap.CompressFormat.PNG; private static final Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;
// 图片后缀正则(用于强制替换)
private static final Pattern IMAGE_SUFFIX_PATTERN = Pattern.compile("\\.(jpg|jpeg|png|bmp|gif)$", Pattern.CASE_INSENSITIVE);
// ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)====================== // ================================== 核心裁剪方法重载Uri/File/BackgroundBean=================================
/** /**
* 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道 * 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文 * @param activity 上下文
@@ -40,18 +48,19 @@ public class ImageCropUtils {
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
LogUtils.d(TAG, "【startImageCrop】调用开始Uri 版)| 请求码=" + requestCode);
// 1. 输入参数校验 // 1. 输入参数校验
if (activity == null || activity.isFinishing()) { if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "裁剪异常Activity 无效或已销毁"); LogUtils.e(TAG, "startImageCrop】参数异常Activity 无效或已销毁");
return; return;
} }
if (inputUri == null || outputUri == null) { if (inputUri == null || outputUri == null) {
LogUtils.e(TAG, "裁剪异常输入/输出 Uri 为空"); LogUtils.e(TAG, "startImageCrop】参数异常输入/输出 Uri 为空");
showToast(activity, "图片 Uri 无效,无法裁剪"); showToast(activity, "图片 Uri 无效,无法裁剪");
return; return;
} }
if (!isValidUri(activity, inputUri)) { if (!isValidUri(activity, inputUri)) {
LogUtils.e(TAG, "裁剪异常输入 Uri 无效" + inputUri); LogUtils.e(TAG, "startImageCrop】参数异常输入 Uri 无效 " + inputUri);
showToast(activity, "原图 Uri 无效,无法裁剪"); showToast(activity, "原图 Uri 无效,无法裁剪");
return; return;
} }
@@ -59,22 +68,23 @@ public class ImageCropUtils {
// 2. 核心:强制修正输出为 PNG忽略原图格式统一转 PNG // 2. 核心:强制修正输出为 PNG忽略原图格式统一转 PNG
File outputFile = uriToFile(activity, outputUri); File outputFile = uriToFile(activity, outputUri);
if (outputFile == null) { if (outputFile == null) {
LogUtils.e(TAG, "裁剪异常输出 Uri 转 File 失败" + outputUri); LogUtils.e(TAG, "startImageCrop】转换异常输出 Uri 转 File 失败 " + outputUri);
showToast(activity, "裁剪输出路径无效"); showToast(activity, "裁剪输出路径无效");
return; return;
} }
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀 outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
LogUtils.d(TAG, "【startImageCrop】格式修正强制输出 PNG " + outputFile.getAbsolutePath());
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心) // 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
UCrop uCrop = UCrop.of(inputUri, outputUri); UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY); //uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数 UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
// 4. 启动裁剪 // 4. 启动裁剪
uCrop.withOptions(options); uCrop.withOptions(options);
uCrop.start(activity, requestCode); uCrop.start(activity, requestCode);
LogUtils.d(TAG, "裁剪启动成功Uri 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath()); LogUtils.d(TAG, "startImageCrop】启动成功Uri 版)| 输出路径=" + outputFile.getAbsolutePath());
} }
/** /**
@@ -94,18 +104,19 @@ public class ImageCropUtils {
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
LogUtils.d(TAG, "【startImageCrop】调用开始File 版)| 请求码=" + requestCode);
// 1. 输入参数校验 // 1. 输入参数校验
if (activity == null || activity.isFinishing()) { if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "裁剪异常Activity 无效或已销毁"); LogUtils.e(TAG, "startImageCrop】参数异常Activity 无效或已销毁");
return; return;
} }
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) { if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
LogUtils.e(TAG, "裁剪异常输入图片文件无效"); LogUtils.e(TAG, "startImageCrop】参数异常输入图片文件无效 " + (inputFile != null ? inputFile.getAbsolutePath() : "null"));
showToast(activity, "无有效图片可裁剪"); showToast(activity, "无有效图片可裁剪");
return; return;
} }
if (outputFile == null) { if (outputFile == null) {
LogUtils.e(TAG, "裁剪异常输出文件路径为空"); LogUtils.e(TAG, "startImageCrop】参数异常输出文件路径为空");
showToast(activity, "裁剪输出路径无效"); showToast(activity, "裁剪输出路径无效");
return; return;
} }
@@ -114,20 +125,27 @@ public class ImageCropUtils {
Uri inputUri = getFileProviderUri(activity, inputFile); Uri inputUri = getFileProviderUri(activity, inputFile);
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀 outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
Uri outputUri = getFileProviderUri(activity, outputFile); Uri outputUri = getFileProviderUri(activity, outputFile);
LogUtils.d(TAG, "【startImageCrop】格式修正强制输出 PNG " + outputFile.getAbsolutePath());
// 3. 初始化 uCrop + 强制 PNG 配置 // 3. 初始化 uCrop + 强制 PNG 配置
UCrop uCrop = UCrop.of(inputUri, outputUri); UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY); //uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数 UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
// 4. 启动裁剪 // 4. 启动裁剪
uCrop.withOptions(options); uCrop.withOptions(options);
uCrop.start(activity, requestCode); uCrop.start(activity, requestCode);
LogUtils.d(TAG, "裁剪启动成功File 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath()); LogUtils.d(TAG, "startImageCrop】启动成功File 版)| 输出路径=" + outputFile.getAbsolutePath());
} }
/** /**
* 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道 * 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param cropBean 背景图片 Bean
* @param aspectX 固定比例 X
* @param aspectY 固定比例 Y
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/ */
public static void startImageCrop(Activity activity, public static void startImageCrop(Activity activity,
BackgroundBean cropBean, BackgroundBean cropBean,
@@ -135,166 +153,233 @@ public class ImageCropUtils {
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
LogUtils.d(TAG, "【startImageCrop】调用开始BackgroundBean 版)| 请求码=" + requestCode);
if (cropBean == null) {
LogUtils.e(TAG, "【startImageCrop】参数异常BackgroundBean 为空");
showToast(activity, "裁剪参数无效");
return;
}
File inputFile = new File(cropBean.getBackgroundFilePath()); File inputFile = new File(cropBean.getBackgroundFilePath());
File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath()); File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath());
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode); startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
LogUtils.d(TAG, "【startImageCrop】启动成功BackgroundBean 版)| 输入路径=" + inputFile.getAbsolutePath());
} }
// ====================== 裁剪结果处理(保持兼容,优化日志)====================== // ================================== 裁剪结果处理(优化日志,增强容错)=================================
/**
* 处理裁剪结果
* @param requestCode 当前请求码
* @param resultCode 结果码
* @param data 结果数据
* @param cropRequestCode 裁剪请求码
* @return 裁剪成功返回输出路径,失败返回 null
*/
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) { public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
if (requestCode != cropRequestCode) return null; LogUtils.d(TAG, "【handleCropResult】调用开始 | 请求码=" + requestCode + " | 裁剪请求码=" + cropRequestCode);
if (requestCode != cropRequestCode) {
LogUtils.d(TAG, "【handleCropResult】请求码不匹配忽略结果");
return null;
}
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
Uri outputUri = UCrop.getOutput(data); Uri outputUri = UCrop.getOutput(data);
if (outputUri != null) { if (outputUri != null) {
String outputPath = uriToPath(outputUri); String outputPath = uriToPath(outputUri);
LogUtils.d(TAG, "裁剪成功】强制输出 PNG透明保留输出路径" + outputPath); LogUtils.d(TAG, "handleCropResult】裁剪成功 | 输出路径=" + outputPath);
return outputPath; return outputPath;
} else {
LogUtils.e(TAG, "【handleCropResult】裁剪失败输出 Uri 为空");
} }
} else if (resultCode == UCrop.RESULT_ERROR) { } else if (resultCode == UCrop.RESULT_ERROR) {
Throwable error = UCrop.getError(data); Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "裁剪失败】原因" + (error != null ? error.getMessage() : "未知错误")); LogUtils.e(TAG, "handleCropResult】裁剪异常" + (error != null ? error.getMessage() : "未知错误"));
} else { } else {
LogUtils.d(TAG, "【裁剪取消用户手动取消"); LogUtils.d(TAG, "handleCropResult】裁剪取消用户手动取消");
} }
return null; return null;
} }
// ====================== 辅助方法(优化适配强制 PNG 逻辑)====================== // ================================== 私有辅助方法(参数校验 + 格式转换 + 配置初始化)=================================
/** 校验 Uri 有效性(确保是图片类型) */ /**
* 校验 Uri 有效性(确保是图片类型)
*/
private static boolean isValidUri(Activity activity, Uri uri) { private static boolean isValidUri(Activity activity, Uri uri) {
try { try {
String type = activity.getContentResolver().getType(uri); String type = activity.getContentResolver().getType(uri);
return type != null && type.startsWith("image/"); boolean isValid = type != null && type.startsWith("image/");
LogUtils.d(TAG, "【isValidUri】Uri 校验结果 | " + uri + " | 有效=" + isValid);
return isValid;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【Uri 校验失败】原因:" + e.getMessage()); LogUtils.e(TAG, "isValidUri】Uri 校验失败 " + uri, e);
return false; return false;
} }
} }
/** Uri 转 File适配 FileProvider Uri 和普通 Uri */ /**
* Uri 转 File适配 FileProvider Uri 和普通 Uri
*/
private static File uriToFile(Activity activity, Uri uri) { private static File uriToFile(Activity activity, Uri uri) {
if (uri == null) return null; if (uri == null) {
LogUtils.e(TAG, "【uriToFile】参数异常Uri 为空");
return null;
}
try { try {
if (uri.getScheme().equals("file")) { if (uri.getScheme().equals("file")) {
return new File(uri.getPath()); File file = new File(uri.getPath());
LogUtils.d(TAG, "【uriToFile】转换成功普通 Uri| " + uri + "" + file.getAbsolutePath());
return file;
} }
String filePath = uri.getPath(); String filePath = uri.getPath();
if (filePath == null) return null; if (filePath == null) {
LogUtils.e(TAG, "【uriToFile】转换失败Uri 路径为空 " + uri);
return null;
}
// 适配 FileProvider 路径
if (filePath.contains("/external_files/")) { if (filePath.contains("/external_files/")) {
filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/"); filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/");
} else if (filePath.contains("/cache/")) { } else if (filePath.contains("/cache/")) {
filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/"); filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/");
} }
return new File(filePath); File file = new File(filePath);
LogUtils.d(TAG, "【uriToFile】转换成功FileProvider Uri| " + uri + "" + file.getAbsolutePath());
return file;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "UriFile 失败】uri=" + uri + ",原因:" + e.getMessage()); LogUtils.e(TAG, "uriToFile】转换失败 " + uri, e);
return null; return null;
} }
} }
/** Uri 提取文件路径 */ /**
* Uri 提取文件路径
*/
private static String uriToPath(Uri uri) { private static String uriToPath(Uri uri) {
if (uri == null) return null; if (uri == null) {
LogUtils.e(TAG, "【uriToPath】参数异常Uri 为空");
return null;
}
try { try {
if (uri.getScheme().equals("file")) { if (uri.getScheme().equals("file")) {
return uri.getPath(); String path = uri.getPath();
LogUtils.d(TAG, "【uriToPath】提取成功普通 Uri| " + uri + "" + path);
return path;
} }
String path = uri.getPath(); String path = uri.getPath();
if (path == null) return null; if (path == null) {
LogUtils.e(TAG, "【uriToPath】提取失败Uri 路径为空 " + uri);
return null;
}
// 适配多种 FileProvider 前缀
String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"}; String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"};
for (String prefix : prefixes) { for (String prefix : prefixes) {
if (path.contains(prefix)) { if (path.contains(prefix)) {
path = path.substring(path.indexOf(prefix) + prefix.length()); path = path.substring(path.indexOf(prefix) + prefix.length());
String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath(); String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
return externalRoot + "/" + path; path = externalRoot + "/" + path;
LogUtils.d(TAG, "【uriToPath】提取成功FileProvider Uri| " + uri + "" + path);
return path;
} }
} }
LogUtils.d(TAG, "【uriToPath】提取成功默认路径| " + uri + "" + path);
return path; return path;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage()); LogUtils.e(TAG, "uriToPath】提取失败 " + uri, e);
return null; return null;
} }
} }
/** /**
* 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心) * 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心)
* 移除 isPng 参数,全程用 PNG 配置
*/ */
private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) { private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
LogUtils.d(TAG, "【initCropOptions】初始化裁剪配置 | 自由裁剪=" + isFreeCrop);
UCrop.Options options = new UCrop.Options(); UCrop.Options options = new UCrop.Options();
// 裁剪模式配置(自由裁剪/固定比例) // 裁剪模式配置(自由裁剪/固定比例)
options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪 options.setFreeStyleCropEnabled(isFreeCrop);
options.withAspectRatio(aspectX, aspectY);
// 裁剪配置(优化体验)
//options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
//options.setCompressionQuality(100); // 图片质量
//options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
//options.setToolbarTitle("图片裁剪"); // 工具栏标题
//options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
//options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
// 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
// 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩 options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明 options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键) options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键)
options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色 options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色 options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
// 3. 通用 UI 配置(保持原有风格) // 通用 UI 配置(保持原有风格)
options.setHideBottomControls(true); // 隐藏底部控制栏
//options.setHideBottomControls(true); // 隐藏底部控制栏
options.setToolbarTitle("图片裁剪"); options.setToolbarTitle("图片裁剪");
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white)); options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark));
LogUtils.d(TAG, "【initCropOptions】配置完成强制 PNG 输出,保留透明通道");
return options; return options;
} }
/** /**
* 修正文件后缀(强制转为 .png,覆盖原有任何图片后缀) * 修正文件后缀(强制转为指定后缀,覆盖原有任何图片后缀)
*/ */
private static File correctFileSuffix(File originFile, String targetSuffix) { private static File correctFileSuffix(File originFile, String targetSuffix) {
String originName = originFile.getName(); String originName = originFile.getName();
// 强制替换所有图片后缀为 targetSuffix(避免漏改) // 强制替换所有图片后缀为 targetSuffix
originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix; String newName = IMAGE_SUFFIX_PATTERN.matcher(originName).replaceAll("") + "." + targetSuffix;
return new File(originFile.getParent(), originName); File newFile = new File(originFile.getParent(), newName);
LogUtils.d(TAG, "【correctFileSuffix】后缀修正 | " + originFile.getName() + "" + newFile.getName());
return newFile;
} }
/** 生成 FileProvider Uri适配 Android 7.0+ */ /**
* 生成 FileProvider Uri适配 Android 7.0+
*/
private static Uri getFileProviderUri(Activity activity, File file) { private static Uri getFileProviderUri(Activity activity, File file) {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX; String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
return FileProvider.getUriForFile(activity, authority, file); Uri uri = FileProvider.getUriForFile(activity, authority, file);
LogUtils.d(TAG, "【getFileProviderUri】生成成功Android 7.0+| " + file.getAbsolutePath() + "" + uri);
return uri;
} else { } else {
return Uri.fromFile(file); Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "【getFileProviderUri】生成成功Android 7.0-| " + file.getAbsolutePath() + "" + uri);
return uri;
} }
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage()); LogUtils.e(TAG, "getFileProviderUri生成失败 " + file.getAbsolutePath(), e);
return null; return null;
} }
} }
/** 显示 Toast避免崩溃 */ /**
* 显示 Toast避免崩溃
*/
private static void showToast(Activity activity, String msg) { private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) { if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show(); Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
LogUtils.d(TAG, "【showToast】显示提示" + msg);
} else {
LogUtils.e(TAG, "【showToast】无法显示提示Activity 无效");
} }
} }
// ====================== 公有辅助方法(供外部调用)====================== // ================================== 公有辅助方法(供外部调用)=================================
/**
* 公有方法:生成 FileProvider Uri
*/
public static Uri getFileProviderUriPublic(Activity activity, File file) { public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, file); return getFileProviderUri(activity, file);
} }
/**
* 公有方法Uri 转 File
*/
public static File getFileFromUriPublic(Activity activity, Uri uri) { public static File getFileFromUriPublic(Activity activity, Uri uri) {
return uriToFile(activity, uri); return uriToFile(activity, uri);
} }
/**
* 公有方法Uri 提取路径
*/
public static String getPathFromUriPublic(Uri uri) { public static String getPathFromUriPublic(Uri uri) {
return uriToPath(uri); return uriToPath(uri);
} }

View File

@@ -1,9 +1,7 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import android.content.Context; import android.content.Context;
import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@@ -18,156 +16,167 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* 图片下载工具类(单例模式)
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
* 适配Java 7 + Android API 30
* 核心策略OkHttp 全局复用、7天文件过期清理、UUID 唯一文件名、内置缓存目录(无需权限)
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 20:52 * @Date 2025/11/19 20:52
* @Describe 图片下载工具类(单例模式)
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
*/ */
public class ImageDownloader { public class ImageDownloader {
public static final String TAG = "ImageDownloader"; // ================================== 静态常量区(置顶归类,消除魔法值)=================================
// 单例实例 public static final String TAG = "ImageDownloader";
private static ImageDownloader sInstance; // 缓存目录子文件夹名称
// OkHttp 客户端(全局复用,提升性能) private static final String CACHE_DIR_NAME = "networkdownload";
private OkHttpClient mOkHttpClient; // 过期时间7天单位毫秒
// 缓存目录:/data/data/应用包名/cache/networkdownload private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000;
private File mCacheDir; // OkHttp 超时配置
// 过期时间7天单位毫秒可按需调整 private static final int CONNECT_TIMEOUT = 10;
private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000; private static final int READ_WRITE_TIMEOUT = 15;
// 文件后缀最大长度
private static final int MAX_EXTENSION_LENGTH = 5;
// 默认文件后缀
private static final String DEFAULT_EXTENSION = ".jpg";
// 缓冲区大小
private static final int BUFFER_SIZE = 1024;
/** // ================================== 成员变量(单例核心 + 全局资源)=================================
* 私有构造(单例模式禁止外部实例化) // 单例实例
* @param context 上下文(用于获取缓存目录) private static ImageDownloader sInstance;
*/ // OkHttp 客户端(全局复用,提升性能)
private ImageDownloader(Context context) { private OkHttpClient mOkHttpClient;
// 初始化 OkHttp 客户端(设置超时时间) // 缓存目录:/data/data/应用包名/cache/networkdownload
mOkHttpClient = new OkHttpClient.Builder() private File mCacheDir;
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS) // ================================== 单例方法(线程安全 + 应用上下文)=================================
.writeTimeout(15, TimeUnit.SECONDS) /**
* 单例获取方法(线程安全)
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
* @return 单例实例
*/
public static synchronized ImageDownloader getInstance(Context context) {
LogUtils.d(TAG, "【getInstance】单例获取方法调用");
if (sInstance == null) {
// 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏
sInstance = new ImageDownloader(context.getApplicationContext());
LogUtils.d(TAG, "【getInstance】单例实例首次创建");
}
return sInstance;
}
// ================================== 构造方法(私有 + 初始化逻辑)=================================
/**
* 私有构造(单例模式禁止外部实例化)
* @param context 应用上下文
*/
private ImageDownloader(Context context) {
LogUtils.d(TAG, "【ImageDownloader】构造方法调用开始初始化");
// 初始化 OkHttp 客户端(设置超时时间)
initOkHttpClient();
// 初始化缓存目录networkdownload
initCacheDir(context);
// 初始化时清理过期文件
clearExpiredFiles();
LogUtils.d(TAG, "【ImageDownloader】初始化完成");
}
// ================================== 核心初始化方法OkHttp + 缓存目录)=================================
/**
* 初始化 OkHttp 客户端(全局复用)
*/
private void initOkHttpClient() {
LogUtils.d(TAG, "【initOkHttpClient】开始初始化 OkHttp 客户端");
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
.build(); .build();
LogUtils.d(TAG, "【initOkHttpClient】OkHttp 客户端初始化完成");
}
// 初始化缓存目录networkdownload /**
initCacheDir(context); * 初始化缓存目录:若不存在则创建
// 初始化时清理过期文件 * @param context 应用上下文
clearExpiredFiles(); */
} private void initCacheDir(Context context) {
LogUtils.d(TAG, "【initCacheDir】开始初始化缓存目录");
// 获取应用内置缓存目录(无需权限)
File cacheRoot = context.getCacheDir();
mCacheDir = new File(cacheRoot, CACHE_DIR_NAME);
/** // 若目录不存在则创建(包括父目录)
* 单例获取方法(线程安全) if (!mCacheDir.exists()) {
* @param context 上下文(建议使用 Application 上下文避免内存泄漏) boolean isCreated = mCacheDir.mkdirs();
* @return 单例实例 if (isCreated) {
*/ LogUtils.d(TAG, "【initCacheDir】缓存目录创建成功" + mCacheDir.getAbsolutePath());
public static synchronized ImageDownloader getInstance(Context context) { } else {
if (sInstance == null) { LogUtils.e(TAG, "【initCacheDir】缓存目录创建失败");
// 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏 }
sInstance = new ImageDownloader(context.getApplicationContext()); } else {
} LogUtils.d(TAG, "【initCacheDir】缓存目录已存在" + mCacheDir.getAbsolutePath());
return sInstance; }
} }
/** // ================================== 核心业务方法(下载 + 清理 + 获取最新文件)=================================
* 初始化缓存目录:若不存在则创建 /**
*/ * 下载网络图片到缓存目录
private void initCacheDir(Context context) { * @param imageUrl 图片网络链接
// 获取应用内置缓存目录(无需权限) * @param callback 下载结果回调(成功/失败)
File cacheRoot = context.getCacheDir(); */
mCacheDir = new File(cacheRoot, "networkdownload"); public void downloadImage(final String imageUrl, final DownloadCallback callback) {
LogUtils.d(TAG, "【downloadImage】下载方法调用 | 图片链接=" + imageUrl);
// 1. 校验参数
if (TextUtils.isEmpty(imageUrl)) {
String errorMsg = "图片链接为空";
LogUtils.e(TAG, "【downloadImage】参数校验失败" + errorMsg);
if (callback != null) {
callback.onFailure(errorMsg);
}
return;
}
// 若目录不存在则创建(包括父目录) if (mCacheDir == null || !mCacheDir.exists()) {
if (!mCacheDir.exists()) { String errorMsg = "缓存目录不存在";
boolean isCreated = mCacheDir.mkdirs(); LogUtils.e(TAG, "【downloadImage】参数校验失败" + errorMsg);
if (isCreated) { if (callback != null) {
LogUtils.d("ImageDownloader", "networkdownload 缓存目录创建成功:" + mCacheDir.getAbsolutePath()); callback.onFailure(errorMsg);
} else { }
LogUtils.e("ImageDownloader", "networkdownload 缓存目录创建失败"); return;
} }
} else {
LogUtils.d("ImageDownloader", "networkdownload 缓存目录已存在:" + mCacheDir.getAbsolutePath());
}
}
/** // 2. 构建 OkHttp 请求
* 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件) Request request = new Request.Builder()
*/
private void clearExpiredFiles() {
if (mCacheDir == null || !mCacheDir.exists()) {
return;
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d("ImageDownloader", "缓存目录无文件,无需清理");
return;
}
long currentTime = System.currentTimeMillis();
int deleteCount = 0;
// 遍历所有文件,删除过期文件
for (File file : files) {
long lastModifyTime = file.lastModified();
if (currentTime - lastModifyTime > EXPIRE_TIME) {
if (file.delete()) {
deleteCount++;
LogUtils.d("ImageDownloader", "删除过期文件:" + file.getName());
} else {
LogUtils.e("ImageDownloader", "删除过期文件失败:" + file.getName());
}
}
}
LogUtils.d("ImageDownloader", "过期文件清理完成,共删除 " + deleteCount + " 个文件");
}
/**
* 下载网络图片到缓存目录
* @param imageUrl 图片网络链接
* @param callback 下载结果回调(成功/失败)
*/
public void downloadImage(final String imageUrl, final DownloadCallback callback) {
// 校验参数
if (TextUtils.isEmpty(imageUrl)) {
if (callback != null) {
callback.onFailure("图片链接为空");
}
return;
}
if (mCacheDir == null || !mCacheDir.exists()) {
if (callback != null) {
callback.onFailure("缓存目录不存在");
}
return;
}
// 构建 OkHttp 请求
Request request = new Request.Builder()
.url(imageUrl) .url(imageUrl)
.build(); .build();
// 异步下载(避免阻塞主线程) // 3. 异步下载(避免阻塞主线程)
mOkHttpClient.newCall(request).enqueue(new Callback() { mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
// 下载失败,回调主线程 String errorMsg = "下载失败:" + e.getMessage();
LogUtils.e(TAG, "【downloadImage】OkHttp 下载失败", e);
if (callback != null) { if (callback != null) {
callback.onFailure("下载失败:" + e.getMessage()); callback.onFailure(errorMsg);
} }
LogUtils.e("ImageDownloader", "图片下载失败:" + e.getMessage());
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(Call call, Response response) throws IOException {
// 3.1 响应状态校验
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
// 响应失败(如 404、500 String errorMsg = "响应失败:" + response.code();
LogUtils.e(TAG, "【downloadImage】响应失败状态码" + response.code());
if (callback != null) { if (callback != null) {
callback.onFailure("响应失败:" + response.code()); callback.onFailure(errorMsg);
}
// 关闭响应体
if (response.body() != null) {
response.body().close();
} }
LogUtils.e("ImageDownloader", "图片响应失败,状态码:" + response.code());
return; return;
} }
// 响应成功,写入文件 // 3.2 响应成功,写入文件
InputStream inputStream = null; InputStream inputStream = null;
FileOutputStream outputStream = null; FileOutputStream outputStream = null;
try { try {
@@ -179,7 +188,7 @@ public class ImageDownloader {
// 写入文件 // 写入文件
outputStream = new FileOutputStream(imageFile); outputStream = new FileOutputStream(imageFile);
byte[] buffer = new byte[1024]; byte[] buffer = new byte[BUFFER_SIZE];
int len; int len;
while ((len = inputStream.read(buffer)) != -1) { while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len); outputStream.write(buffer, 0, len);
@@ -187,30 +196,32 @@ public class ImageDownloader {
outputStream.flush(); outputStream.flush();
// 下载成功,回调主线程并返回文件路径 // 下载成功,回调主线程并返回文件路径
String filePath = imageFile.getAbsolutePath();
LogUtils.d(TAG, "【downloadImage】图片下载成功" + filePath);
if (callback != null) { if (callback != null) {
callback.onSuccess(imageFile.getAbsolutePath()); callback.onSuccess(filePath);
} }
LogUtils.d("ImageDownloader", "图片下载成功:" + imageFile.getAbsolutePath());
} catch (IOException e) { } catch (IOException e) {
String errorMsg = "文件写入失败:" + e.getMessage();
LogUtils.e(TAG, "【downloadImage】文件写入失败", e);
if (callback != null) { if (callback != null) {
callback.onFailure("文件写入失败:" + e.getMessage()); callback.onFailure(errorMsg);
} }
LogUtils.e("ImageDownloader", "图片写入失败:" + e.getMessage());
} finally { } finally {
// 关闭流Java7 手动关闭,避免资源泄漏) // 关闭流Java7 手动关闭,避免资源泄漏)
if (inputStream != null) { if (inputStream != null) {
try { try {
inputStream.close(); inputStream.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LogUtils.e(TAG, "【downloadImage】输入流关闭失败", e);
} }
} }
if (outputStream != null) { if (outputStream != null) {
try { try {
outputStream.close(); outputStream.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LogUtils.e(TAG, "【downloadImage】输出流关闭失败", e);
} }
} }
// 关闭响应体 // 关闭响应体
@@ -220,75 +231,119 @@ public class ImageDownloader {
} }
} }
}); });
} }
/** /**
* 获取 networkdownload 目录中最后下载的文件(修改时间排序 * 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件
* @return 最后下载的文件路径null 表示无文件) */
*/ private void clearExpiredFiles() {
public String getLastDownloadedFile() { LogUtils.d(TAG, "【clearExpiredFiles】开始清理过期文件");
if (mCacheDir == null || !mCacheDir.exists()) { if (mCacheDir == null || !mCacheDir.exists()) {
LogUtils.e("ImageDownloader", "缓存目录不存在"); LogUtils.d(TAG, "【clearExpiredFiles】缓存目录不存在,无需清理");
return null; return;
} }
File[] files = mCacheDir.listFiles(); File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
LogUtils.d("ImageDownloader", "缓存目录无文件"); LogUtils.d(TAG, "【clearExpiredFiles】缓存目录无文件,无需清理");
return null; return;
} }
// 按最后修改时间降序排序,取第一个即为最新文件 long currentTime = System.currentTimeMillis();
File lastFile = files[0]; int deleteCount = 0;
for (File file : files) {
if (file.lastModified() > lastFile.lastModified()) {
lastFile = file;
}
}
LogUtils.d("ImageDownloader", "最后下载的文件:" + lastFile.getAbsolutePath()); // 遍历所有文件,删除过期文件
return lastFile.getAbsolutePath(); for (File file : files) {
} long lastModifyTime = file.lastModified();
if (currentTime - lastModifyTime > EXPIRE_TIME) {
if (file.delete()) {
deleteCount++;
LogUtils.d(TAG, "【clearExpiredFiles】删除过期文件" + file.getName());
} else {
LogUtils.e(TAG, "【clearExpiredFiles】删除过期文件失败" + file.getName());
}
}
}
/** LogUtils.d(TAG, "【clearExpiredFiles】过期文件清理完成共删除 " + deleteCount + " 个文件");
* 工具方法:从图片链接中提取文件后缀(如 .png、.jpg }
* @param imageUrl 图片链接
* @return 文件后缀(含点号,若无法提取则返回 .jpg
*/
private String getFileExtension(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return ".jpg";
}
int lastDotIndex = imageUrl.lastIndexOf("."); /**
int lastSlashIndex = imageUrl.lastIndexOf("/"); * 获取 networkdownload 目录中最后下载的文件(按修改时间排序)
// 确保后缀在最后一个斜杠之后且长度合理1-5 个字符) * @return 最后下载的文件路径null 表示无文件)
if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) { */
String extension = imageUrl.substring(lastDotIndex); public String getLastDownloadedFile() {
if (extension.length() <= 5) { LogUtils.d(TAG, "【getLastDownloadedFile】获取最新下载文件");
return extension.toLowerCase(); // 统一转为小写 if (mCacheDir == null || !mCacheDir.exists()) {
} LogUtils.e(TAG, "【getLastDownloadedFile】缓存目录不存在");
} return null;
}
// 无法提取后缀时,默认使用 .jpg File[] files = mCacheDir.listFiles();
return ".jpg"; if (files == null || files.length == 0) {
} LogUtils.d(TAG, "【getLastDownloadedFile】缓存目录无文件");
return null;
}
/** // 按最后修改时间降序排序,取第一个即为最新文件
* 下载结果回调接口Java7 接口实现) File lastFile = files[0];
*/ for (File file : files) {
public interface DownloadCallback { if (file.lastModified() > lastFile.lastModified()) {
/** lastFile = file;
* 下载成功 }
* @param filePath 图片保存路径 }
*/
void onSuccess(String filePath);
/** String filePath = lastFile.getAbsolutePath();
* 下载失败 LogUtils.d(TAG, "【getLastDownloadedFile】最后下载的文件" + filePath);
* @param errorMsg 失败原因 return filePath;
*/ }
void onFailure(String errorMsg);
} // ================================== 辅助工具方法(文件后缀提取)=================================
/**
* 工具方法:从图片链接中提取文件后缀(如 .png、.jpg
* @param imageUrl 图片链接
* @return 文件后缀(含点号,若无法提取则返回 .jpg
*/
private String getFileExtension(String imageUrl) {
LogUtils.d(TAG, "【getFileExtension】提取文件后缀 | 图片链接=" + imageUrl);
if (TextUtils.isEmpty(imageUrl)) {
LogUtils.d(TAG, "【getFileExtension】图片链接为空返回默认后缀" + DEFAULT_EXTENSION);
return DEFAULT_EXTENSION;
}
int lastDotIndex = imageUrl.lastIndexOf(".");
int lastSlashIndex = imageUrl.lastIndexOf("/");
// 确保后缀在最后一个斜杠之后且长度合理1-5 个字符)
if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) {
String extension = imageUrl.substring(lastDotIndex);
if (extension.length() <= MAX_EXTENSION_LENGTH) {
extension = extension.toLowerCase(); // 统一转为小写
LogUtils.d(TAG, "【getFileExtension】提取后缀成功" + extension);
return extension;
}
}
// 无法提取后缀时,默认使用 .jpg
LogUtils.d(TAG, "【getFileExtension】无法提取有效后缀返回默认后缀" + DEFAULT_EXTENSION);
return DEFAULT_EXTENSION;
}
// ================================== 下载结果回调接口Java7 接口实现)=================================
/**
* 下载结果回调接口
*/
public interface DownloadCallback {
/**
* 下载成功
* @param filePath 图片保存路径
*/
void onSuccess(String filePath);
/**
* 下载失败
* @param errorMsg 失败原因
*/
void onFailure(String errorMsg);
}
} }

View File

@@ -1,53 +1,250 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
/**
* 图片处理工具类(质量压缩专用)
* 功能1. 图片JPEG质量压缩覆盖源文件2. 获取主题colorAccent颜色3. 位图纯色背景合成
* 适配Java 7 + Android API 30
* 核心逻辑:
* - 压缩Bitmap.compress + FileChannel 高效文件复制
* - 主题颜色TypedArray 解析主题属性
* - 位图合成Canvas 绘制(支持透明通道)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class ImageUtils { public class ImageUtils {
public static final String TAG = ImageUtils.class.getSimpleName(); // ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "ImageUtils";
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
private static final int MIN_COMPRESS_QUALITY = 0;
private static final int MAX_COMPRESS_QUALITY = 100;
private static final int[] COLOR_ACCENT_ATTR = new int[]{R.attr.colorAccent}; // colorAccent属性数组
private static final int DEFAULT_COLOR = Color.parseColor("#FFFFFFFF"); // 默认返回颜色
// 这里我们生成了一个Pic文件夹在下面放了我们质量压缩后的图片用于和原图对比 // ================================== 主题颜色获取方法 =================================
// 压缩图片使用Bitmap.compress(),这里是质量压缩 /**
// 参数Context context :调用本函数函数引用的资源体系 * 从当前应用主题中获取 colorAccent 颜色值
// String szSrcImagePath :要压缩的源文件路径 * @param context 上下文,用于获取主题与资源
// String szDstImagePath :压缩后文件要保存的路径 * @return 解析到的 colorAccent 颜色值,解析失败返回默认白色#FFFFFFFF
// int nPictureCompress :图片压缩比例 */
public static void bitmapCompress(Context context, String szSrcImagePath, String szDstImagePath, int nPictureCompress) { public static int getColorAccent(Context context) {
// 方法调用日志
LogUtils.d(TAG, "【getColorAccent】方法调用");
// 参数校验
if (context == null) {
LogUtils.e(TAG, "【getColorAccent】参数异常Context为空");
return DEFAULT_COLOR;
}
TypedArray typedArray = null;
try { try {
Bitmap bmpCompressImage; // 从主题解析属性
typedArray = context.obtainStyledAttributes(COLOR_ACCENT_ATTR);
int colorAccent = typedArray.getColor(0, DEFAULT_COLOR);
LogUtils.d(TAG, String.format("【getColorAccent】解析成功 | colorAccent=0x%08X", colorAccent));
return colorAccent;
} catch (Exception e) {
LogUtils.e(TAG, "【getColorAccent】解析失败返回默认颜色", e);
return DEFAULT_COLOR;
} finally {
// 回收资源
if (typedArray != null) {
typedArray.recycle();
LogUtils.d(TAG, "【getColorAccent】TypedArray资源已回收");
}
}
}
//生成新的文件 // ================================== 位图合成方法 =================================
File fDstCompressImage = new File(szDstImagePath); /**
* 在纯色背景上绘制前景位图实现FIT_CENTER居中效果
* @param bgColor 背景颜色
* @param originalFrameW 目标画布宽度
* @param originalFrameH 目标画布高度
* @param fgBitmap 前景位图
* @return 合成后的位图失败返回null
*/
public static Bitmap drawBitmapOnSolidBackground(final int bgColor, int originalFrameW, int originalFrameH, Bitmap fgBitmap) {
// 方法调用及入参日志
LogUtils.d(TAG, String.format("【drawBitmapOnSolidBackground】方法调用 | 背景色=0x%08X | 目标尺寸=%dx%d | 前景位图=%s",
bgColor, originalFrameW, originalFrameH,
(fgBitmap != null ? fgBitmap.getWidth() + "x" + fgBitmap.getHeight() : "null")));
//裁剪后的图像转成BitMap // 1. 严格参数校验
//photoBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uriClipUri)); if (fgBitmap == null || fgBitmap.isRecycled()) {
bmpCompressImage = BitmapFactory.decodeFile(szSrcImagePath); LogUtils.e(TAG, "【drawBitmapOnSolidBackground】参数异常前景Bitmap为空或已回收");
return null;
}
if (fgBitmap.getWidth() <= 0 || fgBitmap.getHeight() <= 0) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】参数异常前景Bitmap尺寸无效");
return null;
}
if (originalFrameW <= 0 || originalFrameH <= 0) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】参数异常原画框尺寸无效宽高必须大于0");
return null;
}
//创建输出流 // 2. 强制画布尺寸为目标尺寸
OutputStream out = null; int canvasW = originalFrameW;
int canvasH = originalFrameH;
LogUtils.d(TAG, String.format("【drawBitmapOnSolidBackground】画布尺寸已确定%dx%d", canvasW, canvasH));
out = new FileOutputStream(fDstCompressImage.getPath()); // 3. 创建结果位图ARGB_8888支持透明通道
Bitmap resultBitmap = Bitmap.createBitmap(canvasW, canvasH, Bitmap.Config.ARGB_8888);
if (resultBitmap == null) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】创建结果Bitmap失败");
return null;
}
//压缩文件,返回结果,参数分别是压缩的格式,压缩质量的百分比,输出流 // 4. 画布绘制
boolean bCompress = bmpCompressImage.compress(Bitmap.CompressFormat.JPEG, nPictureCompress, out); Canvas canvas = new Canvas(resultBitmap);
try {
// 4.1 绘制纯色背景
Paint bgPaint = new Paint();
bgPaint.setColor(bgColor);
bgPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, canvasW, canvasH, bgPaint);
// 复制压缩后的文件到源路径 // 4.2 计算前景 FIT_CENTER 变换参数(等比缩放至完全放入画布,居中显示)
File fSrcImage = new File(szSrcImagePath); float fgWidth = fgBitmap.getWidth();
FileUtils.copyFileUsingFileChannels(fDstCompressImage, fSrcImage); float fgHeight = fgBitmap.getHeight();
LogUtils.d(TAG, Integer.toString(nPictureCompress) + "%压缩结束。"); float scaleX = (float) canvasW / fgWidth;
float scaleY = (float) canvasH / fgHeight;
float scale = Math.min(scaleX, scaleY); // 取最小比例,保证完全放入画布
// 4.3 计算缩放后前景尺寸
float scaledW = fgWidth * scale;
float scaledH = fgHeight * scale;
// 4.4 计算居中位置(缩放后居中,无裁剪)
float translateX = (canvasW - scaledW) / 2f;
float translateY = (canvasH - scaledH) / 2f;
// 4.5 构建变换矩阵(缩放+平移,实现 FIT_CENTER 效果)
Matrix matrix = new Matrix();
matrix.postScale(scale, scale); // 等比缩放
matrix.postTranslate(translateX, translateY); // 居中平移
// 4.6 绘制前景(保留透明通道,抗锯齿)
Paint fgPaint = new Paint();
fgPaint.setAntiAlias(true);
fgPaint.setDither(true);
canvas.drawBitmap(fgBitmap, matrix, fgPaint);
LogUtils.d(TAG, String.format("【drawBitmapOnSolidBackground】合成成功 | 缩放比例=%.2f | 居中位置=(%.1f,%.1f) | 效果=FIT_CENTER",
scale, translateX, translateY));
return resultBitmap;
} catch (Exception e) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】合成失败", e);
if (resultBitmap != null && !resultBitmap.isRecycled()) {
resultBitmap.recycle();
}
return null;
}
}
// ================================== 核心压缩方法 =================================
/**
* 图片质量压缩JPEG格式压缩后覆盖源文件
* @param context 上下文(备用)
* @param srcImagePath 源图片文件路径(非空,文件需存在)
* @param dstImagePath 压缩后临时保存路径(非空)
* @param compressQuality 压缩质量0-100数值越小压缩率越高
*/
public static void bitmapCompress(Context context, String srcImagePath, String dstImagePath, int compressQuality) {
// 方法调用及入参日志
LogUtils.d(TAG, String.format("【bitmapCompress】方法调用 | 源路径=%s | 临时路径=%s | 压缩质量=%d",
srcImagePath, dstImagePath, compressQuality));
// 1. 前置参数校验
if (srcImagePath == null || srcImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常源文件路径为空");
return;
}
if (dstImagePath == null || dstImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常临时文件路径为空");
return;
}
if (compressQuality < MIN_COMPRESS_QUALITY || compressQuality > MAX_COMPRESS_QUALITY) {
LogUtils.e(TAG, String.format("【bitmapCompress】参数异常压缩质量超出范围0-100当前值=%d", compressQuality));
return;
}
File srcFile = new File(srcImagePath);
if (!srcFile.exists() || !srcFile.isFile()) {
LogUtils.e(TAG, "【bitmapCompress】源文件无效不存在或不是文件 " + srcImagePath);
return;
}
Bitmap compressBitmap = null;
OutputStream outputStream = null;
try {
// 2. 读取源图片为Bitmap
compressBitmap = BitmapFactory.decodeFile(srcImagePath);
if (compressBitmap == null) {
LogUtils.e(TAG, "【bitmapCompress】Bitmap解码失败无法读取源图片 " + srcImagePath);
return;
}
LogUtils.d(TAG, String.format("【bitmapCompress】Bitmap解码成功 | 尺寸=%dx%d",
compressBitmap.getWidth(), compressBitmap.getHeight()));
// 3. 创建临时压缩文件目录
File dstFile = new File(dstImagePath);
File dstParentDir = dstFile.getParentFile();
if (dstParentDir != null && !dstParentDir.exists()) {
boolean isDirCreated = dstParentDir.mkdirs();
LogUtils.d(TAG, String.format("【bitmapCompress】临时目录创建%s%s",
isDirCreated ? "成功" : "失败", dstParentDir.getAbsolutePath()));
}
// 4. 写入压缩数据
outputStream = new FileOutputStream(dstFile);
boolean isCompressSuccess = compressBitmap.compress(COMPRESS_FORMAT, compressQuality, outputStream);
if (!isCompressSuccess) {
LogUtils.e(TAG, "【bitmapCompress】压缩失败Bitmap.compress 执行失败");
return;
}
LogUtils.d(TAG, "【bitmapCompress】压缩成功临时文件已生成 " + dstFile.getAbsolutePath());
// 5. 复制压缩文件覆盖源文件
FileUtils.copyFileUsingFileChannels(dstFile, srcFile);
LogUtils.d(TAG, String.format("【bitmapCompress】%d%%压缩结束:已覆盖源文件 %s",
compressQuality, srcImagePath));
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
LogUtils.d(TAG, "bitmapCompress FileNotFoundException : " + e.getMessage()); LogUtils.e(TAG, "bitmapCompress】文件未找到异常", e);
} catch (IOException e) { } catch (IOException e) {
LogUtils.d(TAG, "bitmapCompress IOException : " + e.getMessage()); LogUtils.e(TAG, "bitmapCompressIO异常", e);
} finally {
// 6. 关闭输出流
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【bitmapCompress】输出流关闭失败", e);
}
}
// 7. 回收Bitmap
if (compressBitmap != null && !compressBitmap.isRecycled()) {
compressBitmap.recycle();
LogUtils.d(TAG, "【bitmapCompress】Bitmap资源已回收");
}
} }
} }
} }

View File

@@ -1,33 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.util.DisplayMetrics;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/14 11:14
* @Describe 米盟 MimoUtils
*/
public final class MimoUtils {
public static final String TAG = "Utils";
public static int dpToPx(Context context, float dp) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (dp * displayMetrics.density + 0.5f);
}
public static int pxToDp(Context context, float px) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (px / displayMetrics.density + 0.5f);
}
public static int pxToSp(Context context, float pxValue) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (pxValue / displayMetrics.scaledDensity + 0.5f);
}
public static int spToPx(Context context, float spValue) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (spValue * displayMetrics.scaledDensity + 0.5f);
}
}

View File

@@ -22,7 +22,7 @@ import cc.winboll.studio.powerbell.models.NotificationMessage;
/** /**
* 通知工具类:统一管理前台服务/电池提醒/应用配置信息通知 * 通知工具类:统一管理前台服务/电池提醒/应用配置信息通知
* 适配API19-30 | Java7 | 小米手机 * 适配API19-30 | Java7 | 小米手机
* 特性:前台服务无铃声、提醒通知系统默认铃声、配置通知低优先级无打扰、API分级适配、内存泄漏防护 * 特性:前台服务无铃声、提醒通知系统默认铃声、配置通知系统默认铃声无振动、API分级适配、内存泄漏防护
*/ */
public class NotificationManagerUtils { public class NotificationManagerUtils {
// ================================== 静态常量(置顶统一管理,杜绝魔法值)================================= // ================================== 静态常量(置顶统一管理,杜绝魔法值)=================================
@@ -48,6 +48,7 @@ public class NotificationManagerUtils {
private static final int PENDING_INTENT_REQUEST_CODE_FOREGROUND = 0; private static final int PENDING_INTENT_REQUEST_CODE_FOREGROUND = 0;
private static final int PENDING_INTENT_REQUEST_CODE_REMIND = 1; private static final int PENDING_INTENT_REQUEST_CODE_REMIND = 1;
private static final int PENDING_INTENT_REQUEST_CODE_CONFIG = 2; // 新增:配置通知请求码 private static final int PENDING_INTENT_REQUEST_CODE_CONFIG = 2; // 新增:配置通知请求码
private static int snMessageNotificationID = 10000;
// ================================== 成员变量(私有封装,按依赖优先级排序)================================= // ================================== 成员变量(私有封装,按依赖优先级排序)=================================
// 核心上下文(应用级,避免内存泄漏) // 核心上下文(应用级,避免内存泄漏)
@@ -59,35 +60,35 @@ public class NotificationManagerUtils {
// ================================== 构造方法(初始化核心资源,前置校验)================================= // ================================== 构造方法(初始化核心资源,前置校验)=================================
public NotificationManagerUtils(Context context) { public NotificationManagerUtils(Context context) {
LogUtils.d(TAG, "NotificationManagerUtils: 构造方法执行 | context=" + context); LogUtils.d(TAG, "NotificationManagerUtils() 构造 | context=" + context);
// 前置校验Context非空 // 前置校验Context非空
if (context == null) { if (context == null) {
LogUtils.e(TAG, "NotificationManagerUtils: 构造失败context is null"); LogUtils.e(TAG, "NotificationManagerUtils() 构造失败context is null");
return; return;
} }
// 初始化核心资源 // 初始化核心资源
this.mContext = context.getApplicationContext(); this.mContext = context.getApplicationContext();
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
LogUtils.d(TAG, "NotificationManagerUtils: 核心资源初始化完成 | mContext=" + mContext + " | mNotificationManager=" + mNotificationManager); LogUtils.d(TAG, "NotificationManagerUtils() 核心资源初始化完成 | mContext=" + mContext + " | mNotificationManager=" + mNotificationManager);
// 初始化通知渠道API26+ 必需) // 初始化通知渠道API26+ 必需)
initNotificationChannels(); initNotificationChannels();
LogUtils.d(TAG, "NotificationManagerUtils: 构造完成"); LogUtils.d(TAG, "NotificationManagerUtils() 构造完成");
} }
// ================================== 核心初始化方法通知渠道API分级适配================================= // ================================== 核心初始化方法通知渠道API分级适配=================================
/** /**
* 初始化通知渠道:前台服务渠道(无铃声+无振动)、提醒渠道(系统默认铃声+无振动)、配置信息渠道(低优先级无打扰 * 初始化通知渠道:前台服务渠道(无铃声+无振动)、提醒渠道(系统默认铃声+无振动)、配置信息渠道(系统默认铃声+无振动
*/ */
private void initNotificationChannels() { private void initNotificationChannels() {
LogUtils.d(TAG, "initNotificationChannels: 执行通知渠道初始化"); LogUtils.d(TAG, "initNotificationChannels() 执行通知渠道初始化");
// API<26 无渠道机制,直接返回 // API<26 无渠道机制,直接返回
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
LogUtils.d(TAG, "initNotificationChannels: API<26无需创建渠道"); LogUtils.d(TAG, "initNotificationChannels() API<26无需创建渠道");
return; return;
} }
// 通知服务为空,避免空指针 // 通知服务为空,避免空指针
if (mNotificationManager == null) { if (mNotificationManager == null) {
LogUtils.e(TAG, "initNotificationChannels: 失败NotificationManager is null"); LogUtils.e(TAG, "initNotificationChannels() 失败NotificationManager is null");
return; return;
} }
@@ -103,7 +104,7 @@ public class NotificationManagerUtils {
foregroundChannel.setSound(null, null); // 强制无铃声 foregroundChannel.setSound(null, null); // 强制无铃声
foregroundChannel.setShowBadge(false); foregroundChannel.setShowBadge(false);
foregroundChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); foregroundChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
LogUtils.d(TAG, "initNotificationChannels: 前台服务渠道配置完成"); LogUtils.d(TAG, "initNotificationChannels() 前台服务渠道配置完成");
// 2. 电池提醒渠道(中优先级,系统默认铃声,无振动) // 2. 电池提醒渠道(中优先级,系统默认铃声,无振动)
NotificationChannel remindChannel = new NotificationChannel( NotificationChannel remindChannel = new NotificationChannel(
@@ -117,27 +118,27 @@ public class NotificationManagerUtils {
remindChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT); remindChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT);
remindChannel.setShowBadge(false); remindChannel.setShowBadge(false);
remindChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); remindChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
LogUtils.d(TAG, "initNotificationChannels: 电池提醒渠道配置完成"); LogUtils.d(TAG, "initNotificationChannels() 电池提醒渠道配置完成");
// 3. 应用配置信息渠道(新增:最低优先级,无铃声无振动,仅提示不打扰 // 3. 应用配置信息渠道(方案1修复默认优先级系统默认铃声无振动)
NotificationChannel configChannel = new NotificationChannel( NotificationChannel configChannel = new NotificationChannel(
CHANNEL_ID_CONFIG, CHANNEL_ID_CONFIG,
"应用配置信息", "应用配置信息",
NotificationManager.IMPORTANCE_MIN NotificationManager.IMPORTANCE_DEFAULT
); );
configChannel.setDescription("应用配置更新、参数变更等提示,无声音、无振动"); configChannel.setDescription("应用配置更新、参数变更等提示,系统默认铃声、无振动");
configChannel.enableLights(false); configChannel.enableLights(true);
configChannel.enableVibration(false); configChannel.enableVibration(false);
configChannel.setSound(null, null); configChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT);
configChannel.setShowBadge(false); configChannel.setShowBadge(false);
configChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); configChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
LogUtils.d(TAG, "initNotificationChannels: 应用配置信息渠道配置完成"); LogUtils.d(TAG, "initNotificationChannels() 应用配置信息渠道配置完成");
// 注册渠道到系统 // 注册渠道到系统
mNotificationManager.createNotificationChannel(foregroundChannel); mNotificationManager.createNotificationChannel(foregroundChannel);
mNotificationManager.createNotificationChannel(remindChannel); mNotificationManager.createNotificationChannel(remindChannel);
mNotificationManager.createNotificationChannel(configChannel); // 注册新增渠道 mNotificationManager.createNotificationChannel(configChannel); // 注册新增渠道
LogUtils.d(TAG, "initNotificationChannels: 成功:创建前台服务+电池提醒+应用配置信息渠道"); LogUtils.d(TAG, "initNotificationChannels() 成功:创建前台服务+电池提醒+应用配置信息渠道");
} }
// ================================== 对外核心方法(前台服务通知:启动/更新/取消)================================= // ================================== 对外核心方法(前台服务通知:启动/更新/取消)=================================
@@ -145,26 +146,26 @@ public class NotificationManagerUtils {
* 启动前台服务通知API30适配无铃声 * 启动前台服务通知API30适配无铃声
*/ */
public void startForegroundServiceNotify(Service service, NotificationMessage message) { public void startForegroundServiceNotify(Service service, NotificationMessage message) {
LogUtils.d(TAG, "startForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | service=" + service + " | message=" + message); LogUtils.d(TAG, "startForegroundServiceNotify() 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | service=" + service + " | message=" + message);
// 前置校验:参数非空 // 前置校验:参数非空
if (service == null || message == null || mNotificationManager == null) { if (service == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "startForegroundServiceNotify: 失败param is null | service=" + service + " | message=" + message + " | mNotificationManager=" + mNotificationManager); LogUtils.e(TAG, "startForegroundServiceNotify() 失败param is null | service=" + service + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return; return;
} }
// 构建前台通知 // 构建前台通知
mForegroundServiceNotify = buildForegroundNotification(message); mForegroundServiceNotify = buildForegroundNotification(message);
if (mForegroundServiceNotify == null) { if (mForegroundServiceNotify == null) {
LogUtils.e(TAG, "startForegroundServiceNotify: 失败:构建通知为空"); LogUtils.e(TAG, "startForegroundServiceNotify() 失败:构建通知为空");
return; return;
} }
// 启动前台服务API30无FOREGROUND_SERVICE_TYPE限制全版本通用 // 启动前台服务API30无FOREGROUND_SERVICE_TYPE限制全版本通用
try { try {
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify); service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "startForegroundServiceNotify: 成功"); LogUtils.d(TAG, "startForegroundServiceNotify() 成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "startForegroundServiceNotify: 异常", e); LogUtils.e(TAG, "startForegroundServiceNotify() 异常", e);
} }
} }
@@ -172,23 +173,23 @@ public class NotificationManagerUtils {
* 更新前台服务通知内容复用通知ID保持无铃声 * 更新前台服务通知内容复用通知ID保持无铃声
*/ */
public void updateForegroundServiceNotify(NotificationMessage message) { public void updateForegroundServiceNotify(NotificationMessage message) {
LogUtils.d(TAG, "updateForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | message=" + message); LogUtils.d(TAG, "updateForegroundServiceNotify() 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | message=" + message);
if (message == null || mNotificationManager == null) { if (message == null || mNotificationManager == null) {
LogUtils.e(TAG, "updateForegroundServiceNotify: 失败param is null | message=" + message + " | mNotificationManager=" + mNotificationManager); LogUtils.e(TAG, "updateForegroundServiceNotify() 失败param is null | message=" + message + " | mNotificationManager=" + mNotificationManager);
return; return;
} }
mForegroundServiceNotify = buildForegroundNotification(message); mForegroundServiceNotify = buildForegroundNotification(message);
if (mForegroundServiceNotify == null) { if (mForegroundServiceNotify == null) {
LogUtils.e(TAG, "updateForegroundServiceNotify: 失败:构建通知为空"); LogUtils.e(TAG, "updateForegroundServiceNotify() 失败:构建通知为空");
return; return;
} }
try { try {
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify); mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "updateForegroundServiceNotify: 成功"); LogUtils.d(TAG, "updateForegroundServiceNotify() 成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "updateForegroundServiceNotify: 异常", e); LogUtils.e(TAG, "updateForegroundServiceNotify() 异常", e);
} }
} }
@@ -196,10 +197,10 @@ public class NotificationManagerUtils {
* 取消前台服务通知Service销毁时调用 * 取消前台服务通知Service销毁时调用
*/ */
public void cancelForegroundServiceNotify() { public void cancelForegroundServiceNotify() {
LogUtils.d(TAG, "cancelForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE); LogUtils.d(TAG, "cancelForegroundServiceNotify() 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE);
cancelNotification(NOTIFY_ID_FOREGROUND_SERVICE); cancelNotification(NOTIFY_ID_FOREGROUND_SERVICE);
mForegroundServiceNotify = null; // 置空释放 mForegroundServiceNotify = null; // 置空释放
LogUtils.d(TAG, "cancelForegroundServiceNotify: 成功"); LogUtils.d(TAG, "cancelForegroundServiceNotify() 成功");
} }
// ================================== 对外核心方法(电池提醒通知:发送)================================= // ================================== 对外核心方法(电池提醒通知:发送)=================================
@@ -207,48 +208,70 @@ public class NotificationManagerUtils {
* 发送电池提醒通知(系统默认铃声,无振动) * 发送电池提醒通知(系统默认铃声,无振动)
*/ */
public void showRemindNotification(Context context, NotificationMessage message) { public void showRemindNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "showRemindNotification: 执行 | notifyId=" + NOTIFY_ID_REMIND + " | context=" + context + " | message=" + message); LogUtils.d(TAG, "showRemindNotification() 执行 | notifyId=" + NOTIFY_ID_REMIND + " | context=" + context + " | message=" + message);
if (context == null || message == null || mNotificationManager == null) { if (context == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "showRemindNotification: 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager); LogUtils.e(TAG, "showRemindNotification() 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return; return;
} }
Notification remindNotify = buildRemindNotification(context, message); Notification remindNotify = buildRemindNotification(context, message);
if (remindNotify == null) { if (remindNotify == null) {
LogUtils.e(TAG, "showRemindNotification: 失败:构建通知为空"); LogUtils.e(TAG, "showRemindNotification() 失败:构建通知为空");
return; return;
} }
try { try {
mNotificationManager.notify(NOTIFY_ID_REMIND, remindNotify); mNotificationManager.notify(NOTIFY_ID_REMIND, remindNotify);
LogUtils.d(TAG, "showRemindNotification: 成功"); LogUtils.d(TAG, "showRemindNotification() 成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "showRemindNotification: 异常", e); LogUtils.e(TAG, "showRemindNotification() 异常", e);
} }
} }
// ================================== 对外核心方法(应用配置信息通知:发送)================================= public synchronized void showMessageNotification(Context context, NotificationMessage message) {
/** snMessageNotificationID++;
* 发送应用配置信息通知(新增:低优先级无铃声,仅提示不打扰) LogUtils.d(TAG, "showMessageNotification() 执行 | notifyId=" + snMessageNotificationID + " | context=" + context + " | message=" + message);
*/
public void showConfigNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "showConfigNotification: 执行 | notifyId=" + NOTIFY_ID_CONFIG + " | context=" + context + " | message=" + message);
if (context == null || message == null || mNotificationManager == null) { if (context == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "showConfigNotification: 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager); LogUtils.e(TAG, "showMessageNotification() 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return; return;
} }
Notification configNotify = buildConfigNotification(context, message); Notification configNotify = buildConfigNotification(context, message);
if (configNotify == null) { if (configNotify == null) {
LogUtils.e(TAG, "showConfigNotification: 失败:构建通知为空"); LogUtils.e(TAG, "showMessageNotification() 失败:构建通知为空");
return;
}
try {
mNotificationManager.notify(snMessageNotificationID, configNotify);
LogUtils.d(TAG, "showMessageNotification() 成功");
} catch (Exception e) {
LogUtils.e(TAG, "showMessageNotification() 异常", e);
}
}
// ================================== 对外核心方法(应用配置信息通知:发送)=================================
/**
* 发送应用配置信息通知方案1修复系统默认铃声无振动
*/
public void showConfigNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "showConfigNotification() 执行 | notifyId=" + NOTIFY_ID_CONFIG + " | context=" + context + " | message=" + message);
if (context == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "showConfigNotification() 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return;
}
Notification configNotify = buildConfigNotification(context, message);
if (configNotify == null) {
LogUtils.e(TAG, "showConfigNotification() 失败:构建通知为空");
return; return;
} }
try { try {
mNotificationManager.notify(NOTIFY_ID_CONFIG, configNotify); mNotificationManager.notify(NOTIFY_ID_CONFIG, configNotify);
LogUtils.d(TAG, "showConfigNotification: 成功"); LogUtils.d(TAG, "showConfigNotification() 成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "showConfigNotification: 异常", e); LogUtils.e(TAG, "showConfigNotification() 异常", e);
} }
} }
@@ -257,16 +280,16 @@ public class NotificationManagerUtils {
* 取消指定ID的通知 * 取消指定ID的通知
*/ */
public void cancelNotification(int notifyId) { public void cancelNotification(int notifyId) {
LogUtils.d(TAG, "cancelNotification: 执行 | notifyId=" + notifyId); LogUtils.d(TAG, "cancelNotification() 执行 | notifyId=" + notifyId);
if (mNotificationManager == null) { if (mNotificationManager == null) {
LogUtils.e(TAG, "cancelNotification: 失败NotificationManager is null"); LogUtils.e(TAG, "cancelNotification() 失败NotificationManager is null");
return; return;
} }
try { try {
mNotificationManager.cancel(notifyId); mNotificationManager.cancel(notifyId);
LogUtils.d(TAG, "cancelNotification: 成功 | notifyId=" + notifyId); LogUtils.d(TAG, "cancelNotification() 成功 | notifyId=" + notifyId);
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "cancelNotification: 异常 | notifyId=" + notifyId, e); LogUtils.e(TAG, "cancelNotification() 异常 | notifyId=" + notifyId, e);
} }
} }
@@ -274,16 +297,16 @@ public class NotificationManagerUtils {
* 取消所有通知(兜底场景使用) * 取消所有通知(兜底场景使用)
*/ */
public void cancelAllNotifications() { public void cancelAllNotifications() {
LogUtils.d(TAG, "cancelAllNotifications: 执行"); LogUtils.d(TAG, "cancelAllNotifications() 执行");
if (mNotificationManager == null) { if (mNotificationManager == null) {
LogUtils.e(TAG, "cancelAllNotifications: 失败NotificationManager is null"); LogUtils.e(TAG, "cancelAllNotifications() 失败NotificationManager is null");
return; return;
} }
try { try {
mNotificationManager.cancelAll(); mNotificationManager.cancelAll();
LogUtils.d(TAG, "cancelAllNotifications: 成功"); LogUtils.d(TAG, "cancelAllNotifications() 成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "cancelAllNotifications: 异常", e); LogUtils.e(TAG, "cancelAllNotifications() 异常", e);
} }
} }
@@ -292,30 +315,30 @@ public class NotificationManagerUtils {
* 构建前台服务通知(全版本无铃声+无振动) * 构建前台服务通知(全版本无铃声+无振动)
*/ */
private Notification buildForegroundNotification(NotificationMessage message) { private Notification buildForegroundNotification(NotificationMessage message) {
LogUtils.d(TAG, "buildForegroundNotification: 执行 | message=" + message); LogUtils.d(TAG, "buildForegroundNotification() 执行 | message=" + message);
if (message == null || mContext == null) { if (message == null || mContext == null) {
LogUtils.e(TAG, "buildForegroundNotification: 失败param is null | message=" + message + " | mContext=" + mContext); LogUtils.e(TAG, "buildForegroundNotification() 失败param is null | message=" + message + " | mContext=" + mContext);
return null; return null;
} }
// 内容兜底 // 内容兜底
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : FOREGROUND_NOTIFY_TITLE_DEFAULT; String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : FOREGROUND_NOTIFY_TITLE_DEFAULT;
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : FOREGROUND_NOTIFY_CONTENT_DEFAULT; String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : FOREGROUND_NOTIFY_CONTENT_DEFAULT;
LogUtils.d(TAG, "buildForegroundNotification: 内容兜底完成 | title=" + title + " | content=" + content); LogUtils.d(TAG, "buildForegroundNotification() 内容兜底完成 | title=" + title + " | content=" + content);
Notification.Builder builder; Notification.Builder builder;
// API分级构建 // API分级构建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API26+:绑定前台渠道(渠道已配置无铃声) // API26+:绑定前台渠道(渠道已配置无铃声)
builder = new Notification.Builder(mContext, CHANNEL_ID_FOREGROUND); builder = new Notification.Builder(mContext, CHANNEL_ID_FOREGROUND);
LogUtils.d(TAG, "buildForegroundNotification: 使用API26+渠道构建"); LogUtils.d(TAG, "buildForegroundNotification() 使用API26+渠道构建");
} else { } else {
// API<26直接构建手动禁用铃声振动 // API<26直接构建手动禁用铃声振动
builder = new Notification.Builder(mContext); builder = new Notification.Builder(mContext);
builder.setSound(null); builder.setSound(null);
builder.setVibrate(new long[]{0}); builder.setVibrate(new long[]{0});
builder.setDefaults(0); builder.setDefaults(0);
LogUtils.d(TAG, "buildForegroundNotification: 使用API<26手动配置"); LogUtils.d(TAG, "buildForegroundNotification() 使用API<26手动配置");
} }
// 通用配置 // 通用配置
@@ -332,11 +355,11 @@ public class NotificationManagerUtils {
builder.setLargeIcon(getAppIcon(mContext)) builder.setLargeIcon(getAppIcon(mContext))
.setColor(mContext.getResources().getColor(R.color.colorPrimary)) .setColor(mContext.getResources().getColor(R.color.colorPrimary))
.setPriority(Notification.PRIORITY_LOW); .setPriority(Notification.PRIORITY_LOW);
LogUtils.d(TAG, "buildForegroundNotification: 补充API21+配置"); LogUtils.d(TAG, "buildForegroundNotification() 补充API21+配置");
} }
Notification notification = builder.build(); Notification notification = builder.build();
LogUtils.d(TAG, "buildForegroundNotification: 成功构建前台通知"); LogUtils.d(TAG, "buildForegroundNotification() 成功构建前台通知");
return notification; return notification;
} }
@@ -345,30 +368,30 @@ public class NotificationManagerUtils {
* 构建电池提醒通知(全版本系统默认铃声+无振动) * 构建电池提醒通知(全版本系统默认铃声+无振动)
*/ */
private Notification buildRemindNotification(Context context, NotificationMessage message) { private Notification buildRemindNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "buildRemindNotification: 执行 | context=" + context + " | message=" + message); LogUtils.d(TAG, "buildRemindNotification() 执行 | context=" + context + " | message=" + message);
if (context == null || message == null) { if (context == null || message == null) {
LogUtils.e(TAG, "buildRemindNotification: 失败param is null | context=" + context + " | message=" + message); LogUtils.e(TAG, "buildRemindNotification() 失败param is null | context=" + context + " | message=" + message);
return null; return null;
} }
// 内容兜底 // 内容兜底
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : REMIND_NOTIFY_TITLE_DEFAULT; String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : REMIND_NOTIFY_TITLE_DEFAULT;
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : REMIND_NOTIFY_CONTENT_DEFAULT; String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : REMIND_NOTIFY_CONTENT_DEFAULT;
LogUtils.d(TAG, "buildRemindNotification: 内容兜底完成 | title=" + title + " | content=" + content); LogUtils.d(TAG, "buildRemindNotification() 内容兜底完成 | title=" + title + " | content=" + content);
Notification.Builder builder; Notification.Builder builder;
// API分级构建 // API分级构建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API26+:绑定提醒渠道(渠道已配置默认铃声) // API26+:绑定提醒渠道(渠道已配置默认铃声)
builder = new Notification.Builder(context, CHANNEL_ID_REMIND); builder = new Notification.Builder(context, CHANNEL_ID_REMIND);
LogUtils.d(TAG, "buildRemindNotification: 使用API26+渠道构建"); LogUtils.d(TAG, "buildRemindNotification() 使用API26+渠道构建");
} else { } else {
// API<26手动配置默认铃声关闭振动 // API<26手动配置默认铃声关闭振动
builder = new Notification.Builder(context); builder = new Notification.Builder(context);
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI) // 显式默认铃声 builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI) // 显式默认铃声
.setVibrate(new long[]{0}) .setVibrate(new long[]{0})
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND); .setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND);
LogUtils.d(TAG, "buildRemindNotification: 使用API<26手动配置"); LogUtils.d(TAG, "buildRemindNotification() 使用API<26手动配置");
} }
// 通用配置 // 通用配置
@@ -386,43 +409,43 @@ public class NotificationManagerUtils {
builder.setLargeIcon(getAppIcon(context)) builder.setLargeIcon(getAppIcon(context))
.setColor(context.getResources().getColor(R.color.colorPrimary)) .setColor(context.getResources().getColor(R.color.colorPrimary))
.setPriority(Notification.PRIORITY_DEFAULT); .setPriority(Notification.PRIORITY_DEFAULT);
LogUtils.d(TAG, "buildRemindNotification: 补充API21+配置"); LogUtils.d(TAG, "buildRemindNotification() 补充API21+配置");
} }
Notification notification = builder.build(); Notification notification = builder.build();
LogUtils.d(TAG, "buildRemindNotification: 成功构建提醒通知"); LogUtils.d(TAG, "buildRemindNotification() 成功构建提醒通知");
return notification; return notification;
} }
// ================================== 内部辅助方法(通知构建:应用配置信息通知)================================= // ================================== 内部辅助方法(通知构建:应用配置信息通知)=================================
/** /**
* 构建应用配置信息通知(新增:全版本无铃声+无振动,低优先级 * 构建应用配置信息通知(方案1修复全版本系统默认铃声+无振动)
*/ */
private Notification buildConfigNotification(Context context, NotificationMessage message) { private Notification buildConfigNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "buildConfigNotification: 执行 | context=" + context + " | message=" + message); LogUtils.d(TAG, "buildConfigNotification() 执行 | context=" + context + " | message=" + message);
if (context == null || message == null) { if (context == null || message == null) {
LogUtils.e(TAG, "buildConfigNotification: 失败param is null | context=" + context + " | message=" + message); LogUtils.e(TAG, "buildConfigNotification() 失败param is null | context=" + context + " | message=" + message);
return null; return null;
} }
// 内容兜底 // 内容兜底
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : CONFIG_NOTIFY_TITLE_DEFAULT; String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : CONFIG_NOTIFY_TITLE_DEFAULT;
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : CONFIG_NOTIFY_CONTENT_DEFAULT; String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : CONFIG_NOTIFY_CONTENT_DEFAULT;
LogUtils.d(TAG, "buildConfigNotification: 内容兜底完成 | title=" + title + " | content=" + content); LogUtils.d(TAG, "buildConfigNotification() 内容兜底完成 | title=" + title + " | content=" + content);
Notification.Builder builder; Notification.Builder builder;
// API分级构建 // API分级构建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API26+:绑定配置渠道(渠道已配置铃声) // API26+:绑定配置渠道(渠道已配置默认铃声)
builder = new Notification.Builder(context, CHANNEL_ID_CONFIG); builder = new Notification.Builder(context, CHANNEL_ID_CONFIG);
LogUtils.d(TAG, "buildConfigNotification: 使用API26+渠道构建"); LogUtils.d(TAG, "buildConfigNotification() 使用API26+渠道构建");
} else { } else {
// API<26直接构建,手动禁用铃声振动 // API<26手动配置默认铃声关闭振动方案1修复保留铃声配置删除冗余DEFAULT_SOUND
builder = new Notification.Builder(context); builder = new Notification.Builder(context);
builder.setSound(null); builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
builder.setVibrate(new long[]{0}); builder.setVibrate(new long[]{0});
builder.setDefaults(0); builder.setDefaults(Notification.DEFAULT_LIGHTS);
LogUtils.d(TAG, "buildConfigNotification: 使用API<26手动配置"); LogUtils.d(TAG, "buildConfigNotification() 使用API<26手动配置");
} }
// 通用配置 // 通用配置
@@ -438,12 +461,12 @@ public class NotificationManagerUtils {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setLargeIcon(getAppIcon(context)) builder.setLargeIcon(getAppIcon(context))
.setColor(context.getResources().getColor(R.color.colorPrimary)) .setColor(context.getResources().getColor(R.color.colorPrimary))
.setPriority(Notification.PRIORITY_MIN); // 最低优先级 .setPriority(Notification.PRIORITY_DEFAULT);
LogUtils.d(TAG, "buildConfigNotification: 补充API21+配置"); LogUtils.d(TAG, "buildConfigNotification() 补充API21+配置");
} }
Notification notification = builder.build(); Notification notification = builder.build();
LogUtils.d(TAG, "buildConfigNotification: 成功构建配置信息通知"); LogUtils.d(TAG, "buildConfigNotification() 成功构建配置信息通知");
return notification; return notification;
} }
@@ -452,20 +475,20 @@ public class NotificationManagerUtils {
* 创建跳转MainActivity的PendingIntentAPI23+ 添加IMMUTABLE标记避免安全异常 * 创建跳转MainActivity的PendingIntentAPI23+ 添加IMMUTABLE标记避免安全异常
*/ */
private PendingIntent createJumpPendingIntent(Context context, int requestCode) { private PendingIntent createJumpPendingIntent(Context context, int requestCode) {
LogUtils.d(TAG, "createJumpPendingIntent: 执行 | requestCode=" + requestCode + " | context=" + context); LogUtils.d(TAG, "createJumpPendingIntent() 执行 | requestCode=" + requestCode + " | context=" + context);
Intent intent = new Intent(context, MainActivity.class); Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
LogUtils.d(TAG, "createJumpPendingIntent: 跳转Intent配置完成"); LogUtils.d(TAG, "createJumpPendingIntent() 跳转Intent配置完成");
// API23+ 必需添加IMMUTABLE适配API30安全规范 // API23+ 必需添加IMMUTABLE适配API30安全规范
int flags = PendingIntent.FLAG_UPDATE_CURRENT; int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE; flags |= PendingIntent.FLAG_IMMUTABLE;
LogUtils.d(TAG, "createJumpPendingIntent: 添加FLAG_IMMUTABLE标记API23+"); LogUtils.d(TAG, "createJumpPendingIntent() 添加FLAG_IMMUTABLE标记API23+");
} }
PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, flags); PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, flags);
LogUtils.d(TAG, "createJumpPendingIntent: 成功 | requestCode=" + requestCode); LogUtils.d(TAG, "createJumpPendingIntent() 成功 | requestCode=" + requestCode);
return pendingIntent; return pendingIntent;
} }
@@ -474,14 +497,14 @@ public class NotificationManagerUtils {
* 获取APP图标失败返回默认图标 * 获取APP图标失败返回默认图标
*/ */
private Bitmap getAppIcon(Context context) { private Bitmap getAppIcon(Context context) {
LogUtils.d(TAG, "getAppIcon: 执行 | context=" + context); LogUtils.d(TAG, "getAppIcon() 执行 | context=" + context);
try { try {
PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
Bitmap appIcon = BitmapFactory.decodeResource(context.getResources(), pkgInfo.applicationInfo.icon); Bitmap appIcon = BitmapFactory.decodeResource(context.getResources(), pkgInfo.applicationInfo.icon);
LogUtils.d(TAG, "getAppIcon: 成功:获取应用图标"); LogUtils.d(TAG, "getAppIcon() 成功:获取应用图标");
return appIcon; return appIcon;
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getAppIcon: 异常:获取应用图标失败,使用默认图标", e); LogUtils.e(TAG, "getAppIcon() 异常:获取应用图标失败,使用默认图标", e);
return BitmapFactory.decodeResource(context.getResources(), NOTIFICATION_DEFAULT_ICON); return BitmapFactory.decodeResource(context.getResources(), NOTIFICATION_DEFAULT_ICON);
} }
} }
@@ -491,11 +514,11 @@ public class NotificationManagerUtils {
* 释放资源,销毁时调用 * 释放资源,销毁时调用
*/ */
public void release() { public void release() {
LogUtils.d(TAG, "release: 执行资源释放"); LogUtils.d(TAG, "release() 执行资源释放");
cancelForegroundServiceNotify(); cancelForegroundServiceNotify();
mNotificationManager = null; mNotificationManager = null;
mContext = null; mContext = null;
LogUtils.d(TAG, "release: 成功:所有资源已释放"); LogUtils.d(TAG, "release() 成功:所有资源已释放");
} }
// ================================== 对外 getter 方法(仅前台通知实例,只读)================================= // ================================== 对外 getter 方法(仅前台通知实例,只读)=================================

View File

@@ -2,29 +2,69 @@ package cc.winboll.studio.powerbell.utils;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.List; import java.util.List;
/**
* 服务状态工具类
* 功能:判断指定服务是否处于运行状态
* 适配Java 7 + Android API 30
* 注意Android 8.0+ 对后台服务限制严格,此方法仅适用于前台服务或兼容场景
*/
public class ServiceUtils { public class ServiceUtils {
// ================================== 静态常量区(置顶归类)=================================
public static final String TAG = ServiceUtils.class.getSimpleName(); public static final String TAG = ServiceUtils.class.getSimpleName();
// 最大查询服务数量
private static final int MAX_RUNNING_SERVICES = 1000;
public static boolean isServiceAlive(Context context, String szServiceName) { // ================================== 核心工具方法(判断服务是否运行)=================================
// 获取Activity管理者对象 /**
ActivityManager manager = (ActivityManager) context * 判断指定服务是否处于运行状态
.getSystemService(Context.ACTIVITY_SERVICE); * @param context 上下文(建议使用 Application 上下文避免内存泄漏)
// 获取正在运行的服务此处设置最多取1000个 * @param serviceName 服务完整类名com.example.app.service.DemoService
List<ActivityManager.RunningServiceInfo> runningServices = manager * @return true-服务运行中false-服务未运行或查询失败
.getRunningServices(1000); */
if (runningServices.size() <= 0) { public static boolean isServiceAlive(Context context, String serviceName) {
LogUtils.d(TAG, "【isServiceAlive】调用开始 | 服务名称=" + serviceName);
// 1. 前置参数校验
if (context == null) {
LogUtils.e(TAG, "【isServiceAlive】参数异常Context 为空");
return false; return false;
} }
// 遍历若存在名字和传入的serviceName的一致则说明存在 if (TextUtils.isEmpty(serviceName)) {
for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) { LogUtils.e(TAG, "【isServiceAlive】参数异常服务名称为空");
if (runningServiceInfo.service.getClassName().equals(szServiceName)) { return false;
}
// 2. 获取 ActivityManager
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
LogUtils.e(TAG, "【isServiceAlive】获取 ActivityManager 失败");
return false;
}
// 3. 查询正在运行的服务
List<ActivityManager.RunningServiceInfo> runningServices = activityManager.getRunningServices(MAX_RUNNING_SERVICES);
if (runningServices == null || runningServices.size() <= 0) {
LogUtils.d(TAG, "【isServiceAlive】正在运行的服务列表为空");
return false;
}
// 4. 遍历服务列表,匹配目标服务
for (ActivityManager.RunningServiceInfo serviceInfo : runningServices) {
if (serviceInfo.service == null) {
continue;
}
String className = serviceInfo.service.getClassName();
if (serviceName.equals(className)) {
LogUtils.d(TAG, "【isServiceAlive】服务运行中 | 匹配成功:" + serviceName);
return true; return true;
} }
} }
LogUtils.d(TAG, "【isServiceAlive】服务未运行 | 未匹配到:" + serviceName);
return false; return false;
} }
} }

View File

@@ -1,114 +1,151 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BatteryInfoBean; import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale;
/**
* 字符串格式化工具类
* 功能:电量使用时间列表格式化、时间跨度计算
* 适配Java 7 + Android API 30
* 核心逻辑:将电池信息列表转换为指定格式字符串,计算时间戳之间的跨度并格式化
*/
public class StringUtils { public class StringUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = StringUtils.class.getSimpleName(); public static final String TAG = StringUtils.class.getSimpleName();
// 时间跨度单位符号
private static final String UNIT_DAY = "";
private static final String UNIT_HOUR = "";
private static final String UNIT_MINUTE = "";
private static final String UNIT_SECOND_DEFAULT = "☆}";
// 时间计算常量
private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000L;
private static final long MILLIS_PER_HOUR = 60 * 60 * 1000L;
private static final long MILLIS_PER_MINUTE = 60 * 1000L;
private static final long MILLIS_PER_SECOND = 1000L;
// 空字符串常量(替代 TextUtils.EMPTY保证 Java 7 兼容)
private static final String EMPTY_STRING = "";
// 电量改变使用分钟数列表 // ================================== 核心格式化方法(电量列表格式化)=================================
// List of power-changing usage minutes /**
// * 格式化电量使用时间列表为单行字符串
public static String formatPCMListString(ArrayList<BatteryInfoBean> arrayListBatteryInfo) { * @param batteryInfoList 电池信息列表(非空)
/* 调试数据 * @return 格式化后的单行字符串,格式:"电量% 时间跨度 电量% 时间跨度 ..."
Time t1 = new Time(); */
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {} public static String formatPCMListString(ArrayList<BatteryInfoBean> batteryInfoList) {
t1.set(4, 8, 0, 27, 4, 2022); LogUtils.d(TAG, "【formatPCMListString】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null));
long ntime1 = t1.toMillis(true); // 1. 参数校验
Time t2 = new Time(); if (batteryInfoList == null || batteryInfoList.size() < 2) {
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {} LogUtils.e(TAG, "【formatPCMListString】参数异常列表为空或长度不足2");
t2.set(9, 12, 3, 29, 4, 2022); return EMPTY_STRING;
long ntime2 = t2.toMillis(true);
LogUtils.d(TAG, "ntime1 is " + Long.toString(ntime1));
LogUtils.d(TAG, "ntime2 is " + Long.toString(ntime2));
LogUtils.d(TAG, "getTimespanDifference(ntime1, ntime2) is " + getTimespanDifference(ntime1, ntime2));
*/
/*String sz = "";
for (int i = 0; i < lnTime.size() - 1; i++) {
sz += getTimespanDifference(lnTime.get(i), lnTime.get(i + 1));
}
return sz;*/
String sz = "";
for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) {
//LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue()));
sz = arrayListBatteryInfo.get(i).getBattetyValue() + "% " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz;
} }
return sz;
} String result = EMPTY_STRING;
// 2. 遍历列表,拼接字符串(倒序拼接)
public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> arrayListBatteryInfo) { for (int i = 0; i < batteryInfoList.size() - 1; i++) {
String sz = ""; BatteryInfoBean currentBean = batteryInfoList.get(i);
for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) { BatteryInfoBean nextBean = batteryInfoList.get(i + 1);
//LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue())); // 空指针防护
sz = "\n" + arrayListBatteryInfo.get(i).getBattetyValue() + "%\n " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz; if (currentBean == null || nextBean == null) {
LogUtils.w(TAG, "【formatPCMListString】列表项为空跳过当前索引" + i);
continue;
}
// 获取电量和时间跨度
int batteryValue = currentBean.getBatteryValue();
String timeSpan = getTimespanDifference(currentBean.getTimeStamp(), nextBean.getTimeStamp());
// 倒序拼接
result = batteryValue + "% " + timeSpan + " " + result;
LogUtils.d(TAG, "【formatPCMListString】循环拼接 | 索引=" + i + " | 电量=" + batteryValue + "% | 时间跨度=" + timeSpan);
} }
return sz;
LogUtils.d(TAG, "【formatPCMListString】格式化完成 | 结果长度=" + result.length());
return result;
} }
// 获取时间之间的时间跨度字符串。 /**
// Get timespan string between times. * 格式化电量使用时间列表为带换行的字符串
// 返回值: {(几天/)(几小时/)(几分钟/)(几秒钟)} * @param batteryInfoList 电池信息列表(非空)
// 返回值: {(几小时/)(几分钟/)(几秒钟)} * @return 格式化后的带换行字符串,每行一个电量和时间跨度
// 返回值: {(几分钟/)(几秒钟)} */
// 返回值: {(几秒钟)} public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> batteryInfoList) {
// (注start == end 时) 返回值: {0} LogUtils.d(TAG, "【formatPCMListStringWithEnter】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null));
// 1. 参数校验
if (batteryInfoList == null || batteryInfoList.size() < 2) {
LogUtils.e(TAG, "【formatPCMListStringWithEnter】参数异常列表为空或长度不足2");
return EMPTY_STRING;
}
String result = EMPTY_STRING;
// 2. 遍历列表,拼接字符串(倒序拼接,带换行)
for (int i = 0; i < batteryInfoList.size() - 1; i++) {
BatteryInfoBean currentBean = batteryInfoList.get(i);
BatteryInfoBean nextBean = batteryInfoList.get(i + 1);
// 空指针防护
if (currentBean == null || nextBean == null) {
LogUtils.w(TAG, "【formatPCMListStringWithEnter】列表项为空跳过当前索引" + i);
continue;
}
// 获取电量和时间跨度
int batteryValue = currentBean.getBatteryValue();
String timeSpan = getTimespanDifference(currentBean.getTimeStamp(), nextBean.getTimeStamp());
// 倒序拼接(带换行)
result = "\n" + batteryValue + "%\n " + timeSpan + " " + result;
LogUtils.d(TAG, "【formatPCMListStringWithEnter】循环拼接 | 索引=" + i + " | 电量=" + batteryValue + "% | 时间跨度=" + timeSpan);
}
LogUtils.d(TAG, "【formatPCMListStringWithEnter】格式化完成 | 结果长度=" + result.length());
return result;
}
// ================================== 时间跨度计算方法(核心工具方法)=================================
/**
* 计算两个时间戳之间的跨度并格式化为指定字符串
* @param start 开始时间戳(毫秒)
* @param end 结束时间戳(毫秒)
* @return 格式化的时间跨度字符串,格式:{天☀时★分✰秒} 或 {☆}当时间差为0时
*/
public static String getTimespanDifference(long start, long end) { public static String getTimespanDifference(long start, long end) {
String szReturn = "{"; LogUtils.d(TAG, "【getTimespanDifference】调用开始 | 开始时间戳=" + start + " | 结束时间戳=" + end);
long between = end - start; long between = end - start;
//LogUtils.d(TAG, "between is " + Long.toString(between)); LogUtils.d(TAG, "【getTimespanDifference】时间差毫秒=" + between);
long day = between / (24 * 60 * 60 * 1000);
long hour = (between / (60 * 60 * 1000) - day * 24);
long min = ((between / (60 * 1000)) - day * 24 * 60 - hour * 60);
long s = (between / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60);
/* 调试数据
day = 0;
hour = 2;
min = 0;
s = 7;
*/
//long ms = (between - day * 24 * 60 * 60 * 1000 - hour * 60 * 60 * 1000 // 计算天、时、分、秒
//- min * 60 * 1000 - s * 1000); long day = between / MILLIS_PER_DAY;
long hour = (between % MILLIS_PER_DAY) / MILLIS_PER_HOUR;
long min = (between % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE;
long sec = (between % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND;
szReturn += day > 0 ? String.format(java.util.Locale.getDefault(), "%d☀", day) : ""; // 拼接结果字符串
szReturn += hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d★", hour) : ""; StringBuilder result = new StringBuilder("{");
szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d✰", min) : ""; boolean hasHigherUnit = false;
szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d}", s) : "☆}";
//String strmin = String.format("%02d", min); // 拼接天
//String strs = String.format("%02d", s); if (day > 0) {
//String strms = String.format("%03d",ms); result.append(String.format(Locale.getDefault(), "%d%s", day, UNIT_DAY));
//String timeDifference = day + "天" + hour + "小时" + strmin + "分" + strs + "秒" + strms + "毫秒"; hasHigherUnit = true;
//String timeDifference = hour + getString(R.string.activity_main_msg_hour) }
// + strmin + getString(R.string.activity_main_msg_minute) // 拼接时(当天>0或后续有单位时
// + strs + getString(R.string.activity_main_msg_second); if (hour > 0 || hasHigherUnit) {
//return timeDifference; result.append(String.format(Locale.getDefault(), "%d%s", hour, UNIT_HOUR));
hasHigherUnit = true;
}
// 拼接分(当时>0或后续有单位时
if (min > 0 || hasHigherUnit) {
result.append(String.format(Locale.getDefault(), "%d%s", min, UNIT_MINUTE));
hasHigherUnit = true;
}
// 拼接秒或默认值
if (hasHigherUnit) {
result.append(String.format(Locale.getDefault(), "%d}", sec));
} else {
result.append(UNIT_SECOND_DEFAULT);
}
return szReturn; String timeSpan = result.toString();
LogUtils.d(TAG, "【getTimespanDifference】计算完成 | 时间跨度=" + timeSpan);
return timeSpan;
} }
// 调试函数: 调试formatPCMListString(ArrayList<Long> lnTime)
//
/*public static String formatPCMListString_test() {
// 调试数据
ArrayList<Long> listTime = new ArrayList<Long>();
Time t1 = new Time();
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
t1.set(0, 8, 0, 27, 4, 2022);
long ntime1 = t1.toMillis(true);
listTime.add(ntime1);
for (int i = 0; i < 5; i++) {
Time t2 = new Time();
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
t2.set(4, 8 + i + 1, 0, 27, 4, 2022);
long ntime2 = t2.toMillis(true);
listTime.add(ntime2);
}
return formatPCMListString(listTime);
//LogUtils.d(TAG, StringUtils.formatPCMListString(listTime));
}*/
} }

View File

@@ -0,0 +1,251 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Build;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.TTSSpeakTextBean;
import java.util.ArrayList;
/**
* TTS语音播放工具类 (单例实现)
* 适配Java7 语法规范 | Android API36 系统版本【修复崩溃】
* 功能:队列播放语音文本 + 播放悬浮窗展示 + 点击悬浮窗停止播放/关闭悬浮窗
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:03
*/
public class TextToSpeechUtils {
// ====================================== 常量区 - 静态全局常量 (置顶) ======================================
public static final String TAG = "TextToSpeechUtils";
public static final String UNIQUE_ID = "UNIQUE_ID";
// ====================================== 单例实例 - 静态私有 (饿汉式优化) ======================================
private static volatile TextToSpeechUtils sTextToSpeechUtils;
// ====================================== 成员属性区 - 私有成员变量 (按功能归类 有序排列) ======================================
private Context mContext;
private WindowManager mWindowManager;
private TextToSpeech mTextToSpeech;
private View mView;
private volatile boolean isExist = false;
private UtteranceProgressListener mUtteranceProgressListener;
// ====================================== 构造方法 - 私有私有化 (单例模式) ======================================
private TextToSpeechUtils(Context context) {
LogUtils.d(TAG, "【构造方法】初始化TextToSpeechUtil实例");
this.mContext = context.getApplicationContext();
this.mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
this.initUtteranceProgressListener();
LogUtils.d(TAG, "【构造方法】初始化完成获取WindowManager实例"+mWindowManager);
}
// ====================================== 对外暴露方法 - 单例获取入口 (线程安全) ======================================
public static synchronized TextToSpeechUtils getInstance(Context context) {
LogUtils.d(TAG, "【getInstance】获取单例实例入参Context" + context);
if (sTextToSpeechUtils == null) {
LogUtils.d(TAG, "【getInstance】实例为空创建新的TextToSpeechUtil对象");
sTextToSpeechUtils = new TextToSpeechUtils(context);
}
return sTextToSpeechUtils;
}
// ====================================== 核心对外业务方法 - 播放TTS语音队列 【主入口】 ======================================
public void speekTTSList(final ArrayList<TTSSpeakTextBean> listTTSSpeakTextBean) {
LogUtils.d(TAG, "【speekTTSList】播放语音队列调用入参队列长度" + (listTTSSpeakTextBean == null ? 0 : listTTSSpeakTextBean.size()));
// 重置播放退出标志位
isExist = false;
LogUtils.d(TAG, "【speekTTSList】重置播放退出标志位 isExist = " + isExist);
// TTS实例为空 → 初始化TTS后重放
if (mTextToSpeech == null) {
LogUtils.d(TAG, "【speekTTSList】TextToSpeech实例为空开始初始化TTS");
mTextToSpeech = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int initStatus) {
LogUtils.d(TAG, "【onInit】TTS初始化回调初始化状态码" + initStatus);
if (initStatus == TextToSpeech.SUCCESS) {
LogUtils.d(TAG, "【onInit】TTS初始化成功重新调用语音播放方法");
speekTTSList(listTTSSpeakTextBean);
} else {
LogUtils.d(TAG, "【onInit】TTS init failed : " + initStatus + ". The app [https://play.google.com/store/apps/details?id=com.google.android.tts] maybe fix this TTS probrem. ");
}
}
});
mTextToSpeech.setOnUtteranceProgressListener(mUtteranceProgressListener);
LogUtils.d(TAG, "【speekTTSList】已为TTS绑定播放进度监听器");
} else {
// TTS实例就绪 → 执行播放逻辑
if (listTTSSpeakTextBean != null && listTTSSpeakTextBean.size() > 0) {
LogUtils.d(TAG, "【speekTTSList】TTS实例就绪语音队列数据有效开始播放逻辑处理");
// 清理过期的悬浮窗 - 防止内存泄漏/重复添加
clearFloatWindow();
// ========== 修复1添加悬浮窗权限检查有权限才初始化悬浮窗无权限则只播语音不崩溃 ==========
if (checkOverlayPermission()) {
initWindow();
LogUtils.d(TAG, "【speekTTSList】悬浮窗初始化并显示完成");
} else {
LogUtils.d(TAG, "【speekTTSList】悬浮窗权限未授予跳过悬浮窗显示仅播放语音");
}
// 获取第一条语音的延迟时间并休眠
int nDelay = listTTSSpeakTextBean.get(0).mnDelay;
LogUtils.d(TAG, "【speekTTSList】获取播放延迟时间" + nDelay + "ms开始休眠等待");
try {
Thread.sleep(nDelay);
} catch (InterruptedException e) {
LogUtils.d(TAG, "【speekTTSList】休眠等待被中断", e);
}
LogUtils.d(TAG, "【speekTTSList】休眠等待完成开始循环播放语音队列");
// 循环播放语音队列
for (int speakPosition = 0; speakPosition < listTTSSpeakTextBean.size() && !isExist; speakPosition++) {
String szSpeakContent = listTTSSpeakTextBean.get(speakPosition).mszSpeakContent;
isExist = (listTTSSpeakTextBean.size() - 2 < speakPosition);
LogUtils.d(TAG, "【speekTTSList】播放索引" + speakPosition + " | 播放文本:" + szSpeakContent + " | 当前退出标记位:" + isExist);
// 第一条语音清空队列播放,后续语音追加播放
if (speakPosition == 0) {
mTextToSpeech.speak(szSpeakContent, TextToSpeech.QUEUE_FLUSH, null, UNIQUE_ID);
LogUtils.d(TAG, "【speekTTSList】执行清空队列播放 → QUEUE_FLUSH");
} else {
mTextToSpeech.speak(szSpeakContent, TextToSpeech.QUEUE_ADD, null, UNIQUE_ID);
LogUtils.d(TAG, "【speekTTSList】执行追加队列播放 → QUEUE_ADD");
}
}
LogUtils.d(TAG, "【speekTTSList】语音队列循环播放逻辑执行完毕");
} else {
LogUtils.d(TAG, "【speekTTSList】语音队列为空/长度0跳过播放逻辑");
}
}
}
// ====================================== 私有工具方法 - 初始化播放监听器 ======================================
private void initUtteranceProgressListener() {
LogUtils.d(TAG, "【initUtteranceProgressListener】初始化TTS播放进度监听器");
mUtteranceProgressListener = new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
LogUtils.d(TAG, "【onStart】TTS语音播放开始唯一标识ID" + utteranceId);
}
@Override
public void onDone(String utteranceId) {
LogUtils.d(TAG, "【onDone】TTS语音播放结束唯一标识ID" + utteranceId + " | 退出标志位:" + isExist);
// 播放完成 关闭悬浮窗
if (isExist && mWindowManager != null && mView != null) {
LogUtils.d(TAG, "【onDone】满足关闭条件执行悬浮窗移除操作");
clearFloatWindow();
}
}
@Override
public void onError(String utteranceId) {
LogUtils.d(TAG, "【onError】TTS语音播放出错唯一标识ID" + utteranceId);
}
};
}
// ====================================== 私有核心方法 - 初始化并添加悬浮窗 【核心修复 根治崩溃】 ======================================
private void initWindow() {
LogUtils.d(TAG, "【initWindow】开始初始化播放悬浮窗");
// 创建Window布局参数
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
// ========== 修复2 重中之重Android 12(API31)+ 彻底废弃TYPE_PHONE统一用TYPE_APPLICATION_OVERLAY 适配API36 ==========
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
LogUtils.d(TAG, "【initWindow】系统版本>=API26悬浮窗类型TYPE_APPLICATION_OVERLAY");
} else {
// 仅低版本用TYPE_PHONE高版本不再走这里
params.type = WindowManager.LayoutParams.TYPE_PHONE;
LogUtils.d(TAG, "【initWindow】系统版本<API26悬浮窗类型TYPE_PHONE");
}
// 悬浮窗样式配置
params.alpha = 0.9f;
params.gravity = Gravity.RIGHT | Gravity.BOTTOM;
params.x = 20;
params.y = 20;
params.format = PixelFormat.RGBA_8888;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 核心Flag无焦点+不阻塞触摸事件穿透
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
// 加载悬浮窗布局
mView = View.inflate(mContext, R.layout.view_tts_back, null);
LinearLayout llMain = mView.findViewById(R.id.viewttsbackLinearLayout1);
llMain.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
LogUtils.d(TAG, "【onClick】悬浮窗被点击执行停止播放+关闭悬浮窗");
isExist = true;
if (mTextToSpeech != null) {
mTextToSpeech.stop();
LogUtils.d(TAG, "【onClick】已调用TTS.stop()停止播放");
}
clearFloatWindow();
}
});
// ========== 修复3添加异常捕获+重复添加判断防止addView时报错导致整个TTS播放崩溃 ==========
try {
if (mWindowManager != null && mView != null && mView.getWindowToken() == null) {
mWindowManager.addView(mView, params);
LogUtils.d(TAG, "【initWindow】悬浮窗添加到Window成功布局加载完成");
}
} catch (Exception e) {
LogUtils.d(TAG, "【initWindow】悬浮窗添加异常(权限/系统限制),不影响语音播放:", e);
mView = null;
}
}
// ====================================== 私有工具方法 - 清理悬浮窗 (通用封装) 【优化修复】 ======================================
private void clearFloatWindow() {
LogUtils.d(TAG, "【clearFloatWindow】执行悬浮窗清理操作");
if (mWindowManager != null && mView != null) {
try {
mWindowManager.removeView(mView);
LogUtils.d(TAG, "【clearFloatWindow】悬浮窗移除成功");
} catch (Exception e) {
LogUtils.d(TAG, "【clearFloatWindow】悬浮窗移除异常", e);
} finally {
mView = null;
}
} else {
LogUtils.d(TAG, "【clearFloatWindow】无需清理WindowManager或View为空");
}
}
// ====================================== ✅ 新增悬浮窗权限检查【必须】Android6.0+ 强制校验 防止崩溃 ✅ ======================================
private boolean checkOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean hasPermission = android.provider.Settings.canDrawOverlays(mContext);
LogUtils.d(TAG, "【checkOverlayPermission】Android6.0+ 悬浮窗权限校验结果:" + hasPermission);
return hasPermission;
} else {
// 低版本默认有权限
return true;
}
}
// ====================================== ✅ 新增释放资源方法【根治内存泄漏】建议在Service/Activity销毁时调用 ✅ ======================================
public void release() {
LogUtils.d(TAG, "【release】释放TTS资源和悬浮窗");
clearFloatWindow();
if (mTextToSpeech != null) {
mTextToSpeech.stop();
mTextToSpeech.shutdown();
mTextToSpeech = null;
}
sTextToSpeechUtils = null;
}
}

View File

@@ -3,291 +3,341 @@ package cc.winboll.studio.powerbell.views;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import java.io.File; import java.io.File;
/** /**
* 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺 * 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪 * 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪、无压缩
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/ */
public class BackgroundView extends RelativeLayout { public class BackgroundView extends RelativeLayout {
// ====================================== 静态常量区(首屏可见,统一管理) ======================================
public static final String TAG = "BackgroundView"; public static final String TAG = "BackgroundView";
// 新增:记录当前已缓存的图片路径 // Bitmap 配置常量(原始品质)
private String mCurrentCachedPath = ""; private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int BITMAP_SAMPLE_SIZE = 1; // 不缩放采样率
// ====================================== 成员变量区(按功能分类:上下文→视图→缓存→图片属性) ======================================
// 上下文
private Context mContext; private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout // 视图组件
private ImageView mIvBackground; // 图片显示控件 private LinearLayout mLlContainer; // 主容器LinearLayout
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高) private ImageView mIvBackground; // 图片显示控件
// 缓存相关
private String mCurrentCachedPath = "";// 当前缓存图片路径
// 图片属性
private float mImageAspectRatio = 1.0f;// 原图宽高比(宽/高)
private int mBgColor = 0xFFFFFFFF; // 当前图片背景色
// ====================================== 构造器Java7兼容 ====================================== // ====================================== 构造器Java7兼容,按参数重载顺序排列 ======================================
public BackgroundView(Context context) { public BackgroundView(Context context) {
super(context); super(context);
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ==="); LogUtils.d(TAG, String.format("构造器1启动 | context=%s", context.getClass().getSimpleName()));
this.mContext = context; this.mContext = context;
initView(); initView();
} }
public BackgroundView(Context context, AttributeSet attrs) { public BackgroundView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ==="); LogUtils.d(TAG, String.format("构造器2启动 | context=%s", context.getClass().getSimpleName()));
this.mContext = context; this.mContext = context;
initView(); initView();
} }
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ==="); LogUtils.d(TAG, String.format("构造器3启动 | context=%s", context.getClass().getSimpleName()));
this.mContext = context; this.mContext = context;
initView(); initView();
} }
// ====================================== 初始化 ====================================== // ====================================== 初始化方法(按执行顺序:主视图→子容器→图片控件→默认背景) ======================================
private void initView() { private void initView() {
LogUtils.d(TAG, "=== initView 启动 ==="); LogUtils.d(TAG, "initView启动");
// 1. 配置当前控件:全屏+透明 // 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
// 2. 初始化主容器LinearLayout // 2. 初始化主容器LinearLayout
initLinearLayout(); initLinearLayout();
// 3. 初始化ImageView // 3. 初始化ImageView
initImageView(); initImageView();
// 4. 初始设置透明背景
// 初始设置透明背景 setDefaultEmptyBackground();
setDefaultTransparentBackground(); LogUtils.d(TAG, "【initView】完成");
LogUtils.d(TAG, "=== initView 完成 ===");
} }
private void initLinearLayout() { private void initLinearLayout() {
LogUtils.d(TAG, "=== initLinearLayout 启动 ==="); LogUtils.d(TAG, "initLinearLayout启动");
mLlContainer = new LinearLayout(mContext); mLlContainer = new LinearLayout(mContext);
// 配置LinearLayout全屏+垂直方向+居中
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT LinearLayout.LayoutParams.MATCH_PARENT
); );
mLlContainer.setLayoutParams(llParams); mLlContainer.setLayoutParams(llParams);
mLlContainer.setOrientation(LinearLayout.VERTICAL); mLlContainer.setOrientation(LinearLayout.VERTICAL);
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中 mLlContainer.setGravity(android.view.Gravity.CENTER);
mLlContainer.setBackgroundColor(0x00000000);
this.addView(mLlContainer); this.addView(mLlContainer);
LogUtils.d(TAG, "=== initLinearLayout 完成 ==="); LogUtils.d(TAG, "initLinearLayout完成");
} }
private void initImageView() { private void initImageView() {
LogUtils.d(TAG, "=== initImageView 启动 ==="); LogUtils.d(TAG, "initImageView启动");
mIvBackground = new ImageView(mContext); mIvBackground = new ImageView(mContext);
// 配置ImageViewwrap_content+居中+透明背景
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
); );
mIvBackground.setLayoutParams(ivParams); mIvBackground.setLayoutParams(ivParams);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺 mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIvBackground.setBackgroundColor(0x00000000);
mLlContainer.addView(mIvBackground); mLlContainer.addView(mIvBackground);
LogUtils.d(TAG, "=== initImageView 完成 ==="); LogUtils.d(TAG, "initImageView完成");
} }
public void loadBackgroundBean(BackgroundBean bean) { // ====================================== 对外公开方法按功能分类Bean加载→图片加载 ======================================
loadBackgroundBean(bean, false); public void loadByBackgroundBean(BackgroundBean bean) {
loadByBackgroundBean(bean, false);
} }
public void loadBackgroundBean(BackgroundBean bean, boolean isRefresh) { public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
if (!bean.isUseBackgroundFile()) { LogUtils.d(TAG, String.format("【loadByBackgroundBean】启动 | isRefresh=%b | bean=%s", isRefresh, bean));
setDefaultTransparentBackground(); // 参数校验
if (bean == null) {
LogUtils.e(TAG, "【loadByBackgroundBean】异常BackgroundBean为空");
setDefaultEmptyBackground();
return; return;
} }
// 设置图片背景色
mBgColor = bean.getPixelColor();
LogUtils.d(TAG, String.format("【loadByBackgroundBean】背景色设置为 0x%08X", mBgColor));
// 判断是否使用背景文件
if (!bean.isUseBackgroundFile()) {
LogUtils.d(TAG, "【loadByBackgroundBean】不使用背景文件设置透明背景");
setDefaultEmptyBackground();
return;
}
// 获取目标路径
String targetPath = bean.isUseBackgroundScaledCompressFile() String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath() ? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath(); : bean.getBackgroundFilePath();
LogUtils.d(TAG, String.format("【loadByBackgroundBean】目标路径=%s | 使用压缩文件=%b",
targetPath, bean.isUseBackgroundScaledCompressFile()));
if (!(new File(targetPath).exists())) { // 校验文件是否存在
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath)); File targetFile = new File(targetPath);
return; if (!targetFile.exists() || !targetFile.isFile()) {
} LogUtils.e(TAG, String.format("【loadByBackgroundBean】异常图片文件不存在 | path=%s", targetPath));
setDefaultEmptyBackground();
return;
}
// 调用带路径判断的loadImage方法 loadImage(mBgColor, targetPath, isRefresh);
if (isRefresh) {
App.sBitmapCacheUtils.removeCachedBitmap(targetPath);
App.sBitmapCacheUtils.cacheBitmap(targetPath);
}
loadImage(targetPath);
} }
// ====================================== 对外方法 ====================================== public void loadImage(int bgColor, String imagePath, boolean isRefresh) {
/** LogUtils.d(TAG, String.format("【loadImage】启动 | bgColor=0x%08X | imagePath=%s | isRefresh=%b",
* 改造后添加路径判断路径更新时同步更新缓存缓存Bitmap为null时提示并加载透明背景 bgColor, imagePath, isRefresh));
* @param imagePath 图片绝对路径 // 隐藏ImageView防止闪烁
*/ mIvBackground.setVisibility(View.GONE);
public void loadImage(String imagePath) {
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
if (TextUtils.isEmpty(imagePath)) {
setDefaultTransparentBackground();
return;
}
File imageFile = new File(imagePath); // 刷新逻辑:重新解码原始品质图片并更新缓存
if (!imageFile.exists() || !imageFile.isFile()) { if (isRefresh) {
LogUtils.e(TAG, "图片文件无效"); LogUtils.d(TAG, "【loadImage】执行刷新逻辑重新解码原始品质图片");
setDefaultTransparentBackground(); File imageFile = new File(imagePath);
return; Bitmap newBitmap = decodeOriginalBitmap(imageFile);
} LogUtils.d(TAG, String.format("【loadImage】原始图片解码完成 | newBitmap=%s",
newBitmap != null ? newBitmap.getWidth() + "x" + newBitmap.getHeight() : "null"));
//mIvBackground.setVisibility(View.GONE); // 合成纯色背景图片(使用配置文件中默认相框尺寸)
Bitmap combinedBitmap = ImageUtils.drawBitmapOnSolidBackground(
bgColor,
App.sAppConfigUtils.mAppConfigBean.getDefaultFrameWidth(),
App.sAppConfigUtils.mAppConfigBean.getDefaultFrameHeight(),
newBitmap
);
// ======================== 新增:路径判断逻辑 ======================== if (combinedBitmap == null) {
// 1. 路径未变化:直接使用缓存 LogUtils.e(TAG, "【loadImage】纯色背景合成失败使用原始Bitmap");
if (imagePath.equals(mCurrentCachedPath)) { combinedBitmap = newBitmap;
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath); } else {
// 核心修改判断缓存Bitmap是否为null LogUtils.d(TAG, String.format("【loadImage】纯色背景合成成功 | combinedBitmap=%dx%d",
if (cachedBitmap != null && !cachedBitmap.isRecycled()) { combinedBitmap.getWidth(), combinedBitmap.getHeight()));
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap"); // 回收原始Bitmap避免重复缓存
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight(); if (newBitmap != null && !newBitmap.isRecycled() && newBitmap != combinedBitmap) {
mIvBackground.setImageBitmap(cachedBitmap); newBitmap.recycle();
adjustImageViewSize(); LogUtils.d(TAG, "【loadImage】原始Bitmap已回收");
return; }
} else { }
// 缓存Bitmap为空或已回收提示并加载透明背景
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath); // 更新缓存
ToastUtils.show("全局位图缓存为空,无法加载图片"); if (combinedBitmap != null) {
setDefaultTransparentBackground(); App.sBitmapCacheUtils.cacheBitmap(imagePath, combinedBitmap);
return; App.sBitmapCacheUtils.increaseRefCount(imagePath);
mCurrentCachedPath = imagePath;
LogUtils.d(TAG, String.format("【loadImage】刷新缓存成功 | path=%s", imagePath));
} else {
LogUtils.e(TAG, String.format("【loadImage】刷新解码失败 | path=%s", imagePath));
} }
} }
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存 // 加载缓存图片
if (!TextUtils.isEmpty(mCurrentCachedPath)) { Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath); LogUtils.d(TAG, String.format("【loadImage】加载缓存图片 | cachedBitmap=%s",
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath); cachedBitmap != null ? cachedBitmap.getWidth() + "x" + cachedBitmap.getHeight() : "null"));
} mIvBackground.setImageBitmap(cachedBitmap);
// ======================== 路径判断逻辑结束 ======================== mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIvBackground.setVisibility(View.VISIBLE);
// 无缓存/路径更新:走原有逻辑加载图片 LogUtils.d(TAG, "【loadImage】完成");
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
}
// 缓存新图片,并更新当前缓存路径记录
App.sBitmapCacheUtils.cacheBitmap(imagePath);
mCurrentCachedPath = imagePath;
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
adjustImageViewSize();
LogUtils.d(TAG, "=== loadImage 完成 ===");
} }
// ====================================== 内部工具方法 ====================================== // ====================================== 内部工具方法按功能分类Bitmap校验→比例计算→解码→背景设置 ======================================
/**
* 工具方法判断Bitmap是否有效非空且未被回收
*/
private boolean isBitmapValid(Bitmap bitmap) {
boolean valid = bitmap != null && !bitmap.isRecycled();
if (!valid) {
LogUtils.w(TAG, "【isBitmapValid】无效Bitmap为空或已回收");
}
return valid;
}
/**
* 计算图片宽高比
*/
private boolean calculateImageAspectRatio(File file) { private boolean calculateImageAspectRatio(File file) {
LogUtils.d(TAG, String.format("【calculateImageAspectRatio】启动 | file=%s", file.getAbsolutePath()));
try { try {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options); BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 尺寸校验
int width = options.outWidth; int width = options.outWidth;
int height = options.outHeight; int height = options.outHeight;
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
LogUtils.e(TAG, "图片尺寸无效"); LogUtils.e(TAG, String.format("【calculateImageAspectRatio】无效尺寸 | width=%d | height=%d", width, height));
return false; return false;
} }
// 计算比例
mImageAspectRatio = (float) width / height; mImageAspectRatio = (float) width / height;
LogUtils.d(TAG, "原图比例:" + mImageAspectRatio); LogUtils.d(TAG, String.format("【calculateImageAspectRatio】完成 | 比例=%.2f", mImageAspectRatio));
return true; return true;
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "计算比例失败:" + e.getMessage()); LogUtils.e(TAG, String.format("【calculateImageAspectRatio】失败:%s", e.getMessage()));
return false; return false;
} }
} }
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) { /**
* 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失)
*/
private Bitmap decodeOriginalBitmap(File file) {
LogUtils.d(TAG, String.format("【decodeOriginalBitmap】启动 | file=%s", file.getAbsolutePath()));
try { try {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 核心配置:原始品质
BitmapFactory.decodeFile(file.getAbsolutePath(), options); options.inSampleSize = BITMAP_SAMPLE_SIZE;
options.inPreferredConfig = BITMAP_CONFIG;
options.inPurgeable = false;
options.inInputShareable = false;
options.inDither = true;
options.inScaled = false;
int scaleX = options.outWidth / maxWidth; // 解码图片
int scaleY = options.outHeight / maxHeight; Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int inSampleSize = Math.max(scaleX, scaleY); if (bitmap != null) {
if (inSampleSize <= 0) inSampleSize = 1; LogUtils.d(TAG, String.format("【decodeOriginalBitmap】成功 | width=%d | height=%d", bitmap.getWidth(), bitmap.getHeight()));
} else {
options.inJustDecodeBounds = false; LogUtils.e(TAG, "【decodeOriginalBitmap】失败返回null");
options.inSampleSize = inSampleSize; }
options.inPreferredConfig = Bitmap.Config.RGB_565; return bitmap;
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage()); LogUtils.e(TAG, String.format("【decodeOriginalBitmap】异常%s", e.getMessage()));
return null; return null;
} }
} }
private void adjustImageViewSize() { /**
if (mLlContainer == null || mIvBackground == null) { * 设置默认透明背景,仅减少引用计数,不删除缓存
return; */
} private void setDefaultEmptyBackground() {
LogUtils.d(TAG, "【setDefaultEmptyBackground】启动");
int llWidth = mLlContainer.getWidth(); // 清空ImageView
int llHeight = mLlContainer.getHeight(); mIvBackground.setImageDrawable(null);
if (llWidth != 0 && llHeight != 0) {
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
ivHeight = (int) (ivWidth / mImageAspectRatio);
} else {
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
//mIvBackground.setVisibility(View.VISIBLE);
}
}
private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f; mImageAspectRatio = 1.0f;
// 清空缓存路径记录
mCurrentCachedPath = ""; // 减少引用计数,不删除缓存
//mIvBackground.setVisibility(View.GONE); if (!TextUtils.isEmpty(mCurrentCachedPath)) {
LogUtils.d(TAG, String.format("【setDefaultEmptyBackground】减少引用计数 | path=%s", mCurrentCachedPath));
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
LogUtils.d(TAG, "【setDefaultEmptyBackground】完成");
} }
// ====================================== 重写方法 ====================================== // ====================================== 重写生命周期方法(按执行顺序:绘制→尺寸变化→窗口分离) ======================================
/**
* 重写绘制前强制校验Bitmap有效性防止已回收Bitmap崩溃
*/
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = mIvBackground.getDrawable();
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
Bitmap bitmap = bitmapDrawable.getBitmap();
if (!isBitmapValid(bitmap)) {
LogUtils.e(TAG, "【onDraw】检测到已回收Bitmap清空绘制");
mIvBackground.setImageDrawable(null);
return;
}
}
super.onDraw(canvas);
}
/**
* 重写恢复尺寸调整逻辑确保View尺寸变化时正确显示无压缩
*/
@Override @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); super.onSizeChanged(w, h, oldw, oldh);
//adjustImageViewSize(); // 尺寸变化时重新调整 LogUtils.d(TAG, String.format("【onSizeChanged】尺寸变化 | newW=%d | newH=%d | oldW=%d | oldH=%d", w, h, oldw, oldh));
}
/**
* 重写View从窗口移除时仅减少引用计数不删除全局缓存强制保持策略
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
LogUtils.d(TAG, "【onDetachedFromWindow】启动");
// 清空ImageView的Drawable释放本地引用
mIvBackground.setImageDrawable(null);
// 减少引用计数,不删除全局缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
LogUtils.d(TAG, String.format("【onDetachedFromWindow】减少引用计数 | path=%s", mCurrentCachedPath));
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
LogUtils.d(TAG, "【onDetachedFromWindow】完成");
} }
} }

View File

@@ -7,34 +7,40 @@ import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BatteryStyle;
/** /**
* 电池电量Drawable适配API30兼容小米机型支持能量/条纹两种绘制风格切换
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 12:55 * @Date 2025/12/17 12:55
* @Describe 电池电量Drawable适配API30兼容小米机型支持能量/条纹两种绘制风格切换
*/ */
public class BatteryDrawable extends Drawable { public class BatteryDrawable extends Drawable {
// ====================== 静态常量(置顶,按重要性排序) ====================== // ====================================== 静态常量区(按功能归类,消除魔法值) ======================================
public static final String TAG = "BatteryDrawable"; public static final String TAG = "BatteryDrawable";
// 小米机型绘制偏移校准适配MIUI渲染特性避免绘制错位 // 小米机型绘制偏移校准适配MIUI渲染特性避免绘制错位
private static final int MIUI_DRAW_OFFSET = 1; private static final int MIUI_DRAW_OFFSET = 1;
// 默认电量透明度兼顾显示效果与API30渲染性能 // 默认电量透明度兼顾显示效果与API30渲染性能
private static final int DEFAULT_BATTERY_ALPHA = 210; private static final int DEFAULT_BATTERY_ALPHA = 210;
// 电量范围常量
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// 条纹风格拆分数量
private static final int STRIPE_COUNT = 100;
// ====================== 核心成员变量按功能归类final优先 ====================== // ====================================== 成员变量区final优先按功能归类 ======================================
// 绘制画笔final修饰避免重复创建提升性能 // 绘制画笔final修饰避免重复创建提升性能
private final Paint mBatteryPaint; private final Paint mBatteryPaint;
// 业务控制变量 // 业务控制变量
private int mBatteryValue = -1; // 当前电量0-100-1=未初始化) private int mBatteryValue = -1; // 当前电量0-100-1=未初始化)
private boolean mIsEnergyStyle = true; // 绘制风格true=能量false=条纹) private BatteryStyle mBatteryStyle = BatteryStyle.ENERGY_STYLE; // 绘制风格true=能量false=条纹)
// ====================== 构造方法(重载适配,优先暴露常用构造) ====================== // ====================================== 构造方法(重载适配,优先暴露常用构造) ======================================
/** /**
* 构造方法(默认能量风格,常用场景) * 构造方法(默认能量风格,常用场景)
* @param batteryColor 电量显示颜色 * @param batteryColor 电量显示颜色
*/ */
public BatteryDrawable(int batteryColor) { public BatteryDrawable(int batteryColor) {
LogUtils.d(TAG, "constructor: 初始化(能量风格颜色=" + Integer.toHexString(batteryColor)); LogUtils.d(TAG, "【BatteryDrawable】构造器1调用 | 能量风格 | 颜色=" + Integer.toHexString(batteryColor));
mBatteryPaint = new Paint(); mBatteryPaint = new Paint();
initPaintConfig(batteryColor); initPaintConfig(batteryColor);
} }
@@ -44,96 +50,200 @@ public class BatteryDrawable extends Drawable {
* @param batteryColor 电量显示颜色 * @param batteryColor 电量显示颜色
* @param isEnergyStyle 是否启用能量风格 * @param isEnergyStyle 是否启用能量风格
*/ */
public BatteryDrawable(int batteryColor, boolean isEnergyStyle) { public BatteryDrawable(int batteryColor, BatteryStyle batteryStyle) {
LogUtils.d(TAG, "constructor: 初始化,颜色=" + Integer.toHexString(batteryColor) + ",风格=" + (isEnergyStyle ? "能量" : "条纹"));
mBatteryPaint = new Paint(); mBatteryPaint = new Paint();
mIsEnergyStyle = isEnergyStyle; mBatteryStyle = batteryStyle;
initPaintConfig(batteryColor); initPaintConfig(batteryColor);
} }
// ====================== 私有初始化方法(封装复用,隐藏内部逻辑) ====================== public void setIsEnergyStyle(BatteryStyle batteryStyle) {
this.mBatteryStyle = batteryStyle;
}
// ====================================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================================
/** /**
* 初始化画笔配置适配API30渲染特性优化小米机型兼容性 * 初始化画笔配置适配API30渲染特性优化小米机型兼容性
* @param color 电量显示颜色
*/ */
private void initPaintConfig(int color) { private void initPaintConfig(int color) {
LogUtils.d(TAG, "【initPaintConfig】画笔配置开始 | 颜色=" + Integer.toHexString(color));
mBatteryPaint.setColor(color); mBatteryPaint.setColor(color);
mBatteryPaint.setAlpha(DEFAULT_BATTERY_ALPHA); mBatteryPaint.setAlpha(DEFAULT_BATTERY_ALPHA);
mBatteryPaint.setAntiAlias(true); // 抗锯齿,解决小米低分辨率锯齿问题 mBatteryPaint.setAntiAlias(true); // 抗锯齿,解决小米低分辨率锯齿问题
mBatteryPaint.setStyle(Paint.Style.FILL); // 固定填充模式,避免混乱 mBatteryPaint.setStyle(Paint.Style.FILL); // 固定填充模式,避免混乱
mBatteryPaint.setDither(false); // 禁用抖动提升API30颜色显示一致性 mBatteryPaint.setDither(false); // 禁用抖动提升API30颜色显示一致性
LogUtils.d(TAG, "initPaintConfig: 画笔配置完成"); LogUtils.d(TAG, "initPaintConfig画笔配置完成");
} }
// ====================== 核心绘制方法Drawable抽象方法优先级最高 ====================== // ====================================== 核心绘制方法Drawable抽象方法优先级最高 ======================================
@Override @Override
public void draw(Canvas canvas) { public void draw(Canvas canvas) {
// 未初始化/异常电量,直接跳过,避免无效绘制 // 未初始化/异常电量,直接跳过,避免无效绘制
if (mBatteryValue < 0) { if (mBatteryValue < 0) {
LogUtils.w(TAG, "draw: 电量未初始化,跳过绘制"); LogUtils.w(TAG, "draw电量未初始化,跳过绘制");
return; return;
} }
// 强制校准电量范围0-100防止异常值导致绘制错误 // 强制校准电量范围0-100防止异常值导致绘制错误
int validBattery = Math.max(0, Math.min(mBatteryValue, 100)); int validBattery = Math.max(BATTERY_MIN, Math.min(mBatteryValue, BATTERY_MAX));
LogUtils.d(TAG, "【draw】电量校准完成 | 有效电量=" + validBattery);
Rect drawBounds = getBounds(); Rect drawBounds = getBounds();
// 绘制边界空指针防护
if (drawBounds == null) {
LogUtils.e(TAG, "【draw】绘制边界为空跳过绘制");
return;
}
int drawHeight = drawBounds.height(); int drawHeight = drawBounds.height();
// 小米机型绘制偏移校准解决MIUI系统渲染偏移问题 // 小米机型绘制偏移校准解决MIUI系统渲染偏移问题
int offset = MIUI_DRAW_OFFSET; int offset = MIUI_DRAW_OFFSET;
int left = drawBounds.left + offset; int left = drawBounds.left + offset;
int right = drawBounds.right - offset; int right = drawBounds.right - offset;
LogUtils.d(TAG, "【draw】绘制参数校准 | 左边界=" + left + " | 右边界=" + right + " | 高度=" + drawHeight);
// 按风格执行绘制(精简日志,仅保留核心绘制参数) // 按风格执行绘制
LogUtils.d(TAG, "draw: 开始绘制,电量=" + validBattery + ",风格=" + (mIsEnergyStyle ? "能量" : "条纹")); if (mBatteryStyle == BatteryStyle.ENERGY_STYLE) {
if (mIsEnergyStyle) {
drawEnergyStyle(canvas, validBattery, left, right, drawHeight); drawEnergyStyle(canvas, validBattery, left, right, drawHeight);
} else { } else if (mBatteryStyle == BatteryStyle.ZEBRA_STYLE) {
drawStripeStyle(canvas, validBattery, left, right, drawHeight); drawZebraStyle(canvas, validBattery, left, right, drawHeight);
} else if (mBatteryStyle == BatteryStyle.POINT_STYLE) {
drawPointStyle(canvas, validBattery, left, right, drawHeight);
} }
LogUtils.d(TAG, "【draw】绘制完成");
} }
// ====================== 绘制风格实现(私有封装,按风格拆分) ====================== // ====================================== 绘制风格实现(私有封装,按风格拆分) ======================================
/** /**
* 能量风格绘制(整块填充,高效简洁,默认风格) * 能量风格绘制(整块填充,高效简洁,默认风格)
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/ */
private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) { private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) {
int top = height - (height * battery / 100); // 计算电量对应顶部坐标 LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制开始 | 电量=" + battery);
canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint); // int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
LogUtils.d(TAG, "drawEnergyStyle: 绘制完成,顶部坐标=" + top); // canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
// LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 绘制耗电电量提醒值电量
// 能量绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
//for (int i = 0; i < mnValue; i ++) {
nBottom = nHeight;
nTop = nHeight - (nHeight * mBatteryValue / 100);
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
} }
/** /**
* 条纹风格绘制(分段条纹,扩展风格) * 条纹风格绘制(分段条纹,扩展风格)
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/ */
private void drawStripeStyle(Canvas canvas, int battery, int left, int right, int height) { private void drawZebraStyle(Canvas canvas, int battery, int left, int right, int height) {
int stripeHeight = height / 100; // 单条条纹高度(均匀拆分) LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
// 从底部向上绘制对应电量条纹 // int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分)
for (int i = 0; i < battery; i++) { // // 从底部向上绘制对应电量条纹
int bottom = height - (stripeHeight * i); // for (int i = 0; i < battery; i++) {
int top = bottom - stripeHeight; // int bottom = height - (stripeHeight * i);
canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint); // int top = bottom - stripeHeight;
} // canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
LogUtils.d(TAG, "drawStripeStyle: 绘制完成,条纹数量=" + battery); // }
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 意兴阑珊绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
for (int i = 0; i < mBatteryValue; i ++) {
nBottom = (nHeight * (100 - i) / 100) - mnDx;
nTop = nBottom + mnDx;
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
}
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
} }
// ====================== 对外暴露方法(业务控制入口,按功能排序) ======================
/**
* 点阵风格绘制
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/
private void drawPointStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 意兴阑珊绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
int nLineWidth = nRight - nLeft;
int radius_horizontal = (nLineWidth / 10) / 2;
int radius_vertical = mnDx/2;
int radius = Math.min(radius_horizontal, radius_vertical);
for (int i = 0; i < mBatteryValue; i ++) {
nBottom = (nHeight * (100 - i) / 100) - mnDx;
nTop = nBottom + mnDx;
//canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
for (int j = 0; j < 10; j++) {
// cx, cy 圆心坐标radius 半径paint 画笔
int cx = radius_horizontal + radius_horizontal * j * 2;
int cy = nTop + radius_vertical;
canvas.drawCircle(cx, cy, radius, mBatteryPaint);
}
}
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
}
// ====================================== 对外暴露方法(业务控制入口,按功能排序) ======================================
/** /**
* 设置当前电量(外部核心调用入口) * 设置当前电量(外部核心调用入口)
* @param value 电量值0-100 * @param value 电量值0-100
*/ */
public void setBatteryValue(int value) { public void setBatteryValue(int value) {
LogUtils.d(TAG, "setBatteryValue: 电量更新旧值=" + mBatteryValue + "新值=" + value); LogUtils.d(TAG, "setBatteryValue电量更新 | 旧值=" + mBatteryValue + " | 新值=" + value);
mBatteryValue = value; mBatteryValue = value;
invalidateSelf(); // 触发重绘确保UI实时更新 invalidateSelf(); // 触发重绘确保UI实时更新
LogUtils.d(TAG, "【setBatteryValue】已触发重绘");
} }
/** /**
* 切换绘制风格 * 切换绘制风格
* @param isEnergyStyle true=能量风格false=条纹风格 * @param isEnergyStyle true=能量风格false=条纹风格
*/ */
public void switchDrawStyle(boolean isEnergyStyle) { public void setDrawStyle(BatteryStyle batteryStyle) {
LogUtils.d(TAG, "switchDrawStyle: 风格切换,旧=" + (mIsEnergyStyle ? "能量" : "条纹") + ",新=" + (isEnergyStyle ? "能量" : "条纹")); mBatteryStyle = batteryStyle;
mIsEnergyStyle = isEnergyStyle;
invalidateSelf(); invalidateSelf();
LogUtils.d(TAG, "【switchDrawStyle】已触发重绘");
} }
/** /**
@@ -141,31 +251,40 @@ public class BatteryDrawable extends Drawable {
* @param color 新颜色值 * @param color 新颜色值
*/ */
public void updateBatteryColor(int color) { public void updateBatteryColor(int color) {
LogUtils.d(TAG, "updateBatteryColor: 颜色更新,旧=" + Integer.toHexString(mBatteryPaint.getColor()) + ",新=" + Integer.toHexString(color)); String oldColor = Integer.toHexString(mBatteryPaint.getColor());
String newColor = Integer.toHexString(color);
LogUtils.d(TAG, "【updateBatteryColor】颜色更新 | 旧颜色=" + oldColor + " | 新颜色=" + newColor);
mBatteryPaint.setColor(color); mBatteryPaint.setColor(color);
invalidateSelf(); invalidateSelf();
LogUtils.d(TAG, "【updateBatteryColor】已触发重绘");
} }
// ====================== Getter方法按需暴露简洁无冗余 ====================== // ====================================== Getter方法按需暴露简洁无冗余 ======================================
/**
* 获取当前电量
* @return 电量值0-100-1=未初始化)
*/
public int getBatteryValue() { public int getBatteryValue() {
return mBatteryValue; return mBatteryValue;
} }
public boolean isEnergyStyle() {
return mIsEnergyStyle;
public BatteryStyle getEnergyStyle() {
return mBatteryStyle;
} }
// ====================== Drawable抽象方法必须实现精简逻辑 ====================== // ====================================== Drawable抽象方法必须实现精简逻辑 ======================================
@Override @Override
public void setAlpha(int alpha) { public void setAlpha(int alpha) {
LogUtils.d(TAG, "setAlpha: 透明度更新,旧=" + mBatteryPaint.getAlpha() + ",新=" + alpha); LogUtils.d(TAG, "setAlpha透明度更新 | 旧值=" + mBatteryPaint.getAlpha() + " | 新值=" + alpha);
mBatteryPaint.setAlpha(alpha); mBatteryPaint.setAlpha(alpha);
invalidateSelf(); invalidateSelf();
} }
@Override @Override
public void setColorFilter(ColorFilter colorFilter) { public void setColorFilter(ColorFilter colorFilter) {
LogUtils.d(TAG, "setColorFilter: 设置颜色过滤filter=" + colorFilter); LogUtils.d(TAG, "setColorFilter设置颜色过滤 | filter=" + colorFilter);
mBatteryPaint.setColorFilter(colorFilter); mBatteryPaint.setColorFilter(colorFilter);
invalidateSelf(); invalidateSelf();
} }

View File

@@ -0,0 +1,285 @@
package cc.winboll.studio.powerbell.views;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BatteryStyle;
/**
* 电池样式单选视图水平展示所有BatteryStyle枚举选项
* 每个选项 = RadioButton单选按钮 + BatteryDrawable预览控件
* 适配API30、Java7规范联动BatteryDrawable绘制样式
* 包含SP持久化存储 + 公共静态方法读取SP枚举值 + 彻底修复点击不回调+单选失效
* 默认选中BatteryStyle.ENERGY_STYLE
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class BatteryStyleView extends LinearLayout implements RadioGroup.OnCheckedChangeListener {
// ====================== 常量区 ======================
public static final String TAG = "BatteryStyleView";
private static final int DEFAULT_BATTERY_COLOR = Color.parseColor("#FF4CAF50");
private static final int DEFAULT_CHECKED_STYLE_INDEX = 1; // ✅ 修改默认选中下标 1 = ENERGY_STYLE
public static final String SP_NAME = "sp_battery_style_config";
public static final String SP_KEY_BATTERY_STYLE = "key_selected_battery_style";
// ====================== 控件变量 ======================
private RadioGroup rgBatteryStyle;
private RadioButton rbZebraStyle;
private RadioButton rbEnergyStyle;
private RadioButton rbPointStyle; // ✅ 新增:圆点样式单选按钮
private RelativeLayout rlZebraPreview;
private RelativeLayout rlEnergyPreview;
private RelativeLayout rlPointPreview; // ✅ 新增:圆点样式预览布局
private BatteryDrawable mZebraDrawable;
private BatteryDrawable mEnergyDrawable;
private BatteryDrawable mPointDrawable; // ✅ 新增圆点样式Drawable实例
// ====================== 业务变量 ======================
private BatteryStyle mCurrentStyle = BatteryStyle.ENERGY_STYLE; // ✅ 修改默认样式为 能量样式
private OnBatteryStyleSelectedListener mStyleSelectedListener;
private int mBatteryColor = DEFAULT_BATTERY_COLOR;
private int mBatteryValue = 100;
private SharedPreferences mSp;
// ====================== 构造方法 ======================
public BatteryStyleView(Context context) {
super(context);
initSP(context);
initView(context, null);
}
public BatteryStyleView(Context context, AttributeSet attrs) {
super(context, attrs);
initSP(context);
initAttrs(context, attrs);
initView(context, attrs);
}
public BatteryStyleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initSP(context);
initAttrs(context, attrs);
initView(context, attrs);
}
// ====================== 初始化SP持久化 ======================
private void initSP(Context context) {
mSp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
LogUtils.d(TAG, "【initSP】SharedPreferences初始化完成文件名称 = " + SP_NAME);
}
// ====================== 初始化方法 ======================
private void initAttrs(Context context, AttributeSet attrs) {
if (attrs == null) return;
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BatteryStyleView);
mBatteryColor = typedArray.getColor(R.styleable.BatteryStyleView_batteryPreviewColor, DEFAULT_BATTERY_COLOR);
mBatteryValue = typedArray.getInt(R.styleable.BatteryStyleView_previewBatteryValue, 100);
int styleIndex = typedArray.getInt(R.styleable.BatteryStyleView_defaultSelectedStyle, DEFAULT_CHECKED_STYLE_INDEX);
mCurrentStyle = getStyleFromSP() == null ? (styleIndex == 0 ? BatteryStyle.ENERGY_STYLE : styleIndex ==1 ? BatteryStyle.ZEBRA_STYLE : BatteryStyle.POINT_STYLE) : getStyleFromSP();
typedArray.recycle();
LogUtils.d(TAG, "【initAttrs】解析属性完成 电量颜色=" + Integer.toHexString(mBatteryColor) + " 预览电量=" + mBatteryValue + " 默认样式=" + mCurrentStyle.name());
}
private void initView(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.view_battery_style, this, true);
rgBatteryStyle = findViewById(R.id.rg_battery_style);
rbZebraStyle = findViewById(R.id.rb_zebra_style);
rbEnergyStyle = findViewById(R.id.rb_energy_style);
rbPointStyle = findViewById(R.id.rb_point_style); // ✅ 新增:绑定圆点样式单选按钮
rlZebraPreview = findViewById(R.id.rl_zebra_preview);
rlEnergyPreview = findViewById(R.id.rl_energy_preview);
rlPointPreview = findViewById(R.id.rl_point_preview); // ✅ 新增:绑定圆点样式预览布局
initPreviewDrawable();
rgBatteryStyle.setOnCheckedChangeListener(this);
addRadioBtnClickLister();
setDefaultChecked();
LogUtils.d(TAG, "【initView】视图初始化完成");
}
private void initPreviewDrawable() {
mZebraDrawable = new BatteryDrawable(mBatteryColor, BatteryStyle.ZEBRA_STYLE);
mZebraDrawable.setBatteryValue(mBatteryValue);
rlZebraPreview.setBackground(mZebraDrawable);
mEnergyDrawable = new BatteryDrawable(mBatteryColor, BatteryStyle.ENERGY_STYLE);
mEnergyDrawable.setBatteryValue(mBatteryValue);
rlEnergyPreview.setBackground(mEnergyDrawable);
// ✅ 新增初始化圆点样式Drawable + 绑定预览布局 + 设置电量值
mPointDrawable = new BatteryDrawable(mBatteryColor, BatteryStyle.POINT_STYLE);
mPointDrawable.setBatteryValue(mBatteryValue);
rlPointPreview.setBackground(mPointDrawable);
LogUtils.d(TAG, "【initPreviewDrawable】Drawable预览初始化完成");
}
private void setDefaultChecked() {
// ✅ 新增:圆点样式的默认选中判断
if (mCurrentStyle == BatteryStyle.ZEBRA_STYLE) {
rbZebraStyle.setChecked(true);
} else if (mCurrentStyle == BatteryStyle.POINT_STYLE) {
rbPointStyle.setChecked(true);
} else {
rbEnergyStyle.setChecked(true);
}
LogUtils.d(TAG, "【setDefaultChecked】默认选中样式 = " + mCurrentStyle.name());
}
private void addRadioBtnClickLister() {
rbZebraStyle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rbZebraStyle.setChecked(true);
rbEnergyStyle.setChecked(false);
rbPointStyle.setChecked(false); // ✅ 新增:取消圆点样式选中
handleStyleSelect(BatteryStyle.ZEBRA_STYLE);
}
});
rbEnergyStyle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rbEnergyStyle.setChecked(true);
rbZebraStyle.setChecked(false);
rbPointStyle.setChecked(false); // ✅ 新增:取消圆点样式选中
handleStyleSelect(BatteryStyle.ENERGY_STYLE);
}
});
// ✅ 新增:圆点样式单选按钮点击事件
rbPointStyle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rbPointStyle.setChecked(true);
rbZebraStyle.setChecked(false);
rbEnergyStyle.setChecked(false);
handleStyleSelect(BatteryStyle.POINT_STYLE);
}
});
}
// ====================== RadioGroup 选中回调 (点击必触发) ======================
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
ToastUtils.show("onCheckedChanged");
if (checkedId == R.id.rb_zebra_style) {
handleStyleSelect(BatteryStyle.ZEBRA_STYLE);
} else if (checkedId == R.id.rb_energy_style) {
handleStyleSelect(BatteryStyle.ENERGY_STYLE);
} else if (checkedId == R.id.rb_point_style) { // ✅ 新增:圆点样式选中回调
handleStyleSelect(BatteryStyle.POINT_STYLE);
}
}
private void handleStyleSelect(BatteryStyle style) {
mCurrentStyle = style;
saveStyle2SP(mCurrentStyle);
MainActivity.sendUpdateBatteryDrawableMessage();
LogUtils.d(TAG, "【handleStyleSelect】选中样式 → " + mCurrentStyle.name() + "已存入SP");
if (mStyleSelectedListener != null) {
mStyleSelectedListener.onStyleSelected(mCurrentStyle);
}
}
// ====================== SP持久化 存储+读取 封装方法 ======================
private void saveStyle2SP(BatteryStyle style) {
mSp.edit().putString(SP_KEY_BATTERY_STYLE, style.name()).commit();
}
private BatteryStyle getStyleFromSP() {
String styleStr = mSp.getString(SP_KEY_BATTERY_STYLE, null);
if (styleStr == null) return null;
try {
return BatteryStyle.valueOf(styleStr);
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "【getStyleFromSP】SP读取样式异常 = " + e.getMessage());
return null;
}
}
// ====================== 公共静态方法 读取SP存储的枚举值 ======================
public static BatteryStyle getSavedBatteryStyle(Context context) {
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String styleStr = sp.getString(SP_KEY_BATTERY_STYLE, null);
if (styleStr == null) {
LogUtils.w(TAG, "【getSavedBatteryStyle】SP无存储值返回默认样式 ENERGY_STYLE");
return BatteryStyle.ENERGY_STYLE; // ✅ 静态方法默认值同步修改为能量样式
}
try {
BatteryStyle style = BatteryStyle.valueOf(styleStr);
LogUtils.d(TAG, "【getSavedBatteryStyle】SP读取成功 → " + style.name());
return style;
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "【getSavedBatteryStyle】SP读取异常 = " + e.getMessage() + ",返回默认样式 ENERGY_STYLE");
return BatteryStyle.ENERGY_STYLE; // ✅ 异常兜底值同步修改为能量样式
}
}
public static BatteryStyle getSavedBatteryStyle(Context context, BatteryStyle defaultStyle) {
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String styleStr = sp.getString(SP_KEY_BATTERY_STYLE, null);
if (styleStr == null) {
LogUtils.w(TAG, "【getSavedBatteryStyle】SP无存储值返回自定义默认样式 → " + defaultStyle.name());
return defaultStyle;
}
try {
BatteryStyle style = BatteryStyle.valueOf(styleStr);
LogUtils.d(TAG, "【getSavedBatteryStyle】SP读取成功 → " + style.name());
return style;
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "【getSavedBatteryStyle】SP读取异常 = " + e.getMessage() + ",返回自定义默认样式 → " + defaultStyle.name());
return defaultStyle;
}
}
// ====================== 对外暴露方法 ======================
public void setSelectedStyle(BatteryStyle style) {
mCurrentStyle = style;
// ✅ 新增:圆点样式的手动选中赋值
rbZebraStyle.setChecked(style == BatteryStyle.ZEBRA_STYLE);
rbEnergyStyle.setChecked(style == BatteryStyle.ENERGY_STYLE);
rbPointStyle.setChecked(style == BatteryStyle.POINT_STYLE);
saveStyle2SP(style);
LogUtils.d(TAG, "【setSelectedStyle】手动设置选中样式 → " + style.name() + "已存入SP");
}
public BatteryStyle getCurrentStyle() {
return mCurrentStyle;
}
public void setPreviewBatteryValue(int batteryValue) {
this.mBatteryValue = batteryValue;
mZebraDrawable.setBatteryValue(batteryValue);
mEnergyDrawable.setBatteryValue(batteryValue);
mPointDrawable.setBatteryValue(batteryValue); // ✅ 新增:圆点样式同步电量值
}
public void setPreviewBatteryColor(int color) {
this.mBatteryColor = color;
mZebraDrawable.updateBatteryColor(color);
mEnergyDrawable.updateBatteryColor(color);
mPointDrawable.updateBatteryColor(color); // ✅ 新增:圆点样式同步颜色值
}
public void setOnBatteryStyleSelectedListener(OnBatteryStyleSelectedListener listener) {
this.mStyleSelectedListener = listener;
}
// ====================== 选中回调接口 ======================
public interface OnBatteryStyleSelectedListener {
void onStyleSelected(BatteryStyle batteryStyle);
}
}

View File

@@ -7,6 +7,7 @@ import android.content.DialogInterface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@@ -14,30 +15,37 @@ import android.widget.RelativeLayout;
import android.widget.Switch; import android.widget.Switch;
import android.widget.TextView; import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.models.BatteryStyle;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean; import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* @Date 2025/12/17 13:14
* @Describe 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* 适配Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框 * 适配Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值 * 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
* 修复updateBatteryDrawable() 电池样式切换后重绘失效问题
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 13:14
*/ */
public class MainContentView { public class MainContentView {
// ======================== 静态常量(置顶,唯一标识)======================== // ====================================== 静态常量区(唯一标识,变更类型分类) ======================================
public static final String TAG = "MainContentView"; public static final String TAG = "MainContentView";
// 变更类型常量(区分不同控件,精准处理逻辑) // 变更类型常量(区分不同控件,精准处理逻辑)
private static final int CHANGE_TYPE_CHARGE_SWITCH = 1; private static final int CHANGE_TYPE_CHARGE_SWITCH = 1;
private static final int CHANGE_TYPE_USAGE_SWITCH = 2; private static final int CHANGE_TYPE_USAGE_SWITCH = 2;
private static final int CHANGE_TYPE_SERVICE_SWITCH = 3; private static final int CHANGE_TYPE_SERVICE_SWITCH = 3;
private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4; private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4;
private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5; private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5;
// 电量范围常量
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// ======================== 内部静态类(临时数据载体,避免外部依赖)======================== // ====================================== 内部缓存类(解耦,避免冗余) ======================================
/** /**
* 临时配置数据实体(缓存变更信息,取消时恢复) * 临时配置数据实体(缓存变更信息,取消时恢复)
*/ */
@@ -63,7 +71,7 @@ public class MainContentView {
} }
} }
// ======================== 事件回调接口(解耦视图与业务,提升扩展性)======================== // ====================================== 事件回调接口(解耦视图与业务,提升扩展性) ======================================
public interface OnViewActionListener { public interface OnViewActionListener {
void onChargeReminderSwitchChanged(boolean isChecked); void onChargeReminderSwitchChanged(boolean isChecked);
void onUsageReminderSwitchChanged(boolean isChecked); void onUsageReminderSwitchChanged(boolean isChecked);
@@ -72,16 +80,19 @@ public class MainContentView {
void onUsageReminderProgressChanged(int progress); void onUsageReminderProgressChanged(int progress);
} }
// ======================== 成员变量(按功能分类,避免混乱)======================== // ====================================== 成员变量(按功能分类,final优先避免混乱 ======================================
// 外部依赖实例(生命周期关联,优先声明) // 外部依赖实例(生命周期关联,优先声明)
private Context mContext; private Context mContext;
private AppConfigUtils mAppConfigUtils; private AppConfigUtils mAppConfigUtils;
private OnViewActionListener mActionListener; private OnViewActionListener mActionListener;
// 视图控件(按「布局→开关→文本→进度条→图标」功能归类) // 视图控件(按「布局→开关→文本→进度条→图标」功能归类public控件标注用途
// 基础布局控件 // 基础布局控件
public RelativeLayout mainLayout; public RelativeLayout mainLayout;
public BackgroundView backgroundView; public MemoryCachedBackgroundView backgroundView;
private LinearLayout mllBackgroundView;
private volatile BatteryStyle mBatteryStyle = BatteryStyle.ENERGY_STYLE;
// 容器布局控件 // 容器布局控件
public LinearLayout llLeftSeekBar; public LinearLayout llLeftSeekBar;
public LinearLayout llRightSeekBar; public LinearLayout llRightSeekBar;
@@ -119,13 +130,14 @@ public class MainContentView {
// 对话框状态锁(避免快速点击重复弹窗) // 对话框状态锁(避免快速点击重复弹窗)
private boolean isDialogShowing = false; private boolean isDialogShowing = false;
// ======================== 构造方法(初始化入口,逻辑闭环)======================== // ====================================== 构造方法(初始化入口,逻辑闭环) ======================================
public MainContentView(Context context, View rootView, OnViewActionListener actionListener) { public MainContentView(Context context, View rootView, OnViewActionListener actionListener) {
LogUtils.d(TAG, "MainContentView() | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener); LogUtils.d(TAG, "MainContentView】构造器调用 | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener);
// 初始化外部依赖 // 初始化外部依赖
this.mContext = context; this.mContext = context;
this.mActionListener = actionListener; this.mActionListener = actionListener;
this.mAppConfigUtils = AppConfigUtils.getInstance(context.getApplicationContext()); this.mAppConfigUtils = AppConfigUtils.getInstance(context.getApplicationContext());
mBatteryStyle = BatteryStyleView.getSavedBatteryStyle(context);
// 执行核心初始化流程(按顺序执行,避免依赖空指针) // 执行核心初始化流程(按顺序执行,避免依赖空指针)
bindViews(rootView); bindViews(rootView);
@@ -133,19 +145,33 @@ public class MainContentView {
initConfirmDialog(); initConfirmDialog();
bindViewListeners(); bindViewListeners();
LogUtils.d(TAG, "MainContentView 初始化完成"); LogUtils.d(TAG, "MainContentView初始化完成");
} }
// ======================== 私有初始化方法(封装内部逻辑,仅暴露入口)======================== // ====================================== 私有初始化方法(封装内部逻辑,仅暴露入口) ======================================
/** /**
* 绑定视图控件(显式强转适配 Java7适配 API30 视图加载机制) * 绑定视图控件(显式强转适配 Java7适配 API30 视图加载机制)
* @param rootView 根视图
*/ */
private void bindViews(View rootView) { private void bindViews(View rootView) {
LogUtils.d(TAG, "bindViews() | rootView=" + rootView); LogUtils.d(TAG, "bindViews】视图绑定开始 | rootView=" + rootView);
// 基础布局绑定 // 基础布局绑定
mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1); mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1);
backgroundView = (BackgroundView) rootView.findViewById(R.id.fragmentmainviewBackgroundView1); mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview);
// 容器布局绑定
backgroundView = App.getInstance().getMemoryCachedBackgroundView();
if (backgroundView == null) {
App.sBackgroundSourceUtils.loadSettings();
BackgroundBean backgroundBean = App.sBackgroundSourceUtils.getCurrentBackgroundBean();
backgroundView = App.getInstance().getMemoryCachedBackgroundView().getInstance(mContext, backgroundBean, true);
}
if (backgroundView.getParent() != null) {
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
LogUtils.d(TAG, "【bindViews】移除背景视图旧父容器");
}
mllBackgroundView.addView(backgroundView);
// 容器布局绑定
llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1); llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1);
llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2); llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2);
// 开关控件绑定 // 开关控件绑定
@@ -168,35 +194,87 @@ public class MainContentView {
// 初始化进度缓存(从配置读取初始值) // 初始化进度缓存(从配置读取初始值)
mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue(); mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue();
mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue(); mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue();
LogUtils.d(TAG, "【bindViews】进度缓存初始化 | charge=" + mCurrentChargeProgress + " | usage=" + mCurrentUsageProgress);
// 关键视图绑定校验(仅保留核心控件错误日志,精简冗余) // 关键视图绑定校验(仅保留核心控件错误日志,精简冗余)
if (mainLayout == null) LogUtils.e(TAG, "mainLayout 绑定失败"); if (mainLayout == null) LogUtils.e(TAG, "【bindViews】mainLayout 绑定失败");
if (backgroundView == null) LogUtils.e(TAG, "backgroundView 绑定失败"); if (backgroundView == null) LogUtils.e(TAG, "【bindViews】backgroundView 绑定失败");
LogUtils.d(TAG, "【bindViews】视图绑定完成");
} }
public void reloadBackgroundView() {
if (backgroundView != null) {
App.sBackgroundSourceUtils.loadSettings();
BackgroundBean backgroundBean = App.sBackgroundSourceUtils.getCurrentBackgroundBean();
backgroundView.loadByBackgroundBean(backgroundBean, true);
}
}
/** /**
* 初始化电池 Drawable集成 BatteryDrawable默认能量风格适配小米机型渲染 * 初始化电池 Drawable集成 BatteryDrawable默认能量风格适配小米机型渲染
*/ */
private void initBatteryDrawables() { private void initBatteryDrawables() {
LogUtils.d(TAG, "initBatteryDrawables()"); LogUtils.d(TAG, "initBatteryDrawables】电池Drawable初始化开始 | style="+mBatteryStyle.name());
// 当前电量 Drawable颜色从资源读取适配 API30 主题) // 当前电量 Drawable颜色从资源读取适配 API30 主题)
int colorCurrent = getResourceColor(R.color.colorCurrent); int colorCurrent = getResourceColor(R.color.colorCurrent);
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent); mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
mCurrentBatteryDrawable.setDrawStyle(mBatteryStyle);
// 充电提醒 Drawable // 充电提醒 Drawable
int colorCharge = getResourceColor(R.color.colorCharge); int colorCharge = getResourceColor(R.color.colorCharge);
mChargeReminderBatteryDrawable = new BatteryDrawable(colorCharge); mChargeReminderBatteryDrawable = new BatteryDrawable(colorCharge);
// 耗电提醒 Drawable mChargeReminderBatteryDrawable.setDrawStyle(mBatteryStyle);
// 耗电提醒 Drawable
int colorUsage = getResourceColor(R.color.colorUsege); int colorUsage = getResourceColor(R.color.colorUsege);
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage); mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
mUsageReminderBatteryDrawable.setDrawStyle(mBatteryStyle);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
} }
/**
* ✅ 核心修复:电池样式切换+强制重绘刷新 完整版
* 修复点1重新创建Drawable后重新给ImageView赋值Drawable
* 修复点2重置所有Drawable的电量值保证样式切换后数值不变
* 修复点3调用ImageView.invalidate()强制触发重绘API30必加
* 修复点4Drawable.invalidateSelf() 双保险刷新绘制内容
* @param batteryStyle 切换后的电池样式
*/
public void updateBatteryDrawable(BatteryStyle batteryStyle) {
if(batteryStyle == null || batteryStyle == mBatteryStyle){
LogUtils.d(TAG, "【updateBatteryDrawable】样式无变化跳过刷新");
return;
}
// 1. 更新样式标记
mBatteryStyle = batteryStyle;
// 2. 重新初始化Drawable并设置新样式
initBatteryDrawables();
// 3. 重置所有Drawable的电量值 → 保证样式切换后数值不变
mCurrentBatteryDrawable.setBatteryValue(mAppConfigUtils.getCurrentBatteryValue());
mChargeReminderBatteryDrawable.setBatteryValue(mCurrentChargeProgress);
mUsageReminderBatteryDrawable.setBatteryValue(mCurrentUsageProgress);
// 4. 重新给ImageView赋值Drawable → 核心修复:之前缺失这一步
if(ivCurrentBattery != null) ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
if(ivChargeReminderBattery != null) ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
if(ivUsageReminderBattery != null) ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
// 5. Drawable自身刷新 → 双保险
mCurrentBatteryDrawable.invalidateSelf();
mChargeReminderBatteryDrawable.invalidateSelf();
mUsageReminderBatteryDrawable.invalidateSelf();
// 6. ✅ API30关键修复ImageView强制重绘解决绘制缓存不刷新问题
if(ivCurrentBattery != null) ivCurrentBattery.invalidate();
if(ivChargeReminderBattery != null) ivChargeReminderBattery.invalidate();
if(ivUsageReminderBattery != null) ivUsageReminderBattery.invalidate();
LogUtils.d(TAG, "【updateBatteryDrawable】样式切换完成"+mBatteryStyle.name() + " | 重绘触发成功");
ToastUtils.show("电池样式已切换为:"+mBatteryStyle.name());
}
/** /**
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题) * 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
*/ */
private void initConfirmDialog() { private void initConfirmDialog() {
LogUtils.d(TAG, "initConfirmDialog()"); LogUtils.d(TAG, "initConfirmDialog】对话框初始化开始");
if (mContext == null) { if (mContext == null) {
LogUtils.e(TAG, "Context 为空,初始化失败"); LogUtils.e(TAG, "【initConfirmDialog】Context 为空,初始化失败");
return; return;
} }
@@ -236,16 +314,17 @@ public class MainContentView {
mConfigConfirmDialog = mDialogBuilder.create(); mConfigConfirmDialog = mDialogBuilder.create();
mConfigConfirmDialog.setCancelable(true); mConfigConfirmDialog.setCancelable(true);
mConfigConfirmDialog.setCanceledOnTouchOutside(true); mConfigConfirmDialog.setCanceledOnTouchOutside(true);
LogUtils.d(TAG, "【initConfirmDialog】对话框初始化完成");
} }
/** /**
* 绑定视图事件监听Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效) * 绑定视图事件监听Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效)
*/ */
private void bindViewListeners() { private void bindViewListeners() {
LogUtils.d(TAG, "bindViewListeners()"); LogUtils.d(TAG, "bindViewListeners】事件监听绑定开始");
// 依赖校验,避免空指针 // 依赖校验,避免空指针
if (mAppConfigUtils == null || mActionListener == null || mConfigConfirmDialog == null) { if (mAppConfigUtils == null || mActionListener == null || mDialogBuilder == null) {
LogUtils.e(TAG, "依赖实例为空,跳过监听绑定"); LogUtils.e(TAG, "【bindViewListeners】依赖实例为空,跳过监听绑定");
return; return;
} }
@@ -258,14 +337,14 @@ public class MainContentView {
int originalValue = mAppConfigUtils.getChargeReminderValue(); int originalValue = mAppConfigUtils.getChargeReminderValue();
// 进度无变化,不处理 // 进度无变化,不处理
if (originalValue == progress) { if (originalValue == progress) {
LogUtils.d(TAG, "ChargeReminderSeekBar: 进度无变化,跳过"); LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar: 进度无变化,跳过");
return; return;
} }
// 缓存变更数据,显示确认对话框 // 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress); mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress);
updateDialogMessageByChangeType(); updateDialogMessageByChangeType();
showConfigConfirmDialog(); showConfigConfirmDialog();
LogUtils.d(TAG, "ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress); LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
} }
@Override @Override
@@ -280,7 +359,7 @@ public class MainContentView {
seekBar.setProgress(originalValue); seekBar.setProgress(originalValue);
// 恢复进度缓存 // 恢复进度缓存
mCurrentChargeProgress = originalValue; mCurrentChargeProgress = originalValue;
LogUtils.d(TAG, "ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue); LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
} }
}); });
@@ -296,6 +375,7 @@ public class MainContentView {
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable); ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(progress + "%"); tvChargeReminderValue.setText(progress + "%");
} }
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar实时更新 | 进度=" + progress);
} }
} }
@@ -305,7 +385,7 @@ public class MainContentView {
@Override @Override
public void onStopTrackingTouch(VerticalSeekBar seekBar) {} public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
}); });
LogUtils.d(TAG, "充电提醒进度条专属监听绑定完成"); LogUtils.d(TAG, "【bindViewListeners】充电提醒进度条专属监听绑定完成");
} }
// 充电提醒开关监听 // 充电提醒开关监听
@@ -321,10 +401,10 @@ public class MainContentView {
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue); mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue);
updateDialogMessageByChangeType(); updateDialogMessageByChangeType();
showConfigConfirmDialog(); showConfigConfirmDialog();
LogUtils.d(TAG, "cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue); LogUtils.d(TAG, "【bindViewListeners】cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
} }
}); });
LogUtils.d(TAG, "充电提醒开关监听绑定完成"); LogUtils.d(TAG, "【bindViewListeners】充电提醒开关监听绑定完成");
} }
// 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口确保弹窗100%触发) // 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口确保弹窗100%触发)
@@ -336,14 +416,14 @@ public class MainContentView {
int originalValue = mAppConfigUtils.getUsageReminderValue(); int originalValue = mAppConfigUtils.getUsageReminderValue();
// 进度无变化,不处理 // 进度无变化,不处理
if (originalValue == progress) { if (originalValue == progress) {
LogUtils.d(TAG, "UsageReminderSeekBar: 进度无变化,跳过"); LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar: 进度无变化,跳过");
return; return;
} }
// 缓存变更数据,显示确认对话框 // 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress); mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress);
updateDialogMessageByChangeType(); updateDialogMessageByChangeType();
showConfigConfirmDialog(); showConfigConfirmDialog();
LogUtils.d(TAG, "UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress); LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
} }
@Override @Override
@@ -358,7 +438,7 @@ public class MainContentView {
seekBar.setProgress(originalValue); seekBar.setProgress(originalValue);
// 恢复进度缓存 // 恢复进度缓存
mCurrentUsageProgress = originalValue; mCurrentUsageProgress = originalValue;
LogUtils.d(TAG, "UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue); LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
} }
}); });
@@ -374,6 +454,7 @@ public class MainContentView {
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable); ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(progress + "%"); tvUsageReminderValue.setText(progress + "%");
} }
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar实时更新 | 进度=" + progress);
} }
} }
@@ -383,7 +464,7 @@ public class MainContentView {
@Override @Override
public void onStopTrackingTouch(VerticalSeekBar seekBar) {} public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
}); });
LogUtils.d(TAG, "耗电提醒进度条专属监听绑定完成"); LogUtils.d(TAG, "【bindViewListeners】耗电提醒进度条专属监听绑定完成");
} }
// 耗电提醒开关监听 // 耗电提醒开关监听
@@ -399,10 +480,10 @@ public class MainContentView {
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue); mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue);
updateDialogMessageByChangeType(); updateDialogMessageByChangeType();
showConfigConfirmDialog(); showConfigConfirmDialog();
LogUtils.d(TAG, "cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue); LogUtils.d(TAG, "【bindViewListeners】cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
} }
}); });
LogUtils.d(TAG, "耗电提醒开关监听绑定完成"); LogUtils.d(TAG, "【bindViewListeners】耗电提醒开关监听绑定完成");
} }
// 服务总开关监听(核心优化:逻辑与其他控件完全对齐) // 服务总开关监听(核心优化:逻辑与其他控件完全对齐)
@@ -421,24 +502,24 @@ public class MainContentView {
updateDialogMessageByChangeType(); updateDialogMessageByChangeType();
// 显示确认对话框 // 显示确认对话框
showConfigConfirmDialog(); showConfigConfirmDialog();
LogUtils.d(TAG, "swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue); LogUtils.d(TAG, "【bindViewListeners】swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
} }
}); });
LogUtils.d(TAG, "服务总开关监听绑定完成"); LogUtils.d(TAG, "【bindViewListeners】服务总开关监听绑定完成");
} }
LogUtils.d(TAG, "所有事件监听绑定完成"); LogUtils.d(TAG, "【bindViewListeners】所有事件监听绑定完成");
} }
// ======================== 对外暴露核心方法(业务入口,精简参数,明确职责)======================== // ====================================== 对外暴露核心方法(业务入口,精简参数,明确职责) ======================================
/** /**
* 更新所有视图数据(从配置读取数据,统一刷新 UI适配 API30 视图更新规范) * 更新所有视图数据(从配置读取数据,统一刷新 UI适配 API30 视图更新规范)
* @param frameDrawable 进度条背景 Drawable外部传入适配主题切换 * @param frameDrawable 进度条背景 Drawable外部传入适配主题切换
*/ */
public void updateViewData(Drawable frameDrawable) { public void updateViewData(Drawable frameDrawable) {
LogUtils.d(TAG, "updateViewData() | frameDrawable=" + frameDrawable); LogUtils.d(TAG, "updateViewData】视图数据更新开始 | frameDrawable=" + frameDrawable);
if (mAppConfigUtils == null) { if (mAppConfigUtils == null) {
LogUtils.e(TAG, "AppConfigUtils 为空,跳过更新"); LogUtils.e(TAG, "【updateViewData】AppConfigUtils 为空,跳过更新");
return; return;
} }
@@ -453,12 +534,13 @@ public class MainContentView {
// 更新进度缓存 // 更新进度缓存
mCurrentChargeProgress = chargeVal; mCurrentChargeProgress = chargeVal;
mCurrentUsageProgress = usageVal; mCurrentUsageProgress = usageVal;
LogUtils.d(TAG, "配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable); LogUtils.d(TAG, "【updateViewData】配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable);
// 进度条背景更新 // 进度条背景更新
if (frameDrawable != null) { if (frameDrawable != null) {
if (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable); if (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable);
if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable); if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable);
LogUtils.d(TAG, "【updateViewData】进度条背景更新完成");
} }
// 当前电量更新(联动 BatteryDrawable实时刷新图标 // 当前电量更新(联动 BatteryDrawable实时刷新图标
@@ -470,6 +552,7 @@ public class MainContentView {
tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent)); tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent));
tvCurrentBatteryValue.setText(currentVal + "%"); tvCurrentBatteryValue.setText(currentVal + "%");
} }
LogUtils.d(TAG, "【updateViewData】当前电量更新完成");
// 充电提醒视图更新 // 充电提醒视图更新
if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) { if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) {
@@ -482,6 +565,7 @@ public class MainContentView {
} }
if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal); if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal);
if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable); if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable);
LogUtils.d(TAG, "【updateViewData】充电提醒视图更新完成");
// 耗电提醒视图更新 // 耗电提醒视图更新
if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) { if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) {
@@ -494,6 +578,7 @@ public class MainContentView {
} }
if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal); if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal);
if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable); if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable);
LogUtils.d(TAG, "【updateViewData】耗电提醒视图更新完成");
// 服务开关+提示文本更新(确保状态准确) // 服务开关+提示文本更新(确保状态准确)
if (swEnableService != null) { if (swEnableService != null) {
@@ -501,8 +586,9 @@ public class MainContentView {
swEnableService.setText(mContext.getString(R.string.txt_aboveswitch)); swEnableService.setText(mContext.getString(R.string.txt_aboveswitch));
} }
if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips)); if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips));
LogUtils.d(TAG, "【updateViewData】服务开关与提示文本更新完成");
LogUtils.d(TAG, "所有视图数据更新完成"); LogUtils.d(TAG, "【updateViewData】所有视图数据更新完成");
} }
/** /**
@@ -510,28 +596,28 @@ public class MainContentView {
* @param value 电量值(自动校准 0-100避免异常值 * @param value 电量值(自动校准 0-100避免异常值
*/ */
public void updateCurrentBattery(int value) { public void updateCurrentBattery(int value) {
LogUtils.d(TAG, "updateCurrentBattery() | 原始值=" + value); LogUtils.d(TAG, "updateCurrentBattery】当前电量更新开始 | 原始值=" + value);
// 核心依赖校验 // 核心依赖校验
if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) { if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) {
LogUtils.e(TAG, "视图/Drawable 为空,跳过更新"); LogUtils.e(TAG, "【updateCurrentBattery】视图/Drawable 为空,跳过更新");
return; return;
} }
// 校准电量范围(强制 0-100防止 API30 视图显示异常) // 校准电量范围(强制 0-100防止 API30 视图显示异常)
int validValue = Math.max(0, Math.min(value, 100)); int validValue = Math.max(BATTERY_MIN, Math.min(value, BATTERY_MAX));
// 联动 BatteryDrawable 更新图标,同步文本显示 // 联动 BatteryDrawable 更新图标,同步文本显示
mCurrentBatteryDrawable.setBatteryValue(validValue); mCurrentBatteryDrawable.setBatteryValue(validValue);
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable); ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
tvCurrentBatteryValue.setText(validValue + "%"); tvCurrentBatteryValue.setText(validValue + "%");
LogUtils.d(TAG, "更新完成 | 校准后值=" + validValue); LogUtils.d(TAG, "【updateCurrentBattery】更新完成 | 校准后值=" + validValue);
} }
/** /**
* 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用) * 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用)
*/ */
public void releaseResources() { public void releaseResources() {
LogUtils.d(TAG, "releaseResources()"); LogUtils.d(TAG, "releaseResources】资源释放开始");
// 释放对话框资源(安全销毁,避免内存泄漏) // 释放对话框资源(安全销毁,避免内存泄漏)
if (mConfigConfirmDialog != null) { if (mConfigConfirmDialog != null) {
if (mConfigConfirmDialog.isShowing()) { if (mConfigConfirmDialog.isShowing()) {
@@ -554,6 +640,7 @@ public class MainContentView {
// 置空视图实例(断开视图引用,辅助 GC 回收) // 置空视图实例(断开视图引用,辅助 GC 回收)
mainLayout = null; mainLayout = null;
backgroundView = null; backgroundView = null;
mllBackgroundView = null;
llLeftSeekBar = null; llLeftSeekBar = null;
llRightSeekBar = null; llRightSeekBar = null;
cbEnableChargeReminder = null; cbEnableChargeReminder = null;
@@ -574,7 +661,7 @@ public class MainContentView {
mAppConfigUtils = null; mAppConfigUtils = null;
mActionListener = null; mActionListener = null;
LogUtils.d(TAG, "所有资源释放完成"); LogUtils.d(TAG, "【releaseResources】所有资源释放完成");
} }
/** /**
@@ -582,7 +669,7 @@ public class MainContentView {
* @param enabled 服务启用状态 * @param enabled 服务启用状态
*/ */
public void setServiceSwitchChecked(boolean enabled) { public void setServiceSwitchChecked(boolean enabled) {
LogUtils.d(TAG, "setServiceSwitchChecked() | enabled=" + enabled); LogUtils.d(TAG, "setServiceSwitchChecked】服务开关状态设置 | enabled=" + enabled);
if (swEnableService != null) { if (swEnableService != null) {
swEnableService.setChecked(enabled); swEnableService.setChecked(enabled);
} }
@@ -593,33 +680,33 @@ public class MainContentView {
* @param enabled 是否允许点击 * @param enabled 是否允许点击
*/ */
public void setServiceSwitchEnabled(boolean enabled) { public void setServiceSwitchEnabled(boolean enabled) {
LogUtils.d(TAG, "setServiceSwitchEnabled() | enabled=" + enabled); LogUtils.d(TAG, "setServiceSwitchEnabled】服务开关点击状态设置 | enabled=" + enabled);
if (swEnableService != null) { if (swEnableService != null) {
swEnableService.setEnabled(enabled); swEnableService.setEnabled(enabled);
} }
} }
// ======================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑)======================== // ====================================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑) ======================================
/** /**
* 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗) * 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗)
*/ */
private void showConfigConfirmDialog() { private void showConfigConfirmDialog() {
LogUtils.d(TAG, "showConfigConfirmDialog() | isDialogShowing=" + isDialogShowing); LogUtils.d(TAG, "showConfigConfirmDialog】对话框显示开始 | isDialogShowing=" + isDialogShowing);
// 对话框状态锁:正在显示则跳过,避免重复触发 // 对话框状态锁:正在显示则跳过,避免重复触发
if (isDialogShowing) { if (isDialogShowing) {
LogUtils.d(TAG, "对话框已显示,跳过重复调用"); LogUtils.d(TAG, "【showConfigConfirmDialog】对话框已显示,跳过重复调用");
return; return;
} }
// 基础校验:对话框/上下文/Builder 为空 // 基础校验:对话框/上下文/Builder 为空
if (mDialogBuilder == null || mContext == null) { if (mDialogBuilder == null || mContext == null) {
LogUtils.e(TAG, "对话框Builder/上下文异常,无法显示"); LogUtils.e(TAG, "【showConfigConfirmDialog】对话框Builder/上下文异常,无法显示");
if (mTempConfigData != null) cancelConfigChange(); if (mTempConfigData != null) cancelConfigChange();
return; return;
} }
// Activity 状态校验:避免销毁后弹窗崩溃(适配 API30 // Activity 状态校验:避免销毁后弹窗崩溃(适配 API30
Activity activity = (Activity) mContext; Activity activity = (Activity) mContext;
if (activity.isFinishing() || activity.isDestroyed()) { if (activity.isFinishing() || activity.isDestroyed()) {
LogUtils.e(TAG, "Activity 已销毁,无法显示对话框"); LogUtils.e(TAG, "【showConfigConfirmDialog】Activity 已销毁,无法显示对话框");
if (mTempConfigData != null) cancelConfigChange(); if (mTempConfigData != null) cancelConfigChange();
return; return;
} }
@@ -636,16 +723,16 @@ public class MainContentView {
mConfigConfirmDialog.setOnDismissListener(null); mConfigConfirmDialog.setOnDismissListener(null);
} }
}); });
LogUtils.d(TAG, "确认对话框显示成功"); LogUtils.d(TAG, "【showConfigConfirmDialog】确认对话框显示成功");
} }
/** /**
* 确认配置变更(保存数据+回调监听+更新视图) * 确认配置变更(保存数据+回调监听+更新视图)
*/ */
private void confirmConfigChange() { private void confirmConfigChange() {
LogUtils.d(TAG, "confirmConfigChange() | mTempConfigData=" + mTempConfigData); LogUtils.d(TAG, "confirmConfigChange】配置确认开始 | mTempConfigData=" + mTempConfigData);
if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) { if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) {
LogUtils.e(TAG, "依赖数据为空,确认失败"); LogUtils.e(TAG, "【confirmConfigChange】依赖数据为空,确认失败");
return; return;
} }
@@ -654,13 +741,13 @@ public class MainContentView {
case CHANGE_TYPE_CHARGE_SWITCH: case CHANGE_TYPE_CHARGE_SWITCH:
mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue); mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue);
mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue); mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue); LogUtils.d(TAG, "【confirmConfigChange】充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
break; break;
// 耗电提醒开关 // 耗电提醒开关
case CHANGE_TYPE_USAGE_SWITCH: case CHANGE_TYPE_USAGE_SWITCH:
mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue); mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue);
mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue); mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue); LogUtils.d(TAG, "【confirmConfigChange】耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
break; break;
// 服务总开关(核心:持久化配置+触发 Activity 回调) // 服务总开关(核心:持久化配置+触发 Activity 回调)
case CHANGE_TYPE_SERVICE_SWITCH: case CHANGE_TYPE_SERVICE_SWITCH:
@@ -672,36 +759,37 @@ public class MainContentView {
} }
// 2. 强制触发 Activity 回调,执行服务启停逻辑 // 2. 强制触发 Activity 回调,执行服务启停逻辑
mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue); mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置"); LogUtils.d(TAG, "【confirmConfigChange】服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
break; break;
// 充电提醒进度条 // 充电提醒进度条
case CHANGE_TYPE_CHARGE_SEEKBAR: case CHANGE_TYPE_CHARGE_SEEKBAR:
mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue); mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue);
mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue); mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue);
LogUtils.d(TAG, "充电提醒进度确认 | 值=" + mTempConfigData.newIntValue); LogUtils.d(TAG, "【confirmConfigChange】充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
break; break;
// 耗电提醒进度条 // 耗电提醒进度条
case CHANGE_TYPE_USAGE_SEEKBAR: case CHANGE_TYPE_USAGE_SEEKBAR:
mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue); mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue);
mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue); mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue);
LogUtils.d(TAG, "耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue); LogUtils.d(TAG, "【confirmConfigChange】耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
break; break;
default: default:
LogUtils.w(TAG, "未知变更类型,跳过"); LogUtils.w(TAG, "【confirmConfigChange】未知变更类型,跳过");
break; break;
} }
// 确认完成,清空临时数据 // 确认完成,清空临时数据
mTempConfigData = null; mTempConfigData = null;
LogUtils.d(TAG, "【confirmConfigChange】配置确认完成");
} }
/** /**
* 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致) * 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致)
*/ */
private void cancelConfigChange() { private void cancelConfigChange() {
LogUtils.d(TAG, "cancelConfigChange() | mTempConfigData=" + mTempConfigData); LogUtils.d(TAG, "cancelConfigChange】配置取消开始 | mTempConfigData=" + mTempConfigData);
if (mTempConfigData == null || mAppConfigUtils == null) { if (mTempConfigData == null || mAppConfigUtils == null) {
LogUtils.e(TAG, "依赖数据为空,取消失败"); LogUtils.e(TAG, "【cancelConfigChange】依赖数据为空,取消失败");
return; return;
} }
@@ -710,19 +798,19 @@ public class MainContentView {
if (cbEnableChargeReminder != null) { if (cbEnableChargeReminder != null) {
cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue); cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue);
} }
LogUtils.d(TAG, "充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); LogUtils.d(TAG, "【cancelConfigChange】充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break; break;
case CHANGE_TYPE_USAGE_SWITCH: case CHANGE_TYPE_USAGE_SWITCH:
if (cbEnableUsageReminder != null) { if (cbEnableUsageReminder != null) {
cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue); cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue);
} }
LogUtils.d(TAG, "耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); LogUtils.d(TAG, "【cancelConfigChange】耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break; break;
case CHANGE_TYPE_SERVICE_SWITCH: case CHANGE_TYPE_SERVICE_SWITCH:
if (swEnableService != null) { if (swEnableService != null) {
swEnableService.setChecked(mTempConfigData.originalBooleanValue); swEnableService.setChecked(mTempConfigData.originalBooleanValue);
} }
LogUtils.d(TAG, "服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); LogUtils.d(TAG, "【cancelConfigChange】服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break; break;
case CHANGE_TYPE_CHARGE_SEEKBAR: case CHANGE_TYPE_CHARGE_SEEKBAR:
if (sbChargeReminder != null) { if (sbChargeReminder != null) {
@@ -733,7 +821,7 @@ public class MainContentView {
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable); ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%"); tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%");
} }
LogUtils.d(TAG, "充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue); LogUtils.d(TAG, "【cancelConfigChange】充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
break; break;
case CHANGE_TYPE_USAGE_SEEKBAR: case CHANGE_TYPE_USAGE_SEEKBAR:
if (sbUsageReminder != null) { if (sbUsageReminder != null) {
@@ -744,22 +832,23 @@ public class MainContentView {
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable); ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%"); tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%");
} }
LogUtils.d(TAG, "耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue); LogUtils.d(TAG, "【cancelConfigChange】耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
break; break;
default: default:
LogUtils.w(TAG, "未知变更类型,跳过"); LogUtils.w(TAG, "【cancelConfigChange】未知变更类型,跳过");
break; break;
} }
// 取消完成,清空临时数据 // 取消完成,清空临时数据
mTempConfigData = null; mTempConfigData = null;
LogUtils.d(TAG, "【cancelConfigChange】配置取消完成");
} }
/** /**
* 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效) * 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效)
*/ */
private void updateDialogMessageByChangeType() { private void updateDialogMessageByChangeType() {
LogUtils.d(TAG, "updateDialogMessageByChangeType() | mTempConfigData=" + mTempConfigData); LogUtils.d(TAG, "updateDialogMessageByChangeType】对话框消息更新开始 | mTempConfigData=" + mTempConfigData);
if (mDialogBuilder == null || mTempConfigData == null) return; if (mDialogBuilder == null || mTempConfigData == null) return;
String message; String message;
if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) { if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) {
@@ -773,42 +862,20 @@ public class MainContentView {
} }
// 通过 Builder 设置消息,确保弹窗显示最新内容 // 通过 Builder 设置消息,确保弹窗显示最新内容
mDialogBuilder.setMessage(message); mDialogBuilder.setMessage(message);
LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新完成 | message=" + message);
} }
// ======================== 内部工具方法(封装重复逻辑,提升复用性)======================== // ====================================== 内部工具方法(封装重复逻辑,提升复用性) ======================================
/**
* 实时计算并更新比值预览sbUsageReminder / sbChargeReminder
* 处理除数为0的情况避免崩溃
*/
// private void updateRatioPreview() {
// if (mTvRatioPreview == null) return;
// float ratio;
// // 处理除数为0充电进度为0时显示0可根据需求改为“--”)
// if (mCurrentChargeProgress == 0) {
// ratio = 0.0f;
// } else {
// ratio = (float) mCurrentUsageProgress / mCurrentChargeProgress;
// }
// // 格式化比值保留1位小数适配本地化解决小米手机小数分隔符问题
// String ratioText = String.format(Locale.getDefault(), "比值:%.1f", ratio);
// mTvRatioPreview.setText(ratioText);
// // 触发比值变化回调
// if (mActionListener != null) {
// mActionListener.onRatioChanged(ratio);
// }
// LogUtils.d(TAG, "比值预览更新 | usage=" + mCurrentUsageProgress + " | charge=" + mCurrentChargeProgress + " | ratio=" + ratio);
// }
/** /**
* 获取资源颜色(适配 API30 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针) * 获取资源颜色(适配 API30 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针)
* @param colorResId 颜色资源 ID * @param colorResId 颜色资源 ID
* @return 校准后的颜色值 * @return 校准后的颜色值
*/ */
private int getResourceColor(int colorResId) { private int getResourceColor(int colorResId) {
LogUtils.d(TAG, "getResourceColor() | colorResId=" + colorResId); LogUtils.d(TAG, "getResourceColor】资源颜色获取 | colorResId=" + colorResId);
// 空指针防御Context 为空返回默认黑色 // 空指针防御Context 为空返回默认黑色
if (mContext == null) { if (mContext == null) {
LogUtils.e(TAG, "Context 为空,返回默认黑色"); LogUtils.e(TAG, "【getResourceColor】Context 为空,返回默认黑色");
return 0xFF000000; return 0xFF000000;
} }
// 适配 API30 主题颜色读取 // 适配 API30 主题颜色读取
@@ -824,11 +891,11 @@ public class MainContentView {
* @return 服务启用状态true=启用false=禁用) * @return 服务启用状态true=启用false=禁用)
*/ */
private boolean getServiceEnableState() { private boolean getServiceEnableState() {
LogUtils.d(TAG, "getServiceEnableState()"); LogUtils.d(TAG, "getServiceEnableState】服务状态获取开始");
ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class); ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
// 本地无配置时,默认禁用服务(与服务初始化逻辑对齐) // 本地无配置时,默认禁用服务(与服务初始化逻辑对齐)
boolean state = serviceBean != null && serviceBean.isEnableService(); boolean state = serviceBean != null && serviceBean.isEnableService();
LogUtils.d(TAG, "服务启用状态获取完成 | state=" + state); LogUtils.d(TAG, "【getServiceEnableState】服务启用状态获取完成 | state=" + state);
return state; return state;
} }
} }

View File

@@ -0,0 +1,221 @@
package cc.winboll.studio.powerbell.views;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.AttributeSet;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/**
* 单实例缓存版背景视图控件基于Java7- 强制缓存版
* 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片
* 新增SP持久化最后加载路径、获取最后加载实例功能
* 强制缓存策略:无论内存是否紧张,不自动清理任何缓存实例和路径记录
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class MemoryCachedBackgroundView extends BackgroundView {
// ====================================== 静态常量区TAG + SP相关常量 ======================================
public static final String TAG = "MemoryCachedBackgroundView";
// SP相关常量持久化最后加载路径
private static final String SP_NAME = "MemoryCachedBackgroundView_SP";
private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path";
// ====================================== 静态属性区(强制缓存核心:保存实例、路径、实例计数) ======================================
// 静态属性:保存当前缓存的路径和实例(强制保持,不自动销毁)
private static String sCachedImagePath;
private static MemoryCachedBackgroundView sCachedView;
// 新增:记录所有创建过的实例数量(用于强制缓存监控)
private static int sInstanceCount = 0;
// ====================================== 构造器(继承并兼容父类,私有构造防止外部实例化) ======================================
private MemoryCachedBackgroundView(Context context) {
super(context);
sInstanceCount++;
LogUtils.d(TAG, String.format("构造器1启动 | 创建实例,当前实例总数=%d", sInstanceCount));
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
sInstanceCount++;
LogUtils.d(TAG, String.format("构造器2启动 | 创建实例,当前实例总数=%d", sInstanceCount));
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
sInstanceCount++;
LogUtils.d(TAG, String.format("构造器3启动 | 创建实例,当前实例总数=%d", sInstanceCount));
}
// ====================================== 核心静态方法:获取/创建缓存实例(强制缓存版) ======================================
/**
* 从缓存获取或创建MemoryCachedBackgroundView实例强制保持旧实例
* @param context 上下文
* @param imagePath 图片绝对路径(作为缓存标识)
* @param isReload 是否强制重新加载图片(路径匹配时仍刷新)
* @return 缓存/新创建的MemoryCachedBackgroundView实例
*/
public static MemoryCachedBackgroundView getInstance(Context context, BackgroundBean bean, boolean isReload) {
LogUtils.d(TAG, String.format("getInstance 调用 | BackgroundBean=%s | 是否重载=%b", bean.toString(), isReload));
//App.notifyMessage(TAG, String.format("getInstance 调用 | BackgroundBean=%s | 是否重载=%b", bean.toString(), isReload));
sCachedView = new MemoryCachedBackgroundView(context);
sCachedView.loadByBackgroundBean(bean, isReload);
saveLastLoadImagePath(context, getBackgroundBeanImagePath(bean));
LogUtils.d(TAG, String.format("getInstance: 已更新当前缓存实例,旧实例路径=%s强制保持", getBackgroundBeanImagePath(bean)));
//App.notifyMessage(TAG, String.format("getInstance: 已更新当前缓存实例,旧实例路径=%s强制保持", getBackgroundBeanImagePath(bean)));
return sCachedView;
}
static String getBackgroundBeanImagePath(BackgroundBean bean) {
if (bean.isUseBackgroundFile()) {
if (bean.isUseBackgroundScaledCompressFile()) {
return bean.getBackgroundScaledCompressFilePath();
}
return bean.getBackgroundFilePath();
}
return "";
}
// ====================================== 新增功能:获取最后加载的实例(强制缓存版) ======================================
/**
* 获取最后一次loadImage的路径对应的实例强制保持所有实例
* 无实例则创建并加载图片,同时更新静态缓存
* @param context 上下文
* @return 最后加载路径对应的实例
*/
public static MemoryCachedBackgroundView getLastInstance(Context context) {
LogUtils.d(TAG, "getLastInstance 调用");
//App.notifyMessage(TAG, "getLastInstance 调用");
// 1. 从SP获取最后加载的路径强制保持不自动删除
sCachedImagePath = getLastLoadImagePath(context);
String lastPath = getBackgroundBeanImagePath(App.sBackgroundSourceUtils.getCurrentBackgroundBean());
//App.notifyMessage(TAG, String.format("sCachedImagePath : %s", sCachedImagePath));
//App.notifyMessage(TAG, String.format("lastPath : %s", lastPath));
if (lastPath.equals(sCachedImagePath) && sCachedView != null) {
LogUtils.d(TAG, String.format("getLastInstance: 使用最后路径缓存实例 | 路径=%s", lastPath));
//App.notifyMessage(TAG, String.format("getLastInstance: 使用最后路径缓存实例 | 路径=%s", lastPath));
return sCachedView;
}
//App.notifyMessage(TAG, "getLastInstance 返回 null");
return null;
}
// ====================================== 工具方法SP持久化最后加载路径强制保持版 ======================================
/**
* 保存最后一次loadImage的路径到SP强制保持不自动删除
* @param context 上下文
* @param imagePath 图片路径
*/
private static void saveLastLoadImagePath(Context context, String imagePath) {
if (TextUtils.isEmpty(imagePath) || context == null) {
LogUtils.w(TAG, "saveLastLoadImagePath: 路径或上下文为空,跳过保存");
return;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_LAST_LOAD_IMAGE_PATH, imagePath).apply();
LogUtils.d(TAG, String.format("saveLastLoadImagePath: 已保存最后路径(强制保持) | 路径=%s", imagePath));
}
/**
* 从SP获取最后一次loadImage的路径强制保持不自动删除
* @param context 上下文
* @return 最后加载的图片路径空则返回null
*/
public static String getLastLoadImagePath(Context context) {
if (context == null) {
LogUtils.e(TAG, "getLastLoadImagePath: 上下文为空返回null");
return null;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String lastPath = sp.getString(KEY_LAST_LOAD_IMAGE_PATH, null);
LogUtils.d(TAG, String.format("getLastLoadImagePath: 获取最后路径(强制保持) | 路径=%s", lastPath));
return lastPath;
}
// ====================================== 工具方法:缓存管理(强制缓存版 - 仅日志,不清理) ======================================
/**
* 清除当前缓存实例和路径(强制缓存策略:仅日志,不实际清理)
*/
public static void clearCache() {
LogUtils.w(TAG, String.format("clearCache 调用(强制缓存策略:不实际清理缓存) | 当前缓存路径=%s", sCachedImagePath));
LogUtils.d(TAG, "clearCache: 强制缓存策略生效,未清除任何实例和路径");
}
/**
* 清除指定路径的缓存(强制缓存策略:仅日志,不实际清理)
* @param imagePath 图片路径
*/
public static void removeCache(String imagePath) {
LogUtils.w(TAG, String.format("removeCache 调用(强制缓存策略:不实际清理缓存) | 图片路径=%s", imagePath));
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "removeCache: 图片路径为空,清除失败");
return;
}
LogUtils.d(TAG, "removeCache: 强制缓存策略生效,未清除任何实例和路径");
}
/**
* 清除所有缓存(强制缓存策略:仅日志,不实际清理)
*/
public static void clearAllCache() {
LogUtils.w(TAG, "clearAllCache 调用(强制缓存策略:不实际清理缓存)");
LogUtils.d(TAG, "clearAllCache: 强制缓存策略生效未清除任何实例、路径和SP记录");
}
/**
* 判断是否存在缓存实例
* @return 存在返回true否则返回false
*/
public static boolean hasCache() {
boolean hasCache = sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
LogUtils.d(TAG, String.format("hasCache 调用 | 缓存存在状态=%b", hasCache));
return hasCache;
}
/**
* 清除SP中最后加载的路径记录强制缓存策略仅日志不实际清理
* @param context 上下文
*/
public static void clearLastLoadImagePath(Context context) {
LogUtils.w(TAG, "clearLastLoadImagePath 调用强制缓存策略不实际清理SP记录");
LogUtils.d(TAG, "clearLastLoadImagePath: 强制缓存策略生效未清除SP中最后路径记录");
}
// ====================================== 辅助方法:从缓存获取上下文 ======================================
/**
* 从缓存实例中获取上下文用于无外部上下文时的SP操作
* @return 上下文实例无则返回null
*/
// private static Context getContextFromCache() {
// Context context = sCachedView != null ? sCachedView.getContext() : null;
// LogUtils.d(TAG, String.format("getContextFromCache 调用 | 从缓存获取上下文=%s", context));
// return context;
// }
// ====================================== 重写父类方法:增强日志+SP持久化强制保持版 ======================================
@Override
public void loadByBackgroundBean(BackgroundBean bean) {
LogUtils.d(TAG, String.format("loadByBackgroundBean 调用 | BackgroundBean=%s", (bean == null ? "null" : bean.toString())));
super.loadByBackgroundBean(bean);
}
@Override
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
LogUtils.d(TAG, String.format("loadByBackgroundBean 调用 | BackgroundBean=%s | 是否刷新=%b", (bean == null ? "null" : bean.toString()), isRefresh));
super.loadByBackgroundBean(bean, isRefresh);
}
// ====================================== 新增:强制缓存监控方法 ======================================
/**
* 获取当前所有创建过的实例总数(用于监控强制缓存状态)
* @return 实例总数
*/
public static int getInstanceCount() {
LogUtils.d(TAG, String.format("getInstanceCount 调用 | 当前实例总数=%d", sInstanceCount));
return sInstanceCount;
}
}

View File

@@ -8,10 +8,10 @@ import android.widget.SeekBar;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
/** /**
* 垂直进度条控件,适配 API30支持逆时针旋转0在下100在上
* 修复滑块同步+弹窗触发bug新增实时进度变化监听接口支持拖动时实时回调进度
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 14:11 * @Date 2025/12/17 14:11
* @Describe 垂直进度条控件,适配 API30支持逆时针旋转0在下100在上修复滑块同步+弹窗触发bug
* 新增:实时进度变化监听接口,支持拖动时实时回调进度
*/ */
public class VerticalSeekBar extends SeekBar { public class VerticalSeekBar extends SeekBar {
// ======================== 静态常量 ========================= // ======================== 静态常量 =========================
@@ -76,26 +76,26 @@ public class VerticalSeekBar extends SeekBar {
public VerticalSeekBar(Context context) { public VerticalSeekBar(Context context) {
super(context); super(context);
initView(); initView();
LogUtils.d(TAG, "VerticalSeekBar(Context) 初始化"); LogUtils.d(TAG, "【构造器1】VerticalSeekBar 初始化完成");
} }
public VerticalSeekBar(Context context, AttributeSet attrs) { public VerticalSeekBar(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
initView(); initView();
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet) 初始化"); LogUtils.d(TAG, "【构造器2】VerticalSeekBar 初始化完成");
} }
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) { public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
initView(); initView();
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet, int) 初始化"); LogUtils.d(TAG, "【构造器3】VerticalSeekBar 初始化完成");
} }
// ======================== 初始化方法 ========================= // ======================== 初始化方法 =========================
private void initView() { private void initView() {
// 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销 // 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销
setBackgroundDrawable(null); setBackgroundDrawable(null);
LogUtils.d(TAG, "initView: 移除默认背景阴影,完成视图初始化"); LogUtils.d(TAG, "initView移除默认背景阴影,完成视图初始化");
} }
// ======================== 对外设置方法(监听接口绑定)======================== // ======================== 对外设置方法(监听接口绑定)========================
@@ -105,7 +105,7 @@ public class VerticalSeekBar extends SeekBar {
*/ */
public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener listener) { public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener listener) {
this.mTouchListener = listener; this.mTouchListener = listener;
LogUtils.d(TAG, "setOnVerticalSeekBarTouchListener: 触摸监听器绑定完成"); LogUtils.d(TAG, "setOnVerticalSeekBarTouchListener触摸监听器绑定完成");
} }
/** /**
@@ -114,7 +114,7 @@ public class VerticalSeekBar extends SeekBar {
*/ */
public void setOnVerticalSeekBarChangeListener(OnVerticalSeekBarChangeListener listener) { public void setOnVerticalSeekBarChangeListener(OnVerticalSeekBarChangeListener listener) {
this.mProgressChangeListener = listener; this.mProgressChangeListener = listener;
LogUtils.d(TAG, "setOnVerticalSeekBarChangeListener: 实时进度监听器绑定完成"); LogUtils.d(TAG, "setOnVerticalSeekBarChangeListener实时进度监听器绑定完成");
} }
// ======================== 重写系统方法(测量/布局/绘制)======================== // ======================== 重写系统方法(测量/布局/绘制)========================
@@ -122,13 +122,13 @@ public class VerticalSeekBar extends SeekBar {
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec); super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
LogUtils.v(TAG, "onMeasure: 垂直测量完成,宽=" + getMeasuredHeight() + ", 高=" + getMeasuredWidth()); LogUtils.v(TAG, "onMeasure垂直测量完成,宽=" + getMeasuredHeight() + "高=" + getMeasuredWidth());
} }
@Override @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw); super.onSizeChanged(h, w, oldh, oldw);
LogUtils.v(TAG, "onSizeChanged: 尺寸变化,新宽=" + h + ", 新高=" + w); LogUtils.v(TAG, "onSizeChanged尺寸变化,新宽=" + h + "新高=" + w);
} }
@Override @Override
@@ -137,7 +137,7 @@ public class VerticalSeekBar extends SeekBar {
canvas.rotate(-90); canvas.rotate(-90);
canvas.translate(-getHeight(), 0); canvas.translate(-getHeight(), 0);
super.onDraw(canvas); super.onDraw(canvas);
LogUtils.v(TAG, "onDraw: 完成垂直绘制,旋转角度=-90°"); LogUtils.v(TAG, "onDraw完成垂直绘制,旋转角度=-90°");
} }
// ======================== 重写进度设置方法(修复滑块同步+新增实时回调)======================== // ======================== 重写进度设置方法(修复滑块同步+新增实时回调)========================
@@ -151,10 +151,11 @@ public class VerticalSeekBar extends SeekBar {
// 强制触发尺寸变化同步刷新滑块位置核心bug修复逻辑 // 强制触发尺寸变化同步刷新滑块位置核心bug修复逻辑
onSizeChanged(getWidth(), getHeight(), 0, 0); onSizeChanged(getWidth(), getHeight(), 0, 0);
mProgress = progress; mProgress = progress;
LogUtils.d(TAG, "setProgress: 进度设置为" + progress + ",滑块同步刷新"); LogUtils.d(TAG, "setProgress进度设置为" + progress + ",滑块同步刷新");
// 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false // 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false
if (mProgressChangeListener != null) { if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, progress, false); mProgressChangeListener.onProgressChanged(this, progress, false);
LogUtils.v(TAG, "【setProgress】触发实时进度回调fromUser=false");
} }
} }
@@ -165,20 +166,22 @@ public class VerticalSeekBar extends SeekBar {
super.onTouchEvent(event); super.onTouchEvent(event);
boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失 boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失
boolean fromUser = true; // 标记是否是用户触摸导致的进度变化 boolean fromUser = true; // 标记是否是用户触摸导致的进度变化
int action = event.getAction();
switch (event.getAction()) { switch (action) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
LogUtils.d(TAG, "onTouchEvent: 触摸按下Y坐标=" + event.getY()); LogUtils.d(TAG, "onTouchEvent触摸按下Y坐标=" + event.getY());
// 触发实时进度监听:开始触摸 // 触发实时进度监听:开始触摸
if (mProgressChangeListener != null) { if (mProgressChangeListener != null) {
mProgressChangeListener.onStartTrackingTouch(this); mProgressChangeListener.onStartTrackingTouch(this);
LogUtils.v(TAG, "【onTouchEvent】触发开始触摸回调");
} }
break; break;
case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_MOVE:
calculateProgress(event.getY()); calculateProgress(event.getY());
setProgress(mProgress); setProgress(mProgress);
LogUtils.v(TAG, "onTouchEvent: 触摸滑动,进度更新为" + mProgress); LogUtils.v(TAG, "onTouchEvent触摸滑动,进度更新为" + mProgress);
// 触发实时进度监听:进度变化 // 触发实时进度监听:进度变化
if (mProgressChangeListener != null) { if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser); mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
@@ -188,27 +191,31 @@ public class VerticalSeekBar extends SeekBar {
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
calculateProgress(event.getY()); calculateProgress(event.getY());
setProgress(mProgress); setProgress(mProgress);
LogUtils.d(TAG, "onTouchEvent: 触摸抬起,进度=" + mProgress + ",触发弹窗回调"); LogUtils.d(TAG, "onTouchEvent触摸抬起,进度=" + mProgress + ",触发弹窗回调");
// 触发实时进度监听:停止触摸 // 触发实时进度监听:进度变化+停止触摸
if (mProgressChangeListener != null) { if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser); mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
mProgressChangeListener.onStopTrackingTouch(this); mProgressChangeListener.onStopTrackingTouch(this);
LogUtils.v(TAG, "【onTouchEvent】触发停止触摸回调");
} }
// 核心:调用原有触摸接口,通知外部触发配置变更对话框 // 核心:调用原有触摸接口,通知外部触发配置变更对话框
if (mTouchListener != null) { if (mTouchListener != null) {
mTouchListener.onTouchUp(this, mProgress); mTouchListener.onTouchUp(this, mProgress);
LogUtils.v(TAG, "【onTouchEvent】触发触摸抬起回调");
} }
break; break;
case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_CANCEL:
LogUtils.d(TAG, "onTouchEvent: 触摸取消,当前进度=" + getProgress()); int currentProgress = getProgress();
LogUtils.d(TAG, "【onTouchEvent】触摸取消当前进度=" + currentProgress);
// 触发实时进度监听:停止触摸 // 触发实时进度监听:停止触摸
if (mProgressChangeListener != null) { if (mProgressChangeListener != null) {
mProgressChangeListener.onStopTrackingTouch(this); mProgressChangeListener.onStopTrackingTouch(this);
} }
// 可选:触摸取消时回调,外部可做进度回滚处理 // 可选:触摸取消时回调,外部可做进度回滚处理
if (mTouchListener != null) { if (mTouchListener != null) {
mTouchListener.onTouchCancel(this, getProgress()); mTouchListener.onTouchCancel(this, currentProgress);
LogUtils.v(TAG, "【onTouchEvent】触发触摸取消回调");
} }
break; break;
} }
@@ -225,7 +232,7 @@ public class VerticalSeekBar extends SeekBar {
mProgress = getMax() - (int) (getMax() * touchY / getHeight()); mProgress = getMax() - (int) (getMax() * touchY / getHeight());
// 校准进度范围,防止超出 0~100兼容 API30 进度边界校验) // 校准进度范围,防止超出 0~100兼容 API30 进度边界校验)
mProgress = Math.max(0, Math.min(mProgress, getMax())); mProgress = Math.max(0, Math.min(mProgress, getMax()));
LogUtils.v(TAG, "calculateProgress: 触摸Y=" + touchY + ",计算进度=" + mProgress); LogUtils.v(TAG, "calculateProgress触摸Y=" + touchY + ",计算进度=" + mProgress + ",校准后=" + mProgress);
} }
} }

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 阴影部分 -->
<!-- 个人觉得更形象的表达top代表下边的阴影高度left代表右边的阴影宽度。其实也就是相对应的offsetsolid中的颜色是阴影的颜色也可以设置角度等等 -->
<item
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0FFFFFFF"
android:startColor="#0FFFFFFF" />
<corners
android:bottomLeftRadius="6dip"
android:bottomRightRadius="6dip"
android:topLeftRadius="6dip"
android:topRightRadius="6dip" />
</shape>
</item>
<!-- 背景部分 -->
<!-- 形象的表达bottom代表背景部分在上边缘超出阴影的高度right代表背景部分在左边超出阴影的宽度相对应的offset -->
<item
android:left="3dp"
android:top="3dp"
android:right="3dp"
android:bottom="5dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#CFFFFFFF"
android:startColor="#CFFFFFFF" />
<corners
android:bottomLeftRadius="6dip"
android:bottomRightRadius="6dip"
android:topLeftRadius="6dip"
android:topRightRadius="6dip" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#ff000000"
android:pathData="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z"/>
</vector>

View File

@@ -1,259 +1,249 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<!-- 顶部Toolbar首屏核心同步加载保留原有ASupportToolbar --> <cc.winboll.studio.libaes.views.ASupportToolbar
<cc.winboll.studio.libaes.views.ASupportToolbar android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="@dimen/toolbar_height"
android:layout_height="@dimen/toolbar_height" android:id="@+id/toolbar"
android:id="@+id/toolbar" android:gravity="center_vertical"
android:gravity="center_vertical" style="@style/DefaultAToolbar"/>
style="@style/DefaultAToolbar"/>
<!-- 主内容区(优化层级,减少冗余RelativeLayout --> <RelativeLayout
<RelativeLayout android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="0dp"
android:layout_height="0dp" android:layout_weight="1.0">
android:layout_weight="1.0">
<!-- 首屏核心容器(合并原冗余RelativeLayout,减少层级) --> <RelativeLayout
<RelativeLayout android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent"
android:layout_height="match_parent" android:id="@+id/activitymainRelativeLayout1">
android:id="@+id/activitymainRelativeLayout1">
<!-- 1. 背景视图(首屏核心,同步加载,保留原有) --> <LinearLayout
<cc.winboll.studio.powerbell.views.BackgroundView android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/fragmentmainviewBackgroundView1"/> android:id="@+id/ll_backgroundview">
<!-- 2. 功能控件容器(首屏核心,同步加载,保留原有结构) --> </LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 服务总开关布局 --> <LinearLayout
<LinearLayout android:orientation="vertical"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent">
android:layout_height="wrap_content"
android:id="@+id/fragmentmainviewLinearLayout3"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp">
<LinearLayout <LinearLayout
android:orientation="horizontal" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:id="@+id/fragmentmainviewLinearLayout3"
android:background="@drawable/bg_frame"> android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp">
<Switch <LinearLayout
android:layout_width="0dp" android:orientation="horizontal"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:id="@+id/fragmentandroidviewSwitch1" android:layout_height="wrap_content"
android:padding="10dp" android:gravity="center_vertical"
android:layout_weight="1.0" android:background="@drawable/bg_frame">
android:textSize="@dimen/text_title_size"/>
</LinearLayout> <Switch
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewSwitch1"
android:padding="10dp"
android:layout_weight="1.0"
android:textSize="@dimen/text_title_size"/>
</LinearLayout> </LinearLayout>
<!-- 电量控制核心布局SeekBar+图标) --> </LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<LinearLayout <LinearLayout
android:orientation="horizontal" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_weight="1.0"> android:layout_weight="1.0"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<!-- 耗电提醒布局 --> <LinearLayout
<LinearLayout android:orientation="horizontal"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_width="50dp" android:layout_height="match_parent"
android:layout_height="match_parent" android:layout_weight="1.0">
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:id="@+id/fragmentmainviewLinearLayout1">
<ImageView <LinearLayout
android:layout_width="36dp" android:orientation="vertical"
android:layout_height="36dp" android:layout_width="50dp"
android:background="@drawable/usege" android:layout_height="match_parent"
android:layout_gravity="center_horizontal" android:layout_marginTop="20dp"
android:layout_marginTop="10dp"/> android:layout_marginBottom="20dp"
android:id="@+id/fragmentmainviewLinearLayout1">
<CheckBox <ImageView
android:layout_width="wrap_content" android:layout_width="36dp"
android:layout_height="wrap_content" android:layout_height="36dp"
android:layout_gravity="center_horizontal" android:background="@drawable/usege"
android:layout_marginTop="10dp" android:layout_gravity="center_horizontal"
android:id="@+id/fragmentmainviewCheckBox2"/> android:layout_marginTop="10dp"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar <CheckBox
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar2" android:layout_gravity="center_horizontal"
android:progressTint="@color/colorUsege" android:layout_marginTop="10dp"
android:progressBackgroundTint="@color/colorUsege" android:id="@+id/fragmentmainviewCheckBox2"/>
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout> <cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar2"
android:progressTint="@color/colorUsege"
android:progressBackgroundTint="@color/colorUsege"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
<!-- 耗电提醒数值+图标 --> </LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView <LinearLayout
android:layout_width="match_parent" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_width="0dp"
android:text="100%" android:layout_height="match_parent"
android:textSize="@dimen/text_title_size" android:layout_weight="1.0">
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView3"
android:gravity="center_horizontal"/>
<ImageView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView2" android:text="100%"
android:layout_weight="1.0"/> android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView3"
android:gravity="center_horizontal"/>
</LinearLayout> <ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView2"
android:layout_weight="1.0"/>
<!-- 当前电量数值+图标 --> </LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView <LinearLayout
android:layout_width="match_parent" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_width="0dp"
android:text="100%" android:layout_height="match_parent"
android:textSize="@dimen/text_title_size" android:layout_weight="1.0">
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView4"/>
<ImageView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView1" android:text="100%"
android:layout_weight="1.0"/> android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView4"/>
</LinearLayout> <ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView1"
android:layout_weight="1.0"/>
<!-- 充电提醒数值+图标 --> </LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView <LinearLayout
android:layout_width="match_parent" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_width="0dp"
android:text="100%" android:layout_height="match_parent"
android:textSize="@dimen/text_title_size" android:layout_weight="1.0">
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView2"
android:gravity="center_horizontal"/>
<ImageView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView3" android:text="100%"
android:layout_weight="1.0"/> android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView2"
android:gravity="center_horizontal"/>
</LinearLayout> <ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView3"
android:layout_weight="1.0"/>
<!-- 充电提醒布局 --> </LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:id="@+id/fragmentmainviewLinearLayout2">
<ImageView <LinearLayout
android:layout_width="36dp" android:orientation="vertical"
android:layout_height="36dp" android:layout_width="50dp"
android:background="@drawable/charge" android:layout_height="match_parent"
android:layout_gravity="center_horizontal" android:layout_marginBottom="20dp"
android:layout_marginTop="10dp"/> android:layout_marginTop="20dp"
android:id="@+id/fragmentmainviewLinearLayout2">
<CheckBox <ImageView
android:layout_width="wrap_content" android:layout_width="36dp"
android:layout_height="wrap_content" android:layout_height="36dp"
android:layout_gravity="center_horizontal" android:background="@drawable/charge"
android:layout_marginTop="10dp" android:layout_gravity="center_horizontal"
android:id="@+id/fragmentmainviewCheckBox1"/> android:layout_marginTop="10dp"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar <CheckBox
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar1" android:layout_gravity="center_horizontal"
android:progressTint="@color/colorCharge" android:layout_marginTop="10dp"
android:progressBackgroundTint="@color/colorCharge" android:id="@+id/fragmentmainviewCheckBox1"/>
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout> <cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar1"
android:progressTint="@color/colorCharge"
android:progressBackgroundTint="@color/colorCharge"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Tips文本 --> </LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_width="match_parent">
android:text="Tips"
android:textSize="@dimen/text_content_size"
android:id="@+id/fragmentandroidviewTextView1"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout> <TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tips"
android:textSize="@dimen/text_content_size"
android:id="@+id/fragmentandroidviewTextView1"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout> </LinearLayout>
<!-- 3. 广告视图关键优化→用ViewStub延迟加载替代原直接加载的ADsBannerView --> </LinearLayout>
<!-- 首次启动仅占位1px不inflate真实广告视图减少首次耗时 -->
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/stub_ads_banner"
android:layout_alignParentBottom="true"
android:layout="@layout/view_ads_banner"/> <!-- 广告视图独立布局文件 -->
</RelativeLayout> <ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/stub_ads_banner"
android:layout_alignParentBottom="true"
android:layout="@layout/view_ads_banner"/>
</RelativeLayout> </RelativeLayout>
</RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0C6BBF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ll_backgroundview">
</LinearLayout>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#AF4FDA4E">
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main"
android:id="@+id/btn_main_activity"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestCropImage"
android:id="@+id/btn_test_cropimage"/>
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
</LinearLayout>

View File

@@ -6,7 +6,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<cc.winboll.studio.libaes.views.AToolbar <cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height" android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar" android:id="@+id/toolbar"

View File

@@ -14,17 +14,46 @@
style="@style/DefaultAToolbar"/> style="@style/DefaultAToolbar"/>
<LinearLayout <LinearLayout
android:orientation="horizontal" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="right"> android:gravity="right">
<Button <LinearLayout
android:layout_width="wrap_content" android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用电提醒启用时的TTS贴心服务"
android:onClick="onEnableUsePowerTts"
android:id="@+id/activitysettingsCheckBox1"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="充电提醒启用时的TTS贴心服务"
android:onClick="onEnableChargeTts"
android:id="@+id/activitysettingsCheckBox2"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="CheckPermission" android:gravity="right">
android:padding="10dp"
android:onClick="onCheckPermission"/> <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="检查TTS语音悬浮窗权限"
android:padding="10dp"
android:onClick="onCheckTTSDrawOverlaysPermission"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -33,5 +62,20 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.powerbell.views.BatteryStyleView
android:id="@+id/battery_style_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:batteryPreviewColor="@color/colorPrimary"
app:previewBatteryValue="100"
app:defaultSelectedStyle="zebra_style"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<RadioGroup
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rg_battery_style"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:spacing="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="4dp">
<RadioButton
android:id="@+id/rb_energy_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/energy_style"
android:textSize="14sp"
android:buttonTint="@color/colorPrimary"
android:textColor="@color/colorPrimary"/>
<RelativeLayout
android:id="@+id/rl_energy_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="1dp"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="4dp">
<RadioButton
android:id="@+id/rb_zebra_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/zebra_style"
android:textSize="14sp"
android:buttonTint="@color/colorPrimary"
android:textColor="@color/colorPrimary"/>
<RelativeLayout
android:id="@+id/rl_zebra_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="1dp"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="4dp">
<RadioButton
android:id="@+id/rb_point_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/point_style"
android:textSize="14sp"
android:buttonTint="@color/colorPrimary"
android:textColor="@color/colorPrimary"/>
<RelativeLayout
android:id="@+id/rl_point_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="1dp"
android:layout_weight="1.0"/>
</LinearLayout>
</RadioGroup>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_frame_white"
android:layout_gravity="center_vertical|center_horizontal"
android:id="@+id/viewttsbackLinearLayout1">
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="#FF000000"
android:textStyle="bold"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:textAllCaps="false"
android:background="@drawable/speaker"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click To Stop TTS Play"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#FF000000"/>
</LinearLayout>
</LinearLayout>

View File

@@ -4,6 +4,10 @@
<item <item
android:id="@+id/action_unittestactivity" android:id="@+id/action_unittestactivity"
android:title="@string/item_mainunittestactivity"/> android:title="MainUnitTestActivity"/>
<item
android:id="@+id/action_unittest2activity"
android:title="MainUnitTest2Activity"/>
</menu> </menu>

View File

@@ -22,4 +22,5 @@
<string name="subtitle_activity_pixelpicker">背景像素拾取</string> <string name="subtitle_activity_pixelpicker">背景像素拾取</string>
<string name="subtitle_activity_about">关于应用</string> <string name="subtitle_activity_about">关于应用</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;向右滑动100%可以清理电量记录。&gt;&gt;&gt;</string> <string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;向右滑动100%可以清理电量记录。&gt;&gt;&gt;</string>
<string name="msg_no_battery_record">无电池记录数据</string>
</resources> </resources>

View File

@@ -1,3 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- BatteryStyleView 自定义属性 -->
<declare-styleable name="BatteryStyleView">
<attr name="batteryPreviewColor" format="color"/>
<attr name="previewBatteryValue" format="integer"/>
<attr name="defaultSelectedStyle" format="integer">
<enum name="zebra_style" value="0"/>
<enum name="energy_style" value="1"/>
</attr>
</declare-styleable>
</resources> </resources>

View File

@@ -31,6 +31,10 @@
<string name="subtitle_activity_pixelpicker">Pixel Picker</string> <string name="subtitle_activity_pixelpicker">Pixel Picker</string>
<string name="subtitle_activity_about">About The APP</string> <string name="subtitle_activity_about">About The APP</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;Seek 100% Right Is Clean Record.&gt;&gt;&gt;</string> <string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;Seek 100% Right Is Clean Record.&gt;&gt;&gt;</string>
<string name="msg_no_battery_record">No Battery Record</string>
<string name="zebra_style">Zebra Style</string>
<string name="energy_style">Energy Style</string>
<string name="point_style">Point Style</string>
<!-- 权限申请相关字符串(统一管理,避免硬编码) --> <!-- 权限申请相关字符串(统一管理,避免硬编码) -->
<string name="permission_title">权限申请</string> <string name="permission_title">权限申请</string>