Compare commits

...

84 Commits

Author SHA1 Message Date
493b7e433c <powerbell>APK 15.14.9 release Publish. 2025-12-16 21:18:25 +08:00
d2ddfedc96 调整当前电量初始化时的默认值。处理bean类序列化问题,未调试。 2025-12-16 21:16:34 +08:00
bba48a4458 <powerbell>APK 15.14.8 release Publish. 2025-12-16 20:17:03 +08:00
b7ae6ce190 编译参数修复 2025-12-16 20:15:58 +08:00
ba5470ebcb 更新像素清空按钮的重置颜色值。重置后为黑色,透明度OriginalAlpha值为FF。 2025-12-16 20:14:23 +08:00
78c7763212 优化颜色拾取流程 2025-12-16 20:06:12 +08:00
d051b1f737 <powerbell>APK 15.14.7 release Publish. 2025-12-16 17:52:54 +08:00
d6323bc1ed <powerbell>Start New Stage Version. 2025-12-16 17:50:40 +08:00
dffcc0f8a0 渐变像素对话框调试完成。 2025-12-16 17:48:59 +08:00
9426618b59 20251216_171108_907 2025-12-16 17:11:20 +08:00
68d98d4be3 调色板基本调试完成 2025-12-16 16:24:09 +08:00
4db458dda8 20251216_154557_434 2025-12-16 15:46:01 +08:00
83a8f5dada 初步完成调色板调用流程调试。 2025-12-16 14:12:41 +08:00
8e1d6ba197 添加调色板对话框,未调试。 2025-12-16 12:11:32 +08:00
70a004d9e3 <powerbell>APK 15.14.6 release Publish. 2025-12-14 19:58:14 +08:00
c7f8aea1ce <powerbell>Start New Stage Version. 2025-12-14 19:57:39 +08:00
6d4381d78a 修复固定剪裁时的宽高比例不准确的BUG。 2025-12-14 19:56:36 +08:00
ddcd9a450e 20251214_191909_625 2025-12-14 19:19:13 +08:00
ca2323f534 <powerbell>APK 15.14.5 release Publish. 2025-12-14 18:30:51 +08:00
851800e39a 背景图片第二次无法更换的Bug修复。 2025-12-14 18:29:44 +08:00
f17624048c <powerbell>APK 15.14.4 release Publish. 2025-12-14 18:12:02 +08:00
724fce895f 减去多余应用图标。 2025-12-14 18:10:40 +08:00
5ece532dd4 <powerbell>APK 15.14.3 release Publish. 2025-12-14 17:48:59 +08:00
8b20bc84c8 更新测试数据 2025-12-14 17:47:02 +08:00
634c71dfd4 剪裁图片透明度问题解决 2025-12-14 17:42:12 +08:00
947df2e9b4 20251214_165544_910 2025-12-14 16:55:59 +08:00
08a33365b3 <powerbell>APK 15.14.2 release Publish. 2025-12-14 04:55:45 +08:00
7cffe5c0a5 <powerbell>Start New Stage Version. 2025-12-14 04:54:24 +08:00
5a0c429131 背景像素切换流程测试完成 2025-12-14 04:53:17 +08:00
cff26b3d11 背景像素拾取功能测试完成 2025-12-14 04:46:17 +08:00
e59034e48d 添加权限申请提示框 2025-12-14 04:18:29 +08:00
3d3301064c 必要权限申请设置完成。 2025-12-14 04:08:10 +08:00
2d12397f5e 减少初始化应用相册目录时的多余目录。 2025-12-14 02:28:53 +08:00
f09bb17cbc 修改所有通知栏,点击后都跳转到主窗口。 2025-12-14 02:10:13 +08:00
28d8a5679f <powerbell>APK 15.14.1 release Publish. 2025-12-13 21:16:25 +08:00
b4d9bdf3b3 <powerbell>APK 15.14.0 release Publish. 2025-12-13 21:15:48 +08:00
111cf01f9a <powerbell>Start New Stage Version. 2025-12-13 21:14:54 +08:00
e51d46186a 数据模型有调整,设定次级版本号。同次级版本应用可以自由切换。 2025-12-13 21:11:03 +08:00
8fc6855066 通知类调试完成 2025-12-13 21:04:35 +08:00
4ceaf1e46a 通知类重构 2025-12-13 20:47:00 +08:00
e669bbb04b 命名空间重构 2025-12-13 20:23:48 +08:00
6bf3ebe2fd 20251212_002702_716 2025-12-12 00:27:07 +08:00
a44f7fe6d4 <powerbell>APK 15.12.15 release Publish. 2025-12-11 20:54:28 +08:00
35a34b5b53 <powerbell>Start New Stage Version. 2025-12-11 20:51:08 +08:00
d35d0d0291 图片选择到剪裁选定流程调试完成。 2025-12-11 20:49:58 +08:00
03212c0554 一次不必要的数据清空操作 2025-12-11 19:58:39 +08:00
0c963213df 调试信息准备 2025-12-11 19:40:30 +08:00
10ddca4f73 调试信息准备 2025-12-11 19:34:47 +08:00
f240d9c057 视图控件与全局位图缓存类优先调整。 2025-12-11 19:18:02 +08:00
2c77bf775b 背景图片控件测试通过,现在的问题就剩下,应用全局位图缓存问题与图片路径传递问题。 2025-12-11 18:48:42 +08:00
1db7c9bf80 20251211_130730_716 2025-12-11 13:07:35 +08:00
fd556fd06f 20251211_130438_907 2025-12-11 13:04:44 +08:00
220aa9dbfb 应用权限框架更新,重新调试剪裁文件流程。 2025-12-11 09:02:41 +08:00
ecafd2026f 相册权限申请模块改进中。。。 2025-12-11 06:53:22 +08:00
6ed9bc0d8e <powerbell>APK 15.12.14 release Publish. 2025-12-11 03:22:10 +08:00
bcb5db0a17 移除启动页cache缓存,以及移除savedInstanceState 窗体缓存。 2025-12-11 03:18:03 +08:00
6b69e04706 <powerbell>APK 15.12.13 release Publish. 2025-12-11 03:04:35 +08:00
a2a61bbf0b 添加BitmapCacheUtils应用全局位图缓存工具类。加速位图加载。 2025-12-11 03:02:46 +08:00
9f4211c83e <powerbell>APK 15.12.12 release Publish. 2025-12-10 20:37:42 +08:00
447a786632 图片尺寸调整函数优化 2025-12-10 20:31:10 +08:00
ff0f239ffc <powerbell>APK 15.12.11 release Publish. 2025-12-10 20:11:13 +08:00
7c59a982fc 使用豆包提供的savedInstanceState 窗体缓存技术,仅限于启动页窗体优化。 2025-12-10 20:09:49 +08:00
895cc4630d <powerbell>APK 15.12.10 release Publish. 2025-12-10 19:31:13 +08:00
851a539364 <powerbell>Start New Stage Version. 2025-12-10 19:30:41 +08:00
d79f2937ba 豆包优化,添加窗体缓存技术。代码优化首次窗体加载速度不多。代码理解难度增加。有可能会回退到上一个提交点。 2025-12-10 19:28:39 +08:00
c524a21429 <powerbell>APK 15.12.9 release Publish. 2025-12-10 18:38:00 +08:00
a148de2ab8 加快图片加载速度。 2025-12-10 18:36:49 +08:00
de34e823b6 <powerbell>APK 15.12.8 release Publish. 2025-12-10 18:32:41 +08:00
fd0833476e 简化冗余图片加载步骤。 2025-12-10 18:31:36 +08:00
361b533b0d <powerbell>APK 15.12.7 release Publish. 2025-12-10 18:12:34 +08:00
f277f76468 <powerbell>Start New Stage Version. 2025-12-10 18:12:02 +08:00
ec54865a8e 清理图片加载前的调试色调。优化加载速度。 2025-12-10 18:11:10 +08:00
6b3682994e <powerbell>APK 15.12.6 release Publish. 2025-12-10 17:52:12 +08:00
7145bff552 修复图片加载时出现的画面抖动问题。 2025-12-10 17:51:15 +08:00
4c3b60128f <powerbell>APK 15.12.5 release Publish. 2025-12-10 17:43:17 +08:00
29d30f3831 优化图片加载速度。 2025-12-10 17:42:09 +08:00
c7b0b1bc80 <powerbell>APK 15.12.4 release Publish. 2025-12-10 16:54:47 +08:00
9082b67bbd <powerbell>Start New Stage Version. 2025-12-10 16:53:55 +08:00
2b5447e65f 整合应用相框框架调整,加快启动页加载速度。 2025-12-10 16:52:47 +08:00
6a5c2dbbe8 <powerbell>APK 15.12.3 release Publish. 2025-12-10 16:20:34 +08:00
8cad25bb11 优化启动页加载速度。 2025-12-10 16:19:03 +08:00
e440943992 <powerbell>APK 15.12.2 release Publish. 2025-12-10 14:57:42 +08:00
7c5f8c3cc2 第一条图片选择与剪裁流程调试通过。 2025-12-10 14:52:43 +08:00
016b3b5e48 20251208_212749_544重新调整背景资源图片存储流程。 2025-12-08 21:28:49 +08:00
64 changed files with 5787 additions and 3723 deletions

View File

@@ -29,11 +29,11 @@ android {
applicationId "cc.winboll.studio.powerbell" applicationId "cc.winboll.studio.powerbell"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 6 versionCode 7
// versionName 更新后需要手动设置 // versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0 // .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.12" versionName "15.14"
if(true) { if(true) {
versionName = genVersionName("${versionName}") versionName = genVersionName("${versionName}")
} }

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sun Dec 07 15:19:14 HKT 2025 #Tue Dec 16 21:18:25 HKT 2025
stageCount=2 stageCount=10
libraryProject= libraryProject=
baseVersion=15.12 baseVersion=15.14
publishVersion=15.12.1 publishVersion=15.14.9
buildCount=0 buildCount=0
baseBetaVersion=15.12.2 baseBetaVersion=15.14.10

View File

@@ -4,55 +4,56 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell"> package="cc.winboll.studio.powerbell">
<!-- 只能在前台获取精确的位置信息 --> <!-- ====================== 原有权限保留 + 补充核心权限 ====================== -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!-- 运行前台服务(原有保留,补充 Android 12+ 特殊前台服务权限) -->
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- 读取您共享存储空间中的内容 --> <!-- 开机启动(原有保留,确保自启广播生效) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- MANAGE_EXTERNAL_STORAGE --> <!-- 显示通知(原有保留,前台服务/保活必备) -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 显示通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- PACKAGE_USAGE_STATS --> <!-- 应用使用统计相关(原有保留,忽略保护权限警告) -->
<uses-permission android:name="android.permission.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.BATTERY_STATS"/>
<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" /> <!-- 补充相机权限原有仅声明feature无权限 -->
<!-- 应用信息相关(原有保留) -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/> <uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/> tools:ignore="QueryAllPackagesPermission"/>
<uses-permission <!-- 新增:文件管理权限(对应 PermissionUtils 全文件管理逻辑) -->
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
tools:ignore="ProtectedPermissions"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- API30+ 全文件权限 -->
<!-- 新增:忽略电池优化权限(对应 PermissionUtils 电池优化逻辑,必须声明) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- 新增API30+ 跳转系统权限页兼容(避免自启/文件权限跳转失败) -->
<queries>
<!-- 全文件管理权限跳转兼容 -->
<!-- 小米自启权限跳转兼容(精准匹配安全中心包名) -->
<package android:name="com.miui.securitycenter" />
<!-- 电池优化权限跳转兼容 -->
</queries>
<application <application
android:name=".App" android:name=".App"
@@ -64,17 +65,20 @@
android:resizeableActivity="true" android:resizeableActivity="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning"> android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<!-- ====================== 页面配置(原有保留,优化 exported 安全) ====================== -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:exported="true" android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
</activity> </activity>
<activity android:name=".activities.CrashActivity"/> <activity
android:name=".activities.CrashActivity"
android:exported="false"/> <!-- 新增:非外部调用,设为 false提升安全 -->
<activity-alias <activity-alias
android:name=".MainActivityEN1" android:name=".MainActivityEN1"
@@ -83,19 +87,13 @@
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:enabled="true"> android:enabled="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/> android:resource="@xml/shortcutsmainen1"/>
</activity-alias> </activity-alias>
<activity-alias <activity-alias
@@ -105,19 +103,13 @@
android:label="@string/app_name_cn1" android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:enabled="false"> android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/> android:resource="@xml/shortcutsmaincn1"/>
</activity-alias> </activity-alias>
<activity-alias <activity-alias
@@ -127,116 +119,121 @@
android:label="@string/app_name_cn2" android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:enabled="false"> android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/> android:resource="@xml/shortcutsmaincn2"/>
</activity-alias> </activity-alias>
<activity <activity
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity" android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity" android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask"> android:launchMode="singleTask"
android:exported="false"/> <!-- 新增:非外部调用,设为 false -->
</activity>
<activity <activity
android:name="cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity" android:name=".activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity" android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/> <data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/> <data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/> <data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/> <data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/> <data android:mimeType="image/*"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- ====================== 广播接收器(优化自启广播,提升保活成功率) ====================== -->
<receiver <receiver
android:name=".receivers.MainReceiver" android:name=".receivers.MainReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="true"
android:directBootAware="true"> android:directBootAware="true">
<intent-filter android:priority="1000"> <!-- 新增:广播优先级最高,确保优先接收开机广播 -->
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <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> </intent-filter>
</receiver> </receiver>
<!-- ====================== 服务配置(核心优化:前台服务保活,适配 API29-30 ====================== -->
<!-- 核心前台服务ControlCenterService保活核心重点优化 -->
<service <service
android:name="cc.winboll.studio.powerbell.services.ControlCenterService" android:name=".services.ControlCenterService"
android:priority="1000" android:priority="1000"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:process=".controlcenterservice"/> android:process=".controlcenterservice"
android:foregroundServiceType="dataSync">
<!-- 新增Android 12+ 前台服务用途声明(系统强制,否则拦截服务启动) -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活" /> <!-- 按实际用途填写,不可空 -->
</service>
<!-- 辅助服务AssistantService按需优化增强稳定性 -->
<service <service
android:name="cc.winboll.studio.powerbell.services.AssistantService" android:name=".services.AssistantService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:process=".assistantservice"/> android:process=".assistantservice"> <!-- 若需前台启动,添加此配置;纯后台可移除 -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行" />
</service>
<!-- ====================== 其他配置(原有保留,补充优化) ====================== -->
<meta-data <meta-data
android:name="android.max_aspect" android:name="android.max_aspect"
android:value="4.0"/> android:value="4.0"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/> <!-- 所有非外部调用的 Activity统一设 exported=false提升安全 -->
<activity
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/> android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/> <activity
android:name=".activities.PixelPickerActivity"
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/> android:exported="false"/>
<activity
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:name=".activities.SettingsActivity"
android:exported="false"/>
<!-- 文件提供者(原有保留,正常使用) -->
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/> android:resource="@xml/file_provider"/>
</provider> </provider>
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/> <!-- UCrop 第三方页面原有保留exported=true 正常) -->
<activity
<activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/> android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
<!-- 1. 注册 UCropActivity关键解决崩溃 --> android:exported="true">
<activity </activity>
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"> <!-- 必须添加Android 12+ 要求显式声明 exported -->
</activity>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -2,28 +2,36 @@ package cc.winboll.studio.powerbell;
import android.content.Context; import android.content.Context;
import android.os.Environment; import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver; import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import java.io.File; import java.io.File;
public class App extends GlobalApplication { public class App extends GlobalApplication {
public static final String TAG = "App"; public static final String TAG = "App";
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1"; public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1"; public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2"; public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"; public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"; public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"; public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
// 数据配置存储工具 // 数据配置存储工具
static AppConfigUtils _mAppConfigUtils; static AppConfigUtils _mAppConfigUtils;
static AppCacheUtils _mAppCacheUtils; static AppCacheUtils _mAppCacheUtils;
// 新增:全局 Bitmap 缓存工具(常驻内存)
public static BitmapCacheUtils _mBitmapCacheUtils;
GlobalApplicationReceiver mReceiver; GlobalApplicationReceiver mReceiver;
static String szTempDir = ""; static String szTempDir = "";
@@ -34,25 +42,16 @@ public class App extends GlobalApplication {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
setIsDebugging(BuildConfig.DEBUG); setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
// 初始化活动窗口管理 // 初始化活动窗口管理
WinBoLLActivityManager.init(this); WinBoLLActivityManager.init(this);
// 初始化 Toast 框架 // 初始化 Toast 框架
ToastUtils.init(this); ToastUtils.init(this);
// 临时文件夹方案1 // 临时文件夹初始化(保持原有逻辑)
// 获取Pictures文件夹路径Android 10及以上推荐使用MediaStore此处为传统方式
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
// 定义目标文件路径在Pictures目录下创建"PowerBell"子文件夹及文件)
File powerBellDir = new File(picturesDir, "PowerBell"); File powerBellDir = new File(picturesDir, "PowerBell");
// 临时文件夹方案2 <图片保存失败>
// 获取Pictures文件夹路径Android 10及以上推荐使用MediaStore此处为传统方式
//File powerBellDir = getExternalFilesDir("TempDir");
// 先创建文件夹(如果不存在)
if (!powerBellDir.exists()) { if (!powerBellDir.exists()) {
powerBellDir.mkdirs(); powerBellDir.mkdirs();
} }
@@ -61,11 +60,54 @@ public class App extends GlobalApplication {
// 设置数据配置存储工具 // 设置数据配置存储工具
_mAppConfigUtils = getAppConfigUtils(this); _mAppConfigUtils = getAppConfigUtils(this);
_mAppCacheUtils = getAppCacheUtils(this); _mAppCacheUtils = getAppCacheUtils(this);
// 初始化全局 Bitmap 缓存工具关键App 启动时初始化,常驻内存)
_mBitmapCacheUtils = BitmapCacheUtils.getInstance();
mReceiver = new GlobalApplicationReceiver(this); mReceiver = new GlobalApplicationReceiver(this);
mReceiver.registerAction(); mReceiver.registerAction();
// ======================== 新增:异步预加载背景图 ========================
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 1. 获取背景源工具类实例
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(App.this);
if (bgSourceUtils == null) {
LogUtils.e(TAG, "preloadBitmap: BackgroundSourceUtils 实例为空");
return;
}
// 2. 获取当前背景Bean
BackgroundBean bgBean = bgSourceUtils.getCurrentBackgroundBean();
if (bgBean == null || !bgBean.isUseBackgroundFile()) {
LogUtils.d(TAG, "preloadBitmap: 无有效背景文件,跳过预加载");
return;
}
// 3. 获取背景图路径(优先取压缩图路径)
String bgPath = bgBean.isUseBackgroundScaledCompressFile()
? bgBean.getBackgroundScaledCompressFilePath()
: bgBean.getBackgroundFilePath();
// 4. 预加载到全局缓存
if (_mBitmapCacheUtils != null) {
_mBitmapCacheUtils.cacheBitmap(bgPath);
LogUtils.d(TAG, "preloadBitmap: 应用启动时预加载成功 - " + bgPath);
} else {
LogUtils.e(TAG, "preloadBitmap: 全局 BitmapCacheUtils 未初始化");
}
} catch (Exception e) {
LogUtils.e(TAG, "preloadBitmap: 预加载失败 - " + e.getMessage());
}
}
}).start();
}
}, 1000); // 延迟1秒执行避免阻塞应用初始化
// ======================== 预加载逻辑结束 ========================
} }
// 保持原有方法不变
public static AppConfigUtils getAppConfigUtils(Context context) { public static AppConfigUtils getAppConfigUtils(Context context) {
if (_mAppConfigUtils == null) { if (_mAppConfigUtils == null) {
_mAppConfigUtils = AppConfigUtils.getInstance(context); _mAppConfigUtils = AppConfigUtils.getInstance(context);
@@ -84,12 +126,14 @@ public class App extends GlobalApplication {
_mAppCacheUtils.clearBatteryHistory(); _mAppCacheUtils.clearBatteryHistory();
} }
@Override @Override
public void onTerminate() { public void onTerminate() {
super.onTerminate(); super.onTerminate();
ToastUtils.release(); ToastUtils.release();
} // 可选App 终止时清空 Bitmap 缓存,释放内存
if (_mBitmapCacheUtils != null) {
_mBitmapCacheUtils.clearAllCache();
}
}
} }

View File

@@ -12,7 +12,7 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.BatteryInfoBean; import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver; import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.StringUtils; import cc.winboll.studio.powerbell.utils.StringUtils;

View File

@@ -26,7 +26,7 @@ import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity; import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity; import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@@ -194,7 +194,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
dialog.dismiss(); dialog.dismiss();
// 可以在这里添加确定后的回调逻辑 // 可以在这里添加确定后的回调逻辑
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getCurrentBackgroundBean(); BackgroundBean bean = utils.getPreviewBackgroundBean();
bean.setPixelColor(pixelColor); bean.setPixelColor(pixelColor);
utils.saveSettings(); utils.saveSettings();
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show(); Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
@@ -218,7 +218,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
void setBackgroundColor() { void setBackgroundColor() {
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getCurrentBackgroundBean(); BackgroundBean bean = utils.getPreviewBackgroundBean();
int nPixelColor = bean.getPixelColor(); int nPixelColor = bean.getPixelColor();
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1); RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
mainLayout.setBackgroundColor(nPixelColor); mainLayout.setBackgroundColor(nPixelColor);
@@ -247,9 +247,11 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
@Override @Override
public void onBackPressed() { public void onBackPressed() {
super.onBackPressed(); super.onBackPressed();
Intent intent = new Intent(); setResult(RESULT_OK);
intent.setClass(this, BackgroundSettingsActivity.class); finish();
startActivity(intent); // Intent intent = new Intent();
// intent.setClass(this, BackgroundSettingsActivity.class);
// startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class); //GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
} }
} }

View File

@@ -6,9 +6,7 @@ import android.view.View;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -18,6 +16,7 @@ import cc.winboll.studio.powerbell.utils.PermissionUtils;
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity { public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "SettingsActivity"; public static final String TAG = "SettingsActivity";
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
private Toolbar mToolbar; private Toolbar mToolbar;
@@ -49,11 +48,4 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
} }
}); });
} }
public void onCheckPermission(View view) {
//ToastUtils.show("onCheckPermission");
if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
ToastUtils.show("【权限检查】存储权限已全部获取");
}
}
} }

View File

