Compare commits
24 Commits
powerbell-
...
powerbell-
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c6d269caf | |||
| 2f7f6b62fd | |||
| db096e716c | |||
| 1ce0a86b53 | |||
| 1e04bd463f | |||
| 7c0e723227 | |||
| accc276fd8 | |||
| 16e581802f | |||
| 3048963fa9 | |||
| 34082c49e9 | |||
| eff1822ee5 | |||
| 4e84ff493b | |||
| c2def0bb3b | |||
| a086a47b2d | |||
| c38b392e39 | |||
| 0f736c2007 | |||
| 26b86cd715 | |||
| 7888696e65 | |||
| 8609c2f784 | |||
| 863b743330 | |||
| 61b7afa4b5 | |||
| 8e4c7a6832 | |||
| d29068b029 | |||
| 51963f8e0f |
@@ -83,11 +83,11 @@ dependencies {
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
// 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'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.7'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
//api fileTree(dir: 'libs', include: ['*.aar'])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon Dec 22 10:04:49 HKT 2025
|
||||
stageCount=19
|
||||
#Tue Dec 23 21:02:16 HKT 2025
|
||||
stageCount=28
|
||||
libraryProject=
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.18
|
||||
publishVersion=15.14.27
|
||||
buildCount=0
|
||||
baseBetaVersion=15.14.19
|
||||
baseBetaVersion=15.14.28
|
||||
|
||||
@@ -4,74 +4,49 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.powerbell">
|
||||
|
||||
<!-- 运行前台服务 -->
|
||||
<!-- 前台服务权限 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<!-- 运行“specialUse”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
|
||||
<!-- 开机启动 -->
|
||||
<!-- 系统事件权限 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<!-- 显示通知 -->
|
||||
<!-- 通知权限 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- PACKAGE_USAGE_STATS -->
|
||||
<!-- 应用统计与查询权限 -->
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
|
||||
|
||||
<!-- BATTERY_STATS -->
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
|
||||
<!-- 计算应用存储空间 -->
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 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.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
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.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
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false"/>
|
||||
|
||||
<!-- 应用查询 -->
|
||||
<queries>
|
||||
|
||||
<package android:name="com.miui.securitycenter"/>
|
||||
|
||||
</queries>
|
||||
|
||||
<application
|
||||
@@ -87,18 +62,14 @@
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<!-- 主活动 -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.CrashActivity"
|
||||
android:exported="false"/>
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<!-- 活动别名(启动器) -->
|
||||
<activity-alias
|
||||
android:name=".MainActivityEN1"
|
||||
android:targetActivity=".MainActivity"
|
||||
@@ -106,19 +77,13 @@
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmainen1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
@@ -128,19 +93,13 @@
|
||||
android:label="@string/app_name_cn1"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
@@ -150,21 +109,20 @@
|
||||
android:label="@string/app_name_cn2"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn2"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<!-- 功能活动 -->
|
||||
<activity
|
||||
android:name=".activities.CrashActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ClearRecordActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
@@ -176,75 +134,17 @@
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:mimeType="image/jpeg"/>
|
||||
|
||||
<data android:mimeType="image/jpg"/>
|
||||
|
||||
<data android:mimeType="image/png"/>
|
||||
|
||||
<data android:mimeType="image/webp"/>
|
||||
|
||||
<data android:mimeType="image/*"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
<action android:name="android.intent.action.POWER_CONNECTED"/>
|
||||
|
||||
<action android:name="android.intent.action.USER_PRESENT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".controlcenterservice"
|
||||
android:foregroundServiceType="dataSync">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="后台核心功能运行、持续保活"/>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".assistantservice">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="辅助核心功能运行"/>
|
||||
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.BatteryReporterActivity"
|
||||
android:exported="false"/>
|
||||
@@ -269,27 +169,69 @@
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
|
||||
android:exported="false"/>
|
||||
|
||||
<!-- 第三方活动 -->
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true"/>
|
||||
|
||||
<!-- 广播接收器 -->
|
||||
<receiver
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.POWER_CONNECTED"/>
|
||||
<action android:name="android.intent.action.USER_PRESENT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 服务 -->
|
||||
<service
|
||||
android:name=".services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".controlcenterservice"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="后台核心功能运行、持续保活"/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".assistantservice">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="辅助核心功能运行"/>
|
||||
</service>
|
||||
|
||||
<!-- 内容提供者 -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"/>
|
||||
<!-- 元数据 -->
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.content.ComponentCallbacks2;
|
||||
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.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 应用全局入口类(适配Android API 30,基于Java 7编写)
|
||||
* 核心策略:极致强制缓存 - 无论内存紧张程度,永不自动清理任何缓存(Bitmap/视图控件/路径记录)
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
// ===================== 常量定义区 =====================
|
||||
// ===================== 常量定义区(按功能分类排序) =====================
|
||||
public static final String TAG = "App";
|
||||
|
||||
// 组件跳转常量
|
||||
@@ -36,48 +30,33 @@ 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_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 final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
|
||||
|
||||
// 定时任务间隔常量(分钟)
|
||||
//private static final long TIMER_INTERVAL_MINUTES = 1;
|
||||
|
||||
// ===================== 静态属性区 =====================
|
||||
// ===================== 静态属性区(按工具类优先级排序) =====================
|
||||
// 数据配置工具
|
||||
private static AppConfigUtils sAppConfigUtils;
|
||||
private static AppCacheUtils sAppCacheUtils;
|
||||
// 全局Bitmap缓存工具
|
||||
public static BitmapCacheUtils sBitmapCacheUtils;
|
||||
// 全局视图控件缓存工具
|
||||
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
|
||||
// 临时文件夹路径
|
||||
private static String sTempDirPath = "";
|
||||
|
||||
// 定时任务静态属性(全局唯一)
|
||||
// private static Handler sTimerHandler;
|
||||
// private static Runnable sTimerRunnable;
|
||||
// private static boolean sIsTimerRunning = false;
|
||||
//
|
||||
// ===================== 成员属性区 =====================
|
||||
// 全局Bitmap缓存工具(极致强制保持:一旦初始化,永不销毁)
|
||||
public static BitmapCacheUtils sBitmapCacheUtils;
|
||||
|
||||
// 全局视图控件缓存工具(极致强制保持:一旦初始化,永不销毁)
|
||||
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
|
||||
|
||||
// ===================== 成员属性区(按生命周期关联度排序) =====================
|
||||
// 全局广播接收器
|
||||
private GlobalApplicationReceiver mGlobalReceiver;
|
||||
|
||||
// 通知管理工具
|
||||
private NotificationManagerUtils mNotificationManager;
|
||||
|
||||
// ===================== 公共方法区 =====================
|
||||
/**
|
||||
* 获取临时文件夹路径
|
||||
*/
|
||||
public static String getTempDirPath() {
|
||||
return sTempDirPath;
|
||||
}
|
||||
|
||||
// ===================== 公共静态方法区(工具类实例获取) =====================
|
||||
/**
|
||||
* 获取应用配置工具实例
|
||||
*/
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
LogUtils.d(TAG, "getAppConfigUtils() 调用,传入Context:" + context.getClass().getSimpleName());
|
||||
LogUtils.d(TAG, "getAppConfigUtils() 调用,传入Context类型:" + (context != null ? context.getClass().getSimpleName() : "null"));
|
||||
if (sAppConfigUtils == null) {
|
||||
sAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "getAppConfigUtils():AppConfigUtils实例已初始化");
|
||||
@@ -89,7 +68,7 @@ public class App extends GlobalApplication {
|
||||
* 获取应用缓存工具实例
|
||||
*/
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
LogUtils.d(TAG, "getAppCacheUtils() 调用,传入Context:" + context.getClass().getSimpleName());
|
||||
LogUtils.d(TAG, "getAppCacheUtils() 调用,传入Context类型:" + (context != null ? context.getClass().getSimpleName() : "null"));
|
||||
if (sAppCacheUtils == null) {
|
||||
sAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "getAppCacheUtils():AppCacheUtils实例已初始化");
|
||||
@@ -97,15 +76,40 @@ public class App extends GlobalApplication {
|
||||
return sAppCacheUtils;
|
||||
}
|
||||
|
||||
// ===================== 公共成员方法区(业务功能) =====================
|
||||
/**
|
||||
* 清除电池历史数据
|
||||
*/
|
||||
public void clearBatteryHistory() {
|
||||
LogUtils.d(TAG, "clearBatteryHistory() 调用");
|
||||
sAppCacheUtils.clearBatteryHistory();
|
||||
if (sAppCacheUtils != null) {
|
||||
sAppCacheUtils.clearBatteryHistory();
|
||||
LogUtils.d(TAG, "clearBatteryHistory():电池历史数据已清除");
|
||||
} else {
|
||||
LogUtils.w(TAG, "clearBatteryHistory():AppCacheUtils未初始化,清除失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 生命周期方法区 =====================
|
||||
/**
|
||||
* 手动清理所有缓存(带严格权限控制,仅主动调用生效)
|
||||
* 极致强制缓存策略下,仅提供手动清理入口,永不自动调用
|
||||
*/
|
||||
public static void manualClearAllCache() {
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存调用(极致强制缓存策略下,需谨慎使用)");
|
||||
// 清理Bitmap缓存
|
||||
if (sBitmapCacheUtils != null) {
|
||||
sBitmapCacheUtils.clearAllCache();
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存已手动清理");
|
||||
}
|
||||
// 清理视图控件缓存(仅清除静态引用,不销毁实例)
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存实例保持,仅清除静态引用");
|
||||
sMemoryCachedBackgroundView = null;
|
||||
}
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存完成(部分缓存实例仍可能保留在内存中)");
|
||||
}
|
||||
|
||||
// ===================== 生命周期方法区(按执行顺序排序) =====================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@@ -117,56 +121,52 @@ public class App extends GlobalApplication {
|
||||
|
||||
// 初始化基础工具
|
||||
initBaseTools();
|
||||
// 初始化临时文件夹
|
||||
initTempDir();
|
||||
// 初始化工具类实例
|
||||
// 初始化工具类实例(核心:极致强制缓存,永不销毁)
|
||||
initUtils();
|
||||
// 初始化广播接收器
|
||||
initReceiver();
|
||||
// 启动定时任务
|
||||
//initTimerTask();
|
||||
|
||||
LogUtils.d(TAG, "onCreate() 应用初始化完成");
|
||||
LogUtils.d(TAG, "onCreate() 应用初始化完成,极致强制缓存策略已启用");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放资源");
|
||||
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放非缓存资源");
|
||||
|
||||
// 释放Toast工具
|
||||
ToastUtils.release();
|
||||
LogUtils.d(TAG, "onTerminate():Toast工具已释放");
|
||||
// 释放通知工具
|
||||
releaseNotificationManager();
|
||||
// 停止定时任务
|
||||
//stopTimerTask();
|
||||
// 释放广播接收器
|
||||
releaseReceiver();
|
||||
|
||||
LogUtils.d(TAG, "onTerminate() 应用资源释放完成");
|
||||
// 核心修改:应用终止时也不清理缓存,保持静态实例
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 应用终止,极致强制缓存策略生效,不清理任何缓存");
|
||||
|
||||
LogUtils.d(TAG, "onTerminate() 非缓存资源释放完成,缓存实例保持");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
LogUtils.d(TAG, "onTrimMemory() 调用,内存等级level:" + level);
|
||||
sMemoryCachedBackgroundView.clearAllCache();
|
||||
sMemoryCachedBackgroundView.getLastInstance(this);
|
||||
//
|
||||
// // 初始化通知工具(若未初始化)
|
||||
// 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缓存已启用");
|
||||
// }
|
||||
// 极致强制缓存:禁止任何缓存清理操作,仅记录日志
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onTrimMemory() 调用,内存等级level:" + level + ",极致强制保持所有缓存");
|
||||
// 记录详细缓存状态,不执行任何清理
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
// ===================== 私有初始化方法区 =====================
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
// 极致强制缓存:低内存时也不清理任何缓存
|
||||
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onLowMemory() 调用,极致强制保持所有缓存");
|
||||
// 记录详细缓存状态,不执行任何清理
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
// ===================== 私有初始化方法区(按初始化顺序排序) =====================
|
||||
/**
|
||||
* 初始化基础工具(Activity管理、Toast)
|
||||
*/
|
||||
@@ -178,30 +178,23 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化临时文件夹(适配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() {
|
||||
LogUtils.d(TAG, "initUtils() 开始初始化工具类");
|
||||
LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用极致强制缓存策略");
|
||||
sAppConfigUtils = getAppConfigUtils(this);
|
||||
sAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
// 极致强制初始化Bitmap缓存工具(必初始化,永不销毁)
|
||||
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "initUtils() Bitmap缓存工具已初始化(极致强制保持,永不销毁)");
|
||||
|
||||
// 极致强制初始化视图控件缓存工具(必初始化,永不销毁)
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
|
||||
LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(极致强制保持,永不销毁)");
|
||||
|
||||
mNotificationManager = new NotificationManagerUtils(this);
|
||||
LogUtils.d(TAG, "initUtils() 工具类初始化完成");
|
||||
LogUtils.d(TAG, "initUtils() 工具类初始化完成,极致强制缓存策略已生效");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,68 +207,21 @@ public class App extends GlobalApplication {
|
||||
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成");
|
||||
}
|
||||
|
||||
// ===================== 私有释放方法区(按资源重要性排序) =====================
|
||||
/**
|
||||
* 初始化定时任务(全局唯一实例)
|
||||
* 释放广播接收器资源
|
||||
*/
|
||||
// private void initTimerTask() {
|
||||
// LogUtils.d(TAG, "initTimerTask() 开始初始化定时任务,当前运行状态:" + sIsTimerRunning);
|
||||
//
|
||||
// // 已运行则直接返回
|
||||
// if (sIsTimerRunning) {
|
||||
// LogUtils.d(TAG, "initTimerTask() 定时任务已在运行,无需重复启动");
|
||||
// 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 releaseReceiver() {
|
||||
LogUtils.d(TAG, "releaseReceiver() 开始释放广播接收器");
|
||||
if (mGlobalReceiver != null) {
|
||||
mGlobalReceiver.unregisterAction();
|
||||
mGlobalReceiver = null;
|
||||
LogUtils.d(TAG, "releaseReceiver() 广播接收器资源已释放");
|
||||
} else {
|
||||
LogUtils.d(TAG, "releaseReceiver() 广播接收器未初始化,无需释放");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 私有工具方法区 =====================
|
||||
/**
|
||||
* 停止定时任务
|
||||
*/
|
||||
// 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() 定时任务未初始化,无需停止");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
/**
|
||||
* 释放通知管理工具资源
|
||||
*/
|
||||
@@ -290,54 +236,31 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 私有工具方法区(辅助功能) =====================
|
||||
/**
|
||||
* 发送内存紧张通知
|
||||
* 记录详细缓存状态(用于调试,监控极致强制缓存效果)
|
||||
*/
|
||||
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) {
|
||||
LogUtils.d(TAG, "getTrimMemoryLevelDesc() 调用,传入level:" + level);
|
||||
String desc;
|
||||
switch (level) {
|
||||
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
|
||||
desc = "TRIM_MEMORY_COMPLETE(应用内存完全紧张)";
|
||||
break;
|
||||
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
|
||||
desc = "MODERATE(中等内存紧张)";
|
||||
break;
|
||||
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
|
||||
desc = "BACKGROUND(应用进入后台)";
|
||||
break;
|
||||
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
|
||||
desc = "BACKGROUND(应用UI隐藏)";
|
||||
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;
|
||||
private void logDetailedCacheStatus() {
|
||||
LogUtils.d(TAG, "logDetailedCacheStatus() 开始记录详细缓存状态");
|
||||
// Bitmap缓存状态
|
||||
if (sBitmapCacheUtils != null) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存工具实例有效(极致强制保持)");
|
||||
// 假设BitmapCacheUtils有获取缓存数量的方法
|
||||
try {
|
||||
int cacheCount = sBitmapCacheUtils.getCacheCount();
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量:" + cacheCount);
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量获取失败(不影响缓存),异常信息:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "getTrimMemoryLevelDesc() 内存等级描述结果:" + desc);
|
||||
return desc;
|
||||
// 视图控件缓存状态
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存工具实例有效(极致强制保持)");
|
||||
// 记录视图实例总数
|
||||
int viewInstanceCount = MemoryCachedBackgroundView.getInstanceCount();
|
||||
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件实例总数:" + viewInstanceCount);
|
||||
}
|
||||
LogUtils.d(TAG, "logDetailedCacheStatus() 详细缓存状态记录完成,所有缓存均极致强制保持");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
|
||||
@@ -21,18 +21,19 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
|
||||
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.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
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.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
@@ -317,7 +318,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
|
||||
return;
|
||||
}
|
||||
int oldColor = previewBean.getPixelColor();
|
||||
previewBean.setPixelColor(0xFF000000);
|
||||
previewBean.setPixelColor(ImageUtils.getColorAccent(BackgroundSettingsActivity.this));
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@@ -36,111 +31,87 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 电池报告页面,统计应用24小时运行时长与电池消耗情况
|
||||
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/22 13:21
|
||||
*/
|
||||
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
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 EditText etSearch;
|
||||
// 数据与适配器
|
||||
private BatteryReportAdapter adapter;
|
||||
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
|
||||
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
|
||||
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 Map<String, Long> appRunTimeCache = new HashMap<String, Long>();
|
||||
private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_battery_report);
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始");
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化PackageManager
|
||||
mPackageManager = getPackageManager();
|
||||
|
||||
// 权限检查(Java7 传统条件判断)
|
||||
if (!hasUsageStatsPermission(this)) {
|
||||
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
|
||||
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
|
||||
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限,引导用户开启");
|
||||
return;
|
||||
}
|
||||
|
||||
etSearch = (EditText) findViewById(R.id.et_search);
|
||||
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
|
||||
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
// 初始化流程:新增“加载24小时累计耗电”步骤
|
||||
// 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
|
||||
loadAllAppPackage();
|
||||
preCacheAllAppNames();
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateInitial24hTotalConsumption(); // 初始化时计算24小时累计耗电
|
||||
calculateInitial24hTotalConsumption();
|
||||
filteredList.addAll(dataList);
|
||||
|
||||
// 初始化适配器
|
||||
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
|
||||
rvBatteryReport.setAdapter(adapter);
|
||||
LogUtils.d(TAG, "【onCreate】适配器初始化完成,数据量:" + filteredList.size());
|
||||
|
||||
// 搜索监听(不变)
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
// 绑定搜索监听
|
||||
bindSearchListener();
|
||||
// 注册电池广播
|
||||
registerBatteryReceiver();
|
||||
|
||||
@Override
|
||||
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));
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,33 +120,133 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
// Java7 显式非空判断
|
||||
if (batteryReceiver != null) {
|
||||
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) {
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化:" + s.toString());
|
||||
filterAppsByPackageAndName(s.toString());
|
||||
}
|
||||
|
||||
@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() {
|
||||
List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
dataList.clear();
|
||||
|
||||
LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用");
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用");
|
||||
|
||||
for (ApplicationInfo appInfo : appList) {
|
||||
String packageName = appInfo.packageName;
|
||||
// 初始化:单次耗电(consumption)=0,累计耗电(totalConsumption)=0,运行时长=0
|
||||
// 初始化:单次耗电=0,累计耗电=0,运行时长=0
|
||||
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名。");
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名");
|
||||
}
|
||||
|
||||
/**
|
||||
* 预缓存应用名称(逻辑不变)
|
||||
* 预缓存所有应用名称,减少PackageManager重复调用
|
||||
*/
|
||||
private void preCacheAllAppNames() {
|
||||
packageToAppNameCache.clear();
|
||||
LogUtils.d(TAG, "开始预缓存包名-应用名称映射");
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
|
||||
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
@@ -183,48 +254,78 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
packageToAppNameCache.put(packageName, appName);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过包名获取应用名称(逻辑不变)
|
||||
* 通过包名获取应用名称,带异常处理
|
||||
* @param packageName 应用包名
|
||||
* @return 应用名称,获取失败返回包名
|
||||
*/
|
||||
private String getAppNameByPackage(String packageName) {
|
||||
try {
|
||||
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
|
||||
return mPackageManager.getApplicationLabel(appInfo).toString();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.e(TAG, "包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
return packageName;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运行时长到模型(逻辑不变)
|
||||
* 更新运行时长到数据模型
|
||||
*/
|
||||
private void updateAppRunTimeToModel() {
|
||||
int nCount = 0;
|
||||
int updateCount = 0;
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long runTime;
|
||||
if (appRunTimeCache.containsKey(packageName)) {
|
||||
runTime = appRunTimeCache.get(packageName);
|
||||
LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName));
|
||||
nCount++;
|
||||
} else {
|
||||
runTime = 0L;
|
||||
}
|
||||
Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
|
||||
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, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount));
|
||||
LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成,数据量:" + dataList.size() + ",更新运行时长应用数:" + updateCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【新增】初始化时计算24小时累计耗电(赋值给totalConsumption)
|
||||
* 获取应用24小时运行时长
|
||||
* @return 应用包名-运行时长(ms)映射
|
||||
*/
|
||||
private Map<String, Long> getAppRunTime() {
|
||||
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 - 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小时消耗
|
||||
*/
|
||||
private void calculateInitial24hTotalConsumption() {
|
||||
@@ -233,23 +334,26 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
|
||||
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) {
|
||||
String packageName = model.getPackageName();
|
||||
Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
|
||||
|
||||
// 计算占比与累计耗电
|
||||
float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
|
||||
float initialTotalConsumption = batteryCapacity * ratio; // 用电池容量近似24小时总消耗
|
||||
model.setTotalConsumption(initialTotalConsumption); // 初始化累计耗电
|
||||
LogUtils.d(TAG, String.format("应用包 %s 24小时累计耗电初始化:%.1f mAh", packageName, initialTotalConsumption));
|
||||
float initialTotalConsumption = batteryCapacity * ratio;
|
||||
model.setTotalConsumption(initialTotalConsumption);
|
||||
LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化:" + initialTotalConsumption + " mAh");
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心修改】计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption)
|
||||
* 计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption)
|
||||
* @param dropPercent 电池下降百分比
|
||||
* @param runTimeMap 应用运行时长映射
|
||||
*/
|
||||
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
|
||||
long totalSingleRunTime = 0;
|
||||
@@ -257,25 +361,26 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
|
||||
totalSingleRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长:" + formatRunTime(totalSingleRunTime));
|
||||
|
||||
// 2. 遍历计算每个应用的“单次耗电”并“累加至累计”
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
|
||||
|
||||
// 步骤1:计算本次单次耗电(赋值给consumption)
|
||||
// 步骤1:计算本次单次耗电
|
||||
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
|
||||
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; // 单次消耗
|
||||
model.setConsumption(singleConsumption); // 存储单次耗电
|
||||
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
|
||||
model.setConsumption(singleConsumption);
|
||||
|
||||
// 步骤2:累加单次耗电到累计耗电(totalConsumption = 原有累计 + 本次单次)
|
||||
// 步骤2:累加单次耗电到累计耗电
|
||||
float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
|
||||
model.setTotalConsumption(newTotalConsumption); // 更新累计耗电
|
||||
model.setTotalConsumption(newTotalConsumption);
|
||||
|
||||
// 同步运行时长
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -289,69 +394,43 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
|
||||
// 4. 重新应用过滤并刷新列表
|
||||
filterAppsByPackageAndName(etSearch.getText().toString());
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成,列表已刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 双维度过滤(逻辑不变)
|
||||
* 双维度过滤(包名+应用名)
|
||||
* @param keyword 搜索关键词
|
||||
*/
|
||||
private void filterAppsByPackageAndName(String keyword) {
|
||||
filteredList.clear();
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
filteredList.addAll(dataList);
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空,显示全部应用,数量:" + filteredList.size());
|
||||
} else {
|
||||
String lowerKeyword = keyword.toLowerCase();
|
||||
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
String packageNameLower = packageName.toLowerCase();
|
||||
String appName = packageToAppNameCache.get(packageName);
|
||||
String appNameLower = appName.toLowerCase();
|
||||
|
||||
boolean isMatched = packageNameLower.contains(lowerKeyword)
|
||||
|| appNameLower.contains(lowerKeyword);
|
||||
boolean isMatched = packageNameLower.contains(lowerKeyword)
|
||||
|| appNameLower.contains(lowerKeyword);
|
||||
|
||||
if (isMatched) {
|
||||
filteredList.add(model);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词:" + keyword + ",匹配应用数量:" + filteredList.size());
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 获取应用运行时长(逻辑不变,返回24小时运行时长)
|
||||
*/
|
||||
private Map<String, Long> getAppRunTime() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化运行时长(逻辑不变)
|
||||
* 格式化运行时长
|
||||
* @param runTimeMs 运行时长(ms)
|
||||
* @return 格式化后的运行时长字符串
|
||||
*/
|
||||
private String formatRunTime(long runTimeMs) {
|
||||
if (runTimeMs <= 0) {
|
||||
@@ -371,66 +450,47 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:数据模型 =========================
|
||||
/**
|
||||
* 权限检查(逻辑不变)
|
||||
*/
|
||||
private boolean hasUsageStatsPermission(Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
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小时初始化值+后续单次累加,显示用)
|
||||
* 应用电池数据模型
|
||||
* - consumption:单次耗电(两次电池广播间的消耗)
|
||||
* - totalConsumption:累计耗电(24小时初始化值+后续单次累加)
|
||||
* - runTime:运行时长(ms)
|
||||
* - packageName:应用包名
|
||||
*/
|
||||
public static class AppBatteryModel {
|
||||
private String packageName; // 应用包名(核心标识)
|
||||
private float consumption; // 单次耗电(mAh,float类型)
|
||||
private float totalConsumption;// 累计耗电(mAh,显示+排序用)
|
||||
private float consumption; // 单次耗电(mAh)
|
||||
private float totalConsumption;// 累计耗电(mAh)
|
||||
private long runTime; // 运行时长(ms)
|
||||
|
||||
// Java7 显式构造:初始化单次耗电、累计耗电为0
|
||||
// Java7 显式构造
|
||||
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
|
||||
this.packageName = packageName;
|
||||
this.consumption = consumption; // 单次耗电初始为0
|
||||
this.totalConsumption = totalConsumption; // 累计耗电初始为0(后续初始化时赋值)
|
||||
this.consumption = consumption;
|
||||
this.totalConsumption = totalConsumption;
|
||||
this.runTime = runTime;
|
||||
}
|
||||
|
||||
// Getter/Setter:覆盖所有字段,确保数据操作正常
|
||||
// Getter/Setter
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public float getConsumption() {
|
||||
return consumption; // 获取单次耗电
|
||||
return consumption;
|
||||
}
|
||||
|
||||
public void setConsumption(float consumption) {
|
||||
this.consumption = consumption; // 设置单次耗电
|
||||
this.consumption = consumption;
|
||||
}
|
||||
|
||||
public float getTotalConsumption() {
|
||||
return totalConsumption; // 获取累计耗电(显示用)
|
||||
return totalConsumption;
|
||||
}
|
||||
|
||||
public void setTotalConsumption(float totalConsumption) {
|
||||
this.totalConsumption = totalConsumption; // 设置累计耗电(初始化/累加用)
|
||||
this.totalConsumption = totalConsumption;
|
||||
}
|
||||
|
||||
public long getRunTime() {
|
||||
@@ -442,8 +502,9 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:RecyclerView适配器 =========================
|
||||
/**
|
||||
* RecyclerView 适配器:仅显示累计耗电(totalConsumption),逻辑适配模型修改
|
||||
* 电池报告列表适配器,显示应用名称、累计耗电、运行时长
|
||||
*/
|
||||
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
|
||||
private Context mContext;
|
||||
@@ -451,8 +512,8 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
private PackageManager mPm;
|
||||
private Map<String, String> mPackageToNameCache;
|
||||
|
||||
// Java7 显式构造:接收名称缓存,确保显示时高效获取应用名
|
||||
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
|
||||
// Java7 显式构造
|
||||
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
|
||||
PackageManager pm, Map<String, String> packageToNameCache) {
|
||||
this.mContext = context;
|
||||
this.mDataList = dataList;
|
||||
@@ -462,18 +523,18 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
// 加载系统列表项布局(text1显示应用名,text2显示累计耗电+时长)
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
// Java7 显式非空判断:避免空指针异常
|
||||
// Java7 显式非空判断
|
||||
if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
|
||||
holder.tvAppName.setText("未知应用");
|
||||
holder.tvConsumption.setText("累计耗电:0.0 mAh | 运行时长:0秒");
|
||||
LogUtils.w(TAG, "【onBindViewHolder】数据异常,位置:" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -481,11 +542,11 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
String packageName = model.getPackageName();
|
||||
String appName = "";
|
||||
|
||||
// 优先从缓存获取应用名:减少PackageManager调用,提升性能
|
||||
// 优先从缓存获取应用名
|
||||
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
|
||||
appName = mPackageToNameCache.get(packageName);
|
||||
} else {
|
||||
// 缓存无数据时兜底查询,并同步更新缓存
|
||||
// 缓存无数据时兜底查询
|
||||
try {
|
||||
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
|
||||
appName = mPm.getApplicationLabel(appInfo).toString();
|
||||
@@ -493,45 +554,40 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
mPackageToNameCache.put(packageName, appName);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
appName = packageName; // 包名不存在时用包名兜底
|
||||
LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
appName = packageName; // 其他异常时用包名兜底
|
||||
LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 显示逻辑:仅展示累计耗电(totalConsumption),隐藏单次耗电
|
||||
// 显示逻辑:应用名称 + 累计耗电 + 运行时长
|
||||
holder.tvAppName.setText(appName);
|
||||
// 格式化运行时长 + 累计耗电(保留1位小数,提升可读性)
|
||||
String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
|
||||
String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
|
||||
model.getTotalConsumption(), runTimeStr);
|
||||
holder.tvConsumption.setText(totalConsumptionText);
|
||||
|
||||
// 显示优化:文字颜色区分(避免所有应用均标蓝,仅示例可按需修改)
|
||||
// 显示优化
|
||||
holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
|
||||
holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
|
||||
|
||||
// 调整文字大小:适配手机屏幕,提升可读性
|
||||
holder.tvAppName.setTextSize(16);
|
||||
holder.tvConsumption.setTextSize(14);
|
||||
}
|
||||
|
||||
// 获取列表长度:Java7 三元运算符判断空值,避免空指针
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mDataList == null ? 0 : mDataList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder:绑定系统布局控件,与显示逻辑对应
|
||||
* ViewHolder:绑定系统布局控件
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvAppName; // 显示应用名称
|
||||
TextView tvConsumption; // 显示累计耗电 + 运行时长
|
||||
TextView tvAppName; // 应用名称
|
||||
TextView tvConsumption; // 累计耗电 + 运行时长
|
||||
|
||||
// Java7 显式构造:绑定控件ID(系统布局固定ID:text1、text2)
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvAppName = (TextView) itemView.findViewById(android.R.id.text1);
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
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.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
@@ -17,39 +18,68 @@ import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
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 static final String TAG = "ClearRecordActivity";
|
||||
private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned.";
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
TextView mtvRecordText;
|
||||
App mApplication;
|
||||
boolean mIsShowRecordWithEnter = false;
|
||||
private TextView mtvRecordText;
|
||||
private TextView tvAOHPCTCSeekBarMSG;
|
||||
private AOHPCTCSeekBar aOHPCTCSeekBar;
|
||||
// 应用与配置
|
||||
private App mApplication;
|
||||
private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_clearrecord);
|
||||
mApplication = (App) getApplication();
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
|
||||
|
||||
// 初始化工具栏
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
// 初始化应用实例
|
||||
mApplication = (App) getApplication();
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化滑动清理控件
|
||||
initSeekBar();
|
||||
// 初始化记录显示文本
|
||||
initRecordText();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化Toolbar与显示文本组件
|
||||
*/
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -58,49 +88,76 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
}
|
||||
});
|
||||
|
||||
// 设置滑动清理控件
|
||||
//
|
||||
// 初始化发送拉动控件
|
||||
final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
|
||||
// 初始化显示文本组件
|
||||
tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
|
||||
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
|
||||
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动清理控件
|
||||
*/
|
||||
private void initSeekBar() {
|
||||
aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
|
||||
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
|
||||
aOHPCTCSeekBar.setThumbOffset(0);
|
||||
aOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
|
||||
@Override
|
||||
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();
|
||||
aOHPCTCSeekBar.setOnOHPCListener(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】电池记录清理完成,已发送更新广播");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 初始化记录显示文本,根据配置切换带换行/不带换行格式
|
||||
*/
|
||||
void initRecordText() {
|
||||
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
|
||||
if (mIsShowRecordWithEnter) {
|
||||
String szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
} else {
|
||||
String szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
}
|
||||
String szRecordText;
|
||||
|
||||
// 判空处理:避免空列表导致异常
|
||||
if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
|
||||
szRecordText = getString(R.string.msg_no_battery_record);
|
||||
LogUtils.d(TAG, "【initRecordText】无电池记录数据");
|
||||
} else {
|
||||
// 根据配置切换显示格式
|
||||
if (mIsShowRecordWithEnter) {
|
||||
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
LogUtils.d(TAG, "【initRecordText】使用带换行格式显示记录,数量:" + listBatteryInfo.size());
|
||||
} else {
|
||||
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
LogUtils.d(TAG, "【initRecordText】使用无换行格式显示记录,数量:" + 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, "【onShowRecordWithEnter】记录显示格式切换,带换行:" + mIsShowRecordWithEnter);
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/22 14:15
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
@@ -22,160 +18,191 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
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.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
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 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 AToolbar mAToolbar;
|
||||
private ImageView imageView;
|
||||
private TextView infoText;
|
||||
private ViewGroup imageContainer;
|
||||
private RelativeLayout mainLayout;
|
||||
// 图片与像素数据
|
||||
private Bitmap originalBitmap; // 原始图片Bitmap(用于像素拾取)
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
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
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pixelpicker);
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 加载传递的图片
|
||||
loadImageFromIntent();
|
||||
// 绑定图片触摸事件
|
||||
bindImageTouchListener();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "【onResume】PixelPickerActivity 恢复显示");
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
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() {
|
||||
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
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() {
|
||||
setActionBar(mAToolbar);
|
||||
mAToolbar.setSubtitle(R.string.subtitle_activity_pixelpicker);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
imageView = findViewById(R.id.imageView);
|
||||
infoText = findViewById(R.id.infoText);
|
||||
imageContainer = findViewById(R.id.imageContainer);
|
||||
|
||||
// 从Intent获取图片路径并加载
|
||||
String imagePath = getIntent().getStringExtra("imagePath");
|
||||
if (imagePath != null) {
|
||||
loadImage(imagePath);
|
||||
} else {
|
||||
infoText.setText("未找到图片路径");
|
||||
}
|
||||
|
||||
// 设置图片点击事件
|
||||
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();
|
||||
|
||||
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;
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载图片
|
||||
*/
|
||||
private void loadImage(String imagePath) {
|
||||
try {
|
||||
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);
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 从Intent中获取图片路径并加载图片
|
||||
*/
|
||||
private void loadImageFromIntent() {
|
||||
String imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||
LogUtils.d(TAG, "【loadImageFromIntent】获取到图片路径:" + imagePath);
|
||||
|
||||
if (originalBitmap != null) {
|
||||
imageView.setImageBitmap(originalBitmap);
|
||||
infoText.setText("图片已加载,点击获取像素值");
|
||||
} else {
|
||||
infoText.setText("图片加载失败");
|
||||
}
|
||||
} else {
|
||||
infoText.setText("图片文件不存在");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
infoText.setText("图片文件未找到");
|
||||
}
|
||||
}
|
||||
if (imagePath != null) {
|
||||
loadImage(imagePath);
|
||||
} else {
|
||||
infoText.setText(MSG_NO_IMAGE_PATH);
|
||||
LogUtils.w(TAG, "【loadImageFromIntent】未获取到图片路径");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示像素对话框
|
||||
*/
|
||||
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);
|
||||
/**
|
||||
* 加载指定路径的图片
|
||||
* @param imagePath 图片文件路径
|
||||
*/
|
||||
private void loadImage(String imagePath) {
|
||||
try {
|
||||
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);
|
||||
|
||||
// 设置像素颜色视图背景
|
||||
TextView colorView = dialog.findViewById(R.id.pixelColorView);
|
||||
colorView.setBackgroundColor(pixelColor);
|
||||
if (originalBitmap != null) {
|
||||
imageView.setImageBitmap(originalBitmap);
|
||||
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" +
|
||||
"ARGB: #%08X\n" +
|
||||
"实际像素位置: (%d, %d)",
|
||||
@@ -184,77 +211,129 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
Color.blue(pixelColor),
|
||||
pixelColor,
|
||||
x, y);
|
||||
infoText.setText(colorInfo);
|
||||
infoTextView.setText(colorInfo);
|
||||
LogUtils.d(TAG, "【showPixelDialog】显示像素信息:" + colorInfo);
|
||||
|
||||
// 设置确定按钮点击事件
|
||||
Button confirmButton = dialog.findViewById(R.id.confirmButton);
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
// 确定按钮点击事件
|
||||
Button confirmButton = dialog.findViewById(R.id.confirmButton);
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
// 可以在这里添加确定后的回调逻辑
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveSettings();
|
||||
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
|
||||
// 保存像素颜色到背景配置
|
||||
savePixelColor(pixelColor);
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_RECORDED, Toast.LENGTH_SHORT).show();
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "【showPixelDialog】像素对话框已显示");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 回收Bitmap资源
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 保存拾取的像素颜色到背景配置
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
*/
|
||||
private void savePixelColor(int pixelColor) {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
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);
|
||||
mainLayout.setBackgroundColor(nPixelColor);
|
||||
}
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 绑定图片容器的触摸事件,处理像素拾取逻辑
|
||||
*/
|
||||
private void bindImageTouchListener() {
|
||||
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
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取图片在窗口中的位置与尺寸
|
||||
int[] imageLocation = new int[2];
|
||||
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
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, BackgroundSettingsActivity.class);
|
||||
LogUtils.d(TAG, "【onOptionsItemSelected】点击返回菜单");
|
||||
Intent intent = new Intent(this, BackgroundSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), );
|
||||
return true;
|
||||
}
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
// Intent intent = new Intent();
|
||||
// intent.setClass(this, BackgroundSettingsActivity.class);
|
||||
// startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
|
||||
}
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
LogUtils.d(TAG, "【onBackPressed】返回键触发,页面关闭");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,37 +9,58 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
/**
|
||||
* 应用设置窗口,提供应用配置项的统一入口
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/27 14:26
|
||||
* @Describe 应用设置窗口
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "SettingsActivity";
|
||||
// 权限请求常量(为后续读取媒体图片权限预留)
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
private Toolbar mToolbar;
|
||||
// ======================== 成员变量 =========================
|
||||
private Toolbar mToolbar; // 顶部工具栏
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化开始");
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化顶部工具栏,设置导航返回与样式
|
||||
*/
|
||||
private void initToolbar() {
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
// 设置工具栏副标题与标题样式
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// 显示返回按钮
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// 绑定导航点击事件
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -47,5 +68,7 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,47 +3,73 @@ package cc.winboll.studio.powerbell.activities;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
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.utils.APPPlusUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
|
||||
/**
|
||||
* 应用快捷方式活动类,处理应用图标快捷菜单的切换请求
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/15 13:45
|
||||
* @Describe 应用快捷方式活动类
|
||||
*/
|
||||
public class ShortcutActionActivity extends Activity {
|
||||
|
||||
// ======================== 静态常量 =========================
|
||||
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) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// 处理应用级别的切换请求
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "【onCreate】ShortcutActionActivity 启动,开始处理快捷方式请求");
|
||||
|
||||
// 处理应用图标快捷菜单的切换请求
|
||||
handleSwitchRequest();
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理应用图标快捷菜单的请求
|
||||
LogUtils.d(TAG, "【onCreate】快捷方式请求处理完成,关闭活动");
|
||||
finish();
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 处理应用图标快捷菜单的请求,根据意图数据切换应用启动组件
|
||||
*/
|
||||
private void handleSwitchRequest() {
|
||||
Intent intent = getIntent();
|
||||
if (intent != null && "switchto_en1".equals(intent.getDataString())) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
|
||||
ToastUtils.show("切换至" + getString(R.string.app_name) + "图标");
|
||||
//moveTaskToBack(true);
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "【handleSwitchRequest】意图为空,无法处理快捷方式请求");
|
||||
return;
|
||||
}
|
||||
if (intent != null && "switchto_cn1".equals(intent.getDataString())) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
|
||||
ToastUtils.show("切换至" + getString(R.string.app_name_cn1) + "图标");
|
||||
//moveTaskToBack(true);
|
||||
}
|
||||
if (intent != null && "switchto_cn2".equals(intent.getDataString())) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
|
||||
ToastUtils.show("切换至" + getString(R.string.app_name_cn2) + "图标");
|
||||
//moveTaskToBack(true);
|
||||
|
||||
String dataString = intent.getDataString();
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】获取到快捷指令:" + dataString);
|
||||
|
||||
// 匹配快捷指令并切换组件
|
||||
if (ACTION_SWITCH_TO_EN1.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name) + "图标";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/19 20:35
|
||||
* @Describe 应用窗口基类
|
||||
*/
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
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.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.BuildConfig;
|
||||
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 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
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化开始", getTag()));
|
||||
// 初始化主题
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
void setThemeStyle() {
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化完成,当前主题:%s", getTag(), mThemeType));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
LogUtils.d(TAG, String.format("【%s-onStart】添加版本标签到页面", getTag()));
|
||||
// 添加版本标签
|
||||
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() {
|
||||
if (!isTagViewVisible()) {
|
||||
LogUtils.d(TAG, String.format("【%s-addVersionNameToContentView】版本标签不可见,跳过添加", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mTagView == null) {
|
||||
mTagView = new TextView(this);
|
||||
// 配置版本标签样式
|
||||
mTagView.setTextColor(Color.GRAY);
|
||||
mTagView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
|
||||
mTagView.setText("MIMO SDK V" + BuildConfig.VERSION_NAME);
|
||||
mTagView.setTextSize(TypedValue.COMPLEX_UNIT_SP, VERSION_TAG_TEXT_SIZE);
|
||||
mTagView.setText(String.format(VERSION_TAG_TEXT, BuildConfig.VERSION_NAME));
|
||||
// 配置布局参数
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
||||
// 添加到根布局
|
||||
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() {
|
||||
Toolbar mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar != null) {
|
||||
setSupportActionBar(mToolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
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】未找到工具栏控件(ID:toolbar)", getTag()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
/**
|
||||
* 版本标签是否可见
|
||||
* @return 默认为true,子类可重写修改
|
||||
*/
|
||||
protected boolean isTagViewVisible() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================== 菜单 & 返回键处理 =========================
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class);
|
||||
LogUtils.d(TAG, String.format("【%s-onOptionsItemSelected】点击返回菜单", getTag()));
|
||||
return true;
|
||||
}
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class);
|
||||
}
|
||||
|
||||
public void changeFullScreen(Activity activity) {
|
||||
Window window = activity.getWindow();
|
||||
if (window == null){
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
LogUtils.d(TAG, String.format("【%s-onBackPressed】触发返回键", getTag()));
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 切换至全屏模式,隐藏状态栏与导航栏
|
||||
* @param activity 目标Activity
|
||||
*/
|
||||
public void changeFullScreen(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】目标Activity为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
Window window = activity.getWindow();
|
||||
if (window == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】窗口为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
View decorView = window.getDecorView();
|
||||
if (decorView == null){
|
||||
if (decorView == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】DecorView为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置全屏标志位
|
||||
int flag = decorView.getSystemUiVisibility();
|
||||
flag |= View.SYSTEM_UI_FLAG_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_IMMERSIVE_STICKY;
|
||||
decorView.setSystemUiVisibility(flag);
|
||||
// 配置窗口标志位
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
LogUtils.d(TAG, String.format("【%s-changeFullScreen】已切换至全屏模式", getTag()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,106 @@
|
||||
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.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
|
||||
import cc.winboll.studio.powerbell.models.BatteryData;
|
||||
import java.util.ArrayList;
|
||||
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 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;
|
||||
notifyDataSetChanged();
|
||||
// ======================== 成员变量 =========================
|
||||
private List<BatteryData> dataList = new ArrayList<>(); // 电池数据列表
|
||||
|
||||
// ======================== 构造方法 =========================
|
||||
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
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "【onCreateViewHolder】创建ViewHolder,父容器:" + parent.getContext().getClass().getSimpleName());
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_battery_report, parent, false);
|
||||
return new ViewHolder(view);
|
||||
.inflate(R.layout.item_battery_report, parent, false);
|
||||
ViewHolder viewHolder = new ViewHolder(view);
|
||||
LogUtils.d(TAG, "【onCreateViewHolder】ViewHolder创建完成");
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
holder.tvLevel.setText(String.format("%d%%", item.getCurrentLevel()));
|
||||
holder.tvDischargeTime.setText("使用时间: " + item.getDischargeTime());
|
||||
holder.tvChargeTime.setText("充电时间: " + item.getChargeTime());
|
||||
// 绑定数据到视图
|
||||
holder.tvLevel.setText(String.format(FORMAT_BATTERY_LEVEL, item.getCurrentLevel()));
|
||||
holder.tvDischargeTime.setText(PREFIX_DISCHARGE_TIME + item.getDischargeTime());
|
||||
holder.tvChargeTime.setText(PREFIX_CHARGE_TIME + item.getChargeTime());
|
||||
|
||||
LogUtils.d(TAG, "【onBindViewHolder】视图绑定完成,位置:" + position + ",电量:" + item.getCurrentLevel() + "%");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return dataList.size();
|
||||
int count = dataList.size();
|
||||
LogUtils.d(TAG, "【getItemCount】获取条目数量:" + count);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ======================== ViewHolder 内部类 =========================
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvLevel;
|
||||
TextView tvDischargeTime;
|
||||
TextView tvChargeTime;
|
||||
TextView tvLevel; // 电量显示
|
||||
TextView tvDischargeTime; // 放电时间显示
|
||||
TextView tvChargeTime; // 充电时间显示
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
// 初始化视图控件
|
||||
tvLevel = itemView.findViewById(R.id.tvLevel);
|
||||
tvDischargeTime = itemView.findViewById(R.id.tvDischargeTime);
|
||||
tvChargeTime = itemView.findViewById(R.id.tvChargeTime);
|
||||
LogUtils.d(TAG, "【ViewHolder】控件初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -11,130 +12,138 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 背景图片的接收分享文件后的预览对话框
|
||||
* 适配 API30,基于 Java7 开发,支持分享图片的Uri解析、预览与确认选择
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/25 16:27:53
|
||||
* @Describe 背景图片的接收分享文件后的预览对话框
|
||||
*/
|
||||
public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "BackgroundPicturePreviewDialog";
|
||||
private static final String TOAST_MSG_EMPTY_FILE = "接收到的文件为空。"; // 空文件提示文本
|
||||
|
||||
Context mContext;
|
||||
//BackgroundSourceUtils mBackgroundPictureUtils;
|
||||
Button dialogbackgroundpicturepreviewButton1;
|
||||
Button dialogbackgroundpicturepreviewButton2;
|
||||
//String mszPreReceivedFileName;
|
||||
IOnRecivedPictureListener mIOnRecivedPictureListener;
|
||||
Uri mUriRecivedPicture;
|
||||
BackgroundView mBackgroundView;
|
||||
|
||||
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();
|
||||
// }
|
||||
// }
|
||||
// ======================== 成员变量 =========================
|
||||
private Context mContext; // 上下文对象
|
||||
private IOnRecivedPictureListener mIOnRecivedPictureListener; // 图片接收监听
|
||||
private Uri mUriRecivedPicture; // 接收的图片Uri
|
||||
// 控件对象
|
||||
private BackgroundView mBackgroundView; // 背景预览视图
|
||||
private Button dialogbackgroundpicturepreviewButton1; // 取消按钮
|
||||
private Button dialogbackgroundpicturepreviewButton2; // 确认按钮
|
||||
|
||||
// ======================== 接口定义 =========================
|
||||
/**
|
||||
* 图片接收监听接口,用于通知确认选择的图片Uri
|
||||
*/
|
||||
public interface IOnRecivedPictureListener {
|
||||
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);
|
||||
if (TextUtils.isEmpty(szSrcImage)) {
|
||||
LogUtils.w(TAG, "【previewRecivedPicture】解析的文件路径为空");
|
||||
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载图片到预览视图
|
||||
mBackgroundView.loadImage(szSrcImage);
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】图片预览完成,文件路径:" + szSrcImage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,17 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import com.a4455jkjh.colorpicker.ColorPickerDialog;
|
||||
import com.a4455jkjh.colorpicker.view.OnColorChangedListener;
|
||||
|
||||
/**
|
||||
* 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
* 适配 API30,基于 Java7 开发,返回 0xAARRGGBB 格式颜色(含透明度)
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/16 11:47
|
||||
* @Describe 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
*/
|
||||
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
|
||||
// ====================== 常量定义(首屏可见,统一管理) ======================
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "ColorPaletteDialog";
|
||||
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值(0-255)
|
||||
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_ALPHA_PERCENT = 100; // 透明度最大值(100%=不透明)
|
||||
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 {
|
||||
@@ -66,9 +71,9 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
|
||||
private static volatile boolean isAppSelfUpdatingColor = false;
|
||||
|
||||
// 控件引用(新增透明度进度条+文本)
|
||||
// 控件引用
|
||||
private ImageView ivColorPicker; // 颜色预览拾取框
|
||||
private ImageView ivColorScaler; // 颜色渐变拾取框
|
||||
private ImageView ivColorScaler; // 颜色渐变拾取框
|
||||
private EditText etR; // R分量输入框(显示实时调节值)
|
||||
private EditText etG; // G分量输入框(显示实时调节值)
|
||||
private EditText etB; // B分量输入框(显示实时调节值)
|
||||
@@ -93,12 +98,11 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
}
|
||||
|
||||
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
|
||||
// 透明度:初始颜色的alpha(0-255)转百分比(0-100%)
|
||||
this.mOriginalAlpha = Color.alpha(initialColor);
|
||||
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
this.mCurrentAlpha = mOriginalAlpha;
|
||||
this.mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
// RGB:初始颜色的RGB分量
|
||||
|
||||
this.mOriginalR = Color.red(initialColor);
|
||||
this.mOriginalG = Color.green(initialColor);
|
||||
this.mOriginalB = Color.blue(initialColor);
|
||||
@@ -110,10 +114,11 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
this.mCurrentColor = initialColor;
|
||||
|
||||
LogUtils.d(TAG, "init dialog success | 初始颜色:" + String.format("#%08X", initialColor)
|
||||
+ " | 原始RGB:" + mOriginalR + "," + mOriginalG + "," + mOriginalB
|
||||
+ " | 原始透明度:" + mOriginalAlphaPercent + "%"
|
||||
+ " | 初始亮度:" + mCurrentBrightnessPercent + "%");
|
||||
LogUtils.d(TAG, String.format("init dialog success | 初始颜色:%s | 原始RGB:%d,%d,%d | 原始透明度:%s | 初始亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, initialColor),
|
||||
mOriginalR, mOriginalG, mOriginalB,
|
||||
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) {
|
||||
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);
|
||||
etG = view.findViewById(R.id.et_g);
|
||||
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
|
||||
|| sbAlpha == null || tvAlphaValue == null
|
||||
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|
||||
|| tvConfirm == null || tvCancel == null) {
|
||||
|| sbAlpha == null || tvAlphaValue == null
|
||||
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|
||||
|| tvConfirm == null || tvCancel == null) {
|
||||
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确!");
|
||||
dismiss();
|
||||
return;
|
||||
@@ -183,22 +188,23 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB,大写更规范)
|
||||
etColorValue.setText(String.format("#%08X", mCurrentColor));
|
||||
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB)
|
||||
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
|
||||
|
||||
// 4. 透明度控件(进度条+文本,初始=原始透明度)
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
|
||||
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
|
||||
|
||||
// 5. 亮度控件(显示默认100%,初始化按钮状态)
|
||||
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
|
||||
updateBrightnessBtnStatus(); // 禁用边界值按钮(初始100%,都可用)
|
||||
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
|
||||
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() {
|
||||
// 点击监听(按钮+颜色拾取框)
|
||||
@@ -225,10 +231,10 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
// 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸)
|
||||
lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8);
|
||||
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
// 软键盘适配:小米虚拟导航栏兼容,避免输入框被遮挡
|
||||
// 软键盘适配:小米虚拟导航栏兼容
|
||||
window.setAttributes(lp);
|
||||
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 | 适配全面屏+软键盘");
|
||||
}
|
||||
}
|
||||
@@ -261,7 +267,7 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 透明度进度条监听实现(核心新增) ======================
|
||||
// ====================== 透明度进度条监听实现 ======================
|
||||
@Override
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* 拖动透明度进度条更新颜色(核心新增逻辑)
|
||||
* 拖动透明度进度条更新颜色
|
||||
*/
|
||||
private synchronized void updateAlphaBySeekBar(int alphaPercent) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
@@ -290,18 +296,17 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
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 {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
isAppSelfUpdatingColor = false; // 释放标记
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 颜色核心逻辑(新增透明度参数,全功能兼容) ======================
|
||||
// ====================== 颜色核心逻辑 ======================
|
||||
/**
|
||||
* 核心计算:基于原始RGB+当前亮度+当前透明度,计算实时RGB+最终颜色
|
||||
* 逻辑:亮度百分比→调节系数→原始RGB×系数→限制0-255→拼接透明度→最终颜色
|
||||
*/
|
||||
private void calculateBrightnessAndUpdate() {
|
||||
// 亮度百分比转调节系数(10%→0.1,100%→1.0,200%→2.0)
|
||||
@@ -317,98 +322,93 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度减少(每次减5%,最低10%,防止过暗)
|
||||
* 亮度减少(每次减5%,最低10%)
|
||||
*/
|
||||
private void decreaseBrightness() {
|
||||
changeBrightness(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度增加(每次加5%,最高200%,防止过曝)
|
||||
* 亮度增加(每次加5%,最高200%)
|
||||
*/
|
||||
private void increaseBrightness() {
|
||||
changeBrightness(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度调节核心方法(统一逻辑,加并发控制,同步所有控件)
|
||||
* 亮度调节核心方法(统一逻辑,加并发控制)
|
||||
*/
|
||||
private synchronized void changeBrightness(boolean isIncrease) {
|
||||
// 关键:判断非应用自身更新,才执行调节(避免重复触发/循环)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
if (isIncrease) {
|
||||
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return; // 达到最大值,不处理
|
||||
mCurrentBrightnessPercent += BRIGHTNESS_STEP; // 增加步长
|
||||
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return;
|
||||
mCurrentBrightnessPercent += BRIGHTNESS_STEP;
|
||||
} else {
|
||||
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return; // 达到最小值,不处理
|
||||
mCurrentBrightnessPercent -= BRIGHTNESS_STEP; // 减少步长
|
||||
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return;
|
||||
mCurrentBrightnessPercent -= BRIGHTNESS_STEP;
|
||||
}
|
||||
// 计算亮度调节后的实时RGB+最终颜色(含当前透明度)
|
||||
// 计算亮度调节后的实时RGB+最终颜色
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, (isIncrease ? "increase" : "decrease") + " brightness | "
|
||||
+ "亮度:" + mCurrentBrightnessPercent + "% | 实时RGB:" + mCurrentR + "," + mCurrentG + "," + mCurrentB);
|
||||
LogUtils.d(TAG, String.format("%s brightness | 亮度:%s | 实时RGB:%d,%d,%d",
|
||||
isIncrease ? "increase" : "decrease",
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent),
|
||||
mCurrentR, mCurrentG, mCurrentB));
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB,容错处理,更新原始基准值+实时值)
|
||||
* 新增:解析颜色的透明度,同步更新透明度进度条
|
||||
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB,更新原始基准值+实时值)
|
||||
*/
|
||||
private void parseColorFromStr(String colorStr, int triggerViewId) {
|
||||
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
if (TextUtils.isEmpty(colorStr)) return;
|
||||
|
||||
// 补全#前缀(兼容用户输入习惯,如直接输AARRGGBB)
|
||||
// 补全#前缀(兼容用户输入习惯)
|
||||
if (!colorStr.startsWith("#")) {
|
||||
colorStr = "#" + colorStr;
|
||||
}
|
||||
|
||||
// 格式校验(仅支持6位RRGGBB/8位AARRGGBB,避免非法格式)
|
||||
// 格式校验(仅支持6位RRGGBB/8位AARRGGBB)
|
||||
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;
|
||||
}
|
||||
|
||||
// 解析颜色(系统API,安全可靠)
|
||||
// 解析颜色
|
||||
int parsedColor = Color.parseColor(colorStr);
|
||||
|
||||
// 更新原始基准值(用户输入颜色,重置基准)
|
||||
// 透明度:解析颜色的alpha(0-255)转百分比(0-100%)
|
||||
// 更新原始基准值与实时值
|
||||
mOriginalAlpha = Color.alpha(parsedColor);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
// RGB:解析颜色的RGB分量
|
||||
mOriginalR = Color.red(parsedColor);
|
||||
mOriginalG = Color.green(parsedColor);
|
||||
mOriginalB = Color.blue(parsedColor);
|
||||
// 更新实时值(原始值=实时值,无调节)
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
// 重置亮度为100%
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = parsedColor;
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "parse color success | 解析颜色:" + String.format("#%08X", parsedColor)
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%");
|
||||
LogUtils.d(TAG, String.format("parse color success | 解析颜色:%s | 透明度:%s | 重置亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, parsedColor),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "parse color failed | 非法颜色格式,输入:" + colorStr, e);
|
||||
LogUtils.e(TAG, String.format("parse color failed | 非法颜色格式,输入:%s", colorStr), e);
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
@@ -416,77 +416,74 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
|
||||
/**
|
||||
* 通过RGB输入框更新颜色(用户输入后,更新原始基准值+实时值,重置亮度为100%)
|
||||
* 新增:透明度基准值保持不变,仅更新RGB
|
||||
*/
|
||||
private synchronized void updateColorByRGB(int triggerViewId) {
|
||||
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
// 解析用户输入的RGB值(限制0-255,非法输入设为0)
|
||||
int inputR = parseInputValue(etR.getText().toString());
|
||||
int inputG = parseInputValue(etG.getText().toString());
|
||||
int inputB = parseInputValue(etB.getText().toString());
|
||||
|
||||
// 更新原始基准值(用户手动输入,作为新的调节基准)
|
||||
// 更新原始基准值与实时值
|
||||
mOriginalR = inputR;
|
||||
mOriginalG = inputG;
|
||||
mOriginalB = inputB;
|
||||
// 更新实时值(输入值=实时值,无亮度调节)
|
||||
mCurrentR = inputR;
|
||||
mCurrentG = inputG;
|
||||
mCurrentB = inputB;
|
||||
// 重置亮度为100%(透明度保持当前值不变)
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
// 计算最终颜色(无亮度调节,拼接当前透明度)
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "update color by RGB | 新原始RGB:" + mOriginalR + "," + mOriginalG + "," + mOriginalB
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%");
|
||||
LogUtils.d(TAG, String.format("update color by RGB | 新原始RGB:%d,%d,%d | 透明度:%s | 重置亮度:%s",
|
||||
mOriginalR, mOriginalG, mOriginalB,
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "update color by RGB failed", e);
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心同步:更新所有控件显示(新增透明度控件同步,统一方法)
|
||||
* 核心同步:更新所有控件显示
|
||||
*/
|
||||
private void updateAllViews() {
|
||||
// 1. 同步颜色预览(显示最终颜色,含透明度+亮度)
|
||||
// 1. 同步颜色预览
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. 同步RGB输入框(显示实时调节值)
|
||||
// 2. 同步RGB输入框
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 同步颜色值输入框(显示最终颜色,含透明度,格式#AARRGGBB)
|
||||
etColorValue.setText(String.format("#%08X", mCurrentColor));
|
||||
// 3. 同步颜色值输入框
|
||||
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
|
||||
|
||||
// 4. 同步透明度控件(进度条+文本,显示实时透明度)
|
||||
// 4. 同步透明度控件
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
|
||||
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
|
||||
|
||||
// 5. 同步亮度控件(数值+按钮状态)
|
||||
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
|
||||
// 5. 同步亮度控件
|
||||
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
|
||||
updateBrightnessBtnStatus();
|
||||
|
||||
LogUtils.d(TAG, "sync all views complete | 最终颜色:" + String.format("#%08X", mCurrentColor)
|
||||
+ " | 实时RGB:" + mCurrentR + "," + mCurrentG + "," + mCurrentB
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "% | 亮度:" + mCurrentBrightnessPercent + "%");
|
||||
LogUtils.d(TAG, String.format("sync all views complete | 最终颜色:%s | 实时RGB:%d,%d,%d | 透明度:%s | 亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, mCurrentColor),
|
||||
mCurrentR, mCurrentG, mCurrentB,
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新亮度按钮状态(边界值禁用,提升交互体验)
|
||||
*/
|
||||
private void updateBrightnessBtnStatus() {
|
||||
// 亮度≤10%:禁用减号(文字变浅灰);≥200%:禁用加号(文字变浅灰)
|
||||
boolean canMinus = mCurrentBrightnessPercent > MIN_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"));
|
||||
}
|
||||
|
||||
// ====================== 工具方法(新增透明度转换工具,通用复用) ======================
|
||||
// ====================== 工具方法 ======================
|
||||
/**
|
||||
* 透明度:0-255 → 0-100%(颜色计算值转用户直观百分比)
|
||||
* 透明度:0-255 → 0-100%
|
||||
*/
|
||||
private int alpha2Percent(int alpha) {
|
||||
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 透明度:0-100% → 0-255(用户操作百分比转颜色计算值)
|
||||
* 透明度:0-100% → 0-255
|
||||
*/
|
||||
private int percent2Alpha(int percent) {
|
||||
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析输入值(限制0-255,非法输入返回0,容错处理)
|
||||
* 解析输入值(限制0-255,非法输入返回0)
|
||||
*/
|
||||
private int parseInputValue(String input) {
|
||||
if (TextUtils.isEmpty(input)) return 0;
|
||||
try {
|
||||
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) {
|
||||
LogUtils.e(TAG, "parse input failed | 非法数字,输入:" + input, e);
|
||||
LogUtils.e(TAG, String.format("parse input failed | 非法数字,输入:%s", input), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB输入框监听复用(减少冗余代码,统一逻辑)
|
||||
* RGB输入框监听复用
|
||||
*/
|
||||
private void setEditTextWatcher(EditText editText, final int viewId) {
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@@ -538,90 +535,77 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
updateColorByRGB(viewId); // 输入变化后更新颜色
|
||||
updateColorByRGB(viewId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* dp转px(适配小米不同分辨率,避免尺寸错乱,通用工具)
|
||||
* dp转px(适配小米不同分辨率)
|
||||
*/
|
||||
private int dp2px(float dp) {
|
||||
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示系统颜色选择器(兼容API29-30,无高版本依赖,小米机型适配)
|
||||
* 核心调整:新增「水平滚动容器+颜色排列容器」二级结构,内置圆形按钮,无额外drawable依赖
|
||||
*/
|
||||
private void showSystemColorPicker() {
|
||||
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30,支持横向滚动");
|
||||
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
|
||||
builder.setTitle("选择基础颜色");
|
||||
/**
|
||||
* 显示系统颜色选择器(兼容API29-30,无高版本依赖,小米机型适配)
|
||||
*/
|
||||
private void showSystemColorPicker() {
|
||||
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30,支持横向滚动");
|
||||
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
|
||||
builder.setTitle("选择基础颜色");
|
||||
|
||||
// 50种常用颜色:按「红→橙→黄→绿→青→蓝→紫→粉→棕→灰→黑白」彩虹光谱顺序排列
|
||||
// 50种常用颜色:按彩虹光谱顺序排列
|
||||
final int[] systemColors = {
|
||||
// 红色系(6种:深红→大红→浅红→玫红→暗红→橘红)
|
||||
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
|
||||
// 橙色系(5种:深橙→橙→浅橙→橙黄→橘橙)
|
||||
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
|
||||
// 黄色系(5种:深黄→黄→浅黄→鹅黄→金黄)
|
||||
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
|
||||
// 绿色系(7种:深绿→绿→浅绿→草绿→薄荷绿→翠绿→墨绿)
|
||||
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
|
||||
// 青色系(5种:深青→青→浅青→蓝绿→青绿)
|
||||
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
|
||||
// 蓝色系(8种:深蓝→藏蓝→蓝→浅蓝→天蓝→宝蓝→湖蓝→靛蓝)
|
||||
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
|
||||
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
|
||||
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
|
||||
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
|
||||
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
|
||||
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
|
||||
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
|
||||
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
|
||||
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
|
||||
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
|
||||
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
|
||||
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
|
||||
};
|
||||
|
||||
// 1. 第一级:水平滚动容器
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
|
||||
horizontalScrollView.setHorizontalScrollBarEnabled(true);
|
||||
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
|
||||
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
||||
// 1. 第一级:水平滚动容器
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
|
||||
horizontalScrollView.setHorizontalScrollBarEnabled(true);
|
||||
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
|
||||
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
||||
|
||||
// 2. 第二级:颜色排列容器(横向)
|
||||
LinearLayout colorLayout = new LinearLayout(getContext());
|
||||
colorLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
colorLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
|
||||
// 2. 第二级:颜色排列容器(横向)
|
||||
LinearLayout colorLayout = new LinearLayout(getContext());
|
||||
colorLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
colorLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
|
||||
|
||||
// 3. 循环添加颜色按钮(内置圆形效果,无额外依赖)
|
||||
for (int i = 0; i < systemColors.length; i++) {
|
||||
final int color = systemColors[i];
|
||||
ImageView colorBtn = new ImageView(getContext());
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
|
||||
if (i != systemColors.length - 1) {
|
||||
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
|
||||
}
|
||||
colorBtn.setLayoutParams(lp);
|
||||
// 3. 循环添加颜色按钮(内置圆形效果)
|
||||
for (int i = 0; i < systemColors.length; i++) {
|
||||
final int color = systemColors[i];
|
||||
ImageView colorBtn = new ImageView(getContext());
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
|
||||
if (i != systemColors.length - 1) {
|
||||
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
|
||||
}
|
||||
colorBtn.setLayoutParams(lp);
|
||||
|
||||
// 核心:内置圆形背景(白色边框+圆形形状,无需drawable文件)
|
||||
GradientDrawable circleBg = new GradientDrawable();
|
||||
circleBg.setShape(GradientDrawable.OVAL); // 圆形
|
||||
circleBg.setColor(color); // 按钮颜色
|
||||
circleBg.setStroke(dp2px(2), Color.WHITE); // 白色边框(2dp宽,区分颜色)
|
||||
colorBtn.setBackground(circleBg); // 设置圆形背景
|
||||
// 内置圆形背景(白色边框+圆形形状)
|
||||
GradientDrawable circleBg = new GradientDrawable();
|
||||
circleBg.setShape(GradientDrawable.OVAL);
|
||||
circleBg.setColor(color);
|
||||
circleBg.setStroke(dp2px(2), Color.WHITE);
|
||||
colorBtn.setBackground(circleBg);
|
||||
|
||||
colorBtn.setClickable(true);
|
||||
colorBtn.setFocusable(true);
|
||||
colorBtn.setClickable(true);
|
||||
colorBtn.setFocusable(true);
|
||||
|
||||
// 点击事件(逻辑不变)
|
||||
colorBtn.setOnClickListener(new View.OnClickListener() {
|
||||
// 点击事件
|
||||
colorBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
@@ -641,57 +625,59 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
builder.create().dismiss();
|
||||
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "%");
|
||||
LogUtils.d(TAG, String.format("select system color | 选择颜色:%s | 透明度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, color),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
colorLayout.addView(colorBtn);
|
||||
}
|
||||
colorLayout.addView(colorBtn);
|
||||
}
|
||||
|
||||
// 层级嵌套(滚动容器→颜色容器)
|
||||
horizontalScrollView.addView(colorLayout);
|
||||
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
|
||||
}
|
||||
// 层级嵌套
|
||||
horizontalScrollView.addView(colorLayout);
|
||||
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
|
||||
}
|
||||
|
||||
// ====================== 点击事件实现(统一处理,逻辑清晰) ======================
|
||||
// ====================== 点击事件实现 ======================
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//ToastUtils.show("onClick");
|
||||
int id = v.getId();
|
||||
// 关键:所有点击事件均加判断(避免并发冲突/重复触发)
|
||||
// 所有点击事件均加并发判断
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
if (id == R.id.iv_color_picker) {
|
||||
showSystemColorPicker(); // 打开系统颜色选择器
|
||||
} if (id == R.id.iv_color_scaler) {
|
||||
//ToastUtils.show("iv_color_scale");
|
||||
openColorScalerDialog(mCurrentColor); // 打开系统颜色选择器
|
||||
showSystemColorPicker();
|
||||
} else if (id == R.id.iv_color_scaler) {
|
||||
openColorScalerDialog(mCurrentColor);
|
||||
} else if (id == R.id.tv_confirm) {
|
||||
mListener.onColorSelected(mCurrentColor); // 确认选择,回调颜色
|
||||
LogUtils.d(TAG, "confirm color | 回调颜色:" + String.format("#%08X", mCurrentColor));
|
||||
mListener.onColorSelected(mCurrentColor);
|
||||
LogUtils.d(TAG, String.format("confirm color | 回调颜色:%s",
|
||||
String.format(FORMAT_COLOR_HEX, mCurrentColor)));
|
||||
dismiss();
|
||||
} else if (id == R.id.tv_cancel) {
|
||||
dismiss(); // 取消,关闭对话框
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
|
||||
} else if (id == R.id.tv_brightness_minus) {
|
||||
decreaseBrightness(); // 减少亮度
|
||||
decreaseBrightness();
|
||||
} 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
|
||||
public void beforeColorChanged() {
|
||||
}
|
||||
public void beforeColorChanged() {}
|
||||
|
||||
@Override
|
||||
public void onColorChanged(int color) {
|
||||
@@ -699,49 +685,49 @@ public class ColorPaletteDialog extends Dialog implements View.OnClickListener,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterColorChanged() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void afterColorChanged() {}
|
||||
});
|
||||
dlg.show();
|
||||
}
|
||||
dlg.show();
|
||||
}
|
||||
|
||||
// ====================== 内部类 ======================
|
||||
class ColorScalerDialog extends ColorPickerDialog {
|
||||
public int currentColorScalerDialogColor = 0;
|
||||
public ColorScalerDialog(Context context, int p) {
|
||||
super(context, p);
|
||||
}
|
||||
public int currentColorScalerDialogColor = 0;
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
int color = currentColorScalerDialogColor;
|
||||
ToastUtils.show(String.format("dismiss color %d", color));
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "%");
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public ColorScalerDialog(Context context, int p) {
|
||||
super(context, p);
|
||||
this.currentColorScalerDialogColor = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
int color = currentColorScalerDialogColor;
|
||||
ToastUtils.show(String.format("选择颜色:%s", String.format(FORMAT_COLOR_HEX, color)));
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -15,69 +15,82 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
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 java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
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>
|
||||
* @Date 2025/11/19 20:11
|
||||
* @Describe 网络后台使用提示对话框
|
||||
* @Describe 网络背景使用提示对话框
|
||||
* 继承 AndroidX AlertDialog,绑定自定义布局 dialog_networkbackground.xml
|
||||
* 适配 API30,基于 Java7 开发,支持网络图片下载、预览与回调
|
||||
*/
|
||||
public class NetworkBackgroundDialog extends AlertDialog {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "NetworkBackgroundDialog";
|
||||
// 消息标识:图片加载成功
|
||||
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_SUCCESS = 1001; // 图片加载成功消息标识
|
||||
private static final int MSG_IMAGE_LOAD_FAILED = 1002; // 图片加载失败消息标识
|
||||
|
||||
// 控件引用
|
||||
private TextView tvTitle;
|
||||
private TextView tvContent;
|
||||
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 接口实现)
|
||||
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
|
||||
/**
|
||||
* 按钮点击回调接口(Java7 接口实现)
|
||||
*/
|
||||
public interface OnDialogClickListener {
|
||||
void onConfirm(String szConfirmFilePath); // 确认按钮点击
|
||||
void onCancel(); // 取消按钮点击
|
||||
void onConfirm(String szConfirmFilePath); // 确认按钮点击,返回图片路径
|
||||
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) {
|
||||
super(context);
|
||||
initHandler(); // 初始化 Handler
|
||||
initView(); // 初始化布局和控件
|
||||
setDismissListener(); // 设置对话框消失监听
|
||||
}
|
||||
|
||||
// 带回调的构造(便于外部处理点击事件)
|
||||
public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) {
|
||||
super(context);
|
||||
this.listener = listener;
|
||||
initHandler(); // 初始化 Handler
|
||||
LogUtils.d(TAG, "NetworkBackgroundDialog: 基础构造初始化");
|
||||
initHandler();
|
||||
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() {
|
||||
mUiHandler = new Handler() {
|
||||
@@ -86,22 +99,28 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
super.handleMessage(msg);
|
||||
// 对话框已消失时,不再处理 UI 消息
|
||||
if (!isShowing()) {
|
||||
LogUtils.d(TAG, "handleMessage: 对话框已消失,忽略消息");
|
||||
return;
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MSG_IMAGE_LOAD_SUCCESS:
|
||||
// 图片加载成功,获取文件路径并设置背景
|
||||
mDownloadSavedPath = (String) msg.obj;
|
||||
mBackgroundView.loadImage(mDownloadSavedPath);
|
||||
LogUtils.d(TAG, String.format("handleMessage: 图片加载成功,保存路径:%s", mDownloadSavedPath));
|
||||
mBackgroundView.loadImage(mDownloadSavedPath);
|
||||
break;
|
||||
case MSG_IMAGE_LOAD_FAILED:
|
||||
// 图片加载失败,设置默认背景
|
||||
LogUtils.e(TAG, "handleMessage: 图片加载失败");
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
ToastUtils.show("图片预览失败,请检查链接");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initHandler: 主线程 Handler 初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,20 +133,22 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
// 对话框消失时,移除所有未处理的消息和回调
|
||||
if (mUiHandler != null) {
|
||||
mUiHandler.removeCallbacksAndMessages(null);
|
||||
LogUtils.d(TAG, "onDismiss: Handler 消息已清理");
|
||||
}
|
||||
LogUtils.d(TAG, "对话框已消失,Handler 消息已清理");
|
||||
LogUtils.d(TAG, "onDismiss: 对话框已消失");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "setDismissListener: 对话框消失监听已设置");
|
||||
}
|
||||
|
||||
// ====================== 初始化方法(布局、控件、点击事件) ======================
|
||||
/**
|
||||
* 初始化布局和控件
|
||||
*/
|
||||
private void initView() {
|
||||
mContext = this.getContext();
|
||||
// 加载自定义布局
|
||||
View dialogView = LayoutInflater.from(getContext())
|
||||
.inflate(R.layout.dialog_networkbackground, null);
|
||||
View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_networkbackground, null);
|
||||
// 设置对话框内容视图
|
||||
setView(dialogView);
|
||||
|
||||
@@ -139,10 +160,21 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
|
||||
etURL = (EditText) dialogView.findViewById(R.id.et_url);
|
||||
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();
|
||||
|
||||
LogUtils.d(TAG, "initView: 布局和控件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,13 +185,14 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击");
|
||||
LogUtils.d(TAG, "onClick: 取消按钮点击");
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.setCurrentSourceToPreview();
|
||||
|
||||
dismiss(); // 关闭对话框
|
||||
if (listener != null) {
|
||||
listener.onCancel();
|
||||
LogUtils.d(TAG, "onClick: 取消回调已执行");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -168,14 +201,16 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
|
||||
LogUtils.d(TAG, "onClick: 确认按钮点击");
|
||||
dismiss(); // 关闭对话框
|
||||
if(TextUtils.isEmpty(mDownloadSavedPath)) {
|
||||
if (TextUtils.isEmpty(mDownloadSavedPath)) {
|
||||
ToastUtils.show("未下载图片。");
|
||||
LogUtils.w(TAG, "onClick: 确认失败,未下载图片");
|
||||
return;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onConfirm(mDownloadSavedPath);
|
||||
LogUtils.d(TAG, String.format("onClick: 确认回调已执行,图片路径:%s", mDownloadSavedPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -184,90 +219,120 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnPreview.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 预览按钮点击");
|
||||
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 背景(主线程调用)
|
||||
* @param filePath 图片文件路径
|
||||
* @param previewFilePath 图片文件路径
|
||||
*/
|
||||
private void previewBackground(String previewFilePath) {
|
||||
if (TextUtils.isEmpty(previewFilePath)) {
|
||||
LogUtils.w(TAG, "previewBackground: 预览文件路径为空");
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
File imageFile = new File(previewFilePath);
|
||||
if (!imageFile.exists()) {
|
||||
ToastUtils.show("图片文件不存在:" + previewFilePath);
|
||||
LogUtils.e(TAG, "图片文件不存在:" + previewFilePath);
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
ToastUtils.show("图片文件不存在:" + previewFilePath);
|
||||
LogUtils.e(TAG, String.format("previewBackground: 图片文件不存在,路径:%s", previewFilePath));
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览背景
|
||||
mPreviewFilePath = previewFilePath;
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
mBackgroundView.loadByBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
// 预览背景
|
||||
mPreviewFilePath = previewFilePath;
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
mBackgroundView.loadByBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
|
||||
LogUtils.d(TAG, String.format("previewBackground: 图片预览成功,路径:%s", previewFilePath));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LogUtils.e(TAG, String.format("previewBackground: 图片预览失败,错误信息:%s", e.getMessage()), e);
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
LogUtils.e(TAG, "图片预览失败:" + e.getMessage());
|
||||
} finally {
|
||||
// Java7 手动关闭流,避免资源泄漏
|
||||
if (fis != null) {
|
||||
try {
|
||||
fis.close();
|
||||
LogUtils.d(TAG, "previewBackground: 文件输入流已关闭");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LogUtils.e(TAG, String.format("previewBackground: 关闭文件输入流失败,错误信息:%s", e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外提供方法(灵活适配不同场景) ======================
|
||||
/**
|
||||
* 对外提供方法:修改对话框标题(灵活适配不同场景)
|
||||
* 对外提供方法:修改对话框标题
|
||||
* @param title 标题文本
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
if (tvTitle != null) {
|
||||
if (tvTitle != null && !TextUtils.isEmpty(title)) {
|
||||
tvTitle.setText(title);
|
||||
LogUtils.d(TAG, String.format("setTitle: 对话框标题已修改为:%s", title));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:修改对话框内容(灵活适配不同场景)
|
||||
* 对外提供方法:修改对话框内容
|
||||
* @param content 内容文本
|
||||
*/
|
||||
public void setContent(String content) {
|
||||
if (tvContent != null) {
|
||||
if (tvContent != null && !TextUtils.isEmpty(content)) {
|
||||
tvContent.setText(content);
|
||||
LogUtils.d(TAG, String.format("setContent: 对话框内容已修改为:%s", content));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:设置按钮点击回调(替代带参构造)
|
||||
* @param listener 按钮点击回调
|
||||
*/
|
||||
public void setOnDialogClickListener(OnDialogClickListener 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);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,127 +1,175 @@
|
||||
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.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
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 {
|
||||
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
// 序列化版本号(Serializable 必备,避免反序列化失败)
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// 日志标签(全局统一,替换 Log 为 LogUtils)
|
||||
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; // 无效电量标识
|
||||
|
||||
// 核心配置字段(保留原有字段,统一状态字段命名)
|
||||
// ====================== 成员变量(按功能分类:提醒配置→电量状态→检测配置) ======================
|
||||
// 耗电提醒配置
|
||||
boolean isEnableUsageReminder = false; // 耗电提醒开关
|
||||
int usageReminderValue = 45; // 耗电提醒阈值(0-100)
|
||||
// 充电提醒配置
|
||||
boolean isEnableChargeReminder = false;// 充电提醒开关
|
||||
int chargeReminderValue = 100; // 充电提醒阈值(0-100)
|
||||
int reminderIntervalTime = 5000; // 铃声提醒间隔(ms,原有)
|
||||
boolean isCharging = false; // 是否充电(状态字段,原有)
|
||||
int currentBatteryValue = -1; // 修正:统一命名为「currentBatteryValue」(原 currentValue)
|
||||
int batteryDetectInterval = 2000; // 新增:电量检测间隔(ms,适配 RemindThread)
|
||||
// 铃声提醒配置
|
||||
int reminderIntervalTime = 5000; // 铃声提醒间隔(ms)
|
||||
// 电量状态
|
||||
boolean isCharging = false; // 是否充电
|
||||
int currentBatteryValue = INVALID_BATTERY; // 当前电池电量(统一命名,替代原 currentValue)
|
||||
// 电量检测配置
|
||||
int batteryDetectInterval = 2000; // 电量检测间隔(ms,适配 RemindThread)
|
||||
|
||||
// 构造方法:初始化默认配置(同步修正字段名,统一默认值)
|
||||
// ====================== 构造方法(初始化默认配置,强化默认值校验) ======================
|
||||
public AppConfigBean() {
|
||||
setChargeReminderValue(100);
|
||||
setEnableChargeReminder(false);
|
||||
setUsageReminderValue(10);
|
||||
setEnableUsageReminder(false);
|
||||
setReminderIntervalTime(5000);
|
||||
setBatteryDetectInterval(1000); // 新增:默认检测间隔1秒
|
||||
setCurrentBatteryValue(-1); // 修正:初始化当前电量字段
|
||||
setBatteryDetectInterval(1000); // 默认检测间隔1秒
|
||||
setCurrentBatteryValue(INVALID_BATTERY); // 初始化无效电量标识
|
||||
LogUtils.d(TAG, "AppConfigBean: 初始化默认配置完成");
|
||||
}
|
||||
|
||||
// ====================== 核心修复:补全缺失方法(适配 RemindThread/Receiver 调用) ======================
|
||||
// ====================== 核心业务方法(Setter/Getter,按字段功能分类,补充调试日志) ======================
|
||||
// --------------- 电量状态相关 ---------------
|
||||
/**
|
||||
* 设置当前电池电量(Receiver 监听电池变化时调用,与 RemindThread 字段对齐)
|
||||
* 设置当前电池电量(Receiver 监听电池变化时调用)
|
||||
* @param currentBatteryValue 当前电量(0-100)
|
||||
*/
|
||||
public void setCurrentBatteryValue(int currentBatteryValue) {
|
||||
// 强化校验:电量范围限制在 0-100,异常值置为 -1(标识无效)
|
||||
this.currentBatteryValue = (currentBatteryValue >= 0 && currentBatteryValue <= 100)
|
||||
? currentBatteryValue : -1;
|
||||
this.currentBatteryValue = (currentBatteryValue >= BATTERY_MIN && currentBatteryValue <= BATTERY_MAX)
|
||||
? currentBatteryValue : INVALID_BATTERY;
|
||||
LogUtils.d(TAG, String.format("setCurrentBatteryValue: 当前电量设置为 %d(输入值:%d)",
|
||||
this.currentBatteryValue, currentBatteryValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前电池电量(RemindThread 同步配置时调用,与 set 方法对应)
|
||||
* 获取当前电池电量(RemindThread 同步配置时调用)
|
||||
* @return 当前电量(0-100 或 INVALID_BATTERY)
|
||||
*/
|
||||
public int getCurrentBatteryValue() {
|
||||
return currentBatteryValue;
|
||||
}
|
||||
|
||||
// ====================== 原有字段 Setter/Getter(修正命名,强化校验) ======================
|
||||
// --------------- 铃声提醒配置相关 ---------------
|
||||
/**
|
||||
* 设置铃声提醒间隔
|
||||
* @param reminderIntervalTime 提醒间隔(ms,不小于 MIN_REMIND_INTERVAL)
|
||||
*/
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
// 校验:提醒间隔不小于 1000ms,避免频繁提醒
|
||||
this.reminderIntervalTime = Math.max(reminderIntervalTime, 1000);
|
||||
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
|
||||
LogUtils.d(TAG, String.format("setReminderIntervalTime: 提醒间隔设置为 %dms(输入值:%d)",
|
||||
this.reminderIntervalTime, reminderIntervalTime));
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return reminderIntervalTime;
|
||||
}
|
||||
|
||||
public void setIsCharging(boolean isCharging) { // 修正:方法名与字段名统一(原 setCharging)
|
||||
// --------------- 充电状态相关 ---------------
|
||||
/**
|
||||
* 设置是否充电
|
||||
* @param isCharging 充电状态
|
||||
*/
|
||||
public void setIsCharging(boolean isCharging) {
|
||||
this.isCharging = isCharging;
|
||||
LogUtils.d(TAG, String.format("setIsCharging: 充电状态设置为 %b", isCharging));
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
// --------------- 耗电提醒配置相关 ---------------
|
||||
public void setEnableUsageReminder(boolean isEnableUsageReminder) {
|
||||
this.isEnableUsageReminder = isEnableUsageReminder;
|
||||
LogUtils.d(TAG, String.format("setEnableUsageReminder: 耗电提醒开关设置为 %b", isEnableUsageReminder));
|
||||
}
|
||||
|
||||
public boolean isEnableUsageReminder() {
|
||||
return isEnableUsageReminder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置耗电提醒阈值
|
||||
* @param usageReminderValue 阈值(0-100)
|
||||
*/
|
||||
public void setUsageReminderValue(int usageReminderValue) {
|
||||
// 校验:阈值范围 0-100
|
||||
this.usageReminderValue = Math.min(Math.max(usageReminderValue, 0), 100);
|
||||
this.usageReminderValue = Math.min(Math.max(usageReminderValue, BATTERY_MIN), BATTERY_MAX);
|
||||
LogUtils.d(TAG, String.format("setUsageReminderValue: 耗电提醒阈值设置为 %d(输入值:%d)",
|
||||
this.usageReminderValue, usageReminderValue));
|
||||
}
|
||||
|
||||
public int getUsageReminderValue() {
|
||||
return usageReminderValue;
|
||||
}
|
||||
|
||||
// --------------- 充电提醒配置相关 ---------------
|
||||
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
this.isEnableChargeReminder = isEnableChargeReminder;
|
||||
LogUtils.d(TAG, String.format("setEnableChargeReminder: 充电提醒开关设置为 %b", isEnableChargeReminder));
|
||||
}
|
||||
|
||||
public boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置充电提醒阈值
|
||||
* @param chargeReminderValue 阈值(0-100)
|
||||
*/
|
||||
public void setChargeReminderValue(int chargeReminderValue) {
|
||||
// 校验:阈值范围 0-100
|
||||
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, 0), 100);
|
||||
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, BATTERY_MIN), BATTERY_MAX);
|
||||
LogUtils.d(TAG, String.format("setChargeReminderValue: 充电提醒阈值设置为 %d(输入值:%d)",
|
||||
this.chargeReminderValue, chargeReminderValue));
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
// ====================== 电量检测间隔 Setter/Getter(适配 RemindThread) ======================
|
||||
// --------------- 电量检测配置相关 ---------------
|
||||
/**
|
||||
* 设置电量检测间隔
|
||||
* @param batteryDetectInterval 检测间隔(ms,不小于 MIN_INTERVAL)
|
||||
*/
|
||||
public void setBatteryDetectInterval(int batteryDetectInterval) {
|
||||
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
|
||||
LogUtils.d(TAG, String.format("setBatteryDetectInterval: 检测间隔设置为 %dms(输入值:%d)",
|
||||
this.batteryDetectInterval, batteryDetectInterval));
|
||||
}
|
||||
|
||||
public int getBatteryDetectInterval() {
|
||||
return batteryDetectInterval;
|
||||
}
|
||||
|
||||
// 强化校验:检测间隔不小于500ms(避免 CPU 高占用,与 RemindThread 最小休眠一致)
|
||||
public void setBatteryDetectInterval(int batteryDetectInterval) {
|
||||
this.batteryDetectInterval = Math.max(batteryDetectInterval, 500);
|
||||
}
|
||||
|
||||
// ====================== JSON 序列化/反序列化(兼容旧配置,同步修正字段) ======================
|
||||
// ====================== JSON 序列化/反序列化(兼容旧配置,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigBean.class.getName();
|
||||
@@ -131,18 +179,19 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
AppConfigBean bean = this;
|
||||
// 原有字段序列化(保留拼写兼容,同步修正字段名)
|
||||
// 原有字段序列化
|
||||
jsonWriter.name("isEnableUsageReminder").value(bean.isEnableUsageReminder());
|
||||
jsonWriter.name("usageReminderValue").value(bean.getUsageReminderValue());
|
||||
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
|
||||
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
|
||||
jsonWriter.name("reminderIntervalTime").value(bean.getReminderIntervalTime());
|
||||
jsonWriter.name("isCharging").value(bean.isCharging());
|
||||
// 修正:序列化新字段名 currentBatteryValue,兼容旧字段 currentValue
|
||||
// 兼容旧字段 currentValue,同步新字段 currentBatteryValue
|
||||
jsonWriter.name("currentBatteryValue").value(bean.getCurrentBatteryValue());
|
||||
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue()); // 兼容旧配置,避免数据丢失
|
||||
// 新增字段序列化:电量检测间隔
|
||||
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue());
|
||||
// 新增字段序列化
|
||||
jsonWriter.name("batteryDetectInterval").value(bean.getBatteryDetectInterval());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,7 +200,7 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
// 原有字段反序列化(兼容旧 Key 拼写,同步修正字段)
|
||||
// 兼容拼写错误字段(isEnableUsegeReminder → isEnableUsageReminder)
|
||||
if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) {
|
||||
bean.setEnableUsageReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
|
||||
@@ -163,58 +212,62 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
|
||||
} else if (name.equals("reminderIntervalTime")) {
|
||||
bean.setReminderIntervalTime(jsonReader.nextInt());
|
||||
} else if (name.equals("isCharging")) {
|
||||
bean.setIsCharging(jsonReader.nextBoolean()); // 修正:调用新方法名
|
||||
}
|
||||
// 核心兼容:优先读取旧字段 currentValue,再读取新字段 currentBatteryValue(新字段覆盖旧字段)
|
||||
else if (name.equals("currentValue")) {
|
||||
bean.setIsCharging(jsonReader.nextBoolean());
|
||||
} else if (name.equals("currentValue")) {
|
||||
// 优先读取旧字段,兼容历史配置
|
||||
bean.setCurrentBatteryValue(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 currentValue 完成");
|
||||
} else if (name.equals("currentBatteryValue")) {
|
||||
// 新字段覆盖旧字段,保证数据最新
|
||||
bean.setCurrentBatteryValue(jsonReader.nextInt());
|
||||
}
|
||||
// 新增字段反序列化(兼容无此字段的旧配置,用默认值1000ms)
|
||||
else if (name.equals("batteryDetectInterval")) {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 currentBatteryValue 完成");
|
||||
} else if (name.equals("batteryDetectInterval")) {
|
||||
bean.setBatteryDetectInterval(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口实现(同步修正字段,确保 Intent 传递正常) ======================
|
||||
// ====================== Parcelable 接口实现(API30 Intent 传递必备,补充调试日志) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0; // 无特殊内容描述,固定返回0
|
||||
}
|
||||
|
||||
// 序列化:将所有字段写入 Parcel(同步修正字段名,Java7 适配)
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0)); // boolean → byte
|
||||
// 按成员变量顺序写入,boolean 转 byte 存储
|
||||
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0));
|
||||
dest.writeInt(usageReminderValue);
|
||||
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0)); // boolean → byte
|
||||
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0));
|
||||
dest.writeInt(chargeReminderValue);
|
||||
dest.writeInt(reminderIntervalTime);
|
||||
dest.writeByte((byte) (isCharging ? 1 : 0)); // boolean → byte
|
||||
dest.writeInt(currentBatteryValue); // 修正:序列化新字段名
|
||||
dest.writeByte((byte) (isCharging ? 1 : 0));
|
||||
dest.writeInt(currentBatteryValue);
|
||||
dest.writeInt(batteryDetectInterval);
|
||||
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>() {
|
||||
@Override
|
||||
public AppConfigBean createFromParcel(Parcel source) {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
// 按 writeToParcel 顺序读取,同步修正字段
|
||||
// 按 writeToParcel 顺序读取
|
||||
bean.isEnableUsageReminder = source.readByte() != 0;
|
||||
bean.usageReminderValue = source.readInt();
|
||||
bean.isEnableChargeReminder = source.readByte() != 0;
|
||||
bean.chargeReminderValue = source.readInt();
|
||||
bean.reminderIntervalTime = source.readInt();
|
||||
bean.isCharging = source.readByte() != 0;
|
||||
bean.currentBatteryValue = source.readInt(); // 修正:读取新字段名
|
||||
bean.currentBatteryValue = source.readInt();
|
||||
bean.batteryDetectInterval = source.readInt();
|
||||
LogUtils.d(TAG, "createFromParcel: Parcel 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,51 +3,61 @@ package cc.winboll.studio.powerbell.models;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 11:52:28
|
||||
* @Describe 应用背景图片数据类(存储正式/预览背景配置,支持JSON序列化/反序列化)
|
||||
* @Describe 应用背景图片数据类
|
||||
* 适配 API30,支持 Serializable 持久化、JSON 序列化/反序列化
|
||||
* 存储正式/预览背景配置,包含原图、压缩图、裁剪比例、像素颜色等核心字段
|
||||
*/
|
||||
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 = "";
|
||||
// 核心字段:背景图片完整路径(解决仅存文件名导致的路径拼接错误,与backgroundScaledCompressFilePath对应)
|
||||
private String backgroundFilePath = "";
|
||||
// 附加字段:图片信息(如Uri、网络地址等,仅作备注,不参与路径生成)
|
||||
private String backgroundFileInfo = "";
|
||||
// 控制字段:是否启用背景图片(true-显示背景图,false-显示透明背景)
|
||||
private boolean isUseBackgroundFile = false;
|
||||
// 核心字段:压缩后背景图片文件名(对应应用私有目录下的压缩图片,与saveCropBitmap的压缩图匹配)
|
||||
private String backgroundScaledCompressFileName = "";
|
||||
// 核心字段:压缩后背景图片完整路径(解决仅存文件名导致的路径拼接错误,适配BackgroundSettingsActivity的私有目录)
|
||||
private String backgroundScaledCompressFilePath = "";
|
||||
// 重命名字段:是否启用压缩背景图(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile,语义更清晰)
|
||||
private boolean isUseBackgroundScaledCompressFile = false;
|
||||
// 裁剪比例字段:背景图宽高比(默认1:1,用于固定比例裁剪)
|
||||
private int backgroundWidth = 100;
|
||||
private int backgroundHeight = 100;
|
||||
// 像素拾取字段:拾取的像素颜色(用于纯色背景)
|
||||
private int pixelColor = 0;
|
||||
// ====================== 成员变量(按功能分类:原图配置→压缩图配置→控制字段→裁剪配置→像素颜色) ======================
|
||||
// 原图配置
|
||||
private String backgroundFileName = ""; // 背景图片文件名
|
||||
private String backgroundFilePath = ""; // 背景图片完整路径
|
||||
private String backgroundFileInfo = ""; // 图片信息(Uri、网络地址等)
|
||||
// 压缩图配置
|
||||
private String backgroundScaledCompressFileName = ""; // 压缩后背景图片文件名
|
||||
private String backgroundScaledCompressFilePath = ""; // 压缩后背景图片完整路径
|
||||
// 控制字段
|
||||
private boolean isUseBackgroundFile = false; // 是否启用背景图片
|
||||
private boolean isUseBackgroundScaledCompressFile = false; // 是否启用压缩背景图(重命名:原isUseScaledCompress)
|
||||
// 裁剪配置
|
||||
private int backgroundWidth = DEFAULT_DIMENSION; // 背景图宽度
|
||||
private int backgroundHeight = DEFAULT_DIMENSION; // 背景图高度
|
||||
// 像素颜色
|
||||
private int pixelColor = 0xFFFFFFFF; // 拾取的像素颜色(纯色背景用)
|
||||
|
||||
// ====================== 构造方法(无参构造,JSON反序列化必备) ======================
|
||||
/**
|
||||
* 无参构造器(必须,JSON反序列化时需默认构造器)
|
||||
*/
|
||||
public BackgroundBean() {
|
||||
LogUtils.d(TAG, "BackgroundBean: 无参构造初始化完成");
|
||||
}
|
||||
|
||||
// ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)======================================
|
||||
// ====================== Getter/Setter 方法(按功能分类,补充调试日志,强化校验) ======================
|
||||
// --------------- 原图配置相关 ---------------
|
||||
public String getBackgroundFileName() {
|
||||
return 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() {
|
||||
@@ -55,7 +65,8 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -63,23 +74,28 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
}
|
||||
|
||||
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() {
|
||||
return isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
LogUtils.d(TAG, String.format("setIsUseBackgroundFile: 是否启用背景图设置为 %b", isUseBackgroundFile));
|
||||
}
|
||||
|
||||
// --------------- 压缩图配置相关 ---------------
|
||||
public String getBackgroundScaledCompressFileName() {
|
||||
return 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() {
|
||||
@@ -87,7 +103,8 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
|
||||
LogUtils.d(TAG, String.format("setIsUseBackgroundScaledCompressFile: 是否启用压缩背景图设置为 %b", isUseBackgroundScaledCompressFile));
|
||||
}
|
||||
|
||||
// --------------- 裁剪配置相关 ---------------
|
||||
public int getBackgroundWidth() {
|
||||
return 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() {
|
||||
@@ -115,44 +135,54 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
}
|
||||
|
||||
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() {
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
public void setPixelColor(int pixelColor) {
|
||||
this.pixelColor = pixelColor;
|
||||
LogUtils.d(TAG, String.format("setPixelColor: 像素颜色设置为 0x%08X", pixelColor));
|
||||
}
|
||||
|
||||
// ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本)======================================
|
||||
// ====================== 序列化/反序列化方法(适配重命名字段,兼容旧版本,补充调试日志) ======================
|
||||
@Override
|
||||
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)
|
||||
* 确保新字段能正常持久化,同时兼容旧版本JSON(可选:保留旧字段写入,避免旧版本读取异常)
|
||||
* 确保新字段能正常持久化,同时兼容旧版本JSON(保留旧字段写入,避免旧版本读取异常)
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BackgroundBean bean = this;
|
||||
// 原图配置序列化
|
||||
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("isUseBackgroundFile").value(bean.isUseBackgroundFile());
|
||||
// 压缩图配置序列化
|
||||
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
|
||||
jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath());
|
||||
// 关键:新字段序列化(核心)
|
||||
jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 兼容旧版本:保留旧字段名写入(可选,避免旧版本Bean读取时缺失字段)
|
||||
jsonWriter.name("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 兼容旧版本:保留旧字段名写入(避免旧版本Bean读取时缺失字段)
|
||||
jsonWriter.name(OLD_FIELD_USE_SCALED_COMPRESS).value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 裁剪配置与像素颜色序列化
|
||||
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
|
||||
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
|
||||
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());
|
||||
break;
|
||||
case "backgroundFilePath":
|
||||
bean.setBackgroundFilePath(jsonReader.nextString()); // 新增字段:读取背景原图完整路径
|
||||
bean.setBackgroundFilePath(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundFileInfo":
|
||||
bean.setBackgroundFileInfo(jsonReader.nextString());
|
||||
@@ -186,13 +216,15 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
case "backgroundScaledCompressFilePath":
|
||||
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
|
||||
break;
|
||||
// 关键:读取新字段(优先)
|
||||
case "isUseBackgroundScaledCompressFile":
|
||||
// 关键:读取新字段(优先)
|
||||
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 isUseBackgroundScaledCompressFile 完成");
|
||||
break;
|
||||
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
|
||||
case "isUseScaledCompress":
|
||||
case OLD_FIELD_USE_SCALED_COMPRESS:
|
||||
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
|
||||
tempUseScaledCompress = jsonReader.nextBoolean();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 isUseScaledCompress 完成");
|
||||
break;
|
||||
case "backgroundWidth":
|
||||
bean.setBackgroundWidth(jsonReader.nextInt());
|
||||
@@ -204,32 +236,36 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
bean.setPixelColor(jsonReader.nextInt());
|
||||
break;
|
||||
default:
|
||||
jsonReader.skipValue(); // 跳过未知字段,兼容旧版本Bean(避免崩溃)
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
// 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充
|
||||
if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) {
|
||||
if (!bean.isUseBackgroundScaledCompressFile()) {
|
||||
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 旧字段值已填充到新字段");
|
||||
}
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================================== 辅助方法(同步更新重命名字段)======================================
|
||||
// ====================== 辅助方法(重置配置、配置校验,补充调试日志) ======================
|
||||
/**
|
||||
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
|
||||
*/
|
||||
public void resetBackgroundConfig() {
|
||||
this.backgroundFileName = "";
|
||||
this.backgroundFilePath = ""; // 新增:重置背景原图完整路径
|
||||
this.backgroundFilePath = "";
|
||||
this.backgroundScaledCompressFileName = "";
|
||||
this.backgroundScaledCompressFilePath = "";
|
||||
this.backgroundFileInfo = "";
|
||||
this.isUseBackgroundFile = false;
|
||||
this.isUseBackgroundScaledCompressFile = false; // 重命名字段:重置为false
|
||||
this.backgroundWidth = 100;
|
||||
this.backgroundHeight = 100;
|
||||
this.isUseBackgroundScaledCompressFile = false;
|
||||
this.backgroundWidth = DEFAULT_DIMENSION;
|
||||
this.backgroundHeight = DEFAULT_DIMENSION;
|
||||
LogUtils.d(TAG, "resetBackgroundConfig: 背景配置已重置为默认值");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,16 +276,21 @@ public class BackgroundBean extends BaseBean implements Serializable {
|
||||
public boolean isBackgroundConfigValid() {
|
||||
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
|
||||
if (!isUseBackgroundFile) {
|
||||
LogUtils.d(TAG, "isBackgroundConfigValid: 未启用背景图,配置无效");
|
||||
return false;
|
||||
}
|
||||
// 原图校验:路径非空 或 文件名非空
|
||||
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
|
||||
// 压缩图校验:启用压缩图时,路径/文件名需非空
|
||||
boolean isCompressValid = true;
|
||||
if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图
|
||||
if (isUseBackgroundScaledCompressFile()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,82 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:30:51
|
||||
* @Describe 电池报告数据模型
|
||||
* 适配 API30,存储当前电量、放电时间、充电时间核心数据
|
||||
* 支持参数校验与调试日志输出
|
||||
*/
|
||||
public class 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 String chargeTime;
|
||||
// ====================== 成员变量(按功能分类:电量→时间) ======================
|
||||
private int currentLevel; // 当前电池电量(0-100)
|
||||
private String dischargeTime; // 放电时间
|
||||
private String chargeTime; // 充电时间
|
||||
|
||||
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) {
|
||||
this.currentLevel = currentLevel;
|
||||
this.dischargeTime = dischargeTime;
|
||||
this.chargeTime = chargeTime;
|
||||
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
|
||||
/**
|
||||
* 无参构造器(适配 JSON 反序列化、反射实例化场景)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,47 +3,88 @@ package cc.winboll.studio.powerbell.models;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 电池信息数据模型
|
||||
* 适配 API30,存储电量时间戳与电量值,支持 JSON 序列化/反序列化
|
||||
* 修复字段拼写错误,补充数据校验与调试日志
|
||||
*/
|
||||
public class BatteryInfoBean extends BaseBean implements Serializable {
|
||||
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
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;
|
||||
|
||||
// 记录电量的时间戳
|
||||
long timeStamp;
|
||||
// 电量值
|
||||
int battetyValue;
|
||||
|
||||
// ====================== 成员变量(修复拼写错误:battetyValue → batteryValue) ======================
|
||||
private long timeStamp; // 记录电量的时间戳
|
||||
private int batteryValue; // 电量值(0-100)
|
||||
|
||||
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
|
||||
/**
|
||||
* 无参构造器(JSON 反序列化、反射实例化必备)
|
||||
*/
|
||||
public BatteryInfoBean() {
|
||||
this.timeStamp = 0;
|
||||
this.battetyValue = 0;
|
||||
this.timeStamp = DEFAULT_TIMESTAMP;
|
||||
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.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) {
|
||||
this.timeStamp = timeStamp;
|
||||
LogUtils.d(TAG, "setTimeStamp: 时间戳设置为 " + timeStamp);
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
|
||||
public void setBattetyValue(int battetyValue) {
|
||||
this.battetyValue = battetyValue;
|
||||
/**
|
||||
* 设置电量值(修复拼写错误:battetyValue → batteryValue)
|
||||
* @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() {
|
||||
return battetyValue;
|
||||
public int getBatteryValue() {
|
||||
return batteryValue;
|
||||
}
|
||||
|
||||
// ====================== JSON 序列化/反序列化方法(修复字段拼写错误,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
return BatteryInfoBean.class.getName();
|
||||
String className = BatteryInfoBean.class.getName();
|
||||
LogUtils.d(TAG, "getName: 类名标识为 " + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,7 +92,9 @@ public class BatteryInfoBean extends BaseBean implements Serializable {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BatteryInfoBean bean = this;
|
||||
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
|
||||
@@ -60,16 +103,28 @@ public class BatteryInfoBean extends BaseBean implements Serializable {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("timeStamp")) {
|
||||
bean.setTimeStamp(jsonReader.nextLong());
|
||||
} else if (name.equals("battetyValue")) {
|
||||
bean.setBattetyValue(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
switch (name) {
|
||||
case "timeStamp":
|
||||
bean.setTimeStamp(jsonReader.nextLong());
|
||||
break;
|
||||
case "batteryValue":
|
||||
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();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,48 +4,44 @@ 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/17 15:55
|
||||
* @Describe 服务控制参数模型:管理服务启用状态,支持序列化、Parcel传递、JSON解析
|
||||
* @Describe 服务控制参数模型
|
||||
* 适配 API30,管理服务启用状态,支持 Serializable 持久化、Parcelable 组件传递、JSON 序列化解析
|
||||
*/
|
||||
public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable {
|
||||
// ================================== 静态常量(置顶统一管理,避免魔法值)=================================
|
||||
// ====================== 静态常量(置顶统一管理,避免魔法值) ======================
|
||||
private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
|
||||
private static final String TAG = "ControlCenterServiceBean";
|
||||
// JSON 字段常量,避免硬编码,减少拼写错误
|
||||
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService";
|
||||
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService"; // JSON 字段常量,避免硬编码
|
||||
|
||||
// ================================== 核心成员变量(私有封装,规范命名)=================================
|
||||
// ====================== 核心成员变量(私有封装,规范命名) ======================
|
||||
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>() {
|
||||
@Override
|
||||
public ControlCenterServiceBean createFromParcel(Parcel source) {
|
||||
LogUtils.d(TAG, "Parcelable createFromParcel: 从Parcel反序列化对象");
|
||||
// Java7 + API30 适配:Parcel 无直接 writeBoolean,用 byte 存储/读取
|
||||
boolean isEnable = source.readByte() != 0;
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnable);
|
||||
LogUtils.d(TAG, "Parcelable createFromParcel: 反序列化完成,isEnableService=" + isEnable);
|
||||
LogUtils.d(TAG, String.format("createFromParcel: 反序列化完成,isEnableService=%b", isEnable));
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlCenterServiceBean[] newArray(int size) {
|
||||
LogUtils.d(TAG, "Parcelable newArray: 创建数组,长度=" + size);
|
||||
LogUtils.d(TAG, String.format("newArray: 创建数组,长度=%d", size));
|
||||
return new ControlCenterServiceBean[size];
|
||||
}
|
||||
};
|
||||
|
||||
// ================================== 构造方法(无参+有参,满足不同初始化场景)=================================
|
||||
// ====================== 构造方法(无参+有参,满足不同初始化场景) ======================
|
||||
/**
|
||||
* 无参构造(JSON解析、反射创建必备)
|
||||
*/
|
||||
@@ -60,25 +56,25 @@ public class ControlCenterServiceBean extends BaseBean implements Parcelable, Se
|
||||
*/
|
||||
public ControlCenterServiceBean(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
LogUtils.d(TAG, "有参构造:初始化服务状态,isEnableService=" + isEnableService);
|
||||
LogUtils.d(TAG, String.format("有参构造:初始化服务状态,isEnableService=%b", isEnableService));
|
||||
}
|
||||
|
||||
// ================================== Getter/Setter 方法(封装成员变量,控制访问)=================================
|
||||
// ====================== Getter/Setter 方法(封装成员变量,控制访问) ======================
|
||||
public boolean isEnableService() {
|
||||
LogUtils.d(TAG, "get isEnableService: 当前状态=" + isEnableService);
|
||||
LogUtils.d(TAG, String.format("isEnableService: 当前状态=%b", isEnableService));
|
||||
return 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;
|
||||
}
|
||||
|
||||
// ================================== 父类 BaseBean 方法重写(核心业务逻辑)=================================
|
||||
// ====================== 父类 BaseBean 方法重写(核心业务逻辑:JSON 序列化/反序列化) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = ControlCenterServiceBean.class.getName();
|
||||
LogUtils.d(TAG, "getName: 返回类名=" + className);
|
||||
LogUtils.d(TAG, String.format("getName: 返回类名=%s", className));
|
||||
return className;
|
||||
}
|
||||
|
||||
@@ -87,11 +83,9 @@ public class ControlCenterServiceBean extends BaseBean implements Parcelable, Se
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将对象序列化到JSON");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 写入服务启用状态字段
|
||||
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
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON反序列化对象");
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (JSON_FIELD_IS_ENABLE_SERVICE.equals(fieldName)) {
|
||||
// 读取并设置服务启用状态
|
||||
boolean isEnable = jsonReader.nextBoolean();
|
||||
bean.setIsEnableService(isEnable);
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取JSON字段," + fieldName + "=" + isEnable);
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader: 读取字段,%s=%b", fieldName, isEnable));
|
||||
} else {
|
||||
// 跳过未知字段,避免解析异常
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段=" + fieldName);
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段=%s", fieldName));
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成");
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ================================== Parcelable 接口方法实现(适配 Intent 组件间传递)=================================
|
||||
// ====================== Parcelable 接口方法实现(适配 Intent 组件间传递,Java7 适配) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
// 无特殊内容(如文件描述符),返回0即可(API30 标准实现)
|
||||
LogUtils.d(TAG, "describeContents: 返回内容描述符=0");
|
||||
return 0;
|
||||
return 0; // 无特殊内容(如文件描述符),返回0即可(API30 标准实现)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 Parcel(Intent 传递必备,Java7 适配)
|
||||
* 序列化对象到 Parcel(Intent 传递必备,Java7 适配:用 byte 存储 boolean)
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
LogUtils.d(TAG, "writeToParcel: 开始将对象序列化到Parcel,flags=" + flags);
|
||||
// Java7 + API30 适配:Parcel 无 writeBoolean 方法,用 byte 存储(1=true,0=false)
|
||||
dest.writeByte((byte) (this.isEnableService ? 1 : 0));
|
||||
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成,isEnableService=" + this.isEnableService + "(存储为byte=" + (this.isEnableService ? 1 : 0) + ")");
|
||||
byte flag = (byte) (this.isEnableService ? 1 : 0);
|
||||
dest.writeByte(flag);
|
||||
LogUtils.d(TAG, String.format("writeToParcel: 序列化完成,isEnableService=%b(存储为byte=%d)", this.isEnableService, flag));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,75 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递
|
||||
* 通知数据模型
|
||||
* 适配 API30,统一存储通知标题、内容、标识信息,支持各组件数据传递
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递
|
||||
*/
|
||||
public class NotificationMessage {
|
||||
// ====================== 静态常量(统一管理) ======================
|
||||
private static final String TAG = "NotificationMessage";
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
// ====================== 核心成员变量(按业务逻辑排序) ======================
|
||||
private String title; // 通知标题
|
||||
private String content; // 通知内容
|
||||
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) {
|
||||
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() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getRemindMSG() {
|
||||
return remindMSG;
|
||||
}
|
||||
|
||||
public void setRemindMSG(String remindMSG) {
|
||||
this.remindMSG = remindMSG;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* 控制中心广播接收器
|
||||
* 功能:监听电池状态变化、前台通知更新、配置变更指令
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:23
|
||||
* @Describe 控制中心广播接收器
|
||||
* 功能:监听电池状态变化、前台通知更新、配置变更指令
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
*/
|
||||
public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "ControlCenterServiceReceiver";
|
||||
|
||||
// 广播Action常量(带包名前缀防冲突)
|
||||
@@ -34,27 +34,28 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// ================================== 静态状态标记(volatile保证多线程可见性)=================================
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见)
|
||||
private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见)
|
||||
|
||||
// ================================== 成员变量区(弱引用防泄漏,按功能分层)=================================
|
||||
// ====================== 成员变量区(弱引用防泄漏,按功能分层) ======================
|
||||
private WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
private boolean isRegistered = false; // 新增:标记广播注册状态,避免冗余操作
|
||||
private boolean isRegistered = false; // 标记广播注册状态,避免冗余操作
|
||||
|
||||
// ================================== 构造方法(初始化弱引用,避免服务强引用泄漏)=================================
|
||||
// ====================== 构造方法(初始化弱引用,避免服务强引用泄漏) ======================
|
||||
public ControlCenterServiceReceiver(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "构造接收器 | service=" + (service != null ? service.getClass().getSimpleName() : "null"));
|
||||
LogUtils.d(TAG, String.format("构造接收器 | 服务实例:%s", service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ================================== 广播核心接收逻辑(入口方法,分Action分发处理)=================================
|
||||
// ====================== 广播核心接收逻辑(入口方法,分Action分发处理) ======================
|
||||
@Override
|
||||
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 + "),终止处理");
|
||||
return;
|
||||
}
|
||||
@@ -62,13 +63,12 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
// 弱引用获取服务,双重校验服务有效性
|
||||
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
|
||||
if (service == null || service.isDestroyed()) {
|
||||
LogUtils.e(TAG, "onReceive: 服务已销毁或为空(service=" + service + "),注销广播");
|
||||
LogUtils.e(TAG, "onReceive: 服务已销毁或为空,注销广播");
|
||||
unregisterAction(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 分Action处理业务逻辑
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case Intent.ACTION_BATTERY_CHANGED:
|
||||
handleBatteryStateChanged(service, intent);
|
||||
@@ -77,30 +77,30 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
handleUpdateForegroundNotification(service);
|
||||
break;
|
||||
case ACTION_APPCONFIG_CHANGED:
|
||||
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播"); // 新增:标记配置广播处理起点
|
||||
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播");
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onReceive: 未知Action=" + action);
|
||||
LogUtils.w(TAG, String.format("onReceive: 未知Action=%s", action));
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ================================== 业务处理方法(按功能拆分,强化容错与日志)=================================
|
||||
// ====================== 业务处理方法(按功能拆分,强化容错与日志) ======================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param service 控制中心服务实例
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态 | service=" + service + " | intent=" + intent);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
|
||||
try {
|
||||
// 1. 解析并校验当前电池状态
|
||||
boolean currentCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
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. 状态无变化则跳过,减少无效运算
|
||||
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
@@ -108,13 +108,14 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 更新静态缓存状态,保证多线程可见
|
||||
// 3. 更新静态缓存状态,保证多线程可见
|
||||
sIsCharging = currentCharging;
|
||||
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) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
|
||||
}
|
||||
@@ -125,23 +126,24 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
* @param service 控制中心服务实例
|
||||
*/
|
||||
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置 | service=" + service);
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置");
|
||||
try {
|
||||
// 加载最新配置
|
||||
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
|
||||
if (latestConfig == null) { // 新增:配置空指针防护
|
||||
if (latestConfig == null) {
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 最新配置为空,终止处理");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=" + latestConfig.getChargeReminderValue() + " | 耗电阈值=" + latestConfig.getUsageReminderValue());
|
||||
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=%d | 耗电阈值=%d",
|
||||
latestConfig.getChargeReminderValue(), latestConfig.getUsageReminderValue()));
|
||||
|
||||
// 同步缓存的电池状态到配置
|
||||
latestConfig.setCurrentBatteryValue(sLastBatteryLevel);
|
||||
latestConfig.setIsCharging(sIsCharging);
|
||||
service.notifyAppConfigUpdate(latestConfig);
|
||||
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=" + sLastBatteryLevel + "% | 充电状态=" + sIsCharging);
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成"); // 新增:标记配置广播处理终点
|
||||
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=%d%% | 充电状态=%b", sLastBatteryLevel, sIsCharging));
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 处理失败", e);
|
||||
}
|
||||
@@ -152,32 +154,32 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
* @param service 控制中心服务实例
|
||||
*/
|
||||
private void handleUpdateForegroundNotification(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知 | service=" + service);
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知");
|
||||
try {
|
||||
NotificationManagerUtils notifyUtils = service.getNotificationManager();
|
||||
NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
|
||||
|
||||
// 非空校验,避免空指针
|
||||
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;
|
||||
}
|
||||
|
||||
notifyUtils.updateForegroundServiceNotify(notifyMsg);
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification: 前台通知更新成功 | 通知标题=" + notifyMsg.getTitle());
|
||||
LogUtils.d(TAG, String.format("handleUpdateForegroundNotification: 前台通知更新成功 | 标题=%s", notifyMsg.getTitle()));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleUpdateForegroundNotification: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 广播注册/注销(强化容错,避免重复操作)=================================
|
||||
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
* @param context 上下文
|
||||
*/
|
||||
public void registerAction(Context context) {
|
||||
LogUtils.d(TAG, "registerAction: 注册广播接收器 | context=" + context);
|
||||
if (context == null || isRegistered) { // 新增:已注册则跳过
|
||||
LogUtils.d(TAG, "registerAction: 注册广播接收器");
|
||||
if (context == null || isRegistered) {
|
||||
LogUtils.e(TAG, "registerAction: 上下文为空或已注册,注册失败");
|
||||
return;
|
||||
}
|
||||
@@ -190,8 +192,8 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
filter.setPriority(BROADCAST_PRIORITY);
|
||||
|
||||
context.registerReceiver(this, filter);
|
||||
isRegistered = true; // 标记为已注册
|
||||
LogUtils.d(TAG, "registerAction: 广播注册成功 | 优先级=" + BROADCAST_PRIORITY);
|
||||
isRegistered = true;
|
||||
LogUtils.d(TAG, String.format("registerAction: 广播注册成功 | 优先级=%d", BROADCAST_PRIORITY));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
}
|
||||
@@ -202,15 +204,15 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
* @param context 上下文
|
||||
*/
|
||||
public void unregisterAction(Context context) {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播接收器 | context=" + context);
|
||||
if (context == null || !isRegistered) { // 新增:未注册则跳过
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播接收器");
|
||||
if (context == null || !isRegistered) {
|
||||
LogUtils.e(TAG, "unregisterAction: 上下文为空或未注册,注销失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
isRegistered = false; // 标记为未注册
|
||||
isRegistered = false;
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
@@ -219,7 +221,7 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 资源释放与Getter方法(按需开放,防泄漏)=================================
|
||||
// ====================== 资源释放与Getter方法(按需开放,防泄漏) ======================
|
||||
/**
|
||||
* 主动释放资源,避免内存泄漏
|
||||
*/
|
||||
@@ -253,3 +255,4 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
return sIsCharging;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,110 +18,113 @@ import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
* 适配:Java7 | API30 | 内存泄漏防护
|
||||
*/
|
||||
public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "GlobalApplicationReceiver";
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// ================================== 静态成员变量(线程安全,volatile保证多线程可见性)=================================
|
||||
private static volatile int sLastBatteryLevel = -1; // 历史电量(0-100)
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
private static volatile int sLastBatteryLevel = -1; // 历史电量(0-100)
|
||||
private static volatile boolean sLastIsCharging = false; // 历史充电状态
|
||||
|
||||
// ================================== 成员变量区(按功能分层)=================================
|
||||
// ====================== 成员变量区(按功能分层,移除冗余的mCurrentReceiver) ======================
|
||||
private App mGlobalApplication;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private GlobalApplicationReceiver mCurrentReceiver;
|
||||
|
||||
// ================================== 构造方法(强化参数校验,初始化核心依赖)=================================
|
||||
// ====================== 构造方法(强化参数校验,初始化核心依赖) ======================
|
||||
public GlobalApplicationReceiver(App globalApplication) {
|
||||
LogUtils.d(TAG, "构造接收器 | App=" + globalApplication);
|
||||
LogUtils.d(TAG, String.format("构造接收器 | App实例:%s", globalApplication));
|
||||
if (globalApplication == null) {
|
||||
LogUtils.e(TAG, "构造失败:App实例为空");
|
||||
throw new IllegalArgumentException("App cannot be null");
|
||||
}
|
||||
this.mCurrentReceiver = this;
|
||||
this.mGlobalApplication = globalApplication;
|
||||
this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
|
||||
LogUtils.d(TAG, "构造完成:AppConfigUtils=" + mAppConfigUtils);
|
||||
LogUtils.d(TAG, String.format("构造完成 | AppConfigUtils:%s", mAppConfigUtils));
|
||||
}
|
||||
|
||||
// ================================== 广播核心接收逻辑(入口方法,过滤电池状态广播)=================================
|
||||
// ====================== 广播核心接收逻辑(入口方法,过滤电池状态广播) ======================
|
||||
@Override
|
||||
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: 参数无效,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅处理电池状态变化广播
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
|
||||
handleBatteryStateChanged(context, intent);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ================================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面)=================================
|
||||
// ====================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面) ======================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param context 上下文
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(Context context, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态 | intent=" + intent);
|
||||
// 1. 解析当前电池状态(复用工具类,二次校验电量范围)
|
||||
boolean currentIsCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 当前状态 | 充电=" + currentIsCharging + " | 电量=" + currentBatteryLevel + "%");
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
|
||||
try {
|
||||
// 1. 解析当前电池状态(复用工具类,二次校验电量范围)
|
||||
boolean currentIsCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentIsCharging, currentBatteryLevel));
|
||||
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 同步最新状态到配置工具类
|
||||
if (mAppConfigUtils != null) {
|
||||
if (currentIsCharging != sLastIsCharging) {
|
||||
mAppConfigUtils.setCharging(currentIsCharging);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 同步充电状态 | " + currentIsCharging);
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
if (currentBatteryLevel != sLastBatteryLevel) {
|
||||
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 同步电量 | " + currentBatteryLevel + "%");
|
||||
|
||||
// 3. 同步最新状态到配置工具类
|
||||
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. 执行状态变化后的业务逻辑
|
||||
// 记录电量变化时间
|
||||
if (App.getAppCacheUtils(context) != null) {
|
||||
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
|
||||
}
|
||||
// 通知MainActivity更新电量
|
||||
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 发送电量更新消息到MainActivity");
|
||||
// 4. 执行状态变化后的业务逻辑
|
||||
// 记录电量变化时间
|
||||
if (App.getAppCacheUtils(context) != null) {
|
||||
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
|
||||
}
|
||||
// 通知MainActivity更新电量
|
||||
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 发送电量更新消息到MainActivity | %d%%", currentBatteryLevel));
|
||||
|
||||
// 5. 更新历史状态缓存
|
||||
sLastIsCharging = currentIsCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
|
||||
// 5. 更新历史状态缓存
|
||||
sLastIsCharging = currentIsCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 广播注册/注销(强化容错,避免重复操作)=================================
|
||||
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
*/
|
||||
public void registerAction() {
|
||||
LogUtils.d(TAG, "registerAction: 注册广播");
|
||||
if (mGlobalApplication == null || mCurrentReceiver == null) {
|
||||
LogUtils.e(TAG, "注册失败:App或Receiver实例为空");
|
||||
if (mGlobalApplication == null) {
|
||||
LogUtils.e(TAG, "注册失败:App实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,7 +133,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
unregisterAction();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
mGlobalApplication.registerReceiver(mCurrentReceiver, filter);
|
||||
mGlobalApplication.registerReceiver(this, filter);
|
||||
LogUtils.d(TAG, "registerAction: 广播注册成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
@@ -142,13 +145,13 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
*/
|
||||
public void unregisterAction() {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播");
|
||||
if (mGlobalApplication == null || mCurrentReceiver == null) {
|
||||
LogUtils.e(TAG, "注销失败:App或Receiver实例为空");
|
||||
if (mGlobalApplication == null) {
|
||||
LogUtils.e(TAG, "注销失败:App实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mGlobalApplication.unregisterReceiver(mCurrentReceiver);
|
||||
mGlobalApplication.unregisterReceiver(this);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
@@ -157,7 +160,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 资源释放方法(主动释放,彻底避免内存泄漏)=================================
|
||||
// ====================== 资源释放方法(主动释放,彻底避免内存泄漏) ======================
|
||||
/**
|
||||
* 释放接收器资源,供App销毁时调用
|
||||
*/
|
||||
@@ -168,7 +171,6 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
// 置空引用,帮助GC回收
|
||||
mGlobalApplication = null;
|
||||
mAppConfigUtils = null;
|
||||
mCurrentReceiver = null;
|
||||
// 重置静态状态缓存
|
||||
sLastBatteryLevel = -1;
|
||||
sLastIsCharging = false;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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.Context;
|
||||
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.utils.ServiceUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/06 15:01:39
|
||||
* @Describe 应用核心广播接收器
|
||||
* 功能:监听开机完成广播,实现服务开机自启
|
||||
* 适配:Java7 | API30 | 服务启动兼容性处理
|
||||
*/
|
||||
public class MainReceiver extends BroadcastReceiver {
|
||||
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
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";
|
||||
// 存储电量指示值,
|
||||
// 用于校验电量消息时的电量变化
|
||||
static volatile int _mnTheQuantityOfElectricityOld = -1;
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
// 历史电量值,用于校验电量变化(暂未使用,保留扩展能力)
|
||||
private static volatile int sLastBatteryLevel = -1;
|
||||
|
||||
// ====================== 广播核心接收逻辑(入口方法,分Action处理) ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String szAction = intent.getAction();
|
||||
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
|
||||
boolean isEnableService = App.getAppConfigUtils(context).isServiceEnabled();
|
||||
if (isEnableService) {
|
||||
if (ServiceUtils.isServiceAlive(context.getApplicationContext(), ControlCenterService.class.getName()) == false) {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain() Wakeup... ControlCenterService");
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(new Intent(context, ControlCenterService.class));
|
||||
} else {
|
||||
context.startService(new Intent(context, ControlCenterService.class));
|
||||
}
|
||||
}
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null) {
|
||||
LogUtils.e(TAG, "onReceive: 上下文或意图为空,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action:%s", action));
|
||||
|
||||
// 仅处理开机完成广播
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,21 +16,25 @@ import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
* 电池提醒核心服务进程守护类
|
||||
* 功能:监听主服务 {@link ControlCenterService} 存活状态,异常断开时自动重启并绑定
|
||||
* 适配:Java7 | API30 | 前台服务启动规则 | 服务绑定稳定性保障
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 守护服务:保障ControlCenterService持续运行
|
||||
*/
|
||||
public class AssistantService extends Service {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
private static final String TAG = "AssistantService";
|
||||
// 服务返回策略常量(统一定义,避免魔法值)
|
||||
// 服务返回策略常量
|
||||
private static final int SERVICE_RETURN_STICKY = START_STICKY;
|
||||
// 服务绑定标记常量
|
||||
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 MyServiceConnection mMyServiceConnection;
|
||||
private volatile boolean mIsThreadAlive;
|
||||
|
||||
// ================================== 内部类(服务连接状态监听,前置定义便于引用)=================================
|
||||
// ====================== 内部类(服务连接状态监听,前置定义便于引用) ======================
|
||||
/**
|
||||
* 服务连接状态监听器
|
||||
* 主服务连接成功时记录状态,断开时自动重连
|
||||
@@ -38,12 +42,14 @@ public class AssistantService extends Service {
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
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
|
||||
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()) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected: 配置启用,尝试重新唤醒并绑定主服务");
|
||||
@@ -52,11 +58,11 @@ public class AssistantService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 服务生命周期方法(按执行顺序排列:onCreate→onStartCommand→onBind→onDestroy)=================================
|
||||
// ====================== 服务生命周期方法(按执行顺序排列:onCreate→onStartCommand→onBind→onDestroy) ======================
|
||||
@Override
|
||||
public void 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);
|
||||
@@ -75,12 +81,12 @@ public class AssistantService extends Service {
|
||||
// 初始化运行状态,执行核心守护逻辑
|
||||
mIsThreadAlive = false;
|
||||
run();
|
||||
LogUtils.d(TAG, "onCreate: 守护服务初始化完成 | 服务启用状态=" + mAppConfigUtils.isServiceEnabled());
|
||||
LogUtils.d(TAG, String.format("onCreate: 守护服务初始化完成 | 服务启用状态=%b", mAppConfigUtils.isServiceEnabled()));
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
LogUtils.e(TAG, "onStartCommand: AppConfigUtils未初始化,终止服务");
|
||||
@@ -90,13 +96,13 @@ public class AssistantService extends Service {
|
||||
|
||||
run();
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务绑定请求 | intent=" + intent);
|
||||
LogUtils.d(TAG, String.format("onBind: 服务绑定请求 | intent=%s", intent));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -116,14 +122,15 @@ public class AssistantService extends Service {
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁完成");
|
||||
}
|
||||
|
||||
// ================================== 核心业务逻辑(守护主服务存活)=================================
|
||||
// ====================== 核心业务逻辑(守护主服务存活) ======================
|
||||
/**
|
||||
* 执行守护逻辑:检查主服务状态,按需唤醒并绑定
|
||||
* 前置条件:mAppConfigUtils 必须初始化完成
|
||||
*/
|
||||
private void run() {
|
||||
LogUtils.d(TAG, "run: 执行守护逻辑 | 配置启用=" + mAppConfigUtils.isServiceEnabled() + " | 线程存活=" + mIsThreadAlive);
|
||||
if (mAppConfigUtils.isServiceEnabled()) {
|
||||
boolean isServiceEnabled = mAppConfigUtils.isServiceEnabled();
|
||||
LogUtils.d(TAG, String.format("run: 执行守护逻辑 | 配置启用=%b | 线程存活=%b", isServiceEnabled, mIsThreadAlive));
|
||||
if (isServiceEnabled) {
|
||||
if (!mIsThreadAlive) {
|
||||
mIsThreadAlive = true;
|
||||
wakeupAndBindMain();
|
||||
@@ -141,13 +148,14 @@ public class AssistantService extends Service {
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
// 检查主服务存活状态
|
||||
boolean isMainServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName());
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 主服务存活状态=" + isMainServiceAlive);
|
||||
String mainServiceName = ControlCenterService.class.getName();
|
||||
boolean isMainServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), mainServiceName);
|
||||
LogUtils.d(TAG, String.format("wakeupAndBindMain: 主服务存活状态=%b", isMainServiceAlive));
|
||||
|
||||
// 主服务未存活时,按需启动(区分API版本)
|
||||
if (!isMainServiceAlive) {
|
||||
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);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: API26+ 以前台服务方式启动主服务");
|
||||
} else {
|
||||
@@ -159,10 +167,10 @@ public class AssistantService extends Service {
|
||||
// 绑定主服务,监听连接状态,添加结果日志
|
||||
Intent bindIntent = new Intent(AssistantService.this, ControlCenterService.class);
|
||||
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);
|
||||
LogUtils.d(TAG, "unbindMainService: 已成功解绑ControlCenterService");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unbindMainService: 解绑服务失败,服务未绑定 | " + e.getMessage());
|
||||
LogUtils.w(TAG, String.format("unbindMainService: 解绑服务失败,服务未绑定 | %s", e.getMessage()));
|
||||
}
|
||||
mMyServiceConnection = null;
|
||||
}
|
||||
|
||||
@@ -24,22 +24,30 @@ import java.util.List;
|
||||
* 电池提醒核心服务
|
||||
* 功能:管理前台服务生命周期、控制提醒线程启停、处理配置更新
|
||||
* 适配:Java7 | API30 | 前台服务超时防护 | 电池优化忽略引导
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 核心服务:实现电池监测、提醒控制与前台服务保活
|
||||
*/
|
||||
public class ControlCenterService extends Service {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "ControlCenterService";
|
||||
// 线程与服务常量
|
||||
private static final long THREAD_STOP_TIMEOUT = 1000L;
|
||||
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_USAGE_REMINDER_VALUE = 20;
|
||||
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 mIsDestroyed = true;
|
||||
|
||||
// ================================== 成员变量区(按功能分层:配置→核心组件→通知相关)=================================
|
||||
// ====================== 成员变量区(按功能分层:配置→核心组件→通知相关) ======================
|
||||
// 服务控制配置
|
||||
private ControlCenterServiceBean mServiceControlBean;
|
||||
private AppConfigBean mCurrentConfigBean;
|
||||
@@ -50,31 +58,33 @@ public class ControlCenterService extends Service {
|
||||
private NotificationManagerUtils mNotificationManager;
|
||||
private NotificationMessage mForegroundNotifyMsg;
|
||||
|
||||
// ================================== 服务生命周期方法(按执行顺序:onCreate→onStartCommand→onBind→onDestroy)=================================
|
||||
// ====================== 服务生命周期方法(按执行顺序:onCreate→onStartCommand→onBind→onDestroy) ======================
|
||||
@Override
|
||||
public void 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();
|
||||
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
|
||||
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();
|
||||
runCoreServiceLogic();
|
||||
|
||||
int returnFlag = (mServiceControlBean != null && mServiceControlBean.isEnableService())
|
||||
? 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind执行 | intent=" + intent);
|
||||
LogUtils.d(TAG, String.format("onBind执行 | intent=%s", intent));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -101,7 +111,7 @@ public class ControlCenterService extends Service {
|
||||
LogUtils.d(TAG, "onDestroy完成:服务销毁完成");
|
||||
}
|
||||
|
||||
// ================================== 核心业务逻辑(独立抽取,统一调用)=================================
|
||||
// ====================== 核心业务逻辑(独立抽取,统一调用) ======================
|
||||
/**
|
||||
* 服务核心运行逻辑,在onCreate/onStartCommand复用
|
||||
* 避免重复初始化,保证前台服务优先启动
|
||||
@@ -111,7 +121,7 @@ public class ControlCenterService extends Service {
|
||||
loadLatestServiceControlConfig();
|
||||
|
||||
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) {
|
||||
isServiceRunning = true;
|
||||
@@ -131,7 +141,7 @@ public class ControlCenterService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 前台通知管理(优先执行,防止API26+前台服务5秒超时)=================================
|
||||
// ====================== 前台通知管理(优先执行,防止API26+前台服务5秒超时) ======================
|
||||
/**
|
||||
* 立即初始化前台通知,防止API26+前台服务超时异常
|
||||
* @return true=成功 false=失败
|
||||
@@ -154,7 +164,7 @@ public class ControlCenterService extends Service {
|
||||
|
||||
mNotificationManager.startForegroundServiceNotify(this, mForegroundNotifyMsg);
|
||||
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;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "initForegroundNotificationImmediately:通知初始化异常", e);
|
||||
@@ -175,7 +185,7 @@ public class ControlCenterService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 配置管理(本地持久化+内存同步)=================================
|
||||
// ====================== 配置管理(本地持久化+内存同步) ======================
|
||||
/**
|
||||
* 加载本地最新服务控制配置
|
||||
*/
|
||||
@@ -184,7 +194,7 @@ public class ControlCenterService extends Service {
|
||||
ControlCenterServiceBean latestBean = ControlCenterServiceBean.loadBean(this, ControlCenterServiceBean.class);
|
||||
if (latestBean != null) {
|
||||
mServiceControlBean = latestBean;
|
||||
LogUtils.d(TAG, "loadLatestServiceControlConfig:配置读取成功 | 启用=" + mServiceControlBean.isEnableService());
|
||||
LogUtils.d(TAG, String.format("loadLatestServiceControlConfig:配置读取成功 | 启用=%b", mServiceControlBean.isEnableService()));
|
||||
} else {
|
||||
LogUtils.w(TAG, "loadLatestServiceControlConfig:本地无配置,沿用内存配置");
|
||||
}
|
||||
@@ -202,13 +212,14 @@ public class ControlCenterService extends Service {
|
||||
mCurrentConfigBean.setEnableUsageReminder(true);
|
||||
mCurrentConfigBean.setUsageReminderValue(DEFAULT_USAGE_REMINDER_VALUE);
|
||||
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 {
|
||||
LogUtils.d(TAG, "loadDefaultConfig:内存已有配置,无需加载");
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 业务组件初始化与销毁(Handler/广播/线程等)=================================
|
||||
// ====================== 业务组件初始化与销毁(Handler/广播/线程等) ======================
|
||||
/**
|
||||
* 初始化Handler等核心业务组件
|
||||
*/
|
||||
@@ -225,7 +236,7 @@ public class ControlCenterService extends Service {
|
||||
if (mControlCenterServiceReceiver == null) {
|
||||
mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this);
|
||||
mControlCenterServiceReceiver.registerAction(this);
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:广播接收器初始化并注册完成 | 接收器=" + mControlCenterServiceReceiver);
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:广播接收器初始化并注册完成");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:广播接收器已存在");
|
||||
}
|
||||
@@ -283,13 +294,13 @@ public class ControlCenterService extends Service {
|
||||
LogUtils.d(TAG, "clearAllReferences:引用清理完成");
|
||||
}
|
||||
|
||||
// ================================== 外部调用接口(静态方法,提供服务启停/配置更新入口)=================================
|
||||
// ====================== 外部调用接口(静态方法,提供服务启停/配置更新入口) ======================
|
||||
/**
|
||||
* 外部启动服务的统一入口
|
||||
* @param 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) {
|
||||
LogUtils.e(TAG, "startControlCenterService:Context为空,启动失败");
|
||||
return;
|
||||
@@ -298,11 +309,11 @@ public class ControlCenterService extends Service {
|
||||
// 保存启用配置
|
||||
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(true);
|
||||
ControlCenterServiceBean.saveBean(context, controlBean);
|
||||
LogUtils.d(TAG, "startControlCenterService:服务启用配置已保存 | 配置=" + controlBean);
|
||||
LogUtils.d(TAG, "startControlCenterService:服务启用配置已保存");
|
||||
|
||||
// 启动服务(区分API版本)
|
||||
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);
|
||||
LogUtils.d(TAG, "startControlCenterService:以前台服务方式启动(API26+)");
|
||||
} else {
|
||||
@@ -316,7 +327,7 @@ public class ControlCenterService extends Service {
|
||||
* @param 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) {
|
||||
LogUtils.e(TAG, "stopControlCenterService:Context为空,停止失败");
|
||||
return;
|
||||
@@ -325,7 +336,7 @@ public class ControlCenterService extends Service {
|
||||
// 保存停用配置
|
||||
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(false);
|
||||
ControlCenterServiceBean.saveBean(context, controlBean);
|
||||
LogUtils.d(TAG, "stopControlCenterService:服务停用配置已保存 | 配置=" + controlBean);
|
||||
LogUtils.d(TAG, "stopControlCenterService:服务停用配置已保存");
|
||||
|
||||
// 停止服务
|
||||
Intent intent = new Intent(context, ControlCenterService.class);
|
||||
@@ -337,28 +348,26 @@ public class ControlCenterService extends Service {
|
||||
* 外部更新配置并触发线程重启
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void sendAppConfigStatusUpdateMessage(Context context) {
|
||||
LogUtils.d(TAG, "sendAppConfigStatusUpdateMessage执行 | context=" + context);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "sendAppConfigStatusUpdateMessage:参数为空,更新失败");
|
||||
return;
|
||||
}
|
||||
public static void sendAppConfigStatusUpdateMessage(Context context) {
|
||||
LogUtils.d(TAG, String.format("sendAppConfigStatusUpdateMessage执行 | context=%s", context));
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "sendAppConfigStatusUpdateMessage:参数为空,更新失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
|
||||
intent.setPackage(context.getPackageName());
|
||||
// 新增:发送广播并记录结果
|
||||
context.sendBroadcast(intent);
|
||||
LogUtils.d(TAG, "sendAppConfigStatusUpdateMessage:配置更新广播发送 :action=" + ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
|
||||
intent.setPackage(context.getPackageName());
|
||||
context.sendBroadcast(intent);
|
||||
LogUtils.d(TAG, String.format("sendAppConfigStatusUpdateMessage:配置更新广播发送 | action=%s", ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并引导用户开启忽略电池优化(API23+)
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void checkIgnoreBatteryOptimization(Context context) {
|
||||
LogUtils.d(TAG, "checkIgnoreBatteryOptimization执行 | context=" + context);
|
||||
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization执行 | context=%s", context));
|
||||
if (context == null || Build.VERSION.SDK_INT < API_LEVEL_23) {
|
||||
LogUtils.w(TAG, "checkIgnoreBatteryOptimization:无需检查(Context为空或API<23)");
|
||||
return;
|
||||
}
|
||||
@@ -371,14 +380,14 @@ public class ControlCenterService extends Service {
|
||||
|
||||
String packageName = context.getPackageName();
|
||||
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(packageName);
|
||||
LogUtils.d(TAG, "checkIgnoreBatteryOptimization:已忽略电池优化=" + isIgnored);
|
||||
LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization:已忽略电池优化=%b", isIgnored));
|
||||
|
||||
if (!isIgnored) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + packageName));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
context.startActivity(intent);
|
||||
LogUtils.d(TAG, "checkIgnoreBatteryOptimization:已跳转至系统设置页 | package=" + packageName);
|
||||
LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization:已跳转至系统设置页 | package=%s", packageName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +398,7 @@ public class ControlCenterService extends Service {
|
||||
* @return true=运行中 false=未运行
|
||||
*/
|
||||
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) {
|
||||
LogUtils.e(TAG, "isServiceRunning:参数为空");
|
||||
return false;
|
||||
@@ -405,7 +414,7 @@ public class ControlCenterService extends Service {
|
||||
String packageName = context.getPackageName();
|
||||
String serviceClassName = serviceClass.getName();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_30) {
|
||||
// API30+ 禁止获取其他应用服务,通过进程状态判断
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
|
||||
if (processes != null) {
|
||||
@@ -418,7 +427,7 @@ public class ControlCenterService extends Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning:API30+ 判断结果=" + isRunning);
|
||||
LogUtils.d(TAG, String.format("isServiceRunning:API30+ 判断结果=%b", isRunning));
|
||||
} else {
|
||||
// API30- 通过服务列表判断
|
||||
List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(RUNNING_SERVICE_LIST_LIMIT);
|
||||
@@ -430,13 +439,13 @@ public class ControlCenterService extends Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning:API30- 判断结果=" + isRunning);
|
||||
LogUtils.d(TAG, String.format("isServiceRunning:API30- 判断结果=%b", isRunning));
|
||||
}
|
||||
|
||||
// 兜底判断:配置启用状态
|
||||
if (!isRunning) {
|
||||
isRunning = isServiceStarted(context, serviceClass);
|
||||
LogUtils.d(TAG, "isServiceRunning:兜底判断结果=" + isRunning);
|
||||
LogUtils.d(TAG, String.format("isServiceRunning:兜底判断结果=%b", isRunning));
|
||||
}
|
||||
return isRunning;
|
||||
}
|
||||
@@ -455,23 +464,25 @@ public class ControlCenterService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 业务方法(配置更新/电池状态回调)=================================
|
||||
// ====================== 业务方法(配置更新/电池状态回调) ======================
|
||||
/**
|
||||
* 接收外部配置更新,同步到提醒线程
|
||||
* @param 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) {
|
||||
mCurrentConfigBean = latestConfig;
|
||||
RemindThread.startRemindThreadWithAppConfig(this, mServiceHandler, latestConfig);
|
||||
LogUtils.d(TAG, "notifyAppConfigUpdate:配置已同步到提醒线程");
|
||||
} 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() {
|
||||
return mServiceControlBean;
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 提醒线程(多实例列表管理)
|
||||
* 电量通知提醒线程(多实例列表管理)
|
||||
* 功能:管理充电/耗电提醒逻辑,触发条件时向Handler发送提醒消息
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
* 对外接口:{@link #startRemindThreadWithAppConfig(Context, ControlCenterServiceHandler, AppConfigBean)}、{
|
||||
* @link #startRemindThreadWithBatteryInfo(Context, ControlCenterServiceHandler, boolean, int)}、{@link #stopRemindThread()}
|
||||
* 对外接口:{@link #startRemindThreadWithAppConfig(Context, ControlCenterServiceHandler, AppConfigBean)}、
|
||||
* {@link #startRemindThreadWithBatteryInfo(Context, ControlCenterServiceHandler, boolean, int)}、{@link #stopRemindThread()}
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 电量通知提醒线程
|
||||
*/
|
||||
public class RemindThread extends Thread {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "RemindThread";
|
||||
|
||||
// 时间常量 (ms)
|
||||
@@ -32,10 +34,10 @@ public class RemindThread extends Thread {
|
||||
private static final String REMIND_TYPE_CHARGE = "+";
|
||||
private static final String REMIND_TYPE_USAGE = "-";
|
||||
|
||||
// ================================== 静态成员(多实例列表管理)=================================
|
||||
// ====================== 静态成员(多实例列表管理) ======================
|
||||
private static volatile ArrayList<RemindThread> sRemindThreadList;
|
||||
|
||||
// ================================== 成员变量区(按功能分层,volatile保证多线程可见性)=================================
|
||||
// ====================== 成员变量区(按功能分层,volatile保证多线程可见性) ======================
|
||||
// 并发安全锁(保护线程状态变更)
|
||||
private final Object mRemindLock = new Object();
|
||||
|
||||
@@ -56,16 +58,16 @@ public class RemindThread extends Thread {
|
||||
private volatile int quantityOfElectricity;
|
||||
private volatile boolean isCharging;
|
||||
|
||||
// ================================== 私有构造器(禁止外部实例化)=================================
|
||||
// ====================== 私有构造器(禁止外部实例化) ======================
|
||||
private RemindThread(Context context, ControlCenterServiceHandler handler) {
|
||||
LogUtils.d(TAG, "构造器调用 | context=" + context + " | handler=" + handler);
|
||||
LogUtils.d(TAG, String.format("构造器调用 | context=%s | handler=%s", context, handler));
|
||||
this.mContext = context.getApplicationContext();
|
||||
this.mwrControlCenterServiceHandler = new WeakReference<>(handler);
|
||||
resetThreadStateInternal();
|
||||
LogUtils.d(TAG, "构造完成 | threadId=" + getId() + " | 初始状态重置成功");
|
||||
LogUtils.d(TAG, String.format("构造完成 | threadId=%d | 初始状态重置成功", getId()));
|
||||
}
|
||||
|
||||
// ================================== 对外公开静态接口(多实例列表管理)=================================
|
||||
// ====================== 对外公开静态接口(多实例列表管理) ======================
|
||||
/**
|
||||
* 启动提醒线程,同步最新配置
|
||||
* 逻辑:停止所有旧线程 → 创建新线程 → 加入列表管理
|
||||
@@ -75,15 +77,15 @@ public class RemindThread extends Thread {
|
||||
* @return true: 启动成功;false: 入参非法
|
||||
*/
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 初始化线程列表
|
||||
// 初始化线程列表(双重校验锁)
|
||||
if (sRemindThreadList == null) {
|
||||
synchronized (RemindThread.class) {
|
||||
if (sRemindThreadList == null) {
|
||||
@@ -102,7 +104,7 @@ public class RemindThread extends Thread {
|
||||
newRemindThread.isExist = false;
|
||||
newRemindThread.start();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -116,15 +118,15 @@ public class RemindThread extends Thread {
|
||||
* @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);
|
||||
LogUtils.d(TAG, String.format("startRemindThreadWithBatteryInfo调用 | context=%s | handler=%s | isCharging=%b | batteryLevel=%d", context, handler, isCharging, batteryLevel));
|
||||
|
||||
// 入参严格校验
|
||||
if (context == null || handler == null) {
|
||||
LogUtils.e(TAG, "启动失败:入参为空 | context=" + context + " | handler=" + handler);
|
||||
LogUtils.e(TAG, String.format("启动失败:入参为空 | context=%s | handler=%s", context, handler));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化线程列表
|
||||
// 初始化线程列表(双重校验锁)
|
||||
if (sRemindThreadList == null) {
|
||||
synchronized (RemindThread.class) {
|
||||
if (sRemindThreadList == null) {
|
||||
@@ -139,13 +141,13 @@ public class RemindThread extends Thread {
|
||||
|
||||
// 创建并启动新线程
|
||||
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() + " | 电池状态同步完成");
|
||||
LogUtils.d(TAG, String.format("新线程启动成功 | threadId=%d | 电池状态同步完成(电量=%d,充电=%b)", newRemindThread.getId(), newRemindThread.quantityOfElectricity, newRemindThread.isCharging));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -153,7 +155,8 @@ public class RemindThread extends Thread {
|
||||
* 安全停止所有线程,清空列表
|
||||
*/
|
||||
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()) {
|
||||
LogUtils.w(TAG, "停止失败:线程列表为空");
|
||||
return;
|
||||
@@ -162,14 +165,14 @@ public class RemindThread extends Thread {
|
||||
// 标记所有线程退出
|
||||
for (RemindThread remindThread : sRemindThreadList) {
|
||||
remindThread.isExist = true;
|
||||
LogUtils.d(TAG, "标记线程退出 | threadId=" + remindThread.getId());
|
||||
LogUtils.d(TAG, String.format("标记线程退出 | threadId=%d", remindThread.getId()));
|
||||
}
|
||||
// 清空列表
|
||||
sRemindThreadList.clear();
|
||||
LogUtils.d(TAG, "所有线程已标记退出,列表已清空");
|
||||
}
|
||||
|
||||
// ================================== 私有静态辅助方法(多实例管理)=================================
|
||||
// ====================== 私有静态辅助方法(多实例管理) ======================
|
||||
/**
|
||||
* 停止所有旧线程并清空列表
|
||||
*/
|
||||
@@ -181,29 +184,29 @@ public class RemindThread extends Thread {
|
||||
// 标记所有旧线程退出
|
||||
for (RemindThread remindThread : sRemindThreadList) {
|
||||
remindThread.isExist = true;
|
||||
LogUtils.d(TAG, "标记旧线程退出 | threadId=" + remindThread.getId());
|
||||
LogUtils.d(TAG, String.format("标记旧线程退出 | threadId=%d", remindThread.getId()));
|
||||
}
|
||||
// 清空旧线程列表
|
||||
sRemindThreadList.clear();
|
||||
LogUtils.d(TAG, "旧线程已全部标记退出,列表已清空");
|
||||
}
|
||||
|
||||
// ================================== 线程核心运行逻辑=================================
|
||||
// ====================== 线程核心运行逻辑 ======================
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "run执行 | threadId=" + getId() + " | 状态=" + getState());
|
||||
LogUtils.d(TAG, String.format("run执行 | threadId=%d | 状态=%s", getId(), getState()));
|
||||
|
||||
// 初始化提醒状态(加锁保护,避免多线程竞争)
|
||||
synchronized (mRemindLock) {
|
||||
if (isReminding) {
|
||||
LogUtils.w(TAG, "线程已在提醒状态,退出运行 | threadId=" + getId());
|
||||
LogUtils.w(TAG, String.format("线程已在提醒状态,退出运行 | threadId=%d", getId()));
|
||||
return;
|
||||
}
|
||||
isReminding = true;
|
||||
}
|
||||
|
||||
// 核心电量检测循环
|
||||
LogUtils.d(TAG, "进入电量检测循环 | 休眠时间=" + sleepTime + "ms | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("进入电量检测循环 | 休眠时间=%dms | threadId=%d", sleepTime, getId()));
|
||||
while (!isExist) {
|
||||
try {
|
||||
// 快速退出判断
|
||||
@@ -211,37 +214,40 @@ public class RemindThread extends Thread {
|
||||
|
||||
// 电量有效性校验(非0-100视为无效),退出电量提醒线程
|
||||
if (quantityOfElectricity < BATTERY_LEVEL_MIN || quantityOfElectricity > BATTERY_LEVEL_MAX) {
|
||||
LogUtils.w(TAG, "电量无效,退出电量提醒线程 | 当前电量=" + quantityOfElectricity + " | threadId=" + getId());
|
||||
LogUtils.w(TAG, String.format("电量无效,退出电量提醒线程 | 当前电量=%d | threadId=%d", quantityOfElectricity, getId()));
|
||||
break;
|
||||
}
|
||||
|
||||
// 充电/耗电提醒触发逻辑
|
||||
if (isCharging && isEnableChargeReminder && quantityOfElectricity >= chargeReminderValue) {
|
||||
LogUtils.d(TAG, "触发充电提醒 | 当前电量=" + quantityOfElectricity + " ≥ 阈值=" + chargeReminderValue + " | threadId=" + getId());
|
||||
boolean chargeRemindTrigger = isCharging && isEnableChargeReminder && quantityOfElectricity >= chargeReminderValue;
|
||||
boolean usageRemindTrigger = !isCharging && isEnableUsageReminder && quantityOfElectricity <= usageReminderValue;
|
||||
|
||||
if (chargeRemindTrigger) {
|
||||
LogUtils.d(TAG, String.format("触发充电提醒 | 当前电量=%d ≥ 阈值=%d | threadId=%d", quantityOfElectricity, chargeReminderValue, getId()));
|
||||
sendNotificationMessageInternal(REMIND_TYPE_CHARGE, quantityOfElectricity, isCharging);
|
||||
} else if (!isCharging && isEnableUsageReminder && quantityOfElectricity <= usageReminderValue) {
|
||||
LogUtils.d(TAG, "触发耗电提醒 | 当前电量=" + quantityOfElectricity + " ≤ 阈值=" + usageReminderValue + " | threadId=" + getId());
|
||||
} else if (usageRemindTrigger) {
|
||||
LogUtils.d(TAG, String.format("触发耗电提醒 | 当前电量=%d ≤ 阈值=%d | threadId=%d", quantityOfElectricity, usageReminderValue, getId()));
|
||||
sendNotificationMessageInternal(REMIND_TYPE_USAGE, quantityOfElectricity, isCharging);
|
||||
} else {
|
||||
// 未有合适类型提醒,退出提醒线程
|
||||
LogUtils.d(TAG, "未有合适类型提醒,退出提醒线程");
|
||||
break;
|
||||
}
|
||||
// 安全休眠,保留中断标记
|
||||
LogUtils.d(TAG, String.format("未有合适类型提醒,退出提醒线程 | threadId=%d", getId()));
|
||||
break;
|
||||
}
|
||||
|
||||
// 安全休眠,保留中断标记
|
||||
safeSleepInternal(sleepTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "循环运行异常,退出电量提醒线程 | 当前电量=" + quantityOfElectricity + " | threadId=" + getId(), e);
|
||||
LogUtils.e(TAG, String.format("循环运行异常,退出电量提醒线程 | 当前电量=%d | threadId=%d", quantityOfElectricity, getId()), e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 循环退出,清理状态
|
||||
cleanThreadStateInternal();
|
||||
LogUtils.d(TAG, "run结束 | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("run结束 | threadId=%d", getId()));
|
||||
}
|
||||
|
||||
// ================================== 内部业务辅助方法=================================
|
||||
// ====================== 内部业务辅助方法 ======================
|
||||
/**
|
||||
* 发送提醒消息到Handler(弱引用避免内存泄漏)
|
||||
* @param type 提醒类型:+充电/-耗电
|
||||
@@ -249,20 +255,21 @@ public class RemindThread extends Thread {
|
||||
* @param 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) {
|
||||
LogUtils.d(TAG, "消息发送跳过:线程已退出或提醒关闭 | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("消息发送跳过:线程已退出或提醒关闭 | threadId=%d", getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取弱引用的Handler
|
||||
// 获取弱引用的Handler(校验有效性)
|
||||
ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get();
|
||||
if (handler == null) {
|
||||
LogUtils.w(TAG, "消息发送失败:Handler已被回收 | threadId=" + getId());
|
||||
LogUtils.w(TAG, String.format("消息发送失败:Handler已被回收 | threadId=%d", getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建并发送消息
|
||||
Message message = Message.obtain(handler, ControlCenterServiceHandler.MSG_REMIND_TEXT);
|
||||
message.obj = type;
|
||||
message.arg1 = battery;
|
||||
@@ -270,9 +277,9 @@ public class RemindThread extends Thread {
|
||||
|
||||
try {
|
||||
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) {
|
||||
LogUtils.e(TAG, "消息发送异常 | threadId=" + getId(), e);
|
||||
LogUtils.e(TAG, String.format("消息发送异常 | threadId=%d", getId()), e);
|
||||
// 异常时回收Message,避免内存泄漏
|
||||
if (message != null) {
|
||||
message.recycle();
|
||||
@@ -285,12 +292,12 @@ public class RemindThread extends Thread {
|
||||
* @param millis 休眠时长(ms)
|
||||
*/
|
||||
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 {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LogUtils.w(TAG, "休眠被中断,线程准备退出 | threadId=" + getId());
|
||||
LogUtils.w(TAG, String.format("休眠被中断,线程准备退出 | threadId=%d", getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +305,7 @@ public class RemindThread extends Thread {
|
||||
* 重置线程初始状态(构造器专用)
|
||||
*/
|
||||
private void resetThreadStateInternal() {
|
||||
LogUtils.d(TAG, "resetThreadStateInternal调用 | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("resetThreadStateInternal调用 | threadId=%d", getId()));
|
||||
// 状态标记初始化
|
||||
isExist = false;
|
||||
isReminding = false;
|
||||
@@ -310,22 +317,23 @@ public class RemindThread extends Thread {
|
||||
usageReminderValue = -1;
|
||||
quantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
isCharging = false;
|
||||
LogUtils.d(TAG, "线程初始状态重置完成 | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("线程初始状态重置完成 | threadId=%d", getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理线程运行状态(循环退出时调用)
|
||||
*/
|
||||
private void cleanThreadStateInternal() {
|
||||
LogUtils.d(TAG, "cleanThreadStateInternal调用 | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("cleanThreadStateInternal调用 | threadId=%d", getId()));
|
||||
isReminding = false;
|
||||
isExist = true;
|
||||
quantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
// 中断当前线程(如果存活)
|
||||
if (isAlive()) {
|
||||
interrupt();
|
||||
LogUtils.d(TAG, String.format("线程已中断 | threadId=%d", getId()));
|
||||
}
|
||||
LogUtils.d(TAG, "线程运行状态清理完成 | threadId=" + getId());
|
||||
LogUtils.d(TAG, String.format("线程运行状态清理完成 | threadId=%d", getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,9 +341,9 @@ public class RemindThread extends Thread {
|
||||
* @param config 应用配置Bean
|
||||
*/
|
||||
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) {
|
||||
LogUtils.e(TAG, "配置同步失败:配置Bean为空 | threadId=" + getId());
|
||||
LogUtils.e(TAG, String.format("配置同步失败:配置Bean为空 | threadId=%d", getId()));
|
||||
quantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
return;
|
||||
}
|
||||
@@ -350,7 +358,8 @@ public class RemindThread extends Thread {
|
||||
? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE;
|
||||
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, quantityOfElectricity, chargeReminderValue, usageReminderValue, getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,13 +368,13 @@ public class RemindThread extends Thread {
|
||||
*/
|
||||
private boolean isRunning() {
|
||||
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;
|
||||
}
|
||||
|
||||
// ================================== Getter/Setter(按需开放)=================================
|
||||
// ====================== Getter/Setter(按需开放) ======================
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -373,7 +382,7 @@ public class RemindThread extends Thread {
|
||||
return isExist;
|
||||
}
|
||||
|
||||
// ================================== 调试辅助方法=================================
|
||||
// ====================== 调试辅助方法 ======================
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RemindThread{" +
|
||||
|
||||
@@ -16,32 +16,36 @@ import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/22 08:31
|
||||
* @Describe MainUnitTest2Activity
|
||||
* 单元测试页面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相关) ======================
|
||||
// ====================== 成员变量区(按功能分层,移除所有Uri相关) ======================
|
||||
private MemoryCachedBackgroundView mMemoryCachedBackgroundView;
|
||||
private LinearLayout mllBackgroundView;
|
||||
private String mAppPrivateDirPath;
|
||||
private File mPrivateTestImageFile; // 仅用File,不用Uri
|
||||
private File mPrivateTestImageFile;
|
||||
private File mPrivateCropImageFile;
|
||||
BackgroundBean mPreviewBackgroundBean;
|
||||
LinearLayout mllBackgroundView;
|
||||
private BackgroundBean mPreviewBackgroundBean;
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
// ====================== 生命周期方法(按执行顺序:onCreate→onActivityResult) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -50,65 +54,66 @@ public class MainUnitTest2Activity extends AppCompatActivity {
|
||||
initBaseParams();
|
||||
initViewAndEvent();
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
//loadBackgroundByFile(); // 直接用File加载
|
||||
mPreviewBackgroundBean = new BackgroundBean();
|
||||
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
doubleRefreshPreview();
|
||||
initBackgroundBean();
|
||||
doubleRefreshPreview();
|
||||
|
||||
ToastUtils.show("单元测试页面启动完成");
|
||||
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, "=== onActivityResult 回调 ===");
|
||||
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, "初始化基础参数:工具类+私有目录+File");
|
||||
|
||||
// 私有目录(无需权限,无UID冲突)
|
||||
LogUtils.d(TAG, "initBaseParams:初始化基础参数");
|
||||
// 初始化私有目录(无需权限,无UID冲突)
|
||||
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
|
||||
File privateDir = new File(mAppPrivateDirPath);
|
||||
if (!privateDir.exists()) {
|
||||
privateDir.mkdirs();
|
||||
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
|
||||
boolean isDirCreated = privateDir.mkdirs();
|
||||
LogUtils.d(TAG, String.format("initBaseParams:创建私有目录 | 路径=%s | 结果=%b", mAppPrivateDirPath, isDirCreated));
|
||||
}
|
||||
|
||||
// 初始化File(无Uri)
|
||||
// 初始化测试文件与裁剪文件(无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, "测试图File路径:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("initBaseParams:测试图路径=%s", mPrivateTestImageFile.getAbsolutePath()));
|
||||
LogUtils.d(TAG, String.format("initBaseParams:裁剪图路径=%s", mPrivateCropImageFile.getAbsolutePath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化布局与控件事件(含单例视图创建)
|
||||
*/
|
||||
private void initViewAndEvent() {
|
||||
LogUtils.d(TAG, "初始化布局与控件事件");
|
||||
LogUtils.d(TAG, "initViewAndEvent:初始化布局与控件事件");
|
||||
setContentView(R.layout.activity_mainunittest2);
|
||||
mllBackgroundView = (LinearLayout) findViewById(R.id.ll_backgroundview);
|
||||
mMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, "", false);
|
||||
mllBackgroundView.addView(mMemoryCachedBackgroundView);
|
||||
|
||||
//mMemoryCachedBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
|
||||
mllBackgroundView = (LinearLayout) findViewById(R.id.ll_backgroundview);
|
||||
|
||||
// 创建MemoryCachedBackgroundView单例并添加到布局
|
||||
mMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, "", false);
|
||||
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, "点击按钮:跳转主页面");
|
||||
LogUtils.d(TAG, "initViewAndEvent:点击按钮→跳转主页面");
|
||||
startActivity(new Intent(MainUnitTest2Activity.this, MainActivity.class));
|
||||
}
|
||||
});
|
||||
@@ -118,11 +123,11 @@ public class MainUnitTest2Activity extends AppCompatActivity {
|
||||
btnCrop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:启动裁剪(File路径版)");
|
||||
LogUtils.d(TAG, "initViewAndEvent:点击按钮→启动裁剪(File路径版)");
|
||||
ToastUtils.show("准备启动图片裁剪");
|
||||
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
startCropTestByFile(); // 直接传File
|
||||
if (isFileValid(mPrivateTestImageFile)) {
|
||||
startCropTestByFile();
|
||||
} else {
|
||||
ToastUtils.show("测试图片未准备好,重新拷贝");
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
@@ -131,11 +136,29 @@ public class MainUnitTest2Activity 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() {
|
||||
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
LogUtils.d(TAG, "图片已存在,无需拷贝");
|
||||
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir:开始拷贝assets图片到私有目录");
|
||||
if (isFileValid(mPrivateTestImageFile)) {
|
||||
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir:图片已存在,无需拷贝");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,97 +166,87 @@ public class MainUnitTest2Activity extends AppCompatActivity {
|
||||
try {
|
||||
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
|
||||
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
|
||||
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
|
||||
LogUtils.d(TAG, String.format("copyAssetsTestImageToPrivateDir:图片拷贝成功 | 大小=%d字节", mPrivateTestImageFile.length()));
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), 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, "关闭流失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir:关闭流失败 | %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法(全改为File路径) ======================
|
||||
/** 直接用File路径加载背景图(无Uri,无冲突) */
|
||||
// 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重载方法) */
|
||||
/**
|
||||
* 直接用File启动裁剪(关键:调用ImageCropUtils的File重载方法)
|
||||
*/
|
||||
private void startCropTestByFile() {
|
||||
LogUtils.d(TAG, "启动裁剪(File路径版),原图:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("startCropTestByFile:启动裁剪 | 原图=%s", mPrivateTestImageFile.getAbsolutePath()));
|
||||
|
||||
// 确保输出目录存在
|
||||
File cropParent = mPrivateCropImageFile.getParentFile();
|
||||
if (!cropParent.exists()) {
|
||||
cropParent.mkdirs();
|
||||
boolean isDirCreated = cropParent.mkdirs();
|
||||
LogUtils.d(TAG, String.format("startCropTestByFile:创建裁剪目录 | 路径=%s | 结果=%b", cropParent.getAbsolutePath(), isDirCreated));
|
||||
}
|
||||
|
||||
// 调用ImageCropUtils的File参数方法(核心:绕开Uri)
|
||||
ImageCropUtils.startImageCrop(
|
||||
this,
|
||||
mPrivateTestImageFile, // 原图File
|
||||
mPrivateCropImageFile, // 输出File
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
this,
|
||||
mPrivateTestImageFile,
|
||||
mPrivateCropImageFile,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
|
||||
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("startCropTestByFile:裁剪请求已发送 | 输出路径=%s", mPrivateCropImageFile.getAbsolutePath()));
|
||||
ToastUtils.show("已启动图片裁剪");
|
||||
}
|
||||
|
||||
/** 处理裁剪结果(直接校验输出File) */
|
||||
/**
|
||||
* 处理裁剪结果(直接校验输出File)
|
||||
* @param 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 (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
|
||||
if (isFileValid(mPrivateCropImageFile)) {
|
||||
mMemoryCachedBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("handleCropResult:裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
|
||||
ToastUtils.show("裁剪成功");
|
||||
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
doubleRefreshPreview();
|
||||
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
doubleRefreshPreview();
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪成功但输出文件无效");
|
||||
LogUtils.e(TAG, "handleCropResult:裁剪成功但输出文件无效");
|
||||
ToastUtils.show("裁剪失败:输出文件无效");
|
||||
}
|
||||
} else if (resultCode == RESULT_CANCELED) {
|
||||
LogUtils.d(TAG, "裁剪取消");
|
||||
LogUtils.d(TAG, "handleCropResult:裁剪取消");
|
||||
ToastUtils.show("裁剪已取消");
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪失败:resultCode异常");
|
||||
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, "【双重刷新】第一重完成");
|
||||
mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "doubleRefreshPreview:【双重刷新】第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
|
||||
LogUtils.e(TAG, String.format("doubleRefreshPreview:【双重刷新】第一重异常 | %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,13 +258,25 @@ public class MainUnitTest2Activity extends AppCompatActivity {
|
||||
try {
|
||||
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
LogUtils.d(TAG, "doubleRefreshPreview:【双重刷新】第二重完成");
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,32 +12,38 @@ 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.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
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.BackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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 static final String TAG = "MainUnitTestActivity";
|
||||
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相关) ======================
|
||||
// ====================== 成员变量区(按功能分层,移除所有Uri相关) ======================
|
||||
private BackgroundView mBackgroundView;
|
||||
private String mAppPrivateDirPath;
|
||||
private File mPrivateTestImageFile; // 仅用File,不用Uri
|
||||
private File mPrivateCropImageFile;
|
||||
BackgroundBean mPreviewBackgroundBean;
|
||||
private BackgroundBean mPreviewBackgroundBean;
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
// ====================== 生命周期方法(按执行顺序:onCreate→onActivityResult) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -46,15 +52,9 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
initBaseParams();
|
||||
initViewAndEvent();
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
//loadBackgroundByFile(); // 直接用File加载
|
||||
mPreviewBackgroundBean = new BackgroundBean();
|
||||
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
doubleRefreshPreview();
|
||||
|
||||
initBackgroundBean();
|
||||
doubleRefreshPreview();
|
||||
|
||||
ToastUtils.show("单元测试页面启动完成");
|
||||
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
|
||||
}
|
||||
@@ -62,36 +62,42 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent 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) {
|
||||
handleCropResult(resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 初始化相关方法 ======================
|
||||
// ====================== 初始化相关方法(基础参数→视图→背景Bean) ======================
|
||||
/**
|
||||
* 初始化基础参数:私有目录、测试文件
|
||||
*/
|
||||
private void initBaseParams() {
|
||||
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File");
|
||||
|
||||
// 私有目录(无需权限,无UID冲突)
|
||||
LogUtils.d(TAG, "initBaseParams:初始化基础参数");
|
||||
// 初始化私有目录(无需权限,无UID冲突)
|
||||
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
|
||||
File privateDir = new File(mAppPrivateDirPath);
|
||||
if (!privateDir.exists()) {
|
||||
privateDir.mkdirs();
|
||||
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
|
||||
boolean isDirCreated = privateDir.mkdirs();
|
||||
LogUtils.d(TAG, String.format("initBaseParams:创建私有目录 | 路径=%s | 结果=%b", mAppPrivateDirPath, isDirCreated));
|
||||
}
|
||||
|
||||
// 初始化File(无Uri)
|
||||
// 初始化测试文件与裁剪文件(无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, "测试图File路径:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("initBaseParams:测试图路径=%s", mPrivateTestImageFile.getAbsolutePath()));
|
||||
LogUtils.d(TAG, String.format("initBaseParams:裁剪图路径=%s", mPrivateCropImageFile.getAbsolutePath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化布局与控件事件
|
||||
*/
|
||||
private void initViewAndEvent() {
|
||||
LogUtils.d(TAG, "初始化布局与控件事件");
|
||||
LogUtils.d(TAG, "initViewAndEvent:初始化布局与控件事件");
|
||||
setContentView(R.layout.activity_mainunittest);
|
||||
mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
|
||||
|
||||
@@ -100,7 +106,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
btnMain.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:跳转主页面");
|
||||
LogUtils.d(TAG, "initViewAndEvent:点击按钮→跳转主页面");
|
||||
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
|
||||
}
|
||||
});
|
||||
@@ -110,11 +116,11 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
btnCrop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:启动裁剪(File路径版)");
|
||||
LogUtils.d(TAG, "initViewAndEvent:点击按钮→启动裁剪(File路径版)");
|
||||
ToastUtils.show("准备启动图片裁剪");
|
||||
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
startCropTestByFile(); // 直接传File
|
||||
if (isFileValid(mPrivateTestImageFile)) {
|
||||
startCropTestByFile();
|
||||
} else {
|
||||
ToastUtils.show("测试图片未准备好,重新拷贝");
|
||||
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() {
|
||||
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
LogUtils.d(TAG, "图片已存在,无需拷贝");
|
||||
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir:开始拷贝assets图片到私有目录");
|
||||
if (isFileValid(mPrivateTestImageFile)) {
|
||||
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir:图片已存在,无需拷贝");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,97 +159,87 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
try {
|
||||
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
|
||||
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
|
||||
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
|
||||
LogUtils.d(TAG, String.format("copyAssetsTestImageToPrivateDir:图片拷贝成功 | 大小=%d字节", mPrivateTestImageFile.length()));
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), 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, "关闭流失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir:关闭流失败 | %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法(全改为File路径) ======================
|
||||
/** 直接用File路径加载背景图(无Uri,无冲突) */
|
||||
// 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重载方法) */
|
||||
/**
|
||||
* 直接用File启动裁剪(关键:调用ImageCropUtils的File重载方法)
|
||||
*/
|
||||
private void startCropTestByFile() {
|
||||
LogUtils.d(TAG, "启动裁剪(File路径版),原图:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("startCropTestByFile:启动裁剪 | 原图=%s", mPrivateTestImageFile.getAbsolutePath()));
|
||||
|
||||
// 确保输出目录存在
|
||||
File cropParent = mPrivateCropImageFile.getParentFile();
|
||||
if (!cropParent.exists()) {
|
||||
cropParent.mkdirs();
|
||||
boolean isDirCreated = cropParent.mkdirs();
|
||||
LogUtils.d(TAG, String.format("startCropTestByFile:创建裁剪目录 | 路径=%s | 结果=%b", cropParent.getAbsolutePath(), isDirCreated));
|
||||
}
|
||||
|
||||
// 调用ImageCropUtils的File参数方法(核心:绕开Uri)
|
||||
ImageCropUtils.startImageCrop(
|
||||
this,
|
||||
mPrivateTestImageFile, // 原图File
|
||||
mPrivateCropImageFile, // 输出File
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
this,
|
||||
mPrivateTestImageFile, // 原图File
|
||||
mPrivateCropImageFile, // 输出File
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
|
||||
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("startCropTestByFile:裁剪请求已发送 | 输出路径=%s", mPrivateCropImageFile.getAbsolutePath()));
|
||||
ToastUtils.show("已启动图片裁剪");
|
||||
}
|
||||
|
||||
/** 处理裁剪结果(直接校验输出File) */
|
||||
/**
|
||||
* 处理裁剪结果(直接校验输出File)
|
||||
* @param 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 (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
|
||||
if (isFileValid(mPrivateCropImageFile)) {
|
||||
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, String.format("handleCropResult:裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
|
||||
ToastUtils.show("裁剪成功");
|
||||
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
doubleRefreshPreview();
|
||||
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
doubleRefreshPreview();
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪成功但输出文件无效");
|
||||
LogUtils.e(TAG, "handleCropResult:裁剪成功但输出文件无效");
|
||||
ToastUtils.show("裁剪失败:输出文件无效");
|
||||
}
|
||||
} else if (resultCode == RESULT_CANCELED) {
|
||||
LogUtils.d(TAG, "裁剪取消");
|
||||
LogUtils.d(TAG, "handleCropResult:裁剪取消");
|
||||
ToastUtils.show("裁剪已取消");
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪失败:resultCode异常");
|
||||
LogUtils.e(TAG, String.format("handleCropResult:裁剪失败 | resultCode异常=%d", resultCode));
|
||||
ToastUtils.show("裁剪失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
* 移除:缓存清空逻辑
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
|
||||
LogUtils.d(TAG, "doubleRefreshPreview:执行双重刷新预览");
|
||||
// 第一重刷新
|
||||
try {
|
||||
mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第一重完成");
|
||||
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "doubleRefreshPreview:【双重刷新】第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
|
||||
LogUtils.e(TAG, String.format("doubleRefreshPreview:【双重刷新】第一重异常 | %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,13 +251,25 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
try {
|
||||
mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
LogUtils.d(TAG, "doubleRefreshPreview:【双重刷新】第二重完成");
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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.Context;
|
||||
import android.content.Intent;
|
||||
@@ -13,75 +8,143 @@ import android.os.Build;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
/**
|
||||
* 应用图标切换工具类(启用组件时创建对应快捷方式)
|
||||
* 适配:Java7 | API30 | 高低版本快捷方式创建兼容
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 应用启动器组件切换与桌面快捷方式创建工具,支持多组件管理与版本兼容
|
||||
*/
|
||||
public class 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) {
|
||||
LogUtils.d(TAG, String.format("switchAppLauncherToComponent调用 | 传入组件名=%s", componentName));
|
||||
|
||||
// 参数校验
|
||||
if (context == null) {
|
||||
LogUtils.d(TAG, "切换失败:上下文为空");
|
||||
Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败", Toast.LENGTH_SHORT).show();
|
||||
LogUtils.e(TAG, "switchAppLauncherToComponent失败:上下文为空");
|
||||
return false;
|
||||
}
|
||||
if (componentName == null || componentName.isEmpty()) {
|
||||
LogUtils.e(TAG, "switchAppLauncherToComponent失败:组件名为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
ComponentName plusComponentSwitchTo = new ComponentName(context, componentName);
|
||||
ComponentName plusComponentEN1 = new ComponentName(context, App.COMPONENT_EN1);
|
||||
ComponentName plusComponentCN1 = new ComponentName(context, App.COMPONENT_CN1);
|
||||
ComponentName plusComponentCN2 = new ComponentName(context, App.COMPONENT_CN2);
|
||||
ComponentName targetComponent = new ComponentName(context, componentName);
|
||||
ComponentName en1Component = new ComponentName(context, App.COMPONENT_EN1);
|
||||
ComponentName cn1Component = new ComponentName(context, App.COMPONENT_CN1);
|
||||
ComponentName cn2Component = new ComponentName(context, App.COMPONENT_CN2);
|
||||
|
||||
try {
|
||||
disableComponent(pm, plusComponentEN1);
|
||||
disableComponent(pm, plusComponentCN1);
|
||||
disableComponent(pm, plusComponentCN2);
|
||||
enableComponent(pm, plusComponentSwitchTo);
|
||||
// 禁用所有其他启动器组件
|
||||
disableComponent(pm, en1Component);
|
||||
disableComponent(pm, cn1Component);
|
||||
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;
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "图标切换失败:" + e.getMessage());
|
||||
Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
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();
|
||||
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+)
|
||||
* @param component 目标组件(如 LAOJUN_ACTIVITY)
|
||||
* @param context 上下文
|
||||
* @param component 目标组件
|
||||
* @param name 快捷方式名称
|
||||
* @param iconRes 快捷方式图标资源ID
|
||||
* @return 是否创建成功
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// 图标资源默认值补全
|
||||
int finalIconRes = iconRes != 0 ? iconRes : SHORTCUT_ICON_DEFAULT;
|
||||
|
||||
// Android 8.0+(API 26+):使用 ShortcutManager(系统推荐)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class);
|
||||
if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) {
|
||||
LogUtils.d(TAG, "系统不支持创建快捷方式");
|
||||
LogUtils.w(TAG, "createComponentShortcut:系统不支持创建快捷方式");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已存在该组件的快捷方式(去重)
|
||||
for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) {
|
||||
if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) {
|
||||
LogUtils.d(TAG, "快捷方式已存在:" + component.getClassName());
|
||||
LogUtils.d(TAG, String.format("createComponentShortcut:快捷方式已存在=%s", componentName));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -96,16 +159,17 @@ public class APPPlusUtils {
|
||||
android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName())
|
||||
.setShortLabel(name)
|
||||
.setLongLabel(name)
|
||||
.setIcon(android.graphics.drawable.Icon.createWithResource(context, iconRes))
|
||||
.setIcon(android.graphics.drawable.Icon.createWithResource(context, finalIconRes))
|
||||
.setIntent(launchIntent)
|
||||
.build();
|
||||
|
||||
// 请求创建快捷方式(需用户确认)
|
||||
shortcutManager.requestPinShortcut(shortcutInfo, null);
|
||||
LogUtils.d(TAG, "createComponentShortcut:Android O+ 快捷方式创建请求已发送");
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "Android O+ 快捷方式创建失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, String.format("createComponentShortcut失败 | Android O+ 异常=%s", e.getMessage()), e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -118,47 +182,22 @@ public class APPPlusUtils {
|
||||
.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_NAME, name);
|
||||
installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
|
||||
Intent.ShortcutIconResource.fromContext(context, iconRes));
|
||||
Intent.ShortcutIconResource.fromContext(context, finalIconRes));
|
||||
installIntent.putExtra("duplicate", false); // 禁止重复创建
|
||||
|
||||
context.sendBroadcast(installIntent);
|
||||
LogUtils.d(TAG, "createComponentShortcut:Android O- 快捷方式创建广播已发送");
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "Android O- 快捷方式创建失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, String.format("createComponentShortcut失败 | Android O- 异常=%s", e.getMessage()), e);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,81 +5,139 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 应用缓存工具类(适配Android API 30,基于Java 7编写)
|
||||
* 负责电池信息的缓存、持久化与管理
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 电池信息缓存工具:实现电量变化记录、持久化存储与缓存限制
|
||||
*/
|
||||
public class AppCacheUtils {
|
||||
// ===================== 静态常量区(置顶归类,消除魔法值) =====================
|
||||
public static final String TAG = "AppCacheUtils";
|
||||
private static final int MAX_BATTERY_RECORD_COUNT = 180; // 电池记录最大条数限制
|
||||
|
||||
// 保存唯一配置实例
|
||||
static AppCacheUtils _mAppCacheUtils;
|
||||
// 配置实例引用的上下文环境
|
||||
Context mContext;
|
||||
// 配置实例的数据的存储文件路径
|
||||
//volatile String mAppCacheDataFilePath = null;
|
||||
ArrayList<BatteryInfoBean> mlBatteryInfo;
|
||||
// ===================== 静态成员区(单例相关) =====================
|
||||
private static AppCacheUtils sInstance;
|
||||
|
||||
// 私有实例构造方法
|
||||
//
|
||||
AppCacheUtils(Context context) {
|
||||
mContext = context;
|
||||
//mAppCacheDataFilePath = context.getExternalFilesDir(TAG) + File.separator + "mlBatteryInfo.dat";
|
||||
mlBatteryInfo = new ArrayList<BatteryInfoBean>();
|
||||
loadAppCacheData();
|
||||
}
|
||||
// ===================== 成员变量区(按功能分层) =====================
|
||||
private Context mContext; // ApplicationContext,避免内存泄漏
|
||||
private ArrayList<BatteryInfoBean> mBatteryInfoList; // 电池信息缓存列表
|
||||
|
||||
// 返回唯一实例
|
||||
//
|
||||
// ===================== 单例方法区(线程安全) =====================
|
||||
/**
|
||||
* 获取单例实例
|
||||
* @param context 上下文(内部会转换为ApplicationContext)
|
||||
* @return 唯一AppCacheUtils实例
|
||||
*/
|
||||
public static synchronized AppCacheUtils getInstance(Context context) {
|
||||
if (_mAppCacheUtils == null) {
|
||||
_mAppCacheUtils = new AppCacheUtils(context);
|
||||
String contextType = context != null ? context.getClass().getSimpleName() : "null";
|
||||
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) {
|
||||
addChangingTimeToList(nBattetyValue);
|
||||
//LogUtils.d(TAG, "nBattetyValue is "+Integer.toString(nBattetyValue));
|
||||
// ===================== 私有构造方法区(禁止外部实例化) =====================
|
||||
/**
|
||||
* 私有构造方法,初始化缓存列表并加载持久化数据
|
||||
* @param context ApplicationContext
|
||||
*/
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电池信息缓存列表
|
||||
* @return 完整的电池信息列表
|
||||
*/
|
||||
public ArrayList<BatteryInfoBean> getArrayListBatteryInfo() {
|
||||
LogUtils.d(TAG, String.format("getArrayListBatteryInfo调用 | 当前缓存数量=%d", mBatteryInfoList.size()));
|
||||
loadAppCacheData();
|
||||
return mlBatteryInfo;
|
||||
}
|
||||
|
||||
// 读取文件存储的数据
|
||||
//
|
||||
void saveAppCacheData() {
|
||||
BatteryInfoBean.saveBeanList(mContext, mlBatteryInfo, BatteryInfoBean.class);
|
||||
}
|
||||
|
||||
// 保存数据到文件
|
||||
//
|
||||
void loadAppCacheData() {
|
||||
mlBatteryInfo.clear();
|
||||
BatteryInfoBean.loadBeanList(mContext, mlBatteryInfo, BatteryInfoBean.class);
|
||||
return mBatteryInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有电池历史记录
|
||||
*/
|
||||
public void clearBatteryHistory() {
|
||||
mlBatteryInfo.clear();
|
||||
LogUtils.d(TAG, String.format("clearBatteryHistory调用 | 清除前缓存数量=%d", mBatteryInfoList.size()));
|
||||
mBatteryInfoList.clear();
|
||||
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完成 | 数据已持久化");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
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.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 13:59
|
||||
* @Describe 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
|
||||
* 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
|
||||
* 适配:Java7 | API30 | 小米手机,单例模式,线程安全,配置持久化
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 应用配置全量管理工具,支持配置持久化、自动校准、线程安全访问
|
||||
*/
|
||||
public class AppConfigUtils {
|
||||
// ======================== 静态常量(顶部统一管理,抽离魔法值)========================
|
||||
// ======================== 静态常量区(魔法值统一管理)========================
|
||||
public static final String TAG = "AppConfigUtils";
|
||||
public static final String BACKGROUND_DIR = "Background"; // 背景图片存储目录
|
||||
private static final int MIN_REMINDER_VALUE = 0; // 提醒阈值最小值
|
||||
@@ -24,90 +21,102 @@ public class AppConfigUtils {
|
||||
private static final int MIN_INTERVAL_TIME = 1000; // 最小提醒间隔(ms)
|
||||
private static final int MIN_DETECT_INTERVAL = 500; // 最小电量检测间隔(ms)
|
||||
|
||||
// ======================== 静态成员(单例实例,严格控制初始化)========================
|
||||
private static AppConfigUtils sInstance; // 单例实例(私有,禁止外部直接创建)
|
||||
// ======================== 静态成员区(单例实例)========================
|
||||
private static volatile AppConfigUtils sInstance; // 单例实例(volatile保障双重校验锁有效性)
|
||||
|
||||
// ======================== 核心依赖属性(优先排列,final保障安全)========================
|
||||
private final Context mContext; // 应用上下文(避免内存泄漏)
|
||||
private App mApplication; // 应用Application实例
|
||||
// ======================== 成员变量区(按依赖优先级排序,final/volatile保障线程安全)========================
|
||||
private final Context mContext; // 应用上下文(ApplicationContext,避免内存泄漏)
|
||||
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读取次数,提升性能)========================
|
||||
private volatile boolean mIsServiceEnabled = false; // 服务开关缓存状态
|
||||
|
||||
|
||||
// ======================== 单例构造方法(私有,禁止外部实例化)========================
|
||||
private AppConfigUtils(Context context) {
|
||||
LogUtils.d(TAG, "初始化配置工具类");
|
||||
this.mContext = context.getApplicationContext(); // 强制取应用上下文,杜绝内存泄漏
|
||||
this.mApplication = (App) context.getApplicationContext();
|
||||
// 初始化配置Bean
|
||||
mAppConfigBean = new AppConfigBean();
|
||||
// 加载持久化配置
|
||||
loadAppConfig();
|
||||
LogUtils.d(TAG, "配置工具类初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 单例获取方法(双重校验锁,线程安全,适配多线程)========================
|
||||
// ======================== 单例相关方法区(双重校验锁+构造方法)========================
|
||||
/**
|
||||
* 双重校验锁单例获取方法,线程安全
|
||||
* @param context 上下文(不可为null)
|
||||
* @return 单例实例
|
||||
*/
|
||||
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) {
|
||||
LogUtils.e(TAG, "getInstance: Context不能为空,获取实例失败");
|
||||
LogUtils.e(TAG, "getInstance失败:Context不能为空");
|
||||
throw new IllegalArgumentException("Context cannot be null");
|
||||
}
|
||||
|
||||
if (sInstance == null) {
|
||||
synchronized (AppConfigUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new AppConfigUtils(context);
|
||||
LogUtils.d(TAG, "getInstance: 单例实例创建成功");
|
||||
LogUtils.d(TAG, "getInstance:单例实例创建成功");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "getInstance:单例实例获取成功");
|
||||
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() {
|
||||
LogUtils.d(TAG, "loadAllConfig: 开始加载所有配置");
|
||||
// 加载应用配置
|
||||
LogUtils.d(TAG, "loadAppConfig调用:开始加载应用配置");
|
||||
AppConfigBean savedAppBean = (AppConfigBean) AppConfigBean.loadBean(mContext, AppConfigBean.class);
|
||||
|
||||
if (savedAppBean != null) {
|
||||
mAppConfigBean = savedAppBean;
|
||||
LogUtils.d(TAG, "loadAllConfig: 应用配置加载成功");
|
||||
LogUtils.d(TAG, String.format("loadAppConfig成功 | 充电阈值=%d%% | 耗电阈值=%d%%",
|
||||
mAppConfigBean.getChargeReminderValue(), mAppConfigBean.getUsageReminderValue()));
|
||||
} else {
|
||||
mAppConfigBean = new AppConfigBean();
|
||||
mAppConfigBean = new AppConfigBean();
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
LogUtils.d(TAG, "loadAllConfig: 无已保存应用配置,使用默认值并持久化");
|
||||
LogUtils.d(TAG, "loadAppConfig:无已保存配置,使用默认值并持久化");
|
||||
}
|
||||
return mAppConfigBean;
|
||||
|
||||
return mAppConfigBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存应用配置(内部核心方法,直接持久化,同步通知服务+Activity)
|
||||
* 保存应用配置(内部核心方法,直接持久化)
|
||||
*/
|
||||
private void saveAppConfig() {
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
LogUtils.d(TAG, "saveAppConfig: 应用配置保存成功,已同步服务和Activity");
|
||||
LogUtils.d(TAG, "saveAppConfig:应用配置保存成功");
|
||||
}
|
||||
|
||||
// ======================== 充电提醒配置方法(单独归类,逻辑聚焦)========================
|
||||
// ======================== 充电提醒配置方法区(开关+阈值)========================
|
||||
/**
|
||||
* 设置充电提醒开关状态(直接生效,无弹窗)
|
||||
* 设置充电提醒开关状态
|
||||
* @param isEnabled 目标状态(true=开启,false=关闭)
|
||||
*/
|
||||
public void setChargeReminderEnabled(final boolean isEnabled) {
|
||||
LogUtils.d(TAG, String.format("setChargeReminderEnabled调用 | 传入状态=%b", isEnabled));
|
||||
|
||||
if (isEnabled == mAppConfigBean.isEnableChargeReminder()) {
|
||||
LogUtils.d(TAG, "setChargeReminderEnabled: 充电提醒状态无变化,无需操作");
|
||||
LogUtils.d(TAG, "setChargeReminderEnabled:充电提醒状态无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setEnableChargeReminder(isEnabled);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setChargeReminderEnabled: 充电提醒状态更新为=" + (isEnabled ? "开启" : "关闭"));
|
||||
LogUtils.d(TAG, String.format("setChargeReminderEnabled成功 | 充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,23 +125,26 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public boolean isChargeReminderEnabled() {
|
||||
boolean isEnabled = mAppConfigBean.isEnableChargeReminder();
|
||||
LogUtils.d(TAG, "isChargeReminderEnabled: 获取充电提醒状态=" + (isEnabled ? "开启" : "关闭"));
|
||||
LogUtils.d(TAG, String.format("isChargeReminderEnabled:获取充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置充电提醒阈值(直接生效,无弹窗,自动校准范围,适配API30数据安全)
|
||||
* @param value 目标阈值(自动校准0-100)
|
||||
* 设置充电提醒阈值(自动校准0-100)
|
||||
* @param 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);
|
||||
|
||||
if (calibratedValue == mAppConfigBean.getChargeReminderValue()) {
|
||||
LogUtils.d(TAG, "setChargeReminderValue: 充电提醒阈值无变化,无需操作");
|
||||
LogUtils.d(TAG, "setChargeReminderValue:充电提醒阈值无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setChargeReminderValue(calibratedValue);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setChargeReminderValue: 充电提醒阈值更新为=" + calibratedValue + "%");
|
||||
LogUtils.d(TAG, String.format("setChargeReminderValue成功 | 充电提醒阈值=%d%%", calibratedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,24 +153,26 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public int getChargeReminderValue() {
|
||||
int value = mAppConfigBean.getChargeReminderValue();
|
||||
LogUtils.d(TAG, "getChargeReminderValue: 获取充电提醒阈值=" + value + "%");
|
||||
LogUtils.d(TAG, String.format("getChargeReminderValue:获取充电提醒阈值=%d%%", value));
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// ======================== 耗电提醒配置方法(单独归类,逻辑聚焦)========================
|
||||
// ======================== 耗电提醒配置方法区(开关+阈值)========================
|
||||
/**
|
||||
* 设置耗电提醒开关状态(直接生效,无弹窗)
|
||||
* 设置耗电提醒开关状态
|
||||
* @param isEnabled 目标状态(true=开启,false=关闭)
|
||||
*/
|
||||
public void setUsageReminderEnabled(final boolean isEnabled) {
|
||||
LogUtils.d(TAG, String.format("setUsageReminderEnabled调用 | 传入状态=%b", isEnabled));
|
||||
|
||||
if (isEnabled == mAppConfigBean.isEnableUsageReminder()) {
|
||||
LogUtils.d(TAG, "setUsageReminderEnabled: 耗电提醒状态无变化,无需操作");
|
||||
LogUtils.d(TAG, "setUsageReminderEnabled:耗电提醒状态无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setEnableUsageReminder(isEnabled);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setUsageReminderEnabled: 耗电提醒状态更新为=" + (isEnabled ? "开启" : "关闭"));
|
||||
LogUtils.d(TAG, String.format("setUsageReminderEnabled成功 | 耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,23 +181,26 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public boolean isUsageReminderEnabled() {
|
||||
boolean isEnabled = mAppConfigBean.isEnableUsageReminder();
|
||||
LogUtils.d(TAG, "isUsageReminderEnabled: 获取耗电提醒状态=" + (isEnabled ? "开启" : "关闭"));
|
||||
LogUtils.d(TAG, String.format("isUsageReminderEnabled:获取耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置耗电提醒阈值(直接生效,无弹窗,自动校准范围,适配小米手机电量跳变)
|
||||
* @param value 目标阈值(自动校准0-100)
|
||||
* 设置耗电提醒阈值(自动校准0-100)
|
||||
* @param 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);
|
||||
|
||||
if (calibratedValue == mAppConfigBean.getUsageReminderValue()) {
|
||||
LogUtils.d(TAG, "setUsageReminderValue: 耗电提醒阈值无变化,无需操作");
|
||||
LogUtils.d(TAG, "setUsageReminderValue:耗电提醒阈值无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setUsageReminderValue(calibratedValue);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setUsageReminderValue: 耗电提醒阈值更新为=" + calibratedValue + "%");
|
||||
LogUtils.d(TAG, String.format("setUsageReminderValue成功 | 耗电提醒阈值=%d%%", calibratedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,23 +209,25 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public int getUsageReminderValue() {
|
||||
int value = mAppConfigBean.getUsageReminderValue();
|
||||
LogUtils.d(TAG, "getUsageReminderValue: 获取耗电提醒阈值=" + value + "%");
|
||||
LogUtils.d(TAG, String.format("getUsageReminderValue:获取耗电提醒阈值=%d%%", value));
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// ======================== 实时电池状态配置方法(临时缓存,不持久化,无需弹窗)========================
|
||||
// ======================== 实时电池状态配置方法区(内存缓存,不持久化)========================
|
||||
/**
|
||||
* 设置当前充电状态(仅内存缓存,不持久化)
|
||||
* 设置当前充电状态(仅内存缓存)
|
||||
* @param isCharging 充电状态(true=充电中,false=未充电)
|
||||
*/
|
||||
public void setCharging(boolean isCharging) {
|
||||
LogUtils.d(TAG, String.format("setCharging调用 | 传入状态=%b", isCharging));
|
||||
|
||||
if (isCharging == mAppConfigBean.isCharging()) {
|
||||
LogUtils.d(TAG, "setCharging: 充电状态无变化,无需操作");
|
||||
LogUtils.d(TAG, "setCharging:充电状态无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setIsCharging(isCharging);
|
||||
LogUtils.d(TAG, "setCharging: 充电状态更新为=" + (isCharging ? "充电中" : "未充电"));
|
||||
LogUtils.d(TAG, String.format("setCharging成功 | 充电状态=%s", isCharging ? "充电中" : "未充电"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,22 +236,25 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public boolean isCharging() {
|
||||
boolean isCharging = mAppConfigBean.isCharging();
|
||||
LogUtils.d(TAG, "isCharging: 获取充电状态=" + (isCharging ? "充电中" : "未充电"));
|
||||
LogUtils.d(TAG, String.format("isCharging:获取充电状态=%s", isCharging ? "充电中" : "未充电"));
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前电池电量(仅内存缓存,不持久化,自动校准范围)
|
||||
* @param value 当前电量(自动校准0-100)
|
||||
* 设置当前电池电量(仅内存缓存,自动校准0-100)
|
||||
* @param 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);
|
||||
|
||||
if (calibratedValue == mAppConfigBean.getCurrentBatteryValue()) {
|
||||
LogUtils.d(TAG, "setCurrentBatteryValue: 电池电量无变化,无需操作");
|
||||
LogUtils.d(TAG, "setCurrentBatteryValue:电池电量无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setCurrentBatteryValue(calibratedValue);
|
||||
LogUtils.d(TAG, "setCurrentBatteryValue: 电池电量更新为=" + calibratedValue + "%");
|
||||
LogUtils.d(TAG, String.format("setCurrentBatteryValue成功 | 电池电量=%d%%", calibratedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,25 +263,27 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public int getCurrentBatteryValue() {
|
||||
int value = mAppConfigBean.getCurrentBatteryValue();
|
||||
LogUtils.d(TAG, "getCurrentBatteryValue: 获取电池电量=" + value + "%");
|
||||
LogUtils.d(TAG, String.format("getCurrentBatteryValue:获取电池电量=%d%%", value));
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// ======================== 间隔配置方法(持久化存储,直接生效,无弹窗)========================
|
||||
// ======================== 间隔配置方法区(持久化)========================
|
||||
/**
|
||||
* 设置提醒间隔时间(直接生效,无弹窗,自动校准最小1000ms)
|
||||
* 设置提醒间隔时间(自动校准最小1000ms)
|
||||
* @param interval 目标间隔(单位:ms)
|
||||
*/
|
||||
public void setReminderIntervalTime(final int interval) {
|
||||
LogUtils.d(TAG, String.format("setReminderIntervalTime调用 | 传入间隔=%dms", interval));
|
||||
final int calibratedInterval = Math.max(interval, MIN_INTERVAL_TIME);
|
||||
|
||||
if (calibratedInterval == mAppConfigBean.getReminderIntervalTime()) {
|
||||
LogUtils.d(TAG, "setReminderIntervalTime: 提醒间隔无变化,无需操作");
|
||||
LogUtils.d(TAG, "setReminderIntervalTime:提醒间隔无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setReminderIntervalTime(calibratedInterval);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setReminderIntervalTime: 提醒间隔更新为=" + calibratedInterval + "ms");
|
||||
LogUtils.d(TAG, String.format("setReminderIntervalTime成功 | 提醒间隔=%dms", calibratedInterval));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,23 +292,26 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public int getReminderIntervalTime() {
|
||||
int interval = mAppConfigBean.getReminderIntervalTime();
|
||||
LogUtils.d(TAG, "getReminderIntervalTime: 获取提醒间隔=" + interval + "ms");
|
||||
LogUtils.d(TAG, String.format("getReminderIntervalTime:获取提醒间隔=%dms", interval));
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置电量检测间隔(直接生效,无弹窗,自动校准最小500ms,与RemindThread同步)
|
||||
* 设置电量检测间隔(自动校准最小500ms)
|
||||
* @param interval 目标间隔(单位:ms)
|
||||
*/
|
||||
public void setBatteryDetectInterval(final int interval) {
|
||||
LogUtils.d(TAG, String.format("setBatteryDetectInterval调用 | 传入间隔=%dms", interval));
|
||||
final int calibratedInterval = Math.max(interval, MIN_DETECT_INTERVAL);
|
||||
|
||||
if (calibratedInterval == mAppConfigBean.getBatteryDetectInterval()) {
|
||||
LogUtils.d(TAG, "setBatteryDetectInterval: 检测间隔无变化,无需操作");
|
||||
LogUtils.d(TAG, "setBatteryDetectInterval:检测间隔无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
|
||||
mAppConfigBean.setBatteryDetectInterval(calibratedInterval);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setBatteryDetectInterval: 电量检测间隔更新为=" + calibratedInterval + "ms");
|
||||
LogUtils.d(TAG, String.format("setBatteryDetectInterval成功 | 电量检测间隔=%dms", calibratedInterval));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,23 +320,38 @@ public class AppConfigUtils {
|
||||
*/
|
||||
public int getBatteryDetectInterval() {
|
||||
int interval = mAppConfigBean.getBatteryDetectInterval();
|
||||
LogUtils.d(TAG, "getBatteryDetectInterval: 获取电量检测间隔=" + interval + "ms");
|
||||
LogUtils.d(TAG, String.format("getBatteryDetectInterval:获取电量检测间隔=%dms", 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);
|
||||
|
||||
if (savedServiceBean != null) {
|
||||
return savedServiceBean.isEnableService();
|
||||
boolean isEnabled = savedServiceBean.isEnableService();
|
||||
LogUtils.d(TAG, String.format("isServiceEnabled:服务开关状态=%b", isEnabled));
|
||||
return isEnabled;
|
||||
} else {
|
||||
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false));
|
||||
LogUtils.d(TAG, "isServiceEnabled:无已保存服务配置,默认关闭并持久化");
|
||||
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));
|
||||
}
|
||||
LogUtils.d(TAG, String.format("setIsServiceEnabled成功 | 服务开关状态=%b", isServiceEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 09:14
|
||||
* @Describe Assets 目录拷贝工具类
|
||||
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
|
||||
* 适配:Java7 | API30 | 递归拷贝 | 覆盖写入
|
||||
*/
|
||||
public class 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/ 目录到指定目标目录
|
||||
* @param context 上下文
|
||||
@@ -25,10 +28,14 @@ public class AssetsCopyUtils {
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
|
||||
LogUtils.d(TAG, "copyAssetsImagesToDir() 调用,目标路径:" + targetDirPath);
|
||||
// 拷贝 assets/images 根目录
|
||||
return copyAssetsDirToDir(context, "images", targetDirPath);
|
||||
boolean result = copyAssetsDirToDir(context, "images", targetDirPath);
|
||||
LogUtils.d(TAG, "copyAssetsImagesToDir() 执行完成,结果:" + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ======================== 公共核心方法区(递归拷贝目录) ========================
|
||||
/**
|
||||
* 递归拷贝 assets 下指定目录到目标目录
|
||||
* @param context 上下文
|
||||
@@ -37,10 +44,16 @@ public class AssetsCopyUtils {
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
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);
|
||||
// 创建目标目录(含多级父目录)
|
||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||
Log.e(TAG, "创建目标目录失败:" + targetDirPath);
|
||||
LogUtils.e(TAG, "copyAssetsDirToDir() 创建目标目录失败:" + targetDirPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,7 +61,7 @@ public class AssetsCopyUtils {
|
||||
// 获取 assets 目录下的文件/子目录列表
|
||||
String[] fileList = context.getAssets().list(assetsDir);
|
||||
if (fileList == null || fileList.length == 0) {
|
||||
Log.d(TAG, "assets 目录为空:" + assetsDir);
|
||||
LogUtils.d(TAG, "copyAssetsDirToDir() assets 目录为空:" + assetsDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,23 +74,26 @@ public class AssetsCopyUtils {
|
||||
if (subFileList != null && subFileList.length > 0) {
|
||||
// 是子目录,递归拷贝
|
||||
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
|
||||
LogUtils.e(TAG, "copyAssetsDirToDir() 递归拷贝子目录失败:" + assetsFilePath);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 是文件,直接拷贝
|
||||
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
|
||||
LogUtils.e(TAG, "copyAssetsDirToDir() 拷贝文件失败:" + assetsFilePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
|
||||
LogUtils.d(TAG, "copyAssetsDirToDir() assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage());
|
||||
LogUtils.e(TAG, "copyAssetsDirToDir() 拷贝 assets 目录异常:" + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 私有辅助方法区(单个文件拷贝) ========================
|
||||
/**
|
||||
* 拷贝 assets 下单个文件到指定路径
|
||||
* @param context 上下文
|
||||
@@ -86,14 +102,16 @@ public class AssetsCopyUtils {
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) {
|
||||
LogUtils.d(TAG, "copyAssetsFileToDir() 调用,源文件:" + assetsFilePath + ",目标文件:" + targetFilePath);
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetsFilePath);
|
||||
File targetFile = new File(targetFilePath);
|
||||
|
||||
// 覆盖已存在的文件
|
||||
if (targetFile.exists() && !targetFile.delete()) {
|
||||
Log.w(TAG, "覆盖目标文件失败,跳过:" + targetFilePath);
|
||||
LogUtils.w(TAG, "copyAssetsFileToDir() 覆盖目标文件失败,跳过:" + targetFilePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,18 +121,22 @@ public class AssetsCopyUtils {
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
|
||||
LogUtils.d(TAG, "copyAssetsFileToDir() 文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage());
|
||||
LogUtils.e(TAG, "copyAssetsFileToDir() 拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage(), e);
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭流
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
if (outputStream != null) outputStream.close();
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "关闭流异常:" + e.getMessage());
|
||||
LogUtils.e(TAG, "copyAssetsFileToDir() 关闭流异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,10 @@ public class BatteryUtils {
|
||||
public static final String TAG = "BatteryUtils";
|
||||
|
||||
// 电池电量计算常量
|
||||
private static final int BATTERY_SCALE_DEFAULT = 100;
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
private static final int BATTERY_SCALE_DEFAULT = 100; // 电量刻度默认值
|
||||
private static final int BATTERY_LEVEL_MIN = 0; // 电量百分比最小值
|
||||
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=未充电
|
||||
*/
|
||||
public static boolean isCharging(Intent intent) {
|
||||
LogUtils.d(TAG, "isCharging: 调用 | intent=" + intent);
|
||||
LogUtils.d(TAG, "【isCharging】调用开始");
|
||||
// 入参非空校验
|
||||
if (intent == null) {
|
||||
LogUtils.e(TAG, "isCharging: intent为空,返回false");
|
||||
LogUtils.e(TAG, "【isCharging】入参异常:intent为空,返回false");
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
LogUtils.d(TAG, "isCharging: 解析完成 | status=" + status + " | result=" + isCharging);
|
||||
// 解析电池状态
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, EXTRA_STATUS_DEFAULT);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -46,29 +52,30 @@ public class BatteryUtils {
|
||||
* @return 电量百分比,异常返回0
|
||||
*/
|
||||
public static int getCurrentBatteryLevel(Intent intent) {
|
||||
LogUtils.d(TAG, "getCurrentBatteryLevel: 调用 | intent=" + intent);
|
||||
LogUtils.d(TAG, "【getCurrentBatteryLevel】调用开始");
|
||||
// 入参非空校验
|
||||
if (intent == null) {
|
||||
LogUtils.e(TAG, "getCurrentBatteryLevel: intent为空,返回0");
|
||||
LogUtils.e(TAG, "【getCurrentBatteryLevel】入参异常:intent为空,返回0");
|
||||
return BATTERY_LEVEL_MIN;
|
||||
}
|
||||
|
||||
// 解析电量原始值与刻度值
|
||||
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN);
|
||||
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或数值越界
|
||||
int batteryLevel;
|
||||
if (scale <= 0) {
|
||||
LogUtils.w(TAG, "【getCurrentBatteryLevel】刻度值无效(scale=" + scale + "),直接使用level值");
|
||||
batteryLevel = level;
|
||||
LogUtils.w(TAG, "getCurrentBatteryLevel: scale无效,直接使用level值");
|
||||
} else {
|
||||
batteryLevel = level * BATTERY_SCALE_DEFAULT / scale;
|
||||
}
|
||||
|
||||
// 确保电量值在0-100范围内
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,65 @@ package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 01:57
|
||||
* @Describe 单例 Bitmap 缓存工具类(Java 7 兼容)
|
||||
* @Describe 单例 Bitmap 缓存工具类(Java 7 兼容)- 极致强制缓存版(无图片压缩)
|
||||
* 功能:内存缓存 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 static final String TAG = "BitmapCacheUtils";
|
||||
// 最大图片尺寸(适配1080P屏幕,可根据需求调整)
|
||||
private static final int MAX_WIDTH = 1080;
|
||||
private static final int MAX_HEIGHT = 1920;
|
||||
|
||||
// SP 相关常量
|
||||
private static final String SP_NAME = "BitmapCacheSP";
|
||||
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;
|
||||
// 路径-Bitmap 缓存容器(内存缓存)
|
||||
private final Map<String, Bitmap> mBitmapCacheMap;
|
||||
// 路径-Bitmap 硬引用缓存(极致强制保持,永不自动回收)
|
||||
private final Map<String, Bitmap> mHardCacheMap;
|
||||
// 路径-引用计数 映射(仅统计,不影响缓存生命周期)
|
||||
private final Map<String, Integer> mRefCountMap;
|
||||
// SP 实例(用于持久化最后缓存路径)
|
||||
private final SharedPreferences mSp;
|
||||
|
||||
// 私有构造器(单例模式)
|
||||
// ================================== 单例方法(双重校验锁,线程安全)=================================
|
||||
/**
|
||||
* 私有构造器(单例模式)
|
||||
*/
|
||||
private BitmapCacheUtils() {
|
||||
mBitmapCacheMap = new HashMap<>();
|
||||
LogUtils.d(TAG, "【BitmapCacheUtils】单例构造开始");
|
||||
// 使用 ConcurrentHashMap 保证线程安全,避免手动同步
|
||||
mHardCacheMap = new ConcurrentHashMap<>();
|
||||
mRefCountMap = new ConcurrentHashMap<>();
|
||||
// 初始化 SP(使用 App 全局上下文,避免内存泄漏)
|
||||
mSp = App.getInstance().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
// 构造时自动预加载 SP 中保存的最后一次缓存路径的图片
|
||||
preloadLastCachedBitmap();
|
||||
// 注册内存状态监听(仅记录日志,不清理缓存)
|
||||
registerMemoryStatusListener();
|
||||
LogUtils.d(TAG, "【BitmapCacheUtils】单例构造完成,极致强制缓存策略已启用");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,180 +77,321 @@ public class BitmapCacheUtils {
|
||||
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 图片绝对路径
|
||||
* @return 缓存成功的 Bitmap / null(路径无效/文件不存在/解码失败)
|
||||
*/
|
||||
public Bitmap cacheBitmap(String imagePath) {
|
||||
LogUtils.d(TAG, "【cacheBitmap】调用开始(路径缓存)| 路径=" + imagePath);
|
||||
// 入参非空校验
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片路径为空");
|
||||
LogUtils.e(TAG, "【cacheBitmap】入参异常:图片路径为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 文件有效性校验
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片文件无效(不存在/非文件/空文件) - " + imagePath);
|
||||
LogUtils.e(TAG, "【cacheBitmap】文件无效:不存在/非文件/空文件 | 路径=" + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已缓存则直接返回,避免重复加载
|
||||
if (mBitmapCacheMap.containsKey(imagePath)) {
|
||||
Bitmap cachedBitmap = mBitmapCacheMap.get(imagePath);
|
||||
// 额外校验缓存的Bitmap是否有效
|
||||
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath);
|
||||
// 持久化当前路径到 SP(更新最后缓存路径)
|
||||
saveLastCachePathToSp(imagePath);
|
||||
return cachedBitmap;
|
||||
} else {
|
||||
// 缓存的Bitmap已失效,移除后重新加载
|
||||
mBitmapCacheMap.remove(imagePath);
|
||||
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效,移除后重新加载 - " + imagePath);
|
||||
}
|
||||
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
|
||||
if (isBitmapValid(hardCacheBitmap)) {
|
||||
LogUtils.d(TAG, "【cacheBitmap】硬引用缓存命中,引用计数+1 | 路径=" + imagePath);
|
||||
// 引用计数+1
|
||||
increaseRefCount(imagePath);
|
||||
// 持久化当前路径到 SP
|
||||
saveLastCachePathToSp(imagePath);
|
||||
LogUtils.d(TAG, "【cacheBitmap】调用成功(缓存命中)| 路径=" + imagePath);
|
||||
return hardCacheBitmap;
|
||||
}
|
||||
|
||||
// 压缩加载 Bitmap(避免OOM)
|
||||
Bitmap bitmap = decodeCompressedBitmap(imagePath);
|
||||
// 无压缩解码 Bitmap(保留原始品质)
|
||||
Bitmap bitmap = decodeOriginalBitmap(imagePath);
|
||||
if (bitmap != null) {
|
||||
// 存入缓存容器
|
||||
mBitmapCacheMap.put(imagePath, bitmap);
|
||||
// 持久化当前路径到 SP(更新最后缓存路径)
|
||||
// 极致强制:存入硬引用缓存,永不自动回收
|
||||
mHardCacheMap.put(imagePath, bitmap);
|
||||
// 初始化引用计数为1
|
||||
mRefCountMap.put(imagePath, 1);
|
||||
// 持久化当前路径到 SP
|
||||
saveLastCachePathToSp(imagePath);
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径 - " + imagePath);
|
||||
LogUtils.d(TAG, "【cacheBitmap】调用成功(新缓存)| 路径=" + imagePath);
|
||||
} else {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
|
||||
LogUtils.e(TAG, "【cacheBitmap】调用失败:图片解码失败 | 路径=" + imagePath);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心接口:根据路径获取缓存的 Bitmap
|
||||
* 根据路径获取缓存的 Bitmap
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存的有效 Bitmap / null(未缓存/已回收)
|
||||
*/
|
||||
public Bitmap getCachedBitmap(String imagePath) {
|
||||
LogUtils.d(TAG, "【getCachedBitmap】调用开始 | 路径=" + imagePath);
|
||||
// 入参非空校验
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "【getCachedBitmap】入参异常:图片路径为空");
|
||||
return null;
|
||||
}
|
||||
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
|
||||
// 校验Bitmap是否有效
|
||||
if (bitmap != null && bitmap.isRecycled()) {
|
||||
mBitmapCacheMap.remove(imagePath);
|
||||
return null;
|
||||
|
||||
// 仅从硬引用缓存获取,无任何 fallback
|
||||
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
|
||||
if (isBitmapValid(hardCacheBitmap)) {
|
||||
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() {
|
||||
synchronized (mBitmapCacheMap) {
|
||||
for (Bitmap bitmap : mBitmapCacheMap.values()) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle(); // 主动回收 Bitmap
|
||||
}
|
||||
LogUtils.w(TAG, "【clearAllCache】调用开始(极致强制缓存策略下,需谨慎使用)");
|
||||
|
||||
// 清空硬引用缓存并回收 Bitmap
|
||||
for (Bitmap bitmap : mHardCacheMap.values()) {
|
||||
if (isBitmapValid(bitmap)) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
mBitmapCacheMap.clear();
|
||||
}
|
||||
mHardCacheMap.clear();
|
||||
|
||||
// 清空引用计数
|
||||
mRefCountMap.clear();
|
||||
|
||||
// 清空 SP 中保存的最后缓存路径
|
||||
clearLastCachePathInSp();
|
||||
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空,SP 路径已清除");
|
||||
|
||||
LogUtils.d(TAG, "【clearAllCache】调用成功:所有 Bitmap 缓存已清空");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定路径的 Bitmap 缓存
|
||||
* 移除指定路径的 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行)
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void removeCachedBitmap(String imagePath) {
|
||||
LogUtils.d(TAG, "【removeCachedBitmap】调用开始 | 路径=" + imagePath);
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "【removeCachedBitmap】入参异常:图片路径为空");
|
||||
return;
|
||||
}
|
||||
synchronized (mBitmapCacheMap) {
|
||||
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
|
||||
|
||||
synchronized (mRefCountMap) {
|
||||
// 手动移除时才回收 Bitmap
|
||||
Bitmap hardBitmap = mHardCacheMap.remove(imagePath);
|
||||
if (isBitmapValid(hardBitmap)) {
|
||||
hardBitmap.recycle();
|
||||
LogUtils.d(TAG, "【removeCachedBitmap】手动回收硬引用缓存 | 路径=" + imagePath);
|
||||
}
|
||||
mRefCountMap.remove(imagePath);
|
||||
|
||||
// 若移除的是最后缓存的路径,清空 SP
|
||||
String lastPath = getLastCachePathFromSp();
|
||||
if (imagePath.equals(lastPath)) {
|
||||
clearLastCachePathInSp();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 移除的是最后缓存路径,已清空 SP");
|
||||
LogUtils.d(TAG, "【removeCachedBitmap】移除最后缓存路径,已清空 SP");
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【removeCachedBitmap】调用成功 | 路径=" + imagePath);
|
||||
}
|
||||
|
||||
// ================================== 内部工具方法(无压缩解码 + Bitmap 有效性判断)=================================
|
||||
/**
|
||||
* 压缩解码 Bitmap(按最大尺寸缩放,避免OOM)
|
||||
* 无压缩解码 Bitmap(保留原始品质)
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 解码后的 Bitmap / null(文件无效/解码失败)
|
||||
*/
|
||||
private Bitmap decodeCompressedBitmap(String imagePath) {
|
||||
private Bitmap decodeOriginalBitmap(String imagePath) {
|
||||
LogUtils.d(TAG, "【decodeOriginalBitmap】调用开始 | 路径=" + imagePath);
|
||||
// 前置校验:确保文件有效
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + imagePath);
|
||||
LogUtils.e(TAG, "【decodeOriginalBitmap】文件无效,跳过解码 | 路径=" + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
// 第一步:只获取图片尺寸,不加载像素
|
||||
// 仅获取尺寸用于日志记录,不参与解码逻辑
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(imagePath, options);
|
||||
|
||||
// 校验尺寸是否有效
|
||||
if (options.outWidth <= 0 || options.outHeight <= 0) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 图片尺寸无效 - " + imagePath);
|
||||
LogUtils.e(TAG, "【decodeOriginalBitmap】图片尺寸无效 | 路径=" + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算缩放比例
|
||||
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
|
||||
LogUtils.d(TAG, "【decodeOriginalBitmap】图片原始尺寸 | 宽=" + options.outWidth + " | 高=" + options.outHeight);
|
||||
|
||||
// 第二步:加载压缩后的 Bitmap
|
||||
// 无压缩解码配置
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存(比ARGB_8888少一半内存)
|
||||
options.inPurgeable = true;
|
||||
options.inInputShareable = true;
|
||||
options.inSampleSize = BITMAP_SAMPLE_SIZE_ORIGINAL; // 不缩放,采样率为1
|
||||
options.inPreferredConfig = BITMAP_CONFIG_DEFAULT; // 保留全彩品质
|
||||
options.inPurgeable = false; // 关闭可清除标志,极致强制保持内存
|
||||
options.inInputShareable = false;
|
||||
options.inDither = true; // 开启抖动,保证色彩还原
|
||||
options.inScaled = false; // 关闭自动缩放,保留原始尺寸
|
||||
|
||||
try {
|
||||
return BitmapFactory.decodeFile(imagePath, options);
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options);
|
||||
LogUtils.d(TAG, "【decodeOriginalBitmap】解码" + (bitmap != null ? "成功" : "失败") + " | 路径=" + imagePath);
|
||||
return bitmap;
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath);
|
||||
LogUtils.e(TAG, "【decodeOriginalBitmap】OOM 异常(无压缩,图片尺寸过大)| 路径=" + imagePath);
|
||||
// 极致强制缓存策略:OOM 时仅放弃当前解码,绝对不清理已缓存的 Bitmap
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
|
||||
LogUtils.e(TAG, "【decodeOriginalBitmap】解码异常 | 路径=" + imagePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Bitmap 缩放比例
|
||||
* 判断 Bitmap 是否有效(非空且未被回收)
|
||||
*/
|
||||
private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
|
||||
int rawWidth = options.outWidth;
|
||||
int rawHeight = options.outHeight;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (rawWidth > maxWidth || rawHeight > maxHeight) {
|
||||
int halfWidth = rawWidth / 2;
|
||||
int halfHeight = rawHeight / 2;
|
||||
while ((halfWidth / inSampleSize) >= maxWidth && (halfHeight / inSampleSize) >= maxHeight) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
boolean isValid = bitmap != null && !bitmap.isRecycled();
|
||||
if (!isValid) {
|
||||
LogUtils.w(TAG, "【isBitmapValid】Bitmap 无效:空或已回收");
|
||||
}
|
||||
return inSampleSize;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// ================================== 内部工具方法:SP 持久化相关 ==================================
|
||||
/**
|
||||
* 从 SP 中获取最后一次缓存的图片路径
|
||||
* @return 最后缓存的路径 / null(未保存)
|
||||
*/
|
||||
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 +399,13 @@ public class BitmapCacheUtils {
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
private void saveLastCachePathToSp(String imagePath) {
|
||||
LogUtils.d(TAG, "【saveLastCachePathToSp】调用开始 | 路径=" + imagePath);
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "【saveLastCachePathToSp】入参异常:图片路径为空");
|
||||
return;
|
||||
}
|
||||
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 +413,78 @@ public class BitmapCacheUtils {
|
||||
*/
|
||||
private void clearLastCachePathInSp() {
|
||||
mSp.edit().remove(SP_KEY_LAST_CACHE_PATH).commit();
|
||||
LogUtils.d(TAG, "clearLastCachePathInSp: SP 中最后缓存路径已清空");
|
||||
LogUtils.d(TAG, "【clearLastCachePathInSp】调用成功:SP 中最后缓存路径已清空");
|
||||
}
|
||||
|
||||
// ================================== 内部工具方法:预加载相关 ==================================
|
||||
/**
|
||||
* 构造时预加载 SP 中保存的最后一次缓存路径的图片
|
||||
*/
|
||||
private void preloadLastCachedBitmap() {
|
||||
LogUtils.d(TAG, "【preloadLastCachedBitmap】调用开始");
|
||||
String lastPath = getLastCachePathFromSp();
|
||||
if (TextUtils.isEmpty(lastPath)) {
|
||||
LogUtils.d(TAG, "preloadLastCachedBitmap: SP 中无保存的缓存路径,跳过预加载");
|
||||
LogUtils.d(TAG, "【preloadLastCachedBitmap】SP 中无保存的缓存路径,跳过预加载");
|
||||
return;
|
||||
}
|
||||
// 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断)
|
||||
Bitmap bitmap = cacheBitmap(lastPath);
|
||||
if (bitmap != null) {
|
||||
LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功 - " + lastPath);
|
||||
LogUtils.d(TAG, "【preloadLastCachedBitmap】预加载成功 | 路径=" + lastPath);
|
||||
} else {
|
||||
LogUtils.w(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径失败,清空无效路径 - " + lastPath);
|
||||
LogUtils.w(TAG, "【preloadLastCachedBitmap】预加载失败,清空无效路径 | 路径=" + lastPath);
|
||||
// 预加载失败,清空 SP 中无效路径
|
||||
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) {
|
||||
// 配置变化时无需处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
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 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() {
|
||||
// 日期类转化成字符串类的工具
|
||||
SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("YYYYMMdd_HHmmssmmm", java.util.Locale.getDefault());
|
||||
// 读取当前时间
|
||||
long nTimeNow = System.currentTimeMillis();
|
||||
return mSimpleDateFormat.format(nTimeNow);
|
||||
LogUtils.d(TAG, "【getDateNowString】调用开始");
|
||||
// 初始化日期格式化工具(Java 7 兼容,使用小写yyyy避免周基年问题)
|
||||
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_PATTERN, DEFAULT_LOCALE);
|
||||
// 读取当前时间戳
|
||||
long currentTime = System.currentTimeMillis();
|
||||
// 格式化时间
|
||||
String formattedTime = sdf.format(currentTime);
|
||||
LogUtils.d(TAG, "【getDateNowString】调用成功 | 格式化时间=" + formattedTime);
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@ package cc.winboll.studio.powerbell.utils;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
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.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -17,85 +15,111 @@ import java.io.IOException;
|
||||
* 适配 PowerBell 项目:支持指定保存路径、自动创建目录、处理PNG图片压缩
|
||||
*/
|
||||
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 对象
|
||||
* @param context 上下文(用于获取 Resources)
|
||||
* @param drawableResId 图片资源ID(如 R.drawable.ic_test_png)
|
||||
* @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.png")
|
||||
* @param filePath 保存的文件路径(可带/不带.png后缀)
|
||||
* @return 保存成功返回 File 对象,失败返回 null
|
||||
*/
|
||||
public static File saveDrawableToFile(Context context, int drawableResId, String filePath) {
|
||||
// 1. 校验参数(避免空指针/无效参数)
|
||||
if (context == null || drawableResId == 0 || filePath == null || filePath.isEmpty()) {
|
||||
LogUtils.e(TAG, "【保存失败】参数无效(context为空/资源ID为0/文件名为空)");
|
||||
LogUtils.d(TAG, "【saveDrawableToFile】调用开始 | 资源ID=" + drawableResId + " | 目标路径=" + filePath);
|
||||
// 1. 校验核心参数(避免空指针/无效参数)
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "【saveDrawableToFile】参数异常:context为空");
|
||||
return null;
|
||||
}
|
||||
if (!filePath.endsWith(".png")) {
|
||||
filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确
|
||||
LogUtils.d(TAG, "【格式适配】自动添加.png后缀,最终文件名:" + filePath);
|
||||
if (drawableResId == 0) {
|
||||
LogUtils.e(TAG, "【saveDrawableToFile】参数异常:drawableResId为0");
|
||||
return null;
|
||||
}
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
LogUtils.e(TAG, "【saveDrawableToFile】参数异常:filePath为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 构建目标 File 对象(最终保存的文件路径)
|
||||
File targetFile = new File(filePath);
|
||||
LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath());
|
||||
// 2. 格式化文件路径(强制添加.png后缀)
|
||||
String targetFilePath = filePath.endsWith(IMAGE_FORMAT_PNG) ? filePath : filePath + IMAGE_FORMAT_PNG;
|
||||
if (!filePath.equals(targetFilePath)) {
|
||||
LogUtils.d(TAG, "【saveDrawableToFile】格式适配:自动添加.png后缀 | 最终路径=" + targetFilePath);
|
||||
}
|
||||
|
||||
// 4. 读取 drawable 资源为 Bitmap(处理高清图/缩放问题)
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
// 读取 drawable 资源(适配不同分辨率的图片,避免变形)
|
||||
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "【读取失败】无法读取drawable资源(资源ID:" + drawableResId + ")");
|
||||
// 3. 构建目标File对象并创建父目录
|
||||
File targetFile = new File(targetFilePath);
|
||||
File parentDir = targetFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
boolean isDirCreated = parentDir.mkdirs();
|
||||
if (!isDirCreated) {
|
||||
LogUtils.e(TAG, "【saveDrawableToFile】目录创建失败:" + parentDir.getAbsolutePath());
|
||||
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 写入 File(PNG格式,无损保存)
|
||||
// 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写入File(PNG无损保存)
|
||||
FileOutputStream fos = new FileOutputStream(targetFile);
|
||||
// 压缩参数:PNG格式,质量100(无损),写入输出流
|
||||
boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.flush(); // 刷新输出流
|
||||
fos.close(); // 关闭输出流
|
||||
boolean isSaved = bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, fos);
|
||||
fos.flush();
|
||||
fos.close();
|
||||
|
||||
// 6. 校验保存结果(文件是否存在且有效)
|
||||
if (isSaved && targetFile.exists() && targetFile.length() > 100) {
|
||||
LogUtils.d(TAG, "【保存成功】drawable图片保存为File:" + targetFile.getAbsolutePath());
|
||||
return targetFile; // 保存成功,返回File对象
|
||||
// 6. 校验保存结果
|
||||
if (isSaved && targetFile.exists() && targetFile.length() > MIN_FILE_SIZE) {
|
||||
LogUtils.d(TAG, "【saveDrawableToFile】保存成功:" + targetFile.getAbsolutePath());
|
||||
return targetFile;
|
||||
} else {
|
||||
LogUtils.e(TAG, "【保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)");
|
||||
// 保存失败,删除无效文件
|
||||
LogUtils.e(TAG, "【saveDrawableToFile】保存失败:文件无效(存在=" + targetFile.exists() + " | 大小=" + targetFile.length() + "字节)");
|
||||
// 清理无效文件
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete();
|
||||
LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【saveDrawableToFile】清理无效文件:" + targetFile.getAbsolutePath());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【保存异常】写入文件时出错:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e));
|
||||
LogUtils.e(TAG, "【saveDrawableToFile】保存异常:" + e.getMessage());
|
||||
return null;
|
||||
} finally {
|
||||
// 回收Bitmap资源(避免内存溢出)
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
LogUtils.d(TAG, "【资源回收】Bitmap资源已回收");
|
||||
LogUtils.d(TAG, "【saveDrawableToFile】资源回收:Bitmap已回收");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 重载工具方法(扩展版:指定目录+文件名)=================================
|
||||
/**
|
||||
* 重载方法:自定义保存路径(灵活适配不同场景)
|
||||
* @param context 上下文
|
||||
* @param drawableResId 图片资源ID
|
||||
* @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/")
|
||||
* @param fileName 保存的文件名(带.png后缀)
|
||||
* @param fileName 保存的文件名(可带/不带.png后缀)
|
||||
* @return 保存成功返回File对象,失败返回null
|
||||
*/
|
||||
public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) {
|
||||
File filePath = new File(saveDirPath, fileName);
|
||||
return saveDrawableToFile(context, drawableResId, filePath.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【saveDrawableToFile】重载方法调用开始 | 资源ID=" + drawableResId + " | 目录=" + saveDirPath + " | 文件名=" + fileName);
|
||||
// 构建完整文件路径
|
||||
File targetFile = new File(saveDirPath, fileName);
|
||||
return saveDrawableToFile(context, drawableResId, targetFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,33 +4,37 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedInputStream;
|
||||
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.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* 文件操作工具类
|
||||
* 功能:文件读写、复制、图片转换、文件名处理等常用文件操作
|
||||
* 适配:Java 7+,支持Android全版本
|
||||
* 适配:Java 7 + Android API 30
|
||||
* 注意:调用文件操作前需确保已获取存储权限(Android 6.0+ 需动态申请)
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
/** 日志标签 */
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "FileUtils";
|
||||
/** 读取文件默认缓冲区大小(10KB) */
|
||||
private static final int BUFFER_SIZE = 10240;
|
||||
/** 最大读取文件大小(1GB),防止OOM */
|
||||
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 文件绝对路径(非空)
|
||||
@@ -38,25 +42,34 @@ public class FileUtils {
|
||||
* @throws IOException 异常:文件不存在、文件过大、读取失败等
|
||||
*/
|
||||
public static String readFileAsString(String filePath) throws IOException {
|
||||
LogUtils.d(TAG, "【readFileAsString】调用开始 | 文件路径=" + filePath);
|
||||
// 1. 校验文件合法性
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
LogUtils.e(TAG, "【readFileAsString】文件不存在:" + filePath);
|
||||
throw new FileNotFoundException("文件不存在:" + filePath);
|
||||
}
|
||||
if (file.length() > MAX_READ_FILE_SIZE) {
|
||||
LogUtils.e(TAG, "【readFileAsString】文件过大(超过1GB):" + filePath);
|
||||
throw new IOException("文件过大(超过1GB),禁止读取:" + filePath);
|
||||
}
|
||||
|
||||
// 2. 读取文件内容(使用StringBuilder高效拼接)
|
||||
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];
|
||||
int readLen;
|
||||
// 循环读取缓冲区,避免一次性读取大文件导致OOM
|
||||
while ((readLen = fis.read(buffer)) > 0) {
|
||||
sb.append(new String(buffer, 0, readLen));
|
||||
}
|
||||
} finally {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【readFileAsString】读取成功 | 文件大小=" + file.length() + "字节");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -67,28 +80,39 @@ public class FileUtils {
|
||||
* @throws IOException 异常:文件不存在、读取失败等
|
||||
*/
|
||||
public static byte[] readFileByBytes(String filePath) throws IOException {
|
||||
LogUtils.d(TAG, "【readFileByBytes】调用开始 | 文件路径=" + filePath);
|
||||
// 1. 校验文件合法性
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
LogUtils.e(TAG, "【readFileByBytes】文件不存在:" + filePath);
|
||||
throw new FileNotFoundException("文件不存在:" + filePath);
|
||||
}
|
||||
|
||||
// 2. 缓冲流读取(高效,减少IO次数)
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
|
||||
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
|
||||
|
||||
ByteArrayOutputStream bos = null;
|
||||
BufferedInputStream bis = null;
|
||||
try {
|
||||
bos = new ByteArrayOutputStream((int) file.length());
|
||||
bis = new BufferedInputStream(new FileInputStream(file));
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int readLen;
|
||||
while ((readLen = bis.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, readLen);
|
||||
}
|
||||
bos.flush();
|
||||
LogUtils.d(TAG, "【readFileByBytes】读取成功 | 文件大小=" + file.length() + "字节");
|
||||
return bos.toByteArray();
|
||||
} finally {
|
||||
if (bis != null) {
|
||||
bis.close();
|
||||
}
|
||||
if (bos != null) {
|
||||
bos.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 文件复制相关 ======================================
|
||||
|
||||
// ================================== 文件复制相关(FileChannel + 简化版 + 流复制)=================================
|
||||
/**
|
||||
* 基于FileChannel复制文件(高效,适用于大文件复制)
|
||||
* @param source 源文件(非空,必须存在)
|
||||
@@ -96,62 +120,117 @@ public class FileUtils {
|
||||
* @throws IOException 异常:源文件不存在、复制失败等
|
||||
*/
|
||||
public static void copyFileUsingFileChannels(File source, File dest) throws IOException {
|
||||
LogUtils.d(TAG, "【copyFileUsingFileChannels】调用开始 | 源文件=" + source.getAbsolutePath() + " | 目标文件=" + dest.getAbsolutePath());
|
||||
// 1. 校验源文件合法性
|
||||
if (!source.exists() || !source.isFile()) {
|
||||
LogUtils.e(TAG, "【copyFileUsingFileChannels】源文件无效:" + source.getAbsolutePath());
|
||||
throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 2. 创建目标文件父目录
|
||||
if (!dest.getParentFile().exists()) {
|
||||
dest.getParentFile().mkdirs();
|
||||
LogUtils.d(TAG, "【copyFileUsingFileChannels】创建父目录:" + dest.getParentFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
// 3. 通道复制(try-with-resources 自动关闭通道,无需手动关闭)
|
||||
try (FileChannel inputChannel = new FileInputStream(source).getChannel();
|
||||
FileChannel outputChannel = new FileOutputStream(dest).getChannel()) {
|
||||
// 从输入通道复制到输出通道(高效,底层优化)
|
||||
// 3. 通道复制(手动关闭流,兼容Java 7)
|
||||
FileChannel inputChannel = null;
|
||||
FileChannel outputChannel = null;
|
||||
try {
|
||||
inputChannel = new FileInputStream(source).getChannel();
|
||||
outputChannel = new FileOutputStream(dest).getChannel();
|
||||
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 newFile 目标文件(非空,父目录会自动创建)
|
||||
* @return 复制结果:true-成功,false-失败
|
||||
*/
|
||||
public static boolean copyFile(File oldFile, File newFile) {
|
||||
LogUtils.d(TAG, "【copyFile】调用开始 | 源文件=" + (oldFile != null ? oldFile.getAbsolutePath() : "null") + " | 目标文件=" + (newFile != null ? newFile.getAbsolutePath() : "null"));
|
||||
// 1. 校验源文件合法性
|
||||
if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) {
|
||||
LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null"));
|
||||
LogUtils.e(TAG, "【copyFile】源文件无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 创建目标文件父目录
|
||||
if (!newFile.getParentFile().exists()) {
|
||||
newFile.getParentFile().mkdirs();
|
||||
LogUtils.d(TAG, "【copyFile】创建父目录:" + newFile.getParentFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
// 3. 复制文件(覆盖已有目标文件)
|
||||
if (newFile.exists()) {
|
||||
newFile.delete();
|
||||
LogUtils.d(TAG, "【copyFile】删除已有目标文件:" + newFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
try {
|
||||
Path sourcePath = Paths.get(oldFile.getPath());
|
||||
Path destPath = Paths.get(newFile.getPath());
|
||||
// 先删除已有目标文件(避免覆盖失败)
|
||||
if (newFile.exists()) {
|
||||
newFile.delete();
|
||||
}
|
||||
Files.copy(sourcePath, destPath);
|
||||
LogUtils.d(TAG, "文件复制成功(Files):" + oldFile.getAbsolutePath() + " → " + newFile.getAbsolutePath());
|
||||
copyFileUsingFileChannels(oldFile, newFile);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e);
|
||||
LogUtils.e(TAG, "【copyFile】复制失败:" + e.getMessage(), e);
|
||||
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图片显示)
|
||||
* @param path 图片文件绝对路径(非空)
|
||||
@@ -159,40 +238,50 @@ public class FileUtils {
|
||||
* @throws IOException 异常:文件读取IO错误
|
||||
*/
|
||||
public static BitmapDrawable getImageDrawable(String path) throws IOException {
|
||||
LogUtils.d(TAG, "【getImageDrawable】调用开始 | 图片路径=" + path);
|
||||
// 1. 校验文件合法性
|
||||
File file = new File(path);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
LogUtils.e(TAG, "图片文件不存在:" + path);
|
||||
LogUtils.e(TAG, "【getImageDrawable】图片文件无效:" + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 读取文件并转为BitmapDrawable(缓冲流读取,减少内存占用)
|
||||
try (InputStream is = new FileInputStream(file);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
|
||||
InputStream is = null;
|
||||
ByteArrayOutputStream bos = null;
|
||||
try {
|
||||
is = new FileInputStream(file);
|
||||
bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int readLen;
|
||||
while ((readLen = is.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, readLen);
|
||||
}
|
||||
|
||||
// 3. 生成Bitmap并包装为BitmapDrawable
|
||||
byte[] imageBytes = bos.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||
LogUtils.d(TAG, "【getImageDrawable】转换成功 | 图片尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight());
|
||||
return new BitmapDrawable(bitmap);
|
||||
} finally {
|
||||
if (is != null) {
|
||||
is.close();
|
||||
}
|
||||
if (bos != null) {
|
||||
bos.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 文件名处理相关 ======================================
|
||||
|
||||
// ================================== 文件名处理相关(后缀截取 + 唯一文件名)=================================
|
||||
/**
|
||||
* 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png")
|
||||
* @param file 目标文件(可为null)
|
||||
* @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串
|
||||
*/
|
||||
public static String getFileSuffixWithMultiDot(File file) {
|
||||
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null"));
|
||||
// 1. 校验文件合法性
|
||||
if (file == null || !file.isFile()) {
|
||||
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】文件无效,返回空后缀");
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -201,14 +290,39 @@ public class FileUtils {
|
||||
int lastDotIndex = fileName.lastIndexOf(".");
|
||||
|
||||
// 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件)
|
||||
if (lastDotIndex == -1 // 无 "."
|
||||
|| lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore")
|
||||
|| (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5(异常文件名)
|
||||
if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1 || (fileName.length() - lastDotIndex) > MAX_SUFFIX_LENGTH) {
|
||||
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】无有效后缀 | 文件名=" + fileName);
|
||||
return "";
|
||||
}
|
||||
|
||||
// 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",无后缀则不带点)
|
||||
*/
|
||||
public static String createUniqueFileName(File refFile) {
|
||||
LogUtils.d(TAG, "【createUniqueFileName】调用开始 | 参考文件=" + (refFile != null ? refFile.getAbsolutePath() : "null"));
|
||||
// 1. 获取参考文件的后缀名(自动容错null/无效文件)
|
||||
String suffix = getFileSuffixWithMultiDot(refFile);
|
||||
|
||||
// 2. 生成唯一标识(UUID确保全局唯一,时间戳进一步降低重复概率)
|
||||
String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名
|
||||
String uniqueId = UUID.randomUUID().toString().replace("-", "");
|
||||
long timeStamp = System.currentTimeMillis();
|
||||
|
||||
// 3. 拼接文件名(分场景处理,避免多余点)
|
||||
String fileName;
|
||||
if (suffix.isEmpty()) {
|
||||
// 无后缀:唯一ID + 时间戳
|
||||
return String.format("%s_%d", uniqueId, timeStamp);
|
||||
fileName = String.format("%s_%d", uniqueId, timeStamp);
|
||||
} else {
|
||||
// 有后缀:唯一ID + 时间戳 + 后缀(无多余点)
|
||||
return String.format("%s_%d%s", uniqueId, timeStamp, suffix);
|
||||
fileName = 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 {
|
||||
if (inputStream == null || file == null) {
|
||||
throw new IllegalArgumentException("InputStream或File不能为空");
|
||||
}
|
||||
File parentDir = file.getParentFile();
|
||||
if (!parentDir.exists() && !parentDir.mkdirs()) {
|
||||
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
|
||||
}
|
||||
try {
|
||||
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();
|
||||
}
|
||||
// ================================== 工具辅助方法(文件存在性判断)=================================
|
||||
/**
|
||||
* 判断文件是否存在
|
||||
* @param path 文件绝对路径
|
||||
* @return true-存在,false-不存在
|
||||
*/
|
||||
public static boolean isFileExists(String path) {
|
||||
LogUtils.d(TAG, "【isFileExists】调用开始 | 文件路径=" + path);
|
||||
File file = new File(path);
|
||||
boolean exists = file.exists();
|
||||
LogUtils.d(TAG, "【isFileExists】判断结果 | 路径=" + path + " | 存在=" + exists);
|
||||
return exists;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,27 +2,35 @@ package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.widget.Toast;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import com.yalantis.ucrop.UCrop;
|
||||
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 static final String TAG = "ImageCropUtils";
|
||||
// FileProvider 授权(与 AndroidManifest 配置一致)
|
||||
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
|
||||
// 强制输出格式:固定为 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,保留透明通道
|
||||
* @param activity 上下文
|
||||
@@ -40,18 +48,19 @@ public class ImageCropUtils {
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
LogUtils.d(TAG, "【startImageCrop】调用开始(Uri 版)| 请求码=" + requestCode);
|
||||
// 1. 输入参数校验
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
|
||||
LogUtils.e(TAG, "【startImageCrop】参数异常:Activity 无效或已销毁");
|
||||
return;
|
||||
}
|
||||
if (inputUri == null || outputUri == null) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入/输出 Uri 为空");
|
||||
LogUtils.e(TAG, "【startImageCrop】参数异常:输入/输出 Uri 为空");
|
||||
showToast(activity, "图片 Uri 无效,无法裁剪");
|
||||
return;
|
||||
}
|
||||
if (!isValidUri(activity, inputUri)) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入 Uri 无效:" + inputUri);
|
||||
LogUtils.e(TAG, "【startImageCrop】参数异常:输入 Uri 无效 " + inputUri);
|
||||
showToast(activity, "原图 Uri 无效,无法裁剪");
|
||||
return;
|
||||
}
|
||||
@@ -59,22 +68,23 @@ public class ImageCropUtils {
|
||||
// 2. 核心:强制修正输出为 PNG(忽略原图格式,统一转 PNG)
|
||||
File outputFile = uriToFile(activity, outputUri);
|
||||
if (outputFile == null) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输出 Uri 转 File 失败:" + outputUri);
|
||||
LogUtils.e(TAG, "【startImageCrop】转换异常:输出 Uri 转 File 失败 " + outputUri);
|
||||
showToast(activity, "裁剪输出路径无效");
|
||||
return;
|
||||
}
|
||||
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
|
||||
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
|
||||
LogUtils.d(TAG, "【startImageCrop】格式修正:强制输出 PNG " + outputFile.getAbsolutePath());
|
||||
|
||||
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
|
||||
UCrop uCrop = UCrop.of(inputUri, outputUri);
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
|
||||
|
||||
// 4. 启动裁剪
|
||||
uCrop.withOptions(options);
|
||||
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,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
LogUtils.d(TAG, "【startImageCrop】调用开始(File 版)| 请求码=" + requestCode);
|
||||
// 1. 输入参数校验
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
|
||||
LogUtils.e(TAG, "【startImageCrop】参数异常:Activity 无效或已销毁");
|
||||
return;
|
||||
}
|
||||
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入图片文件无效");
|
||||
LogUtils.e(TAG, "【startImageCrop】参数异常:输入图片文件无效 " + (inputFile != null ? inputFile.getAbsolutePath() : "null"));
|
||||
showToast(activity, "无有效图片可裁剪");
|
||||
return;
|
||||
}
|
||||
if (outputFile == null) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输出文件路径为空");
|
||||
LogUtils.e(TAG, "【startImageCrop】参数异常:输出文件路径为空");
|
||||
showToast(activity, "裁剪输出路径无效");
|
||||
return;
|
||||
}
|
||||
@@ -114,20 +125,27 @@ public class ImageCropUtils {
|
||||
Uri inputUri = getFileProviderUri(activity, inputFile);
|
||||
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
|
||||
Uri outputUri = getFileProviderUri(activity, outputFile);
|
||||
LogUtils.d(TAG, "【startImageCrop】格式修正:强制输出 PNG " + outputFile.getAbsolutePath());
|
||||
|
||||
// 3. 初始化 uCrop + 强制 PNG 配置
|
||||
UCrop uCrop = UCrop.of(inputUri, outputUri);
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
|
||||
|
||||
// 4. 启动裁剪
|
||||
uCrop.withOptions(options);
|
||||
uCrop.start(activity, requestCode);
|
||||
LogUtils.d(TAG, "【裁剪启动成功(File 版)】强制输出 PNG(透明保留),输出路径:" + outputFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【startImageCrop】启动成功(File 版)| 输出路径=" + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG,保留透明通道
|
||||
* @param activity 上下文
|
||||
* @param cropBean 背景图片 Bean
|
||||
* @param aspectX 固定比例 X
|
||||
* @param aspectY 固定比例 Y
|
||||
* @param isFreeCrop 是否自由裁剪
|
||||
* @param requestCode 裁剪请求码
|
||||
*/
|
||||
public static void startImageCrop(Activity activity,
|
||||
BackgroundBean cropBean,
|
||||
@@ -135,166 +153,231 @@ public class ImageCropUtils {
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
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 outputFile = new File(cropBean.getBackgroundScaledCompressFilePath());
|
||||
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) {
|
||||
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) {
|
||||
Uri outputUri = UCrop.getOutput(data);
|
||||
if (outputUri != null) {
|
||||
String outputPath = uriToPath(outputUri);
|
||||
LogUtils.d(TAG, "【裁剪成功】强制输出 PNG(透明保留),输出路径:" + outputPath);
|
||||
LogUtils.d(TAG, "【handleCropResult】裁剪成功 | 输出路径=" + outputPath);
|
||||
return outputPath;
|
||||
} else {
|
||||
LogUtils.e(TAG, "【handleCropResult】裁剪失败:输出 Uri 为空");
|
||||
}
|
||||
} else if (resultCode == UCrop.RESULT_ERROR) {
|
||||
Throwable error = UCrop.getError(data);
|
||||
LogUtils.e(TAG, "【裁剪失败】原因:" + (error != null ? error.getMessage() : "未知错误"));
|
||||
LogUtils.e(TAG, "【handleCropResult】裁剪异常:" + (error != null ? error.getMessage() : "未知错误。"));
|
||||
} else {
|
||||
LogUtils.d(TAG, "【裁剪取消】用户手动取消");
|
||||
LogUtils.d(TAG, "【handleCropResult】裁剪取消:用户手动取消");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ====================== 辅助方法(优化适配强制 PNG 逻辑)======================
|
||||
/** 校验 Uri 有效性(确保是图片类型) */
|
||||
// ================================== 私有辅助方法(参数校验 + 格式转换 + 配置初始化)=================================
|
||||
/**
|
||||
* 校验 Uri 有效性(确保是图片类型)
|
||||
*/
|
||||
private static boolean isValidUri(Activity activity, Uri uri) {
|
||||
try {
|
||||
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) {
|
||||
LogUtils.e(TAG, "【Uri 校验失败】原因:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【isValidUri】Uri 校验失败 " + uri, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Uri 转 File(适配 FileProvider Uri 和普通 Uri) */
|
||||
/**
|
||||
* Uri 转 File(适配 FileProvider 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 {
|
||||
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();
|
||||
if (filePath == null) return null;
|
||||
if (filePath == null) {
|
||||
LogUtils.e(TAG, "【uriToFile】转换失败:Uri 路径为空 " + uri);
|
||||
return null;
|
||||
}
|
||||
// 适配 FileProvider 路径
|
||||
if (filePath.contains("/external_files/")) {
|
||||
filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/");
|
||||
} else if (filePath.contains("/cache/")) {
|
||||
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) {
|
||||
LogUtils.e(TAG, "【Uri 转 File 失败】uri=" + uri + ",原因:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【uriToFile】转换失败 " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Uri 提取文件路径 */
|
||||
/**
|
||||
* Uri 提取文件路径
|
||||
*/
|
||||
private static String uriToPath(Uri uri) {
|
||||
if (uri == null) return null;
|
||||
if (uri == null) {
|
||||
LogUtils.e(TAG, "【uriToPath】参数异常:Uri 为空");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
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();
|
||||
if (path == null) return null;
|
||||
if (path == null) {
|
||||
LogUtils.e(TAG, "【uriToPath】提取失败:Uri 路径为空 " + uri);
|
||||
return null;
|
||||
}
|
||||
// 适配多种 FileProvider 前缀
|
||||
String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"};
|
||||
for (String prefix : prefixes) {
|
||||
if (path.contains(prefix)) {
|
||||
path = path.substring(path.indexOf(prefix) + prefix.length());
|
||||
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;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【uriToPath】提取失败 " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心)
|
||||
* 移除 isPng 参数,全程用 PNG 配置
|
||||
*/
|
||||
private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
|
||||
|
||||
LogUtils.d(TAG, "【initCropOptions】初始化裁剪配置 | 自由裁剪=" + isFreeCrop);
|
||||
UCrop.Options options = new UCrop.Options();
|
||||
|
||||
// 裁剪模式配置(自由裁剪/固定比例)
|
||||
options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
|
||||
options.setFreeStyleCropEnabled(isFreeCrop);
|
||||
|
||||
// 裁剪配置(优化体验)
|
||||
//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)); // 状态栏颜色
|
||||
|
||||
|
||||
// 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
|
||||
// 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
|
||||
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
|
||||
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
|
||||
options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键)
|
||||
options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色
|
||||
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
|
||||
|
||||
// 3. 通用 UI 配置(保持原有风格)
|
||||
// 通用 UI 配置(保持原有风格)
|
||||
options.setHideBottomControls(true); // 隐藏底部控制栏
|
||||
options.setToolbarTitle("图片裁剪");
|
||||
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
|
||||
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));
|
||||
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark));
|
||||
|
||||
LogUtils.d(TAG, "【initCropOptions】配置完成:强制 PNG 输出,保留透明通道");
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修正文件后缀(强制转为 .png,覆盖原有任何图片后缀)
|
||||
* 修正文件后缀(强制转为指定后缀,覆盖原有任何图片后缀)
|
||||
*/
|
||||
private static File correctFileSuffix(File originFile, String targetSuffix) {
|
||||
String originName = originFile.getName();
|
||||
// 强制替换所有图片后缀为 targetSuffix(避免漏改)
|
||||
originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix;
|
||||
return new File(originFile.getParent(), originName);
|
||||
// 强制替换所有图片后缀为 targetSuffix
|
||||
String newName = IMAGE_SUFFIX_PATTERN.matcher(originName).replaceAll("") + "." + targetSuffix;
|
||||
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) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
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 {
|
||||
return Uri.fromFile(file);
|
||||
Uri uri = Uri.fromFile(file);
|
||||
LogUtils.d(TAG, "【getFileProviderUri】生成成功(Android 7.0-)| " + file.getAbsolutePath() + " → " + uri);
|
||||
return uri;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【getFileProviderUri】生成失败 " + file.getAbsolutePath(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示 Toast(避免崩溃) */
|
||||
/**
|
||||
* 显示 Toast(避免崩溃)
|
||||
*/
|
||||
private static void showToast(Activity activity, String msg) {
|
||||
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) {
|
||||
return getFileProviderUri(activity, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公有方法:Uri 转 File
|
||||
*/
|
||||
public static File getFileFromUriPublic(Activity activity, Uri uri) {
|
||||
return uriToFile(activity, uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公有方法:Uri 提取路径
|
||||
*/
|
||||
public static String getPathFromUriPublic(Uri uri) {
|
||||
return uriToPath(uri);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
@@ -18,156 +16,167 @@ import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 图片下载工具类(单例模式)
|
||||
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
|
||||
* 适配:Java 7 + Android API 30
|
||||
* 核心策略:OkHttp 全局复用、7天文件过期清理、UUID 唯一文件名、内置缓存目录(无需权限)
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/19 20:52
|
||||
* @Describe 图片下载工具类(单例模式)
|
||||
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
|
||||
*/
|
||||
public class ImageDownloader {
|
||||
public static final String TAG = "ImageDownloader";
|
||||
// 单例实例
|
||||
private static ImageDownloader sInstance;
|
||||
// OkHttp 客户端(全局复用,提升性能)
|
||||
private OkHttpClient mOkHttpClient;
|
||||
// 缓存目录:/data/data/应用包名/cache/networkdownload
|
||||
private File mCacheDir;
|
||||
// 过期时间:7天(单位:毫秒),可按需调整
|
||||
private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000;
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "ImageDownloader";
|
||||
// 缓存目录子文件夹名称
|
||||
private static final String CACHE_DIR_NAME = "networkdownload";
|
||||
// 过期时间:7天(单位:毫秒)
|
||||
private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000;
|
||||
// OkHttp 超时配置
|
||||
private static final int CONNECT_TIMEOUT = 10;
|
||||
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 ImageDownloader(Context context) {
|
||||
// 初始化 OkHttp 客户端(设置超时时间)
|
||||
mOkHttpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
// ================================== 成员变量(单例核心 + 全局资源)=================================
|
||||
// 单例实例
|
||||
private static ImageDownloader sInstance;
|
||||
// OkHttp 客户端(全局复用,提升性能)
|
||||
private OkHttpClient mOkHttpClient;
|
||||
// 缓存目录:/data/data/应用包名/cache/networkdownload
|
||||
private File mCacheDir;
|
||||
|
||||
// ================================== 单例方法(线程安全 + 应用上下文)=================================
|
||||
/**
|
||||
* 单例获取方法(线程安全)
|
||||
* @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();
|
||||
LogUtils.d(TAG, "【initOkHttpClient】OkHttp 客户端初始化完成");
|
||||
}
|
||||
|
||||
// 初始化缓存目录:networkdownload
|
||||
initCacheDir(context);
|
||||
// 初始化时清理过期文件
|
||||
clearExpiredFiles();
|
||||
}
|
||||
/**
|
||||
* 初始化缓存目录:若不存在则创建
|
||||
* @param context 应用上下文
|
||||
*/
|
||||
private void initCacheDir(Context context) {
|
||||
LogUtils.d(TAG, "【initCacheDir】开始初始化缓存目录");
|
||||
// 获取应用内置缓存目录(无需权限)
|
||||
File cacheRoot = context.getCacheDir();
|
||||
mCacheDir = new File(cacheRoot, CACHE_DIR_NAME);
|
||||
|
||||
/**
|
||||
* 单例获取方法(线程安全)
|
||||
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
|
||||
* @return 单例实例
|
||||
*/
|
||||
public static synchronized ImageDownloader getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
// 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏
|
||||
sInstance = new ImageDownloader(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
// 若目录不存在则创建(包括父目录)
|
||||
if (!mCacheDir.exists()) {
|
||||
boolean isCreated = mCacheDir.mkdirs();
|
||||
if (isCreated) {
|
||||
LogUtils.d(TAG, "【initCacheDir】缓存目录创建成功:" + mCacheDir.getAbsolutePath());
|
||||
} else {
|
||||
LogUtils.e(TAG, "【initCacheDir】缓存目录创建失败");
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "【initCacheDir】缓存目录已存在:" + mCacheDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化缓存目录:若不存在则创建
|
||||
*/
|
||||
private void initCacheDir(Context context) {
|
||||
// 获取应用内置缓存目录(无需权限)
|
||||
File cacheRoot = context.getCacheDir();
|
||||
mCacheDir = new File(cacheRoot, "networkdownload");
|
||||
// ================================== 核心业务方法(下载 + 清理 + 获取最新文件)=================================
|
||||
/**
|
||||
* 下载网络图片到缓存目录
|
||||
* @param imageUrl 图片网络链接
|
||||
* @param callback 下载结果回调(成功/失败)
|
||||
*/
|
||||
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.exists()) {
|
||||
boolean isCreated = mCacheDir.mkdirs();
|
||||
if (isCreated) {
|
||||
LogUtils.d("ImageDownloader", "networkdownload 缓存目录创建成功:" + mCacheDir.getAbsolutePath());
|
||||
} else {
|
||||
LogUtils.e("ImageDownloader", "networkdownload 缓存目录创建失败");
|
||||
}
|
||||
} else {
|
||||
LogUtils.d("ImageDownloader", "networkdownload 缓存目录已存在:" + mCacheDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (mCacheDir == null || !mCacheDir.exists()) {
|
||||
String errorMsg = "缓存目录不存在";
|
||||
LogUtils.e(TAG, "【downloadImage】参数校验失败:" + errorMsg);
|
||||
if (callback != null) {
|
||||
callback.onFailure(errorMsg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件)
|
||||
*/
|
||||
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()
|
||||
// 2. 构建 OkHttp 请求
|
||||
Request request = new Request.Builder()
|
||||
.url(imageUrl)
|
||||
.build();
|
||||
|
||||
// 异步下载(避免阻塞主线程)
|
||||
mOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||
// 3. 异步下载(避免阻塞主线程)
|
||||
mOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
// 下载失败,回调主线程
|
||||
String errorMsg = "下载失败:" + e.getMessage();
|
||||
LogUtils.e(TAG, "【downloadImage】OkHttp 下载失败", e);
|
||||
if (callback != null) {
|
||||
callback.onFailure("下载失败:" + e.getMessage());
|
||||
callback.onFailure(errorMsg);
|
||||
}
|
||||
LogUtils.e("ImageDownloader", "图片下载失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
// 3.1 响应状态校验
|
||||
if (!response.isSuccessful()) {
|
||||
// 响应失败(如 404、500)
|
||||
String errorMsg = "响应失败:" + response.code();
|
||||
LogUtils.e(TAG, "【downloadImage】响应失败,状态码:" + response.code());
|
||||
if (callback != null) {
|
||||
callback.onFailure("响应失败:" + response.code());
|
||||
callback.onFailure(errorMsg);
|
||||
}
|
||||
// 关闭响应体
|
||||
if (response.body() != null) {
|
||||
response.body().close();
|
||||
}
|
||||
LogUtils.e("ImageDownloader", "图片响应失败,状态码:" + response.code());
|
||||
return;
|
||||
}
|
||||
|
||||
// 响应成功,写入文件
|
||||
// 3.2 响应成功,写入文件
|
||||
InputStream inputStream = null;
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
@@ -179,7 +188,7 @@ public class ImageDownloader {
|
||||
|
||||
// 写入文件
|
||||
outputStream = new FileOutputStream(imageFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, len);
|
||||
@@ -187,30 +196,32 @@ public class ImageDownloader {
|
||||
outputStream.flush();
|
||||
|
||||
// 下载成功,回调主线程并返回文件路径
|
||||
String filePath = imageFile.getAbsolutePath();
|
||||
LogUtils.d(TAG, "【downloadImage】图片下载成功:" + filePath);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(imageFile.getAbsolutePath());
|
||||
callback.onSuccess(filePath);
|
||||
}
|
||||
LogUtils.d("ImageDownloader", "图片下载成功:" + imageFile.getAbsolutePath());
|
||||
|
||||
} catch (IOException e) {
|
||||
String errorMsg = "文件写入失败:" + e.getMessage();
|
||||
LogUtils.e(TAG, "【downloadImage】文件写入失败", e);
|
||||
if (callback != null) {
|
||||
callback.onFailure("文件写入失败:" + e.getMessage());
|
||||
callback.onFailure(errorMsg);
|
||||
}
|
||||
LogUtils.e("ImageDownloader", "图片写入失败:" + e.getMessage());
|
||||
} finally {
|
||||
// 关闭流(Java7 手动关闭,避免资源泄漏)
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LogUtils.e(TAG, "【downloadImage】输入流关闭失败", e);
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LogUtils.e(TAG, "【downloadImage】输出流关闭失败", e);
|
||||
}
|
||||
}
|
||||
// 关闭响应体
|
||||
@@ -220,75 +231,119 @@ public class ImageDownloader {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 networkdownload 目录中最后下载的文件(按修改时间排序)
|
||||
* @return 最后下载的文件路径(null 表示无文件)
|
||||
*/
|
||||
public String getLastDownloadedFile() {
|
||||
if (mCacheDir == null || !mCacheDir.exists()) {
|
||||
LogUtils.e("ImageDownloader", "缓存目录不存在");
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件)
|
||||
*/
|
||||
private void clearExpiredFiles() {
|
||||
LogUtils.d(TAG, "【clearExpiredFiles】开始清理过期文件");
|
||||
if (mCacheDir == null || !mCacheDir.exists()) {
|
||||
LogUtils.d(TAG, "【clearExpiredFiles】缓存目录不存在,无需清理");
|
||||
return;
|
||||
}
|
||||
|
||||
File[] files = mCacheDir.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
LogUtils.d("ImageDownloader", "缓存目录无文件");
|
||||
return null;
|
||||
}
|
||||
File[] files = mCacheDir.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
LogUtils.d(TAG, "【clearExpiredFiles】缓存目录无文件,无需清理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按最后修改时间降序排序,取第一个即为最新文件
|
||||
File lastFile = files[0];
|
||||
for (File file : files) {
|
||||
if (file.lastModified() > lastFile.lastModified()) {
|
||||
lastFile = file;
|
||||
}
|
||||
}
|
||||
long currentTime = System.currentTimeMillis();
|
||||
int deleteCount = 0;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:从图片链接中提取文件后缀(如 .png、.jpg)
|
||||
* @param imageUrl 图片链接
|
||||
* @return 文件后缀(含点号,若无法提取则返回 .jpg)
|
||||
*/
|
||||
private String getFileExtension(String imageUrl) {
|
||||
if (TextUtils.isEmpty(imageUrl)) {
|
||||
return ".jpg";
|
||||
}
|
||||
LogUtils.d(TAG, "【clearExpiredFiles】过期文件清理完成,共删除 " + deleteCount + " 个文件");
|
||||
}
|
||||
|
||||
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() <= 5) {
|
||||
return extension.toLowerCase(); // 统一转为小写
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取 networkdownload 目录中最后下载的文件(按修改时间排序)
|
||||
* @return 最后下载的文件路径(null 表示无文件)
|
||||
*/
|
||||
public String getLastDownloadedFile() {
|
||||
LogUtils.d(TAG, "【getLastDownloadedFile】获取最新下载文件");
|
||||
if (mCacheDir == null || !mCacheDir.exists()) {
|
||||
LogUtils.e(TAG, "【getLastDownloadedFile】缓存目录不存在");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 无法提取后缀时,默认使用 .jpg
|
||||
return ".jpg";
|
||||
}
|
||||
File[] files = mCacheDir.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
LogUtils.d(TAG, "【getLastDownloadedFile】缓存目录无文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载结果回调接口(Java7 接口实现)
|
||||
*/
|
||||
public interface DownloadCallback {
|
||||
/**
|
||||
* 下载成功
|
||||
* @param filePath 图片保存路径
|
||||
*/
|
||||
void onSuccess(String filePath);
|
||||
// 按最后修改时间降序排序,取第一个即为最新文件
|
||||
File lastFile = files[0];
|
||||
for (File file : files) {
|
||||
if (file.lastModified() > lastFile.lastModified()) {
|
||||
lastFile = file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载失败
|
||||
* @param errorMsg 失败原因
|
||||
*/
|
||||
void onFailure(String errorMsg);
|
||||
}
|
||||
String filePath = lastFile.getAbsolutePath();
|
||||
LogUtils.d(TAG, "【getLastDownloadedFile】最后下载的文件:" + filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// ================================== 辅助工具方法(文件后缀提取)=================================
|
||||
/**
|
||||
* 工具方法:从图片链接中提取文件后缀(如 .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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +1,150 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* 图片处理工具类(质量压缩专用)
|
||||
* 功能:对图片进行JPEG质量压缩,并将压缩结果覆盖源文件;获取主题colorPrimary颜色值
|
||||
* 适配:Java 7 + Android API 30
|
||||
* 核心逻辑:Bitmap.compress 质量压缩 + FileChannel 高效文件复制;主题属性解析
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class ImageUtils {
|
||||
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = ImageUtils.class.getSimpleName();
|
||||
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}; // colorPrimary属性数组
|
||||
|
||||
// 这里我们生成了一个Pic文件夹,在下面放了我们质量压缩后的图片,用于和原图对比
|
||||
// 压缩图片使用Bitmap.compress(),这里是质量压缩
|
||||
// 参数:Context context :调用本函数函数引用的资源体系
|
||||
// String szSrcImagePath :要压缩的源文件路径
|
||||
// String szDstImagePath :压缩后文件要保存的路径
|
||||
// int nPictureCompress :图片压缩比例
|
||||
public static void bitmapCompress(Context context, String szSrcImagePath, String szDstImagePath, int nPictureCompress) {
|
||||
// ================================== 新增:Accent 颜色获取方法 =================================
|
||||
/**
|
||||
* 从当前应用主题中获取 colorAccent 颜色值
|
||||
* @param context 上下文,用于获取主题与资源
|
||||
* @return 解析到的 colorAccent 颜色值,解析失败返回默认白色#FFFFFFFF
|
||||
*/
|
||||
public static int getColorAccent(Context context) {
|
||||
LogUtils.d(TAG, "【getColorAccent】方法调用");
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "【getColorAccent】参数异常:Context为空,返回默认颜色");
|
||||
return Color.parseColor("#FFFFFFFF");
|
||||
}
|
||||
|
||||
TypedArray typedArray = null;
|
||||
try {
|
||||
Bitmap bmpCompressImage;
|
||||
// 从当前主题中解析 colorAccent 属性
|
||||
typedArray = context.obtainStyledAttributes(COLOR_ACCENT_ATTR);
|
||||
int colorAccent = typedArray.getColor(0, Color.parseColor("#FFFFFFFF"));
|
||||
LogUtils.d(TAG, String.format("【getColorAccent】解析成功 | colorAccent=0x%08X", colorAccent));
|
||||
return colorAccent;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【getColorAccent】解析失败,返回默认颜色", e);
|
||||
return Color.parseColor("#FFFFFFFF");
|
||||
} finally {
|
||||
// 回收TypedArray资源,避免内存泄漏
|
||||
if (typedArray != null) {
|
||||
typedArray.recycle();
|
||||
LogUtils.d(TAG, "【getColorAccent】TypedArray资源已回收");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//生成新的文件
|
||||
File fDstCompressImage = new File(szDstImagePath);
|
||||
// ================================== 核心工具方法(图片质量压缩)=================================
|
||||
/**
|
||||
* 图片质量压缩(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;
|
||||
}
|
||||
|
||||
//裁剪后的图像转成BitMap
|
||||
//photoBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uriClipUri));
|
||||
bmpCompressImage = BitmapFactory.decodeFile(szSrcImagePath);
|
||||
File srcFile = new File(srcImagePath);
|
||||
if (!srcFile.exists() || !srcFile.isFile()) {
|
||||
LogUtils.e(TAG, "【bitmapCompress】源文件无效:不存在或不是文件 " + srcImagePath);
|
||||
return;
|
||||
}
|
||||
|
||||
//创建输出流
|
||||
OutputStream out = null;
|
||||
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()));
|
||||
|
||||
out = new FileOutputStream(fDstCompressImage.getPath());
|
||||
// 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()));
|
||||
}
|
||||
|
||||
//压缩文件,返回结果,参数分别是压缩的格式,压缩质量的百分比,输出流
|
||||
boolean bCompress = bmpCompressImage.compress(Bitmap.CompressFormat.JPEG, nPictureCompress, out);
|
||||
// 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());
|
||||
|
||||
// 复制压缩后的文件到源路径
|
||||
File fSrcImage = new File(szSrcImagePath);
|
||||
FileUtils.copyFileUsingFileChannels(fDstCompressImage, fSrcImage);
|
||||
LogUtils.d(TAG, Integer.toString(nPictureCompress) + "%压缩结束。");
|
||||
// 5. 复制压缩文件覆盖源文件
|
||||
FileUtils.copyFileUsingFileChannels(dstFile, srcFile);
|
||||
LogUtils.d(TAG, String.format("【bitmapCompress】%d%%压缩结束:已覆盖源文件 %s",
|
||||
compressQuality, srcImagePath));
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
LogUtils.d(TAG, "bitmapCompress FileNotFoundException : " + e.getMessage());
|
||||
LogUtils.e(TAG, "【bitmapCompress】文件未找到异常", e);
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "bitmapCompress IOException : " + e.getMessage());
|
||||
LogUtils.e(TAG, "【bitmapCompress】IO异常", 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资源已回收");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,69 @@ package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 服务状态工具类
|
||||
* 功能:判断指定服务是否处于运行状态
|
||||
* 适配:Java 7 + Android API 30
|
||||
* 注意:Android 8.0+ 对后台服务限制严格,此方法仅适用于前台服务或兼容场景
|
||||
*/
|
||||
public class ServiceUtils {
|
||||
|
||||
// ================================== 静态常量区(置顶归类)=================================
|
||||
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);
|
||||
// 获取正在运行的服务(此处设置最多取1000个)
|
||||
List<ActivityManager.RunningServiceInfo> runningServices = manager
|
||||
.getRunningServices(1000);
|
||||
if (runningServices.size() <= 0) {
|
||||
// ================================== 核心工具方法(判断服务是否运行)=================================
|
||||
/**
|
||||
* 判断指定服务是否处于运行状态
|
||||
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
|
||||
* @param serviceName 服务完整类名(如:com.example.app.service.DemoService)
|
||||
* @return true-服务运行中,false-服务未运行或查询失败
|
||||
*/
|
||||
public static boolean isServiceAlive(Context context, String serviceName) {
|
||||
LogUtils.d(TAG, "【isServiceAlive】调用开始 | 服务名称=" + serviceName);
|
||||
// 1. 前置参数校验
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "【isServiceAlive】参数异常:Context 为空");
|
||||
return false;
|
||||
}
|
||||
// 遍历,若存在名字和传入的serviceName的一致则说明存在
|
||||
for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) {
|
||||
if (runningServiceInfo.service.getClassName().equals(szServiceName)) {
|
||||
if (TextUtils.isEmpty(serviceName)) {
|
||||
LogUtils.e(TAG, "【isServiceAlive】参数异常:服务名称为空");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "【isServiceAlive】服务未运行 | 未匹配到:" + serviceName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,114 +1,151 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 字符串格式化工具类
|
||||
* 功能:电量使用时间列表格式化、时间跨度计算
|
||||
* 适配:Java 7 + Android API 30
|
||||
* 核心逻辑:将电池信息列表转换为指定格式字符串,计算时间戳之间的跨度并格式化
|
||||
*/
|
||||
public class StringUtils {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
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) {
|
||||
/* 调试数据
|
||||
Time t1 = new Time();
|
||||
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
|
||||
t1.set(4, 8, 0, 27, 4, 2022);
|
||||
long ntime1 = t1.toMillis(true);
|
||||
Time t2 = new Time();
|
||||
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
|
||||
t2.set(9, 12, 3, 29, 4, 2022);
|
||||
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;
|
||||
// ================================== 核心格式化方法(电量列表格式化)=================================
|
||||
/**
|
||||
* 格式化电量使用时间列表为单行字符串
|
||||
* @param batteryInfoList 电池信息列表(非空)
|
||||
* @return 格式化后的单行字符串,格式:"电量% 时间跨度 电量% 时间跨度 ..."
|
||||
*/
|
||||
public static String formatPCMListString(ArrayList<BatteryInfoBean> batteryInfoList) {
|
||||
LogUtils.d(TAG, "【formatPCMListString】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null));
|
||||
// 1. 参数校验
|
||||
if (batteryInfoList == null || batteryInfoList.size() < 2) {
|
||||
LogUtils.e(TAG, "【formatPCMListString】参数异常:列表为空或长度不足2");
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
return sz;
|
||||
}
|
||||
|
||||
public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> arrayListBatteryInfo) {
|
||||
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 = "\n" + arrayListBatteryInfo.get(i).getBattetyValue() + "%\n " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz;
|
||||
|
||||
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, "【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.
|
||||
// 返回值: {(几天/)(几小时/)(几分钟/)(几秒钟)}
|
||||
// 返回值: {(几小时/)(几分钟/)(几秒钟)}
|
||||
// 返回值: {(几分钟/)(几秒钟)}
|
||||
// 返回值: {(几秒钟)}
|
||||
// (注:start == end 时) 返回值: {0}
|
||||
/**
|
||||
* 格式化电量使用时间列表为带换行的字符串
|
||||
* @param batteryInfoList 电池信息列表(非空)
|
||||
* @return 格式化后的带换行字符串,每行一个电量和时间跨度
|
||||
*/
|
||||
public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> batteryInfoList) {
|
||||
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) {
|
||||
String szReturn = "{";
|
||||
LogUtils.d(TAG, "【getTimespanDifference】调用开始 | 开始时间戳=" + start + " | 结束时间戳=" + end);
|
||||
long between = end - start;
|
||||
//LogUtils.d(TAG, "between is " + Long.toString(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;
|
||||
*/
|
||||
LogUtils.d(TAG, "【getTimespanDifference】时间差(毫秒)=" + between);
|
||||
|
||||
//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) : "";
|
||||
szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d✰", min) : "";
|
||||
szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d}", s) : "☆}";
|
||||
// 拼接结果字符串
|
||||
StringBuilder result = new StringBuilder("{");
|
||||
boolean hasHigherUnit = false;
|
||||
|
||||
//String strmin = String.format("%02d", min);
|
||||
//String strs = String.format("%02d", s);
|
||||
//String strms = String.format("%03d",ms);
|
||||
//String timeDifference = day + "天" + hour + "小时" + strmin + "分" + strs + "秒" + strms + "毫秒";
|
||||
//String timeDifference = hour + getString(R.string.activity_main_msg_hour)
|
||||
// + strmin + getString(R.string.activity_main_msg_minute)
|
||||
// + strs + getString(R.string.activity_main_msg_second);
|
||||
//return timeDifference;
|
||||
// 拼接天
|
||||
if (day > 0) {
|
||||
result.append(String.format(Locale.getDefault(), "%d%s", day, UNIT_DAY));
|
||||
hasHigherUnit = true;
|
||||
}
|
||||
// 拼接时(当天>0或后续有单位时)
|
||||
if (hour > 0 || hasHigherUnit) {
|
||||
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));
|
||||
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
@@ -3,87 +3,89 @@ package cc.winboll.studio.powerbell.views;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 基于Java7的BackgroundView(LinearLayout+ImageView,保持原图比例居中平铺)
|
||||
* 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪
|
||||
* 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪、无压缩
|
||||
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质
|
||||
*/
|
||||
public class BackgroundView extends RelativeLayout {
|
||||
|
||||
// ====================================== 静态常量区 ======================================
|
||||
public static final String TAG = "BackgroundView";
|
||||
// 新增:记录当前已缓存的图片路径
|
||||
private String mCurrentCachedPath = "";
|
||||
// Bitmap 配置常量(原始品质)
|
||||
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
|
||||
private static final int BITMAP_SAMPLE_SIZE = 1; // 不缩放采样率
|
||||
|
||||
// ====================================== 成员变量区 ======================================
|
||||
// 缓存相关
|
||||
private String mCurrentCachedPath = "";
|
||||
// 视图相关
|
||||
private Context mContext;
|
||||
private LinearLayout mLlContainer; // 主容器LinearLayout
|
||||
private ImageView mIvBackground; // 图片显示控件
|
||||
// 图片属性
|
||||
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高)
|
||||
|
||||
// ====================================== 构造器(Java7兼容) ======================================
|
||||
public BackgroundView(Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ===");
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器1启动 [context] ===");
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
public BackgroundView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ===");
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器2启动 [context, attrs] ===");
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ===");
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器3启动 [context, attrs, defStyleAttr] ===");
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
// ====================================== 初始化 ======================================
|
||||
// ====================================== 初始化方法 ======================================
|
||||
private void initView() {
|
||||
LogUtils.d(TAG, "=== initView 启动 ===");
|
||||
// 1. 配置当前控件:全屏+透明
|
||||
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
|
||||
// 2. 初始化主容器LinearLayout
|
||||
initLinearLayout();
|
||||
|
||||
// 3. 初始化ImageView
|
||||
initImageView();
|
||||
|
||||
// 初始设置透明背景
|
||||
// 4. 初始设置透明背景
|
||||
setDefaultTransparentBackground();
|
||||
|
||||
LogUtils.d(TAG, "=== initView 完成 ===");
|
||||
}
|
||||
|
||||
private void initLinearLayout() {
|
||||
LogUtils.d(TAG, "=== initLinearLayout 启动 ===");
|
||||
mLlContainer = new LinearLayout(mContext);
|
||||
// 配置LinearLayout:全屏+垂直方向+居中
|
||||
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
mLlContainer.setLayoutParams(llParams);
|
||||
mLlContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中
|
||||
mLlContainer.setGravity(android.view.Gravity.CENTER);
|
||||
mLlContainer.setBackgroundColor(0x00000000);
|
||||
this.addView(mLlContainer);
|
||||
LogUtils.d(TAG, "=== initLinearLayout 完成 ===");
|
||||
@@ -92,202 +94,313 @@ public class BackgroundView extends RelativeLayout {
|
||||
private void initImageView() {
|
||||
LogUtils.d(TAG, "=== initImageView 启动 ===");
|
||||
mIvBackground = new ImageView(mContext);
|
||||
// 配置ImageView:wrap_content+居中+透明背景
|
||||
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
mIvBackground.setLayoutParams(ivParams);
|
||||
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺
|
||||
mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
mIvBackground.setBackgroundColor(0x00000000);
|
||||
mLlContainer.addView(mIvBackground);
|
||||
LogUtils.d(TAG, "=== initImageView 完成 ===");
|
||||
}
|
||||
|
||||
// ====================================== 对外方法 ======================================
|
||||
public void loadByBackgroundBean(BackgroundBean bean) {
|
||||
loadByBackgroundBean(bean, false);
|
||||
}
|
||||
|
||||
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
if (!bean.isUseBackgroundFile()) {
|
||||
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
LogUtils.d(TAG, "=== loadByBackgroundBean 启动 [isRefresh:" + isRefresh + "] ===");
|
||||
// 参数校验
|
||||
if (bean == null) {
|
||||
LogUtils.e(TAG, "loadByBackgroundBean: BackgroundBean为空");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
// 判断是否使用背景文件
|
||||
if (!bean.isUseBackgroundFile()) {
|
||||
LogUtils.d(TAG, "loadByBackgroundBean: 不使用背景文件,设置透明背景");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
// 获取目标路径
|
||||
String targetPath = bean.isUseBackgroundScaledCompressFile()
|
||||
? bean.getBackgroundScaledCompressFilePath()
|
||||
: bean.getBackgroundFilePath();
|
||||
|
||||
if (!(new File(targetPath).exists())) {
|
||||
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用带路径判断的loadImage方法
|
||||
if (isRefresh) {
|
||||
App.sBitmapCacheUtils.removeCachedBitmap(targetPath);
|
||||
App.sBitmapCacheUtils.cacheBitmap(targetPath);
|
||||
}
|
||||
loadImage(targetPath);
|
||||
LogUtils.d(TAG, "loadByBackgroundBean: 目标路径=" + targetPath);
|
||||
// 校验文件是否存在
|
||||
File targetFile = new File(targetPath);
|
||||
if (!targetFile.exists() || !targetFile.isFile()) {
|
||||
LogUtils.e(TAG, "loadByBackgroundBean: 视图控件图片不存在:" + targetPath);
|
||||
return;
|
||||
}
|
||||
// 刷新逻辑:重新解码原始品质图片并更新缓存
|
||||
if (isRefresh) {
|
||||
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存");
|
||||
Bitmap newBitmap = decodeOriginalBitmap(targetFile);
|
||||
if (newBitmap != null) {
|
||||
App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap);
|
||||
App.sBitmapCacheUtils.increaseRefCount(targetPath);
|
||||
LogUtils.d(TAG, "loadByBackgroundBean: 刷新缓存成功,路径=" + targetPath);
|
||||
} else {
|
||||
LogUtils.e(TAG, "loadByBackgroundBean: 刷新解码失败,路径=" + targetPath);
|
||||
}
|
||||
}
|
||||
// 加载图片
|
||||
loadImage(targetPath);
|
||||
LogUtils.d(TAG, "=== loadByBackgroundBean 完成 ===");
|
||||
}
|
||||
|
||||
// ====================================== 对外方法 ======================================
|
||||
/**
|
||||
* 改造后:添加路径判断,路径更新时同步更新缓存;缓存Bitmap为null时提示并加载透明背景
|
||||
* 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理,保留图片原始品质
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void loadImage(String imagePath) {
|
||||
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
|
||||
// 1. 路径空校验
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "loadImage: 图片路径为空");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 文件有效性校验
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile()) {
|
||||
LogUtils.e(TAG, "图片文件无效");
|
||||
LogUtils.e(TAG, "loadImage: 图片文件无效");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
// 3. 隐藏ImageView防止闪烁
|
||||
mIvBackground.setVisibility(View.GONE);
|
||||
|
||||
//mIvBackground.setVisibility(View.GONE);
|
||||
|
||||
// ======================== 新增:路径判断逻辑 ========================
|
||||
// 1. 路径未变化:直接使用缓存
|
||||
// ======================== 路径判断逻辑(强制缓存版) ========================
|
||||
// 3.1 路径未变化:校验缓存有效性
|
||||
if (imagePath.equals(mCurrentCachedPath)) {
|
||||
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
|
||||
// 核心修改:判断缓存Bitmap是否为null
|
||||
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
|
||||
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
|
||||
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
|
||||
mIvBackground.setImageBitmap(cachedBitmap);
|
||||
adjustImageViewSize();
|
||||
return;
|
||||
} else {
|
||||
// 缓存Bitmap为空或已回收,提示并加载透明背景
|
||||
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
|
||||
ToastUtils.show("全局位图缓存为空,无法加载图片");
|
||||
setDefaultTransparentBackground();
|
||||
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
|
||||
if (isBitmapValid(cachedBitmap)) {
|
||||
LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存Bitmap(原始品质)");
|
||||
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
|
||||
mIvBackground.setImageBitmap(cachedBitmap);
|
||||
adjustImageViewSize();
|
||||
LogUtils.d(TAG, "=== loadImage 完成(缓存命中) ===");
|
||||
return;
|
||||
} else {
|
||||
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效,尝试重加载原始品质图片");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
|
||||
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
|
||||
App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
|
||||
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
|
||||
// 3.2 路径已更新:保留旧缓存,仅更新路径记录
|
||||
if (!TextUtils.isEmpty(mCurrentCachedPath) && !mCurrentCachedPath.equals(imagePath)) {
|
||||
LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,原路径=" + mCurrentCachedPath + ",新路径=" + imagePath);
|
||||
}
|
||||
// ======================== 路径判断逻辑结束 ========================
|
||||
|
||||
// 无缓存/路径更新:走原有逻辑加载图片
|
||||
// 4. 计算图片宽高比
|
||||
if (!calculateImageAspectRatio(imageFile)) {
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "loadImage: 图片解码失败");
|
||||
ToastUtils.show("图片解码失败,无法加载");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
// 5. 获取或解码Bitmap
|
||||
Bitmap bitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
|
||||
if (isBitmapValid(bitmap)) {
|
||||
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap(原始品质)");
|
||||
} else {
|
||||
LogUtils.d(TAG, "loadImage: 缓存未命中,解码原始品质图片");
|
||||
bitmap = decodeOriginalBitmap(imageFile);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "loadImage: 图片解码失败(原始品质)");
|
||||
ToastUtils.show("图片解码失败,无法加载");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
// 缓存新图片
|
||||
App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap);
|
||||
LogUtils.d(TAG, "loadImage: 新图片缓存成功,路径=" + imagePath);
|
||||
}
|
||||
|
||||
// 缓存新图片,并更新当前缓存路径记录
|
||||
App.sBitmapCacheUtils.cacheBitmap(imagePath);
|
||||
// 6. 引用计数管理
|
||||
App.sBitmapCacheUtils.increaseRefCount(imagePath);
|
||||
// 7. 更新当前缓存路径
|
||||
mCurrentCachedPath = imagePath;
|
||||
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
|
||||
|
||||
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
|
||||
// 8. 设置图片并调整尺寸
|
||||
mIvBackground.setImageBitmap(bitmap);
|
||||
adjustImageViewSize();
|
||||
LogUtils.d(TAG, "=== loadImage 完成 ===");
|
||||
}
|
||||
|
||||
// ====================================== 内部工具方法 ======================================
|
||||
/**
|
||||
* 工具方法:判断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) {
|
||||
LogUtils.d(TAG, "=== calculateImageAspectRatio 启动,文件=" + file.getAbsolutePath() + " ===");
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
|
||||
// 尺寸校验
|
||||
int width = options.outWidth;
|
||||
int height = options.outHeight;
|
||||
if (width <= 0 || height <= 0) {
|
||||
LogUtils.e(TAG, "图片尺寸无效");
|
||||
LogUtils.e(TAG, "calculateImageAspectRatio: 图片尺寸无效,宽=" + width + ",高=" + height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算比例
|
||||
mImageAspectRatio = (float) width / height;
|
||||
LogUtils.d(TAG, "原图比例:" + mImageAspectRatio);
|
||||
LogUtils.d(TAG, "calculateImageAspectRatio: 原图比例=" + mImageAspectRatio);
|
||||
LogUtils.d(TAG, "=== calculateImageAspectRatio 完成 ===");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "计算比例失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, "calculateImageAspectRatio: 计算比例失败:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) {
|
||||
/**
|
||||
* 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失)
|
||||
*/
|
||||
private Bitmap decodeOriginalBitmap(File file) {
|
||||
LogUtils.d(TAG, "=== decodeOriginalBitmap 启动,文件=" + file.getAbsolutePath() + " ===");
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
|
||||
int scaleX = options.outWidth / maxWidth;
|
||||
int scaleY = options.outHeight / maxHeight;
|
||||
int inSampleSize = Math.max(scaleX, scaleY);
|
||||
if (inSampleSize <= 0) inSampleSize = 1;
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = inSampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
return 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;
|
||||
// 解码图片
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
if (bitmap != null) {
|
||||
LogUtils.d(TAG, "decodeOriginalBitmap: 解码成功,宽=" + bitmap.getWidth() + ",高=" + bitmap.getHeight());
|
||||
} else {
|
||||
LogUtils.e(TAG, "decodeOriginalBitmap: 解码返回null");
|
||||
}
|
||||
LogUtils.d(TAG, "=== decodeOriginalBitmap 完成 ===");
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, "decodeOriginalBitmap: 原始品质解码失败:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整ImageView尺寸,保持原图比例
|
||||
*/
|
||||
private void adjustImageViewSize() {
|
||||
LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
|
||||
// 空指针校验
|
||||
if (mLlContainer == null || mIvBackground == null) {
|
||||
LogUtils.e(TAG, "adjustImageViewSize: 容器或ImageView未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取容器尺寸
|
||||
int llWidth = mLlContainer.getWidth();
|
||||
int llHeight = mLlContainer.getHeight();
|
||||
|
||||
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);
|
||||
if (llWidth == 0 || llHeight == 0) {
|
||||
LogUtils.w(TAG, "adjustImageViewSize: 容器尺寸未初始化,延迟调整");
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adjustImageViewSize();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 计算ImageView尺寸
|
||||
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(ImageView.ScaleType.FIT_CENTER);
|
||||
mIvBackground.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "adjustImageViewSize: 尺寸调整完成,宽=" + ivWidth + ",高=" + ivHeight);
|
||||
LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认透明背景,仅减少引用计数,不删除缓存
|
||||
*/
|
||||
private void setDefaultTransparentBackground() {
|
||||
mIvBackground.setImageBitmap(null);
|
||||
LogUtils.d(TAG, "=== setDefaultTransparentBackground 启动 ===");
|
||||
// 清空ImageView
|
||||
mIvBackground.setImageDrawable(null);
|
||||
mIvBackground.setBackgroundColor(0x00000000);
|
||||
mImageAspectRatio = 1.0f;
|
||||
// 清空缓存路径记录
|
||||
mCurrentCachedPath = "";
|
||||
//mIvBackground.setVisibility(View.GONE);
|
||||
// 减少引用计数,不删除缓存
|
||||
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
|
||||
LogUtils.d(TAG, "setDefaultTransparentBackground: 减少引用计数,路径=" + mCurrentCachedPath);
|
||||
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
|
||||
mCurrentCachedPath = "";
|
||||
}
|
||||
LogUtils.d(TAG, "=== setDefaultTransparentBackground 完成 ===");
|
||||
}
|
||||
|
||||
// ====================================== 重写方法 ======================================
|
||||
// ====================================== 重写生命周期方法 ======================================
|
||||
/**
|
||||
* 重写:绘制前强制校验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
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
LogUtils.d(TAG, "=== onDetachedFromWindow 启动 ===");
|
||||
// 清空ImageView的Drawable,释放本地引用
|
||||
mIvBackground.setImageDrawable(null);
|
||||
// 减少引用计数,不删除全局缓存
|
||||
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
|
||||
LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,路径=" + mCurrentCachedPath);
|
||||
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
|
||||
mCurrentCachedPath = "";
|
||||
}
|
||||
LogUtils.d(TAG, "=== onDetachedFromWindow 完成 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写:恢复尺寸调整逻辑,确保View尺寸变化时正确显示(无压缩)
|
||||
*/
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
//adjustImageViewSize(); // 尺寸变化时重新调整
|
||||
LogUtils.d(TAG, "onSizeChanged: 尺寸变化,宽=" + w + ",高=" + h + ",调整ImageView尺寸");
|
||||
adjustImageViewSize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -9,32 +9,37 @@ import android.graphics.drawable.Drawable;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 电池电量Drawable:适配API30,兼容小米机型,支持能量/条纹两种绘制风格切换
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 12:55
|
||||
* @Describe 电池电量Drawable:适配API30,兼容小米机型,支持能量/条纹两种绘制风格切换
|
||||
*/
|
||||
public class BatteryDrawable extends Drawable {
|
||||
// ====================== 静态常量(置顶,按重要性排序) ======================
|
||||
// ====================================== 静态常量区(按功能归类,消除魔法值) ======================================
|
||||
public static final String TAG = "BatteryDrawable";
|
||||
// 小米机型绘制偏移校准(适配MIUI渲染特性,避免绘制错位)
|
||||
private static final int MIUI_DRAW_OFFSET = 1;
|
||||
// 默认电量透明度(兼顾显示效果与API30渲染性能)
|
||||
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修饰,避免重复创建,提升性能)
|
||||
private final Paint mBatteryPaint;
|
||||
// 业务控制变量
|
||||
private int mBatteryValue = -1; // 当前电量(0-100,-1=未初始化)
|
||||
private boolean mIsEnergyStyle = true; // 绘制风格(true=能量,false=条纹)
|
||||
|
||||
// ====================== 构造方法(重载适配,优先暴露常用构造) ======================
|
||||
// ====================================== 构造方法(重载适配,优先暴露常用构造) ======================================
|
||||
/**
|
||||
* 构造方法(默认能量风格,常用场景)
|
||||
* @param batteryColor 电量显示颜色
|
||||
*/
|
||||
public BatteryDrawable(int batteryColor) {
|
||||
LogUtils.d(TAG, "constructor: 初始化(能量风格),颜色=" + Integer.toHexString(batteryColor));
|
||||
LogUtils.d(TAG, "【BatteryDrawable】构造器1调用 | 能量风格 | 颜色=" + Integer.toHexString(batteryColor));
|
||||
mBatteryPaint = new Paint();
|
||||
initPaintConfig(batteryColor);
|
||||
}
|
||||
@@ -45,85 +50,109 @@ public class BatteryDrawable extends Drawable {
|
||||
* @param isEnergyStyle 是否启用能量风格
|
||||
*/
|
||||
public BatteryDrawable(int batteryColor, boolean isEnergyStyle) {
|
||||
LogUtils.d(TAG, "constructor: 初始化,颜色=" + Integer.toHexString(batteryColor) + ",风格=" + (isEnergyStyle ? "能量" : "条纹"));
|
||||
LogUtils.d(TAG, "【BatteryDrawable】构造器2调用 | 颜色=" + Integer.toHexString(batteryColor) + " | 风格=" + (isEnergyStyle ? "能量" : "条纹"));
|
||||
mBatteryPaint = new Paint();
|
||||
mIsEnergyStyle = isEnergyStyle;
|
||||
initPaintConfig(batteryColor);
|
||||
}
|
||||
|
||||
// ====================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================
|
||||
// ====================================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================================
|
||||
/**
|
||||
* 初始化画笔配置(适配API30渲染特性,优化小米机型兼容性)
|
||||
* @param color 电量显示颜色
|
||||
*/
|
||||
private void initPaintConfig(int color) {
|
||||
LogUtils.d(TAG, "【initPaintConfig】画笔配置开始 | 颜色=" + Integer.toHexString(color));
|
||||
mBatteryPaint.setColor(color);
|
||||
mBatteryPaint.setAlpha(DEFAULT_BATTERY_ALPHA);
|
||||
mBatteryPaint.setAntiAlias(true); // 抗锯齿,解决小米低分辨率锯齿问题
|
||||
mBatteryPaint.setStyle(Paint.Style.FILL); // 固定填充模式,避免混乱
|
||||
mBatteryPaint.setDither(false); // 禁用抖动,提升API30颜色显示一致性
|
||||
LogUtils.d(TAG, "initPaintConfig: 画笔配置完成");
|
||||
LogUtils.d(TAG, "【initPaintConfig】画笔配置完成");
|
||||
}
|
||||
|
||||
// ====================== 核心绘制方法(Drawable抽象方法,优先级最高) ======================
|
||||
// ====================================== 核心绘制方法(Drawable抽象方法,优先级最高) ======================================
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
LogUtils.d(TAG, "【draw】绘制开始 | 当前电量=" + mBatteryValue + " | 风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
|
||||
// 未初始化/异常电量,直接跳过,避免无效绘制
|
||||
if (mBatteryValue < 0) {
|
||||
LogUtils.w(TAG, "draw: 电量未初始化,跳过绘制");
|
||||
LogUtils.w(TAG, "【draw】电量未初始化,跳过绘制");
|
||||
return;
|
||||
}
|
||||
// 强制校准电量范围(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();
|
||||
// 绘制边界空指针防护
|
||||
if (drawBounds == null) {
|
||||
LogUtils.e(TAG, "【draw】绘制边界为空,跳过绘制");
|
||||
return;
|
||||
}
|
||||
int drawHeight = drawBounds.height();
|
||||
|
||||
// 小米机型绘制偏移校准(解决MIUI系统渲染偏移问题)
|
||||
int offset = MIUI_DRAW_OFFSET;
|
||||
int left = drawBounds.left + offset;
|
||||
int right = drawBounds.right - offset;
|
||||
LogUtils.d(TAG, "【draw】绘制参数校准 | 左边界=" + left + " | 右边界=" + right + " | 高度=" + drawHeight);
|
||||
|
||||
// 按风格执行绘制(精简日志,仅保留核心绘制参数)
|
||||
LogUtils.d(TAG, "draw: 开始绘制,电量=" + validBattery + ",风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
|
||||
// 按风格执行绘制
|
||||
if (mIsEnergyStyle) {
|
||||
drawEnergyStyle(canvas, validBattery, left, right, drawHeight);
|
||||
} else {
|
||||
drawStripeStyle(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) {
|
||||
int top = height - (height * battery / 100); // 计算电量对应顶部坐标
|
||||
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制开始 | 电量=" + battery);
|
||||
int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
|
||||
canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
|
||||
LogUtils.d(TAG, "drawEnergyStyle: 绘制完成,顶部坐标=" + top);
|
||||
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
|
||||
}
|
||||
|
||||
/**
|
||||
* 条纹风格绘制(分段条纹,扩展风格)
|
||||
* @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) {
|
||||
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);
|
||||
int top = bottom - stripeHeight;
|
||||
canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
|
||||
}
|
||||
LogUtils.d(TAG, "drawStripeStyle: 绘制完成,条纹数量=" + battery);
|
||||
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
|
||||
}
|
||||
|
||||
// ====================== 对外暴露方法(业务控制入口,按功能排序) ======================
|
||||
// ====================================== 对外暴露方法(业务控制入口,按功能排序) ======================================
|
||||
/**
|
||||
* 设置当前电量(外部核心调用入口)
|
||||
* @param value 电量值(0-100)
|
||||
*/
|
||||
public void setBatteryValue(int value) {
|
||||
LogUtils.d(TAG, "setBatteryValue: 电量更新,旧值=" + mBatteryValue + ",新值=" + value);
|
||||
LogUtils.d(TAG, "【setBatteryValue】电量更新 | 旧值=" + mBatteryValue + " | 新值=" + value);
|
||||
mBatteryValue = value;
|
||||
invalidateSelf(); // 触发重绘,确保UI实时更新
|
||||
LogUtils.d(TAG, "【setBatteryValue】已触发重绘");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,9 +160,10 @@ public class BatteryDrawable extends Drawable {
|
||||
* @param isEnergyStyle true=能量风格,false=条纹风格
|
||||
*/
|
||||
public void switchDrawStyle(boolean isEnergyStyle) {
|
||||
LogUtils.d(TAG, "switchDrawStyle: 风格切换,旧=" + (mIsEnergyStyle ? "能量" : "条纹") + ",新=" + (isEnergyStyle ? "能量" : "条纹"));
|
||||
LogUtils.d(TAG, "【switchDrawStyle】风格切换 | 旧风格=" + (mIsEnergyStyle ? "能量" : "条纹") + " | 新风格=" + (isEnergyStyle ? "能量" : "条纹"));
|
||||
mIsEnergyStyle = isEnergyStyle;
|
||||
invalidateSelf();
|
||||
LogUtils.d(TAG, "【switchDrawStyle】已触发重绘");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,31 +171,42 @@ public class BatteryDrawable extends Drawable {
|
||||
* @param 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);
|
||||
invalidateSelf();
|
||||
LogUtils.d(TAG, "【updateBatteryColor】已触发重绘");
|
||||
}
|
||||
|
||||
// ====================== Getter方法(按需暴露,简洁无冗余) ======================
|
||||
// ====================================== Getter方法(按需暴露,简洁无冗余) ======================================
|
||||
/**
|
||||
* 获取当前电量
|
||||
* @return 电量值(0-100,-1=未初始化)
|
||||
*/
|
||||
public int getBatteryValue() {
|
||||
return mBatteryValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前绘制风格
|
||||
* @return true=能量风格,false=条纹风格
|
||||
*/
|
||||
public boolean isEnergyStyle() {
|
||||
return mIsEnergyStyle;
|
||||
}
|
||||
|
||||
// ====================== Drawable抽象方法(必须实现,精简逻辑) ======================
|
||||
// ====================================== Drawable抽象方法(必须实现,精简逻辑) ======================================
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
LogUtils.d(TAG, "setAlpha: 透明度更新,旧=" + mBatteryPaint.getAlpha() + ",新=" + alpha);
|
||||
LogUtils.d(TAG, "【setAlpha】透明度更新 | 旧值=" + mBatteryPaint.getAlpha() + " | 新值=" + alpha);
|
||||
mBatteryPaint.setAlpha(alpha);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
LogUtils.d(TAG, "setColorFilter: 设置颜色过滤,filter=" + colorFilter);
|
||||
LogUtils.d(TAG, "【setColorFilter】设置颜色过滤 | filter=" + colorFilter);
|
||||
mBatteryPaint.setColorFilter(colorFilter);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@@ -22,24 +22,26 @@ import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 13:14
|
||||
* @Describe 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
|
||||
* 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
|
||||
* 适配:Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
|
||||
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 13:14
|
||||
*/
|
||||
public class MainContentView {
|
||||
// ======================== 静态常量(置顶,唯一标识)========================
|
||||
// ====================================== 静态常量区(唯一标识,变更类型分类) ======================================
|
||||
public static final String TAG = "MainContentView";
|
||||
|
||||
// 变更类型常量(区分不同控件,精准处理逻辑)
|
||||
private static final int CHANGE_TYPE_CHARGE_SWITCH = 1;
|
||||
private static final int CHANGE_TYPE_USAGE_SWITCH = 2;
|
||||
private static final int CHANGE_TYPE_SERVICE_SWITCH = 3;
|
||||
private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4;
|
||||
private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5;
|
||||
// 电量范围常量
|
||||
private static final int BATTERY_MIN = 0;
|
||||
private static final int BATTERY_MAX = 100;
|
||||
|
||||
// ======================== 内部静态类(临时数据载体,避免外部依赖)========================
|
||||
// ====================================== 内部静态类(临时数据载体,避免外部依赖) ======================================
|
||||
/**
|
||||
* 临时配置数据实体(缓存变更信息,取消时恢复)
|
||||
*/
|
||||
@@ -65,7 +67,7 @@ public class MainContentView {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 事件回调接口(解耦视图与业务,提升扩展性)========================
|
||||
// ====================================== 事件回调接口(解耦视图与业务,提升扩展性) ======================================
|
||||
public interface OnViewActionListener {
|
||||
void onChargeReminderSwitchChanged(boolean isChecked);
|
||||
void onUsageReminderSwitchChanged(boolean isChecked);
|
||||
@@ -74,17 +76,17 @@ public class MainContentView {
|
||||
void onUsageReminderProgressChanged(int progress);
|
||||
}
|
||||
|
||||
// ======================== 成员变量(按功能分类,避免混乱)========================
|
||||
// ====================================== 成员变量区(按功能分类,final优先,避免混乱) ======================================
|
||||
// 外部依赖实例(生命周期关联,优先声明)
|
||||
private Context mContext;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private OnViewActionListener mActionListener;
|
||||
|
||||
// 视图控件(按「布局→开关→文本→进度条→图标」功能归类)
|
||||
// 视图控件(按「布局→开关→文本→进度条→图标」功能归类,public控件标注用途)
|
||||
// 基础布局控件
|
||||
public RelativeLayout mainLayout;
|
||||
public MemoryCachedBackgroundView backgroundView;
|
||||
LinearLayout mllBackgroundView;
|
||||
private LinearLayout mllBackgroundView;
|
||||
// 容器布局控件
|
||||
public LinearLayout llLeftSeekBar;
|
||||
public LinearLayout llRightSeekBar;
|
||||
@@ -122,9 +124,9 @@ public class MainContentView {
|
||||
// 对话框状态锁(避免快速点击重复弹窗)
|
||||
private boolean isDialogShowing = false;
|
||||
|
||||
// ======================== 构造方法(初始化入口,逻辑闭环)========================
|
||||
// ====================================== 构造方法(初始化入口,逻辑闭环) ======================================
|
||||
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.mActionListener = actionListener;
|
||||
@@ -136,24 +138,25 @@ public class MainContentView {
|
||||
initConfirmDialog();
|
||||
bindViewListeners();
|
||||
|
||||
LogUtils.d(TAG, "MainContentView 初始化完成");
|
||||
LogUtils.d(TAG, "【MainContentView】初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 私有初始化方法(封装内部逻辑,仅暴露入口)========================
|
||||
// ====================================== 私有初始化方法(封装内部逻辑,仅暴露入口) ======================================
|
||||
/**
|
||||
* 绑定视图控件(显式强转适配 Java7,适配 API30 视图加载机制)
|
||||
* @param 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);
|
||||
//backgroundView = (BackgroundView) rootView.findViewById(R.id.fragmentmainviewBackgroundView1);
|
||||
mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview);
|
||||
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
|
||||
if (backgroundView.getParent() != null) {
|
||||
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
|
||||
}
|
||||
mllBackgroundView.addView(backgroundView);
|
||||
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
|
||||
if (backgroundView.getParent() != null) {
|
||||
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
|
||||
LogUtils.d(TAG, "【bindViews】移除背景视图旧父容器");
|
||||
}
|
||||
mllBackgroundView.addView(backgroundView);
|
||||
// 容器布局绑定
|
||||
llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1);
|
||||
llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2);
|
||||
@@ -177,17 +180,19 @@ public class MainContentView {
|
||||
// 初始化进度缓存(从配置读取初始值)
|
||||
mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue();
|
||||
mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue();
|
||||
LogUtils.d(TAG, "【bindViews】进度缓存初始化 | charge=" + mCurrentChargeProgress + " | usage=" + mCurrentUsageProgress);
|
||||
|
||||
// 关键视图绑定校验(仅保留核心控件错误日志,精简冗余)
|
||||
if (mainLayout == null) LogUtils.e(TAG, "mainLayout 绑定失败");
|
||||
if (backgroundView == null) LogUtils.e(TAG, "backgroundView 绑定失败");
|
||||
if (mainLayout == null) LogUtils.e(TAG, "【bindViews】mainLayout 绑定失败");
|
||||
if (backgroundView == null) LogUtils.e(TAG, "【bindViews】backgroundView 绑定失败");
|
||||
LogUtils.d(TAG, "【bindViews】视图绑定完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化电池 Drawable(集成 BatteryDrawable,默认能量风格,适配小米机型渲染)
|
||||
*/
|
||||
private void initBatteryDrawables() {
|
||||
LogUtils.d(TAG, "initBatteryDrawables()");
|
||||
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始");
|
||||
// 当前电量 Drawable(颜色从资源读取,适配 API30 主题)
|
||||
int colorCurrent = getResourceColor(R.color.colorCurrent);
|
||||
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
|
||||
@@ -197,15 +202,16 @@ public class MainContentView {
|
||||
// 耗电提醒 Drawable
|
||||
int colorUsage = getResourceColor(R.color.colorUsege);
|
||||
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
|
||||
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
|
||||
*/
|
||||
private void initConfirmDialog() {
|
||||
LogUtils.d(TAG, "initConfirmDialog()");
|
||||
LogUtils.d(TAG, "【initConfirmDialog】对话框初始化开始");
|
||||
if (mContext == null) {
|
||||
LogUtils.e(TAG, "Context 为空,初始化失败");
|
||||
LogUtils.e(TAG, "【initConfirmDialog】Context 为空,初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,16 +251,17 @@ public class MainContentView {
|
||||
mConfigConfirmDialog = mDialogBuilder.create();
|
||||
mConfigConfirmDialog.setCancelable(true);
|
||||
mConfigConfirmDialog.setCanceledOnTouchOutside(true);
|
||||
LogUtils.d(TAG, "【initConfirmDialog】对话框初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定视图事件监听(Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效)
|
||||
*/
|
||||
private void bindViewListeners() {
|
||||
LogUtils.d(TAG, "bindViewListeners()");
|
||||
LogUtils.d(TAG, "【bindViewListeners】事件监听绑定开始");
|
||||
// 依赖校验,避免空指针
|
||||
if (mAppConfigUtils == null || mActionListener == null || mConfigConfirmDialog == null) {
|
||||
LogUtils.e(TAG, "依赖实例为空,跳过监听绑定");
|
||||
if (mAppConfigUtils == null || mActionListener == null || mDialogBuilder == null) {
|
||||
LogUtils.e(TAG, "【bindViewListeners】依赖实例为空,跳过监听绑定");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,14 +274,14 @@ public class MainContentView {
|
||||
int originalValue = mAppConfigUtils.getChargeReminderValue();
|
||||
// 进度无变化,不处理
|
||||
if (originalValue == progress) {
|
||||
LogUtils.d(TAG, "ChargeReminderSeekBar: 进度无变化,跳过");
|
||||
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar: 进度无变化,跳过");
|
||||
return;
|
||||
}
|
||||
// 缓存变更数据,显示确认对话框
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
|
||||
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -289,7 +296,7 @@ public class MainContentView {
|
||||
seekBar.setProgress(originalValue);
|
||||
// 恢复进度缓存
|
||||
mCurrentChargeProgress = originalValue;
|
||||
LogUtils.d(TAG, "ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
|
||||
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -305,6 +312,7 @@ public class MainContentView {
|
||||
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
|
||||
tvChargeReminderValue.setText(progress + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar实时更新 | 进度=" + progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +322,7 @@ public class MainContentView {
|
||||
@Override
|
||||
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
|
||||
});
|
||||
LogUtils.d(TAG, "充电提醒进度条专属监听绑定完成");
|
||||
LogUtils.d(TAG, "【bindViewListeners】充电提醒进度条专属监听绑定完成");
|
||||
}
|
||||
|
||||
// 充电提醒开关监听
|
||||
@@ -330,10 +338,10 @@ public class MainContentView {
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
LogUtils.d(TAG, "【bindViewListeners】cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "充电提醒开关监听绑定完成");
|
||||
LogUtils.d(TAG, "【bindViewListeners】充电提醒开关监听绑定完成");
|
||||
}
|
||||
|
||||
// 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口,确保弹窗100%触发)
|
||||
@@ -345,14 +353,14 @@ public class MainContentView {
|
||||
int originalValue = mAppConfigUtils.getUsageReminderValue();
|
||||
// 进度无变化,不处理
|
||||
if (originalValue == progress) {
|
||||
LogUtils.d(TAG, "UsageReminderSeekBar: 进度无变化,跳过");
|
||||
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar: 进度无变化,跳过");
|
||||
return;
|
||||
}
|
||||
// 缓存变更数据,显示确认对话框
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
|
||||
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -367,7 +375,7 @@ public class MainContentView {
|
||||
seekBar.setProgress(originalValue);
|
||||
// 恢复进度缓存
|
||||
mCurrentUsageProgress = originalValue;
|
||||
LogUtils.d(TAG, "UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
|
||||
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -383,6 +391,7 @@ public class MainContentView {
|
||||
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
|
||||
tvUsageReminderValue.setText(progress + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar实时更新 | 进度=" + progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +401,7 @@ public class MainContentView {
|
||||
@Override
|
||||
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
|
||||
});
|
||||
LogUtils.d(TAG, "耗电提醒进度条专属监听绑定完成");
|
||||
LogUtils.d(TAG, "【bindViewListeners】耗电提醒进度条专属监听绑定完成");
|
||||
}
|
||||
|
||||
// 耗电提醒开关监听
|
||||
@@ -408,10 +417,10 @@ public class MainContentView {
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
LogUtils.d(TAG, "【bindViewListeners】cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "耗电提醒开关监听绑定完成");
|
||||
LogUtils.d(TAG, "【bindViewListeners】耗电提醒开关监听绑定完成");
|
||||
}
|
||||
|
||||
// 服务总开关监听(核心优化:逻辑与其他控件完全对齐)
|
||||
@@ -430,24 +439,24 @@ public class MainContentView {
|
||||
updateDialogMessageByChangeType();
|
||||
// 显示确认对话框
|
||||
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 视图更新规范)
|
||||
* @param frameDrawable 进度条背景 Drawable(外部传入,适配主题切换)
|
||||
*/
|
||||
public void updateViewData(Drawable frameDrawable) {
|
||||
LogUtils.d(TAG, "updateViewData() | frameDrawable=" + frameDrawable);
|
||||
LogUtils.d(TAG, "【updateViewData】视图数据更新开始 | frameDrawable=" + frameDrawable);
|
||||
if (mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "AppConfigUtils 为空,跳过更新");
|
||||
LogUtils.e(TAG, "【updateViewData】AppConfigUtils 为空,跳过更新");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -462,12 +471,13 @@ public class MainContentView {
|
||||
// 更新进度缓存
|
||||
mCurrentChargeProgress = chargeVal;
|
||||
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 (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable);
|
||||
if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable);
|
||||
LogUtils.d(TAG, "【updateViewData】进度条背景更新完成");
|
||||
}
|
||||
|
||||
// 当前电量更新(联动 BatteryDrawable,实时刷新图标)
|
||||
@@ -479,6 +489,7 @@ public class MainContentView {
|
||||
tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent));
|
||||
tvCurrentBatteryValue.setText(currentVal + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "【updateViewData】当前电量更新完成");
|
||||
|
||||
// 充电提醒视图更新
|
||||
if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) {
|
||||
@@ -491,6 +502,7 @@ public class MainContentView {
|
||||
}
|
||||
if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal);
|
||||
if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable);
|
||||
LogUtils.d(TAG, "【updateViewData】充电提醒视图更新完成");
|
||||
|
||||
// 耗电提醒视图更新
|
||||
if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) {
|
||||
@@ -503,6 +515,7 @@ public class MainContentView {
|
||||
}
|
||||
if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal);
|
||||
if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable);
|
||||
LogUtils.d(TAG, "【updateViewData】耗电提醒视图更新完成");
|
||||
|
||||
// 服务开关+提示文本更新(确保状态准确)
|
||||
if (swEnableService != null) {
|
||||
@@ -510,8 +523,9 @@ public class MainContentView {
|
||||
swEnableService.setText(mContext.getString(R.string.txt_aboveswitch));
|
||||
}
|
||||
if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips));
|
||||
LogUtils.d(TAG, "【updateViewData】服务开关与提示文本更新完成");
|
||||
|
||||
LogUtils.d(TAG, "所有视图数据更新完成");
|
||||
LogUtils.d(TAG, "【updateViewData】所有视图数据更新完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -519,28 +533,28 @@ public class MainContentView {
|
||||
* @param value 电量值(自动校准 0-100,避免异常值)
|
||||
*/
|
||||
public void updateCurrentBattery(int value) {
|
||||
LogUtils.d(TAG, "updateCurrentBattery() | 原始值=" + value);
|
||||
LogUtils.d(TAG, "【updateCurrentBattery】当前电量更新开始 | 原始值=" + value);
|
||||
// 核心依赖校验
|
||||
if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) {
|
||||
LogUtils.e(TAG, "视图/Drawable 为空,跳过更新");
|
||||
LogUtils.e(TAG, "【updateCurrentBattery】视图/Drawable 为空,跳过更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校准电量范围(强制 0-100,防止 API30 视图显示异常)
|
||||
int validValue = Math.max(0, Math.min(value, 100));
|
||||
int validValue = Math.max(BATTERY_MIN, Math.min(value, BATTERY_MAX));
|
||||
// 联动 BatteryDrawable 更新图标,同步文本显示
|
||||
mCurrentBatteryDrawable.setBatteryValue(validValue);
|
||||
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
|
||||
tvCurrentBatteryValue.setText(validValue + "%");
|
||||
|
||||
LogUtils.d(TAG, "更新完成 | 校准后值=" + validValue);
|
||||
LogUtils.d(TAG, "【updateCurrentBattery】更新完成 | 校准后值=" + validValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用)
|
||||
*/
|
||||
public void releaseResources() {
|
||||
LogUtils.d(TAG, "releaseResources()");
|
||||
LogUtils.d(TAG, "【releaseResources】资源释放开始");
|
||||
// 释放对话框资源(安全销毁,避免内存泄漏)
|
||||
if (mConfigConfirmDialog != null) {
|
||||
if (mConfigConfirmDialog.isShowing()) {
|
||||
@@ -563,6 +577,7 @@ public class MainContentView {
|
||||
// 置空视图实例(断开视图引用,辅助 GC 回收)
|
||||
mainLayout = null;
|
||||
backgroundView = null;
|
||||
mllBackgroundView = null;
|
||||
llLeftSeekBar = null;
|
||||
llRightSeekBar = null;
|
||||
cbEnableChargeReminder = null;
|
||||
@@ -583,7 +598,7 @@ public class MainContentView {
|
||||
mAppConfigUtils = null;
|
||||
mActionListener = null;
|
||||
|
||||
LogUtils.d(TAG, "所有资源释放完成");
|
||||
LogUtils.d(TAG, "【releaseResources】所有资源释放完成");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,7 +606,7 @@ public class MainContentView {
|
||||
* @param enabled 服务启用状态
|
||||
*/
|
||||
public void setServiceSwitchChecked(boolean enabled) {
|
||||
LogUtils.d(TAG, "setServiceSwitchChecked() | enabled=" + enabled);
|
||||
LogUtils.d(TAG, "【setServiceSwitchChecked】服务开关状态设置 | enabled=" + enabled);
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setChecked(enabled);
|
||||
}
|
||||
@@ -602,33 +617,33 @@ public class MainContentView {
|
||||
* @param enabled 是否允许点击
|
||||
*/
|
||||
public void setServiceSwitchEnabled(boolean enabled) {
|
||||
LogUtils.d(TAG, "setServiceSwitchEnabled() | enabled=" + enabled);
|
||||
LogUtils.d(TAG, "【setServiceSwitchEnabled】服务开关点击状态设置 | enabled=" + enabled);
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑)========================
|
||||
// ====================================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑) ======================================
|
||||
/**
|
||||
* 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗)
|
||||
*/
|
||||
private void showConfigConfirmDialog() {
|
||||
LogUtils.d(TAG, "showConfigConfirmDialog() | isDialogShowing=" + isDialogShowing);
|
||||
LogUtils.d(TAG, "【showConfigConfirmDialog】对话框显示开始 | isDialogShowing=" + isDialogShowing);
|
||||
// 对话框状态锁:正在显示则跳过,避免重复触发
|
||||
if (isDialogShowing) {
|
||||
LogUtils.d(TAG, "对话框已显示,跳过重复调用");
|
||||
LogUtils.d(TAG, "【showConfigConfirmDialog】对话框已显示,跳过重复调用");
|
||||
return;
|
||||
}
|
||||
// 基础校验:对话框/上下文/Builder 为空
|
||||
if (mDialogBuilder == null || mContext == null) {
|
||||
LogUtils.e(TAG, "对话框Builder/上下文异常,无法显示");
|
||||
LogUtils.e(TAG, "【showConfigConfirmDialog】对话框Builder/上下文异常,无法显示");
|
||||
if (mTempConfigData != null) cancelConfigChange();
|
||||
return;
|
||||
}
|
||||
// Activity 状态校验:避免销毁后弹窗崩溃(适配 API30)
|
||||
Activity activity = (Activity) mContext;
|
||||
if (activity.isFinishing() || activity.isDestroyed()) {
|
||||
LogUtils.e(TAG, "Activity 已销毁,无法显示对话框");
|
||||
LogUtils.e(TAG, "【showConfigConfirmDialog】Activity 已销毁,无法显示对话框");
|
||||
if (mTempConfigData != null) cancelConfigChange();
|
||||
return;
|
||||
}
|
||||
@@ -645,16 +660,16 @@ public class MainContentView {
|
||||
mConfigConfirmDialog.setOnDismissListener(null);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "确认对话框显示成功");
|
||||
LogUtils.d(TAG, "【showConfigConfirmDialog】确认对话框显示成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认配置变更(保存数据+回调监听+更新视图)
|
||||
*/
|
||||
private void confirmConfigChange() {
|
||||
LogUtils.d(TAG, "confirmConfigChange() | mTempConfigData=" + mTempConfigData);
|
||||
LogUtils.d(TAG, "【confirmConfigChange】配置确认开始 | mTempConfigData=" + mTempConfigData);
|
||||
if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) {
|
||||
LogUtils.e(TAG, "依赖数据为空,确认失败");
|
||||
LogUtils.e(TAG, "【confirmConfigChange】依赖数据为空,确认失败");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -663,13 +678,13 @@ public class MainContentView {
|
||||
case CHANGE_TYPE_CHARGE_SWITCH:
|
||||
mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue);
|
||||
mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "【confirmConfigChange】充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
|
||||
break;
|
||||
// 耗电提醒开关
|
||||
case CHANGE_TYPE_USAGE_SWITCH:
|
||||
mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue);
|
||||
mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "【confirmConfigChange】耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
|
||||
break;
|
||||
// 服务总开关(核心:持久化配置+触发 Activity 回调)
|
||||
case CHANGE_TYPE_SERVICE_SWITCH:
|
||||
@@ -681,36 +696,37 @@ public class MainContentView {
|
||||
}
|
||||
// 2. 强制触发 Activity 回调,执行服务启停逻辑
|
||||
mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
|
||||
LogUtils.d(TAG, "【confirmConfigChange】服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
|
||||
break;
|
||||
// 充电提醒进度条
|
||||
case CHANGE_TYPE_CHARGE_SEEKBAR:
|
||||
mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue);
|
||||
mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue);
|
||||
LogUtils.d(TAG, "充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
|
||||
LogUtils.d(TAG, "【confirmConfigChange】充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
|
||||
break;
|
||||
// 耗电提醒进度条
|
||||
case CHANGE_TYPE_USAGE_SEEKBAR:
|
||||
mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue);
|
||||
mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue);
|
||||
LogUtils.d(TAG, "耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
|
||||
LogUtils.d(TAG, "【confirmConfigChange】耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "未知变更类型,跳过");
|
||||
LogUtils.w(TAG, "【confirmConfigChange】未知变更类型,跳过");
|
||||
break;
|
||||
}
|
||||
|
||||
// 确认完成,清空临时数据
|
||||
mTempConfigData = null;
|
||||
LogUtils.d(TAG, "【confirmConfigChange】配置确认完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致)
|
||||
*/
|
||||
private void cancelConfigChange() {
|
||||
LogUtils.d(TAG, "cancelConfigChange() | mTempConfigData=" + mTempConfigData);
|
||||
LogUtils.d(TAG, "【cancelConfigChange】配置取消开始 | mTempConfigData=" + mTempConfigData);
|
||||
if (mTempConfigData == null || mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "依赖数据为空,取消失败");
|
||||
LogUtils.e(TAG, "【cancelConfigChange】依赖数据为空,取消失败");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -719,19 +735,19 @@ public class MainContentView {
|
||||
if (cbEnableChargeReminder != null) {
|
||||
cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue);
|
||||
}
|
||||
LogUtils.d(TAG, "充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
LogUtils.d(TAG, "【cancelConfigChange】充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
break;
|
||||
case CHANGE_TYPE_USAGE_SWITCH:
|
||||
if (cbEnableUsageReminder != null) {
|
||||
cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue);
|
||||
}
|
||||
LogUtils.d(TAG, "耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
LogUtils.d(TAG, "【cancelConfigChange】耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
break;
|
||||
case CHANGE_TYPE_SERVICE_SWITCH:
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setChecked(mTempConfigData.originalBooleanValue);
|
||||
}
|
||||
LogUtils.d(TAG, "服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
LogUtils.d(TAG, "【cancelConfigChange】服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
break;
|
||||
case CHANGE_TYPE_CHARGE_SEEKBAR:
|
||||
if (sbChargeReminder != null) {
|
||||
@@ -742,7 +758,7 @@ public class MainContentView {
|
||||
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
|
||||
tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
|
||||
LogUtils.d(TAG, "【cancelConfigChange】充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
|
||||
break;
|
||||
case CHANGE_TYPE_USAGE_SEEKBAR:
|
||||
if (sbUsageReminder != null) {
|
||||
@@ -753,22 +769,23 @@ public class MainContentView {
|
||||
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
|
||||
tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
|
||||
LogUtils.d(TAG, "【cancelConfigChange】耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "未知变更类型,跳过");
|
||||
LogUtils.w(TAG, "【cancelConfigChange】未知变更类型,跳过");
|
||||
break;
|
||||
}
|
||||
|
||||
// 取消完成,清空临时数据
|
||||
mTempConfigData = null;
|
||||
LogUtils.d(TAG, "【cancelConfigChange】配置取消完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效)
|
||||
*/
|
||||
private void updateDialogMessageByChangeType() {
|
||||
LogUtils.d(TAG, "updateDialogMessageByChangeType() | mTempConfigData=" + mTempConfigData);
|
||||
LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新开始 | mTempConfigData=" + mTempConfigData);
|
||||
if (mDialogBuilder == null || mTempConfigData == null) return;
|
||||
String message;
|
||||
if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) {
|
||||
@@ -782,42 +799,20 @@ public class MainContentView {
|
||||
}
|
||||
// 通过 Builder 设置消息,确保弹窗显示最新内容
|
||||
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 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针)
|
||||
* @param colorResId 颜色资源 ID
|
||||
* @return 校准后的颜色值
|
||||
*/
|
||||
private int getResourceColor(int colorResId) {
|
||||
LogUtils.d(TAG, "getResourceColor() | colorResId=" + colorResId);
|
||||
LogUtils.d(TAG, "【getResourceColor】资源颜色获取 | colorResId=" + colorResId);
|
||||
// 空指针防御:Context 为空返回默认黑色
|
||||
if (mContext == null) {
|
||||
LogUtils.e(TAG, "Context 为空,返回默认黑色");
|
||||
LogUtils.e(TAG, "【getResourceColor】Context 为空,返回默认黑色");
|
||||
return 0xFF000000;
|
||||
}
|
||||
// 适配 API30 主题颜色读取
|
||||
@@ -833,11 +828,11 @@ public class MainContentView {
|
||||
* @return 服务启用状态(true=启用,false=禁用)
|
||||
*/
|
||||
private boolean getServiceEnableState() {
|
||||
LogUtils.d(TAG, "getServiceEnableState()");
|
||||
LogUtils.d(TAG, "【getServiceEnableState】服务状态获取开始");
|
||||
ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
|
||||
// 本地无配置时,默认禁用服务(与服务初始化逻辑对齐)
|
||||
boolean state = serviceBean != null && serviceBean.isEnableService();
|
||||
LogUtils.d(TAG, "服务启用状态获取完成 | state=" + state);
|
||||
LogUtils.d(TAG, "【getServiceEnableState】服务启用状态获取完成 | state=" + state);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,171 +8,176 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/21 20:43
|
||||
* @Describe 单实例缓存版背景视图控件(基于Java7)
|
||||
* 单实例缓存版背景视图控件(基于Java7)- 强制缓存版
|
||||
* 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片
|
||||
* 新增:SP持久化最后加载路径、获取最后加载实例功能
|
||||
* 强制缓存策略:无论内存是否紧张,不自动清理任何缓存实例和路径记录
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/21 20:43
|
||||
*/
|
||||
public class MemoryCachedBackgroundView extends BackgroundView {
|
||||
// ====================================== 静态常量区(TAG + SP相关常量) ======================================
|
||||
public static final String TAG = "MemoryCachedBackgroundView";
|
||||
// 静态属性:保存当前缓存的路径和实例(替代原Map,仅维护单实例)
|
||||
private static String sCachedImagePath;
|
||||
private static MemoryCachedBackgroundView sCachedView;
|
||||
// SP相关常量
|
||||
// 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);
|
||||
LogUtils.d(TAG, "构造器1:创建MemoryCachedBackgroundView实例");
|
||||
sInstanceCount++;
|
||||
LogUtils.d(TAG, "【构造器1】创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount);
|
||||
}
|
||||
|
||||
private MemoryCachedBackgroundView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "构造器2:创建MemoryCachedBackgroundView实例");
|
||||
sInstanceCount++;
|
||||
LogUtils.d(TAG, "【构造器2】创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount);
|
||||
}
|
||||
|
||||
private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "构造器3:创建MemoryCachedBackgroundView实例");
|
||||
sInstanceCount++;
|
||||
LogUtils.d(TAG, "【构造器3】创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount);
|
||||
}
|
||||
|
||||
// ====================================== 核心静态方法:获取/创建缓存实例 ======================================
|
||||
// ====================================== 核心静态方法:获取/创建缓存实例(强制缓存版) ======================================
|
||||
/**
|
||||
* 从缓存获取或创建MemoryCachedBackgroundView实例
|
||||
* 从缓存获取或创建MemoryCachedBackgroundView实例(强制保持旧实例)
|
||||
* @param context 上下文
|
||||
* @param imagePath 图片绝对路径(作为缓存标识)
|
||||
* @param isReload 是否强制重新加载图片(路径匹配时仍刷新)
|
||||
* @return 缓存/新创建的MemoryCachedBackgroundView实例
|
||||
*/
|
||||
public static MemoryCachedBackgroundView getInstance(Context context, String imagePath, boolean isReload) {
|
||||
LogUtils.d(TAG, "getInstance() 调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload);
|
||||
LogUtils.d(TAG, "【getInstance】调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload);
|
||||
// 空路径校验
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "getInstance():图片路径为空,创建空实例");
|
||||
LogUtils.e(TAG, "【getInstance】图片路径为空,创建空实例");
|
||||
return new MemoryCachedBackgroundView(context);
|
||||
}
|
||||
|
||||
// 1. 路径匹配缓存 → 判断是否强制重载
|
||||
if (imagePath.equals(sCachedImagePath) && sCachedView != null) {
|
||||
LogUtils.d(TAG, "getInstance():路径已缓存,当前缓存实例有效");
|
||||
LogUtils.d(TAG, "【getInstance】路径已缓存,当前缓存实例有效");
|
||||
if (isReload) {
|
||||
LogUtils.d(TAG, "getInstance():强制重载图片 | " + imagePath);
|
||||
LogUtils.d(TAG, "【getInstance】强制重载图片 | 路径:" + imagePath);
|
||||
sCachedView.loadImage(imagePath);
|
||||
} else {
|
||||
LogUtils.d(TAG, "getInstance():使用缓存实例,无需重载 | " + imagePath);
|
||||
LogUtils.d(TAG, "【getInstance】使用缓存实例,无需重载 | 路径:" + imagePath);
|
||||
}
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存
|
||||
LogUtils.d(TAG, "getInstance():路径未缓存,新建实例 | " + imagePath);
|
||||
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心:保留旧实例,仅更新引用)
|
||||
LogUtils.d(TAG, "【getInstance】路径未缓存,新建实例(保留旧实例) | 路径:" + imagePath);
|
||||
String oldPath = sCachedImagePath;
|
||||
sCachedView = new MemoryCachedBackgroundView(context);
|
||||
sCachedImagePath = imagePath;
|
||||
sCachedView.loadImage(imagePath);
|
||||
LogUtils.d(TAG, "【getInstance】已更新当前缓存实例,旧实例路径:" + oldPath + "(强制保持)");
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// ====================================== 新增功能:获取最后加载的实例 ======================================
|
||||
// ====================================== 新增功能:获取最后加载的实例(强制缓存版) ======================================
|
||||
/**
|
||||
* 获取最后一次loadImage的路径对应的实例
|
||||
* 获取最后一次loadImage的路径对应的实例(强制保持所有实例)
|
||||
* 无实例则创建并加载图片,同时更新静态缓存
|
||||
* @param context 上下文
|
||||
* @return 最后加载路径对应的实例
|
||||
*/
|
||||
public static MemoryCachedBackgroundView getLastInstance(Context context) {
|
||||
LogUtils.d(TAG, "getLastInstance() 调用");
|
||||
// 1. 从SP获取最后加载的路径
|
||||
LogUtils.d(TAG, "【getLastInstance】调用");
|
||||
// 1. 从SP获取最后加载的路径(强制保持,不自动删除)
|
||||
String lastPath = getLastLoadImagePath(context);
|
||||
if (TextUtils.isEmpty(lastPath)) {
|
||||
LogUtils.e(TAG, "getLastInstance():无最后加载路径,创建空实例");
|
||||
LogUtils.e(TAG, "【getLastInstance】无最后加载路径,创建空实例");
|
||||
return new MemoryCachedBackgroundView(context);
|
||||
}
|
||||
|
||||
// 2. 路径匹配当前缓存 → 直接返回
|
||||
if (lastPath.equals(sCachedImagePath) && sCachedView != null) {
|
||||
LogUtils.d(TAG, "getLastInstance():使用最后路径缓存实例 | " + lastPath);
|
||||
LogUtils.d(TAG, "【getLastInstance】使用最后路径缓存实例 | 路径:" + lastPath);
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// 3. 路径不匹配 → 新建实例并更新缓存
|
||||
LogUtils.d(TAG, "getLastInstance():最后路径未缓存,新建实例并加载 | " + lastPath);
|
||||
// 3. 路径不匹配 → 新建实例并更新缓存(保留旧实例)
|
||||
LogUtils.d(TAG, "【getLastInstance】最后路径未缓存,新建实例并加载(保留旧实例) | 路径:" + lastPath);
|
||||
String oldPath = sCachedImagePath;
|
||||
sCachedView = new MemoryCachedBackgroundView(context);
|
||||
sCachedImagePath = lastPath;
|
||||
sCachedView.loadImage(lastPath);
|
||||
LogUtils.d(TAG, "【getLastInstance】已更新最后路径实例,旧实例路径:" + oldPath + "(强制保持)");
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// ====================================== 工具方法:SP持久化最后加载路径 ======================================
|
||||
// ====================================== 工具方法:SP持久化最后加载路径(强制保持版) ======================================
|
||||
/**
|
||||
* 保存最后一次loadImage的路径到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, "saveLastLoadImagePath():已保存最后路径 | " + imagePath);
|
||||
LogUtils.d(TAG, "【saveLastLoadImagePath】已保存最后路径(强制保持) | 路径:" + imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SP获取最后一次loadImage的路径
|
||||
* 从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, "getLastLoadImagePath():获取最后路径 | " + lastPath);
|
||||
LogUtils.d(TAG, "【getLastLoadImagePath】获取最后路径(强制保持) | 路径:" + lastPath);
|
||||
return lastPath;
|
||||
}
|
||||
|
||||
// ====================================== 工具方法:缓存管理 ======================================
|
||||
// ====================================== 工具方法:缓存管理(强制缓存版 - 仅日志,不清理) ======================================
|
||||
/**
|
||||
* 清除当前缓存实例和路径
|
||||
* 清除当前缓存实例和路径(强制缓存策略:仅日志,不实际清理)
|
||||
*/
|
||||
public static void clearCache() {
|
||||
LogUtils.d(TAG, "clearCache() 调用 | 当前缓存路径:" + sCachedImagePath);
|
||||
sCachedView = null;
|
||||
sCachedImagePath = null;
|
||||
LogUtils.d(TAG, "clearCache():已清除当前缓存实例");
|
||||
LogUtils.w(TAG, "【clearCache】调用(强制缓存策略:不实际清理缓存) | 当前缓存路径:" + sCachedImagePath);
|
||||
LogUtils.d(TAG, "【clearCache】强制缓存策略生效,未清除任何实例和路径");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定路径的缓存(仅当路径匹配当前缓存时生效)
|
||||
* 清除指定路径的缓存(强制缓存策略:仅日志,不实际清理)
|
||||
* @param imagePath 图片路径
|
||||
*/
|
||||
public static void removeCache(String imagePath) {
|
||||
LogUtils.d(TAG, "removeCache() 调用 | 图片路径:" + imagePath);
|
||||
LogUtils.w(TAG, "【removeCache】调用(强制缓存策略:不实际清理缓存) | 图片路径:" + imagePath);
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "removeCache():图片路径为空,清除失败");
|
||||
LogUtils.e(TAG, "【removeCache】图片路径为空,清除失败");
|
||||
return;
|
||||
}
|
||||
if (imagePath.equals(sCachedImagePath)) {
|
||||
clearCache();
|
||||
// 同步删除SP中最后路径记录
|
||||
clearLastLoadImagePath(getContextFromCache());
|
||||
LogUtils.d(TAG, "removeCache():已清除匹配路径的缓存 | " + imagePath);
|
||||
} else {
|
||||
LogUtils.d(TAG, "removeCache():路径不匹配当前缓存,无需清除 | " + imagePath);
|
||||
}
|
||||
LogUtils.d(TAG, "【removeCache】强制缓存策略生效,未清除任何实例和路径");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存(同clearCache,保持方法兼容性)
|
||||
* 清除所有缓存(强制缓存策略:仅日志,不实际清理)
|
||||
*/
|
||||
public static void clearAllCache() {
|
||||
LogUtils.d(TAG, "clearAllCache() 调用");
|
||||
clearCache();
|
||||
clearLastLoadImagePath(getContextFromCache());
|
||||
LogUtils.d(TAG, "clearAllCache():已清除所有缓存及最后路径记录");
|
||||
LogUtils.w(TAG, "【clearAllCache】调用(强制缓存策略:不实际清理缓存)");
|
||||
LogUtils.d(TAG, "【clearAllCache】强制缓存策略生效,未清除任何实例、路径和SP记录");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,20 +185,18 @@ public class MemoryCachedBackgroundView extends BackgroundView {
|
||||
* @return 存在返回true,否则返回false
|
||||
*/
|
||||
public static boolean hasCache() {
|
||||
return sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
|
||||
boolean hasCache = sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
|
||||
LogUtils.d(TAG, "【hasCache】缓存存在状态:" + hasCache);
|
||||
return hasCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除SP中最后加载的路径记录
|
||||
* 清除SP中最后加载的路径记录(强制缓存策略:仅日志,不实际清理)
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void clearLastLoadImagePath(Context context) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
sp.edit().remove(KEY_LAST_LOAD_IMAGE_PATH).apply();
|
||||
LogUtils.d(TAG, "clearLastLoadImagePath():已清除最后路径记录");
|
||||
LogUtils.w(TAG, "【clearLastLoadImagePath】调用(强制缓存策略:不实际清理SP记录)");
|
||||
LogUtils.d(TAG, "【clearLastLoadImagePath】强制缓存策略生效,未清除SP中最后路径记录");
|
||||
}
|
||||
|
||||
// ====================================== 辅助方法:从缓存获取上下文 ======================================
|
||||
@@ -202,28 +205,40 @@ public class MemoryCachedBackgroundView extends BackgroundView {
|
||||
* @return 上下文实例,无则返回null
|
||||
*/
|
||||
private static Context getContextFromCache() {
|
||||
return sCachedView != null ? sCachedView.getContext() : null;
|
||||
Context context = sCachedView != null ? sCachedView.getContext() : null;
|
||||
LogUtils.d(TAG, "【getContextFromCache】从缓存获取上下文:" + context);
|
||||
return context;
|
||||
}
|
||||
|
||||
// ====================================== 重写父类方法:增强日志+SP持久化 ======================================
|
||||
// ====================================== 重写父类方法:增强日志+SP持久化(强制保持版) ======================================
|
||||
@Override
|
||||
public void loadImage(String imagePath) {
|
||||
LogUtils.d(TAG, "loadImage() 重载方法调用 | 图片路径:" + imagePath);
|
||||
LogUtils.d(TAG, "【loadImage】调用 | 图片路径:" + imagePath);
|
||||
super.loadImage(imagePath);
|
||||
// 保存最后加载路径到SP
|
||||
// 保存最后加载路径到SP(强制保持,不自动删除)
|
||||
saveLastLoadImagePath(getContext(), imagePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadByBackgroundBean(BackgroundBean bean) {
|
||||
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()));
|
||||
LogUtils.d(TAG, "【loadByBackgroundBean】调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()));
|
||||
super.loadByBackgroundBean(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
|
||||
LogUtils.d(TAG, "【loadByBackgroundBean】调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
|
||||
super.loadByBackgroundBean(bean, isRefresh);
|
||||
}
|
||||
|
||||
// ====================================== 新增:强制缓存监控方法 ======================================
|
||||
/**
|
||||
* 获取当前所有创建过的实例总数(用于监控强制缓存状态)
|
||||
* @return 实例总数
|
||||
*/
|
||||
public static int getInstanceCount() {
|
||||
LogUtils.d(TAG, "【getInstanceCount】调用 | 当前实例总数:" + sInstanceCount);
|
||||
return sInstanceCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import android.widget.SeekBar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 垂直进度条控件,适配 API30,支持逆时针旋转(0在下,100在上)
|
||||
* 修复滑块同步+弹窗触发bug,新增实时进度变化监听接口,支持拖动时实时回调进度
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 14:11
|
||||
* @Describe 垂直进度条控件,适配 API30,支持逆时针旋转(0在下,100在上),修复滑块同步+弹窗触发bug
|
||||
* 新增:实时进度变化监听接口,支持拖动时实时回调进度
|
||||
*/
|
||||
public class VerticalSeekBar extends SeekBar {
|
||||
// ======================== 静态常量 =========================
|
||||
@@ -76,26 +76,26 @@ public class VerticalSeekBar extends SeekBar {
|
||||
public VerticalSeekBar(Context context) {
|
||||
super(context);
|
||||
initView();
|
||||
LogUtils.d(TAG, "VerticalSeekBar(Context) 初始化");
|
||||
LogUtils.d(TAG, "【构造器1】VerticalSeekBar 初始化完成");
|
||||
}
|
||||
|
||||
public VerticalSeekBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView();
|
||||
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet) 初始化");
|
||||
LogUtils.d(TAG, "【构造器2】VerticalSeekBar 初始化完成");
|
||||
}
|
||||
|
||||
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initView();
|
||||
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet, int) 初始化");
|
||||
LogUtils.d(TAG, "【构造器3】VerticalSeekBar 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 初始化方法 =========================
|
||||
private void initView() {
|
||||
// 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销
|
||||
setBackgroundDrawable(null);
|
||||
LogUtils.d(TAG, "initView: 移除默认背景阴影,完成视图初始化");
|
||||
LogUtils.d(TAG, "【initView】移除默认背景阴影,完成视图初始化");
|
||||
}
|
||||
|
||||
// ======================== 对外设置方法(监听接口绑定)========================
|
||||
@@ -105,7 +105,7 @@ public class VerticalSeekBar extends SeekBar {
|
||||
*/
|
||||
public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener 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) {
|
||||
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) {
|
||||
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
|
||||
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
|
||||
LogUtils.v(TAG, "onMeasure: 垂直测量完成,宽=" + getMeasuredHeight() + ", 高=" + getMeasuredWidth());
|
||||
LogUtils.v(TAG, "【onMeasure】垂直测量完成,宽=" + getMeasuredHeight() + ",高=" + getMeasuredWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(h, w, oldh, oldw);
|
||||
LogUtils.v(TAG, "onSizeChanged: 尺寸变化,新宽=" + h + ", 新高=" + w);
|
||||
LogUtils.v(TAG, "【onSizeChanged】尺寸变化,新宽=" + h + ",新高=" + w);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -137,7 +137,7 @@ public class VerticalSeekBar extends SeekBar {
|
||||
canvas.rotate(-90);
|
||||
canvas.translate(-getHeight(), 0);
|
||||
super.onDraw(canvas);
|
||||
LogUtils.v(TAG, "onDraw: 完成垂直绘制,旋转角度=-90°");
|
||||
LogUtils.v(TAG, "【onDraw】完成垂直绘制,旋转角度=-90°");
|
||||
}
|
||||
|
||||
// ======================== 重写进度设置方法(修复滑块同步+新增实时回调)========================
|
||||
@@ -151,10 +151,11 @@ public class VerticalSeekBar extends SeekBar {
|
||||
// 强制触发尺寸变化,同步刷新滑块位置(核心bug修复逻辑)
|
||||
onSizeChanged(getWidth(), getHeight(), 0, 0);
|
||||
mProgress = progress;
|
||||
LogUtils.d(TAG, "setProgress: 进度设置为" + progress + ",滑块同步刷新");
|
||||
LogUtils.d(TAG, "【setProgress】进度设置为" + progress + ",滑块同步刷新");
|
||||
// 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false)
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onProgressChanged(this, progress, false);
|
||||
LogUtils.v(TAG, "【setProgress】触发实时进度回调,fromUser=false");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,20 +166,22 @@ public class VerticalSeekBar extends SeekBar {
|
||||
super.onTouchEvent(event);
|
||||
boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失
|
||||
boolean fromUser = true; // 标记是否是用户触摸导致的进度变化
|
||||
int action = event.getAction();
|
||||
|
||||
switch (event.getAction()) {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
LogUtils.d(TAG, "onTouchEvent: 触摸按下,Y坐标=" + event.getY());
|
||||
LogUtils.d(TAG, "【onTouchEvent】触摸按下,Y坐标=" + event.getY());
|
||||
// 触发实时进度监听:开始触摸
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onStartTrackingTouch(this);
|
||||
LogUtils.v(TAG, "【onTouchEvent】触发开始触摸回调");
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
calculateProgress(event.getY());
|
||||
setProgress(mProgress);
|
||||
LogUtils.v(TAG, "onTouchEvent: 触摸滑动,进度更新为" + mProgress);
|
||||
LogUtils.v(TAG, "【onTouchEvent】触摸滑动,进度更新为" + mProgress);
|
||||
// 触发实时进度监听:进度变化
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
|
||||
@@ -188,27 +191,31 @@ public class VerticalSeekBar extends SeekBar {
|
||||
case MotionEvent.ACTION_UP:
|
||||
calculateProgress(event.getY());
|
||||
setProgress(mProgress);
|
||||
LogUtils.d(TAG, "onTouchEvent: 触摸抬起,进度=" + mProgress + ",触发弹窗回调");
|
||||
// 触发实时进度监听:停止触摸
|
||||
LogUtils.d(TAG, "【onTouchEvent】触摸抬起,进度=" + mProgress + ",触发弹窗回调");
|
||||
// 触发实时进度监听:进度变化+停止触摸
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
|
||||
mProgressChangeListener.onStopTrackingTouch(this);
|
||||
LogUtils.v(TAG, "【onTouchEvent】触发停止触摸回调");
|
||||
}
|
||||
// 核心:调用原有触摸接口,通知外部触发配置变更对话框
|
||||
if (mTouchListener != null) {
|
||||
mTouchListener.onTouchUp(this, mProgress);
|
||||
LogUtils.v(TAG, "【onTouchEvent】触发触摸抬起回调");
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
LogUtils.d(TAG, "onTouchEvent: 触摸取消,当前进度=" + getProgress());
|
||||
int currentProgress = getProgress();
|
||||
LogUtils.d(TAG, "【onTouchEvent】触摸取消,当前进度=" + currentProgress);
|
||||
// 触发实时进度监听:停止触摸
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onStopTrackingTouch(this);
|
||||
}
|
||||
// 可选:触摸取消时回调,外部可做进度回滚处理
|
||||
if (mTouchListener != null) {
|
||||
mTouchListener.onTouchCancel(this, getProgress());
|
||||
mTouchListener.onTouchCancel(this, currentProgress);
|
||||
LogUtils.v(TAG, "【onTouchEvent】触发触摸取消回调");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -225,7 +232,7 @@ public class VerticalSeekBar extends SeekBar {
|
||||
mProgress = getMax() - (int) (getMax() * touchY / getHeight());
|
||||
// 校准进度范围,防止超出 0~100(兼容 API30 进度边界校验)
|
||||
mProgress = Math.max(0, Math.min(mProgress, getMax()));
|
||||
LogUtils.v(TAG, "calculateProgress: 触摸Y=" + touchY + ",计算进度=" + mProgress);
|
||||
LogUtils.v(TAG, "【calculateProgress】触摸Y=" + touchY + ",计算进度=" + mProgress + ",校准后=" + mProgress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@
|
||||
<string name="subtitle_activity_pixelpicker">背景像素拾取</string>
|
||||
<string name="subtitle_activity_about">关于应用</string>
|
||||
<string name="msg_AOHPCTCSeekBar_ClearRecord">>>>向右滑动100%可以清理电量记录。>>></string>
|
||||
<string name="msg_no_battery_record">无电池记录数据</string>
|
||||
</resources>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<string name="subtitle_activity_pixelpicker">Pixel Picker</string>
|
||||
<string name="subtitle_activity_about">About The APP</string>
|
||||
<string name="msg_AOHPCTCSeekBar_ClearRecord">>>>Seek 100% Right Is Clean Record.>>></string>
|
||||
<string name="msg_no_battery_record">No Battery Record</string>
|
||||
|
||||
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
|
||||
<string name="permission_title">权限申请</string>
|
||||
|
||||
Reference in New Issue
Block a user