@@ -12,7 +12,7 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.adapters.BatteryAdapter; import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
import cc.winboll.studio.powerbell.model.BatteryData; import cc.winboll.studio.powerbell.models.BatteryData;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

View File

@@ -2,22 +2,19 @@ package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity; import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils; import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.utils.UriUtil; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File; import java.io.File;
import java.io.IOException;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
@@ -29,21 +26,25 @@ public class BackgroundPicturePreviewDialog extends Dialog {
public static final String TAG = "BackgroundPicturePreviewDialog"; public static final String TAG = "BackgroundPicturePreviewDialog";
Context mContext; Context mContext;
BackgroundSourceUtils mBackgroundPictureUtils; //BackgroundSourceUtils mBackgroundPictureUtils;
Button dialogbackgroundpicturepreviewButton1; Button dialogbackgroundpicturepreviewButton1;
Button dialogbackgroundpicturepreviewButton2; Button dialogbackgroundpicturepreviewButton2;
String mszPreReceivedFileName; //String mszPreReceivedFileName;
IOnRecivedPictureListener mIOnRecivedPictureListener;
Uri mUriRecivedPicture;
BackgroundView mBackgroundView;
public BackgroundPicturePreviewDialog(Context context) { public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
super(context); super(context);
setContentView(R.layout.dialog_backgroundpicturepreview); setContentView(R.layout.dialog_backgroundpicturepreview);
initEnv(); mIOnRecivedPictureListener = iOnRecivedPictureListener;
//initEnv();
mContext = context; mContext = context;
mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext); //mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1); mBackgroundView = findViewById(R.id.backgroundview);
copyAndViewRecivePicture(imageView); previewRecivedPicture();
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1); dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() { dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
@@ -53,6 +54,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
// 跳转到主窗口 // 跳转到主窗口
Intent i = new Intent(mContext, MainActivity.class); Intent i = new Intent(mContext, MainActivity.class);
mContext.startActivity(i); mContext.startActivity(i);
dismiss();
} }
}); });
@@ -62,79 +64,77 @@ public class BackgroundPicturePreviewDialog extends Dialog {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// 使用分享到的图片 // 使用分享到的图片
// mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
//LogUtils.d(TAG, "mszReceivedFileName : " + mszReceivedFileName);
((IOnRecivedPictureListener)mContext).onAcceptRecivedPicture(mszPreReceivedFileName);
// 关闭对话框 // 关闭对话框
dismiss(); dismiss();
} }
}); });
} }
void initEnv() { // void initEnv() {
LogUtils.d(TAG, "initEnv()"); // LogUtils.d(TAG, "initEnv()");
mszPreReceivedFileName = "PreReceived.data"; // mszPreReceivedFileName = "PreReceived.data";
} // }
void copyAndViewRecivePicture(ImageView imageView) { void previewRecivedPicture() {
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext); BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
//取出文件uri //取出文件uri
Uri uri = activity.getIntent().getData(); mUriRecivedPicture = activity.getIntent().getData();
if (uri == null) { if (mUriRecivedPicture == null) {
uri = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM); mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
} }
//获取文件真实地址 //获取文件真实地址
String szSrcImage = UriUtil.getFilePathFromUri(mContext, uri); String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
if (TextUtils.isEmpty(szSrcImage)) { if (TextUtils.isEmpty(szSrcImage)) {
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show(); Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
dismiss(); dismiss();
return; return;
} }
mBackgroundView.loadImage(szSrcImage);
File fSrcImage = new File(szSrcImage); //
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName(); // File fSrcImage = new File(szSrcImage);
File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName); // //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
// 复制源图片到剪裁文件 // File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
try { // // 复制源图片到剪裁文件
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto); // try {
LogUtils.d(TAG, "copyFileUsingFileChannels"); // FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath()); // LogUtils.d(TAG, "copyFileUsingFileChannels");
imageView.setBackground(drawable); // Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
//LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName); // imageView.setBackground(drawable);
} catch (IOException e) { // //LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); // } catch (IOException e) {
} // LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// }
} }
// //
// 创建图片背景图片目录 // 创建图片背景图片目录
// //
boolean createBackgroundFolder2(String szBackgroundFolder) { // boolean createBackgroundFolder2(String szBackgroundFolder) {
// 文件路径参数为空值或无效值时返回false. // // 文件路径参数为空值或无效值时返回false.
if (szBackgroundFolder == null | szBackgroundFolder.equals("")) { // if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
return false; // return false;
} // }
//
LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder); // LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
File f = new File(szBackgroundFolder); // File f = new File(szBackgroundFolder);
if (f.exists()) { // if (f.exists()) {
if (f.isDirectory()) { // if (f.isDirectory()) {
return true; // return true;
} else { // } else {
// 工作路径不是一个目录 // // 工作路径不是一个目录
LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder); // LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
return false; // return false;
} // }
} else { // } else {
return f.mkdirs(); // return f.mkdirs();
} // }
} // }
public interface IOnRecivedPictureListener { public interface IOnRecivedPictureListener {
void onAcceptRecivedPicture(String szBackgroundFileName); void onAcceptRecivedPicture(Uri uriRecivedPicture);
} }
} }

View File

@@ -0,0 +1,747 @@
package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
/**
* @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%,无调节)
private static final int BRIGHTNESS_STEP = 5; // 亮度调节步长每次±5%,精准流畅)
private static final int MIN_BRIGHTNESS = 10; // 亮度最小值10%,避免全黑看不见)
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%=完全透明)
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
public interface OnColorSelectedListener {
void onColorSelected(int color); // 返回0xAARRGGBB格式颜色含透明度
}
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
// 核心数据:原始基准值(用户输入/选择颜色时更新)+ 实时调节值(亮度/透明度变化时更新)
private OnColorSelectedListener mListener; // 颜色选择回调(非空校验)
private int mInitialColor; // 初始颜色(传入的默认颜色)
private int mCurrentColor; // 当前最终颜色(含亮度+透明度调节)
private int mCurrentBrightnessPercent; // 当前亮度百分比10%-200%
// 透明度百分比0-100%,用户直观操作)+ 原始/实时值0-255颜色计算用
private int mOriginalAlphaPercent; // 原始透明度百分比(基准值,用户输入/选色时更新)
private int mCurrentAlphaPercent; // 实时透明度百分比(调节进度条时更新)
private int mOriginalAlpha; // 原始透明度0-255基准值
private int mCurrentAlpha; // 实时透明度0-255计算用
// RGB原始基准值+实时调节值
private int mOriginalR; // 原始R分量基准值用户输入/选色时更新)
private int mOriginalG; // 原始G分量基准值用户输入/选色时更新)
private int mOriginalB; // 原始B分量基准值用户输入/选色时更新)
private int mCurrentR; // 实时R分量亮度调节后同步输入框显示
private int mCurrentG; // 实时G分量亮度调节后同步输入框显示
private int mCurrentB; // 实时B分量亮度调节后同步输入框显示
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
private static volatile boolean isAppSelfUpdatingColor = false;
// 控件引用(新增透明度进度条+文本)
private ImageView ivColorPicker; // 颜色预览拾取框
private ImageView ivColorScaler; // 颜色渐变拾取框
private EditText etR; // R分量输入框显示实时调节值
private EditText etG; // G分量输入框显示实时调节值
private EditText etB; // B分量输入框显示实时调节值
private EditText etColorValue; // 颜色值输入框(#AARRGGBB显示最终值
private SeekBar sbAlpha; // 透明度调节进度条0-100%
private TextView tvAlphaValue; // 透明度数值显示X%
private TextView tvBrightnessMinus;// 亮度减少按钮(-
private TextView tvBrightnessValue;// 亮度数值显示X%,直观易懂)
private TextView tvBrightnessPlus; // 亮度增加按钮(+
private TextView tvConfirm; // 确认按钮
private TextView tvCancel; // 取消按钮
// ====================== 构造方法(初始化核心数据,严格校验) ======================
public ColorPaletteDialog(Context context, int initialColor, OnColorSelectedListener listener) {
super(context, R.style.CustomDialogStyle);
this.mInitialColor = initialColor;
this.mListener = listener;
// 1. 强制回调非空,避免后续空指针(容错)
if (mListener == null) {
throw new IllegalArgumentException("OnColorSelectedListener can not be null!");
}
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
// 透明度初始颜色的alpha0-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);
this.mCurrentR = mOriginalR;
this.mCurrentG = mOriginalG;
this.mCurrentB = mOriginalB;
// 3. 初始化当前状态默认亮度100%,当前颜色=初始颜色)
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
this.mCurrentColor = initialColor;
LogUtils.d(TAG, "init dialog success | 初始颜色:" + String.format("#%08X", initialColor)
+ " | 原始RGB" + mOriginalR + "," + mOriginalG + "," + mOriginalB
+ " | 原始透明度:" + mOriginalAlphaPercent + "%"
+ " | 初始亮度:" + mCurrentBrightnessPercent + "%");
}
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); // 隐藏标题栏
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_color_palette, null);
setContentView(view);
// 初始化流程:控件绑定→数据赋值→监听设置→尺寸适配(小米机型优先适配)
initViewBind(view);
initData();
initListener();
adjustDialogSize();
LogUtils.d(TAG, "dialog create complete | 适配小米API29-30机型");
}
@Override
public void dismiss() {
super.dismiss();
// 释放资源,避免内存泄漏(回调引用置空)
mListener = null;
LogUtils.d(TAG, "dialog dismiss | 释放资源完成");
}
// ====================== 初始化核心方法(职责单一,便于维护) ======================
/**
* 控件绑定(新增透明度进度条+文本绑定)
*/
private void initViewBind(View view) {
ivColorPicker = view.findViewById(R.id.iv_color_picker);
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);
etColorValue = view.findViewById(R.id.et_color_value);
sbAlpha = view.findViewById(R.id.sb_alpha);
tvAlphaValue = view.findViewById(R.id.tv_alpha_value);
tvBrightnessMinus = view.findViewById(R.id.tv_brightness_minus);
tvBrightnessValue = view.findViewById(R.id.tv_brightness_value);
tvBrightnessPlus = view.findViewById(R.id.tv_brightness_plus);
tvConfirm = view.findViewById(R.id.tv_confirm);
tvCancel = view.findViewById(R.id.tv_cancel);
// 控件非空校验(小米低版本容错,绑定失败直接关闭对话框)
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) {
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确");
dismiss();
return;
}
LogUtils.d(TAG, "view bind complete | 所有控件绑定成功");
}
/**
* 数据初始化(无监听状态下赋值,避免循环回调)
*/
private void initData() {
// 1. 颜色预览(显示当前最终颜色,初始=原始颜色)
ivColorPicker.setBackgroundColor(mCurrentColor);
// 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));
// 4. 透明度控件(进度条+文本,初始=原始透明度)
sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
// 5. 亮度控件显示默认100%,初始化按钮状态)
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
updateBrightnessBtnStatus(); // 禁用边界值按钮初始100%,都可用)
LogUtils.d(TAG, "init data complete | 原始透明度:" + mOriginalAlphaPercent + "%");
}
/**
* 监听初始化(新增透明度进度条监听)
*/
private void initListener() {
// 点击监听(按钮+颜色拾取框)
ivColorPicker.setOnClickListener(this);
ivColorScaler.setOnClickListener(this);
tvConfirm.setOnClickListener(this);
tvCancel.setOnClickListener(this);
tvBrightnessMinus.setOnClickListener(this);
tvBrightnessPlus.setOnClickListener(this);
// 透明度进度条监听
sbAlpha.setOnSeekBarChangeListener(this);
// 输入框监听RGB+颜色值,避免循环同步)
initTextWatcherListener();
LogUtils.d(TAG, "all listener init complete | 监听绑定成功");
}
/**
* 对话框尺寸适配(小米全面屏+软键盘优化,避免输入框被遮挡)
*/
private void adjustDialogSize() {
Window window = getWindow();
if (window != null) {
WindowManager.LayoutParams lp = window.getAttributes();
// 宽度占屏幕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);
LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘");
}
}
// ====================== 监听子方法(细分类型,逻辑清晰) ======================
/**
* 输入框文本监听RGB+颜色值传入触发ID避免循环同步
*/
private void initTextWatcherListener() {
// RGB输入框监听复用方法减少冗余
setEditTextWatcher(etR, R.id.et_r);
setEditTextWatcher(etG, R.id.et_g);
setEditTextWatcher(etB, R.id.et_b);
// 颜色值输入框监听(支持#RRGGBB/#AARRGGBB格式
etColorValue.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) {}
@Override
public void afterTextChanged(Editable s) {
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
if (!isAppSelfUpdatingColor) {
parseColorFromStr(s.toString().trim(), R.id.et_color_value);
}
}
});
}
// ====================== 透明度进度条监听实现(核心新增) ======================
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// 仅处理用户手动拖动进度条(避免应用自身更新时触发)
if (fromUser && !isAppSelfUpdatingColor) {
updateAlphaBySeekBar(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
/**
* 拖动透明度进度条更新颜色(核心新增逻辑)
*/
private synchronized void updateAlphaBySeekBar(int alphaPercent) {
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
// 更新实时透明度(百分比+0-255值
mCurrentAlphaPercent = alphaPercent;
mCurrentAlpha = percent2Alpha(alphaPercent);
// 重新计算最终颜色(基于当前亮度+新透明度)
calculateBrightnessAndUpdate();
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, "update alpha by seekbar | 透明度:" + mCurrentAlphaPercent + "%");
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
// ====================== 颜色核心逻辑(新增透明度参数,全功能兼容) ======================
/**
* 核心计算基于原始RGB+当前亮度+当前透明度计算实时RGB+最终颜色
* 逻辑亮度百分比→调节系数→原始RGB×系数→限制0-255→拼接透明度→最终颜色
*/
private void calculateBrightnessAndUpdate() {
// 亮度百分比转调节系数10%→0.1100%→1.0200%→2.0
float brightnessFactor = mCurrentBrightnessPercent / 100.0f;
// RGB三个分量同时调节基于原始基准值避免叠加失真限制0-255
mCurrentR = Math.min(Math.max(Math.round(mOriginalR * brightnessFactor), 0), MAX_RGB_VALUE);
mCurrentG = Math.min(Math.max(Math.round(mOriginalG * brightnessFactor), 0), MAX_RGB_VALUE);
mCurrentB = Math.min(Math.max(Math.round(mOriginalB * brightnessFactor), 0), MAX_RGB_VALUE);
// 拼接「实时透明度」+「实时RGB」得到最终颜色0xAARRGGBB
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
}
/**
* 亮度减少每次减5%最低10%,防止过暗)
*/
private void decreaseBrightness() {
changeBrightness(false);
}
/**
* 亮度增加每次加5%最高200%,防止过曝)
*/
private void increaseBrightness() {
changeBrightness(true);
}
/**
* 亮度调节核心方法(统一逻辑,加并发控制,同步所有控件)
*/
private synchronized void changeBrightness(boolean isIncrease) {
// 关键:判断非应用自身更新,才执行调节(避免重复触发/循环)
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
if (isIncrease) {
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return; // 达到最大值,不处理
mCurrentBrightnessPercent += BRIGHTNESS_STEP; // 增加步长
} else {
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return; // 达到最小值,不处理
mCurrentBrightnessPercent -= BRIGHTNESS_STEP; // 减少步长
}
// 计算亮度调节后的实时RGB+最终颜色(含当前透明度)
calculateBrightnessAndUpdate();
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, (isIncrease ? "increase" : "decrease") + " brightness | "
+ "亮度:" + mCurrentBrightnessPercent + "% | 实时RGB" + mCurrentR + "," + mCurrentG + "," + mCurrentB);
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
/**
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB容错处理更新原始基准值+实时值)
* 新增:解析颜色的透明度,同步更新透明度进度条
*/
private void parseColorFromStr(String colorStr, int triggerViewId) {
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
if (TextUtils.isEmpty(colorStr)) return;
// 补全#前缀兼容用户输入习惯如直接输AARRGGBB
if (!colorStr.startsWith("#")) {
colorStr = "#" + colorStr;
}
// 格式校验仅支持6位RRGGBB/8位AARRGGBB避免非法格式
if (colorStr.length() != 7 && colorStr.length() != 9) {
LogUtils.e(TAG, "parse color failed | 格式错误(需#RRGGBB/#AARRGGBB输入" + colorStr);
return;
}
// 解析颜色系统API安全可靠
int parsedColor = Color.parseColor(colorStr);
// 更新原始基准值(用户输入颜色,重置基准)
// 透明度解析颜色的alpha0-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 + "%");
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "parse color failed | 非法颜色格式,输入:" + colorStr, e);
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
/**
* 通过RGB输入框更新颜色用户输入后更新原始基准值+实时值重置亮度为100%
* 新增透明度基准值保持不变仅更新RGB
*/
private synchronized void updateColorByRGB(int triggerViewId) {
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
if (!isAppSelfUpdatingColor) {
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 + "%");
} catch (Exception e) {
LogUtils.e(TAG, "update color by RGB failed", e);
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
/**
* 核心同步:更新所有控件显示(新增透明度控件同步,统一方法)
*/
private void updateAllViews() {
// 1. 同步颜色预览(显示最终颜色,含透明度+亮度)
ivColorPicker.setBackgroundColor(mCurrentColor);
// 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));
// 4. 同步透明度控件(进度条+文本,显示实时透明度)
sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
// 5. 同步亮度控件(数值+按钮状态)
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
updateBrightnessBtnStatus();
LogUtils.d(TAG, "sync all views complete | 最终颜色:" + String.format("#%08X", mCurrentColor)
+ " | 实时RGB" + mCurrentR + "," + mCurrentG + "," + mCurrentB
+ " | 透明度:" + mCurrentAlphaPercent + "% | 亮度:" + mCurrentBrightnessPercent + "%");
}
/**
* 更新亮度按钮状态(边界值禁用,提升交互体验)
*/
private void updateBrightnessBtnStatus() {
// 亮度≤10%禁用减号文字变浅灰≥200%:禁用加号(文字变浅灰)
boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS;
boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS;
tvBrightnessMinus.setEnabled(canMinus);
tvBrightnessPlus.setEnabled(canPlus);
tvBrightnessMinus.setTextColor(canMinus ? Color.BLACK : Color.parseColor("#CCCCCC"));
tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC"));
}
// ====================== 工具方法(新增透明度转换工具,通用复用) ======================
/**
* 透明度0-255 → 0-100%(颜色计算值转用户直观百分比)
*/
private int alpha2Percent(int alpha) {
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
}
/**
* 透明度0-100% → 0-255用户操作百分比转颜色计算值
*/
private int percent2Alpha(int percent) {
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
}
/**
* 解析输入值限制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); // 限制范围,避免溢出
} catch (NumberFormatException e) {
LogUtils.e(TAG, "parse input failed | 非法数字,输入:" + input, e);
return 0;
}
}
/**
* RGB输入框监听复用减少冗余代码统一逻辑
*/
private void setEditTextWatcher(EditText editText, final int viewId) {
editText.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) {}
@Override
public void afterTextChanged(Editable s) {
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
if (!isAppSelfUpdatingColor) {
updateColorByRGB(viewId); // 输入变化后更新颜色
}
}
});
}
/**
* 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("选择基础颜色");
// 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
};
// 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));
// 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); // 设置圆形背景
colorBtn.setClickable(true);
colorBtn.setFocusable(true);
// 点击事件(逻辑不变)
colorBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
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();
builder.create().dismiss();
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
+ " | 透明度:" + mCurrentAlphaPercent + "%");
} finally {
isAppSelfUpdatingColor = false;
}
}
}
});
colorLayout.addView(colorBtn);
}
// 层级嵌套(滚动容器→颜色容器)
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); // 打开系统颜色选择器
} else if (id == R.id.tv_confirm) {
mListener.onColorSelected(mCurrentColor); // 确认选择,回调颜色
LogUtils.d(TAG, "confirm color | 回调颜色:" + String.format("#%08X", mCurrentColor));
dismiss();
} else if (id == R.id.tv_cancel) {
dismiss(); // 取消,关闭对话框
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
} else if (id == R.id.tv_brightness_minus) {
decreaseBrightness(); // 减少亮度
} else if (id == R.id.tv_brightness_plus) {
increaseBrightness(); // 增加亮度
}
}
}
void openColorScalerDialog(int nColor) {
//ToastUtils.show("openColorPickerDialog");
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int color) {
dlg.currentColorScalerDialogColor = color;
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
}
class ColorScalerDialog extends ColorPickerDialog {
public int currentColorScalerDialogColor = 0;
public ColorScalerDialog(Context context, int p) {
super(context, p);
}
@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;
}
}
}
}
}

View File

@@ -20,7 +20,7 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity; import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
@@ -33,333 +33,8 @@ public class MainViewFragment extends Fragment {
public static final String TAG = "MainViewFragment"; public static final String TAG = "MainViewFragment";
public static final int MSG_RELOAD_APPCONFIG = 0;
public static final int MSG_CURRENTVALUEBATTERY = 1;
static MainViewFragment _mMainViewFragment;
AppConfigUtils mAppConfigUtils;
View mView;
Drawable mDrawableFrame;
LinearLayout mllLeftSeekBar;
LinearLayout mllRightSeekBar;
CheckBox mcbIsEnableChargeReminder;
CheckBox mcbIsEnableUsegeReminder;
Switch mswIsEnableService;
TextView mtvTips;
private BackgroundSourceUtils mBgSourceUtils;
// 背景布局
//LinearLayout mLinearLayoutloadBackground;
// 现在电量图示
BatteryDrawable mCurrentValueBatteryDrawable;
// 现在充电提醒电量图示
BatteryDrawable mChargeReminderValueBatteryDrawable;
// 现在耗电提醒电量图示
BatteryDrawable mUsegeReminderValueBatteryDrawable;
ImageView mCurrentValueBatteryImageView;
ImageView mChargeReminderValueBatteryImageView;
ImageView mUsegeReminderValueBatteryImageView;
VerticalSeekBar mChargeReminderSeekBar;
ChargeReminderSeekBarChangeListener mChargeReminderSeekBarChangeListener;
TextView mtvChargeReminderValue;
VerticalSeekBar mUsegeReminderSeekBar;
UsegeReminderSeekBarChangeListener mUsegeReminderSeekBarChangeListener;
TextView mtvUsegeReminderValue;
CheckBox mcbUsegeReminderValue;
TextView mtvCurrentValue;
BackgroundView mBackgroundView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.fragment_mainview, container, false);
_mMainViewFragment = MainViewFragment.this;
mAppConfigUtils = App.getAppConfigUtils(getActivity());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
// 获取指定ID的View实例
mBackgroundView = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
loadBackground();
/*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1);
// 注册OnGlobalLayoutListener
mainImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 获取宽度和高度
int width = mainImageView.getMeasuredWidth();
int height = mainImageView.getMeasuredHeight();
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(getActivity());
BackgroundPictureBean bean = utils.loadBackgroundPictureBean();
bean.setBackgroundWidth(width);
bean.setBackgroundHeight(height);
utils.saveData();
// 移除监听器以避免内存泄漏
mainImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});*/
mDrawableFrame = getActivity().getDrawable(R.drawable.bg_frame);
mllLeftSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout1);
mllRightSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout2);
// 初始化充电电量提醒设置控件
mtvChargeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView2);
mChargeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar1);
mcbIsEnableChargeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox1);
// 初始化耗电电量提醒设置控件
mtvUsegeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView3);
mUsegeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar2);
mcbIsEnableUsegeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox2);
// 初始化现在电量显示控件
mtvCurrentValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView4);
// 初始化服务总开关
mswIsEnableService = (Switch) mView.findViewById(R.id.fragmentandroidviewSwitch1);
mtvTips = mView.findViewById(R.id.fragmentandroidviewTextView1);
// 设置视图显示数据
setViewData();
// 设置视图控件响应
setViewListener();
// 注册一个广播接收
//mMainActivityReceiver = new MainActivityReceiver(this);
//mMainActivityReceiver.registerAction();
// 启动的时候检查一下服务
if (mAppConfigUtils.getIsEnableService()
&& ServiceUtils.isServiceAlive(getActivity(), ControlCenterService.class.getName()) == false) {
// 如果配置了服务启动,服务没有启动
// 就启动服务
Intent intent = new Intent(getActivity(), ControlCenterService.class);
getActivity().startForegroundService(intent);
}
return mView;
}
void loadBackground() {
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
mBackgroundView.loadBackgroundBean(bean);
}
@Override
public void onResume() {
super.onResume();
loadBackground();
}
void setViewData() {
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue();
int nCurrentValue = mAppConfigUtils.getCurrentValue();
mllLeftSeekBar.setBackground(mDrawableFrame);
mllRightSeekBar.setBackground(mDrawableFrame);
// 初始化电量图
mCurrentValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCurrent));
mCurrentValueBatteryDrawable.setValue(mAppConfigUtils.getCurrentValue());
mCurrentValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView1);
mCurrentValueBatteryImageView.setImageDrawable(mCurrentValueBatteryDrawable);
// 初始化充电电量提醒图
mChargeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCharge));
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
mChargeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView3);
mChargeReminderValueBatteryImageView.setImageDrawable(mChargeReminderValueBatteryDrawable);
// 初始化耗电电量提醒图
mUsegeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorUsege));
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
mUsegeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView2);
mUsegeReminderValueBatteryImageView.setImageDrawable(mUsegeReminderValueBatteryDrawable);
// 初始化充电电量提醒设置控件
mtvChargeReminderValue.setTextColor(getActivity().getColor(R.color.colorCharge));
//LogUtils.d(TAG, "Color.YELLOW is " + Integer.toString(mApplication.getColor(R.color.colorCharge)));
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
mChargeReminderSeekBar.setProgress(nChargeReminderValue);
mcbIsEnableChargeReminder.setChecked(mAppConfigUtils.getIsEnableChargeReminder());
// 初始化耗电电量提醒设置控件
mtvUsegeReminderValue.setTextColor(getActivity().getColor(R.color.colorUsege));
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
mUsegeReminderSeekBar.setProgress(nUsegeReminderValue);
mcbIsEnableUsegeReminder.setChecked(mAppConfigUtils.getIsEnableUsegeReminder());
// 初始化现在电量显示控件
mtvCurrentValue.setTextColor(getActivity().getColor(R.color.colorCurrent));
mtvCurrentValue.setText(Integer.toString(nCurrentValue) + "%");
// 初始化服务总开关
mswIsEnableService.setChecked(mAppConfigUtils.getIsEnableService());
if (mAppConfigUtils.getIsEnableService()) {
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
ControlCenterService.startControlCenterService(getActivity());
} else {
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
ControlCenterService.stopControlCenterService(getActivity());
}
mswIsEnableService.setText(getString(R.string.txt_aboveswitch));
mtvTips.setText(getString(R.string.txt_aboveswitchtips));
}
void setViewListener() {
// 初始化充电电量提醒设置控件
mChargeReminderSeekBarChangeListener = new ChargeReminderSeekBarChangeListener();
mChargeReminderSeekBar.setOnSeekBarChangeListener(mChargeReminderSeekBarChangeListener);
mcbIsEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "setIsEnableChargeReminder");
mAppConfigUtils.setIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
//ControlCenterService.updateIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
}
});
// 初始化耗电电量提醒设置控件
mUsegeReminderSeekBarChangeListener = new UsegeReminderSeekBarChangeListener();
mUsegeReminderSeekBar.setOnSeekBarChangeListener(mUsegeReminderSeekBarChangeListener);
mcbIsEnableUsegeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "setIsEnableUsegeReminder");
mAppConfigUtils.setIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
//ControlCenterService.updateIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
}
});
// 初始化服务总开关
mswIsEnableService.setOnClickListener(new CompoundButton.OnClickListener() {
@Override
public void onClick(View view) {
mAppConfigUtils.setIsEnableService(getActivity(), mswIsEnableService.isChecked());
}
});
}
void setCurrentValueBattery(int value) {
//LogUtils.d(TAG, "setCurrentValueBattery");
mtvCurrentValue.setText(Integer.toString(value) + "%");
mCurrentValueBatteryDrawable.setValue(value);
mCurrentValueBatteryDrawable.invalidateSelf();
}
class ChargeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
//LogUtils.d(TAG, "call onProgressChanged");
int nChargeReminderValue = progress;
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
mChargeReminderValueBatteryDrawable.invalidateSelf();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStartTrackingTouch");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStopTrackingTouch");
//取得当前进度条的刻度
int nChargeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
mAppConfigUtils.setChargeReminderValue(nChargeReminderValue);
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
//ControlCenterService.updateChargeReminderValue(nChargeReminderValue);
}
}
class UsegeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
//LogUtils.d(TAG, "call onProgressChanged");
int nUsegeReminderValue = progress;
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
mUsegeReminderValueBatteryDrawable.invalidateSelf();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStartTrackingTouch");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStopTrackingTouch");
//取得当前进度条的刻度
int nUsegeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
LogUtils.d(TAG, "nUsegeReminderValue is " + Integer.toString(nUsegeReminderValue));
//LogUtils.d(TAG, "mPowerReminder is " + mApplication);
mAppConfigUtils.setUsegeReminderValue(nUsegeReminderValue);
//LogUtils.d(TAG, "opopopopopopopop");
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
//ControlCenterService.updateUsegeReminderValue(nUsegeReminderValue);
}
}
public void reloadBackground() {
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(getActivity());
mBgSourceUtils.loadSettings();
BackgroundBean bean = utils.getCurrentBackgroundBean();
mBackgroundView.loadBackgroundBean(bean);
}
Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RELOAD_APPCONFIG : {
setViewData();
break;
}
case MSG_CURRENTVALUEBATTERY : {
setCurrentValueBattery(msg.arg1);
break;
}
}
super.handleMessage(msg);
}
};
public static void relaodAppConfigs() {
if (_mMainViewFragment != null) {
Handler handler = _mMainViewFragment.mHandler;
handler.sendMessage(handler.obtainMessage(MSG_RELOAD_APPCONFIG));
}
}
public static void sendMsgCurrentValueBattery(int value) {
if (_mMainViewFragment != null) {
Handler handler = _mMainViewFragment.mHandler;
Message msg = handler.obtainMessage(MSG_CURRENTVALUEBATTERY);
msg.arg1 = value;
handler.sendMessage(msg);
}
}
} }

View File

@@ -1,130 +0,0 @@
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.io.Serializable;
public class AppConfigBean extends BaseBean implements Serializable {
transient public static final String TAG = "AppConfigBean";
boolean isEnableUsegeReminder = false;
int usegeReminderValue = 45;
boolean isEnableChargeReminder = false;
int chargeReminderValue = 100;
// 铃声提醒间隔时间。.
int reminderIntervalTime = 5000;
// 电池是否正在充电。
boolean isCharging = false;
// 电池当前电量。.
int currentValue = -1;
public AppConfigBean() {
setChargeReminderValue(100);
setIsEnableChargeReminder(false);
setUsegeReminderValue(10);
setIsEnableUsegeReminder(false);
setReminderIntervalTime(5000);
}
public void setReminderIntervalTime(int reminderIntervalTime) {
this.reminderIntervalTime = reminderIntervalTime;
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
public void setIsCharging(boolean isCharging) {
this.isCharging = isCharging;
}
public boolean isCharging() {
return isCharging;
}
public void setCurrentValue(int currentValue) {
this.currentValue = currentValue;
}
public int getCurrentValue() {
return currentValue;
}
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
this.isEnableUsegeReminder = isEnableUsegeReminder;
}
public boolean isEnableUsegeReminder() {
return isEnableUsegeReminder;
}
public void setUsegeReminderValue(int usegeReminderValue) {
this.usegeReminderValue = usegeReminderValue;
}
public int getUsegeReminderValue() {
return usegeReminderValue;
}
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
this.isEnableChargeReminder = isEnableChargeReminder;
}
public boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
public void setChargeReminderValue(int chargeReminderValue) {
this.chargeReminderValue = chargeReminderValue;
}
public int getChargeReminderValue() {
return chargeReminderValue;
}
@Override
public String getName() {
return AppConfigBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
AppConfigBean bean = this;
jsonWriter.name("isEnableUsegeReminder").value(bean.isEnableUsegeReminder());
jsonWriter.name("usegeReminderValue").value(bean.getUsegeReminderValue());
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
AppConfigBean bean = new AppConfigBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("isEnableUsegeReminder")) {
bean.setIsEnableUsegeReminder(jsonReader.nextBoolean());
} else if (name.equals("usegeReminderValue")) {
bean.setUsegeReminderValue(jsonReader.nextInt());
} else if (name.equals("isEnableChargeReminder")) {
bean.setIsEnableChargeReminder(jsonReader.nextBoolean());
} else if (name.equals("chargeReminderValue")) {
bean.setChargeReminderValue(jsonReader.nextInt());
} else {
jsonReader.skipValue();
}
}
// 结束 JSON 对象
jsonReader.endObject();
return bean;
}
}

View File

@@ -0,0 +1,160 @@
package cc.winboll.studio.powerbell.models;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.io.Serializable;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类(电池提醒配置、电量状态等)
*/
public class AppConfigBean extends BaseBean implements Serializable {
// ====================== 核心修复:序列化版本号(固定不变,解决崩溃) ======================
private static final long serialVersionUID = -2588138142254163296L; // 直接用崩溃日志中的流版本号
// ====================== 常量定义(默认值抽离,统一管理) ======================
public static final String TAG = "AppConfigBean";
// 默认配置常量(避免魔法值,后续修改更便捷)
private static final boolean DEFAULT_ENABLE_USAGE_REMINDER = false;
private static final int DEFAULT_USAGE_REMINDER_VALUE = 10; // 低电量提醒阈值默认10%
private static final boolean DEFAULT_ENABLE_CHARGE_REMINDER = false;
private static final int DEFAULT_CHARGE_REMINDER_VALUE = 100; // 满电提醒阈值默认100%
private static final int DEFAULT_REMINDER_INTERVAL_TIME = 5000; // 提醒间隔默认5秒
private static final boolean DEFAULT_IS_CHARGING = false; // 默认未充电
private static final int DEFAULT_CURRENT_VALUE = 100; // 默认满电
// ====================== 成员变量(规范命名+访问控制transient排除无需序列化字段 ======================
// 低电量提醒开关
private boolean isEnableUsageReminder = DEFAULT_ENABLE_USAGE_REMINDER;
// 低电量提醒阈值(百分比)
private int usageReminderValue = DEFAULT_USAGE_REMINDER_VALUE;
// 满电提醒开关
private boolean isEnableChargeReminder = DEFAULT_ENABLE_CHARGE_REMINDER;
// 满电提醒阈值(百分比)
private int chargeReminderValue = DEFAULT_CHARGE_REMINDER_VALUE;
// 铃声提醒间隔时间(毫秒)
private int reminderIntervalTime = DEFAULT_REMINDER_INTERVAL_TIME;
// 电池充电状态瞬时状态无需序列化加transient
private transient boolean isCharging = DEFAULT_IS_CHARGING;
// 当前电池电量瞬时状态无需序列化加transient
private transient int currentValue = DEFAULT_CURRENT_VALUE;
// ====================== 构造方法(简化逻辑,与默认值保持一致) ======================
public AppConfigBean() {
// 直接复用常量默认值,无需重复赋值(构造方法逻辑极简)
}
// ====================== Getter/Setter规范命名+参数校验,避免非法值) ======================
public boolean isEnableUsageReminder() {
return isEnableUsageReminder;
}
public void setEnableUsageReminder(boolean enableUsageReminder) {
isEnableUsageReminder = enableUsageReminder;
}
public int getUsageReminderValue() {
return usageReminderValue;
}
public void setUsageReminderValue(int usageReminderValue) {
// 校验电量阈值限制在0-100之间避免非法值导致业务异常
this.usageReminderValue = Math.min(Math.max(usageReminderValue, 0), 100);
}
public boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
public void setEnableChargeReminder(boolean enableChargeReminder) {
isEnableChargeReminder = enableChargeReminder;
}
public int getChargeReminderValue() {
return chargeReminderValue;
}
public void setChargeReminderValue(int chargeReminderValue) {
// 校验电量阈值限制在0-100之间
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, 0), 100);
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
public void setReminderIntervalTime(int reminderIntervalTime) {
// 校验间隔时间不小于1秒1000ms避免过短触发频繁提醒
this.reminderIntervalTime = Math.max(reminderIntervalTime, 1000);
}
// 瞬时状态字段仅保留Get/Set无序列化影响
public boolean isCharging() {
return isCharging;
}
public void setCharging(boolean charging) {
isCharging = charging;
}
public int getCurrentValue() {
return currentValue;
}
public void setCurrentValue(int currentValue) {
// 校验当前电量限制在0-100之间
this.currentValue = Math.min(Math.max(currentValue, 0), 100);
}
// ====================== 父类方法重写补全JSON序列化逻辑兼容所有配置字段 ======================
@Override
public String getName() {
return AppConfigBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
// 仅序列化「配置类字段」瞬时状态isCharging/currentValue不序列化
jsonWriter.name("isEnableUsageReminder").value(isEnableUsageReminder());
jsonWriter.name("usageReminderValue").value(getUsageReminderValue());
jsonWriter.name("isEnableChargeReminder").value(isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(getChargeReminderValue());
jsonWriter.name("reminderIntervalTime").value(getReminderIntervalTime()); // 补全之前遗漏的间隔时间序列化
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
AppConfigBean bean = new AppConfigBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
switch (name) {
case "isEnableUsageReminder":
bean.setEnableUsageReminder(jsonReader.nextBoolean());
break;
case "usageReminderValue":
bean.setUsageReminderValue(jsonReader.nextInt());
break;
case "isEnableChargeReminder":
bean.setEnableChargeReminder(jsonReader.nextBoolean());
break;
case "chargeReminderValue":
bean.setChargeReminderValue(jsonReader.nextInt());
break;
case "reminderIntervalTime":
bean.setReminderIntervalTime(jsonReader.nextInt()); // 补全间隔时间反序列化
break;
default:
jsonReader.skipValue(); // 未知字段跳过,避免解析崩溃
break;
}
}
jsonReader.endObject();
return bean;
}
}

View File

@@ -1,16 +1,17 @@
package cc.winboll.studio.powerbell.model; package cc.winboll.studio.powerbell.models;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.JsonWriter; import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean; import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 11:52:28 * @Date 2024/07/18 11:52:28
* @Describe 应用背景图片数据类存储正式/预览背景配置支持JSON序列化/反序列化 * @Describe 应用背景图片数据类存储正式/预览背景配置支持JSON序列化/反序列化
*/ */
public class BackgroundBean extends BaseBean { public class BackgroundBean extends BaseBean implements Serializable {
public static final String TAG = "BackgroundPictureBean"; public static final String TAG = "BackgroundPictureBean";

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model; package cc.winboll.studio.powerbell.models;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model; package cc.winboll.studio.powerbell.models;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.JsonWriter; import android.util.JsonWriter;

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model; package cc.winboll.studio.powerbell.models;
/** /**
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model; package cc.winboll.studio.powerbell.models;
// 应用消息结构 // 应用消息结构
// //

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.AppConfigBean; import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils; import cc.winboll.studio.powerbell.utils.BatteryUtils;
@@ -50,7 +50,7 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
appConfigUtils.loadAppConfigBean(); appConfigUtils.loadAppConfigBean();
AppConfigBean appConfigBean = appConfigUtils.mAppConfigBean; AppConfigBean appConfigBean = appConfigUtils.mAppConfigBean;
appConfigBean.setCurrentValue(nTheQuantityOfElectricity); appConfigBean.setCurrentValue(nTheQuantityOfElectricity);
appConfigBean.setIsCharging(isCharging); appConfigBean.setCharging(isCharging);
mwrService.get().startRemindThread(appConfigBean); mwrService.get().startRemindThread(appConfigBean);
// 保存电池报告 // 保存电池报告

View File

@@ -5,10 +5,9 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.fragments.MainViewFragment; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils; import cc.winboll.studio.powerbell.utils.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper;
public class GlobalApplicationReceiver extends BroadcastReceiver { public class GlobalApplicationReceiver extends BroadcastReceiver {
@@ -36,7 +35,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
// 先设置好新电池状态标志 // 先设置好新电池状态标志
boolean isCharging = BatteryUtils.isCharging(intent); boolean isCharging = BatteryUtils.isCharging(intent);
if (_mIsCharging != isCharging) { if (_mIsCharging != isCharging) {
mAppConfigUtils.setIsCharging(isCharging); mAppConfigUtils.setCharging(isCharging);
} }
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent); int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) { if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
@@ -48,7 +47,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
//NotificationHelper.cancelRemindNotification(context); //NotificationHelper.cancelRemindNotification(context);
App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity); App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity);
MainViewFragment.sendMsgCurrentValueBattery(nTheQuantityOfElectricity); MainActivity.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
// 保存好新的电池状态标志 // 保存好新的电池状态标志
_mIsCharging = isCharging; _mIsCharging = isCharging;
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity; _mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;

View File

@@ -23,15 +23,15 @@ import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.NotificationMessage;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler; import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver; import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.services.AssistantService; import cc.winboll.studio.powerbell.services.AssistantService;
import cc.winboll.studio.powerbell.threads.RemindThread; import cc.winboll.studio.powerbell.threads.RemindThread;
import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper; import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils; import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.utils.StringUtils; import cc.winboll.studio.powerbell.utils.StringUtils;
@@ -48,7 +48,7 @@ public class ControlCenterService extends Service {
AppConfigUtils mAppConfigUtils; AppConfigUtils mAppConfigUtils;
AppCacheUtils mAppCacheUtils; AppCacheUtils mAppCacheUtils;
// 前台服务通知工具 // 前台服务通知工具
NotificationHelper mNotificationHelper; NotificationManagerUtils mNotificationManagerUtils;
Notification notification; Notification notification;
RemindThread mRemindThread; RemindThread mRemindThread;
ControlCenterServiceHandler mControlCenterServiceHandler; ControlCenterServiceHandler mControlCenterServiceHandler;
@@ -72,7 +72,7 @@ public class ControlCenterService extends Service {
isServiceRunning = false; isServiceRunning = false;
mAppConfigUtils = App.getAppConfigUtils(this); mAppConfigUtils = App.getAppConfigUtils(this);
mAppCacheUtils = App.getAppCacheUtils(this); mAppCacheUtils = App.getAppCacheUtils(this);
mNotificationHelper = new NotificationHelper(ControlCenterService.this); mNotificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
if (mMyServiceConnection == null) { if (mMyServiceConnection == null) {
@@ -101,10 +101,10 @@ public class ControlCenterService extends Service {
wakeupAndBindAssistant(); wakeupAndBindAssistant();
// 显示前台通知栏 // 显示前台通知栏
// 在Service中 // 在Service中
NotificationHelper helper = new NotificationHelper(this); NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(this);
Intent intent = new Intent(this, MainActivity.class); //Intent intent = new Intent(this, MainActivity.class);
notification = helper.showForegroundNotification(intent, getString(R.string.app_name), "Service Running, Click to open app"); notificationManagerUtils.startForegroundServiceNotify(ControlCenterService.this, new NotificationMessage(getString(R.string.app_name), "Service Running, Click to open app"));
startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification); //startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
// NotificationMessage notificationMessage=createNotificationMessage(); // NotificationMessage notificationMessage=createNotificationMessage();
// //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show(); // //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show();
@@ -132,9 +132,9 @@ public class ControlCenterService extends Service {
String getValuesString() { String getValuesString() {
String szReturn = "Usege: "; String szReturn = "Usege: ";
szReturn += mAppConfigUtils.getIsEnableUsegeReminder() ? Integer.toString(mAppConfigUtils.getUsegeReminderValue()) : "?"; szReturn += mAppConfigUtils.isEnableUsageReminder() ? Integer.toString(mAppConfigUtils.getUsageReminderValue()) : "?";
szReturn += "% Charge: "; szReturn += "% Charge: ";
szReturn += mAppConfigUtils.getIsEnableChargeReminder() ? Integer.toString(mAppConfigUtils.getChargeReminderValue()) : "?"; szReturn += mAppConfigUtils.isEnableChargeReminder() ? Integer.toString(mAppConfigUtils.getChargeReminderValue()) : "?";
szReturn += "%\nCurrent: " + Integer.toString(mAppConfigUtils.getCurrentValue()) + "%"; szReturn += "%\nCurrent: " + Integer.toString(mAppConfigUtils.getCurrentValue()) + "%";
return szReturn; return szReturn;
} }
@@ -185,9 +185,9 @@ public class ControlCenterService extends Service {
} else { } else {
// 提醒进程正在进行中就更新状态后退出 // 提醒进程正在进行中就更新状态后退出
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue()); mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue()); mRemindThread.setUsegeReminderValue(appConfigBean.getUsageReminderValue());
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder()); mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder()); mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsageReminder());
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime()); mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
mRemindThread.setIsCharging(appConfigBean.isCharging()); mRemindThread.setIsCharging(appConfigBean.isCharging());
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue()); mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
@@ -196,12 +196,12 @@ public class ControlCenterService extends Service {
} }
} }
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue()); mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue()); mRemindThread.setUsegeReminderValue(appConfigBean.getUsageReminderValue());
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime()); mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
mRemindThread.setIsCharging(appConfigBean.isCharging()); mRemindThread.setIsCharging(appConfigBean.isCharging());
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue()); mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder()); mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder()); mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsageReminder());
mRemindThread.start(); mRemindThread.start();
//LogUtils.d(TAG, "mRemindThread.start()"); //LogUtils.d(TAG, "mRemindThread.start()");
} }
@@ -260,9 +260,9 @@ public class ControlCenterService extends Service {
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
msg += szRemindMSG; msg += szRemindMSG;
} }
NotificationHelper helper = new NotificationHelper(ControlCenterService.this); NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
Intent intent = new Intent(ControlCenterService.this, MainActivity.class); Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
helper.showTemporaryNotification(intent, getString(R.string.app_name), msg); notificationManagerUtils.showTempAlertNotify(getString(R.string.app_name), msg);

View File

@@ -1,183 +1,249 @@
package cc.winboll.studio.powerbell.unittest; package cc.winboll.studio.powerbell.unittest;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils; import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import cc.winboll.studio.powerbell.models.BackgroundBean;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * 终极修复版放弃FileProvider直接用私有目录File路径彻底解决UID冲突
* @Date 2025/11/19 18:04
* @Describe 单元测试启动主页窗口
*/ */
public class MainUnitTestActivity extends AppCompatActivity { public class MainUnitTestActivity extends AppCompatActivity {
// ====================== 常量定义 ======================
public static final String TAG = "MainUnitTestActivity"; public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0; public static final int REQUEST_CROP_IMAGE = 0;
// 新增:权限请求码 private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
public static final int REQUEST_STORAGE_PERMISSION = 1001;
View mainView; // ====================== 成员变量移除所有Uri相关 ======================
BackgroundSourceUtils mBgSourceUtils; private BackgroundView mBackgroundView;
BackgroundView mBackgroundView; private String mAppPrivateDirPath;
// 测试图片路径用Environment获取适配低版本避免硬编码 private File mPrivateTestImageFile; // 仅用File不用Uri
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) private File mPrivateCropImageFile;
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg"; BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法 ======================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
setContentView(R.layout.activity_mainunittest);
mBackgroundView = findViewById(R.id.backgroundview);
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){ initBaseParams();
@Override initViewAndEvent();
public void onClick(View v) { copyAssetsTestImageToPrivateDir();
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class)); //loadBackgroundByFile(); // 直接用File加载
} mPreviewBackgroundBean = new BackgroundBean();
}); mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
// 裁剪测试按钮点击事件(新增权限校验) mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){ mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
@Override mPreviewBackgroundBean.setIsUseBackgroundFile(true);
public void onClick(View v) { doubleRefreshPreview();
ToastUtils.show("onClick准备启动裁剪");
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
// 修复1移除高版本API依赖适配低版本存储权限校验
if (checkStoragePermission()) {
// 权限已授予,启动裁剪
startCropTest();
} else {
// 权限未授予,申请权限
requestStoragePermission();
}
}
});
ToastUtils.show(String.format("%s onCreate", TAG)); ToastUtils.show("单元测试页面启动完成");
// 加载测试图片(验证图片路径是否有效) LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
loadBackground();
} }
/**
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
*/
private void startCropTest() {
// 修复2输出路径用Environment获取确保目录存在避免路径无效
File outputDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/");
if (!outputDir.exists()) {
outputDir.mkdirs(); // 创建目录(避免输出路径不存在导致裁剪失败)
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
}
String dstOutputPath = outputDir.getAbsolutePath()
+ "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
// 修复3自由裁剪时比例传0避免100:100过大导致机型崩溃
ImageCropUtils.startImageCrop(
MainUnitTestActivity.this,
new File(szTestSource),
new File(dstOutputPath),
0, // 自由裁剪传0
0, // 自由裁剪传0
true,
REQUEST_CROP_IMAGE
);
}
/**
* 校验存储读写权限适配Android 6.0+ 低版本SDK移除TIRAMISU依赖
*/
private boolean checkStoragePermission() {
// 适配Android 6.0API 23及以上用通用的读写权限移除高版本API
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
/**
* 申请存储读写权限适配低版本SDK移除READ_MEDIA_IMAGES依赖
*/
private void requestStoragePermission() {
LogUtils.d(TAG, "【裁剪测试】申请存储读写权限");
// 用通用的读写权限适配所有Android 6.0+ 机型,无高版本依赖)
ActivityCompat.requestPermissions(
this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION
);
}
/**
* 权限申请回调
*/
@Override @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_STORAGE_PERMISSION) { LogUtils.d(TAG, "=== onActivityResult 回调 ===");
// 校验权限是否授予 if (requestCode == REQUEST_CROP_IMAGE) {
boolean allGranted = true; handleCropResult(resultCode);
for (int result : grantResults) { }
if (result != PackageManager.PERMISSION_GRANTED) { }
allGranted = false;
break; // ====================== 初始化相关方法 ======================
private void initBaseParams() {
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File");
// 私有目录无需权限无UID冲突
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
File privateDir = new File(mAppPrivateDirPath);
if (!privateDir.exists()) {
privateDir.mkdirs();
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
}
// 初始化File无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());
}
private void initViewAndEvent() {
LogUtils.d(TAG, "初始化布局与控件事件");
setContentView(R.layout.activity_mainunittest);
mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
// 跳转主页面按钮
Button btnMain = (Button) findViewById(R.id.btn_main_activity);
btnMain.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击按钮:跳转主页面");
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
// 裁剪按钮直接用File路径启动无Uri
Button btnCrop = (Button) findViewById(R.id.btn_test_cropimage);
btnCrop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击按钮启动裁剪File路径版");
ToastUtils.show("准备启动图片裁剪");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
startCropTestByFile(); // 直接传File
} else {
ToastUtils.show("测试图片未准备好,重新拷贝");
copyAssetsTestImageToPrivateDir();
}
}
});
}
// 从assets拷贝图片不变确保File存在
private void copyAssetsTestImageToPrivateDir() {
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
LogUtils.d(TAG, "图片已存在,无需拷贝");
return;
}
InputStream inputStream = null;
try {
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
} catch (IOException e) {
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), e);
ToastUtils.show("图片准备失败");
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "关闭流失败:" + e.getMessage());
} }
} }
if (allGranted) {
ToastUtils.show("存储权限已授予,启动裁剪");
startCropTest(); // 权限授予后启动裁剪
} else {
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
}
} }
} }
@Override // ====================== 核心业务方法全改为File路径 ======================
protected void onActivityResult(int requestCode, int resultCode, Intent data) { /** 直接用File路径加载背景图无Uri无冲突 */
super.onActivityResult(requestCode, resultCode, data); // private void loadBackgroundByFile() {
LogUtils.d(TAG, "【裁剪回调】requestCode" + requestCode + "resultCode" + resultCode + "data" + (data == null ? "null" : data.toString())); // LogUtils.d(TAG, "开始加载背景图File路径版");
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null)); // if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
// 裁剪完成后回收权限 // mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
if (requestCode == REQUEST_CROP_IMAGE) { // LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) // ToastUtils.show("背景图加载成功");
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg"; // } else {
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath)); // LogUtils.e(TAG, "背景图加载失败:文件无效");
//ImageCropUtils.releaseCropPermission(this, outputUri); // ToastUtils.show("背景图加载失败");
mBackgroundView.loadImage(dstOutputPath); // }
// }
/** 直接用File启动裁剪关键调用ImageCropUtils的File重载方法 */
private void startCropTestByFile() {
LogUtils.d(TAG, "启动裁剪File路径版原图" + mPrivateTestImageFile.getAbsolutePath());
// 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) {
cropParent.mkdirs();
} }
}
// 调用ImageCropUtils的File参数方法核心绕开Uri
void loadBackground() { ImageCropUtils.startImageCrop(
// 校验测试图片是否存在(避免路径错误) this,
File testFile = new File(szTestSource); mPrivateTestImageFile, // 原图File
if (testFile.exists() && testFile.length() > 100) { mPrivateCropImageFile, // 输出File
mBackgroundView.loadImage(szTestSource); 0,
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource); 0,
true,
REQUEST_CROP_IMAGE
);
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
ToastUtils.show("已启动图片裁剪");
}
/** 处理裁剪结果直接校验输出File */
private void handleCropResult(int resultCode) {
LogUtils.d(TAG, "裁剪回调处理resultCode=" + resultCode);
if (resultCode == RESULT_OK) {
if (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview();
} else {
LogUtils.e(TAG, "裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效");
}
} else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "裁剪取消");
ToastUtils.show("裁剪已取消");
} else { } else {
ToastUtils.show("测试图片不存在或无效"); LogUtils.e(TAG, "裁剪失败resultCode异常");
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource); ToastUtils.show("裁剪失败");
} }
} }
/**
* 双重刷新预览,确保背景加载最新数据
* 移除:缓存清空逻辑
*/
private void doubleRefreshPreview() {
// 第一重刷新
try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing()) {
try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
}
}
}
}, 200);
}
} }

View File

@@ -2,7 +2,7 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context; import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.BatteryInfoBean; import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList; import java.util.ArrayList;
public class AppCacheUtils { public class AppCacheUtils {

View File

@@ -5,8 +5,8 @@ import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.model.AppConfigBean; import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.model.ControlCenterServiceBean; import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog; import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.fragments.MainViewFragment; import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.services.ControlCenterService;
@@ -79,7 +79,7 @@ public class AppConfigUtils {
@Override @Override
public void onNo() { public void onNo() {
MainViewFragment.relaodAppConfigs(); MainActivity.relaodAppConfigs();
} }
}); });
} }
@@ -93,22 +93,22 @@ public class AppConfigUtils {
return bean.isEnableService(); return bean.isEnableService();
} }
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) { public void setEnableChargeReminder(boolean isEnableChargeReminder) {
mAppConfigBean.setIsEnableChargeReminder(isEnableChargeReminder); mAppConfigBean.setEnableChargeReminder(isEnableChargeReminder);
saveConfigData(MainActivity._mMainActivity); saveConfigData(MainActivity._mMainActivity);
} }
public boolean getIsEnableChargeReminder() { public boolean isEnableChargeReminder() {
return mAppConfigBean.isEnableChargeReminder(); return mAppConfigBean.isEnableChargeReminder();
} }
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) { public void setEnableUsageReminder(boolean isEnableUsegeReminder) {
mAppConfigBean.setIsEnableUsegeReminder(isEnableUsegeReminder); mAppConfigBean.setEnableUsageReminder(isEnableUsegeReminder);
saveConfigData(MainActivity._mMainActivity); saveConfigData(MainActivity._mMainActivity);
} }
public boolean getIsEnableUsegeReminder() { public boolean isEnableUsageReminder() {
return mAppConfigBean.isEnableUsegeReminder(); return mAppConfigBean.isEnableUsageReminder();
} }
public void setChargeReminderValue(int value) { public void setChargeReminderValue(int value) {
@@ -120,17 +120,17 @@ public class AppConfigUtils {
return mAppConfigBean.getChargeReminderValue(); return mAppConfigBean.getChargeReminderValue();
} }
public void setUsegeReminderValue(int value) { public void setUsageReminderValue(int value) {
mAppConfigBean.setUsegeReminderValue(value); mAppConfigBean.setUsageReminderValue(value);
saveConfigData(MainActivity._mMainActivity); saveConfigData(MainActivity._mMainActivity);
} }
public int getUsegeReminderValue() { public int getUsageReminderValue() {
return mAppConfigBean.getUsegeReminderValue(); return mAppConfigBean.getUsageReminderValue();
} }
public void setIsCharging(boolean isCharging) { public void setCharging(boolean isCharging) {
mAppConfigBean.setIsCharging(isCharging); mAppConfigBean.setCharging(isCharging);
} }
public boolean isCharging() { public boolean isCharging() {
@@ -162,9 +162,9 @@ public class AppConfigUtils {
bean = new AppConfigBean(); bean = new AppConfigBean();
AppConfigBean.saveBean(mContext, mAppConfigBean); AppConfigBean.saveBean(mContext, mAppConfigBean);
} }
mAppConfigBean.setIsEnableUsegeReminder(bean.isEnableUsegeReminder()); mAppConfigBean.setEnableUsageReminder(bean.isEnableUsageReminder());
mAppConfigBean.setUsegeReminderValue(bean.getUsegeReminderValue()); mAppConfigBean.setUsageReminderValue(bean.getUsageReminderValue());
mAppConfigBean.setIsEnableChargeReminder(bean.isEnableChargeReminder()); mAppConfigBean.setEnableChargeReminder(bean.isEnableChargeReminder());
mAppConfigBean.setChargeReminderValue(bean.getChargeReminderValue()); mAppConfigBean.setChargeReminderValue(bean.getChargeReminderValue());
} }
@@ -183,7 +183,7 @@ public class AppConfigUtils {
@Override @Override
public void onNo() { public void onNo() {
AppConfigUtils.getInstance(activity).loadAppConfigBean(); AppConfigUtils.getInstance(activity).loadAppConfigBean();
MainViewFragment.relaodAppConfigs(); MainActivity.relaodAppConfigs();
} }
}); });
} }
@@ -198,6 +198,6 @@ public class AppConfigUtils {
AppConfigBean.saveBean(mContext, mAppConfigBean); AppConfigBean.saveBean(mContext, mAppConfigBean);
// 通知活动窗口和服务配置已更新 // 通知活动窗口和服务配置已更新
ControlCenterService.updateStatus(mContext, mAppConfigBean); ControlCenterService.updateStatus(mContext, mAppConfigBean);
MainViewFragment.relaodAppConfigs(); MainActivity.relaodAppConfigs();
} }
} }

View File

@@ -0,0 +1,122 @@
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;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 09:14
* @Describe Assets 目录拷贝工具类
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
*/
public class AssetsCopyUtils {
public static final String TAG = "AssetsCopyUtils";
private static final int BUFFER_SIZE = 1024 * 8;
/**
* 拷贝 assets/images/ 目录到指定目标目录
* @param context 上下文
* @param targetDirPath 目标目录完整路径(如 /sdcard/PowerBell/assets_images
* @return 拷贝是否成功
*/
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
// 拷贝 assets/images 根目录
return copyAssetsDirToDir(context, "images", targetDirPath);
}
/**
* 递归拷贝 assets 下指定目录到目标目录
* @param context 上下文
* @param assetsDir assets 下的源目录(如 "images"、"images/subdir"
* @param targetDirPath 目标目录完整路径
* @return 拷贝是否成功
*/
public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) {
File targetDir = new File(targetDirPath);
// 创建目标目录(含多级父目录)
if (!targetDir.exists() && !targetDir.mkdirs()) {
Log.e(TAG, "创建目标目录失败:" + targetDirPath);
return false;
}
try {
// 获取 assets 目录下的文件/子目录列表
String[] fileList = context.getAssets().list(assetsDir);
if (fileList == null || fileList.length == 0) {
Log.d(TAG, "assets 目录为空:" + assetsDir);
return true;
}
for (String fileName : fileList) {
String assetsFilePath = assetsDir + File.separator + fileName;
String targetFilePath = targetDirPath + File.separator + fileName;
// 判断当前项是文件还是子目录
String[] subFileList = context.getAssets().list(assetsFilePath);
if (subFileList != null && subFileList.length > 0) {
// 是子目录,递归拷贝
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
return false;
}
} else {
// 是文件,直接拷贝
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
return false;
}
}
}
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
return true;
} catch (IOException e) {
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage());
return false;
}
}
/**
* 拷贝 assets 下单个文件到指定路径
* @param context 上下文
* @param assetsFilePath assets 下的文件路径(如 "images/cloud.png"
* @param targetFilePath 目标文件完整路径
* @return 拷贝是否成功
*/
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String 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);
return true;
}
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
return true;
} catch (IOException e) {
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage());
return false;
} finally {
// 关闭流
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
} catch (IOException e) {
Log.e(TAG, "关闭流异常:" + e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,206 @@
package cc.winboll.studio.powerbell.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.TextUtils;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 01:57
* @Describe 单例 Bitmap 缓存工具类Java 7 兼容)
* 功能:内存缓存 Bitmap支持路径关联缓存、全局获取、缓存清空
* 特点1. 单例模式 2. 压缩加载避免OOM 3. 路径- Bitmap 映射 4. 线程安全
*/
public class BitmapCacheUtils {
public static final String TAG = "BitmapCacheUtils";
// 最大图片尺寸适配1080P屏幕可根据需求调整
private static final int MAX_WIDTH = 1080;
private static final int MAX_HEIGHT = 1920;
// 单例实例volatile 保证多线程可见性)
private static volatile BitmapCacheUtils sInstance;
// 路径-Bitmap 缓存容器(内存缓存)
private final Map<String, Bitmap> mBitmapCacheMap;
// 私有构造器(单例模式)
private BitmapCacheUtils() {
mBitmapCacheMap = new HashMap<>();
}
/**
* 获取单例实例(双重校验锁,线程安全)
*/
public static BitmapCacheUtils getInstance() {
if (sInstance == null) {
synchronized (BitmapCacheUtils.class) {
if (sInstance == null) {
sInstance = new BitmapCacheUtils();
}
}
}
return sInstance;
}
/**
* 核心接口:根据图片路径缓存 Bitmap 到内存
* @param imagePath 图片绝对路径
* @return 缓存成功的 Bitmap / null路径无效/文件不存在/解码失败)
*/
public Bitmap cacheBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "cacheBitmap: 图片路径为空");
return null;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
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);
return cachedBitmap;
} else {
// 缓存的Bitmap已失效移除后重新加载
mBitmapCacheMap.remove(imagePath);
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效移除后重新加载 - " + imagePath);
}
}
// 压缩加载 Bitmap避免OOM
Bitmap bitmap = decodeCompressedBitmap(imagePath);
if (bitmap != null) {
// 存入缓存容器
mBitmapCacheMap.put(imagePath, bitmap);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功 - " + imagePath);
} else {
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
}
return bitmap;
}
/**
* 核心接口:根据路径获取缓存的 Bitmap
* @param imagePath 图片绝对路径
* @return 缓存的有效 Bitmap / null未缓存/已回收)
*/
public Bitmap getCachedBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return null;
}
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
// 校验Bitmap是否有效
if (bitmap != null && bitmap.isRecycled()) {
mBitmapCacheMap.remove(imagePath);
return null;
}
return bitmap;
}
/**
* 清空所有 Bitmap 缓存(释放内存)
*/
public void clearAllCache() {
synchronized (mBitmapCacheMap) {
for (Bitmap bitmap : mBitmapCacheMap.values()) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle(); // 主动回收 Bitmap
}
}
mBitmapCacheMap.clear();
}
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
}
/**
* 移除指定路径的 Bitmap 缓存
* @param imagePath 图片绝对路径
*/
public void removeCachedBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return;
}
synchronized (mBitmapCacheMap) {
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
}
}
}
/**
* 压缩解码 Bitmap按最大尺寸缩放避免OOM
* @param imagePath 图片绝对路径
* @return 解码后的 Bitmap / null文件无效/解码失败)
*/
private Bitmap decodeCompressedBitmap(String imagePath) {
// 前置校验:确保文件有效
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + 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);
return null;
}
// 计算缩放比例
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
// 第二步:加载压缩后的 Bitmap
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存比ARGB_8888少一半内存
options.inPurgeable = true;
options.inInputShareable = true;
try {
return BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath);
return null;
} catch (Exception e) {
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
return null;
}
}
/**
* 计算 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;
}
}
return inSampleSize;
}
}

View File

@@ -0,0 +1,101 @@
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;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/08 21:11
* @Describe 把 R.drawable 中的图片保存为 File 对象的工具类
* 适配 PowerBell 项目支持指定保存路径、自动创建目录、处理PNG图片压缩
*/
public class DrawableToFileUtils {
private static final String TAG = "DrawableToFileUtils";
/**
* 核心方法:将 R.drawable 图片保存为 File 对象
* @param context 上下文(用于获取 Resources
* @param drawableResId 图片资源ID如 R.drawable.ic_test_png
* @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.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/文件名为空)");
return null;
}
if (!filePath.endsWith(".png")) {
filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确
LogUtils.d(TAG, "【格式适配】自动添加.png后缀最终文件名" + filePath);
}
// 3. 构建目标 File 对象(最终保存的文件路径)
File targetFile = new File(filePath);
LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath());
// 4. 读取 drawable 资源为 Bitmap处理高清图/缩放问题)
Bitmap bitmap = null;
try {
// 读取 drawable 资源(适配不同分辨率的图片,避免变形)
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
if (bitmap == null) {
LogUtils.e(TAG, "【读取失败】无法读取drawable资源资源ID" + drawableResId + "");
return null;
}
LogUtils.d(TAG, "【读取成功】drawable资源转Bitmap成功" + bitmap.getWidth() + ",高:" + bitmap.getHeight() + "");
// 5. 将 Bitmap 写入 FilePNG格式无损保存
FileOutputStream fos = new FileOutputStream(targetFile);
// 压缩参数PNG格式质量100无损写入输出流
boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush(); // 刷新输出流
fos.close(); // 关闭输出流
// 6. 校验保存结果(文件是否存在且有效)
if (isSaved && targetFile.exists() && targetFile.length() > 100) {
LogUtils.d(TAG, "【保存成功】drawable图片保存为File" + targetFile.getAbsolutePath());
return targetFile; // 保存成功返回File对象
} else {
LogUtils.e(TAG, "【保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)");
// 保存失败,删除无效文件
if (targetFile.exists()) {
targetFile.delete();
LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath());
}
return null;
}
} catch (IOException e) {
LogUtils.e(TAG, "【保存异常】写入文件时出错:" + e.getMessage());
LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e));
return null;
} finally {
// 回收Bitmap资源避免内存溢出
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
LogUtils.d(TAG, "【资源回收】Bitmap资源已回收");
}
}
}
/**
* 重载方法:自定义保存路径(灵活适配不同场景)
* @param context 上下文
* @param drawableResId 图片资源ID
* @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/"
* @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());
}
}

View File

@@ -263,19 +263,28 @@ public class FileUtils {
} }
} }
public static String getFileSuffix(Context context, Uri uri){ public static boolean isFileExists(String path) {
String szType = context.getContentResolver().getType(uri); File file = new File(path);
// 2. 截取MIME类型后缀如从image/jpeg中提取jpeg【核心新增逻辑】 return file.exists();
String fileSuffix = ""; }
if (szType != null && szType.contains("/")) {
// 分割字符串,取"/"后面的部分(如"image/jpeg" → 分割后取索引1的"jpeg" /**
fileSuffix = szType.split("/")[1]; * 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
// 调试日志:打印截取后的文件后缀 * @param file 目标文件
} else { * @return 后缀字符串(无后缀返回空字符串,非空统一小写)
// 异常处理若类型为空或格式错误默认后缀设为jpeg保留原逻辑兼容性 */
fileSuffix = "jpeg"; public static String getFileSuffix(File file) {
if (file == null || file.getName().isEmpty()) {
return ""; // 空文件/空文件名,返回空
} }
return fileSuffix; String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
return fileName.substring(lastDotIndex + 1).toLowerCase();
} }
} }

View File

@@ -6,42 +6,101 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import com.yalantis.ucrop.UCropActivity;
import java.io.File;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import java.io.File;
/** /**
* 图片裁剪工具类集成uCrop,脱离系统依赖 * 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File 双传参
*/ */
public class ImageCropUtils { public class ImageCropUtils {
public static final String TAG = "ImageCropUtils"; public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与项目一致) // FileProvider 授权(与 AndroidManifest 配置一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; 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;
// ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)======================
/**
* 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param inputUri 输入图片 Uri本应用 FileProvider Uri非空
* @param outputUri 输出图片 Uri本应用 FileProvider Uri非空
* @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
public static void startImageCrop(Activity activity,
Uri inputUri,
Uri outputUri,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return;
}
if (inputUri == null || outputUri == null) {
LogUtils.e(TAG, "【裁剪异常】输入/输出 Uri 为空");
showToast(activity, "图片 Uri 无效,无法裁剪");
return;
}
if (!isValidUri(activity, inputUri)) {
LogUtils.e(TAG, "【裁剪异常】输入 Uri 无效:" + inputUri);
showToast(activity, "原图 Uri 无效,无法裁剪");
return;
}
// 2. 核心:强制修正输出为 PNG忽略原图格式统一转 PNG
File outputFile = uriToFile(activity, outputUri);
if (outputFile == null) {
LogUtils.e(TAG, "【裁剪异常】输出 Uri 转 File 失败:" + outputUri);
showToast(activity, "裁剪输出路径无效");
return;
}
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 4. 启动裁剪
uCrop.withOptions(options);
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【裁剪启动成功Uri 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
}
/** /**
* 启动uCrop裁剪(核心方法,替代系统裁剪) * 【File 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文 * @param activity 上下文
* @param inputFile 输入图片文件 * @param inputFile 输入图片文件(任意格式)
* @param outputFile 输出图片文件 * @param outputFile 输出图片文件(最终强制转为 PNG
* @param isFreeCrop 是否自由裁剪true=自由false=固定比例 * @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码 * @param requestCode 裁剪请求码
*/ */
public static void startImageCrop(Activity activity, public static void startImageCrop(Activity activity,
File inputFile, File inputFile,
File outputFile, File outputFile,
int aspectX, int aspectX,
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
// 校验输入参数 // 1. 输入参数校验
if (activity == null || activity.isFinishing()) { if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】上下文Activity无效"); LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return; return;
} }
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) { if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
LogUtils.e(TAG, "【裁剪异常】输入文件无效"); LogUtils.e(TAG, "【裁剪异常】输入图片文件无效");
showToast(activity, "无有效图片可裁剪"); showToast(activity, "无有效图片可裁剪");
return; return;
} }
@@ -51,47 +110,28 @@ public class ImageCropUtils {
return; return;
} }
// 生成输入/输出Uri适配FileProvider // 2. 核心:强制修正输出为 PNG忽略原图格式
Uri inputUri = getFileProviderUri(activity, inputFile); Uri inputUri = getFileProviderUri(activity, inputFile);
Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri兼容低版本 outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
Uri outputUri = getFileProviderUri(activity, outputFile);
// 配置uCrop参数 // 3. 初始化 uCrop + 强制 PNG 配置
UCrop uCrop = UCrop.of(inputUri, outputUri); UCrop uCrop = UCrop.of(inputUri, outputUri);
UCrop.Options options = new UCrop.Options(); uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 裁剪模式配置(自由裁剪/固定比例) // 4. 启动裁剪
if (isFreeCrop) {
// 自由裁剪:无固定比例,可随意调整
uCrop.withAspectRatio(0, 0);
options.setFreeStyleCropEnabled(true); // 开启自由裁剪
} else {
// 固定比例默认1:1可根据需求修改
uCrop.withAspectRatio(aspectX, aspectY);
options.setFreeStyleCropEnabled(false);
}
// 裁剪配置(优化体验)
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)); // 状态栏颜色
// 应用配置并启动裁剪
uCrop.withOptions(options); uCrop.withOptions(options);
// 启动uCrop裁剪Activity替代系统裁剪
uCrop.start(activity, requestCode); uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【裁剪启动成功File 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
LogUtils.d(TAG, "【uCrop启动】成功输入Uri" + inputUri + "输出Uri" + outputUri + ",请求码:" + requestCode);
} }
/** /**
* 重载方法:适配BackgroundBean * BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
*/ */
public static void startImageCrop(Activity activity, public static void startImageCrop(Activity activity,
BackgroundBean cropBean, BackgroundBean cropBean,
int aspectX, int aspectX,
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
@@ -100,70 +140,163 @@ public class ImageCropUtils {
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode); startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
} }
/** // ====================== 裁剪结果处理(保持兼容,优化日志)======================
* 生成FileProvider Uri public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
*/ if (requestCode != cropRequestCode) return null;
private static Uri getFileProviderUri(Activity activity, File file) {
try { if (resultCode == Activity.RESULT_OK && data != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Uri outputUri = UCrop.getOutput(data);
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX; if (outputUri != null) {
Uri uri = FileProvider.getUriForFile(activity, authority, file); String outputPath = uriToPath(outputUri);
LogUtils.d(TAG, "Uri生成】FileProvider Uri" + uri); LogUtils.d(TAG, "裁剪成功】强制输出 PNG透明保留输出路径" + outputPath);
return uri; return outputPath;
} else {
Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "【Uri生成】普通Uri" + uri);
return uri;
} }
} else if (resultCode == UCrop.RESULT_ERROR) {
Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "【裁剪失败】原因:" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【裁剪取消】用户手动取消");
}
return null;
}
// ====================== 辅助方法(优化适配强制 PNG 逻辑)======================
/** 校验 Uri 有效性(确保是图片类型) */
private static boolean isValidUri(Activity activity, Uri uri) {
try {
String type = activity.getContentResolver().getType(uri);
return type != null && type.startsWith("image/");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "【Uri生成】失败" + e.getMessage()); LogUtils.e(TAG, "【Uri 校验失败】原因" + e.getMessage());
return false;
}
}
/** Uri 转 File适配 FileProvider Uri 和普通 Uri */
private static File uriToFile(Activity activity, Uri uri) {
if (uri == null) return null;
try {
if (uri.getScheme().equals("file")) {
return new File(uri.getPath());
}
String filePath = uri.getPath();
if (filePath == null) return null;
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);
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 转 File 失败】uri=" + uri + ",原因:" + e.getMessage());
return null;
}
}
/** Uri 提取文件路径 */
private static String uriToPath(Uri uri) {
if (uri == null) return null;
try {
if (uri.getScheme().equals("file")) {
return uri.getPath();
}
String path = uri.getPath();
if (path == null) return null;
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;
}
}
return path;
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage());
return null; return null;
} }
} }
/** /**
* 处理uCrop裁剪回调在Activity的onActivityResult中调用 * 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心
* @param requestCode 请求码 * 移除 isPng 参数,全程用 PNG 配置
* @param resultCode 结果码
* @param data 回调数据
* @return 裁剪成功返回输出文件路径失败返回null
*/ */
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) { private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
// 校验是否是uCrop的回调
if (requestCode == cropRequestCode) { UCrop.Options options = new UCrop.Options();
if (resultCode == Activity.RESULT_OK && data != null) {
// 裁剪成功获取输出Uri // 裁剪模式配置(自由裁剪/固定比例)
Uri outputUri = UCrop.getOutput(data); options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
if (outputUri != null) {
String outputPath = outputUri.getPath(); // 裁剪配置(优化体验)
LogUtils.d(TAG, "【uCrop回调】裁剪成功输出路径" + outputPath); //options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
return outputPath; //options.setCompressionQuality(100); // 图片质量
} //options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
} else if (resultCode == UCrop.RESULT_ERROR) { //options.setToolbarTitle("图片裁剪"); // 工具栏标题
// 裁剪失败,获取异常信息 //options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
Throwable error = UCrop.getError(data); //options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
LogUtils.e(TAG, "【uCrop回调】裁剪失败" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【uCrop回调】裁剪被取消"); // 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
} options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
} options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
return null; 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 配置(保持原有风格)
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));
return options;
} }
/** /**
* 显示Toast * 修正文件后缀(强制转为 .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);
}
/** 生成 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);
} else {
return Uri.fromFile(file);
}
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage());
return null;
}
}
/** 显示 Toast避免崩溃 */
private static void showToast(Activity activity, String msg) { private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) { if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show(); android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
} }
} }
/** // ====================== 公有辅助方法(供外部调用)======================
* 暴露getFileProviderUri方法供外部调用
*/
public static Uri getFileProviderUriPublic(Activity activity, File file) { public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, file); return getFileProviderUri(activity, file);
} }
public static File getFileFromUriPublic(Activity activity, Uri uri) {
return uriToFile(activity, uri);
}
public static String getPathFromUriPublic(Uri uri) {
return uriToPath(uri);
}
} }

View File

@@ -1,151 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 04:39:40
* @Describe 通知工具类
*/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.powerbell.R;
public class NotificationHelper {
public static final String TAG = "NotificationHelper";
// 渠道ID和名称
private static final String CHANNEL_ID_FOREGROUND = "foreground_channel";
private static final String CHANNEL_NAME_FOREGROUND = "Foreground Service";
private static final String CHANNEL_ID_TEMPORARY = "temporary_channel";
private static final String CHANNEL_NAME_TEMPORARY = "Temporary Notifications";
// 通知ID
public static final int FOREGROUND_NOTIFICATION_ID = 1001;
public static final int TEMPORARY_NOTIFICATION_ID = 2001;
private final Context mContext;
private final NotificationManager mNotificationManager;
public NotificationHelper(Context context) {
mContext = context;
mNotificationManager = context.getSystemService(NotificationManager.class);
createNotificationChannels();
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundChannel();
createTemporaryChannel();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createForegroundChannel() {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_FOREGROUND,
CHANNEL_NAME_FOREGROUND,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Persistent service notifications");
channel.setSound(null, null);
channel.enableVibration(false);
mNotificationManager.createNotificationChannel(channel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createTemporaryChannel() {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_TEMPORARY,
CHANNEL_NAME_TEMPORARY,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Temporary alert notifications");
channel.setSound(null, null);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
channel.setBypassDnd(true);
mNotificationManager.createNotificationChannel(channel);
}
// 显示常驻通知(通常用于前台服务)
public Notification showForegroundNotification(Intent intent, String title, String content) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_FOREGROUND)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
//.setContentTitle(title + "\n" + content)
.setContentTitle(content)
//.setContentText(content)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build();
mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
return notification;
}
// 显示临时通知(自动消失)
public void showTemporaryNotification(Intent intent, String title, String content) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
.setContentTitle(title)
.setContentText(content)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setVibrate(new long[]{100, 200, 300, 400})
.build();
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID, notification);
}
// 创建自定义布局通知(可扩展)
public void showCustomNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pendingIntent)
.setContent(contentView)
.setCustomBigContentView(bigContentView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build();
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID + 1, notification);
}
// 取消所有通知
public void cancelAllNotifications() {
mNotificationManager.cancelAll();
}
// 创建PendingIntent兼容不同API版本
private PendingIntent createPendingIntent(Intent intent) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// flags |= PendingIntent.FLAG_IMMUTABLE;
// }
return PendingIntent.getActivity(
mContext,
0,
intent,
flags
);
}
}

View File

@@ -0,0 +1,416 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.os.Build;
import android.view.View;
import android.widget.RemoteViews;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/13 20:44
* @Describe 全局通知管理工具类整合所有通知能力适配API29-30兼容Java7所有通知统一跳转MainActivity
*/
public class NotificationManagerUtils {
// ====================== 常量定义(统一管理,避免冲突,首屏可见)======================
public static final String TAG = "NotificationManagerUtils";
// 通知渠道4大渠道场景隔离API26+必填)
// 1. 前台服务保活渠道(低优先级,无打扰)
private static final String CHANNEL_ID_FOREGROUND_SERVICE = "channel_foreground_service";
private static final String CHANNEL_NAME_FOREGROUND_SERVICE = "前台服务保活通知";
private static final String CHANNEL_DESC_FOREGROUND_SERVICE = "后台服务运行状态,无声音无震动,不打扰用户";
// 2. 电量提醒渠道(高优先级,闹钟铃声+震动,强提醒)
private static final String CHANNEL_ID_BATTERY_REMIND = "channel_battery_remind";
private static final String CHANNEL_NAME_BATTERY_REMIND = "电量异常提醒通知";
private static final String CHANNEL_DESC_BATTERY_REMIND = "电量过高/过低提醒,强震动+闹钟铃声,突破免打扰";
// 3. 通用临时通知渠道(高优先级,仅震动,普通告警)
private static final String CHANNEL_ID_TEMP_ALERT = "channel_temp_alert";
private static final String CHANNEL_NAME_TEMP_ALERT = "通用临时提醒通知";
private static final String CHANNEL_DESC_TEMP_ALERT = "普通即时告警,仅震动提醒,自动取消";
// 通知ID唯一区分避免覆盖按场景分段
public static final int NOTIFY_ID_FOREGROUND_SERVICE = 1001; // 前台服务
public static final int NOTIFY_ID_BATTERY_REMIND = 1002; // 电量提醒
public static final int NOTIFY_ID_TEMP_ALERT = 1003; // 通用临时通知
public static final int NOTIFY_ID_CUSTOM_LAYOUT = 1004; // 自定义布局通知
// 通用配置
private static final int PENDING_INTENT_FLAGS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
: PendingIntent.FLAG_UPDATE_CURRENT; // API30安全标志
private static final long[] VIBRATE_PATTERN = new long[]{100, 200, 300, 400}; // 标准震动节奏
//private static final String DEFAULT_JUMP_PACKAGE = "cc.winboll.studio.powerbell"; // 默认跳转包名API29+必填)
// ====================== 成员变量(按场景分组,私有封装,避免外部篡改)======================
private final Context mContext;
private final NotificationManager mNotificationManager;
// 前台服务通知专属
private Notification mForegroundServiceNotify;
private RemoteViews mForegroundServiceRemoteViews;
// 电量提醒通知专属
private Notification mBatteryRemindNotify;
private RemoteViews mBatteryRemindRemoteViews;
// ====================== 构造方法(单例思想/实例化通用,自动初始化渠道)======================
public NotificationManagerUtils(Context context) {
LogUtils.d(TAG, "【初始化】全局通知管理工具类 构造方法调用");
this.mContext = context.getApplicationContext(); // 用应用上下文,避免内存泄漏
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
createAllNotificationChannels(); // 自动创建所有渠道API26+
LogUtils.d(TAG, "【初始化】全局通知管理工具类 完成,渠道创建状态:" + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "已创建4个渠道" : "无需创建"));
}
// ====================== 核心基础能力(渠道创建+Intent构建复用逻辑减少冗余======================
/**
* 创建所有通知渠道API26+专属,低版本自动跳过,确保通知正常显示)
*/
@SuppressWarnings("deprecation")
public void createAllNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LogUtils.d(TAG, "【渠道管理】开始创建所有通知渠道");
createForegroundServiceChannel();
createBatteryRemindChannel();
createTempAlertChannel();
LogUtils.d(TAG, "【渠道管理】4个通知渠道创建完成含3个核心渠道+预留扩展)");
}
}
/**
* 创建前台服务保活渠道IMPORTANCE_LOW无声音无震动不打扰用户
*/
private void createForegroundServiceChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_FOREGROUND_SERVICE,
CHANNEL_NAME_FOREGROUND_SERVICE,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription(CHANNEL_DESC_FOREGROUND_SERVICE);
channel.setSound(null, null);
channel.enableVibration(false);
channel.setShowBadge(false); // 不显示应用角标
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); // 锁屏隐藏
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】前台服务保活渠道创建成功:" + CHANNEL_NAME_FOREGROUND_SERVICE);
}
}
/**
* 创建电量提醒渠道IMPORTANCE_HIGH闹钟铃声+震动,突破免打扰)
*/
private void createBatteryRemindChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_BATTERY_REMIND,
CHANNEL_NAME_BATTERY_REMIND,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_BATTERY_REMIND);
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(true); // 突破免打扰
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // 锁屏可见
channel.setShowBadge(true);
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】电量提醒渠道创建成功:" + CHANNEL_NAME_BATTERY_REMIND);
}
}
/**
* 创建通用临时通知渠道IMPORTANCE_HIGH仅震动普通告警
*/
private void createTempAlertChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_TEMP_ALERT,
CHANNEL_NAME_TEMP_ALERT,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_TEMP_ALERT);
//channel.setSound(null, null); // 仅震动,不发声
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(false); // 不突破免打扰
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
channel.setShowBadge(true);
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】通用临时通知渠道创建成功:" + CHANNEL_NAME_TEMP_ALERT);
}
}
/**
* 构建固定跳转PendingIntent所有通知统一跳转MainActivity适配API29-30安全规范
* @return 安全的PendingIntent确保跳转稳定不泄露
*/
private PendingIntent buildFixedPendingIntent() {
// 固定跳MainActivity不支持自定义目标
Intent intent = new Intent(mContext, MainActivity.class);
// API29+ 强制要求:明确包名,避免跳转目标模糊
intent.setPackage(mContext.getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); // 跳转时清除栈顶避免重复创建Activity
LogUtils.d(TAG, "【Intent构建】所有通知统一跳转MainActivity包名" + mContext.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext,
0,
intent,
PENDING_INTENT_FLAGS
);
LogUtils.d(TAG, "【Intent构建】PendingIntent创建成功安全标志" + PENDING_INTENT_FLAGS);
return pendingIntent;
}
// ====================== 场景1前台服务保活通知支持自定义布局+更新)======================
/**
* 初始化前台服务通知自定义布局RemoteViews
*/
private void initForegroundServiceRemoteViews(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【布局初始化】开始初始化前台服务通知布局,标题:" + msg.getTitle());
mForegroundServiceRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView1, msg.getTitle());
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView3, msg.getContent());
mForegroundServiceRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
LogUtils.d(TAG, "【布局初始化】前台服务通知布局填充完成");
}
/**
* 启动前台服务保活通知ControlCenterService专用API26+强制要求,保活后台服务)
*/
public void startForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【前台服务通知】开始构建保活通知,内容:" + msg.getContent());
if (service == null || msg == null) {
LogUtils.e(TAG, "【前台服务通知】构建失败Service/NotificationMessage为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知兼容API26+渠道低版本用Builder
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(service, CHANNEL_ID_FOREGROUND_SERVICE);
} else {
builder = new Notification.Builder(service);
}
mForegroundServiceNotify = builder
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentTitle(msg.getTitle())
.setContentText(msg.getContent())
.setWhen(System.currentTimeMillis())
.setColor(Color.parseColor("#F00606")) // 小图标背景色
.setContentIntent(pendingIntent)
.setOngoing(true) // 常驻通知,不可滑动取消(保活关键)
.setAutoCancel(false) // 禁止点击取消
.build();
// 3. 设置自定义布局
initForegroundServiceRemoteViews(service, msg);
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
// 4. 启动前台服务必须调用否则Service易被回收
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "【前台服务通知】保活通知启动成功通知ID" + NOTIFY_ID_FOREGROUND_SERVICE);
}
/**
* 更新前台服务保活通知内容(无需重启服务,直接刷新布局)
*/
public void updateForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【前台服务通知】开始更新保活通知,新内容:" + msg.getContent());
if (mForegroundServiceNotify == null || mForegroundServiceRemoteViews == null) {
LogUtils.e(TAG, "【前台服务通知】更新失败通知对象未初始化先调用startForegroundServiceNotify");
return;
}
// 更新自定义布局数据
initForegroundServiceRemoteViews(service, msg);
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
// 发送更新
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "【前台服务通知】保活通知更新成功");
}
// ====================== 场景2电量提醒通知支持自定义布局+更新+单独取消)======================
/**
* 初始化电量提醒通知自定义布局RemoteViews支持充电/耗电切换)
*/
private void initBatteryRemindRemoteViews(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【布局初始化】开始初始化电量提醒布局,提醒类型:" + msg.getRemindMSG());
mBatteryRemindRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
mBatteryRemindRemoteViews.setTextViewText(R.id.viewremindnotificationTextView1, msg.getTitle());
mBatteryRemindRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
// 切换布局(+:充电提醒,-:耗电提醒)
String remindType = msg.getRemindMSG() != null ? msg.getRemindMSG().trim() : "";
if ("+".equals(remindType)) {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:充电提醒");
} else if ("-".equals(remindType)) {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:耗电提醒");
} else {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
LogUtils.w(TAG, "【布局初始化】未知电量提醒类型:" + remindType);
}
}
/**
* 初始化电量提醒通知仅构建不发送配合update触发提醒
*/
public void initBatteryRemindNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【电量提醒通知】开始初始化提醒通知,标题:" + msg.getTitle());
if (service == null || msg == null) {
LogUtils.e(TAG, "【电量提醒通知】初始化失败Service/NotificationMessage为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(service, CHANNEL_ID_BATTERY_REMIND);
} else {
builder = new Notification.Builder(service);
}
mBatteryRemindNotify = builder
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentTitle(msg.getTitle())
.setContentText(msg.getContent())
.setWhen(System.currentTimeMillis())
.setColor(Color.parseColor("#F00606"))
.setContentIntent(pendingIntent)
.setAutoCancel(true) // 点击取消
.build();
// 3. 初始化自定义布局
initBatteryRemindRemoteViews(service, msg);
mBatteryRemindNotify.contentView = mBatteryRemindRemoteViews;
mBatteryRemindNotify.bigContentView = mBatteryRemindRemoteViews;
LogUtils.d(TAG, "【电量提醒通知】初始化完成");
}
/**
* 发送/更新电量提醒通知(初始化后调用,触发强提醒)
*/
public void sendOrUpdateBatteryRemindNotify() {
LogUtils.d(TAG, "【电量提醒通知】开始发送/更新提醒");
if (mBatteryRemindNotify == null || mBatteryRemindRemoteViews == null) {
LogUtils.e(TAG, "【电量提醒通知】发送失败通知未初始化先调用initBatteryRemindNotify");
return;
}
mNotificationManager.notify(NOTIFY_ID_BATTERY_REMIND, mBatteryRemindNotify);
LogUtils.d(TAG, "【电量提醒通知】发送/更新成功通知ID" + NOTIFY_ID_BATTERY_REMIND);
}
/**
* 单独取消电量提醒通知(静态方法,外部可直接调用,无需实例化)
*/
public static void cancelBatteryRemindNotify(Context context) {
LogUtils.d(TAG, "【电量提醒通知】开始取消提醒通知ID" + NOTIFY_ID_BATTERY_REMIND);
if (context == null) {
LogUtils.e(TAG, "【电量提醒通知】取消失败Context为空");
return;
}
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFY_ID_BATTERY_REMIND);
LogUtils.d(TAG, "【电量提醒通知】取消成功");
}
// ====================== 场景3通用临时通知简单文本自动取消无需自定义布局======================
/**
* 显示通用临时通知普通告警仅震动自动取消统一跳转MainActivity
* @param title 通知标题
* @param content 通知内容
*/
public void showTempAlertNotify(String title, String content) {
LogUtils.d(TAG, "【通用临时通知】开始构建,标题:" + title + ",内容:" + content);
if (title == null || content == null) {
LogUtils.e(TAG, "【通用临时通知】构建失败:标题/内容为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 用NotificationCompat.Builder兼容所有版本简化逻辑
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
.setContentTitle(title)
.setContentText(content)
.setWhen(System.currentTimeMillis())
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setVibrate(VIBRATE_PATTERN);
// 3. 发送通知
Notification notification = builder.build();
mNotificationManager.notify(NOTIFY_ID_TEMP_ALERT, notification);
LogUtils.d(TAG, "【通用临时通知】显示成功通知ID" + NOTIFY_ID_TEMP_ALERT);
}
// ====================== 场景4自定义布局通知灵活扩展支持复杂样式======================
/**
* 显示自定义布局通知(支持普通布局+大布局通用所有场景统一跳转MainActivity
* @param contentView 普通自定义布局(必填)
* @param bigContentView 下拉大布局(可选)
*/
public void showCustomLayoutNotify(RemoteViews contentView, RemoteViews bigContentView) {
LogUtils.d(TAG, "【自定义布局通知】开始构建布局ID" + (contentView != null ? contentView.getLayoutId() : null));
if (contentView == null) {
LogUtils.e(TAG, "【自定义布局通知】构建失败普通布局contentView为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建自定义布局通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher) // 必传,不可省略
.setContentIntent(pendingIntent)
.setContent(contentView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
// 添加大布局(可选)
if (bigContentView != null) {
builder.setCustomBigContentView(bigContentView);
LogUtils.d(TAG, "【自定义布局通知】已添加下拉大布局布局ID" + bigContentView.getLayoutId());
}
// 3. 发送通知
Notification notification = builder.build();
mNotificationManager.notify(NOTIFY_ID_CUSTOM_LAYOUT, notification);
LogUtils.d(TAG, "【自定义布局通知】显示成功通知ID" + NOTIFY_ID_CUSTOM_LAYOUT);
}
// ====================== 通知取消工具(支持精准取消/全取消)======================
/**
* 取消指定ID的通知精准取消灵活控制
*/
public void cancelNotifyById(int notifyId) {
LogUtils.d(TAG, "【通知管理】开始取消通知ID" + notifyId);
mNotificationManager.cancel(notifyId);
LogUtils.d(TAG, "【通知管理】通知取消成功ID" + notifyId);
}
/**
* 取消所有通知(谨慎使用,会清除所有场景的通知)
*/
public void cancelAllNotifies() {
LogUtils.d(TAG, "【通知管理】开始取消所有通知");
mNotificationManager.cancelAll();
LogUtils.d(TAG, "【通知管理】所有通知取消完成");
}
}

View File

@@ -1,247 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/*
* 参考:
* https://blog.csdn.net/qq_35507234/article/details/90676587
* https://blog.csdn.net/qq_16628781/article/details/51548324
*/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.os.Build;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
public class NotificationUtils2 {
public static final String TAG = NotificationHelper.class.getSimpleName();
Context mContext;
NotificationManager mNotificationManager;
Notification mForegroundNotification;
PendingIntent mForegroundPendingIntent;
Notification mRemindNotification;
PendingIntent mRemindPendingIntent;
RemoteViews mrvServiceNotificationView;
RemoteViews mrvRemindNotificationView;
static enum NotificationType { MIN, MAX };
private static int _mnServiceNotificationID = 1;
private static int _mnRemindNotificationID = 2;
private static String _mszChannelIDService = "1";
private static String _mszChannelNameService = "Service";
private static String _mszChannelIDRemind = "2";
private static String _mszChannelNameRemind = "Remind";
// public NotificationUtils(Context context) {
// mContext = context;
// mNotificationManager = (NotificationManager) context.getSystemService(
// Context.NOTIFICATION_SERVICE);
// }
public NotificationUtils2(Context context) {
mContext = context;
mNotificationManager = context.getSystemService(NotificationManager.class);
//createNotificationChannels();
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createServiceChannel();
createRemindChannel();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createServiceChannel() {
NotificationChannel channel = new NotificationChannel(
_mszChannelIDService,
_mszChannelNameService,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Background service updates");
channel.setSound(null, null);
channel.enableVibration(false);
mNotificationManager.createNotificationChannel(channel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createRemindChannel() {
NotificationChannel channel = new NotificationChannel(
_mszChannelIDRemind,
_mszChannelNameRemind,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Critical reminders");
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
channel.setBypassDnd(true);
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
mNotificationManager.createNotificationChannel(channel);
}
// 创建并发送服务通知
//
public void createForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//创建Notification传入Context和channelId
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
intent.setPackage(service.getPackageName());
//LogUtils.d(TAG, "mService.getPackageName() : " + service.getPackageName());
intent.setClass(service, MainActivity.class);
//LogUtils.d(TAG, "MainActivity.class.getName() : " + MainActivity.class.getName());
//这里放一个count用来区分每一个通知
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
//参数1:context 上下文对象
//参数2:发送者私有的请求码(Private request code for the sender)
//参数3:intent 意图对象
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
//mForegroundPendingIntent = PendingIntent.getActivity(mService, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mForegroundPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mForegroundPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_IMMUTABLE);
}
mForegroundNotification = new Notification.Builder(service, _mszChannelIDService)
.setAutoCancel(true)
.setContentTitle(notificationMessage.getTitle())
.setContentText(notificationMessage.getContent())
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_launcher)
//设置红色
.setColor(Color.parseColor("#F00606"))
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentIntent(mForegroundPendingIntent)
.build();
setForegroundNotificationRemoteViews(service, notificationMessage);
service.startForeground(_mnServiceNotificationID, mForegroundNotification);
}
void initmrvRemindNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
mrvRemindNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
mrvRemindNotificationView.setTextViewText(R.id.viewremindnotificationTextView1, notificationMessage.getTitle());
String szRemindMSG = notificationMessage.getRemindMSG();
//LogUtils.d(TAG, "szRemindMSG : " + szRemindMSG);
//mrvRemindNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
if (szRemindMSG != null) {
if (szRemindMSG.trim().equals("-")) {
//LogUtils.d(TAG, "-");
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
} else if (szRemindMSG.trim().equals("+")) {
//LogUtils.d(TAG, "+");
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.GONE);
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
}
mrvRemindNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
//给我remoteViews上的控件tv_content添加监听事件
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
//return mrvServiceNotificationView;
}
}
void initmrvServiceNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
mrvServiceNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView1, notificationMessage.getTitle());
//String szRemindMSG = notificationMessage.getRemindMSG();
//mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
//rvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent() + Integer.toString(nTest));
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent());
mrvServiceNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
//给我remoteViews上的控件tv_content添加监听事件
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
//return mrvServiceNotificationView;
}
void setForegroundNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
initmrvServiceNotificationView(service, notificationMessage);
mForegroundNotification.contentView = mrvServiceNotificationView;
mForegroundNotification.bigContentView = mrvServiceNotificationView;
}
void setRemindNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
initmrvRemindNotificationView(service, notificationMessage);
mRemindNotification.contentView = mrvRemindNotificationView;
mRemindNotification.bigContentView = mrvRemindNotificationView;
}
// 更新服务通知
//
public void updateForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
setForegroundNotificationRemoteViews(service, notificationMessage);
mNotificationManager.notify(_mnServiceNotificationID, mForegroundNotification);
}
// 创建并发送电量提醒通知
//
public void updateRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//LogUtils.d(TAG, "updateRemindNotification : " + notificationMessage.getRemindMSG());
setRemindNotificationRemoteViews(service, notificationMessage);
mNotificationManager.notify(_mnRemindNotificationID, mRemindNotification);
}
public void createRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
//创建Notification传入Context和channelId
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
intent.setPackage(service.getPackageName());
intent.setClass(service, MainActivity.class);
//这里放一个count用来区分每一个通知
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
//参数1:context 上下文对象
//参数2:发送者私有的请求码(Private request code for the sender)
//参数3:intent 意图对象
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
//mRemindPendingIntent = PendingIntent.getActivity(mService, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mRemindPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mRemindPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_IMMUTABLE);
}
mRemindNotification = new Notification.Builder(service, _mszChannelIDRemind)
.setAutoCancel(true)
.setContentTitle(notificationMessage.getTitle())
.setContentText(notificationMessage.getContent())
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_launcher)
//设置红色
.setColor(Color.parseColor("#F00606"))
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentIntent(mRemindPendingIntent)
.build();
setRemindNotificationRemoteViews(service, notificationMessage);
}
public static void cancelRemindNotification(Context context){
// 获取 NotificationManager 实例
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 撤回指定 ID 的通知栏消息
notificationManager.cancel(_mnRemindNotificationID);
}
}

View File

@@ -2,213 +2,349 @@ package cc.winboll.studio.powerbell.utils;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import java.util.ArrayList;
import java.util.List;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/01 16:05 * @Date 2025/12/14 03:05
* @Describe 权限申请工具类(单例 * @Describe 权限申请工具类(Java7兼容版
* 核心特性: * 适配 小米手机+API29-30整合自启动、电池优化、全文件管理权限专注后台保活核心权限
* 1. 适配全Android版本6.0+ 动态权限 / 13+ 兼容)
* 2. 支持多包名场景(无硬编码包名)
* 3. 统一权限校验、申请、回调处理
* 4. 自带用户引导(拒绝权限+不再询问场景)
*/ */
public class PermissionUtils { public class PermissionUtils {
public static final String TAG = "PermissionUtils"; // ====================== 常量定义(首屏可见,统一管理,避免冲突)======================
// 存储权限请求码与Activity保持一致避免冲突 // 日志标签
public static final int STORAGE_PERMISSION_REQUEST2 = 100; public static final String TAG = "PermissionUtils";
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101; // 权限请求码(按场景分段,避免重复)
public static final int REQUEST_IGNORE_BATTERY_OPTIMIZATION = 1000; // 电池优化权限
public static final int REQUEST_AUTO_START = 1001; // 自启动权限(小米专属)
public static final int REQUEST_ALL_FILE_MANAGE = 1002; // 全文件管理权限API30+
// SDK版本常量适配API29-30替代系统枚举Java7兼容
private static final int SDK_VERSION_Q = 29; // Android 10API29
private static final int SDK_VERSION_R = 30; // Android 11API30
// 小米自启动权限页面配置(专属跳转路径,精准适配)
private static final String XIAOMI_AUTO_START_PACKAGE = "com.miui.securitycenter";
private static final String XIAOMI_AUTO_START_CLASS = "com.miui.permcenter.autostart.AutoStartManagementActivity";
// 单例实例(双重校验锁,线程安全) // ====================== 单例模式Java7标准双重校验锁线程安全+懒加载)======================
private static volatile PermissionUtils sInstance; private static volatile PermissionUtils sInstance;
// 私有构造(禁止外部实例化) private PermissionUtils() {}
private PermissionUtils() {}
/** public static PermissionUtils getInstance() {
* 获取单例实例(适配多包名,无硬编码) if (sInstance == null) {
*/ synchronized (PermissionUtils.class) {
public static PermissionUtils getInstance() { if (sInstance == null) {
if (sInstance == null) { sInstance = new PermissionUtils();
synchronized (PermissionUtils.class) { LogUtils.d(TAG, "初始化PermissionUtils 单例创建成功");
if (sInstance == null) { }
sInstance = new PermissionUtils(); }
} }
} return sInstance;
} }
return sInstance;
}
// ======================================== 存储权限核心方法 ======================================== // ====================== 核心权限1全文件管理权限API29-30适配通用所有机型======================
/** /**
* 检查并申请存储权限统一入口适配全Android版本 * 检查全文件管理权限适配API30+ MANAGE_EXTERNAL_STORAGE兼容API29-旧权限
* @param activity 上下文(用于权限申请和弹窗 * @param activity 上下文Activity不可为null
* @return true权限已全部获取false需要申请权限 * @return true=权限已授予false=权限未授予
*/ */
public boolean checkAndRequestStoragePermission(Activity activity) { public boolean checkAllFileManagePermission(Activity activity) {
if (activity == null || activity.isFinishing()) { LogUtils.d(TAG, "全文件权限-检查:开始校验,系统版本=" + Build.VERSION.SDK_INT);
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁权限检查失败"); if (activity == null) {
return false; LogUtils.e(TAG, "全文件权限-检查失败Activity为空");
} return false;
LogUtils.d(TAG, "【权限检查】开始检查存储权限Android版本" + Build.VERSION.SDK_INT); }
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE适配所有版本避免READ_MEDIA_IMAGES找不到符号 // API30+:校验 MANAGE_EXTERNAL_STORAGE 特殊权限
String[] requiredPermissions = { if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
android.Manifest.permission.WRITE_EXTERNAL_STORAGE, boolean hasManagePerm = Environment.isExternalStorageManager();
android.Manifest.permission.READ_EXTERNAL_STORAGE LogUtils.d(TAG, "全文件权限-检查API30+MANAGE_EXTERNAL_STORAGE权限=" + (hasManagePerm ? "已授予" : "未授予"));
}; return hasManagePerm;
} else if (Build.VERSION.SDK_INT == SDK_VERSION_Q) {
LogUtils.d(TAG, "全文件权限-检查API29无需申请默认支持文件管理");
return true;
} else {
boolean hasWritePerm = ContextCompat.checkSelfPermission(activity,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
LogUtils.d(TAG, "全文件权限-检查API29以下WRITE_EXTERNAL_STORAGE权限=" + (hasWritePerm ? "已授予" : "未授予"));
return hasWritePerm;
}
}
// 筛选未授予的权限 /**
List<String> needPermissions = new ArrayList<>(); * 申请全文件管理权限适配API30+特殊权限流程兼容API29-旧权限申请)
for (String permission : requiredPermissions) { * @param activity 申请权限的Activity不可为null
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { */
needPermissions.add(permission); public void requestAllFileManagePermission(Activity activity) {
} LogUtils.d(TAG, "全文件权限-申请:开始处理,系统版本=" + Build.VERSION.SDK_INT);
} if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "全文件权限-申请失败Activity无效/已销毁");
return;
}
// 无权限需要申请:触发动态申请 // 先检查权限,已授予直接返回
if (!needPermissions.isEmpty()) { if (checkAllFileManagePermission(activity)) {
String[] permissionsArr = needPermissions.toArray(new String[0]); LogUtils.d(TAG, "全文件权限-申请:已拥有权限,无需发起");
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2); return;
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr)); }
return false;
}
// 所有权限已授予 // API30+:跳转系统特殊权限申请页(用户手动授权)
LogUtils.d(TAG, "【权限检查】存储权限已全部获取"); if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
return true; try {
} Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
LogUtils.d(TAG, "全文件权限-申请API30+,跳转特殊权限申请页");
} catch (Exception e) {
// 备用跳转:系统设置首页,引导手动操作
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
LogUtils.w(TAG, "全文件权限-申请:跳转失败,引导手动开启");
showAllFileManageTipsDialog(activity);
}
} else {
ActivityCompat.requestPermissions(activity,
new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_ALL_FILE_MANAGE);
LogUtils.d(TAG, "全文件权限-申请API29以下发起WRITE_EXTERNAL_STORAGE权限申请");
}
}
/** // ====================== 核心权限2自启动权限小米专属API29-30适配======================
* 处理存储权限申请回调统一逻辑无需在Activity中重复编写 /**
* @param activity 上下文 * 检查自启动权限(仅小米机型需要,非小米直接返回无需申请)
* @param requestCode 请求码匹配STORAGE_PERMISSION_REQUEST * @param activity 上下文Activity不可为null
* @param permissions 申请的权限数组 * @return true=小米机型需手动开启false=非小米机型(无需申请)
* @param grantResults 权限授予结果数组 */
* @return true回调已处理false非当前工具类的权限回调 // public boolean checkAutoStartPermission(Activity activity) {
*/ // LogUtils.d(TAG, "自启动权限-检查:开始,设备品牌=" + Build.BRAND);
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) { // if (activity == null) {
// 过滤非存储权限回调 // LogUtils.e(TAG, "自启动权限-检查失败Activity为空");
if (requestCode != STORAGE_PERMISSION_REQUEST2) { // return false;
return false; // }
} //
LogUtils.d(TAG, "【权限回调】处理存储权限回调requestCode" + requestCode); // boolean isXiaomi = Build.BRAND.toLowerCase().contains("xiaomi");
// LogUtils.d(TAG, "自启动权限-检查:结果=" + (isXiaomi ? "小米机型(需开启)" : "非小米机型(无需申请)"));
// return isXiaomi;
// }
if (activity == null || activity.isFinishing()) { /**
LogUtils.e(TAG, "【权限回调】Activity为空或已销毁回调处理终止"); * 请求自启动权限小米专属多方案跳转适配API29-30机型差异
return true; * @param activity 申请权限的Activity不可为null
} */
public void requestAutoStartPermission(Activity activity) {
LogUtils.d(TAG, "自启动权限-申请:开始处理");
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "自启动权限-申请失败Activity无效/已销毁");
return;
}
// 校验所有权限是否授予 // 非小米机型,直接返回
boolean allGranted = true; // if (!checkAutoStartPermission(activity)) {
for (int grantResult : grantResults) { // LogUtils.d(TAG, "自启动权限-申请:非小米机型,无需处理");
if (grantResult != PackageManager.PERMISSION_GRANTED) { // return;
allGranted = false; // }
break;
}
}
if (allGranted) { // API30+ 小米:优先精准跳转自启动管理页
// 全部授予:提示用户重新操作 if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
ToastUtils.show(activity.getString(R.string.permission_grant_success)); try {
LogUtils.d(TAG, "【权限回调】所有存储权限已授予"); // 方案1组件名精准跳转成功率最高
} else { Intent intent = new Intent();
// 部分/全部拒绝:判断是否勾选“不再询问” intent.setComponent(new ComponentName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS));
boolean shouldShowRationale = false; activity.startActivityForResult(intent, REQUEST_AUTO_START);
for (String permission : permissions) { LogUtils.d(TAG, "自启动权限-申请API30+,组件名跳转自启动管理页");
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { } catch (Exception e1) {
shouldShowRationale = true; try {
break; // 方案2Action备用跳转兼容机型差异
} Intent intent = new Intent("miui.intent.action.OP_AUTO_START");
} intent.setClassName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-申请API30+Action跳转自启动管理页");
} catch (Exception e2) {
// 方案3终极备用跳转系统设置+提示
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.w(TAG, "自启动权限-申请:跳转失败,引导手动操作");
showAutoStartTipsDialog(activity);
}
}
return;
}
if (shouldShowRationale) { // API29 小米:低版本兼容跳转
// 未勾选“不再询问”:弹窗引导重新申请 try {
showPermissionRationaleDialog(activity); Intent intent = new Intent(XIAOMI_AUTO_START_CLASS);
} else { intent.setPackage(XIAOMI_AUTO_START_PACKAGE);
// 已勾选“不再询问”:引导用户去设置页开启 activity.startActivityForResult(intent, REQUEST_AUTO_START);
showPermissionSettingDialog(activity); LogUtils.d(TAG, "自启动权限-申请API29低版本跳转自启动管理页");
} } catch (Exception e) {
LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale); Intent intent = new Intent(Settings.ACTION_SETTINGS);
} activity.startActivityForResult(intent, REQUEST_AUTO_START);
return true; showAutoStartTipsDialog(activity);
} }
}
// ======================================== 辅助方法(私有,封装细节) ======================================== // ====================== 核心权限3电池优化权限通用所有机型API29-30适配======================
/** /**
* 弹窗:未勾选“不再询问”时,提示用户授予权限 * 检查忽略电池优化权限精准判断API23+有效,低版本视为已拥有)
*/ * @param activity 上下文Activity不可为null
private void showPermissionRationaleDialog(final Activity activity) { * @return true=已忽略优化false=未忽略(需申请)
new AlertDialog.Builder(activity) */
.setTitle(activity.getString(R.string.permission_title)) public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) {
.setMessage(activity.getString(R.string.permission_storage_rationale)) LogUtils.d(TAG, "电池优化权限-检查:开始,系统版本=" + Build.VERSION.SDK_INT);
.setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() { if (activity == null) {
LogUtils.e(TAG, "电池优化权限-检查失败Activity为空");
return false;
}
// API23以下无此权限视为已拥有
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
LogUtils.d(TAG, "电池优化权限-检查API23以下无需校验视为已拥有");
return true;
}
// API23+ 精准校验权限状态
PowerManager powerManager = (PowerManager) activity.getSystemService(Activity.POWER_SERVICE);
if (powerManager == null) {
LogUtils.e(TAG, "电池优化权限-检查获取PowerManager失败校验异常");
return false;
}
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());
LogUtils.d(TAG, "电池优化权限-检查:结果=" + (isIgnored ? "已忽略优化" : "未忽略(需申请)"));
return isIgnored;
}
/**
* 请求忽略电池优化权限多方案跳转适配API29-30自动判断是否需要申请
* @param activity 申请权限的Activity不可为null
*/
public void requestIgnoreBatteryOptimizationPermission(Activity activity) {
LogUtils.d(TAG, "电池优化权限-申请:开始处理");
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "电池优化权限-申请失败Activity无效/已销毁");
return;
}
// 已拥有权限,直接返回
if (checkIgnoreBatteryOptimizationPermission(activity)) {
LogUtils.d(TAG, "电池优化权限-申请:已拥有权限,无需发起");
return;
}
try {
// 方案1直接跳转一键授权页优先使用用户操作简单
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.d(TAG, "电池优化权限-申请:跳转一键授权页");
} catch (Exception e) {
// 方案2备用跳转优化管理页+提示
Intent intent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.w(TAG, "电池优化权限-申请:跳转失败,引导手动操作");
showBatteryOptTipsDialog(activity);
}
}
// ====================== 辅助方法:手动开启提示弹窗(适配跳转失败场景)======================
/**
* 全文件管理权限手动开启提示弹窗
*/
private void showAllFileManageTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("全文件管理权限申请提示")
.setMessage("请手动开启全文件管理权限,否则文件操作功能异常:\n1. 进入设置 → 应用 → 本应用 → 权限\n2. 找到「文件管理」/「存储」权限,开启「允许管理所有文件」")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
// 重新申请权限 dialog.dismiss();
checkAndRequestStoragePermission(activity);
} }
}) })
.setNegativeButton(activity.getString(R.string.cancel), null) .setCancelable(false)
.show(); .show();
} LogUtils.d(TAG, "全文件权限:显示手动开启提示弹窗");
}
/** /**
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限 * 自启动权限手动开启提示弹窗(小米专属)
*/ */
private void showPermissionSettingDialog(final Activity activity) { private void showAutoStartTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_denied_title)) .setTitle("自启动权限申请提示")
.setMessage(activity.getString(R.string.permission_storage_setting_guide)) .setMessage("请手动开启自启动权限,否则应用后台保活异常:\n1. 进入小米安全中心 → 应用管理 → 自启动管理\n2. 找到本应用,开启「允许自启动」开关")
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() { .setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
// 跳转应用设置页(适配多包名,动态获取当前包名) dialog.dismiss();
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
} }
}) })
.setNegativeButton(activity.getString(R.string.cancel), null) .setCancelable(false)
.show(); .show();
} LogUtils.d(TAG, "自启动权限:显示手动开启提示弹窗");
}
// ======================================== 扩展:其他权限方法(可选) ======================================== /**
/** * 电池优化权限手动开启提示弹窗
* 检查单个权限是否已授予(通用方法,可复用) */
*/ private void showBatteryOptTipsDialog(final Activity activity) {
public boolean isPermissionGranted(Activity activity, String permission) { new AlertDialog.Builder(activity)
if (activity == null || TextUtils.isEmpty(permission)) { .setTitle("电池优化权限申请提示")
return false; .setMessage("请手动忽略电池优化,否则应用后台运行被限制:\n1. 进入设置 → 电池 → 电池优化\n2. 找到本应用,选择「不优化」选项")
} .setPositiveButton("知道了", new DialogInterface.OnClickListener() {
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED; @Override
} public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
LogUtils.d(TAG, "电池优化权限:显示手动开启提示弹窗");
}
/** public void startPermissionRequest(final Activity activity) {
* 申请单个权限(通用方法,可复用) // 电池优化权限(通用所有机型)
*/ if (!checkIgnoreBatteryOptimizationPermission(activity)) {
public void requestSinglePermission(Activity activity, String permission, int requestCode) { YesNoAlertDialog.show(activity, activity.getString(R.string.app_name) + "权限申请提示:", "本应用要正常使用,需要申请电池优化与自启动权限。是否进入权限设置步骤?", new YesNoAlertDialog.OnDialogResultListener(){
if (activity == null || TextUtils.isEmpty(permission)) { @Override
return; public void onNo() {
ToastUtils.show(activity.getString(R.string.app_name) + "应用可能无法正常使用。");
}
@Override
public void onYes() {
requestIgnoreBatteryOptimizationPermission(activity);
}
});
} }
if (!isPermissionGranted(activity, permission)) { }
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
public void handlePermissionRequest(final Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == PermissionUtils.REQUEST_IGNORE_BATTERY_OPTIMIZATION) {
// 自启动权限(小米专属)
// 小米机型,发起自启动权限申请
requestAutoStartPermission(activity);
} else if (requestCode == PermissionUtils.REQUEST_AUTO_START) {
// 自启动权限(小米专属)
if (App.isDebugging() && !checkAllFileManagePermission(activity)) {
// 小米机型,发起自启动权限申请
requestAllFileManagePermission(activity);
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import cc.winboll.studio.powerbell.model.BatteryInfoBean; import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList; import java.util.ArrayList;
public class StringUtils { public class StringUtils {

View File

@@ -1,145 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/28 04:23:04
* @Describe UriUtil
*/
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class UriUtil {
public static final String TAG = "UriUtil";
/**
* 获取真实路径
*
* @param context
*/
public static String getFilePathFromUri(Context context, Uri uri) {
if (uri == null) {
return null;
}
switch (uri.getScheme()) {
case ContentResolver.SCHEME_CONTENT:
//Android7.0之后的uri content:// URI
return getFilePathFromContentUri(context, uri);
case ContentResolver.SCHEME_FILE:
default:
//Android7.0之前的uri file://
return new File(uri.getPath()).getAbsolutePath();
}
}
/**
* 从uri获取path
*
* @param uri content://media/external/file/109009
* <p>
* FileProvider适配
* content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/
* content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
if (null == uri) return null;
String data = null;
String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
} else {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
data = getPathFromInputStreamUri(context, uri, fileName);
}
}
cursor.close();
}
return data;
}
/**
* 用流拷贝文件一份到自己APP私有目录下
*
* @param context
* @param uri
* @param fileName
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
InputStream inputStream = null;
String filePath = null;
if (uri.getAuthority() != null) {
try {
inputStream = context.getContentResolver().openInputStream(uri);
File file = createTemporalFileFrom(context, inputStream, fileName);
filePath = file.getPath();
} catch (Exception e) {
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
}
}
}
return filePath;
}
public static Uri getUriForFile(Context context, File file) {
//Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
}
return Uri.fromFile(file);
}
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
throws IOException {
File targetFile = null;
if (inputStream != null) {
int read;
byte[] buffer = new byte[8 * 1024];
//自己定义拷贝文件路径
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
outputStream.flush();
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return targetFile;
}
}

View File

@@ -0,0 +1,481 @@
package cc.winboll.studio.powerbell.utils;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* Uri 工具类Java7兼容适配API29-30+小米机型FileProvider安全适配
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/28
*/
public class UriUtils {
// ====================== 常量定义(顶部统一管理)======================
public static final String TAG = "UriUtils";
// FileProvider 授权后缀与Manifest配置保持一致
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 应用公共图片目录API29+ 适配替代废弃API
private static final String APP_PUBLIC_PIC_DIR = "PowerBell/";
// MIME类型与文件后缀映射表覆盖常见格式小米机型精准匹配
private static final Map<String, String> MIME_SUFFIX_MAP = new HashMap<String, String>() {{
// 图片格式(重点,含透明格式)
put("image/png", "png");
put("image/jpeg", "jpg");
put("image/jpg", "jpg");
put("image/gif", "gif");
put("image/bmp", "bmp");
put("image/webp", "webp");
// 音视频格式
put("video/mp4", "mp4");
put("video/avi", "avi");
put("video/mkv", "mkv");
put("audio/mp3", "mp3");
put("audio/wav", "wav");
// 文档格式
put("application/pdf", "pdf");
put("application/msword", "doc");
put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
put("application/vnd.ms-excel", "xls");
put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
}};
// ====================== 新增核心方法Uri 转文件后缀 ======================
/**
* 【静态公共方法】根据 Uri 获取文件真实后缀优先MIME类型匹配适配所有Uri场景+小米机型)
* @param context 上下文非空用于获取ContentResolver
* @param uri 待解析 Uri支持 content:// / file:// 双Scheme
* @return 小写文件后缀(如 png/jpg/mp4无匹配返回空字符串
*/
public static String getSuffixFromUri(Context context, Uri uri) {
LogUtils.d(TAG, "=== getSuffixFromUri 调用 startUri" + (uri != null ? uri.toString() : "null") + " ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getSuffixFromUriContext 为空,获取失败");
return "";
}
if (uri == null) {
LogUtils.e(TAG, "getSuffixFromUriUri 为空,获取失败");
return "";
}
String suffix = "";
String scheme = uri.getScheme();
// 2. 按 Uri Scheme 分类处理(优先精准匹配,再降级截取)
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
// 场景1content:// Uri优先通过MIME类型获取最精准
suffix = getSuffixFromContentUri(context, uri);
LogUtils.d(TAG, "getSuffixFromUricontent:// UriMIME匹配后缀" + suffix);
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
// 场景2file:// Uri直接解析文件名截取后缀
String filePath = new File(uri.getPath()).getAbsolutePath();
suffix = getSuffixFromFilePath(filePath);
LogUtils.d(TAG, "getSuffixFromUrifile:// Uri路径截取后缀" + suffix);
} else {
// 场景3未知Scheme尝试解析Uri路径截取兜底
String uriPath = uri.getPath();
suffix = uriPath != null ? getSuffixFromFilePath(uriPath) : "";
LogUtils.w(TAG, "getSuffixFromUri未知Scheme=" + scheme + ",兜底截取后缀:" + suffix);
}
// 3. 最终结果处理(统一小写,去空)
suffix = suffix != null ? suffix.trim().toLowerCase() : "";
LogUtils.d(TAG, "=== getSuffixFromUri 调用 end最终后缀" + suffix + " ===");
return suffix;
}
// ====================== 公有核心方法(对外提供能力,按功能排序)======================
/**
* Uri 转真实文件路径(核心方法,适配 content:// / file:// 双 Scheme
* @param context 上下文(非空)
* @param uri 待转换 Uri非空
* @return 真实文件绝对路径(转换失败返回 null
*/
public static String getFilePathFromUri(Context context, Uri uri) {
LogUtils.d(TAG, "=== getFilePathFromUri 调用 start ===");
if (context == null) {
LogUtils.e(TAG, "getFilePathFromUriContext 为空,转换失败");
return null;
}
if (uri == null) {
LogUtils.e(TAG, "getFilePathFromUriUri 为空,转换失败");
return null;
}
String scheme = uri.getScheme();
String filePath = null;
// 按 Uri Scheme 分类处理
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
LogUtils.d(TAG, "getFilePathFromUriScheme=content执行ContentUri转换");
filePath = getFilePathFromContentUri(context, uri);
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
LogUtils.d(TAG, "getFilePathFromUriScheme=file直接转换路径");
filePath = new File(uri.getPath()).getAbsolutePath();
} else {
LogUtils.w(TAG, "getFilePathFromUri未知Scheme=" + scheme + ",转换失败");
}
LogUtils.d(TAG, "=== getFilePathFromUri 调用 end结果" + filePath + " ===");
return filePath;
}
/**
* 文件路径转 Uri核心方法适配 Android7.0+ FileProviderAPI29-30兼容
* @param context 上下文(非空)
* @param filePath 真实文件路径(非空)
* @return 安全 Uri转换失败返回 null
*/
public static Uri getUriForFile(Context context, String filePath) {
LogUtils.d(TAG, "=== getUriForFile路径版调用 start ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getUriForFileContext 为空,转换失败");
return null;
}
if (filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "getUriForFile文件路径为空转换失败");
return null;
}
// 2. File 对象初始化与校验
File file = new File(filePath);
LogUtils.d(TAG, "getUriForFile文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 3. 合法路径校验适配小米机型避免FileProvider配置外路径
if (!isPathInValidDir(context, file)) {
LogUtils.w(TAG, "getUriForFile路径不在安全配置目录内小米机型可能出现权限异常");
}
// 4. 调用重载方法生成 Uri
Uri uri = getUriForFile(context, file);
LogUtils.d(TAG, "=== getUriForFile路径版调用 end结果" + (uri != null ? uri.toString() : "null") + " ===");
return uri;
}
/**
* File 对象转 Uri重载方法直接接收File内部安全适配
* @param context 上下文(非空)
* @param file 待转换 File 对象(非空)
* @return 安全 Uri转换失败返回 null
*/
public static Uri getUriForFile(Context context, File file) {
LogUtils.d(TAG, "=== getUriForFileFile版调用 start ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getUriForFileContext 为空,转换失败");
return null;
}
if (file == null) {
LogUtils.e(TAG, "getUriForFileFile 对象为空,转换失败");
return null;
}
LogUtils.d(TAG, "getUriForFile文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 2. 按系统版本生成 UriAPI24+ 强制 FileProvider适配小米机型
Uri uri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LogUtils.d(TAG, "getUriForFileAndroid7.0+使用FileProvider生成Uri");
String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX;
LogUtils.d(TAG, "getUriForFileFileProvider Authority=" + authority);
try {
uri = FileProvider.getUriForFile(context, authority, file);
LogUtils.d(TAG, "getUriForFileContent Uri生成成功=" + uri.toString());
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "getUriForFileFileProvider生成失败小米机型常见原因路径未配置/Authority不匹配", e);
}
} else {
LogUtils.d(TAG, "getUriForFileAndroid7.0以下使用Uri.fromFile生成");
uri = Uri.fromFile(file);
LogUtils.d(TAG, "getUriForFileFile Uri生成成功=" + uri.toString());
}
LogUtils.d(TAG, "=== getUriForFileFile版调用 end ===");
return uri;
}
// ====================== 私有辅助方法(内部逻辑封装,不对外暴露)======================
/**
* ContentUri 转真实路径(适配 content:// 格式处理小米机型特殊Uri
* @param context 上下文
* @param uri ContentUricontent://media/external/file/xxx
* @return 真实文件路径(失败返回 null
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
LogUtils.d(TAG, "getFilePathFromContentUriUri=" + uri.toString());
String filePath = null;
Cursor cursor = null;
// Java7 语法try-catch-finally 手动关闭Cursor避免内存泄漏
try {
// 查询字段:优先 DATA 字段,失败则通过文件名+流拷贝获取
String[] queryColumns = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
// 优先读取 DATA 字段(直接获取路径)
int dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (dataIndex != -1) {
filePath = cursor.getString(dataIndex);
LogUtils.d(TAG, "getFilePathFromContentUri从DATA字段获取路径=" + filePath);
} else {
// DATA 字段为空,通过流拷贝到私有目录获取路径(小米机型特殊场景适配)
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
LogUtils.d(TAG, "getFilePathFromContentUriDATA字段为空通过流拷贝获取文件名=" + fileName);
filePath = getPathFromInputStreamUri(context, uri, fileName);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "getFilePathFromContentUri查询失败", e);
} finally {
// 强制关闭Cursor避免资源泄漏Java7 必须手动处理)
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
LogUtils.e(TAG, "getFilePathFromContentUri关闭Cursor失败", e);
}
}
}
return filePath;
}
/**
* 流拷贝获取路径(适配无 DATA 字段的 ContentUri小米机型特殊Uri兼容
* 将目标文件拷贝到应用私有缓存目录,返回拷贝后的路径
* @param context 上下文
* @param uri ContentUri
* @param fileName 文件名
* @return 拷贝后的文件路径(失败返回 null
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
LogUtils.d(TAG, "getPathFromInputStreamUri开始流拷贝文件名=" + fileName);
InputStream inputStream = null;
OutputStream outputStream = null;
File targetFile = null;
try {
// 1. 打开输入流读取Uri对应文件
inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) {
LogUtils.e(TAG, "getPathFromInputStreamUri输入流打开失败");
return null;
}
// 2. 创建目标文件(应用私有缓存目录,无权限限制)
targetFile = new File(context.getExternalCacheDir(), fileName);
// 若文件已存在,先删除(避免覆盖导致格式异常)
if (targetFile.exists()) {
boolean deleteSuccess = targetFile.delete();
LogUtils.d(TAG, "getPathFromInputStreamUri删除已存在文件结果=" + deleteSuccess);
}
// 3. 流拷贝Java7 手动处理流,避免 try-with-resources
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[8 * 1024]; // 8KB 缓冲区,平衡效率与内存
int readLength;
while ((readLength = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readLength);
}
outputStream.flush();
LogUtils.d(TAG, "getPathFromInputStreamUri流拷贝成功路径=" + targetFile.getAbsolutePath());
} catch (Exception e) {
LogUtils.e(TAG, "getPathFromInputStreamUri流拷贝失败", e);
// 拷贝失败,删除临时文件
if (targetFile != null && targetFile.exists()) {
targetFile.delete();
}
targetFile = null;
} finally {
// 强制关闭流避免资源泄漏Java7 必须手动关闭)
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
LogUtils.e(TAG, "getPathFromInputStreamUri关闭输出流失败", e);
}
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
LogUtils.e(TAG, "getPathFromInputStreamUri关闭输入流失败", e);
}
}
return targetFile != null ? targetFile.getAbsolutePath() : null;
}
/**
* 校验路径是否在安全目录内适配API29-30+小米机型避免FileProvider权限异常
* 仅允许:应用私有目录、缓存目录、应用专属公共目录
* @param context 上下文
* @param file 待校验文件
* @return true=安全路径false=非安全路径
*/
private static boolean isPathInValidDir(Context context, File file) {
String absolutePath = file.getAbsolutePath();
// 1. 应用外部私有目录API29+ 推荐,无权限限制)
String externalPrivateDir = context.getExternalFilesDir(null) != null
? context.getExternalFilesDir(null).getAbsolutePath()
: "";
// 2. 应用内部私有目录(无权限限制)
String internalPrivateDir = context.getFilesDir().getAbsolutePath();
// 3. 应用缓存目录(无权限限制)
String cacheDir = context.getCacheDir().getAbsolutePath();
// 4. 应用专属公共目录API29+ 适配,替代废弃的 getExternalStoragePublicDirectory
String appPublicDir = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + Environment.DIRECTORY_PICTURES
+ File.separator + APP_PUBLIC_PIC_DIR;
// 校验路径是否在安全目录内小米机型必须严格校验否则FileProvider会抛异常
boolean isInValidDir = absolutePath.startsWith(externalPrivateDir)
|| absolutePath.startsWith(internalPrivateDir)
|| absolutePath.startsWith(cacheDir)
|| absolutePath.startsWith(appPublicDir);
LogUtils.d(TAG, "isPathInValidDir外部私有目录=" + externalPrivateDir
+ ",公共目录=" + appPublicDir
+ ",校验结果=" + isInValidDir);
return isInValidDir;
}
/**
* 流拷贝创建临时文件(内部辅助,封装拷贝逻辑)
* @param context 上下文
* @param inputStream 输入流
* @param fileName 文件名
* @return 临时文件(失败返回 null
* @throws IOException 流操作异常
*/
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) throws IOException {
File targetFile = null;
if (inputStream != null) {
byte[] buffer = new byte[8 * 1024];
int readLength;
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);
while ((readLength = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readLength);
}
outputStream.flush();
outputStream.close();
}
return targetFile;
}
/**
* 辅助ContentUri 通过 MIME 类型获取后缀(精准匹配,不受文件名伪造影响)
* @param context 上下文
* @param uri ContentUri
* @return 匹配的后缀(无匹配返回空字符串)
*/
private static String getSuffixFromContentUri(Context context, Uri uri) {
String mime = null;
try {
// 通过 ContentResolver 获取 Uri 对应的 MIME 类型(系统级匹配,最精准)
mime = context.getContentResolver().getType(uri);
LogUtils.d(TAG, "getSuffixFromContentUri获取MIME类型=" + mime);
if (mime == null || mime.isEmpty()) {
// MIME 为空,尝试解析文件名兜底
String fileName = getFileNameFromContentUri(context, uri);
return getSuffixFromFilePath(fileName);
}
// MIME 类型匹配后缀(优先完全匹配,再模糊匹配)
if (MIME_SUFFIX_MAP.containsKey(mime)) {
return MIME_SUFFIX_MAP.get(mime);
}
// 模糊匹配(如 image/* 匹配通用图片后缀默认png
if (mime.startsWith("image/")) {
return "png";
} else if (mime.startsWith("video/")) {
return "mp4";
} else if (mime.startsWith("audio/")) {
return "mp3";
} else if (mime.startsWith("application/")) {
return "pdf";
}
} catch (Exception e) {
LogUtils.e(TAG, "getSuffixFromContentUriMIME解析失败mime=" + mime, e);
}
// 所有方式失败解析Uri路径兜底
return getSuffixFromFilePath(uri.getPath());
}
/**
* 辅助:从 ContentUri 获取文件名MIME 解析失败时兜底)
* @param context 上下文
* @param uri ContentUri
* @return 文件名(失败返回空字符串)
*/
private static String getFileNameFromContentUri(Context context, Uri uri) {
Cursor cursor = null;
try {
String[] queryColumns = {MediaStore.MediaColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
return cursor.getString(nameIndex);
}
} catch (Exception e) {
LogUtils.e(TAG, "getFileNameFromContentUri查询失败", e);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
LogUtils.e(TAG, "getFileNameFromContentUri关闭Cursor失败", e);
}
}
}
return "";
}
/**
* 辅助:从文件路径/文件名截取后缀(兜底方案,处理各种路径格式)
* @param path 文件路径/文件名
* @return 截取的后缀(无后缀返回空字符串)
*/
private static String getSuffixFromFilePath(String path) {
if (path == null || path.isEmpty()) {
return "";
}
// 处理路径中的分隔符(兼容 Windows/Android 路径格式)
path = path.replace("\\", "/");
// 取最后一个 "/" 后的文件名(避免路径包含 "." 导致误判)
int lastSepIndex = path.lastIndexOf("/");
if (lastSepIndex != -1 && lastSepIndex < path.length() - 1) {
path = path.substring(lastSepIndex + 1);
}
// 截取最后一个 "." 后的后缀(过滤无后缀/点开头/点结尾场景)
int lastDotIndex = path.lastIndexOf(".");
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == path.length() - 1) {
return "";
}
// 过滤后缀中的非法字符(仅保留字母/数字,避免特殊字符干扰)
String suffix = path.substring(lastDotIndex + 1).replaceAll("[^a-zA-Z0-9]", "");
// 限制后缀长度1-5位避免超长伪造后缀
return suffix.length() >= 1 && suffix.length() <= 5 ? suffix : "";
}
}

View File

@@ -4,15 +4,17 @@ import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ImageView.ScaleType; import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import java.io.File; import java.io.File;
/** /**
@@ -22,6 +24,8 @@ import java.io.File;
public class BackgroundView extends RelativeLayout { public class BackgroundView extends RelativeLayout {
public static final String TAG = "BackgroundView"; public static final String TAG = "BackgroundView";
// 新增:记录当前已缓存的图片路径
private String mCurrentCachedPath = "";
private Context mContext; private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout private LinearLayout mLlContainer; // 主容器LinearLayout
@@ -55,8 +59,6 @@ public class BackgroundView extends RelativeLayout {
LogUtils.d(TAG, "=== initView 启动 ==="); LogUtils.d(TAG, "=== initView 启动 ===");
// 1. 配置当前控件:全屏+透明 // 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setBackgroundColor(0x00000000);
setBackground(new ColorDrawable(0x00000000));
// 2. 初始化主容器LinearLayout // 2. 初始化主容器LinearLayout
initLinearLayout(); initLinearLayout();
@@ -64,8 +66,9 @@ public class BackgroundView extends RelativeLayout {
// 3. 初始化ImageView // 3. 初始化ImageView
initImageView(); initImageView();
// 4. 初始设置透明背景 // 初始设置透明背景
setDefaultTransparentBackground(); setDefaultTransparentBackground();
LogUtils.d(TAG, "=== initView 完成 ==="); LogUtils.d(TAG, "=== initView 完成 ===");
} }
@@ -100,22 +103,35 @@ public class BackgroundView extends RelativeLayout {
LogUtils.d(TAG, "=== initImageView 完成 ==="); LogUtils.d(TAG, "=== initImageView 完成 ===");
} }
public void loadBackgroundBean(BackgroundBean bean) { public void loadBackgroundBean(BackgroundBean bean) {
if(!bean.isUseBackgroundFile()) { loadBackgroundBean(bean, false);
setDefaultTransparentBackground(); }
public void loadBackgroundBean(BackgroundBean bean, boolean isRefresh) {
if (!bean.isUseBackgroundFile()) {
setDefaultTransparentBackground();
return;
}
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
if (!(new File(targetPath).exists())) {
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
return; return;
} }
if(bean.isUseBackgroundScaledCompressFile()) {
loadImage(bean.getBackgroundScaledCompressFilePath());
} else {
loadImage(bean.getBackgroundFilePath()); // 调用带路径判断的loadImage方法
if (isRefresh) {
App._mBitmapCacheUtils.removeCachedBitmap(targetPath);
App._mBitmapCacheUtils.cacheBitmap(targetPath);
} }
} loadImage(targetPath);
}
// ====================================== 对外方法 ====================================== // ====================================== 对外方法 ======================================
/** /**
* 加载图片保持原图比例在LinearLayout中居中平铺 * 改造后添加路径判断路径更新时同步更新缓存缓存Bitmap为null时提示并加载透明背景
* @param imagePath 图片绝对路径 * @param imagePath 图片绝对路径
*/ */
public void loadImage(String imagePath) { public void loadImage(String imagePath) {
@@ -132,22 +148,58 @@ public class BackgroundView extends RelativeLayout {
return; return;
} }
// 计算原图比例 mIvBackground.setVisibility(View.GONE);
// ======================== 新增:路径判断逻辑 ========================
// 1. 路径未变化:直接使用缓存
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App._mBitmapCacheUtils.getCachedBitmap(imagePath);
// 核心修改判断缓存Bitmap是否为null
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
mIvBackground.setVisibility(View.VISIBLE);
return;
} else {
// 缓存Bitmap为空或已回收提示并加载透明背景
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
ToastUtils.show("全局位图缓存为空,无法加载图片");
setDefaultTransparentBackground();
return;
}
}
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
App._mBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
}
// ======================== 路径判断逻辑结束 ========================
// 无缓存/路径更新:走原有逻辑加载图片
if (!calculateImageAspectRatio(imageFile)) { if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground(); setDefaultTransparentBackground();
return; return;
} }
// 压缩加载Bitmap
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920); Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) { if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground(); setDefaultTransparentBackground();
return; return;
} }
// 设置图片 // 缓存新图片,并更新当前缓存路径记录
App._mBitmapCacheUtils.cacheBitmap(imagePath);
mCurrentCachedPath = imagePath;
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap)); mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
adjustImageViewSize(); // 调整尺寸 adjustImageViewSize();
mIvBackground.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "=== loadImage 完成 ==="); LogUtils.d(TAG, "=== loadImage 完成 ===");
} }
@@ -195,54 +247,40 @@ public class BackgroundView extends RelativeLayout {
} }
} }
/**
* 调整ImageView尺寸保持原图比例在LinearLayout中居中平铺
*/
private void adjustImageViewSize() { private void adjustImageViewSize() {
LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
if (mLlContainer == null || mIvBackground == null) { if (mLlContainer == null || mIvBackground == null) {
LogUtils.e(TAG, "控件为空");
return; return;
} }
// 获取LinearLayout尺寸
int llWidth = mLlContainer.getWidth(); int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight(); int llHeight = mLlContainer.getHeight();
if (llWidth == 0 || llHeight == 0) {
postDelayed(new Runnable() { if (llWidth != 0 && llHeight != 0) {
@Override int ivWidth, ivHeight;
public void run() { if (mImageAspectRatio >= 1.0f) {
adjustImageViewSize(); ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
} ivHeight = (int) (ivWidth / mImageAspectRatio);
}, 10); } else {
return; 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);
} }
// 计算ImageView尺寸保持比例不超出LinearLayout
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); // 确保居中平铺
LogUtils.d(TAG, "ImageView尺寸" + ivWidth + "x" + ivHeight);
LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
} }
private void setDefaultTransparentBackground() { private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null); mIvBackground.setImageBitmap(null);
mIvBackground.setBackgroundColor(0x00000000); mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f; mImageAspectRatio = 1.0f;
// 清空缓存路径记录
mCurrentCachedPath = "";
mIvBackground.setVisibility(View.GONE);
} }
// ====================================== 重写方法 ====================================== // ====================================== 重写方法 ======================================
@@ -252,3 +290,4 @@ public class BackgroundView extends RelativeLayout {
adjustImageViewSize(); // 尺寸变化时重新调整 adjustImageViewSize(); // 尺寸变化时重新调整
} }
} }

View File

@@ -14,7 +14,7 @@ public class BatteryDrawable extends Drawable {
// 电量颜色画笔 // 电量颜色画笔
final Paint mPaint; final Paint mPaint;
// 电量值 // 电量值
int mnValue = 1; int mnValue = 100;
// @int color 电量颜色 // @int color 电量颜色
// //

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/holo_blue_light" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:浅灰色背景 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#E0E0E0" /> <!-- 按压深色 -->
<corners android:radius="8dp" /> <!-- 圆角适配小米UI风格 -->
<stroke android:width="1dp" android:color="#CCCCCC" /> <!-- 边框 -->
</shape>
</item>
<!-- 正常状态:白色背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" /> <!-- 正常白色 -->
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#CCCCCC" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:深灰色背景 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#D0D0D0" />
<corners android:radius="8dp" /> <!-- 与亮度按钮圆角一致,统一风格 -->
</shape>
</item>
<!-- 正常状态:浅灰色背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:深灰色 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#CCCCCC" /> <!-- 按压时颜色 -->
<corners android:radius="8dp" /> <!-- 圆角(可按需调整) -->
<stroke android:width="1dp" android:color="#EEEEEE" /> <!-- 边框(可选,不加可删除) -->
</shape>
</item>
<!-- 正常状态:浅灰色 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#F5F5F5" /> <!-- 正常时颜色 -->
<corners android:radius="8dp" /> <!-- 圆角(和按压状态一致) -->
<stroke android:width="1dp" android:color="#EEEEEE" /> <!-- 边框(可选) -->
</shape>
</item>
</selector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:加深深色背景 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#2D7CFF" /> <!-- 按压深蓝 -->
<corners android:radius="8dp" />
</shape>
</item>
<!-- 正常状态:主色背景(可改为项目主题色) -->
<item>
<shape android:shape="rectangle">
<solid android:color="#4096FF" /> <!-- 正常浅蓝适配小米系统UI -->
<corners android:radius="8dp" />
</shape>
</item>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 背景色:白色 -->
<solid android:color="@android:color/white" />
<!-- 圆角12dp适配小米机型圆角无锯齿 -->
<corners android:radius="12dp" />
<!-- 边框:浅灰色细边框(避免弹窗边缘模糊) -->
<stroke
android:width="1dp"
android:color="@android:color/darker_gray" />
<!-- 内边距:轻微留白,避免内容贴边 -->
<padding
android:bottom="5dp"
android:top="5dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="6dp" />
<stroke
android:width="1dp"
android:color="@android:color/darker_gray" />
</shape>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 进度条未完成部分:浅灰色 -->
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<corners android:radius="10dp" />
</shape>
</item>
<!-- 进度条已完成部分:系统蓝色(无需额外定义颜色) -->
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle">
<solid android:color="@android:color/holo_blue_light" />
<corners android:radius="10dp" />
</shape>
</clip>
</item>
</layer-list>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<!-- 滑块颜色:系统蓝色 -->
<solid android:color="@android:color/holo_blue_light" />
<!-- 滑块大小20dp适配小米机型触摸区域 -->
<size
android:width="20dp"
android:height="20dp" />
<!-- 白色边框:区分滑块与进度条 -->
<stroke
android:width="2dp"
android:color="@android:color/white" />
</shape>

View File

@@ -13,138 +13,132 @@
android:gravity="center_vertical" android:gravity="center_vertical"
style="@style/DefaultAToolbar"/> style="@style/DefaultAToolbar"/>
<LinearLayout <RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="0dp"
android:layout_weight="1.0">
<RelativeLayout <cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#FF28C000"> android:id="@+id/background_view">
</cc.winboll.studio.powerbell.views.BackgroundView>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="400dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Origin BG"
android:id="@+id/activitybackgroundpictureAButton5"
android:layout_alignParentLeft="true"
android:layout_margin="5dp"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Received BG"
android:id="@+id/activitybackgroundpictureAButton4"
android:layout_alignParentRight="true"
android:layout_margin="5dp"/>
</RelativeLayout>
<LinearLayout <LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.powerbell.views.BackgroundView <cc.winboll.studio.libaes.views.AButton
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="50dp"
android:orientation="vertical" android:layout_height="36dp"
android:layout_width="match_parent" android:text="◎"
android:layout_height="match_parent" android:layout_gravity="center_vertical"
android:background="#FF3243E2" android:layout_margin="5dp"
android:id="@+id/background_view"> android:id="@+id/activitybackgroundpictureAButton1"/>
</cc.winboll.studio.powerbell.views.BackgroundView> <cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="☑"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton2"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="♾"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton9"
android:onClick="onNetworkBackgroundDialog"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="400dp" android:layout_height="wrap_content"
android:background="#B92FABE6"> android:gravity="right">
<RelativeLayout <cc.winboll.studio.libaes.views.AButton
android:layout_width="match_parent" android:layout_width="50dp"
android:layout_height="wrap_content"> android:layout_height="36dp"
android:text="[+]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton3"/>
<cc.winboll.studio.libaes.views.AButton <cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp" android:layout_width="50dp"
android:layout_height="36dp" android:layout_height="36dp"
android:text="Origin BG" android:text="[+~]"
android:id="@+id/activitybackgroundpictureAButton5" android:layout_gravity="center_vertical"
android:layout_alignParentLeft="true" android:layout_margin="5dp"
android:layout_margin="5dp"/> android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton <cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp" android:layout_width="50dp"
android:layout_height="36dp" android:layout_height="36dp"
android:text="Received BG" android:text="[◐]"
android:id="@+id/activitybackgroundpictureAButton4" android:layout_gravity="center_vertical"
android:layout_alignParentRight="true" android:layout_margin="5dp"
android:layout_margin="5dp"/> android:id="@+id/activitybackgroundpictureAButton7"/>
</RelativeLayout> <cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[©]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundsettingsAButton1"
android:onClick="onColorPaletteDialog"/>
<LinearLayout <cc.winboll.studio.libaes.views.AButton
android:orientation="horizontal" android:layout_width="50dp"
android:layout_width="match_parent" android:layout_height="36dp"
android:layout_height="wrap_content" android:text="[○]"
android:gravity="right"> android:layout_gravity="center_vertical"
android:layout_margin="5dp"
<cc.winboll.studio.libaes.views.AButton android:id="@+id/activitybackgroundpictureAButton8"/>
android:layout_width="50dp"
android:layout_height="36dp"
android:text="◎"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton1"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="☑"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton2"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="♾"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton9"
android:onClick="onNetworkBackgroundDialog"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton3"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+~]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[◐]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton7"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[○]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton8"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</RelativeLayout> </LinearLayout>
</LinearLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

View File

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

View File

@@ -25,11 +25,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical|center_horizontal"> android:gravity="center_vertical|center_horizontal">
<ImageView <cc.winboll.studio.powerbell.views.BackgroundView
android:layout_width="wrap_content" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_width="200dp"
android:scaleType="centerCrop" android:layout_height="200dp"
android:id="@+id/dialogbackgroundpicturepreviewImageView1"/> android:id="@+id/backgroundview"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
android:background="#FFFFFFFF">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/iv_color_picker"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0000"
android:clickable="true"
android:focusable="true"/>
<ImageView
android:id="@+id/iv_color_scaler"
android:layout_width="100dp"
android:layout_height="100dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/color_scale_logo"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RGB"
android:textSize="16sp"/>
<EditText
android:id="@+id/et_r"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:hint="R"
android:inputType="number"
android:gravity="center"
android:maxLength="3"/>
<EditText
android:id="@+id/et_g"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:hint="G"
android:inputType="number"
android:gravity="center"
android:maxLength="3"/>
<EditText
android:id="@+id/et_b"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:hint="B"
android:inputType="number"
android:gravity="center"
android:maxLength="3"/>
</LinearLayout>
<EditText
android:id="@+id/et_color_value"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="#AARRGGBB"
android:inputType="text"
android:gravity="center"
android:maxLength="9"
android:layout_marginBottom="15dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="透明度:"
android:textSize="16sp"/>
<TextView
android:id="@+id/tv_alpha_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="16sp"
android:layout_marginLeft="10dp"/>
</LinearLayout>
<SeekBar
android:id="@+id/sb_alpha"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progress="100"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_marginBottom="20dp">
<TextView
android:id="@+id/tv_brightness_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="-"
android:textSize="20sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"/>
<TextView
android:id="@+id/tv_brightness_value"
android:layout_width="80dp"
android:layout_height="40dp"
android:text="100%"
android:textSize="16sp"
android:gravity="center"/>
<TextView
android:id="@+id/tv_brightness_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:textSize="20sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="100dp"
android:layout_height="45dp"
android:text="取消"
android:textSize="16sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"
android:layout_marginRight="20dp"/>
<TextView
android:id="@+id/tv_confirm"
android:layout_width="100dp"
android:layout_height="45dp"
android:text="确认"
android:textSize="16sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
</LinearLayout>

View File

@@ -1,218 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF7381FF"
android:id="@+id/fragmentmainviewBackgroundView1"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentmainviewLinearLayout3"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="@drawable/bg_frame">
<Switch
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewSwitch1"
android:padding="10dp"
android:layout_weight="1.0"
android:textSize="@dimen/text_title_size"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.0">
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:id="@+id/fragmentmainviewLinearLayout1">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/usege"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:id="@+id/fragmentmainviewCheckBox2"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar2"
android:progressTint="@color/colorUsege"
android:progressBackgroundTint="@color/colorUsege"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView3"
android:gravity="center_horizontal"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView2"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView4"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView1"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView2"
android:gravity="center_horizontal"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView3"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:id="@+id/fragmentmainviewLinearLayout2">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/charge"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:id="@+id/fragmentmainviewCheckBox1"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar1"
android:progressTint="@color/colorCharge"
android:progressBackgroundTint="@color/colorCharge"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tips"
android:textSize="@dimen/text_content_size"
android:id="@+id/fragmentandroidviewTextView1"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="10dp">
</LinearLayout>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 广告视图独立布局供ViewStub延迟加载 -->
<cc.winboll.studio.libaes.views.ADsBannerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>

View File

@@ -2,9 +2,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_log"
android:title="@string/item_logview"/>
<item <item
android:id="@+id/action_unittestactivity" android:id="@+id/action_unittestactivity"
android:title="@string/item_mainunittestactivity"/> android:title="@string/item_mainunittestactivity"/>

View File

@@ -58,9 +58,73 @@
<color name="colorUsege">@color/colorRed</color> <color name="colorUsege">@color/colorRed</color>
<color name="colorCurrent">@color/colorBlue</color> <color name="colorCurrent">@color/colorBlue</color>
<color name="colorCharge">@color/colorYellow</color> <color name="colorCharge">@color/colorYellow</color>
<!--CustomSlideToUnlockView控件配置--> <!--CustomSlideToUnlockView控件配置-->
<color name="colorCustomSlideToUnlockViewWhite">#FFFFFFFF</color> <color name="colorCustomSlideToUnlockViewWhite">#FFFFFFFF</color>
<!----> <!-- ============== 基础黑白(必含,适配文字/背景) ============== -->
<color name="white">#FFFFFF</color> <!-- 纯白色(文字/背景) -->
<color name="black">#000000</color> <!-- 近黑色(重要文字) -->
<!-- ============== 基础色系(按钮/强调色常用) ============== -->
<!-- 蓝色系(常用:确认/链接/主题色) -->
<color name="blue_light">#4A90E2</color> <!-- 浅蓝(次要按钮) -->
<color name="blue_normal">#2196F3</color> <!-- 标准蓝(主题/确认按钮) -->
<color name="blue_dark">#1976D2</color> <!-- 深蓝(按压态/重要强调) -->
<!-- 绿色系(常用:成功/完成/安全提示) -->
<color name="green_light">#66BB6A</color> <!-- 浅绿(次要成功态) -->
<color name="green_normal">#4CAF50</color> <!-- 标准绿(成功按钮/提示) -->
<color name="green_dark">#388E3C</color> <!-- 深绿(按压态/重要成功) -->
<!-- 红色系(常用:错误/警告/删除按钮) -->
<color name="red_light">#EF5350</color> <!-- 浅红(次要错误提示) -->
<color name="red_normal">#F44336</color> <!-- 标准红(删除/错误按钮) -->
<color name="red_dark">#D32F2F</color> <!-- 深红(按压态/重要错误) -->
<!-- 黄色系(常用:警告/提醒/高亮) -->
<color name="yellow_light">#FFF59D</color> <!-- 浅黄(次要提醒) -->
<color name="yellow_normal">#FFC107</color> <!-- 标准黄(警告提示/高亮) -->
<color name="yellow_dark">#FFA000</color> <!-- 深黄(重要警告) -->
<!-- 橙色系(常用:提醒/进度/活力色) -->
<color name="orange_normal">#FF9800</color> <!-- 标准橙(提醒按钮/进度) -->
<!-- 紫色系(常用:特殊强调/个性按钮) -->
<color name="purple_normal">#9C27B0</color> <!-- 标准紫(特殊功能按钮) -->
<!-- ============== 透明色(遮罩/背景叠加) ============== -->
<color name="transparent">#00000000</color> <!-- 全透明 -->
<color name="black_transparent_50">#80000000</color> <!-- 50%透明黑(遮罩) -->
<!-- 1. 不透明灰色(常用深浅梯度,直接用) -->
<color name="gray_100">#F5F5F5</color> <!-- 极浅灰(接近白色,背景用) -->
<color name="gray_200">#EEEEEE</color> <!-- 浅灰(卡片/分割线背景) -->
<color name="gray_300">#E0E0E0</color> <!-- 中浅灰(边框/次要背景) -->
<color name="gray_400">#BDBDBD</color> <!-- 中灰(次要文字/图标) -->
<color name="gray_500">#9E9E9E</color> <!-- 标准中灰(常用辅助文字) -->
<color name="gray_600">#757575</color> <!-- 中深灰(常规辅助文字) -->
<color name="gray_700">#616161</color> <!-- 深灰(重要辅助文字) -->
<color name="gray_800">#424242</color> <!-- 极深灰(接近黑色,标题副文本) -->
<color name="gray_900">#212121</color> <!-- 近黑色(特殊场景用) -->
<!-- 2. 半透明灰色(带透明度,遮罩/蒙层用) -->
<color name="gray_transparent_30">#4D9E9E9E</color> <!-- 30%透明中灰A=4D -->
<color name="gray_transparent_50">#809E9E9E</color> <!-- 50%透明中灰A=80 -->
<color name="gray_transparent_70">#B39E9E9E</color> <!-- 70%透明中灰A=B3 -->
<color name="gray_light">#EEE</color> <!-- 等价 #EEEEEE浅灰 -->
<color name="gray_mid">#999</color> <!-- 等价 #999999中灰 -->
<color name="gray_dark">#666</color> <!-- 等价 #666666深灰 -->
<color name="gray_black">#333</color> <!-- 等价 #333333极深灰 -->
<!-- 50% 透明中灰(弹窗遮罩常用) -->
<color name="mask_gray">#809E9E9E</color>
<!-- 30% 透明深灰(背景叠加) -->
<color name="bg_overlay_gray">#4D424242</color>
<!-- 1. 常规灰色(按钮默认态,常用中灰) -->
<color name="btn_gray_normal">#9E9E9E</color>
<!-- 2. 按压深色(按钮点击态,加深一级,提升交互感) -->
<color name="btn_gray_pressed">#757575</color>
<!-- 3. 禁用灰色(按钮不可点击态,浅灰) -->
<color name="btn_gray_disabled">#E0E0E0</color>
</resources> </resources>

View File

@@ -26,4 +26,18 @@
<item name="android:textSize">@dimen/text_subtitle_size</item> <item name="android:textSize">@dimen/text_subtitle_size</item>
</style> </style>
<!-- 自定义调色板对话框样式 -->
<style name="CustomDialogStyle" parent="@android:style/Theme.Dialog">
<!-- 去除标题栏 -->
<item name="android:windowNoTitle">true</item>
<!-- 背景透明(避免小米机型弹窗周围黑边) -->
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 禁止弹窗全屏 -->
<item name="android:windowFullscreen">false</item>
<!-- 小米机型适配:弹窗大小自适应 -->
<item name="android:windowContentOverlay">@null</item>
<!-- 禁止触摸外部关闭(可选,避免误触) -->
<item name="android:windowCloseOnTouchOutside">false</item>
</style>
</resources> </resources>