Compare commits

...

152 Commits

Author SHA1 Message Date
16e581802f <powerbell>APK 15.14.24 release Publish. 2025-12-23 14:29:30 +08:00
3048963fa9 <powerbell>Start New Stage Version. 2025-12-23 14:28:13 +08:00
34082c49e9 源码整理 2025-12-23 14:16:58 +08:00
eff1822ee5 源码整理 2025-12-23 13:43:30 +08:00
4e84ff493b 源码整理 2025-12-23 13:12:17 +08:00
c2def0bb3b 源码整理 2025-12-22 23:19:50 +08:00
a086a47b2d 源码整理 2025-12-22 23:05:26 +08:00
c38b392e39 <powerbell>APK 15.14.23 release Publish. 2025-12-22 13:00:54 +08:00
0f736c2007 去掉位图压缩功能,尽量保持位图品质。 2025-12-22 12:59:50 +08:00
26b86cd715 <powerbell>APK 15.14.22 release Publish. 2025-12-22 11:21:24 +08:00
7888696e65 改进缓存策略。 2025-12-22 11:20:14 +08:00
8609c2f784 <powerbell>APK 15.14.21 release Publish. 2025-12-22 11:02:54 +08:00
863b743330 改进缓存策略,修复位图绘画时的异常。 2025-12-22 11:01:35 +08:00
61b7afa4b5 <powerbell>APK 15.14.20 release Publish. 2025-12-22 10:25:46 +08:00
8e4c7a6832 添加位图缓存 2025-12-22 10:24:29 +08:00
d29068b029 <powerbell>APK 15.14.19 release Publish. 2025-12-22 10:20:01 +08:00
51963f8e0f 优化启动界面显示 2025-12-22 10:18:28 +08:00
18a5762c15 <powerbell>APK 15.14.18 release Publish. 2025-12-22 10:04:49 +08:00
60de16ab45 <powerbell>Start New Stage Version. 2025-12-22 10:03:27 +08:00
6d60d71991 首页启动窗口改用视图控件缓存替代位图缓存。 2025-12-22 10:01:13 +08:00
0cfbc43acb MemoryCachedBackgroundView测试完成 2025-12-22 08:55:09 +08:00
20227e29ef 添加缓存视图控件类 2025-12-21 20:46:14 +08:00
2b7007f478 <powerbell>APK 15.14.17 release Publish. 2025-12-21 19:13:50 +08:00
212b8185c8 移除白色启动屏。 2025-12-21 19:12:31 +08:00
2ef09e020e <powerbell>APK 15.14.16 release Publish. 2025-12-21 17:57:41 +08:00
6de9b7379b 添加每分钟加载一次位图缓存的功能。预防系统优化内存时位图缓存被清理。 2025-12-21 17:50:40 +08:00
7fba2c8812 <powerbell>APK 15.14.15 release Publish. 2025-12-21 13:40:20 +08:00
20e3a5f974 修复系统分享图片的接收功能。 2025-12-21 13:38:46 +08:00
c8333e1e81 <powerbell>APK 15.14.14 release Publish. 2025-12-21 11:22:23 +08:00
6beb56efae <powerbell>Start New Stage Version. 2025-12-21 11:21:35 +08:00
eb51b2c8f4 网络下载按钮修复完成 2025-12-21 11:20:51 +08:00
36bdd059b0 修复空白图片背景按钮与图片背景按钮。 2025-12-21 09:49:40 +08:00
bf051dcc9f <powerbell>APK 15.14.13 release Publish. 2025-12-21 09:22:26 +08:00
b38a8df462 添加应用内存整理后重新缓存位图的功能。 2025-12-21 09:20:15 +08:00
a8dbe43d4b 源码整理 2025-12-21 02:05:38 +08:00
05a8dc5205 <powerbell>APK 15.14.12 release Publish. 2025-12-20 22:59:40 +08:00
280fc1abd6 <powerbell>Start New Stage Version. 2025-12-20 22:58:26 +08:00
fe2084f5ff 优化RemindThread线程,节约电量。 2025-12-20 22:57:42 +08:00
2a75aa140e 修改背景控件默认背景颜色。 2025-12-20 21:23:52 +08:00
04b597cbe7 <powerbell>APK 15.14.11 release Publish. 2025-12-20 19:28:00 +08:00
35d210c378 添加滚动条拉动时,实时预览数值的功能。 2025-12-20 19:26:32 +08:00
79ae6de6fc <powerbell>APK 15.14.10 release Publish. 2025-12-20 18:45:13 +08:00
328f559d2e 改进背景图片位图缓存方法。 2025-12-20 18:40:29 +08:00
28d340a772 电量提醒线程测试完成。 2025-12-20 17:40:06 +08:00
65acbfcd04 调整电量消息频率以及消息内容。 2025-12-20 16:30:34 +08:00
6af2096b30 修复广播消息发送方法 2025-12-20 15:55:40 +08:00
e539922478 广播消息分开为,电量消息与应用配置更新消息。 2025-12-20 15:44:12 +08:00
1d9a03554c 添加广播接收器的注册与释放。 2025-12-20 13:49:02 +08:00
23beabe99b 源码整理 2025-12-20 13:23:07 +08:00
9c1e9dc75b 函数简化重构。 2025-12-20 03:07:08 +08:00
76c1dee625 MainActivity本地消息发送整理中。。。 2025-12-19 21:05:42 +08:00
e967ce5511 修复服务启动逻辑 2025-12-19 20:42:40 +08:00
bea77409a5 重置通知架构为旧式方法,用线程启动法。过程顺便整理代码。 2025-12-19 20:31:43 +08:00
e584e824c0 恢复2a74fd2c304b571ab5ae349ffc3b7f06c5b4daf7提交点,旧版电量计算方法。 2025-12-19 20:11:10 +08:00
bc6a82af41 电量提醒消息,通路架构基本完成。 2025-12-19 18:48:47 +08:00
9bc71bb3f6 正在修复电量提醒线程。。。 2025-12-19 00:13:03 +08:00
b0dee5e98e 服务线程调试中,目标就是线程放入服务类使用handler与服务通信。 2025-12-18 21:15:24 +08:00
7d796b5c3f 更新基础类库,更新调试工具。 2025-12-18 17:52:47 +08:00
d43ba4bff2 剔除服务锁,正在调试RemindThread调用。。。 2025-12-18 15:20:32 +08:00
796d826331 源码整理。 2025-12-18 13:14:04 +08:00
b3f4571b57 移除提醒线程前台标志 2025-12-18 13:11:00 +08:00
5e6de91430 服务启停调试中。。。 2025-12-17 20:40:04 +08:00
d61d1da5d1 服务自启动修复中。。。 2025-12-17 16:50:38 +08:00
5cf47172f4 服务自启配置持久化 2025-12-17 16:24:49 +08:00
683dc6791e 修复应用初始安装时电量提醒无声音的问题。 2025-12-17 15:17:45 +08:00
1e9a6adc88 修复电量进度条拉动时无响应问题 2025-12-17 14:50:21 +08:00
dbcb5259d9 源码整理 2025-12-17 14:14:07 +08:00
740ab932a4 源码整理 2025-12-17 14:01:57 +08:00
29b9f3c82b 源码整理 2025-12-17 13:50:33 +08:00
47601ef542 编译参数修复 2025-12-17 12:04:08 +08:00
7b17fae798 豆包完美想象版,未调试。 2025-12-17 07:48:32 +08:00
405b914f02 20251217_043730_107 2025-12-17 04:37:34 +08:00
1c7ceebb78 添加软著申请文档生成脚本 2025-12-17 04:03:43 +08:00
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
95 changed files with 13392 additions and 6219 deletions

View File

@@ -29,11 +29,11 @@ android {
applicationId "cc.winboll.studio.powerbell"
minSdkVersion 23
targetSdkVersion 30
versionCode 6
versionCode 7
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.12"
versionName "15.14"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -88,7 +88,7 @@ dependencies {
// WinBoLL备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Dec 10 16:20:34 HKT 2025
stageCount=4
#Tue Dec 23 14:29:30 HKT 2025
stageCount=25
libraryProject=
baseVersion=15.12
publishVersion=15.12.3
baseVersion=15.14
publishVersion=15.14.24
buildCount=0
baseBetaVersion=15.12.4
baseBetaVersion=15.14.25

View File

@@ -0,0 +1,279 @@
#!/bin/bash
# PowerBell软著版本号快速修改+生成脚本
# 无需手动改主脚本,输入版本号直接运行
# 颜色输出函数
red_echo() { echo -e "\033[31m$1\033[0m"; }
green_echo() { echo -e "\033[32m$1\033[0m"; }
blue_echo() { echo -e "\033[34m$1\033[0m"; }
# 1. 提示用户输入新版本号
blue_echo "==== 请输入软著版本号格式示例V15、V15.0.1 ===="
read -p "输入版本号:" NEW_VERSION
# 校验版本号格式(避免特殊符号)
if [[ ! $NEW_VERSION =~ ^V[0-9]+(\.[0-9]+)*$ ]]; then
red_echo "错误版本号格式无效请遵循「V+数字」格式如V15、V15.0.1),不含特殊符号"
exit 1
fi
# 2. 定义固定配置(仅需修改这里的著作权人,其他无需动)
SOFTWARE_NAME="PowerBell"
COPYRIGHT_OWNER="张绍建陆丰东海镇云宝软件开发工作室"
LINES_PER_PAGE=55
# 3. 生成主脚本(自动替换新版本号)
blue_echo -e "\n==== 生成${NEW_VERSION}版本主脚本 ===="
cat > build_copyright_pdf_temp.sh << EOF
#!/bin/bash
# PowerBell软著PDF生成脚本版本$NEW_VERSION
red_echo() { echo -e "\033[31m\$1\033[0m"; }
green_echo() { echo -e "\033[32m\$1\033[0m"; }
blue_echo() { echo -e "\033[34m\$1\033[0m"; }
# 配置项(已自动替换为${NEW_VERSION}
SOFTWARE_NAME="$SOFTWARE_NAME"
SOFTWARE_VERSION="$NEW_VERSION"
COPYRIGHT_OWNER="$COPYRIGHT_OWNER"
LINES_PER_PAGE=$LINES_PER_PAGE
# 步骤1检查依赖
blue_echo "==== 1/7 检查并安装依赖 ===="
sudo apt update > /dev/null 2>&1
REQUIRED_PKGS=("python3" "wkhtmltopdf" "fonts-wqy-microhei" "pdftk" "poppler-utils")
for pkg in "\${REQUIRED_PKGS[@]}"; do
if ! dpkg -s "\$pkg" > /dev/null 2>&1; then
green_echo "安装依赖:\$pkg"
sudo apt install -y "\$pkg" > /dev/null 2>&1
fi
done
# 步骤2生成纯文本源码
blue_echo -e "\n==== 2/7 生成纯文本核心源码 ===="
cat > generate_source.py << GEN_EOF
import os
PROJECT_PATH = "./"
OUTPUT_TXT = "PowerBell_Core_Source.txt"
INCLUDE_EXT = [".java", ".kt"]
EXCLUDE_DIRS = ["build", "libs", "test", "androidTest", ".git", ".idea", "gradle", "unittest"]
MIN_LINE_COUNT = 3
SOFTWARE_NAME = "$SOFTWARE_NAME"
SOFTWARE_VERSION = "$NEW_VERSION"
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
def clean_text(text):
return ''.join(c for c in text if c.isprintable() or c in "\\n\\r\\t")
def generate_source_txt():
valid_files = []
main_dir = os.path.join(PROJECT_PATH, "src", "main")
if not os.path.exists(main_dir):
print("Error: src/main directory not found!")
return
for root, dirs, files in os.walk(main_dir):
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
for file in files:
if os.path.splitext(file)[1] in INCLUDE_EXT:
file_path = os.path.join(root, file)
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
code_lines = [l for l in lines if l.strip() and not l.strip().startswith("//")]
if len(code_lines) >= MIN_LINE_COUNT:
valid_files.append(file_path)
except:
continue
valid_files.sort(key=lambda x: os.path.getsize(x), reverse=True)
with open(OUTPUT_TXT, "w", encoding="utf-8-sig") as f:
f.write(f"\{SOFTWARE_NAME} \{SOFTWARE_VERSION} 核心源码 - 著作权人:\{COPYRIGHT_OWNER}\\n\\n")
for idx, file_path in enumerate(valid_files, 1):
f.write(f"\\n{'='*60}\\n")
f.write(f"文件 \{idx}\{file_path.replace(PROJECT_PATH, '')}\\n")
f.write(f"{'='*60}\\n\\n")
try:
try:
with open(file_path, "r", encoding="utf-8") as src_f:
content = clean_text(src_f.read())
except UnicodeDecodeError:
with open(file_path, "r", encoding="gbk") as src_f:
content = clean_text(src_f.read())
f.write(content)
f.write("\\n\\n")
except Exception as e:
f.write(f"文件读取失败:\{str(e)}\\n\\n")
continue
print(f"有效源码文件数:\{len(valid_files)}")
print(f"纯文本文件路径:\{os.path.abspath(OUTPUT_TXT)}")
if __name__ == "__main__":
generate_source_txt()
GEN_EOF
python3 generate_source.py
if [ ! -f "PowerBell_Core_Source.txt" ]; then
red_echo "纯文本源码生成失败!"
exit 1
fi
# 步骤3生成带版本号页眉的HTML
blue_echo -e "\n==== 3/7 生成带${NEW_VERSION}页眉的HTML ===="
cat > txt2html.py << TXT_EOF
import os
TXT_FILE = "PowerBell_Core_Source.txt"
HTML_FILE = "PowerBell_Source.html"
SOFTWARE_NAME = "$SOFTWARE_NAME"
SOFTWARE_VERSION = "$NEW_VERSION"
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
LINES_PER_PAGE = $LINES_PER_PAGE
CSS_STYLE = """
<style>
@page {{
size: A4;
margin: 10mm 5mm;
@top-center {{
content: "{} {} - 源代码(著作权人:{}";
font-family: 'WenQuanYi Micro Hei', monospace;
font-size: 9pt;
font-weight: bold;
}}
@bottom-center {{
content: "页码 " counter(page) " / " counter(pages);
font-family: 'WenQuanYi Micro Hei', monospace;
font-size: 9pt;
}}
}}
body {{
font-family: 'WenQuanYi Micro Hei', monospace;
font-size: 9pt;
line-height: 1.1;
margin: 0;
padding: 5mm 0 0 0;
counter-reset: code-line;
}}
.file-header {{
background: #f0f0f0;
padding: 3px;
margin: 6px 0;
font-weight: bold;
font-size: 10pt;
}}
.code-block {{
white-space: pre;
margin-left: 8px;
line-height: 1.1;
counter-increment: code-line;
}}
.code-block:before {{
content: counter(code-line) " ";
color: #888;
display: inline-block;
width: 30px;
text-align: right;
margin-right: 5px;
}}
.page-break {{ page-break-after: always; counter-reset: code-line; }}
</style>
""".format(SOFTWARE_NAME, SOFTWARE_VERSION, COPYRIGHT_OWNER)
def txt_to_html():
with open(TXT_FILE, "r", encoding="utf-8") as f:
content = f.read()
html_content = "<!DOCTYPE html><html><head><meta charset='utf-8'>" + CSS_STYLE + "</head><body>"
content_lines = content.split("\\n")[2:]
content_clean = "\\n".join(content_lines)
blocks = content_clean.split("====")
line_count = 0
for block in blocks:
if not block.strip():
continue
if "文件 " in block and ":" in block:
file_header = block.split("\\n")[0].strip() if "\\n" in block else block.strip()
html_content += f"<div class='file-header'>\{file_header}</div>"
code_part = block.split("\\n")[1:] if "\\n" in block else []
block = "\\n".join(code_part)
code_lines = block.split("\\n")
for line in code_lines:
if line.strip() or line_count > 0:
line_count += 1
html_content += f"<div class='code-block'>\{line}</div>"
if line_count >= LINES_PER_PAGE:
html_content += "<div class='page-break'></div>"
line_count = 0
html_content += "</body></html>"
with open(HTML_FILE, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"HTML文件路径\{os.path.abspath(HTML_FILE)}")
if __name__ == "__main__":
txt_to_html()
TXT_EOF
python3 txt2html.py
if [ ! -f "PowerBell_Source.html" ]; then
red_echo "HTML文件生成失败"
exit 1
fi
# 步骤4生成完整PDF
blue_echo -e "\n==== 4/7 生成完整PDF版本${NEW_VERSION} ===="
wkhtmltopdf --page-size A4 \
--margin-top 15mm --margin-bottom 15mm --margin-left 5mm --margin-right 5mm \
--encoding utf-8 \
--no-images --disable-javascript \
--enable-local-file-access \
--no-stop-slow-scripts \
PowerBell_Source.html PowerBell_soft_full.pdf
if [ ! -f "PowerBell_soft_full.pdf" ]; then
red_echo "完整PDF生成失败"
exit 1
fi
# 步骤5截取60页
blue_echo -e "\n==== 5/7 截取前30+后30页 ===="
TOTAL_PAGES=\$(pdfinfo PowerBell_soft_full.pdf | grep "Pages" | awk '{print \$2}')
green_echo "源码完整PDF总页数\$TOTAL_PAGES 页"
if [ "\$TOTAL_PAGES" -le 60 ]; then
cp PowerBell_soft_full.pdf PowerBell_软著源码_${NEW_VERSION}_60页.pdf
green_echo "源码不足60页直接使用完整PDF"
else
pdftk PowerBell_soft_full.pdf cat 1-30 output PowerBell_前30页.pdf
START_PAGE=\$((TOTAL_PAGES - 29))
pdftk PowerBell_soft_full.pdf cat \$START_PAGE-\$TOTAL_PAGES output PowerBell_后30页.pdf
pdftk PowerBell_前30页.pdf PowerBell_后30页.pdf cat output PowerBell_软著源码_${NEW_VERSION}_60页.pdf
rm -f PowerBell_前30页.pdf PowerBell_后30页.pdf
green_echo "源码超过60页已截取前30页+后30页合并为60页"
fi
# 步骤6验证规范
blue_echo -e "\n==== 6/7 验证${NEW_VERSION}版本PDF规范 ===="
FINAL_PAGES=\$(pdfinfo PowerBell_软著源码_${NEW_VERSION}_60页.pdf | grep "Pages" | awk '{print \$2}')
green_echo "最终PDF页数\$FINAL_PAGES 页"
green_echo "每页代码行数:\$LINES_PER_PAGE 行≥50行"
green_echo "页眉信息:$SOFTWARE_NAME $NEW_VERSION - 源代码(著作权人:$COPYRIGHT_OWNER"
# 步骤7清理临时文件
blue_echo -e "\n==== 7/7 清理临时文件 ===="
rm -f generate_source.py txt2html.py PowerBell_Core_Source.txt PowerBell_Source.html PowerBell_soft_full.pdf
green_echo "临时文件清理完成!"
# 输出结果
green_echo -e "\n====================================="
green_echo "$SOFTWARE_NAME $NEW_VERSION 软著PDF生成成功🎉"
green_echo "📄 最终文件:\$(pwd)/PowerBell_软著源码_${NEW_VERSION}_60页.pdf"
green_echo "💡 可直接提交软著登记,无需手动修改!"
green_echo "====================================="
EOF
# 4. 赋予执行权限并运行
chmod +x build_copyright_pdf_temp.sh
blue_echo -e "\n==== 开始生成${NEW_VERSION}版本PDF ===="
./build_copyright_pdf_temp.sh
# 5. 删除临时主脚本(可选,保留则注释此行)
rm -f build_copyright_pdf_temp.sh
green_echo -e "\n==== 操作完成!${NEW_VERSION}版本PDF已生成 ===="

View File

@@ -4,55 +4,50 @@
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 只有在前台运行时才能获取大致位置信息 -->
<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_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"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 显示通知 -->
<!-- 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- PACKAGE_USAGE_STATS -->
<!-- 应用统计与查询权限 -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-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
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<!-- 电池与存储统计权限 -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- 外部存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 硬件特性声明 -->
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<!-- 应用查询 -->
<queries>
<package android:name="com.miui.securitycenter"/>
</queries>
<application
android:name=".App"
@@ -64,18 +59,17 @@
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning">
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<!-- 主活动 -->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:launchMode="singleTask">
</activity>
<activity android:name=".activities.CrashActivity"/>
android:launchMode="singleTask"/>
<!-- 活动别名(启动器) -->
<activity-alias
android:name=".MainActivityEN1"
android:targetActivity=".MainActivity"
@@ -83,19 +77,13 @@
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/>
</activity-alias>
<activity-alias
@@ -105,19 +93,13 @@
android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/>
</activity-alias>
<activity-alias
@@ -127,116 +109,129 @@
android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/>
</activity-alias>
<!-- 功能活动 -->
<activity
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity"
android:name=".activities.CrashActivity"
android:exported="false"/>
<activity
android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask">
</activity>
android:launchMode="singleTask"
android:exported="false"/>
<activity
android:name="cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity"
android:name=".activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<activity
android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:name=".activities.SettingsActivity"
android:exported="false"/>
<activity
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
android:exported="false"/>
<!-- 第三方活动 -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"/>
<!-- 广播接收器 -->
<receiver
android:name=".receivers.MainReceiver"
android:enabled="true"
android:exported="false"
android:exported="true"
android:directBootAware="true">
<intent-filter>
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.POWER_CONNECTED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>
<!-- 服务 -->
<service
android:name="cc.winboll.studio.powerbell.services.ControlCenterService"
android:name=".services.ControlCenterService"
android:priority="1000"
android:enabled="true"
android:exported="false"
android:process=".controlcenterservice"/>
android:process=".controlcenterservice"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活"/>
</service>
<service
android:name="cc.winboll.studio.powerbell.services.AssistantService"
android:name=".services.AssistantService"
android:enabled="true"
android:exported="false"
android:process=".assistantservice"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/>
android:process=".assistantservice">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行"/>
</service>
<!-- 内容提供者 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/>
<!-- 1. 注册 UCropActivity关键解决崩溃 -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"> <!-- 必须添加Android 12+ 要求显式声明 exported -->
</activity>
<!-- 元数据 -->
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,95 +1,269 @@
package cc.winboll.studio.powerbell;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
import java.io.File;
/**
* 应用全局入口类适配Android API 30基于Java 7编写
* 核心策略:极致强制缓存 - 无论内存紧张程度永不自动清理任何缓存Bitmap/视图控件/路径记录)
*/
public class App extends GlobalApplication {
// ===================== 常量定义区(按功能分类排序) =====================
public static final String TAG = "App";
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_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_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 COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
// 数据配置存储工具
static AppConfigUtils _mAppConfigUtils;
static AppCacheUtils _mAppCacheUtils;
GlobalApplicationReceiver mReceiver;
static String szTempDir = "";
// 动作跳转常量
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_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
public static String getTempDirPath() {
return szTempDir;
// 缓存防护常量
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
// ===================== 静态属性区(按工具类优先级排序) =====================
// 数据配置工具
private static AppConfigUtils sAppConfigUtils;
private static AppCacheUtils sAppCacheUtils;
// 全局Bitmap缓存工具极致强制保持一旦初始化永不销毁
public static BitmapCacheUtils sBitmapCacheUtils;
// 全局视图控件缓存工具(极致强制保持:一旦初始化,永不销毁)
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// ===================== 成员属性区(按生命周期关联度排序) =====================
// 全局广播接收器
private GlobalApplicationReceiver mGlobalReceiver;
// 通知管理工具
private NotificationManagerUtils mNotificationManager;
// ===================== 公共静态方法区(工具类实例获取) =====================
/**
* 获取应用配置工具实例
*/
public static AppConfigUtils getAppConfigUtils(Context context) {
LogUtils.d(TAG, "getAppConfigUtils() 调用传入Context类型" + (context != null ? context.getClass().getSimpleName() : "null"));
if (sAppConfigUtils == null) {
sAppConfigUtils = AppConfigUtils.getInstance(context);
LogUtils.d(TAG, "getAppConfigUtils()AppConfigUtils实例已初始化");
}
return sAppConfigUtils;
}
/**
* 获取应用缓存工具实例
*/
public static AppCacheUtils getAppCacheUtils(Context context) {
LogUtils.d(TAG, "getAppCacheUtils() 调用传入Context类型" + (context != null ? context.getClass().getSimpleName() : "null"));
if (sAppCacheUtils == null) {
sAppCacheUtils = AppCacheUtils.getInstance(context);
LogUtils.d(TAG, "getAppCacheUtils()AppCacheUtils实例已初始化");
}
return sAppCacheUtils;
}
// ===================== 公共成员方法区(业务功能) =====================
/**
* 清除电池历史数据
*/
public void clearBatteryHistory() {
LogUtils.d(TAG, "clearBatteryHistory() 调用");
if (sAppCacheUtils != null) {
sAppCacheUtils.clearBatteryHistory();
LogUtils.d(TAG, "clearBatteryHistory():电池历史数据已清除");
} else {
LogUtils.w(TAG, "clearBatteryHistory()AppCacheUtils未初始化清除失败");
}
}
/**
* 手动清理所有缓存(带严格权限控制,仅主动调用生效)
* 极致强制缓存策略下,仅提供手动清理入口,永不自动调用
*/
public static void manualClearAllCache() {
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存调用(极致强制缓存策略下,需谨慎使用)");
// 清理Bitmap缓存
if (sBitmapCacheUtils != null) {
sBitmapCacheUtils.clearAllCache();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存已手动清理");
}
// 清理视图控件缓存(仅清除静态引用,不销毁实例)
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存实例保持,仅清除静态引用");
sMemoryCachedBackgroundView = null;
}
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存完成(部分缓存实例仍可能保留在内存中)");
}
// ===================== 生命周期方法区(按执行顺序排序) =====================
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
LogUtils.d(TAG, "onCreate() 应用启动,开始初始化");
// 初始化活动窗口管理
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
// 初始化调试模式
setIsDebugging(BuildConfig.DEBUG);
LogUtils.d(TAG, "onCreate() 调试模式:" + BuildConfig.DEBUG);
// 初始化基础工具
initBaseTools();
// 初始化工具类实例(核心:极致强制缓存,永不销毁)
initUtils();
// 初始化广播接收器
initReceiver();
LogUtils.d(TAG, "onCreate() 应用初始化完成,极致强制缓存策略已启用");
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放非缓存资源");
// 释放Toast工具
ToastUtils.release();
LogUtils.d(TAG, "onTerminate()Toast工具已释放");
// 释放通知工具
releaseNotificationManager();
// 释放广播接收器
releaseReceiver();
// 核心修改:应用终止时也不清理缓存,保持静态实例
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 应用终止,极致强制缓存策略生效,不清理任何缓存");
LogUtils.d(TAG, "onTerminate() 非缓存资源释放完成,缓存实例保持");
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
// 极致强制缓存:禁止任何缓存清理操作,仅记录日志
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onTrimMemory() 调用内存等级level" + level + ",极致强制保持所有缓存");
// 记录详细缓存状态,不执行任何清理
logDetailedCacheStatus();
}
@Override
public void onLowMemory() {
super.onLowMemory();
// 极致强制缓存:低内存时也不清理任何缓存
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onLowMemory() 调用,极致强制保持所有缓存");
// 记录详细缓存状态,不执行任何清理
logDetailedCacheStatus();
}
// ===================== 私有初始化方法区(按初始化顺序排序) =====================
/**
* 初始化基础工具Activity管理、Toast
*/
private void initBaseTools() {
LogUtils.d(TAG, "initBaseTools() 开始初始化基础工具");
WinBoLLActivityManager.init(this);
ToastUtils.init(this);
// 临时文件夹方案1
// 获取Pictures文件夹路径Android 10及以上推荐使用MediaStore此处为传统方式
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
// 定义目标文件路径在Pictures目录下创建"PowerBell"子文件夹及文件)
File powerBellDir = new File(picturesDir, "PowerBell");
LogUtils.d(TAG, "initBaseTools() 基础工具初始化完成");
}
// 临时文件夹方案2 <图片保存失败>
// 获取Pictures文件夹路径Android 10及以上推荐使用MediaStore此处为传统方式
//File powerBellDir = getExternalFilesDir("TempDir");
/**
* 初始化工具类实例(核心:极致强制缓存,一旦初始化永不销毁)
*/
private void initUtils() {
LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用极致强制缓存策略");
sAppConfigUtils = getAppConfigUtils(this);
sAppCacheUtils = getAppCacheUtils(this);
// 先创建文件夹(如果不存在
if (!powerBellDir.exists()) {
powerBellDir.mkdirs();
// 极致强制初始化Bitmap缓存工具必初始化永不销毁
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "initUtils() Bitmap缓存工具已初始化极致强制保持永不销毁");
// 极致强制初始化视图控件缓存工具(必初始化,永不销毁)
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(极致强制保持,永不销毁)");
mNotificationManager = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initUtils() 工具类初始化完成,极致强制缓存策略已生效");
}
/**
* 初始化广播接收器
*/
private void initReceiver() {
LogUtils.d(TAG, "initReceiver() 开始初始化广播接收器");
mGlobalReceiver = new GlobalApplicationReceiver(this);
mGlobalReceiver.registerAction();
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成");
}
// ===================== 私有释放方法区(按资源重要性排序) =====================
/**
* 释放广播接收器资源
*/
private void releaseReceiver() {
LogUtils.d(TAG, "releaseReceiver() 开始释放广播接收器");
if (mGlobalReceiver != null) {
mGlobalReceiver.unregisterAction();
mGlobalReceiver = null;
LogUtils.d(TAG, "releaseReceiver() 广播接收器资源已释放");
} else {
LogUtils.d(TAG, "releaseReceiver() 广播接收器未初始化,无需释放");
}
szTempDir = powerBellDir.getAbsolutePath();
// 设置数据配置存储工具
_mAppConfigUtils = getAppConfigUtils(this);
_mAppCacheUtils = getAppCacheUtils(this);
mReceiver = new GlobalApplicationReceiver(this);
mReceiver.registerAction();
}
public static AppConfigUtils getAppConfigUtils(Context context) {
if (_mAppConfigUtils == null) {
_mAppConfigUtils = AppConfigUtils.getInstance(context);
/**
* 释放通知管理工具资源
*/
private void releaseNotificationManager() {
LogUtils.d(TAG, "releaseNotificationManager() 开始释放通知工具");
if (mNotificationManager != null) {
mNotificationManager.release();
mNotificationManager = null;
LogUtils.d(TAG, "releaseNotificationManager() 通知工具资源已释放");
} else {
LogUtils.d(TAG, "releaseNotificationManager() 通知工具未初始化,无需释放");
}
return _mAppConfigUtils;
}
public static AppCacheUtils getAppCacheUtils(Context context) {
if (_mAppCacheUtils == null) {
_mAppCacheUtils = AppCacheUtils.getInstance(context);
// ===================== 私有工具方法区(辅助功能) =====================
/**
* 记录详细缓存状态(用于调试,监控极致强制缓存效果)
*/
private void logDetailedCacheStatus() {
LogUtils.d(TAG, "logDetailedCacheStatus() 开始记录详细缓存状态");
// Bitmap缓存状态
if (sBitmapCacheUtils != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存工具实例有效极致强制保持");
// 假设BitmapCacheUtils有获取缓存数量的方法
try {
int cacheCount = sBitmapCacheUtils.getCacheCount();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量" + cacheCount);
} catch (Exception e) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量获取失败不影响缓存异常信息" + e.getMessage());
}
}
return _mAppCacheUtils;
// 视图控件缓存状态
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存工具实例有效(极致强制保持)");
// 记录视图实例总数
int viewInstanceCount = MemoryCachedBackgroundView.getInstanceCount();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件实例总数:" + viewInstanceCount);
}
LogUtils.d(TAG, "logDetailedCacheStatus() 详细缓存状态记录完成,所有缓存均极致强制保持");
}
public void clearBatteryHistory() {
_mAppCacheUtils.clearBatteryHistory();
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}

View File

@@ -1,19 +1,19 @@
package cc.winboll.studio.powerbell;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.RelativeLayout;
import android.view.View;
import android.view.ViewStub;
import android.widget.LinearLayout;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.activitys.AboutActivity;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
@@ -26,37 +26,55 @@ import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
import cc.winboll.studio.powerbell.activities.SettingsActivity;
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity;
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.views.MainContentView;
/**
* 主活动修复小米广告SDK空Context崩溃问题
* 核心修改点:
* 1. 添加全局Context安全校验
* 2. 优化广告请求的生命周期判断
* 3. 确保广告操作在主线程执行
* 4. 完善广告资源释放逻辑
* 应用核心主活动
* 功能:管理电池监控、背景设置、服务启停、权限申请等核心功能
* 适配Java7 | API30 | 内存泄漏防护 | UI与服务状态实时同步
*/
public class MainActivity extends WinBoLLActivity {
public class MainActivity extends WinBoLLActivity implements MainContentView.OnViewActionListener {
// ======================== 静态常量(首屏排列,统一管理========================
// ======================== 静态常量(置顶统一,抽离魔法值========================
public static final String TAG = "MainActivity";
private static final int REQUEST_WRITE_STORAGE_PERMISSION = 1001;
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
private static final long DELAY_LOAD_NON_CRITICAL = 500L;
public static final int MSG_RELOAD_APPCONFIG = 0;
public static final int MSG_CURRENTVALUEBATTERY = 1;
public static final int MSG_LOAD_BACKGROUND = 2;
private static final int MSG_UPDATE_SERVICE_SWITCH = 3;
// ======================== 成员属性(按「静态→非静态」「核心→辅助」排序========================
public static MainActivity _mMainActivity; // 静态Activity实例全局调用
private App mApplication; // 应用Application实例
private Menu mMenu; // 顶部菜单实例
private Fragment mCurrentShowFragment; // 当前显示的Fragment
private MainViewFragment mMainViewFragment; // 主Fragment核心视图
private Toolbar mToolbar; // 顶部工具栏
private ADsBannerView mADsBannerView; // 广告Banner视图
// ======================== 静态成员(全局共享,严格管控生命周期========================
private static MainActivity sMainActivity;
private static Handler sGlobalHandler;
// ======================== 工具类实例(单例,避免重复初始化)========================
private PermissionUtils mPermissionUtils;
private AppConfigUtils mAppConfigUtils;
private BackgroundSourceUtils mBgSourceUtils;
// ======================== 重写父类抽象方法(优先排列,明确实现)========================
// ======================== 应用核心实例 =========================
private App mApplication;
private MainContentView mMainContentView;
private ControlCenterServiceBean mServiceControlBean;
// ======================== 基础视图组件 =========================
private Toolbar mToolbar;
private ViewStub mAdsViewStub;
private ADsBannerView mADsBannerView;
private Drawable mFrameDrawable;
private Menu mMenu;
// ======================== 生命周期方法(按系统调用顺序排列)========================
@Override
public Activity getActivity() {
return this;
@@ -69,76 +87,117 @@ public class MainActivity extends WinBoLLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreate(...)");
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate() | savedInstanceState=" + savedInstanceState);
initGlobalHandler();
setContentView(R.layout.activity_main);
// 初始化视图与实例
initView();
initInstance();
initFragment();
// 权限申请
PermissionUtils.getInstance().checkAndRequestStoragePermission(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 释放广告资源
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
}
// 置空静态引用,避免内存泄漏
_mMainActivity = null;
initPermissionUtils();
initMainContentView();
initCriticalView();
initCoreUtilsAsync();
loadNonCriticalViewDelayed();
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
reloadBackground();
LogUtils.d(TAG, "onPostCreate() | savedInstanceState=" + savedInstanceState);
mPermissionUtils.startPermissionRequest(this);
LogUtils.d(TAG, "onPostCreate: 发起权限申请");
}
@Override
protected void onResume() {
super.onResume();
reloadBackground();
setBackgroundColor();
// 恢复广告
LogUtils.d(TAG, "onResume()");
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
}
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
mADsBannerView.resumeADs(this);
}
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause()");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy()");
// 释放广告资源
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
mADsBannerView = null;
}
// 释放核心视图
if (mMainContentView != null) {
mMainContentView.releaseResources();
mMainContentView = null;
}
// 销毁Handler防止内存泄漏
if (sGlobalHandler != null) {
sGlobalHandler.removeCallbacksAndMessages(null);
sGlobalHandler = null;
}
// 释放Drawable
if (mFrameDrawable != null) {
mFrameDrawable.setCallback(null);
mFrameDrawable = null;
}
// 置空所有引用
sMainActivity = null;
mPermissionUtils = null;
mAppConfigUtils = null;
mBgSourceUtils = null;
mServiceControlBean = null;
mMenu = null;
mApplication = null;
mToolbar = null;
mAdsViewStub = null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "onActivityResult() | requestCode=" + requestCode + " | resultCode=" + resultCode + " | data=" + data);
mPermissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
if (requestCode == REQUEST_READ_MEDIA_IMAGES && sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
}
}
// ======================== 菜单与导航方法 ========================
@Override
public boolean onCreateOptionsMenu(Menu menu) {
LogUtils.d(TAG, "onCreateOptionsMenu() | menu=" + menu);
mMenu = menu;
// 主题菜单
AESThemeUtil.inflateMenu(this, menu);
// 调试工具菜单仅Debug模式显示
if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
}
// 应用核心菜单
getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int menuItemId = item.getItemId();
// 主题切换菜单
LogUtils.d(TAG, "onOptionsItemSelected() | itemId=" + item.getItemId());
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
recreate();
return true;
}
// 调试工具菜单
if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
return true;
}
// 应用核心菜单
switch (menuItemId) {
switch (item.getItemId()) {
case R.id.action_settings:
startActivity(new Intent(this, SettingsActivity.class));
break;
@@ -149,16 +208,16 @@ public class MainActivity extends WinBoLLActivity {
startActivity(new Intent(this, ClearRecordActivity.class));
break;
case R.id.action_changepicture:
startActivity(new Intent(this, BackgroundSettingsActivity.class));
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_READ_MEDIA_IMAGES);
break;
case R.id.action_unittestactivity:
startActivity(new Intent(this, MainUnitTestActivity.class));
break;
case R.id.action_unittest2activity:
startActivity(new Intent(this, MainUnitTest2Activity.class));
break;
case R.id.action_about:
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
APPInfo appInfo = genDefaultAPPInfo();
intent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo);
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class);
startAboutActivity();
break;
default:
return super.onOptionsItemSelected(item);
@@ -166,144 +225,324 @@ public class MainActivity extends WinBoLLActivity {
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
if (mCurrentShowFragment != mMainViewFragment) {
showFragment(mMainViewFragment);
} else {
moveTaskToBack(true);
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event);
}
@Override
public void setupToolbar() {
super.setupToolbar();
LogUtils.d(TAG, "setupToolbar()");
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
}
// ======================== 核心业务逻辑方法(按功能优先级排序)========================
/**
* 显示指定Fragment隐藏其他Fragment
*/
void showFragment(Fragment fragment) {
FragmentTransaction tx = getFragmentManager().beginTransaction();
for (Fragment item : getFragmentManager().getFragments()) {
tx.hide(item);
}
tx.show(fragment);
tx.commit();
mCurrentShowFragment = fragment;
@Override
public void onBackPressed() {
LogUtils.d(TAG, "onBackPressed()");
moveTaskToBack(true);
}
/**
* 刷新背景(全局静态调用,带生命周期校验)
*/
public static void reloadBackground() {
if (_mMainActivity != null && !_mMainActivity.isFinishing() && !_mMainActivity.isDestroyed()) {
_mMainActivity.mMainViewFragment.reloadBackground();
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
LogUtils.d(TAG, "dispatchKeyEvent() | event=" + event);
return super.dispatchKeyEvent(event);
}
// ======================== 核心初始化方法 ========================
private void initPermissionUtils() {
LogUtils.d(TAG, "initPermissionUtils()");
mPermissionUtils = PermissionUtils.getInstance();
}
private void initGlobalHandler() {
LogUtils.d(TAG, "initGlobalHandler()");
if (sGlobalHandler == null) {
sGlobalHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (sMainActivity == null || sMainActivity.isFinishing() || sMainActivity.isDestroyed()) {
LogUtils.w(TAG, "handleMessage: Activity已销毁跳过消息 | what=" + msg.what);
return;
}
LogUtils.d(TAG, "handleMessage() | what=" + msg.what);
switch (msg.what) {
case MSG_RELOAD_APPCONFIG:
sMainActivity.updateViewData();
break;
case MSG_CURRENTVALUEBATTERY:
if (sMainActivity.mMainContentView != null) {
sMainActivity.mMainContentView.updateCurrentBattery(msg.arg1);
LogUtils.d(TAG, "handleMessage: 更新当前电量 | value=" + msg.arg1);
}
break;
case MSG_LOAD_BACKGROUND:
sMainActivity.reloadBackground();
sMainActivity.setMainLayoutBackgroundColor();
break;
case MSG_UPDATE_SERVICE_SWITCH:
sMainActivity.updateServiceSwitchUI();
break;
}
}
};
}
}
/**
* 设置主页面背景颜色从BackgroundBean获取颜色值
*/
void setBackgroundColor() {
// 生命周期校验避免Activity销毁后操作UI
if (isFinishing() || isDestroyed()) {
private void initMainContentView() {
LogUtils.d(TAG, "initMainContentView()");
View rootView = findViewById(android.R.id.content);
mMainContentView = new MainContentView(this, rootView, this);
}
private void initCriticalView() {
LogUtils.d(TAG, "initCriticalView()");
sMainActivity = this;
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
if (mToolbar != null) {
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
}
mAdsViewStub = findViewById(R.id.stub_ads_banner);
}
private void initCoreUtilsAsync() {
LogUtils.d(TAG, "initCoreUtilsAsync()");
new Thread(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId=" + Thread.currentThread().getId());
mApplication = (App) getApplication();
mAppConfigUtils = AppConfigUtils.getInstance(getApplicationContext());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
// 初始化服务控制配置
mServiceControlBean = ControlCenterServiceBean.loadBean(getApplicationContext(), ControlCenterServiceBean.class);
if (mServiceControlBean == null) {
mServiceControlBean = new ControlCenterServiceBean(false);
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
}
// 根据配置启停服务
final boolean isServiceEnable = mServiceControlBean.isEnableService();
final boolean isServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置状态 | isServiceEnable=" + isServiceEnable + " | isServiceAlive=" + isServiceAlive);
if (isServiceEnable && !isServiceAlive) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ControlCenterService.startControlCenterService(getApplicationContext());
}
});
} else if (!isServiceEnable && isServiceAlive) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ControlCenterService.stopControlCenterService(getApplicationContext());
}
});
}
// 主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isFinishing() || isDestroyed()) {
LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁跳过UI更新");
return;
}
// 加载框架背景适配API23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame, getTheme());
} else {
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame);
}
updateViewData();
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
}
});
}
}).start();
}
private void loadNonCriticalViewDelayed() {
LogUtils.d(TAG, "loadNonCriticalViewDelayed() | 延迟时长=" + DELAY_LOAD_NON_CRITICAL + "ms");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (isFinishing() || isDestroyed()) {
LogUtils.w(TAG, "loadNonCriticalViewDelayed: Activity已销毁跳过广告加载");
return;
}
loadAdsView();
}
}, DELAY_LOAD_NON_CRITICAL);
}
// ======================== 视图操作方法 ========================
private void loadAdsView() {
LogUtils.d(TAG, "loadAdsView()");
if (mAdsViewStub == null) {
LogUtils.e(TAG, "loadAdsView: 广告ViewStub为空加载失败");
return;
}
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
BackgroundBean bean = utils.getCurrentBackgroundBean();
int nPixelColor = bean.getPixelColor();
RelativeLayout mainLayout = findViewById(R.id.activitymainRelativeLayout1);
if (mainLayout != null) {
mainLayout.setBackgroundColor(nPixelColor);
if (mADsBannerView == null) {
View adsView = mAdsViewStub.inflate();
mADsBannerView = adsView.findViewById(R.id.adsbanner);
}
}
/**
* 通过Uri获取文件本地真实路径兼容图片选择等场景
*/
private String getRealPathFromURI(Uri contentUri) {
String[] proj = {MediaStore.MediaColumns.DATA};
Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
if (cursor != null && cursor.moveToNext()) {
int nColumnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (nColumnIndex > -1) {
String path = cursor.getString(nColumnIndex);
cursor.close();
return path;
} else {
LogUtils.d(TAG, "getRealPathFromURI nColumnIndex is -1.");
private void updateViewData() {
LogUtils.d(TAG, "updateViewData()");
if (mMainContentView == null || mFrameDrawable == null) {
LogUtils.e(TAG, "updateViewData: 核心视图或框架背景为空,更新失败");
return;
}
mMainContentView.updateViewData(mFrameDrawable);
}
private void reloadBackground() {
LogUtils.d(TAG, "reloadBackground()");
if (mMainContentView == null || mBgSourceUtils == null) {
LogUtils.e(TAG, "reloadBackground: 核心视图或背景工具类为空,加载失败");
return;
}
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
if (currentBgBean != null) {
mMainContentView.backgroundView.loadByBackgroundBean(currentBgBean);
} else {
mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background);
}
}
private void setMainLayoutBackgroundColor() {
LogUtils.d(TAG, "setMainLayoutBackgroundColor()");
if (isFinishing() || isDestroyed() || mMainContentView == null || mBgSourceUtils == null) {
LogUtils.e(TAG, "setMainLayoutBackgroundColor: 上下文无效,设置失败");
return;
}
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
if (currentBgBean != null) {
mMainContentView.mainLayout.setBackgroundColor(currentBgBean.getPixelColor());
LogUtils.d(TAG, "setMainLayoutBackgroundColor: 主布局背景色设置完成 | color=" + currentBgBean.getPixelColor());
}
}
private void updateServiceSwitchUI() {
LogUtils.d(TAG, "updateServiceSwitchUI()");
if (mMainContentView == null || mServiceControlBean == null) {
LogUtils.e(TAG, "updateServiceSwitchUI: 核心视图或服务配置为空,更新失败");
return;
}
boolean configEnabled = mServiceControlBean.isEnableService();
mMainContentView.setServiceSwitchEnabled(false);
mMainContentView.setServiceSwitchChecked(configEnabled);
mMainContentView.setServiceSwitchEnabled(true);
}
// ======================== 服务与线程管理方法 ========================
private void toggleServiceEnableState(boolean isEnable) {
LogUtils.d(TAG, "toggleServiceEnableState() | 目标状态=" + isEnable);
if (mServiceControlBean == null) {
LogUtils.e(TAG, "toggleServiceEnableState: 服务配置为空,切换失败");
return;
}
mServiceControlBean.setIsEnableService(isEnable);
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
// UI开关联动服务启停
if (isEnable) {
if (!ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName())) {
ControlCenterService.startControlCenterService(getApplicationContext());
}
cursor.close();
} else {
ControlCenterService.stopControlCenterService(getApplicationContext());
}
return null;
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
}
/**
* 生成默认APP信息用于关于页面
*/
APPInfo genDefaultAPPInfo() {
String szBranchName = "powerbell";
// ======================== 页面跳转方法 ========================
private void startAboutActivity() {
LogUtils.d(TAG, "startAboutActivity()");
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
APPInfo appInfo = genDefaultAppInfo();
aboutIntent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo);
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class);
}
// ======================== 消息发送方法 ========================
private void notifyServiceAppConfigChange() {
LogUtils.d(TAG, "notifyServiceAppConfigChange()");
ControlCenterService.sendAppConfigStatusUpdateMessage(this);
reloadAppConfig();
}
public static void reloadAppConfig() {
LogUtils.d(TAG, "reloadAppConfig()");
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_RELOAD_APPCONFIG);
} else {
LogUtils.w(TAG, "reloadAppConfig: 全局Handler为空消息发送失败");
}
}
public static void sendCurrentBatteryValueMessage(int value) {
LogUtils.d(TAG, "sendCurrentBatteryValueMessage() | 电量=" + value);
if (sGlobalHandler != null) {
Message msg = sGlobalHandler.obtainMessage(MSG_CURRENTVALUEBATTERY);
msg.arg1 = value;
sGlobalHandler.sendMessage(msg);
} else {
LogUtils.w(TAG, "sendCurrentBatteryValueMessage: 全局Handler为空消息发送失败");
}
}
// ======================== 辅助工具方法 ========================
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo()");
String branchName = "powerbell";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_launcher);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(szBranchName);
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
appInfo.setAppAPKName("PowerBell");
appInfo.setAppAPKFolderName("PowerBell");
return appInfo;
}
// ======================== 内部初始化辅助方法(私有,统一放在最后)========================
/**
* 初始化视图控件findViewById
*/
private void initView() {
mADsBannerView = findViewById(R.id.adsbanner);
mToolbar = findViewById(R.id.toolbar);
// ======================== MainContentView 事件回调 ========================
@Override
public void onChargeReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onChargeReminderSwitchChanged() | isChecked=" + isChecked);
notifyServiceAppConfigChange();
}
/**
* 初始化实例Application、工具类等
*/
private void initInstance() {
_mMainActivity = this;
mApplication = (App) getApplication();
// 初始化工具栏
setSupportActionBar(mToolbar);
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
@Override
public void onUsageReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onUsageReminderSwitchChanged() | isChecked=" + isChecked);
notifyServiceAppConfigChange();
}
/**
* 初始化主Fragment添加并显示
*/
private void initFragment() {
if (mMainViewFragment == null) {
FragmentTransaction tx = getFragmentManager().beginTransaction();
mMainViewFragment = new MainViewFragment();
tx.add(R.id.activitymainFrameLayout1, mMainViewFragment, MainViewFragment.TAG);
tx.commit();
}
showFragment(mMainViewFragment);
@Override
public void onServiceSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onServiceSwitchChanged() | isChecked=" + isChecked);
toggleServiceEnableState(isChecked);
}
@Override
public void onChargeReminderProgressChanged(int progress) {
LogUtils.d(TAG, "onChargeReminderProgressChanged() | progress=" + progress);
notifyServiceAppConfigChange();
}
@Override
public void onUsageReminderProgressChanged(int progress) {
LogUtils.d(TAG, "onUsageReminderProgressChanged() | progress=" + progress);
notifyServiceAppConfigChange();
}
}

View File

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

View File

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

View File

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

View File

@@ -6,41 +6,61 @@ import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/**
* 应用设置窗口,提供应用配置项的统一入口
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/27 14:26
* @Describe 应用设置窗口
*/
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量 =========================
public static final String TAG = "SettingsActivity";
// 权限请求常量(为后续读取媒体图片权限预留)
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
private Toolbar mToolbar;
// ======================== 成员变量 =========================
private Toolbar mToolbar; // 顶部工具栏
@Override
public Activity getActivity() {
return this;
}
// ======================== 接口实现方法 =========================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
public String getTag() {
return TAG;
}
// ======================== 生命周期方法 =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化开始");
mToolbar = findViewById(R.id.toolbar);
// 初始化工具栏
initToolbar();
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
}
// ======================== UI初始化方法 =========================
/**
* 初始化顶部工具栏,设置导航返回与样式
*/
private void initToolbar() {
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
// 设置工具栏副标题与标题样式
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// 显示返回按钮
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// 绑定导航点击事件
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -48,12 +68,7 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
finish();
}
});
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
}
public void onCheckPermission(View view) {
//ToastUtils.show("onCheckPermission");
if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
ToastUtils.show("【权限检查】存储权限已全部获取");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,733 @@
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;
import com.a4455jkjh.colorpicker.view.OnColorChangedListener;
/**
* 调色板对话框支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
* 适配 API30基于 Java7 开发,返回 0xAARRGGBB 格式颜色(含透明度)
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/16 11:47
* @Describe 调色板对话框支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
*/
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
// ====================== 静态常量(首屏可见,统一管理) ======================
public static final String TAG = "ColorPaletteDialog";
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值0-255
private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比100%,无调节)
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%=完全透明)
private static final String FORMAT_COLOR_HEX = "#%08X"; // 颜色值格式化AARRGGBB
private static final String FORMAT_PERCENT = "%d%%"; // 百分比格式化X%
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
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. 解析初始颜色:原始基准值 = 实时值(初始无调节)
this.mOriginalAlpha = Color.alpha(initialColor);
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
this.mCurrentAlpha = mOriginalAlpha;
this.mCurrentAlphaPercent = mOriginalAlphaPercent;
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, String.format("init dialog success | 初始颜色:%s | 原始RGB%d,%d,%d | 原始透明度:%s | 初始亮度:%s",
String.format(FORMAT_COLOR_HEX, initialColor),
mOriginalR, mOriginalG, mOriginalB,
String.format(FORMAT_PERCENT, mOriginalAlphaPercent),
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
}
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
@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(FORMAT_COLOR_HEX, mCurrentColor));
// 4. 透明度控件(进度条+文本,初始=原始透明度)
sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
// 5. 亮度控件显示默认100%,初始化按钮状态)
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
updateBrightnessBtnStatus(); // 禁用边界值按钮
LogUtils.d(TAG, String.format("init data complete | 原始透明度:%s",
String.format(FORMAT_PERCENT, 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, String.format("update alpha by seekbar | 透明度:%s",
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
} finally {
isAppSelfUpdatingColor = false; // 释放标记
}
}
}
// ====================== 颜色核心逻辑 ======================
/**
* 核心计算基于原始RGB+当前亮度+当前透明度计算实时RGB+最终颜色
*/
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, String.format("%s brightness | 亮度:%s | 实时RGB%d,%d,%d",
isIncrease ? "increase" : "decrease",
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent),
mCurrentR, mCurrentG, mCurrentB));
} finally {
isAppSelfUpdatingColor = false;
}
}
}
/**
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB更新原始基准值+实时值)
*/
private void parseColorFromStr(String colorStr, int triggerViewId) {
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true;
try {
if (TextUtils.isEmpty(colorStr)) return;
// 补全#前缀(兼容用户输入习惯)
if (!colorStr.startsWith("#")) {
colorStr = "#" + colorStr;
}
// 格式校验仅支持6位RRGGBB/8位AARRGGBB
if (colorStr.length() != 7 && colorStr.length() != 9) {
LogUtils.e(TAG, String.format("parse color failed | 格式错误(需#RRGGBB/#AARRGGBB输入%s", colorStr));
return;
}
// 解析颜色
int parsedColor = Color.parseColor(colorStr);
// 更新原始基准值与实时值
mOriginalAlpha = Color.alpha(parsedColor);
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
mOriginalR = Color.red(parsedColor);
mOriginalG = Color.green(parsedColor);
mOriginalB = Color.blue(parsedColor);
mCurrentAlpha = mOriginalAlpha;
mCurrentAlphaPercent = mOriginalAlphaPercent;
mCurrentR = mOriginalR;
mCurrentG = mOriginalG;
mCurrentB = mOriginalB;
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = parsedColor;
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, String.format("parse color success | 解析颜色:%s | 透明度:%s | 重置亮度:%s",
String.format(FORMAT_COLOR_HEX, parsedColor),
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, String.format("parse color failed | 非法颜色格式,输入:%s", colorStr), e);
} finally {
isAppSelfUpdatingColor = false;
}
}
}
/**
* 通过RGB输入框更新颜色用户输入后更新原始基准值+实时值重置亮度为100%
*/
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;
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, String.format("update color by RGB | 新原始RGB%d,%d,%d | 透明度:%s | 重置亮度:%s",
mOriginalR, mOriginalG, mOriginalB,
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
} catch (Exception e) {
LogUtils.e(TAG, "update color by RGB failed", e);
} finally {
isAppSelfUpdatingColor = false;
}
}
}
/**
* 核心同步:更新所有控件显示
*/
private void updateAllViews() {
// 1. 同步颜色预览
ivColorPicker.setBackgroundColor(mCurrentColor);
// 2. 同步RGB输入框
etR.setText(String.valueOf(mCurrentR));
etG.setText(String.valueOf(mCurrentG));
etB.setText(String.valueOf(mCurrentB));
// 3. 同步颜色值输入框
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
// 4. 同步透明度控件
sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
// 5. 同步亮度控件
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
updateBrightnessBtnStatus();
LogUtils.d(TAG, String.format("sync all views complete | 最终颜色:%s | 实时RGB%d,%d,%d | 透明度:%s | 亮度:%s",
String.format(FORMAT_COLOR_HEX, mCurrentColor),
mCurrentR, mCurrentG, mCurrentB,
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
}
/**
* 更新亮度按钮状态(边界值禁用,提升交互体验)
*/
private void updateBrightnessBtnStatus() {
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, String.format("parse input failed | 非法数字,输入:%s", 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无高版本依赖小米机型适配
*/
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 = {
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
};
// 1. 第一级:水平滚动容器
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
horizontalScrollView.setHorizontalScrollBarEnabled(true);
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
// 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);
// 内置圆形背景(白色边框+圆形形状)
GradientDrawable circleBg = new GradientDrawable();
circleBg.setShape(GradientDrawable.OVAL);
circleBg.setColor(color);
circleBg.setStroke(dp2px(2), Color.WHITE);
colorBtn.setBackground(circleBg);
colorBtn.setClickable(true);
colorBtn.setFocusable(true);
// 点击事件
colorBtn.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, String.format("select system color | 选择颜色:%s | 透明度:%s",
String.format(FORMAT_COLOR_HEX, color),
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
} finally {
isAppSelfUpdatingColor = false;
}
}
}
});
colorLayout.addView(colorBtn);
}
// 层级嵌套
horizontalScrollView.addView(colorLayout);
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
}
// ====================== 点击事件实现 ======================
@Override
public void onClick(View v) {
int id = v.getId();
// 所有点击事件均加并发判断
if (!isAppSelfUpdatingColor) {
if (id == R.id.iv_color_picker) {
showSystemColorPicker();
} else if (id == R.id.iv_color_scaler) {
openColorScalerDialog(mCurrentColor);
} else if (id == R.id.tv_confirm) {
mListener.onColorSelected(mCurrentColor);
LogUtils.d(TAG, String.format("confirm color | 回调颜色:%s",
String.format(FORMAT_COLOR_HEX, mCurrentColor)));
dismiss();
} else if (id == R.id.tv_cancel) {
dismiss();
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) {
LogUtils.d(TAG, String.format("openColorScalerDialog | 初始颜色:%s",
String.format(FORMAT_COLOR_HEX, nColor)));
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
dlg.setOnColorChangedListener(new OnColorChangedListener() {
@Override
public void beforeColorChanged() {}
@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);
this.currentColorScalerDialogColor = p;
}
@Override
public void dismiss() {
super.dismiss();
int color = currentColorScalerDialogColor;
ToastUtils.show(String.format("选择颜色:%s", String.format(FORMAT_COLOR_HEX, color)));
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true;
try {
mOriginalAlpha = Color.alpha(color);
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
mOriginalR = Color.red(color);
mOriginalG = Color.green(color);
mOriginalB = Color.blue(color);
mCurrentAlpha = mOriginalAlpha;
mCurrentAlphaPercent = mOriginalAlphaPercent;
mCurrentR = mOriginalR;
mCurrentG = mOriginalG;
mCurrentB = mOriginalB;
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = color;
updateAllViews();
LogUtils.d(TAG, String.format("select scaler color | 选择颜色:%s | 透明度:%s",
String.format(FORMAT_COLOR_HEX, color),
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
} finally {
isAppSelfUpdatingColor = false;
}
}
}
}
}

View File

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

View File

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

View File

@@ -1,367 +0,0 @@
package cc.winboll.studio.powerbell.fragments;
import android.app.Fragment;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import cc.winboll.studio.powerbell.views.BatteryDrawable;
import cc.winboll.studio.powerbell.views.VerticalSeekBar;
public class MainViewFragment extends Fragment {
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());
utils.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

@@ -2,35 +2,119 @@ package cc.winboll.studio.powerbell.handlers;
import android.os.Handler;
import android.os.Message;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import java.lang.ref.WeakReference;
/**
* 服务通信Handler
* 功能:处理电量提醒消息,构建并发送标准化通知
* 特性:弱引用防泄漏、参数严格校验、通知格式统一
* 适配Java7 | API30 | 小米手机
*/
public class ControlCenterServiceHandler extends Handler {
public static final String TAG = ControlCenterServiceHandler.class.getSimpleName();
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "ControlCenterServiceHandler";
public static final int MSG_REMIND_TEXT = 1001; // 电量提醒消息标识
public static final int MSG_REMIND_TEXT = 0;
// 提醒类型常量
private static final String REMIND_TYPE_CHARGE = "+";
private static final String REMIND_TYPE_USAGE = "-";
WeakReference<ControlCenterService> serviceWeakReference;
// 电量范围常量
private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100;
// 通知文案常量(抽离魔法值,便于统一修改)
private static final String CHARGE_REMIND_TITLE = "充电提醒";
private static final String USAGE_REMIND_TITLE = "耗电提醒";
private static final String CHARGE_REMIND_CONTENT_FORMAT = "(+)电量已达额定值。当前电量%d%%%s。";
private static final String USAGE_REMIND_CONTENT_FORMAT = "(-)电量低于指定值。当前电量%d%%%s。";
private static final String CHARGE_STATE_CHARGING = "充电中";
private static final String CHARGE_STATE_NOT_CHARGING = "未充电";
// ================================== 成员变量区弱引用防泄漏final保证不可变=================================
private final WeakReference<ControlCenterService> mwrControlCenterService;
// ================================== 构造方法(强制传入服务,初始化弱引用)=================================
public ControlCenterServiceHandler(ControlCenterService service) {
serviceWeakReference = new WeakReference<ControlCenterService>(service);
LogUtils.d(TAG, "构造方法执行 | service=" + (service != null ? service.getClass().getSimpleName() : "null"));
this.mwrControlCenterService = new WeakReference<>(service);
}
// ================================== 核心消息处理重写handleMessage解析多参数消息=================================
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 解析消息参数obj=提醒类型(+/-)arg1=当前电量arg2=充电状态(1=充电/0=未充电)
String remindType = (msg.obj != null) ? (String) msg.obj : "";
int currentBattery = msg.arg1;
boolean isCharging = msg.arg2 == 1;
LogUtils.d(TAG, "handleMessage: 接收消息 | what=" + msg.what + " | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
// 弱引用获取服务,避免内存泄漏
ControlCenterService service = mwrControlCenterService.get();
if (service == null) {
LogUtils.e(TAG, "handleMessage: 服务实例已被GC回收终止消息处理");
return;
}
// 按消息类型分发处理
switch (msg.what) {
case MSG_REMIND_TEXT: // 处理下载完成消息更新UI
{
// 显示提醒消息
//
//LogUtils.d(TAG, "显示提醒消息");
ControlCenterService controlCenterService = serviceWeakReference.get();
if (controlCenterService != null) {
//LogUtils.d(TAG, ((NotificationMessage)msg.obj).getTitle());
//LogUtils.d(TAG, ((NotificationMessage)msg.obj).getContent());
controlCenterService.appenRemindMSG((String)msg.obj);
}
break;
}
case MSG_REMIND_TEXT:
handleRemindMessage(service, remindType, currentBattery, isCharging);
break;
default:
LogUtils.w(TAG, "handleMessage: 未知消息类型,忽略处理 | what=" + msg.what);
break;
}
}
// ================================== 业务辅助方法(构建通知并发送,全链路参数校验)=================================
/**
* 处理电量提醒消息,构建带电量+充电状态的通知并发送
* @param service 控制中心服务实例(已校验非空)
* @param remindType 提醒类型(+充电/-耗电)
* @param currentBattery 当前电量0-100
* @param isCharging 充电状态
*/
private void handleRemindMessage(ControlCenterService service, String remindType, int currentBattery, boolean isCharging) {
LogUtils.d(TAG, "handleRemindMessage: 开始处理提醒消息 | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
// 1. 前置校验:通知工具类+参数有效性
if (service.getNotificationManager() == null) {
LogUtils.e(TAG, "handleRemindMessage: 通知管理工具类未初始化,无法发送提醒");
return;
}
if (!REMIND_TYPE_CHARGE.equals(remindType) && !REMIND_TYPE_USAGE.equals(remindType)) {
LogUtils.w(TAG, "handleRemindMessage: 提醒类型无效,忽略 | type=" + remindType + " | 允许值:" + REMIND_TYPE_CHARGE + "/" + REMIND_TYPE_USAGE);
return;
}
if (currentBattery < BATTERY_LEVEL_MIN || currentBattery > BATTERY_LEVEL_MAX) {
LogUtils.w(TAG, "handleRemindMessage: 电量值超出范围,忽略 | battery=" + currentBattery + " | 允许范围:" + BATTERY_LEVEL_MIN + "-" + BATTERY_LEVEL_MAX);
return;
}
// 2. 构建通知模型,使用统一格式
NotificationMessage remindMsg = new NotificationMessage();
String chargeStateDesc = isCharging ? CHARGE_STATE_CHARGING : CHARGE_STATE_NOT_CHARGING;
if (REMIND_TYPE_CHARGE.equals(remindType)) {
remindMsg.setTitle(CHARGE_REMIND_TITLE);
remindMsg.setContent(String.format(CHARGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc));
remindMsg.setRemindMSG("charge_remind");
} else {
remindMsg.setTitle(USAGE_REMIND_TITLE);
remindMsg.setContent(String.format(USAGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc));
remindMsg.setRemindMSG("usage_remind");
}
LogUtils.d(TAG, "handleRemindMessage: 通知模型构建完成 | title=" + remindMsg.getTitle() + " | content=" + remindMsg.getContent());
// 3. 调用通知工具类发送提醒
LogUtils.d(TAG, "handleRemindMessage: 调用通知工具类发送提醒 | remindMSG=" + remindMsg.getRemindMSG());
service.getNotificationManager().showRemindNotification(service, remindMsg);
LogUtils.d(TAG, "handleRemindMessage: 提醒通知发送流程执行完毕");
}
}

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

@@ -1,26 +0,0 @@
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 14:30:51
* @Describe 电池报告数据模型
*/
public class BatteryData {
public static final String TAG = "BatteryData";
private int currentLevel;
private String dischargeTime;
private String chargeTime;
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) {
this.currentLevel = currentLevel;
this.dischargeTime = dischargeTime;
this.chargeTime = chargeTime;
}
public int getCurrentLevel() { return currentLevel; }
public String getDischargeTime() { return dischargeTime; }
public String getChargeTime() { return chargeTime; }
}

View File

@@ -1,75 +0,0 @@
package cc.winboll.studio.powerbell.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.io.Serializable;
public class BatteryInfoBean extends BaseBean implements Serializable {
public static final String TAG = "BatteryInfoBean";
// 记录电量的时间戳
long timeStamp;
// 电量值
int battetyValue;
public BatteryInfoBean() {
this.timeStamp = 0;
this.battetyValue = 0;
}
public BatteryInfoBean(long timeStamp, int battetyValue) {
this.timeStamp = timeStamp;
this.battetyValue = battetyValue;
}
public void setTimeStamp(long timeStamp) {
this.timeStamp = timeStamp;
}
public long getTimeStamp() {
return timeStamp;
}
public void setBattetyValue(int battetyValue) {
this.battetyValue = battetyValue;
}
public int getBattetyValue() {
return battetyValue;
}
@Override
public String getName() {
return BatteryInfoBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
BatteryInfoBean bean = this;
jsonWriter.name("timeStamp").value(bean.getTimeStamp());
jsonWriter.name("battetyValue").value(bean.getBattetyValue());
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
BatteryInfoBean bean = new BatteryInfoBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("timeStamp")) {
bean.setTimeStamp(jsonReader.nextLong());
} else if (name.equals("battetyValue")) {
bean.setBattetyValue(jsonReader.nextInt());
} else {
jsonReader.skipValue();
}
}
// 结束 JSON 对象
jsonReader.endObject();
return bean;
}
}

View File

@@ -1,63 +0,0 @@
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 07:06:07
* @Describe 服务控制参数
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
public class ControlCenterServiceBean extends BaseBean {
public static final String TAG = "ControlCenterServiceBean";
boolean isEnableService = false;
public ControlCenterServiceBean() {
this.isEnableService = false;
}
public ControlCenterServiceBean(boolean isEnableService) {
this.isEnableService = isEnableService;
}
public void setIsEnableService(boolean isEnableService) {
this.isEnableService = isEnableService;
}
public boolean isEnableService() {
return isEnableService;
}
@Override
public String getName() {
return ControlCenterServiceBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
ControlCenterServiceBean bean = this;
jsonWriter.name("isEnableService").value(bean.isEnableService());
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
ControlCenterServiceBean bean = new ControlCenterServiceBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("isEnableService")) {
bean.setIsEnableService(jsonReader.nextBoolean());
} else {
jsonReader.skipValue();
}
}
// 结束 JSON 对象
jsonReader.endObject();
return bean;
}
}

View File

@@ -1,40 +0,0 @@
package cc.winboll.studio.powerbell.model;
// 应用消息结构
//
public class NotificationMessage {
String Title;
String Content;
String RemindMSG;
public NotificationMessage(String title, String content) {
Title = title;
Content = content;
}
public void setRemindMSG(String remindMSG) {
RemindMSG = remindMSG;
}
public String getRemindMSG() {
return RemindMSG;
}
public void setTitle(String title) {
Title = title;
}
public String getTitle() {
return Title;
}
public void setContent(String content) {
Content = content;
}
public String getContent() {
return Content;
}
}

View File

@@ -0,0 +1,280 @@
package cc.winboll.studio.powerbell.models;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
import java.io.Serializable;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类
* 适配 API30支持 Serializable 持久化、Parcelable Intent 传递、JSON 序列化/反序列化
* 包含耗电提醒、充电提醒、电量检测、铃声提醒等核心配置
*/
public class AppConfigBean extends BaseBean implements Serializable, Parcelable {
// ====================== 静态常量(首屏可见,统一管理) ======================
// 序列化版本号Serializable 必备,避免反序列化失败)
private static final long serialVersionUID = 1L;
// 日志标签(全局统一,替换 Log 为 LogUtils
transient public static final String TAG = "AppConfigBean";
// 字段校验常量(统一阈值,避免硬编码)
private static final int MIN_INTERVAL = 500; // 最小检测间隔ms
private static final int MIN_REMIND_INTERVAL = 1000; // 最小提醒间隔ms
private static final int BATTERY_MIN = 0; // 电量最小值
private static final int BATTERY_MAX = 100; // 电量最大值
private static final int INVALID_BATTERY = -1; // 无效电量标识
// ====================== 成员变量(按功能分类:提醒配置→电量状态→检测配置) ======================
// 耗电提醒配置
boolean isEnableUsageReminder = false; // 耗电提醒开关
int usageReminderValue = 45; // 耗电提醒阈值0-100
// 充电提醒配置
boolean isEnableChargeReminder = false;// 充电提醒开关
int chargeReminderValue = 100; // 充电提醒阈值0-100
// 铃声提醒配置
int reminderIntervalTime = 5000; // 铃声提醒间隔ms
// 电量状态
boolean isCharging = false; // 是否充电
int currentBatteryValue = INVALID_BATTERY; // 当前电池电量(统一命名,替代原 currentValue
// 电量检测配置
int batteryDetectInterval = 2000; // 电量检测间隔ms适配 RemindThread
// ====================== 构造方法(初始化默认配置,强化默认值校验) ======================
public AppConfigBean() {
setChargeReminderValue(100);
setEnableChargeReminder(false);
setUsageReminderValue(10);
setEnableUsageReminder(false);
setReminderIntervalTime(5000);
setBatteryDetectInterval(1000); // 默认检测间隔1秒
setCurrentBatteryValue(INVALID_BATTERY); // 初始化无效电量标识
LogUtils.d(TAG, "AppConfigBean: 初始化默认配置完成");
}
// ====================== 核心业务方法Setter/Getter按字段功能分类补充调试日志 ======================
// --------------- 电量状态相关 ---------------
/**
* 设置当前电池电量Receiver 监听电池变化时调用)
* @param currentBatteryValue 当前电量0-100
*/
public void setCurrentBatteryValue(int currentBatteryValue) {
this.currentBatteryValue = (currentBatteryValue >= BATTERY_MIN && currentBatteryValue <= BATTERY_MAX)
? currentBatteryValue : INVALID_BATTERY;
LogUtils.d(TAG, String.format("setCurrentBatteryValue: 当前电量设置为 %d输入值%d",
this.currentBatteryValue, currentBatteryValue));
}
/**
* 获取当前电池电量RemindThread 同步配置时调用)
* @return 当前电量0-100 或 INVALID_BATTERY
*/
public int getCurrentBatteryValue() {
return currentBatteryValue;
}
// --------------- 铃声提醒配置相关 ---------------
/**
* 设置铃声提醒间隔
* @param reminderIntervalTime 提醒间隔ms不小于 MIN_REMIND_INTERVAL
*/
public void setReminderIntervalTime(int reminderIntervalTime) {
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
LogUtils.d(TAG, String.format("setReminderIntervalTime: 提醒间隔设置为 %dms输入值%d",
this.reminderIntervalTime, reminderIntervalTime));
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
// --------------- 充电状态相关 ---------------
/**
* 设置是否充电
* @param isCharging 充电状态
*/
public void setIsCharging(boolean isCharging) {
this.isCharging = isCharging;
LogUtils.d(TAG, String.format("setIsCharging: 充电状态设置为 %b", isCharging));
}
public boolean isCharging() {
return isCharging;
}
// --------------- 耗电提醒配置相关 ---------------
public void setEnableUsageReminder(boolean isEnableUsageReminder) {
this.isEnableUsageReminder = isEnableUsageReminder;
LogUtils.d(TAG, String.format("setEnableUsageReminder: 耗电提醒开关设置为 %b", isEnableUsageReminder));
}
public boolean isEnableUsageReminder() {
return isEnableUsageReminder;
}
/**
* 设置耗电提醒阈值
* @param usageReminderValue 阈值0-100
*/
public void setUsageReminderValue(int usageReminderValue) {
this.usageReminderValue = Math.min(Math.max(usageReminderValue, BATTERY_MIN), BATTERY_MAX);
LogUtils.d(TAG, String.format("setUsageReminderValue: 耗电提醒阈值设置为 %d输入值%d",
this.usageReminderValue, usageReminderValue));
}
public int getUsageReminderValue() {
return usageReminderValue;
}
// --------------- 充电提醒配置相关 ---------------
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
this.isEnableChargeReminder = isEnableChargeReminder;
LogUtils.d(TAG, String.format("setEnableChargeReminder: 充电提醒开关设置为 %b", isEnableChargeReminder));
}
public boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
/**
* 设置充电提醒阈值
* @param chargeReminderValue 阈值0-100
*/
public void setChargeReminderValue(int chargeReminderValue) {
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, BATTERY_MIN), BATTERY_MAX);
LogUtils.d(TAG, String.format("setChargeReminderValue: 充电提醒阈值设置为 %d输入值%d",
this.chargeReminderValue, chargeReminderValue));
}
public int getChargeReminderValue() {
return chargeReminderValue;
}
// --------------- 电量检测配置相关 ---------------
/**
* 设置电量检测间隔
* @param batteryDetectInterval 检测间隔ms不小于 MIN_INTERVAL
*/
public void setBatteryDetectInterval(int batteryDetectInterval) {
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
LogUtils.d(TAG, String.format("setBatteryDetectInterval: 检测间隔设置为 %dms输入值%d",
this.batteryDetectInterval, batteryDetectInterval));
}
public int getBatteryDetectInterval() {
return batteryDetectInterval;
}
// ====================== JSON 序列化/反序列化(兼容旧配置,补充调试日志) ======================
@Override
public String getName() {
return AppConfigBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
AppConfigBean bean = this;
// 原有字段序列化
jsonWriter.name("isEnableUsageReminder").value(bean.isEnableUsageReminder());
jsonWriter.name("usageReminderValue").value(bean.getUsageReminderValue());
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
jsonWriter.name("reminderIntervalTime").value(bean.getReminderIntervalTime());
jsonWriter.name("isCharging").value(bean.isCharging());
// 兼容旧字段 currentValue同步新字段 currentBatteryValue
jsonWriter.name("currentBatteryValue").value(bean.getCurrentBatteryValue());
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue());
// 新增字段序列化
jsonWriter.name("batteryDetectInterval").value(bean.getBatteryDetectInterval());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成");
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
AppConfigBean bean = new AppConfigBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
// 兼容拼写错误字段isEnableUsegeReminder → isEnableUsageReminder
if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) {
bean.setEnableUsageReminder(jsonReader.nextBoolean());
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
bean.setUsageReminderValue(jsonReader.nextInt());
} else if (name.equals("isEnableChargeReminder")) {
bean.setEnableChargeReminder(jsonReader.nextBoolean());
} else if (name.equals("chargeReminderValue")) {
bean.setChargeReminderValue(jsonReader.nextInt());
} else if (name.equals("reminderIntervalTime")) {
bean.setReminderIntervalTime(jsonReader.nextInt());
} else if (name.equals("isCharging")) {
bean.setIsCharging(jsonReader.nextBoolean());
} else if (name.equals("currentValue")) {
// 优先读取旧字段,兼容历史配置
bean.setCurrentBatteryValue(jsonReader.nextInt());
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 currentValue 完成");
} else if (name.equals("currentBatteryValue")) {
// 新字段覆盖旧字段,保证数据最新
bean.setCurrentBatteryValue(jsonReader.nextInt());
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 currentBatteryValue 完成");
} else if (name.equals("batteryDetectInterval")) {
bean.setBatteryDetectInterval(jsonReader.nextInt());
} else {
jsonReader.skipValue();
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
return bean;
}
// ====================== Parcelable 接口实现API30 Intent 传递必备,补充调试日志) ======================
@Override
public int describeContents() {
return 0; // 无特殊内容描述固定返回0
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// 按成员变量顺序写入boolean 转 byte 存储
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0));
dest.writeInt(usageReminderValue);
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0));
dest.writeInt(chargeReminderValue);
dest.writeInt(reminderIntervalTime);
dest.writeByte((byte) (isCharging ? 1 : 0));
dest.writeInt(currentBatteryValue);
dest.writeInt(batteryDetectInterval);
LogUtils.d(TAG, "writeToParcel: Parcel 序列化完成");
}
// 反序列化 Creator必须 public static final 修饰Java7 适配)
public static final Parcelable.Creator<AppConfigBean> CREATOR = new Parcelable.Creator<AppConfigBean>() {
@Override
public AppConfigBean createFromParcel(Parcel source) {
AppConfigBean bean = new AppConfigBean();
// 按 writeToParcel 顺序读取
bean.isEnableUsageReminder = source.readByte() != 0;
bean.usageReminderValue = source.readInt();
bean.isEnableChargeReminder = source.readByte() != 0;
bean.chargeReminderValue = source.readInt();
bean.reminderIntervalTime = source.readInt();
bean.isCharging = source.readByte() != 0;
bean.currentBatteryValue = source.readInt();
bean.batteryDetectInterval = source.readInt();
LogUtils.d(TAG, "createFromParcel: Parcel 反序列化完成");
return bean;
}
@Override
public AppConfigBean[] newArray(int size) {
return new AppConfigBean[size];
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,83 +5,254 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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.models.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import java.lang.ref.WeakReference;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/19 20:23
* @Describe 控制中心广播接收器
* 功能:监听电池状态变化、前台通知更新、配置变更指令
* 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步
*/
public class ControlCenterServiceReceiver extends BroadcastReceiver {
public static final String TAG = ControlCenterServiceReceiver.class.getSimpleName();
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "ControlCenterServiceReceiver";
public static final String ACTION_UPDATE_SERVICENOTIFICATION = ControlCenterServiceReceiver.class.getName() + ".ACTION_UPDATE_NOTIFICATION";
public static final String ACTION_START_REMINDTHREAD = ControlCenterServiceReceiver.class.getName() + ".ACTION_UPDATE_REMINDTHREAD";
// 广播Action常量带包名前缀防冲突
public static final String ACTION_UPDATE_FOREGROUND_NOTIFICATION = "cc.winboll.studio.powerbell.action.ACTION_UPDATE_FOREGROUND_NOTIFICATION";
public static final String ACTION_APPCONFIG_CHANGED = "cc.winboll.studio.powerbell.action.ACTION_APPCONFIG_CHANGED";
public static final String EXTRA_APP_CONFIG_BEAN = "extra_app_config_bean";
WeakReference<ControlCenterService> mwrService;
// 存储电量指示值,
// 用于校验电量消息时的电量变化
static volatile int _mnTheQuantityOfElectricityOld = -1;
static volatile boolean _mIsCharging = false;
// 广播优先级与电量范围常量
private static final int BROADCAST_PRIORITY = IntentFilter.SYSTEM_HIGH_PRIORITY - 10;
private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100;
// ====================== 静态状态标记volatile保证多线程可见性 ======================
private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见)
private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见)
// ====================== 成员变量区(弱引用防泄漏,按功能分层) ======================
private WeakReference<ControlCenterService> mwrControlCenterService;
private boolean isRegistered = false; // 标记广播注册状态,避免冗余操作
// ====================== 构造方法(初始化弱引用,避免服务强引用泄漏) ======================
public ControlCenterServiceReceiver(ControlCenterService service) {
mwrService = new WeakReference<ControlCenterService>(service);
LogUtils.d(TAG, String.format("构造接收器 | 服务实例:%s", service != null ? service.getClass().getSimpleName() : "null"));
this.mwrControlCenterService = new WeakReference<>(service);
}
// ====================== 广播核心接收逻辑入口方法分Action分发处理 ======================
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_UPDATE_SERVICENOTIFICATION)) {
mwrService.get().updateServiceNotification();
} else if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
boolean isCharging = BatteryUtils.isCharging(intent);
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
if (mwrService.get().getRemindThread() != null) {
// 先设置提醒进程电池状态标志
if (_mIsCharging != isCharging) {
mwrService.get().getRemindThread().setIsCharging(isCharging);
}
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
mwrService.get().getRemindThread().setQuantityOfElectricity(nTheQuantityOfElectricity);
}
String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action%s", action));
// 基础参数校验
if (context == null || intent == null || action == null) {
LogUtils.e(TAG, "onReceive: 参数无效context=" + context + " | intent=" + intent + "),终止处理");
return;
}
// 弱引用获取服务,双重校验服务有效性
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
if (service == null || service.isDestroyed()) {
LogUtils.e(TAG, "onReceive: 服务已销毁或为空,注销广播");
unregisterAction(context);
return;
}
// 分Action处理业务逻辑
switch (action) {
case Intent.ACTION_BATTERY_CHANGED:
handleBatteryStateChanged(service, intent);
break;
case ACTION_UPDATE_FOREGROUND_NOTIFICATION:
handleUpdateForegroundNotification(service);
break;
case ACTION_APPCONFIG_CHANGED:
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播");
handleNotifyAppConfigUpdate(service);
break;
default:
LogUtils.w(TAG, String.format("onReceive: 未知Action=%s", action));
}
LogUtils.d(TAG, "onReceive: 广播处理完成");
}
// ====================== 业务处理方法(按功能拆分,强化容错与日志) ======================
/**
* 处理电池状态变化广播
* @param service 控制中心服务实例
* @param intent 电池状态广播意图
*/
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
try {
// 1. 解析并校验当前电池状态
boolean currentCharging = BatteryUtils.isCharging(intent);
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
// 2. 状态无变化则跳过,减少无效运算
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态无变化,跳过处理");
return;
}
// 新电池状态标志某一个有变化就更新显示信息
if (_mIsCharging != isCharging || _mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
mwrService.get().updateServiceNotification();
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(context);
appConfigUtils.loadAppConfigBean();
AppConfigBean appConfigBean = appConfigUtils.mAppConfigBean;
appConfigBean.setCurrentValue(nTheQuantityOfElectricity);
appConfigBean.setIsCharging(isCharging);
mwrService.get().startRemindThread(appConfigBean);
// 保存电池报告
// 示例数据更新逻辑
// List<BatteryData> newData = new ArrayList<>(adapter.getDataList());
// newData.add(0, new BatteryData(percentage, "00:00:00", "00:00:00"));
// adapter.updateData(newData);
// 保存好新的电池状态标志
_mIsCharging = isCharging;
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
}
} else if (intent.getAction().equals(ACTION_START_REMINDTHREAD)) {
LogUtils.d(TAG, "ACTION_START_REMINDTHREAD");
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(context);
//appConfigUtils.loadAppConfigBean();
AppConfigBean appConfigBean = (AppConfigBean)intent.getSerializableExtra("appConfigBean");
mwrService.get().startRemindThread(appConfigBean);
// 3. 更新静态缓存状态,保证多线程可见
sIsCharging = currentCharging;
sLastBatteryLevel = currentBatteryLevel;
// 4. 同步缓存状态到配置
handleNotifyAppConfigUpdate(service);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 处理成功 | 缓存电量=%d%% | 缓存充电状态=%b", sLastBatteryLevel, sIsCharging));
} catch (Exception e) {
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
}
}
// 注册 Receiver
//
/**
* 处理配置变更通知,同步缓存状态到配置
* @param service 控制中心服务实例
*/
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置");
try {
// 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 最新配置为空,终止处理");
return;
}
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=%d | 耗电阈值=%d",
latestConfig.getChargeReminderValue(), latestConfig.getUsageReminderValue()));
// 同步缓存的电池状态到配置
latestConfig.setCurrentBatteryValue(sLastBatteryLevel);
latestConfig.setIsCharging(sIsCharging);
service.notifyAppConfigUpdate(latestConfig);
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=%d%% | 充电状态=%b", sLastBatteryLevel, sIsCharging));
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成");
} catch (Exception e) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 处理失败", e);
}
}
/**
* 处理前台服务通知更新
* @param service 控制中心服务实例
*/
private void handleUpdateForegroundNotification(ControlCenterService service) {
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知");
try {
NotificationManagerUtils notifyUtils = service.getNotificationManager();
NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
// 非空校验,避免空指针
if (notifyUtils == null || notifyMsg == null) {
LogUtils.e(TAG, String.format("handleUpdateForegroundNotification: 通知工具类或消息为空notifyUtils=%s | notifyMsg=%s", notifyUtils, notifyMsg));
return;
}
notifyUtils.updateForegroundServiceNotify(notifyMsg);
LogUtils.d(TAG, String.format("handleUpdateForegroundNotification: 前台通知更新成功 | 标题=%s", notifyMsg.getTitle()));
} catch (Exception e) {
LogUtils.e(TAG, "handleUpdateForegroundNotification: 处理失败", e);
}
}
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
/**
* 注册广播接收器
* @param context 上下文
*/
public void registerAction(Context context) {
IntentFilter filter=new IntentFilter();
filter.addAction(ACTION_UPDATE_SERVICENOTIFICATION);
filter.addAction(ACTION_START_REMINDTHREAD);
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
context.registerReceiver(this, filter);
LogUtils.d(TAG, "registerAction: 注册广播接收器");
if (context == null || isRegistered) {
LogUtils.e(TAG, "registerAction: 上下文为空或已注册,注册失败");
return;
}
try {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(ACTION_UPDATE_FOREGROUND_NOTIFICATION);
filter.addAction(ACTION_APPCONFIG_CHANGED);
filter.setPriority(BROADCAST_PRIORITY);
context.registerReceiver(this, filter);
isRegistered = true;
LogUtils.d(TAG, String.format("registerAction: 广播注册成功 | 优先级=%d", BROADCAST_PRIORITY));
} catch (Exception e) {
LogUtils.e(TAG, "registerAction: 注册失败", e);
}
}
/**
* 注销广播接收器
* @param context 上下文
*/
public void unregisterAction(Context context) {
LogUtils.d(TAG, "unregisterAction: 注销广播接收器");
if (context == null || !isRegistered) {
LogUtils.e(TAG, "unregisterAction: 上下文为空或未注册,注销失败");
return;
}
try {
context.unregisterReceiver(this);
isRegistered = false;
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
} catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
} catch (Exception e) {
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
}
}
// ====================== 资源释放与Getter方法按需开放防泄漏 ======================
/**
* 主动释放资源,避免内存泄漏
*/
public void release() {
LogUtils.d(TAG, "release: 释放广播接收器资源");
// 清空弱引用帮助GC回收
if (mwrControlCenterService != null) {
mwrControlCenterService.clear();
mwrControlCenterService = null;
LogUtils.d(TAG, "release: 弱引用已清空");
}
// 重置静态状态缓存
sLastBatteryLevel = -1;
sIsCharging = false;
LogUtils.d(TAG, "release: 静态状态缓存已重置");
}
/**
* 获取上次记录的电池电量
* @return 电量值0-100未初始化返回-1
*/
public static int getLastBatteryLevel() {
return sLastBatteryLevel;
}
/**
* 获取上次记录的充电状态
* @return true=充电中false=未充电
*/
public static boolean isLastCharging() {
return sIsCharging;
}
}

View File

@@ -4,63 +4,177 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import cc.winboll.studio.libappbase.LogUtils;
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.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/19 20:13
* @Describe 全局应用广播接收器
* 功能:监听系统电池状态变化,同步状态到配置工具类,通知页面更新
* 适配Java7 | API30 | 内存泄漏防护
*/
public class GlobalApplicationReceiver extends BroadcastReceiver {
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "GlobalApplicationReceiver";
private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100;
AppConfigUtils mAppConfigUtils;
App mGlobalApplication;
// 存储电量指示值,
// 用于校验电量消息时的电量变化
static volatile int _mnTheQuantityOfElectricityOld = -1;
static volatile boolean _mIsCharging = false;
// 保存当前实例,
// 便利封装 registerAction() 函数
GlobalApplicationReceiver mReceiver;
// ====================== 静态状态标记volatile保证多线程可见性 ======================
private static volatile int sLastBatteryLevel = -1; // 历史电量0-100
private static volatile boolean sLastIsCharging = false; // 历史充电状态
// ====================== 成员变量区按功能分层移除冗余的mCurrentReceiver ======================
private App mGlobalApplication;
private AppConfigUtils mAppConfigUtils;
// ====================== 构造方法(强化参数校验,初始化核心依赖) ======================
public GlobalApplicationReceiver(App globalApplication) {
mReceiver = this;
mGlobalApplication = globalApplication;
mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
LogUtils.d(TAG, String.format("构造接收器 | App实例%s", globalApplication));
if (globalApplication == null) {
LogUtils.e(TAG, "构造失败App实例为空");
throw new IllegalArgumentException("App cannot be null");
}
this.mGlobalApplication = globalApplication;
this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
LogUtils.d(TAG, String.format("构造完成 | AppConfigUtils%s", mAppConfigUtils));
}
// ====================== 广播核心接收逻辑(入口方法,过滤电池状态广播) ======================
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
// 先设置好新电池状态标志
boolean isCharging = BatteryUtils.isCharging(intent);
if (_mIsCharging != isCharging) {
mAppConfigUtils.setIsCharging(isCharging);
String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onReceive: 接收广播 | 上下文:%s | Action%s", context, action));
// 基础参数校验
if (context == null || intent == null || action == null) {
LogUtils.e(TAG, "onReceive: 参数无效,终止处理");
return;
}
// 仅处理电池状态变化广播
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
handleBatteryStateChanged(context, intent);
}
LogUtils.d(TAG, "onReceive: 广播处理完成");
}
// ====================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面) ======================
/**
* 处理电池状态变化广播
* @param context 上下文
* @param intent 电池状态广播意图
*/
private void handleBatteryStateChanged(Context context, Intent intent) {
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
try {
// 1. 解析当前电池状态(复用工具类,二次校验电量范围)
boolean currentIsCharging = BatteryUtils.isCharging(intent);
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentIsCharging, currentBatteryLevel));
// 2. 状态无变化则跳过,减少无效运算
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
return;
}
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
mAppConfigUtils.setCurrentValue(nTheQuantityOfElectricity);
// 3. 同步最新状态到配置工具类
if (mAppConfigUtils != null) {
if (currentIsCharging != sLastIsCharging) {
mAppConfigUtils.setCharging(currentIsCharging);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步充电状态 | %b", currentIsCharging));
}
if (currentBatteryLevel != sLastBatteryLevel) {
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步电量 | %d%%", currentBatteryLevel));
}
} else {
LogUtils.e(TAG, "handleBatteryStateChanged: AppConfigUtils为空同步失败");
}
// 新电池状态标志某一个有变化就更新显示信息
if (_mIsCharging != isCharging || _mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
// 电池状态改变先取消旧的提醒消息
//NotificationHelper.cancelRemindNotification(context);
App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity);
MainViewFragment.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
// 保存好新的电池状态标志
_mIsCharging = isCharging;
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
// 4. 执行状态变化后的业务逻辑
// 记录电量变化时间
if (App.getAppCacheUtils(context) != null) {
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
}
// 通知MainActivity更新电量
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 发送电量更新消息到MainActivity | %d%%", currentBatteryLevel));
// 5. 更新历史状态缓存
sLastIsCharging = currentIsCharging;
sLastBatteryLevel = currentBatteryLevel;
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
} catch (Exception e) {
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
}
}
// 注册 Receiver
//
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
/**
* 注册广播接收器
*/
public void registerAction() {
IntentFilter filter=new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
mGlobalApplication.registerReceiver(mReceiver, filter);
LogUtils.d(TAG, "registerAction: 注册广播");
if (mGlobalApplication == null) {
LogUtils.e(TAG, "注册失败App实例为空");
return;
}
try {
// 先注销再注册,避免重复注册异常
unregisterAction();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
mGlobalApplication.registerReceiver(this, filter);
LogUtils.d(TAG, "registerAction: 广播注册成功");
} catch (Exception e) {
LogUtils.e(TAG, "registerAction: 注册失败", e);
}
}
/**
* 注销广播接收器
*/
public void unregisterAction() {
LogUtils.d(TAG, "unregisterAction: 注销广播");
if (mGlobalApplication == null) {
LogUtils.e(TAG, "注销失败App实例为空");
return;
}
try {
mGlobalApplication.unregisterReceiver(this);
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
} catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
} catch (Exception e) {
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
}
}
// ====================== 资源释放方法(主动释放,彻底避免内存泄漏) ======================
/**
* 释放接收器资源供App销毁时调用
*/
public void release() {
LogUtils.d(TAG, "release: 释放接收器资源");
// 注销广播
unregisterAction();
// 置空引用帮助GC回收
mGlobalApplication = null;
mAppConfigUtils = null;
// 重置静态状态缓存
sLastBatteryLevel = -1;
sLastIsCharging = false;
LogUtils.d(TAG, "release: 资源释放完成");
}
}

View File

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

View File

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

View File

@@ -1,314 +1,506 @@
package cc.winboll.studio.powerbell.services;
/*
* PowerBy : ZhanGSKen(ZhangShaojian2018@163.com)
* 参考:
* 进程保活-双进程守护的正确姿势
* https://blog.csdn.net/sinat_35159441/article/details/75267380
* Android Service之onStartCommand方法研究
* https://blog.csdn.net/cyp331203/article/details/38920491
*/
import android.app.Notification;
import android.app.ActivityManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.widget.RemoteViews;
import android.os.PowerManager;
import android.provider.Settings;
import cc.winboll.studio.libappbase.LogUtils;
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.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.NotificationMessage;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.services.AssistantService;
import cc.winboll.studio.powerbell.threads.RemindThread;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import java.util.List;
/**
* 电池提醒核心服务
* 功能:管理前台服务生命周期、控制提醒线程启停、处理配置更新
* 适配Java7 | API30 | 前台服务超时防护 | 电池优化忽略引导
* @Author ZhanGSKen<zhangsken@qq.com>
* @Describe 核心服务:实现电池监测、提醒控制与前台服务保活
*/
public class ControlCenterService extends Service {
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "ControlCenterService";
// 线程与服务常量
private static final long THREAD_STOP_TIMEOUT = 1000L;
private static final int SERVICE_RETURN_STICKY = START_STICKY;
private static final int RUNNING_SERVICE_LIST_LIMIT = 100;
// 默认配置常量
private static final int DEFAULT_CHARGE_REMINDER_VALUE = 80;
private static final int DEFAULT_USAGE_REMINDER_VALUE = 20;
private static final int DEFAULT_BATTERY_DETECT_INTERVAL = 1000;
// API版本常量
private static final int API_LEVEL_26 = Build.VERSION_CODES.O;
private static final int API_LEVEL_30 = Build.VERSION_CODES.R;
private static final int API_LEVEL_23 = Build.VERSION_CODES.M;
public static final int MSG_UPDATE_STATUS = 0;
// ====================== 静态状态标记volatile保证多线程可见性 ======================
private static volatile boolean isServiceRunning = false;
private static volatile boolean mIsDestroyed = true;
static ControlCenterService _mControlCenterService;
volatile boolean isServiceRunning;
AppConfigUtils mAppConfigUtils;
AppCacheUtils mAppCacheUtils;
// 前台服务通知工具
NotificationHelper mNotificationHelper;
Notification notification;
RemindThread mRemindThread;
ControlCenterServiceHandler mControlCenterServiceHandler;
MyServiceConnection mMyServiceConnection;
ControlCenterServiceReceiver mControlCenterServiceReceiver;
ControlCenterServiceReceiver mControlCenterServiceReceiverLocalBroadcast;
@Override
public IBinder onBind(Intent intent) {
return null;
}
public RemindThread getRemindThread() {
return mRemindThread;
}
// ====================== 成员变量区(按功能分层:配置→核心组件→通知相关) ======================
// 服务控制配置
private ControlCenterServiceBean mServiceControlBean;
private AppConfigBean mCurrentConfigBean;
// 业务核心组件
private ControlCenterServiceHandler mServiceHandler;
private ControlCenterServiceReceiver mControlCenterServiceReceiver;
// 通知相关
private NotificationManagerUtils mNotificationManager;
private NotificationMessage mForegroundNotifyMsg;
// ====================== 服务生命周期方法按执行顺序onCreate→onStartCommand→onBind→onDestroy ======================
@Override
public void onCreate() {
super.onCreate();
_mControlCenterService = ControlCenterService.this;
isServiceRunning = false;
mAppConfigUtils = App.getAppConfigUtils(this);
mAppCacheUtils = App.getAppCacheUtils(this);
mNotificationHelper = new NotificationHelper(ControlCenterService.this);
if (mMyServiceConnection == null) {
mMyServiceConnection = new MyServiceConnection();
}
mControlCenterServiceHandler = new ControlCenterServiceHandler(this);
// 运行服务内容
run();
LogUtils.d(TAG, String.format("onCreate执行 | 线程=%s | 进程ID=%d", Thread.currentThread().getName(), android.os.Process.myPid()));
runCoreServiceLogic();
boolean serviceEnabled = mServiceControlBean != null && mServiceControlBean.isEnableService();
LogUtils.d(TAG, String.format("onCreate完成 | 前台状态=%b | 服务启用=%b", isServiceRunning, serviceEnabled));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 运行服务内容
run();
return (mAppConfigUtils.getIsEnableService()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onStartCommand执行 | startId=%d | action=%s", startId, action));
loadLatestServiceControlConfig();
runCoreServiceLogic();
int returnFlag = (mServiceControlBean != null && mServiceControlBean.isEnableService())
? SERVICE_RETURN_STICKY
: super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, String.format("onStartCommand完成 | 返回策略=%s", returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT"));
return returnFlag;
}
// 运行服务内容
//
void run() {
if (mAppConfigUtils.getIsEnableService() && isServiceRunning == false) {
LogUtils.d(TAG, "run");
isServiceRunning = true;
// 唤醒守护进程
wakeupAndBindAssistant();
// 显示前台通知栏
// 在Service中
NotificationHelper helper = new NotificationHelper(this);
Intent intent = new Intent(this, MainActivity.class);
notification = helper.showForegroundNotification(intent, getString(R.string.app_name), "Service Running, Click to open app");
startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
// NotificationMessage notificationMessage=createNotificationMessage();
// //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show();
// mNotificationUtils.createForegroundNotification(this, notificationMessage);
// mNotificationUtils.createRemindNotification(this, notificationMessage);
if (mControlCenterServiceReceiver == null) {
// 注册广播接收器
mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this);
mControlCenterServiceReceiver.registerAction(this);
}
new Handler(Looper.getMainLooper()).postDelayed(new Runnable(){
@Override
public void run() {
startRemindThread(mAppConfigUtils.mAppConfigBean);
ToastUtils.show("Service Is Start.");
LogUtils.i(TAG, "Service Is Start.");
}
}, 2000);
}
}
String getValuesString() {
String szReturn = "Usege: ";
szReturn += mAppConfigUtils.getIsEnableUsegeReminder() ? Integer.toString(mAppConfigUtils.getUsegeReminderValue()) : "?";
szReturn += "% Charge: ";
szReturn += mAppConfigUtils.getIsEnableChargeReminder() ? Integer.toString(mAppConfigUtils.getChargeReminderValue()) : "?";
szReturn += "%\nCurrent: " + Integer.toString(mAppConfigUtils.getCurrentValue()) + "%";
return szReturn;
}
NotificationMessage createNotificationMessage() {
String szTitle = ((App)getApplication()).getString(R.string.app_name);
String szContent = getValuesString() + " {?} " + StringUtils.formatPCMListString(mAppCacheUtils.getArrayListBatteryInfo());
return new NotificationMessage(szTitle, szContent);
}
// 更新前台通知
//
public void updateServiceNotification() {
//mNotificationUtils.updateForegroundNotification(ControlCenterService.this, createNotificationMessage());
}
// 更新前台通知
//
public void updateServiceNotification(NotificationMessage notificationMessage) {
//mNotificationUtils.updateForegroundNotification(ControlCenterService.this, notificationMessage);
}
// 更新前台通知
//
public void updateRemindNotification(NotificationMessage notificationMessage) {
//mNotificationUtils.updateRemindNotification(ControlCenterService.this, notificationMessage);
}
// 唤醒和绑定守护进程
//
void wakeupAndBindAssistant() {
if (ServiceUtils.isServiceAlive(getApplicationContext(), AssistantService.class.getName()) == false) {
startService(new Intent(ControlCenterService.this, AssistantService.class));
//LogUtils.d(TAG, "call wakeupAndBindAssistant() : Binding... AssistantService");
bindService(new Intent(ControlCenterService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
}
}
// 开启提醒铃声线程
//
public void startRemindThread(AppConfigBean appConfigBean) {
//LogUtils.d(TAG, "startRemindThread");
if (mRemindThread == null) {
mRemindThread = new RemindThread(this, mControlCenterServiceHandler);
} else {
if (mRemindThread.isExist() == true) {
mRemindThread = new RemindThread(this, mControlCenterServiceHandler);
} else {
// 提醒进程正在进行中就更新状态后退出
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue());
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder());
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
mRemindThread.setIsCharging(appConfigBean.isCharging());
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
//LogUtils.d(TAG, "mRemindThread update.");
return;
}
}
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue());
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
mRemindThread.setIsCharging(appConfigBean.isCharging());
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder());
mRemindThread.start();
//LogUtils.d(TAG, "mRemindThread.start()");
}
public void stopRemindThread() {
if (mRemindThread != null) {
mRemindThread.setIsExist(true);
mRemindThread = null;
}
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, String.format("onBind执行 | intent=%s", intent));
return null;
}
@Override
public void onDestroy() {
//LogUtils.d(TAG, "onDestroy");
mAppConfigUtils.loadAppConfigBean();
if (mAppConfigUtils.getIsEnableService() == false) {
// 设置运行状态
isServiceRunning = false;
// 停止守护进程
Intent intent = new Intent(this, AssistantService.class);
stopService(intent);
// 停止Receiver
if (mControlCenterServiceReceiver != null) {
unregisterReceiver(mControlCenterServiceReceiver);
mControlCenterServiceReceiver = null;
LogUtils.d(TAG, "onDestroy执行服务销毁流程启动");
super.onDestroy();
// 资源释放顺序:前台服务 → 线程 → 广播接收器 → Handler → 通知 → 引用(避免内存泄漏)
stopForegroundService();
RemindThread.stopRemindThread();
releaseBroadcastReceiver();
destroyHandler();
releaseNotificationResource();
clearAllReferences();
// 状态重置
mCurrentConfigBean = null;
mForegroundNotifyMsg = null;
mServiceHandler = null;
isServiceRunning = false;
mIsDestroyed = true;
LogUtils.d(TAG, "onDestroy完成服务销毁完成");
}
// ====================== 核心业务逻辑(独立抽取,统一调用) ======================
/**
* 服务核心运行逻辑在onCreate/onStartCommand复用
* 避免重复初始化,保证前台服务优先启动
*/
private synchronized void runCoreServiceLogic() {
LogUtils.d(TAG, "runCoreServiceLogic执行");
loadLatestServiceControlConfig();
boolean serviceEnabled = mServiceControlBean != null && mServiceControlBean.isEnableService();
LogUtils.d(TAG, String.format("runCoreServiceLogic服务启用=%b | 已运行=%b | 已销毁=%b", serviceEnabled, isServiceRunning, mIsDestroyed));
if (serviceEnabled && !isServiceRunning) {
isServiceRunning = true;
mIsDestroyed = false;
if (initForegroundNotificationImmediately()) {
loadDefaultConfig();
initServiceBusinessLogic();
LogUtils.d(TAG, "runCoreServiceLogic核心组件初始化成功");
} else {
LogUtils.e(TAG, "runCoreServiceLogic前台通知初始化失败终止业务");
stopForegroundService();
isServiceRunning = false;
}
// 停止前台通知栏
} else {
LogUtils.d(TAG, "runCoreServiceLogic无需执行核心逻辑");
}
}
// ====================== 前台通知管理优先执行防止API26+前台服务5秒超时 ======================
/**
* 立即初始化前台通知防止API26+前台服务超时异常
* @return true=成功 false=失败
*/
private boolean initForegroundNotificationImmediately() {
LogUtils.d(TAG, "initForegroundNotificationImmediately执行");
try {
if (mNotificationManager == null) {
mNotificationManager = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initForegroundNotificationImmediately通知工具类初始化完成");
}
if (mForegroundNotifyMsg == null) {
mForegroundNotifyMsg = new NotificationMessage();
mForegroundNotifyMsg.setTitle("电池监测服务");
mForegroundNotifyMsg.setContent("后台运行中");
mForegroundNotifyMsg.setRemindMSG("service_running");
LogUtils.d(TAG, "initForegroundNotificationImmediately通知消息构建完成");
}
mNotificationManager.startForegroundServiceNotify(this, mForegroundNotifyMsg);
ToastUtils.show("电池监测服务已启动");
LogUtils.d(TAG, String.format("initForegroundNotificationImmediately前台通知发送成功 | ID=%d", NotificationManagerUtils.NOTIFY_ID_FOREGROUND_SERVICE));
return true;
} catch (Exception e) {
LogUtils.e(TAG, "initForegroundNotificationImmediately通知初始化异常", e);
return false;
}
}
/**
* 停止前台服务并取消通知
*/
private void stopForegroundService() {
LogUtils.d(TAG, "stopForegroundService执行");
try {
stopForeground(true);
// 停止消息提醒进程
stopRemindThread();
super.onDestroy();
//LogUtils.d(TAG, "onDestroy done");
LogUtils.d(TAG, "stopForegroundService前台服务已停止通知已取消");
} catch (Exception e) {
LogUtils.e(TAG, "stopForegroundService停止异常", e);
}
}
// 主进程与守护进程连接时需要用到此类
//
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//LogUtils.d(TAG, "call onServiceConnected(...)");
}
@Override
public void onServiceDisconnected(ComponentName name) {
//LogUtils.d(TAG, "call onServiceConnected(...)");
if (mAppConfigUtils.getIsEnableService()) {
// 唤醒守护进程
wakeupAndBindAssistant();
}
// ====================== 配置管理(本地持久化+内存同步) ======================
/**
* 加载本地最新服务控制配置
*/
private void loadLatestServiceControlConfig() {
LogUtils.d(TAG, "loadLatestServiceControlConfig执行");
ControlCenterServiceBean latestBean = ControlCenterServiceBean.loadBean(this, ControlCenterServiceBean.class);
if (latestBean != null) {
mServiceControlBean = latestBean;
LogUtils.d(TAG, String.format("loadLatestServiceControlConfig配置读取成功 | 启用=%b", mServiceControlBean.isEnableService()));
} else {
LogUtils.w(TAG, "loadLatestServiceControlConfig本地无配置沿用内存配置");
}
}
public void appenRemindMSG(String szRemindMSG) {
String msg = "";
for (int i = 0; i < 20; i++) {
msg += szRemindMSG;
/**
* 加载默认业务配置(首次启动兜底)
*/
private void loadDefaultConfig() {
LogUtils.d(TAG, "loadDefaultConfig执行");
if (mCurrentConfigBean == null) {
mCurrentConfigBean = new AppConfigBean();
mCurrentConfigBean.setEnableChargeReminder(true);
mCurrentConfigBean.setChargeReminderValue(DEFAULT_CHARGE_REMINDER_VALUE);
mCurrentConfigBean.setEnableUsageReminder(true);
mCurrentConfigBean.setUsageReminderValue(DEFAULT_USAGE_REMINDER_VALUE);
mCurrentConfigBean.setBatteryDetectInterval(DEFAULT_BATTERY_DETECT_INTERVAL);
LogUtils.d(TAG, String.format("loadDefaultConfig默认配置加载完成 | 充电阈值=%d | 耗电阈值=%d | 检测间隔=%dms",
DEFAULT_CHARGE_REMINDER_VALUE, DEFAULT_USAGE_REMINDER_VALUE, DEFAULT_BATTERY_DETECT_INTERVAL));
} else {
LogUtils.d(TAG, "loadDefaultConfig内存已有配置无需加载");
}
NotificationHelper helper = new NotificationHelper(ControlCenterService.this);
Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
helper.showTemporaryNotification(intent, getString(R.string.app_name), msg);
// NotificationMessage notificationMessage = createNotificationMessage();
// notificationMessage.setRemindMSG(szRemindMSG);
// //LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
// updateRemindNotification(notificationMessage);
}
// 设置颜色背景
public static RemoteViews setLinearLayoutColor(RemoteViews remoteViews, int viewId, int color) {
remoteViews.setInt(viewId, "setBackgroundColor", color);
return remoteViews;
// ====================== 业务组件初始化与销毁Handler/广播/线程等) ======================
/**
* 初始化Handler等核心业务组件
*/
private void initServiceBusinessLogic() {
LogUtils.d(TAG, "initServiceBusinessLogic执行");
// 初始化Handler
if (mServiceHandler == null) {
mServiceHandler = new ControlCenterServiceHandler(this);
LogUtils.d(TAG, "initServiceBusinessLogicHandler初始化完成");
} else {
LogUtils.d(TAG, "initServiceBusinessLogicHandler已存在");
}
// 初始化广播接收器
if (mControlCenterServiceReceiver == null) {
mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this);
mControlCenterServiceReceiver.registerAction(this);
LogUtils.d(TAG, "initServiceBusinessLogic广播接收器初始化并注册完成");
} else {
LogUtils.d(TAG, "initServiceBusinessLogic广播接收器已存在");
}
}
// 设置Drawable背景
public static RemoteViews setLinearLayoutDrawable(RemoteViews remoteViews, int viewId, int drawableRes) {
remoteViews.setInt(viewId, "setBackgroundResource", drawableRes);
return remoteViews;
/**
* 释放广播接收器资源
*/
private void releaseBroadcastReceiver() {
LogUtils.d(TAG, "releaseBroadcastReceiver执行");
if (mControlCenterServiceReceiver != null) {
mControlCenterServiceReceiver.release();
mControlCenterServiceReceiver = null;
LogUtils.d(TAG, "releaseBroadcastReceiver广播接收器已释放");
} else {
LogUtils.w(TAG, "releaseBroadcastReceiver广播接收器实例为空");
}
}
//
// 启动服务
//
/**
* 销毁Handler移除所有消息和回调防止内存泄漏
*/
private void destroyHandler() {
LogUtils.d(TAG, "destroyHandler执行");
if (mServiceHandler != null) {
mServiceHandler.removeCallbacksAndMessages(null);
mServiceHandler = null;
LogUtils.d(TAG, "destroyHandlerHandler已销毁");
} else {
LogUtils.w(TAG, "destroyHandlerHandler实例为空");
}
}
/**
* 释放通知工具类资源
*/
private void releaseNotificationResource() {
LogUtils.d(TAG, "releaseNotificationResource执行");
if (mNotificationManager != null) {
mNotificationManager.release();
mNotificationManager = null;
LogUtils.d(TAG, "releaseNotificationResource通知资源已释放");
} else {
LogUtils.w(TAG, "releaseNotificationResource通知工具类实例为空");
}
}
/**
* 置空所有引用,防止内存泄漏
*/
private void clearAllReferences() {
LogUtils.d(TAG, "clearAllReferences执行");
mForegroundNotifyMsg = null;
mServiceControlBean = null;
LogUtils.d(TAG, "clearAllReferences引用清理完成");
}
// ====================== 外部调用接口(静态方法,提供服务启停/配置更新入口) ======================
/**
* 外部启动服务的统一入口
* @param context 上下文
*/
public static void startControlCenterService(Context context) {
LogUtils.d(TAG, String.format("startControlCenterService执行 | context=%s", context));
if (context == null) {
LogUtils.e(TAG, "startControlCenterServiceContext为空启动失败");
return;
}
// 保存启用配置
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(true);
ControlCenterServiceBean.saveBean(context, controlBean);
LogUtils.d(TAG, "startControlCenterService服务启用配置已保存");
// 启动服务区分API版本
Intent intent = new Intent(context, ControlCenterService.class);
context.startForegroundService(intent);
if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
context.startForegroundService(intent);
LogUtils.d(TAG, "startControlCenterService以前台服务方式启动API26+");
} else {
context.startService(intent);
LogUtils.d(TAG, "startControlCenterService以普通服务方式启动API26-");
}
}
//
// 停止服务
//
/**
* 外部停止服务的统一入口
* @param context 上下文
*/
public static void stopControlCenterService(Context context) {
LogUtils.d(TAG, String.format("stopControlCenterService执行 | context=%s", context));
if (context == null) {
LogUtils.e(TAG, "stopControlCenterServiceContext为空停止失败");
return;
}
// 保存停用配置
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(false);
ControlCenterServiceBean.saveBean(context, controlBean);
LogUtils.d(TAG, "stopControlCenterService服务停用配置已保存");
// 停止服务
Intent intent = new Intent(context, ControlCenterService.class);
context.stopService(intent);
LogUtils.d(TAG, "stopControlCenterService停止指令已发送");
}
public static void updateStatus(Context context, AppConfigBean appConfigBean) {
//LogUtils.d(TAG, "updateStatus");
// 创建一个Intent实例定义广播的内容
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_START_REMINDTHREAD);
// 设置可选的Action数据如额外信息
intent.putExtra("appConfigBean", appConfigBean);
// 发送广播
/**
* 外部更新配置并触发线程重启
* @param context 上下文
*/
public static void sendAppConfigStatusUpdateMessage(Context context) {
LogUtils.d(TAG, String.format("sendAppConfigStatusUpdateMessage执行 | context=%s", context));
if (context == null) {
LogUtils.e(TAG, "sendAppConfigStatusUpdateMessage参数为空更新失败");
return;
}
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent);
LogUtils.d(TAG, String.format("sendAppConfigStatusUpdateMessage配置更新广播发送 | action=%s", ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED));
}
/**
* 检查并引导用户开启忽略电池优化API23+
* @param context 上下文
*/
public static void checkIgnoreBatteryOptimization(Context context) {
LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization执行 | context=%s", context));
if (context == null || Build.VERSION.SDK_INT < API_LEVEL_23) {
LogUtils.w(TAG, "checkIgnoreBatteryOptimization无需检查Context为空或API<23");
return;
}
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (powerManager == null) {
LogUtils.e(TAG, "checkIgnoreBatteryOptimizationPowerManager获取失败");
return;
}
String packageName = context.getPackageName();
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(packageName);
LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization已忽略电池优化=%b", isIgnored));
if (!isIgnored) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent);
LogUtils.d(TAG, String.format("checkIgnoreBatteryOptimization已跳转至系统设置页 | package=%s", packageName));
}
}
/**
* 检查服务是否运行适配API30+
* @param context 上下文
* @param serviceClass 服务类
* @return true=运行中 false=未运行
*/
private static boolean isServiceRunning(Context context, Class<?> serviceClass) {
LogUtils.d(TAG, String.format("isServiceRunning执行 | context=%s | service=%s", context, serviceClass != null ? serviceClass.getName() : "null"));
if (context == null || serviceClass == null) {
LogUtils.e(TAG, "isServiceRunning参数为空");
return false;
}
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
LogUtils.e(TAG, "isServiceRunningActivityManager获取失败");
return false;
}
boolean isRunning = false;
String packageName = context.getPackageName();
String serviceClassName = serviceClass.getName();
if (Build.VERSION.SDK_INT >= API_LEVEL_30) {
// API30+ 禁止获取其他应用服务,通过进程状态判断
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes != null) {
for (ActivityManager.RunningAppProcessInfo process : processes) {
if (packageName.equals(process.processName) &&
(process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE ||
process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND)) {
isRunning = true;
break;
}
}
}
LogUtils.d(TAG, String.format("isServiceRunningAPI30+ 判断结果=%b", isRunning));
} else {
// API30- 通过服务列表判断
List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(RUNNING_SERVICE_LIST_LIMIT);
if (services != null) {
for (ActivityManager.RunningServiceInfo info : services) {
if (serviceClassName.equals(info.service.getClassName())) {
isRunning = true;
break;
}
}
}
LogUtils.d(TAG, String.format("isServiceRunningAPI30- 判断结果=%b", isRunning));
}
// 兜底判断:配置启用状态
if (!isRunning) {
isRunning = isServiceStarted(context, serviceClass);
LogUtils.d(TAG, String.format("isServiceRunning兜底判断结果=%b", isRunning));
}
return isRunning;
}
/**
* 兜底判断服务是否已启动(通过配置文件)
*/
private static boolean isServiceStarted(Context context, Class<?> serviceClass) {
LogUtils.d(TAG, "isServiceStarted执行");
try {
ControlCenterServiceBean controlBean = ControlCenterServiceBean.loadBean(context, ControlCenterServiceBean.class);
return controlBean != null && controlBean.isEnableService();
} catch (Exception e) {
LogUtils.e(TAG, "isServiceStarted兜底判断异常", e);
return false;
}
}
// ====================== 业务方法(配置更新/电池状态回调) ======================
/**
* 接收外部配置更新,同步到提醒线程
* @param latestConfig 最新配置
*/
public void notifyAppConfigUpdate(AppConfigBean latestConfig) {
int chargeThreshold = latestConfig != null ? latestConfig.getChargeReminderValue() : -1;
int usageThreshold = latestConfig != null ? latestConfig.getUsageReminderValue() : -1;
LogUtils.d(TAG, String.format("notifyAppConfigUpdate执行 | 充电阈值=%d | 耗电阈值=%d", chargeThreshold, usageThreshold));
if (latestConfig != null && mServiceHandler != null) {
mCurrentConfigBean = latestConfig;
RemindThread.startRemindThreadWithAppConfig(this, mServiceHandler, latestConfig);
LogUtils.d(TAG, "notifyAppConfigUpdate配置已同步到提醒线程");
} else {
LogUtils.e(TAG, String.format("notifyAppConfigUpdate参数为空同步失败 | latestConfig=%s | mServiceHandler=%s", latestConfig, mServiceHandler));
}
}
// ====================== Getter 方法按需开放避免冗余Setter ======================
public ControlCenterServiceBean getServiceControlBean() {
return mServiceControlBean;
}
public NotificationManagerUtils getNotificationManager() {
return mNotificationManager;
}
public NotificationMessage getForegroundNotifyMsg() {
return mForegroundNotifyMsg;
}
public AppConfigBean getCurrentConfigBean() {
return mCurrentConfigBean;
}
public boolean isDestroyed() {
return mIsDestroyed;
}
}

View File

@@ -4,36 +4,377 @@ import android.content.Context;
import android.os.Message;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
* 电量通知提醒线程(多实例列表管理)
* 功能:管理充电/耗电提醒逻辑触发条件时向Handler发送提醒消息
* 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步
* 对外接口:{@link #startRemindThreadWithAppConfig(Context, ControlCenterServiceHandler, AppConfigBean)}、
* {@link #startRemindThreadWithBatteryInfo(Context, ControlCenterServiceHandler, boolean, int)}、{@link #stopRemindThread()}
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 电量通知提醒线程
*/
public class RemindThread extends Thread {
public static final String TAG = RemindThread.class.getSimpleName();
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "RemindThread";
Context mContext;
// 控制线程是否退出的标志
volatile boolean isExist = false;
// 消息提醒开关
static volatile boolean isReminding = false;
// 充电提醒开关
static volatile boolean isEnableUsegeReminder = false;
// 耗电提醒开关
static volatile boolean isEnableChargeReminder = false;
// 电量比较停顿时间
static volatile int sleepTime = 1000;
// 充电提醒电量
static volatile int chargeReminderValue = -1;
// 耗电提醒电量
static volatile int usegeReminderValue = -1;
// 当前电量
static volatile int quantityOfElectricity = -1;
// 是否正在充电
static volatile boolean isCharging = false;
// 服务Handler, 用于线程发送消息使用
WeakReference<ControlCenterServiceHandler> mwrControlCenterServiceHandler;
// 时间常量 (ms)
private static final int MIN_SLEEP_TIME = 2000;
private static final long THREAD_JOIN_TIMEOUT = 1000L;
// 状态常量
private static final int INVALID_BATTERY_VALUE = -1;
private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100;
// 提醒类型常量
private static final String REMIND_TYPE_CHARGE = "+";
private static final String REMIND_TYPE_USAGE = "-";
// ====================== 静态成员(多实例列表管理) ======================
private static volatile ArrayList<RemindThread> sRemindThreadList;
// ====================== 成员变量区按功能分层volatile保证多线程可见性 ======================
// 并发安全锁(保护线程状态变更)
private final Object mRemindLock = new Object();
// 弱引用依赖防内存泄漏ApplicationContext 避免 Activity 引用)
private Context mContext;
private WeakReference<ControlCenterServiceHandler> mwrControlCenterServiceHandler;
// 线程状态标记volatile 确保多线程可见)
private volatile boolean isReminding;
public volatile boolean isExist;
// 业务配置参数volatile 确保配置变更实时生效)
private volatile boolean isEnableChargeReminder;
private volatile boolean isEnableUsageReminder;
private volatile long sleepTime;
private volatile int chargeReminderValue;
private volatile int usageReminderValue;
private volatile int quantityOfElectricity;
private volatile boolean isCharging;
// ====================== 私有构造器(禁止外部实例化) ======================
private RemindThread(Context context, ControlCenterServiceHandler handler) {
LogUtils.d(TAG, String.format("构造器调用 | context=%s | handler=%s", context, handler));
this.mContext = context.getApplicationContext();
this.mwrControlCenterServiceHandler = new WeakReference<>(handler);
resetThreadStateInternal();
LogUtils.d(TAG, String.format("构造完成 | threadId=%d | 初始状态重置成功", getId()));
}
// ====================== 对外公开静态接口(多实例列表管理) ======================
/**
* 启动提醒线程,同步最新配置
* 逻辑:停止所有旧线程 → 创建新线程 → 加入列表管理
* @param context 上下文(非空)
* @param handler 服务处理器(非空)
* @param config 应用配置Bean非空
* @return true: 启动成功false: 入参非法
*/
public static boolean startRemindThreadWithAppConfig(Context context, ControlCenterServiceHandler handler, AppConfigBean config) {
LogUtils.d(TAG, String.format("startRemindThreadWithAppConfig调用 | context=%s | handler=%s | config=%s", context, handler, config));
// 入参严格校验
if (context == null || handler == null || config == null) {
LogUtils.e(TAG, String.format("启动失败:入参为空 | context=%s | handler=%s | config=%s", context, handler, config));
return false;
}
// 初始化线程列表(双重校验锁)
if (sRemindThreadList == null) {
synchronized (RemindThread.class) {
if (sRemindThreadList == null) {
sRemindThreadList = new ArrayList<RemindThread>();
LogUtils.d(TAG, "线程列表初始化完成");
}
}
}
// 停止所有旧线程
stopAllOldThreadsInternal();
// 创建并启动新线程
RemindThread newRemindThread = new RemindThread(context, handler);
newRemindThread.setAppConfigBean(config);
newRemindThread.isExist = false;
newRemindThread.start();
sRemindThreadList.add(newRemindThread);
LogUtils.d(TAG, String.format("新线程启动成功 | threadId=%d | 列表大小=%d", newRemindThread.getId(), sRemindThreadList.size()));
return true;
}
/**
* 启动提醒线程,同步电池状态信息
* 逻辑:停止所有旧线程 → 创建新线程 → 同步电池状态 → 加入列表管理
* @param context 上下文(非空)
* @param handler 服务处理器(非空)
* @param isCharging 充电状态
* @param batteryLevel 当前电量
* @return true: 启动成功false: 入参非法
*/
public static boolean startRemindThreadWithBatteryInfo(Context context, ControlCenterServiceHandler handler, boolean isCharging, int batteryLevel) {
LogUtils.d(TAG, String.format("startRemindThreadWithBatteryInfo调用 | context=%s | handler=%s | isCharging=%b | batteryLevel=%d", context, handler, isCharging, batteryLevel));
// 入参严格校验
if (context == null || handler == null) {
LogUtils.e(TAG, String.format("启动失败:入参为空 | context=%s | handler=%s", context, handler));
return false;
}
// 初始化线程列表(双重校验锁)
if (sRemindThreadList == null) {
synchronized (RemindThread.class) {
if (sRemindThreadList == null) {
sRemindThreadList = new ArrayList<RemindThread>();
LogUtils.d(TAG, "线程列表初始化完成");
}
}
}
// 停止所有旧线程
stopAllOldThreadsInternal();
// 创建并启动新线程
RemindThread newRemindThread = new RemindThread(context, handler);
// 同步电池状态(范围校验)
newRemindThread.isCharging = isCharging;
newRemindThread.quantityOfElectricity = Math.min(Math.max(batteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
newRemindThread.isExist = false;
newRemindThread.start();
sRemindThreadList.add(newRemindThread);
LogUtils.d(TAG, String.format("新线程启动成功 | threadId=%d | 电池状态同步完成(电量=%d充电=%b", newRemindThread.getId(), newRemindThread.quantityOfElectricity, newRemindThread.isCharging));
return true;
}
/**
* 安全停止所有线程,清空列表
*/
public static void stopRemindThread() {
int listSize = sRemindThreadList != null ? sRemindThreadList.size() : 0;
LogUtils.d(TAG, String.format("stopRemindThread调用 | 列表存在=%b | 列表大小=%d", sRemindThreadList != null, listSize));
if (sRemindThreadList == null || sRemindThreadList.isEmpty()) {
LogUtils.w(TAG, "停止失败:线程列表为空");
return;
}
// 标记所有线程退出
for (RemindThread remindThread : sRemindThreadList) {
remindThread.isExist = true;
LogUtils.d(TAG, String.format("标记线程退出 | threadId=%d", remindThread.getId()));
}
// 清空列表
sRemindThreadList.clear();
LogUtils.d(TAG, "所有线程已标记退出,列表已清空");
}
// ====================== 私有静态辅助方法(多实例管理) ======================
/**
* 停止所有旧线程并清空列表
*/
private static void stopAllOldThreadsInternal() {
if (sRemindThreadList == null || sRemindThreadList.isEmpty()) {
return;
}
// 标记所有旧线程退出
for (RemindThread remindThread : sRemindThreadList) {
remindThread.isExist = true;
LogUtils.d(TAG, String.format("标记旧线程退出 | threadId=%d", remindThread.getId()));
}
// 清空旧线程列表
sRemindThreadList.clear();
LogUtils.d(TAG, "旧线程已全部标记退出,列表已清空");
}
// ====================== 线程核心运行逻辑 ======================
@Override
public void run() {
LogUtils.d(TAG, String.format("run执行 | threadId=%d | 状态=%s", getId(), getState()));
// 初始化提醒状态(加锁保护,避免多线程竞争)
synchronized (mRemindLock) {
if (isReminding) {
LogUtils.w(TAG, String.format("线程已在提醒状态,退出运行 | threadId=%d", getId()));
return;
}
isReminding = true;
}
// 核心电量检测循环
LogUtils.d(TAG, String.format("进入电量检测循环 | 休眠时间=%dms | threadId=%d", sleepTime, getId()));
while (!isExist) {
try {
// 快速退出判断
if (isExist) break;
// 电量有效性校验非0-100视为无效退出电量提醒线程
if (quantityOfElectricity < BATTERY_LEVEL_MIN || quantityOfElectricity > BATTERY_LEVEL_MAX) {
LogUtils.w(TAG, String.format("电量无效,退出电量提醒线程 | 当前电量=%d | threadId=%d", quantityOfElectricity, getId()));
break;
}
// 充电/耗电提醒触发逻辑
boolean chargeRemindTrigger = isCharging && isEnableChargeReminder && quantityOfElectricity >= chargeReminderValue;
boolean usageRemindTrigger = !isCharging && isEnableUsageReminder && quantityOfElectricity <= usageReminderValue;
if (chargeRemindTrigger) {
LogUtils.d(TAG, String.format("触发充电提醒 | 当前电量=%d ≥ 阈值=%d | threadId=%d", quantityOfElectricity, chargeReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_CHARGE, quantityOfElectricity, isCharging);
} else if (usageRemindTrigger) {
LogUtils.d(TAG, String.format("触发耗电提醒 | 当前电量=%d ≤ 阈值=%d | threadId=%d", quantityOfElectricity, usageReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_USAGE, quantityOfElectricity, isCharging);
} else {
LogUtils.d(TAG, String.format("未有合适类型提醒,退出提醒线程 | threadId=%d", getId()));
break;
}
// 安全休眠,保留中断标记
safeSleepInternal(sleepTime);
} catch (Exception e) {
LogUtils.e(TAG, String.format("循环运行异常,退出电量提醒线程 | 当前电量=%d | threadId=%d", quantityOfElectricity, getId()), e);
break;
}
}
// 循环退出,清理状态
cleanThreadStateInternal();
LogUtils.d(TAG, String.format("run结束 | threadId=%d", getId()));
}
// ====================== 内部业务辅助方法 ======================
/**
* 发送提醒消息到Handler弱引用避免内存泄漏
* @param type 提醒类型:+充电/-耗电
* @param battery 当前电量
* @param isCharging 充电状态
*/
private void sendNotificationMessageInternal(String type, int battery, boolean isCharging) {
LogUtils.d(TAG, String.format("sendNotificationMessageInternal调用 | 类型=%s | 电量=%d | isCharging=%b | threadId=%d", type, battery, isCharging, getId()));
// 前置状态校验
if (isExist || !isReminding) {
LogUtils.d(TAG, String.format("消息发送跳过:线程已退出或提醒关闭 | threadId=%d", getId()));
return;
}
// 获取弱引用的Handler校验有效性
ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get();
if (handler == null) {
LogUtils.w(TAG, String.format("消息发送失败Handler已被回收 | threadId=%d", getId()));
return;
}
// 构建并发送消息
Message message = Message.obtain(handler, ControlCenterServiceHandler.MSG_REMIND_TEXT);
message.obj = type;
message.arg1 = battery;
message.arg2 = isCharging ? 1 : 0;
try {
handler.sendMessage(message);
LogUtils.d(TAG, String.format("提醒消息发送成功 | 类型=%s | 电量=%d | threadId=%d", type, battery, getId()));
} catch (Exception e) {
LogUtils.e(TAG, String.format("消息发送异常 | threadId=%d", getId()), e);
// 异常时回收Message避免内存泄漏
if (message != null) {
message.recycle();
}
}
}
/**
* 安全休眠,响应线程中断
* @param millis 休眠时长(ms)
*/
private void safeSleepInternal(long millis) {
LogUtils.d(TAG, String.format("safeSleepInternal调用 | 休眠时长=%dms | threadId=%d", millis, getId()));
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LogUtils.w(TAG, String.format("休眠被中断,线程准备退出 | threadId=%d", getId()));
}
}
/**
* 重置线程初始状态(构造器专用)
*/
private void resetThreadStateInternal() {
LogUtils.d(TAG, String.format("resetThreadStateInternal调用 | threadId=%d", getId()));
// 状态标记初始化
isExist = false;
isReminding = false;
// 配置参数初始化
isEnableChargeReminder = false;
isEnableUsageReminder = false;
sleepTime = MIN_SLEEP_TIME;
chargeReminderValue = -1;
usageReminderValue = -1;
quantityOfElectricity = INVALID_BATTERY_VALUE;
isCharging = false;
LogUtils.d(TAG, String.format("线程初始状态重置完成 | threadId=%d", getId()));
}
/**
* 清理线程运行状态(循环退出时调用)
*/
private void cleanThreadStateInternal() {
LogUtils.d(TAG, String.format("cleanThreadStateInternal调用 | threadId=%d", getId()));
isReminding = false;
isExist = true;
quantityOfElectricity = INVALID_BATTERY_VALUE;
// 中断当前线程(如果存活)
if (isAlive()) {
interrupt();
LogUtils.d(TAG, String.format("线程已中断 | threadId=%d", getId()));
}
LogUtils.d(TAG, String.format("线程运行状态清理完成 | threadId=%d", getId()));
}
/**
* 同步应用配置,校验参数有效性
* @param config 应用配置Bean
*/
public void setAppConfigBean(AppConfigBean config) {
LogUtils.d(TAG, String.format("setAppConfigBean调用 | config=%s | threadId=%d", config, getId()));
if (config == null) {
LogUtils.e(TAG, String.format("配置同步失败配置Bean为空 | threadId=%d", getId()));
quantityOfElectricity = INVALID_BATTERY_VALUE;
return;
}
// 配置参数同步 + 范围校验(确保参数合法)
isEnableChargeReminder = config.isEnableChargeReminder();
isEnableUsageReminder = config.isEnableUsageReminder();
chargeReminderValue = Math.min(Math.max(config.getChargeReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
usageReminderValue = Math.min(Math.max(config.getUsageReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
sleepTime = Math.max(config.getBatteryDetectInterval(), MIN_SLEEP_TIME);
quantityOfElectricity = (config.getCurrentBatteryValue() >= BATTERY_LEVEL_MIN && config.getCurrentBatteryValue() <= BATTERY_LEVEL_MAX)
? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE;
isCharging = config.isCharging();
LogUtils.d(TAG, String.format("配置同步完成 | 休眠时间=%dms | 充电提醒=%b | 耗电提醒=%b | 当前电量=%d | 充电阈值=%d | 耗电阈值=%d | threadId=%d",
sleepTime, isEnableChargeReminder, isEnableUsageReminder, quantityOfElectricity, chargeReminderValue, usageReminderValue, getId()));
}
/**
* 判断线程是否处于运行状态
* @return true: 运行中false: 已停止
*/
private boolean isRunning() {
boolean running = !isExist && isAlive();
LogUtils.d(TAG, String.format("isRunning调用 | 运行中=%b | 退出标记=%b | 存活=%b | threadId=%d", running, isExist, isAlive(), getId()));
return running;
}
// ====================== Getter/Setter按需开放 ======================
public void setIsExist(boolean isExist) {
LogUtils.d(TAG, String.format("setIsExist调用 | isExist=%b | threadId=%d", isExist, getId()));
this.isExist = isExist;
}
@@ -41,157 +382,20 @@ public class RemindThread extends Thread {
return isExist;
}
public static void setIsReminding(boolean isReminding) {
RemindThread.isReminding = isReminding;
}
public static boolean isReminding() {
return isReminding;
}
public static void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
RemindThread.isEnableUsegeReminder = isEnableUsegeReminder;
}
public static boolean isEnableUsegeReminder() {
return isEnableUsegeReminder;
}
public static void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
RemindThread.isEnableChargeReminder = isEnableChargeReminder;
}
public static boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
public static void setSleepTime(int sleepTime) {
RemindThread.sleepTime = sleepTime;
}
public static int getSleepTime() {
return sleepTime;
}
public static void setChargeReminderValue(int chargeReminderValue) {
RemindThread.chargeReminderValue = chargeReminderValue;
}
public static int getChargeReminderValue() {
return chargeReminderValue;
}
public static void setUsegeReminderValue(int usegeReminderValue) {
RemindThread.usegeReminderValue = usegeReminderValue;
}
public static int getUsegeReminderValue() {
return usegeReminderValue;
}
public static void setQuantityOfElectricity(int quantityOfElectricity) {
RemindThread.quantityOfElectricity = quantityOfElectricity;
}
public static int getQuantityOfElectricity() {
return quantityOfElectricity;
}
public static void setIsCharging(boolean isCharging) {
RemindThread.isCharging = isCharging;
}
public static boolean isCharging() {
return isCharging;
}
// 发送消息给用户
//
void sendNotificationMessage(String sz) {
//LogUtils.d(TAG, "sz is " + sz);
Message message = Message.obtain();
message.what = ControlCenterServiceHandler.MSG_REMIND_TEXT;
//message.obj = new NotificationMessage(mContext.getString(R.string.app_name), sz);
message.obj = sz;
ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get();
if (isReminding && handler != null) {
handler.sendMessage(message);
}
}
public RemindThread(Context context, ControlCenterServiceHandler handler) {
mContext = context;
mwrControlCenterServiceHandler = new WeakReference<ControlCenterServiceHandler>(handler);
}
// ====================== 调试辅助方法 ======================
@Override
public void run() {
//LogUtils.d(TAG, "call run()");
if (isReminding == false) {
isReminding = true;
// 等待些许时间,等所有数据初始化完成再执行下面的程序
// 解决窗口移除后自动重启后会发送一个错误消息的问题
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
// 发送提醒线程开始的参数设置
//sendMessageToUser(Integer.toString(_mnTheQuantityOfElectricity) + ">>>" + Integer.toString(_mnTargetNumber));
//ToastUtils.show("Service Is Start.");
//LogUtils.i(TAG, "Service Is Start.");
while (!isExist()) {
/*
LogUtils.d(TAG, "isCharging is " + Boolean.toString(isCharging));
LogUtils.d(TAG, "usegeReminderValue is " + Integer.toString(usegeReminderValue));
LogUtils.d(TAG, "quantityOfElectricity is " + Integer.toString(quantityOfElectricity));
LogUtils.d(TAG, "chargeReminderValue is " + Integer.toString(chargeReminderValue));
LogUtils.d(TAG, "isEnableChargeReminder is " + Boolean.toString(isEnableChargeReminder));
LogUtils.d(TAG, "isEnableUsegeReminder is " + Boolean.toString(isEnableUsegeReminder));
*/
try {
if (isCharging) {
if ((quantityOfElectricity >= chargeReminderValue)
&& (isEnableChargeReminder)) {
// 正在充电时电量大于指定电量发送提醒
sendNotificationMessage("+");
// 应用需要继续提醒,设置退出标志为否
setIsExist(false);
//sendNotificationMessage("I am ready! +");
} else {
// 设置退出标志,如果后续不需要继续提醒就退出当前进程,用于应用节能。
setIsExist(true);
isReminding = false;
return;
}
} else {
if ((quantityOfElectricity <= usegeReminderValue)
&& (isEnableUsegeReminder)) {
// 正在放电时电量小于指定电量发送提醒
sendNotificationMessage("-");
// 应用需要继续提醒,设置退出标志为否
setIsExist(false);
//sendNotificationMessage("I am ready! -");
} else {
// 设置退出标志,如果后续不需要继续提醒就退出当前进程,用于应用节能。
setIsExist(true);
isReminding = false;
return;
}
}
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
//ToastUtils.show("Service Is Stop.");
//LogUtils.i(TAG, "Service Is Stop.");
isReminding = false;
}
public String toString() {
return "RemindThread{" +
"threadId=" + getId() +
", threadName='" + getName() + '\'' +
", isRunning=" + isRunning() +
", isReminding=" + isReminding +
", chargeThreshold=" + chargeReminderValue +
", usageThreshold=" + usageReminderValue +
", currentBattery=" + quantityOfElectricity +
", isCharging=" + isCharging +
", sleepTime=" + sleepTime + "ms" +
'}';
}
}

View File

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

View File

@@ -1,183 +1,273 @@
package cc.winboll.studio.powerbell.unittest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import 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.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:04
* @Describe 单元测试启动主页窗口
* 单元测试页面
* 功能:测试背景图加载、图片裁剪、双重刷新预览等功能
* 适配Java7 | API30 | 私有目录文件操作 | 无Uri冲突
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 单元测试页:验证图片处理与背景预览相关逻辑
*/
public class MainUnitTestActivity extends AppCompatActivity {
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0;
// 新增:权限请求码
public static final int REQUEST_STORAGE_PERMISSION = 1001;
View mainView;
BackgroundSourceUtils mBgSourceUtils;
BackgroundView mBackgroundView;
// 测试图片路径用Environment获取适配低版本避免硬编码
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
private static final long FILE_MIN_SIZE = 100L;
private static final long DOUBLE_REFRESH_DELAY = 200L;
// ====================== 成员变量区按功能分层移除所有Uri相关 ======================
private BackgroundView mBackgroundView;
private String mAppPrivateDirPath;
private File mPrivateTestImageFile; // 仅用File不用Uri
private File mPrivateCropImageFile;
private BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法按执行顺序onCreate→onActivityResult ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
setContentView(R.layout.activity_mainunittest);
mBackgroundView = findViewById(R.id.backgroundview);
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
initBaseParams();
initViewAndEvent();
copyAssetsTestImageToPrivateDir();
initBackgroundBean();
doubleRefreshPreview();
// 裁剪测试按钮点击事件(新增权限校验)
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
ToastUtils.show("onClick准备启动裁剪");
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
// 修复1移除高版本API依赖适配低版本存储权限校验
if (checkStoragePermission()) {
// 权限已授予,启动裁剪
startCropTest();
} else {
// 权限未授予,申请权限
requestStoragePermission();
}
}
});
ToastUtils.show(String.format("%s onCreate", TAG));
// 加载测试图片(验证图片路径是否有效)
loadBackground();
ToastUtils.show("单元测试页面启动完成");
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
}
/**
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
*/
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
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
// 校验权限是否授予
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, String.format("=== onActivityResult 回调 | requestCode=%d | resultCode=%d ===", requestCode, resultCode));
if (requestCode == REQUEST_CROP_IMAGE) {
handleCropResult(resultCode);
}
}
// ====================== 初始化相关方法基础参数→视图→背景Bean ======================
/**
* 初始化基础参数:私有目录、测试文件
*/
private void initBaseParams() {
LogUtils.d(TAG, "initBaseParams初始化基础参数");
// 初始化私有目录无需权限无UID冲突
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
File privateDir = new File(mAppPrivateDirPath);
if (!privateDir.exists()) {
boolean isDirCreated = privateDir.mkdirs();
LogUtils.d(TAG, String.format("initBaseParams创建私有目录 | 路径=%s | 结果=%b", mAppPrivateDirPath, isDirCreated));
}
// 初始化测试文件与裁剪文件无Uri
File refFile = new File(ASSETS_TEST_IMAGE_PATH);
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
LogUtils.d(TAG, String.format("initBaseParams测试图路径=%s", mPrivateTestImageFile.getAbsolutePath()));
LogUtils.d(TAG, String.format("initBaseParams裁剪图路径=%s", mPrivateCropImageFile.getAbsolutePath()));
}
/**
* 初始化布局与控件事件
*/
private void initViewAndEvent() {
LogUtils.d(TAG, "initViewAndEvent初始化布局与控件事件");
setContentView(R.layout.activity_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, "initViewAndEvent点击按钮→跳转主页面");
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, "initViewAndEvent点击按钮→启动裁剪File路径版");
ToastUtils.show("准备启动图片裁剪");
if (isFileValid(mPrivateTestImageFile)) {
startCropTestByFile();
} else {
ToastUtils.show("测试图片未准备好,重新拷贝");
copyAssetsTestImageToPrivateDir();
}
}
});
}
/**
* 初始化背景Bean
*/
private void initBackgroundBean() {
LogUtils.d(TAG, "initBackgroundBean初始化背景Bean");
mPreviewBackgroundBean = new BackgroundBean();
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
LogUtils.d(TAG, "initBackgroundBean背景Bean初始化完成");
}
// ====================== 核心业务方法(文件拷贝→裁剪→结果处理→预览刷新) ======================
/**
* 从assets拷贝图片到私有目录
*/
private void copyAssetsTestImageToPrivateDir() {
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir开始拷贝assets图片到私有目录");
if (isFileValid(mPrivateTestImageFile)) {
LogUtils.d(TAG, "copyAssetsTestImageToPrivateDir图片已存在无需拷贝");
return;
}
InputStream inputStream = null;
try {
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
LogUtils.d(TAG, String.format("copyAssetsTestImageToPrivateDir图片拷贝成功 | 大小=%d字节", mPrivateTestImageFile.length()));
} catch (IOException e) {
LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir图片拷贝失败 | %s", e.getMessage()), e);
ToastUtils.show("图片准备失败");
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, String.format("copyAssetsTestImageToPrivateDir关闭流失败 | %s", e.getMessage()));
}
}
if (allGranted) {
ToastUtils.show("存储权限已授予,启动裁剪");
startCropTest(); // 权限授予后启动裁剪
} else {
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【裁剪回调】requestCode" + requestCode + "resultCode" + resultCode + "data" + (data == null ? "null" : data.toString()));
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null));
// 裁剪完成后回收权限
if (requestCode == REQUEST_CROP_IMAGE) {
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
//ImageCropUtils.releaseCropPermission(this, outputUri);
mBackgroundView.loadImage(dstOutputPath);
/**
* 直接用File启动裁剪关键调用ImageCropUtils的File重载方法
*/
private void startCropTestByFile() {
LogUtils.d(TAG, String.format("startCropTestByFile启动裁剪 | 原图=%s", mPrivateTestImageFile.getAbsolutePath()));
// 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) {
boolean isDirCreated = cropParent.mkdirs();
LogUtils.d(TAG, String.format("startCropTestByFile创建裁剪目录 | 路径=%s | 结果=%b", cropParent.getAbsolutePath(), isDirCreated));
}
}
void loadBackground() {
// 校验测试图片是否存在(避免路径错误)
File testFile = new File(szTestSource);
if (testFile.exists() && testFile.length() > 100) {
mBackgroundView.loadImage(szTestSource);
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource);
// 调用ImageCropUtils的File参数方法核心绕开Uri
ImageCropUtils.startImageCrop(
this,
mPrivateTestImageFile, // 原图File
mPrivateCropImageFile, // 输出File
0,
0,
true,
REQUEST_CROP_IMAGE
);
LogUtils.d(TAG, String.format("startCropTestByFile裁剪请求已发送 | 输出路径=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("已启动图片裁剪");
}
/**
* 处理裁剪结果直接校验输出File
* @param resultCode 裁剪结果码
*/
private void handleCropResult(int resultCode) {
LogUtils.d(TAG, String.format("handleCropResult裁剪回调处理 | resultCode=%d", resultCode));
if (resultCode == RESULT_OK) {
if (isFileValid(mPrivateCropImageFile)) {
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
LogUtils.d(TAG, String.format("handleCropResult裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview();
} else {
LogUtils.e(TAG, "handleCropResult裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效");
}
} else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "handleCropResult裁剪取消");
ToastUtils.show("裁剪已取消");
} else {
ToastUtils.show("测试图片不存在或无效");
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource);
LogUtils.e(TAG, String.format("handleCropResult裁剪失败 | resultCode异常=%d", resultCode));
ToastUtils.show("裁剪失败");
}
}
}
/**
* 双重刷新预览,确保背景加载最新数据
*/
private void doubleRefreshPreview() {
LogUtils.d(TAG, "doubleRefreshPreview执行双重刷新预览");
// 第一重刷新
try {
mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第一重异常 | %s", e.getMessage()));
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing()) {
try {
mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第二重异常 | %s", e.getMessage()));
}
}
}
}, DOUBLE_REFRESH_DELAY);
}
// ====================== 工具辅助方法(文件校验) ======================
/**
* 校验文件是否有效(存在且大小达标)
* @param file 待校验文件
* @return true=有效 false=无效
*/
private boolean isFileValid(File file) {
boolean isValid = file != null && file.exists() && file.length() > FILE_MIN_SIZE;
LogUtils.d(TAG, String.format("isFileValid文件校验 | 路径=%s | 结果=%b", file != null ? file.getAbsolutePath() : "null", isValid));
return isValid;
}
}

View File

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

View File

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

View File

@@ -1,203 +1,357 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import java.io.File;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
// 应用配置工具类
//
/**
* 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
* 适配Java7 | API30 | 小米手机,单例模式,线程安全,配置持久化
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Describe 应用配置全量管理工具,支持配置持久化、自动校准、线程安全访问
*/
public class AppConfigUtils {
// ======================== 静态常量区(魔法值统一管理)========================
public static final String TAG = "AppConfigUtils";
public static final String BACKGROUND_DIR = "Background"; // 背景图片存储目录
private static final int MIN_REMINDER_VALUE = 0; // 提醒阈值最小值
private static final int MAX_REMINDER_VALUE = 100; // 提醒阈值最大值
private static final int MIN_INTERVAL_TIME = 1000; // 最小提醒间隔ms
private static final int MIN_DETECT_INTERVAL = 500; // 最小电量检测间隔ms
public static final String BACKGROUND_DIR = "Background";
// ======================== 静态成员区(单例实例)========================
private static volatile AppConfigUtils sInstance; // 单例实例volatile保障双重校验锁有效性
// 保存唯一配置实例
static AppConfigUtils _mAppConfigUtils;
// 应用环境上下文
Context mContext;
// ======================== 成员变量区按依赖优先级排序final/volatile保障线程安全========================
private final Context mContext; // 应用上下文ApplicationContext避免内存泄漏
private final App mApplication; // 应用Application实例final保障不可变
public volatile AppConfigBean mAppConfigBean; // 应用配置Bean持久化核心volatile保障线程安全
private volatile boolean mIsServiceEnabled = false; // 服务开关缓存状态减少Bean读取次数
// 是否启动铃声提醒服务
volatile boolean mIsEnableService = false;
public volatile AppConfigBean mAppConfigBean;
// 电池充电提醒值。
// Battery charge reminder value.
volatile int mnChargeReminderValue = -1;
volatile boolean mIsEnableChargeReminder = false;
// 电池耗电量提醒值。
// Battery power usege reminder value.
volatile int mnUsegeReminderValue = -1;
volatile boolean mIsEnableUsegeReminder = false;
volatile boolean mIsUseBackgroundFile = false;
volatile String mszBackgroundFileName = "";
// 保存应用实例
App mApplication;
AppConfigUtils(Context context) {
mContext = context;
String szExternalFilesDir = mContext.getExternalFilesDir(TAG) + File.separator;
//mlistAppConfigBean = new ArrayList<AppConfigBean>();
mAppConfigBean = new AppConfigBean();
loadAppConfigBean();
}
// 返回唯一实例
//
// ======================== 单例相关方法区(双重校验锁+构造方法)========================
/**
* 双重校验锁单例获取方法,线程安全
* @param context 上下文不可为null
* @return 单例实例
*/
public static AppConfigUtils getInstance(Context context) {
if (_mAppConfigUtils == null) {
_mAppConfigUtils = new AppConfigUtils(context);
String contextType = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, String.format("getInstance调用 | 传入Context类型=%s", contextType));
if (context == null) {
LogUtils.e(TAG, "getInstance失败Context不能为空");
throw new IllegalArgumentException("Context cannot be null");
}
return _mAppConfigUtils;
}
public void setIsEnableService(Activity activity, final boolean isEnableService) {
YesNoAlertDialog.show(activity, "应用设置信息", "是否保存应用配置?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onYes() {
mIsEnableService = isEnableService;
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnableService);
ControlCenterServiceBean.saveBean(mContext, bean);
if (mIsEnableService) {
LogUtils.d(TAG, "startControlCenterService");
ControlCenterService.startControlCenterService(mContext);
} else {
LogUtils.d(TAG, "stopControlCenterService");
ControlCenterService.stopControlCenterService(mContext);
}
}
@Override
public void onNo() {
MainViewFragment.relaodAppConfigs();
if (sInstance == null) {
synchronized (AppConfigUtils.class) {
if (sInstance == null) {
sInstance = new AppConfigUtils(context);
LogUtils.d(TAG, "getInstance单例实例创建成功");
}
});
}
public boolean getIsEnableService() {
ControlCenterServiceBean bean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
if (bean == null) {
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false));
return false;
}
}
return bean.isEnableService();
LogUtils.d(TAG, "getInstance单例实例获取成功");
return sInstance;
}
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
mAppConfigBean.setIsEnableChargeReminder(isEnableChargeReminder);
saveConfigData(MainActivity._mMainActivity);
/**
* 私有构造方法,禁止外部实例化
* @param context 上下文内部转换为ApplicationContext
*/
private AppConfigUtils(Context context) {
LogUtils.d(TAG, "AppConfigUtils构造方法调用");
this.mContext = context.getApplicationContext();
this.mApplication = (App) context.getApplicationContext();
mAppConfigBean = new AppConfigBean();
loadAppConfig(); // 加载持久化配置
LogUtils.d(TAG, "AppConfigUtils构造完成配置初始化成功");
}
public boolean getIsEnableChargeReminder() {
return mAppConfigBean.isEnableChargeReminder();
}
// ======================== 核心配置持久化方法区(加载+保存)========================
/**
* 加载应用配置(初始化/重载通用入口)
* @return 加载后的应用配置Bean
*/
public AppConfigBean loadAppConfig() {
LogUtils.d(TAG, "loadAppConfig调用开始加载应用配置");
AppConfigBean savedAppBean = (AppConfigBean) AppConfigBean.loadBean(mContext, AppConfigBean.class);
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
mAppConfigBean.setIsEnableUsegeReminder(isEnableUsegeReminder);
saveConfigData(MainActivity._mMainActivity);
}
public boolean getIsEnableUsegeReminder() {
return mAppConfigBean.isEnableUsegeReminder();
}
public void setChargeReminderValue(int value) {
mAppConfigBean.setChargeReminderValue(value);
saveConfigData(MainActivity._mMainActivity);
}
public int getChargeReminderValue() {
return mAppConfigBean.getChargeReminderValue();
}
public void setUsegeReminderValue(int value) {
mAppConfigBean.setUsegeReminderValue(value);
saveConfigData(MainActivity._mMainActivity);
}
public int getUsegeReminderValue() {
return mAppConfigBean.getUsegeReminderValue();
}
public void setIsCharging(boolean isCharging) {
mAppConfigBean.setIsCharging(isCharging);
}
public boolean isCharging() {
return mAppConfigBean.isCharging();
}
public void setCurrentValue(int nCurrentValue) {
mAppConfigBean.setCurrentValue(nCurrentValue);
}
public int getCurrentValue() {
return mAppConfigBean.getCurrentValue();
}
public void setReminderIntervalTime(int reminderIntervalTime) {
mAppConfigBean.setReminderIntervalTime(reminderIntervalTime);
}
public int getReminderIntervalTime() {
return mAppConfigBean.getReminderIntervalTime();
}
//
// 加载电池提醒配置数据
//
public void loadAppConfigBean() {
AppConfigBean bean = AppConfigBean.loadBean(mContext, AppConfigBean.class);
if (bean == null) {
bean = new AppConfigBean();
if (savedAppBean != null) {
mAppConfigBean = savedAppBean;
LogUtils.d(TAG, String.format("loadAppConfig成功 | 充电阈值=%d%% | 耗电阈值=%d%%",
mAppConfigBean.getChargeReminderValue(), mAppConfigBean.getUsageReminderValue()));
} else {
mAppConfigBean = new AppConfigBean();
AppConfigBean.saveBean(mContext, mAppConfigBean);
LogUtils.d(TAG, "loadAppConfig无已保存配置使用默认值并持久化");
}
mAppConfigBean.setIsEnableUsegeReminder(bean.isEnableUsegeReminder());
mAppConfigBean.setUsegeReminderValue(bean.getUsegeReminderValue());
mAppConfigBean.setIsEnableChargeReminder(bean.isEnableChargeReminder());
mAppConfigBean.setChargeReminderValue(bean.getChargeReminderValue());
return mAppConfigBean;
}
public void saveConfigData(final MainActivity activity) {
if (MainActivity._mMainActivity == null) {
/**
* 保存应用配置(内部核心方法,直接持久化)
*/
private void saveAppConfig() {
AppConfigBean.saveBean(mContext, mAppConfigBean);
LogUtils.d(TAG, "saveAppConfig应用配置保存成功");
}
// ======================== 充电提醒配置方法区(开关+阈值)========================
/**
* 设置充电提醒开关状态
* @param isEnabled 目标状态true=开启false=关闭)
*/
public void setChargeReminderEnabled(final boolean isEnabled) {
LogUtils.d(TAG, String.format("setChargeReminderEnabled调用 | 传入状态=%b", isEnabled));
if (isEnabled == mAppConfigBean.isEnableChargeReminder()) {
LogUtils.d(TAG, "setChargeReminderEnabled充电提醒状态无变化无需操作");
return;
}
YesNoAlertDialog.show(activity, "应用设置信息", "是否保存应用配置?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onYes() {
saveConfigData();
}
@Override
public void onNo() {
AppConfigUtils.getInstance(activity).loadAppConfigBean();
MainViewFragment.relaodAppConfigs();
}
});
mAppConfigBean.setEnableChargeReminder(isEnabled);
saveAppConfig();
LogUtils.d(TAG, String.format("setChargeReminderEnabled成功 | 充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
}
//
// 保存应用配置数据
//
void saveConfigData() {
// 更新配置先取消一下旧的的提醒消息
//NotificationHelper.cancelRemindNotification(mContext);
AppConfigBean.saveBean(mContext, mAppConfigBean);
// 通知活动窗口和服务配置已更新
ControlCenterService.updateStatus(mContext, mAppConfigBean);
MainViewFragment.relaodAppConfigs();
/**
* 获取充电提醒开关状态
* @return 充电提醒状态true=开启false=关闭)
*/
public boolean isChargeReminderEnabled() {
boolean isEnabled = mAppConfigBean.isEnableChargeReminder();
LogUtils.d(TAG, String.format("isChargeReminderEnabled获取充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
return isEnabled;
}
/**
* 设置充电提醒阈值自动校准0-100
* @param value 目标阈值
*/
public void setChargeReminderValue(final int value) {
LogUtils.d(TAG, String.format("setChargeReminderValue调用 | 传入阈值=%d", value));
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getChargeReminderValue()) {
LogUtils.d(TAG, "setChargeReminderValue充电提醒阈值无变化无需操作");
return;
}
mAppConfigBean.setChargeReminderValue(calibratedValue);
saveAppConfig();
LogUtils.d(TAG, String.format("setChargeReminderValue成功 | 充电提醒阈值=%d%%", calibratedValue));
}
/**
* 获取充电提醒阈值
* @return 充电提醒阈值0-100
*/
public int getChargeReminderValue() {
int value = mAppConfigBean.getChargeReminderValue();
LogUtils.d(TAG, String.format("getChargeReminderValue获取充电提醒阈值=%d%%", value));
return value;
}
// ======================== 耗电提醒配置方法区(开关+阈值)========================
/**
* 设置耗电提醒开关状态
* @param isEnabled 目标状态true=开启false=关闭)
*/
public void setUsageReminderEnabled(final boolean isEnabled) {
LogUtils.d(TAG, String.format("setUsageReminderEnabled调用 | 传入状态=%b", isEnabled));
if (isEnabled == mAppConfigBean.isEnableUsageReminder()) {
LogUtils.d(TAG, "setUsageReminderEnabled耗电提醒状态无变化无需操作");
return;
}
mAppConfigBean.setEnableUsageReminder(isEnabled);
saveAppConfig();
LogUtils.d(TAG, String.format("setUsageReminderEnabled成功 | 耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
}
/**
* 获取耗电提醒开关状态
* @return 耗电提醒状态true=开启false=关闭)
*/
public boolean isUsageReminderEnabled() {
boolean isEnabled = mAppConfigBean.isEnableUsageReminder();
LogUtils.d(TAG, String.format("isUsageReminderEnabled获取耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
return isEnabled;
}
/**
* 设置耗电提醒阈值自动校准0-100
* @param value 目标阈值
*/
public void setUsageReminderValue(final int value) {
LogUtils.d(TAG, String.format("setUsageReminderValue调用 | 传入阈值=%d", value));
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getUsageReminderValue()) {
LogUtils.d(TAG, "setUsageReminderValue耗电提醒阈值无变化无需操作");
return;
}
mAppConfigBean.setUsageReminderValue(calibratedValue);
saveAppConfig();
LogUtils.d(TAG, String.format("setUsageReminderValue成功 | 耗电提醒阈值=%d%%", calibratedValue));
}
/**
* 获取耗电提醒阈值
* @return 耗电提醒阈值0-100
*/
public int getUsageReminderValue() {
int value = mAppConfigBean.getUsageReminderValue();
LogUtils.d(TAG, String.format("getUsageReminderValue获取耗电提醒阈值=%d%%", value));
return value;
}
// ======================== 实时电池状态配置方法区(内存缓存,不持久化)========================
/**
* 设置当前充电状态(仅内存缓存)
* @param isCharging 充电状态true=充电中false=未充电)
*/
public void setCharging(boolean isCharging) {
LogUtils.d(TAG, String.format("setCharging调用 | 传入状态=%b", isCharging));
if (isCharging == mAppConfigBean.isCharging()) {
LogUtils.d(TAG, "setCharging充电状态无变化无需操作");
return;
}
mAppConfigBean.setIsCharging(isCharging);
LogUtils.d(TAG, String.format("setCharging成功 | 充电状态=%s", isCharging ? "充电中" : "未充电"));
}
/**
* 获取当前充电状态
* @return 充电状态true=充电中false=未充电)
*/
public boolean isCharging() {
boolean isCharging = mAppConfigBean.isCharging();
LogUtils.d(TAG, String.format("isCharging获取充电状态=%s", isCharging ? "充电中" : "未充电"));
return isCharging;
}
/**
* 设置当前电池电量仅内存缓存自动校准0-100
* @param value 当前电量
*/
public void setCurrentBatteryValue(int value) {
LogUtils.d(TAG, String.format("setCurrentBatteryValue调用 | 传入电量=%d", value));
int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getCurrentBatteryValue()) {
LogUtils.d(TAG, "setCurrentBatteryValue电池电量无变化无需操作");
return;
}
mAppConfigBean.setCurrentBatteryValue(calibratedValue);
LogUtils.d(TAG, String.format("setCurrentBatteryValue成功 | 电池电量=%d%%", calibratedValue));
}
/**
* 获取当前电池电量
* @return 当前电池电量0-100
*/
public int getCurrentBatteryValue() {
int value = mAppConfigBean.getCurrentBatteryValue();
LogUtils.d(TAG, String.format("getCurrentBatteryValue获取电池电量=%d%%", value));
return value;
}
// ======================== 间隔配置方法区(持久化)========================
/**
* 设置提醒间隔时间自动校准最小1000ms
* @param interval 目标间隔单位ms
*/
public void setReminderIntervalTime(final int interval) {
LogUtils.d(TAG, String.format("setReminderIntervalTime调用 | 传入间隔=%dms", interval));
final int calibratedInterval = Math.max(interval, MIN_INTERVAL_TIME);
if (calibratedInterval == mAppConfigBean.getReminderIntervalTime()) {
LogUtils.d(TAG, "setReminderIntervalTime提醒间隔无变化无需操作");
return;
}
mAppConfigBean.setReminderIntervalTime(calibratedInterval);
saveAppConfig();
LogUtils.d(TAG, String.format("setReminderIntervalTime成功 | 提醒间隔=%dms", calibratedInterval));
}
/**
* 获取提醒间隔时间
* @return 提醒间隔单位ms
*/
public int getReminderIntervalTime() {
int interval = mAppConfigBean.getReminderIntervalTime();
LogUtils.d(TAG, String.format("getReminderIntervalTime获取提醒间隔=%dms", interval));
return interval;
}
/**
* 设置电量检测间隔自动校准最小500ms
* @param interval 目标间隔单位ms
*/
public void setBatteryDetectInterval(final int interval) {
LogUtils.d(TAG, String.format("setBatteryDetectInterval调用 | 传入间隔=%dms", interval));
final int calibratedInterval = Math.max(interval, MIN_DETECT_INTERVAL);
if (calibratedInterval == mAppConfigBean.getBatteryDetectInterval()) {
LogUtils.d(TAG, "setBatteryDetectInterval检测间隔无变化无需操作");
return;
}
mAppConfigBean.setBatteryDetectInterval(calibratedInterval);
saveAppConfig();
LogUtils.d(TAG, String.format("setBatteryDetectInterval成功 | 电量检测间隔=%dms", calibratedInterval));
}
/**
* 获取电量检测间隔
* @return 电量检测间隔单位ms
*/
public int getBatteryDetectInterval() {
int interval = mAppConfigBean.getBatteryDetectInterval();
LogUtils.d(TAG, String.format("getBatteryDetectInterval获取电量检测间隔=%dms", interval));
return interval;
}
// ======================== 服务开关配置方法区独立Bean========================
/**
* 获取服务开关状态
* @return 服务开关状态true=开启false=关闭)
*/
public boolean isServiceEnabled() {
LogUtils.d(TAG, "isServiceEnabled调用开始获取服务开关状态");
ControlCenterServiceBean savedServiceBean = (ControlCenterServiceBean) ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
if (savedServiceBean != null) {
boolean isEnabled = savedServiceBean.isEnableService();
LogUtils.d(TAG, String.format("isServiceEnabled服务开关状态=%b", isEnabled));
return isEnabled;
} else {
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false));
LogUtils.d(TAG, "isServiceEnabled无已保存服务配置默认关闭并持久化");
return false;
}
}
/**
* 设置服务开关状态
* @param isServiceEnabled 目标状态true=开启false=关闭)
*/
public void setIsServiceEnabled(boolean isServiceEnabled) {
LogUtils.d(TAG, String.format("setIsServiceEnabled调用 | 传入状态=%b", isServiceEnabled));
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(isServiceEnabled));
LogUtils.d(TAG, String.format("setIsServiceEnabled成功 | 服务开关状态=%b", isServiceEnabled));
}
}

View File

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

View File

@@ -1,28 +1,82 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 04:32:46
* @Describe 电池工具类
*/
import android.content.Context;
import android.content.Intent;
import android.os.BatteryManager;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2024/07/18 04:32:46
* @Describe 电池状态工具类
* 功能解析电池广播Intent获取充电状态、当前电量
* 适配Java7 | API30 | 小米手机
*/
public class BatteryUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "BatteryUtils";
// 电池电量计算常量
private static final int BATTERY_SCALE_DEFAULT = 100; // 电量刻度默认值
private static final int BATTERY_LEVEL_MIN = 0; // 电量百分比最小值
private static final int BATTERY_LEVEL_MAX = 100; // 电量百分比最大值
private static final int EXTRA_STATUS_DEFAULT = -1; // 电池状态默认值
// ================================== 工具方法(静态方法,无状态设计)=================================
/**
* 判断当前是否处于充电状态
* @param intent 电池状态广播Intent非空
* @return true=充电中/已充满false=未充电
*/
public static boolean isCharging(Intent intent) {
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL;
LogUtils.d(TAG, "【isCharging】调用开始");
// 入参非空校验
if (intent == null) {
LogUtils.e(TAG, "【isCharging】入参异常intent为空返回false");
return false;
}
// 解析电池状态
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, EXTRA_STATUS_DEFAULT);
LogUtils.d(TAG, "【isCharging】解析电池状态status=" + status);
// 判断充电状态(充电中/已充满均视为充电状态)
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING
|| status == BatteryManager.BATTERY_STATUS_FULL;
LogUtils.d(TAG, "【isCharging】调用结束 | 充电状态=" + isCharging);
return isCharging;
}
public static int getTheQuantityOfElectricity(Intent intent) {
int intLevel = intent.getIntExtra("level", 0);
int intScale = intent.getIntExtra("scale", 100);
return intLevel * 100 / intScale;
/**
* 获取当前电池电量百分比0-100
* @param intent 电池状态广播Intent非空
* @return 电量百分比异常返回0
*/
public static int getCurrentBatteryLevel(Intent intent) {
LogUtils.d(TAG, "【getCurrentBatteryLevel】调用开始");
// 入参非空校验
if (intent == null) {
LogUtils.e(TAG, "【getCurrentBatteryLevel】入参异常intent为空返回0");
return BATTERY_LEVEL_MIN;
}
// 解析电量原始值与刻度值
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE_DEFAULT);
LogUtils.d(TAG, "【getCurrentBatteryLevel】解析原始数据 | level=" + level + " | scale=" + scale);
// 计算并校验电量百分比避免除以0或数值越界
int batteryLevel;
if (scale <= 0) {
LogUtils.w(TAG, "【getCurrentBatteryLevel】刻度值无效scale=" + scale + "直接使用level值");
batteryLevel = level;
} else {
batteryLevel = level * BATTERY_SCALE_DEFAULT / scale;
}
// 确保电量值在0-100范围内
batteryLevel = Math.max(BATTERY_LEVEL_MIN, Math.min(batteryLevel, BATTERY_LEVEL_MAX));
LogUtils.d(TAG, "【getCurrentBatteryLevel】调用结束 | 电量百分比=" + batteryLevel + "%");
return batteryLevel;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,44 +10,100 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* 图片处理工具类(质量压缩专用)
* 功能对图片进行JPEG质量压缩并将压缩结果覆盖源文件
* 适配Java 7 + Android API 30
* 核心逻辑Bitmap.compress 质量压缩 + FileChannel 高效文件复制
*/
public class ImageUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = ImageUtils.class.getSimpleName();
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
private static final int MIN_COMPRESS_QUALITY = 0;
private static final int MAX_COMPRESS_QUALITY = 100;
// 这里我们生成了一个Pic文件夹在下面放了我们质量压缩后的图片用于和原图对比
// 压缩图片使用Bitmap.compress(),这里是质量压缩
// 参数Context context :调用本函数函数引用的资源体系
// String szSrcImagePath :要压缩的源文件路径
// String szDstImagePath :压缩后文件要保存的路径
// int nPictureCompress :图片压缩比例
public static void bitmapCompress(Context context, String szSrcImagePath, String szDstImagePath, int nPictureCompress) {
// ================================== 核心工具方法(图片质量压缩)=================================
/**
* 图片质量压缩JPEG格式压缩后覆盖源文件
* @param context 上下文(备用,当前逻辑未直接使用)
* @param srcImagePath 源图片文件路径(非空,文件需存在)
* @param dstImagePath 压缩后临时保存路径(非空,用于存储压缩中间文件)
* @param compressQuality 压缩质量0-100数值越小压缩率越高
*/
public static void bitmapCompress(Context context, String srcImagePath, String dstImagePath, int compressQuality) {
LogUtils.d(TAG, "【bitmapCompress】调用开始 | 源路径=" + srcImagePath + " | 临时路径=" + dstImagePath + " | 压缩质量=" + compressQuality);
// 1. 前置参数校验
if (srcImagePath == null || srcImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常源文件路径为空");
return;
}
if (dstImagePath == null || dstImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常临时文件路径为空");
return;
}
if (compressQuality < MIN_COMPRESS_QUALITY || compressQuality > MAX_COMPRESS_QUALITY) {
LogUtils.e(TAG, "【bitmapCompress】参数异常压缩质量超出范围0-100当前值=" + compressQuality);
return;
}
File srcFile = new File(srcImagePath);
if (!srcFile.exists() || !srcFile.isFile()) {
LogUtils.e(TAG, "【bitmapCompress】源文件无效不存在或不是文件 " + srcImagePath);
return;
}
Bitmap compressBitmap = null;
OutputStream outputStream = null;
try {
Bitmap bmpCompressImage;
// 2. 读取源图片为Bitmap
compressBitmap = BitmapFactory.decodeFile(srcImagePath);
if (compressBitmap == null) {
LogUtils.e(TAG, "【bitmapCompress】Bitmap解码失败无法读取源图片 " + srcImagePath);
return;
}
LogUtils.d(TAG, "【bitmapCompress】Bitmap解码成功 | 尺寸=" + compressBitmap.getWidth() + "x" + compressBitmap.getHeight());
//生成新的文件
File fDstCompressImage = new File(szDstImagePath);
// 3. 创建临时压缩文件
File dstFile = new File(dstImagePath);
File dstParentDir = dstFile.getParentFile();
if (dstParentDir != null && !dstParentDir.exists()) {
boolean isDirCreated = dstParentDir.mkdirs();
LogUtils.d(TAG, "【bitmapCompress】临时目录创建" + (isDirCreated ? "成功" : "失败") + "" + dstParentDir.getAbsolutePath());
}
//裁剪后的图像转成BitMap
//photoBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uriClipUri));
bmpCompressImage = BitmapFactory.decodeFile(szSrcImagePath);
// 4. 写入压缩数据
outputStream = new FileOutputStream(dstFile);
boolean isCompressSuccess = compressBitmap.compress(COMPRESS_FORMAT, compressQuality, outputStream);
if (!isCompressSuccess) {
LogUtils.e(TAG, "【bitmapCompress】压缩失败Bitmap.compress 执行失败");
return;
}
LogUtils.d(TAG, "【bitmapCompress】压缩成功临时文件已生成 " + dstFile.getAbsolutePath());
//创建输出流
OutputStream out = null;
out = new FileOutputStream(fDstCompressImage.getPath());
//压缩文件,返回结果,参数分别是压缩的格式,压缩质量的百分比,输出流
boolean bCompress = bmpCompressImage.compress(Bitmap.CompressFormat.JPEG, nPictureCompress, out);
// 复制压缩后的文件到源路径
File fSrcImage = new File(szSrcImagePath);
FileUtils.copyFileUsingFileChannels(fDstCompressImage, fSrcImage);
LogUtils.d(TAG, Integer.toString(nPictureCompress) + "%压缩结束。");
// 5. 复制压缩文件覆盖源文件
FileUtils.copyFileUsingFileChannels(dstFile, srcFile);
LogUtils.d(TAG, "【bitmapCompress】" + compressQuality + "%压缩结束:已覆盖源文件 " + srcImagePath);
} catch (FileNotFoundException e) {
LogUtils.d(TAG, "bitmapCompress FileNotFoundException : " + e.getMessage());
LogUtils.e(TAG, "bitmapCompress】文件未找到异常", e);
} catch (IOException e) {
LogUtils.d(TAG, "bitmapCompress IOException : " + e.getMessage());
LogUtils.e(TAG, "bitmapCompressIO异常", e);
} finally {
// 6. 资源释放:关闭输出流
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【bitmapCompress】输出流关闭失败", e);
}
}
// 7. 资源释放回收Bitmap
if (compressBitmap != null && !compressBitmap.isRecycled()) {
compressBitmap.recycle();
LogUtils.d(TAG, "【bitmapCompress】Bitmap资源已回收");
}
}
}
}

View File

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

View File

@@ -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,506 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.RingtoneManager;
import android.os.Build;
import android.provider.Settings;
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;
/**
* 通知工具类:统一管理前台服务/电池提醒/应用配置信息通知
* 适配API19-30 | Java7 | 小米手机
* 特性前台服务无铃声、提醒通知系统默认铃声、配置通知低优先级无打扰、API分级适配、内存泄漏防护
*/
public class NotificationManagerUtils {
// ================================== 静态常量(置顶统一管理,杜绝魔法值)=================================
public static final String TAG = "NotificationManagerUtils";
// 通知渠道IDAPI26+ 必需,区分通知类型)
public static final String CHANNEL_ID_FOREGROUND = "cc.winboll.studio.powerbell.channel.foreground";
public static final String CHANNEL_ID_REMIND = "cc.winboll.studio.powerbell.channel.remind";
public static final String CHANNEL_ID_CONFIG = "cc.winboll.studio.powerbell.channel.config"; // 新增:应用配置信息渠道
// 通知ID唯一标识避免重复
public static final int NOTIFY_ID_FOREGROUND_SERVICE = 1001;
public static final int NOTIFY_ID_REMIND = 1002;
public static final int NOTIFY_ID_CONFIG = 1003; // 新增应用配置信息通知ID
// 低版本兼容默认通知图标API<21 避免显示异常)
private static final int NOTIFICATION_DEFAULT_ICON = R.drawable.ic_launcher;
// 通知内容兜底常量
private static final String FOREGROUND_NOTIFY_TITLE_DEFAULT = "电池服务运行中";
private static final String FOREGROUND_NOTIFY_CONTENT_DEFAULT = "后台监测电池状态";
private static final String REMIND_NOTIFY_TITLE_DEFAULT = "电池状态提醒";
private static final String REMIND_NOTIFY_CONTENT_DEFAULT = "电池状态异常,请及时处理";
private static final String CONFIG_NOTIFY_TITLE_DEFAULT = "应用配置更新"; // 新增:配置通知默认标题
private static final String CONFIG_NOTIFY_CONTENT_DEFAULT = "配置信息已更新,生效中"; // 新增:配置通知默认内容
// PendingIntent请求码
private static final int PENDING_INTENT_REQUEST_CODE_FOREGROUND = 0;
private static final int PENDING_INTENT_REQUEST_CODE_REMIND = 1;
private static final int PENDING_INTENT_REQUEST_CODE_CONFIG = 2; // 新增:配置通知请求码
// ================================== 成员变量(私有封装,按依赖优先级排序)=================================
// 核心上下文(应用级,避免内存泄漏)
private Context mContext;
// 系统通知服务(核心依赖)
private NotificationManager mNotificationManager;
// 前台服务通知实例(单独持有,便于更新/取消)
private Notification mForegroundServiceNotify;
// ================================== 构造方法(初始化核心资源,前置校验)=================================
public NotificationManagerUtils(Context context) {
LogUtils.d(TAG, "NotificationManagerUtils: 构造方法执行 | context=" + context);
// 前置校验Context非空
if (context == null) {
LogUtils.e(TAG, "NotificationManagerUtils: 构造失败context is null");
return;
}
// 初始化核心资源
this.mContext = context.getApplicationContext();
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
LogUtils.d(TAG, "NotificationManagerUtils: 核心资源初始化完成 | mContext=" + mContext + " | mNotificationManager=" + mNotificationManager);
// 初始化通知渠道API26+ 必需)
initNotificationChannels();
LogUtils.d(TAG, "NotificationManagerUtils: 构造完成");
}
// ================================== 核心初始化方法通知渠道API分级适配=================================
/**
* 初始化通知渠道:前台服务渠道(无铃声+无振动)、提醒渠道(系统默认铃声+无振动)、配置信息渠道(低优先级无打扰)
*/
private void initNotificationChannels() {
LogUtils.d(TAG, "initNotificationChannels: 执行通知渠道初始化");
// API<26 无渠道机制,直接返回
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
LogUtils.d(TAG, "initNotificationChannels: API<26无需创建渠道");
return;
}
// 通知服务为空,避免空指针
if (mNotificationManager == null) {
LogUtils.e(TAG, "initNotificationChannels: 失败NotificationManager is null");
return;
}
// 1. 前台服务渠道(低优先级,后台保活无打扰)
NotificationChannel foregroundChannel = new NotificationChannel(
CHANNEL_ID_FOREGROUND,
"电池服务保活",
NotificationManager.IMPORTANCE_LOW
);
foregroundChannel.setDescription("电池监测服务后台运行,无声音、无振动");
foregroundChannel.enableLights(false);
foregroundChannel.enableVibration(false);
foregroundChannel.setSound(null, null); // 强制无铃声
foregroundChannel.setShowBadge(false);
foregroundChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
LogUtils.d(TAG, "initNotificationChannels: 前台服务渠道配置完成");
// 2. 电池提醒渠道(中优先级,系统默认铃声,无振动)
NotificationChannel remindChannel = new NotificationChannel(
CHANNEL_ID_REMIND,
"电池状态提醒",
NotificationManager.IMPORTANCE_DEFAULT
);
remindChannel.setDescription("电池满电/低电量提醒,系统默认铃声,无振动");
remindChannel.enableLights(true);
remindChannel.enableVibration(false);
remindChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT);
remindChannel.setShowBadge(false);
remindChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
LogUtils.d(TAG, "initNotificationChannels: 电池提醒渠道配置完成");
// 3. 应用配置信息渠道(新增:最低优先级,无铃声无振动,仅提示不打扰)
NotificationChannel configChannel = new NotificationChannel(
CHANNEL_ID_CONFIG,
"应用配置信息",
NotificationManager.IMPORTANCE_MIN
);
configChannel.setDescription("应用配置更新、参数变更等提示,无声音、无振动");
configChannel.enableLights(false);
configChannel.enableVibration(false);
configChannel.setSound(null, null);
configChannel.setShowBadge(false);
configChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
LogUtils.d(TAG, "initNotificationChannels: 应用配置信息渠道配置完成");
// 注册渠道到系统
mNotificationManager.createNotificationChannel(foregroundChannel);
mNotificationManager.createNotificationChannel(remindChannel);
mNotificationManager.createNotificationChannel(configChannel); // 注册新增渠道
LogUtils.d(TAG, "initNotificationChannels: 成功:创建前台服务+电池提醒+应用配置信息渠道");
}
// ================================== 对外核心方法(前台服务通知:启动/更新/取消)=================================
/**
* 启动前台服务通知API30适配无铃声
*/
public void startForegroundServiceNotify(Service service, NotificationMessage message) {
LogUtils.d(TAG, "startForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | service=" + service + " | message=" + message);
// 前置校验:参数非空
if (service == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "startForegroundServiceNotify: 失败param is null | service=" + service + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return;
}
// 构建前台通知
mForegroundServiceNotify = buildForegroundNotification(message);
if (mForegroundServiceNotify == null) {
LogUtils.e(TAG, "startForegroundServiceNotify: 失败:构建通知为空");
return;
}
// 启动前台服务API30无FOREGROUND_SERVICE_TYPE限制全版本通用
try {
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "startForegroundServiceNotify: 成功");
} catch (Exception e) {
LogUtils.e(TAG, "startForegroundServiceNotify: 异常", e);
}
}
/**
* 更新前台服务通知内容复用通知ID保持无铃声
*/
public void updateForegroundServiceNotify(NotificationMessage message) {
LogUtils.d(TAG, "updateForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | message=" + message);
if (message == null || mNotificationManager == null) {
LogUtils.e(TAG, "updateForegroundServiceNotify: 失败param is null | message=" + message + " | mNotificationManager=" + mNotificationManager);
return;
}
mForegroundServiceNotify = buildForegroundNotification(message);
if (mForegroundServiceNotify == null) {
LogUtils.e(TAG, "updateForegroundServiceNotify: 失败:构建通知为空");
return;
}
try {
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "updateForegroundServiceNotify: 成功");
} catch (Exception e) {
LogUtils.e(TAG, "updateForegroundServiceNotify: 异常", e);
}
}
/**
* 取消前台服务通知Service销毁时调用
*/
public void cancelForegroundServiceNotify() {
LogUtils.d(TAG, "cancelForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE);
cancelNotification(NOTIFY_ID_FOREGROUND_SERVICE);
mForegroundServiceNotify = null; // 置空释放
LogUtils.d(TAG, "cancelForegroundServiceNotify: 成功");
}
// ================================== 对外核心方法(电池提醒通知:发送)=================================
/**
* 发送电池提醒通知(系统默认铃声,无振动)
*/
public void showRemindNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "showRemindNotification: 执行 | notifyId=" + NOTIFY_ID_REMIND + " | context=" + context + " | message=" + message);
if (context == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "showRemindNotification: 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return;
}
Notification remindNotify = buildRemindNotification(context, message);
if (remindNotify == null) {
LogUtils.e(TAG, "showRemindNotification: 失败:构建通知为空");
return;
}
try {
mNotificationManager.notify(NOTIFY_ID_REMIND, remindNotify);
LogUtils.d(TAG, "showRemindNotification: 成功");
} catch (Exception e) {
LogUtils.e(TAG, "showRemindNotification: 异常", e);
}
}
// ================================== 对外核心方法(应用配置信息通知:发送)=================================
/**
* 发送应用配置信息通知(新增:低优先级无铃声,仅提示不打扰)
*/
public void showConfigNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "showConfigNotification: 执行 | notifyId=" + NOTIFY_ID_CONFIG + " | context=" + context + " | message=" + message);
if (context == null || message == null || mNotificationManager == null) {
LogUtils.e(TAG, "showConfigNotification: 失败param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
return;
}
Notification configNotify = buildConfigNotification(context, message);
if (configNotify == null) {
LogUtils.e(TAG, "showConfigNotification: 失败:构建通知为空");
return;
}
try {
mNotificationManager.notify(NOTIFY_ID_CONFIG, configNotify);
LogUtils.d(TAG, "showConfigNotification: 成功");
} catch (Exception e) {
LogUtils.e(TAG, "showConfigNotification: 异常", e);
}
}
// ================================== 对外工具方法(通知取消:单个/全部)=================================
/**
* 取消指定ID的通知
*/
public void cancelNotification(int notifyId) {
LogUtils.d(TAG, "cancelNotification: 执行 | notifyId=" + notifyId);
if (mNotificationManager == null) {
LogUtils.e(TAG, "cancelNotification: 失败NotificationManager is null");
return;
}
try {
mNotificationManager.cancel(notifyId);
LogUtils.d(TAG, "cancelNotification: 成功 | notifyId=" + notifyId);
} catch (Exception e) {
LogUtils.e(TAG, "cancelNotification: 异常 | notifyId=" + notifyId, e);
}
}
/**
* 取消所有通知(兜底场景使用)
*/
public void cancelAllNotifications() {
LogUtils.d(TAG, "cancelAllNotifications: 执行");
if (mNotificationManager == null) {
LogUtils.e(TAG, "cancelAllNotifications: 失败NotificationManager is null");
return;
}
try {
mNotificationManager.cancelAll();
LogUtils.d(TAG, "cancelAllNotifications: 成功");
} catch (Exception e) {
LogUtils.e(TAG, "cancelAllNotifications: 异常", e);
}
}
// ================================== 内部辅助方法(通知构建:前台服务通知)=================================
/**
* 构建前台服务通知(全版本无铃声+无振动)
*/
private Notification buildForegroundNotification(NotificationMessage message) {
LogUtils.d(TAG, "buildForegroundNotification: 执行 | message=" + message);
if (message == null || mContext == null) {
LogUtils.e(TAG, "buildForegroundNotification: 失败param is null | message=" + message + " | mContext=" + mContext);
return null;
}
// 内容兜底
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : FOREGROUND_NOTIFY_TITLE_DEFAULT;
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : FOREGROUND_NOTIFY_CONTENT_DEFAULT;
LogUtils.d(TAG, "buildForegroundNotification: 内容兜底完成 | title=" + title + " | content=" + content);
Notification.Builder builder;
// API分级构建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API26+:绑定前台渠道(渠道已配置无铃声)
builder = new Notification.Builder(mContext, CHANNEL_ID_FOREGROUND);
LogUtils.d(TAG, "buildForegroundNotification: 使用API26+渠道构建");
} else {
// API<26直接构建手动禁用铃声振动
builder = new Notification.Builder(mContext);
builder.setSound(null);
builder.setVibrate(new long[]{0});
builder.setDefaults(0);
LogUtils.d(TAG, "buildForegroundNotification: 使用API<26手动配置");
}
// 通用配置
builder.setSmallIcon(NOTIFICATION_DEFAULT_ICON)
.setContentTitle(title)
.setContentText(content)
.setAutoCancel(false)
.setOngoing(true) // 不可手动关闭
.setWhen(System.currentTimeMillis())
.setContentIntent(createJumpPendingIntent(mContext, PENDING_INTENT_REQUEST_CODE_FOREGROUND));
// API21+ 新增大图标+主题色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setLargeIcon(getAppIcon(mContext))
.setColor(mContext.getResources().getColor(R.color.colorPrimary))
.setPriority(Notification.PRIORITY_LOW);
LogUtils.d(TAG, "buildForegroundNotification: 补充API21+配置");
}
Notification notification = builder.build();
LogUtils.d(TAG, "buildForegroundNotification: 成功构建前台通知");
return notification;
}
// ================================== 内部辅助方法(通知构建:电池提醒通知)=================================
/**
* 构建电池提醒通知(全版本系统默认铃声+无振动)
*/
private Notification buildRemindNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "buildRemindNotification: 执行 | context=" + context + " | message=" + message);
if (context == null || message == null) {
LogUtils.e(TAG, "buildRemindNotification: 失败param is null | context=" + context + " | message=" + message);
return null;
}
// 内容兜底
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : REMIND_NOTIFY_TITLE_DEFAULT;
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : REMIND_NOTIFY_CONTENT_DEFAULT;
LogUtils.d(TAG, "buildRemindNotification: 内容兜底完成 | title=" + title + " | content=" + content);
Notification.Builder builder;
// API分级构建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API26+:绑定提醒渠道(渠道已配置默认铃声)
builder = new Notification.Builder(context, CHANNEL_ID_REMIND);
LogUtils.d(TAG, "buildRemindNotification: 使用API26+渠道构建");
} else {
// API<26手动配置默认铃声关闭振动
builder = new Notification.Builder(context);
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI) // 显式默认铃声
.setVibrate(new long[]{0})
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND);
LogUtils.d(TAG, "buildRemindNotification: 使用API<26手动配置");
}
// 通用配置
builder.setSmallIcon(NOTIFICATION_DEFAULT_ICON)
.setContentTitle(title)
.setContentText(content)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT)
.setAutoCancel(true) // 点击关闭
.setOngoing(false)
.setWhen(System.currentTimeMillis())
.setContentIntent(createJumpPendingIntent(context, PENDING_INTENT_REQUEST_CODE_REMIND));
// API21+ 新增大图标+主题色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setLargeIcon(getAppIcon(context))
.setColor(context.getResources().getColor(R.color.colorPrimary))
.setPriority(Notification.PRIORITY_DEFAULT);
LogUtils.d(TAG, "buildRemindNotification: 补充API21+配置");
}
Notification notification = builder.build();
LogUtils.d(TAG, "buildRemindNotification: 成功构建提醒通知");
return notification;
}
// ================================== 内部辅助方法(通知构建:应用配置信息通知)=================================
/**
* 构建应用配置信息通知(新增:全版本无铃声+无振动,低优先级)
*/
private Notification buildConfigNotification(Context context, NotificationMessage message) {
LogUtils.d(TAG, "buildConfigNotification: 执行 | context=" + context + " | message=" + message);
if (context == null || message == null) {
LogUtils.e(TAG, "buildConfigNotification: 失败param is null | context=" + context + " | message=" + message);
return null;
}
// 内容兜底
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : CONFIG_NOTIFY_TITLE_DEFAULT;
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : CONFIG_NOTIFY_CONTENT_DEFAULT;
LogUtils.d(TAG, "buildConfigNotification: 内容兜底完成 | title=" + title + " | content=" + content);
Notification.Builder builder;
// API分级构建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API26+:绑定配置渠道(渠道已配置无铃声)
builder = new Notification.Builder(context, CHANNEL_ID_CONFIG);
LogUtils.d(TAG, "buildConfigNotification: 使用API26+渠道构建");
} else {
// API<26直接构建手动禁用铃声振动
builder = new Notification.Builder(context);
builder.setSound(null);
builder.setVibrate(new long[]{0});
builder.setDefaults(0);
LogUtils.d(TAG, "buildConfigNotification: 使用API<26手动配置");
}
// 通用配置
builder.setSmallIcon(NOTIFICATION_DEFAULT_ICON)
.setContentTitle(title)
.setContentText(content)
.setAutoCancel(true) // 点击关闭
.setOngoing(false)
.setWhen(System.currentTimeMillis())
.setContentIntent(createJumpPendingIntent(context, PENDING_INTENT_REQUEST_CODE_CONFIG));
// API21+ 新增大图标+主题色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setLargeIcon(getAppIcon(context))
.setColor(context.getResources().getColor(R.color.colorPrimary))
.setPriority(Notification.PRIORITY_MIN); // 最低优先级
LogUtils.d(TAG, "buildConfigNotification: 补充API21+配置");
}
Notification notification = builder.build();
LogUtils.d(TAG, "buildConfigNotification: 成功构建配置信息通知");
return notification;
}
// ================================== 内部辅助方法创建跳转PendingIntentAPI30安全适配=================================
/**
* 创建跳转MainActivity的PendingIntentAPI23+ 添加IMMUTABLE标记避免安全异常
*/
private PendingIntent createJumpPendingIntent(Context context, int requestCode) {
LogUtils.d(TAG, "createJumpPendingIntent: 执行 | requestCode=" + requestCode + " | context=" + context);
Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
LogUtils.d(TAG, "createJumpPendingIntent: 跳转Intent配置完成");
// API23+ 必需添加IMMUTABLE适配API30安全规范
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
LogUtils.d(TAG, "createJumpPendingIntent: 添加FLAG_IMMUTABLE标记API23+");
}
PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, flags);
LogUtils.d(TAG, "createJumpPendingIntent: 成功 | requestCode=" + requestCode);
return pendingIntent;
}
// ================================== 内部辅助方法获取APP图标异常兜底=================================
/**
* 获取APP图标失败返回默认图标
*/
private Bitmap getAppIcon(Context context) {
LogUtils.d(TAG, "getAppIcon: 执行 | context=" + context);
try {
PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
Bitmap appIcon = BitmapFactory.decodeResource(context.getResources(), pkgInfo.applicationInfo.icon);
LogUtils.d(TAG, "getAppIcon: 成功:获取应用图标");
return appIcon;
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getAppIcon: 异常:获取应用图标失败,使用默认图标", e);
return BitmapFactory.decodeResource(context.getResources(), NOTIFICATION_DEFAULT_ICON);
}
}
// ================================== 资源释放方法(避免内存泄漏)=================================
/**
* 释放资源,销毁时调用
*/
public void release() {
LogUtils.d(TAG, "release: 执行资源释放");
cancelForegroundServiceNotify();
mNotificationManager = null;
mContext = null;
LogUtils.d(TAG, "release: 成功:所有资源已释放");
}
// ================================== 对外 getter 方法(仅前台通知实例,只读)=================================
public Notification getForegroundServiceNotify() {
return mForegroundServiceNotify;
}
}

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

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

View File

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

View File

@@ -1,150 +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, String filePath) {
File file = new File(filePath);
return getUriForFile(context, file);
}
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

@@ -3,67 +3,74 @@ package cc.winboll.studio.powerbell.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.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;
/**
* 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪、无压缩
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质
*/
public class BackgroundView extends RelativeLayout {
// ====================================== 静态常量区 ======================================
public static final String TAG = "BackgroundView";
// Bitmap 配置常量(原始品质)
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int BITMAP_SAMPLE_SIZE = 1; // 不缩放采样率
// ====================================== 成员变量区 ======================================
// 缓存相关
private String mCurrentCachedPath = "";
// 视图相关
private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout
private ImageView mIvBackground; // 图片显示控件
// 图片属性
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高)
// ====================================== 构造器Java7兼容 ======================================
public BackgroundView(Context context) {
super(context);
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ===");
LogUtils.d(TAG, "=== BackgroundView 构造器1启动 [context] ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ===");
LogUtils.d(TAG, "=== BackgroundView 构造器2启动 [context, attrs] ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ===");
LogUtils.d(TAG, "=== BackgroundView 构造器3启动 [context, attrs, defStyleAttr] ===");
this.mContext = context;
initView();
}
// ====================================== 初始化 ======================================
// ====================================== 初始化方法 ======================================
private void initView() {
LogUtils.d(TAG, "=== initView 启动 ===");
// 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setBackgroundColor(0x00000000);
setBackground(new ColorDrawable(0x00000000));
// 2. 初始化主容器LinearLayout
initLinearLayout();
// 3. 初始化ImageView
initImageView();
// 4. 初始设置透明背景
setDefaultTransparentBackground();
LogUtils.d(TAG, "=== initView 完成 ===");
@@ -72,14 +79,13 @@ public class BackgroundView extends RelativeLayout {
private void initLinearLayout() {
LogUtils.d(TAG, "=== initLinearLayout 启动 ===");
mLlContainer = new LinearLayout(mContext);
// 配置LinearLayout全屏+垂直方向+居中
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
);
mLlContainer.setLayoutParams(llParams);
mLlContainer.setOrientation(LinearLayout.VERTICAL);
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中
mLlContainer.setGravity(android.view.Gravity.CENTER);
mLlContainer.setBackgroundColor(0x00000000);
this.addView(mLlContainer);
LogUtils.d(TAG, "=== initLinearLayout 完成 ===");
@@ -88,137 +94,231 @@ public class BackgroundView extends RelativeLayout {
private void initImageView() {
LogUtils.d(TAG, "=== initImageView 启动 ===");
mIvBackground = new ImageView(mContext);
// 配置ImageViewwrap_content+居中+透明背景
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
mIvBackground.setLayoutParams(ivParams);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺
mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIvBackground.setBackgroundColor(0x00000000);
mLlContainer.addView(mIvBackground);
LogUtils.d(TAG, "=== initImageView 完成 ===");
}
public void loadBackgroundBean(BackgroundBean bean) {
if(!bean.isUseBackgroundFile()) {
setDefaultTransparentBackground();
return;
}
if(bean.isUseBackgroundScaledCompressFile()) {
loadImage(bean.getBackgroundScaledCompressFilePath());
} else {
loadImage(bean.getBackgroundFilePath());
}
}
// ====================================== 对外方法 ======================================
public void loadByBackgroundBean(BackgroundBean bean) {
loadByBackgroundBean(bean, false);
}
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
LogUtils.d(TAG, "=== loadByBackgroundBean 启动 [isRefresh:" + isRefresh + "] ===");
// 参数校验
if (bean == null) {
LogUtils.e(TAG, "loadByBackgroundBean: BackgroundBean为空");
setDefaultTransparentBackground();
return;
}
// 判断是否使用背景文件
if (!bean.isUseBackgroundFile()) {
LogUtils.d(TAG, "loadByBackgroundBean: 不使用背景文件,设置透明背景");
setDefaultTransparentBackground();
return;
}
// 获取目标路径
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
LogUtils.d(TAG, "loadByBackgroundBean: 目标路径=" + targetPath);
// 校验文件是否存在
File targetFile = new File(targetPath);
if (!targetFile.exists() || !targetFile.isFile()) {
LogUtils.e(TAG, "loadByBackgroundBean: 视图控件图片不存在:" + targetPath);
return;
}
// 刷新逻辑:重新解码原始品质图片并更新缓存
if (isRefresh) {
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存");
Bitmap newBitmap = decodeOriginalBitmap(targetFile);
if (newBitmap != null) {
App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap);
App.sBitmapCacheUtils.increaseRefCount(targetPath);
LogUtils.d(TAG, "loadByBackgroundBean: 刷新缓存成功,路径=" + targetPath);
} else {
LogUtils.e(TAG, "loadByBackgroundBean: 刷新解码失败,路径=" + targetPath);
}
}
// 加载图片
loadImage(targetPath);
LogUtils.d(TAG, "=== loadByBackgroundBean 完成 ===");
}
/**
* 加载图片保持原图比例在LinearLayout中居中平铺
* 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理,保留图片原始品质
* @param imagePath 图片绝对路径
*/
public void loadImage(String imagePath) {
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
// 1. 路径空校验
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "loadImage: 图片路径为空");
setDefaultTransparentBackground();
return;
}
// 2. 文件有效性校验
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile()) {
LogUtils.e(TAG, "图片文件无效");
LogUtils.e(TAG, "loadImage: 图片文件无效");
setDefaultTransparentBackground();
return;
}
// 3. 隐藏ImageView防止闪烁
mIvBackground.setVisibility(View.GONE);
// 计算原图比例
// ======================== 路径判断逻辑(强制缓存版) ========================
// 3.1 路径未变化:校验缓存有效性
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(cachedBitmap)) {
LogUtils.d(TAG, "loadImage: 路径未变使用有效缓存Bitmap原始品质");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
LogUtils.d(TAG, "=== loadImage 完成(缓存命中) ===");
return;
} else {
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效尝试重加载原始品质图片");
}
}
// 3.2 路径已更新:保留旧缓存,仅更新路径记录
if (!TextUtils.isEmpty(mCurrentCachedPath) && !mCurrentCachedPath.equals(imagePath)) {
LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,原路径=" + mCurrentCachedPath + ",新路径=" + imagePath);
}
// ======================== 路径判断逻辑结束 ========================
// 4. 计算图片宽高比
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
// 压缩加载Bitmap
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) {
setDefaultTransparentBackground();
return;
// 5. 获取或解码Bitmap
Bitmap bitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(bitmap)) {
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap原始品质");
} else {
LogUtils.d(TAG, "loadImage: 缓存未命中,解码原始品质图片");
bitmap = decodeOriginalBitmap(imageFile);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败(原始品质)");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
}
// 缓存新图片
App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap);
LogUtils.d(TAG, "loadImage: 新图片缓存成功,路径=" + imagePath);
}
// 设置图片
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
adjustImageViewSize(); // 调整尺寸
// 6. 引用计数管理
App.sBitmapCacheUtils.increaseRefCount(imagePath);
// 7. 更新当前缓存路径
mCurrentCachedPath = imagePath;
// 8. 设置图片并调整尺寸
mIvBackground.setImageBitmap(bitmap);
adjustImageViewSize();
LogUtils.d(TAG, "=== loadImage 完成 ===");
}
// ====================================== 内部工具方法 ======================================
/**
* 工具方法判断Bitmap是否有效非空且未被回收
*/
private boolean isBitmapValid(Bitmap bitmap) {
boolean valid = bitmap != null && !bitmap.isRecycled();
if (!valid) {
LogUtils.w(TAG, "isBitmapValid: Bitmap无效空或已回收");
}
return valid;
}
/**
* 计算图片宽高比
*/
private boolean calculateImageAspectRatio(File file) {
LogUtils.d(TAG, "=== calculateImageAspectRatio 启动,文件=" + file.getAbsolutePath() + " ===");
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 尺寸校验
int width = options.outWidth;
int height = options.outHeight;
if (width <= 0 || height <= 0) {
LogUtils.e(TAG, "图片尺寸无效");
LogUtils.e(TAG, "calculateImageAspectRatio: 图片尺寸无效,宽=" + width + ",高=" + height);
return false;
}
// 计算比例
mImageAspectRatio = (float) width / height;
LogUtils.d(TAG, "原图比例" + mImageAspectRatio);
LogUtils.d(TAG, "calculateImageAspectRatio: 原图比例=" + mImageAspectRatio);
LogUtils.d(TAG, "=== calculateImageAspectRatio 完成 ===");
return true;
} catch (Exception e) {
LogUtils.e(TAG, "计算比例失败:" + e.getMessage());
LogUtils.e(TAG, "calculateImageAspectRatio: 计算比例失败:" + e.getMessage());
return false;
}
}
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) {
/**
* 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失)
*/
private Bitmap decodeOriginalBitmap(File file) {
LogUtils.d(TAG, "=== decodeOriginalBitmap 启动,文件=" + file.getAbsolutePath() + " ===");
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int scaleX = options.outWidth / maxWidth;
int scaleY = options.outHeight / maxHeight;
int inSampleSize = Math.max(scaleX, scaleY);
if (inSampleSize <= 0) inSampleSize = 1;
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 核心配置:原始品质
options.inSampleSize = BITMAP_SAMPLE_SIZE;
options.inPreferredConfig = BITMAP_CONFIG;
options.inPurgeable = false;
options.inInputShareable = false;
options.inDither = true;
options.inScaled = false;
// 解码图片
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bitmap != null) {
LogUtils.d(TAG, "decodeOriginalBitmap: 解码成功,宽=" + bitmap.getWidth() + ",高=" + bitmap.getHeight());
} else {
LogUtils.e(TAG, "decodeOriginalBitmap: 解码返回null");
}
LogUtils.d(TAG, "=== decodeOriginalBitmap 完成 ===");
return bitmap;
} catch (Exception e) {
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
LogUtils.e(TAG, "decodeOriginalBitmap: 原始品质解码失败:" + e.getMessage());
return null;
}
}
/**
* 调整ImageView尺寸保持原图比例在LinearLayout中居中平铺
* 调整ImageView尺寸保持原图比例
*/
private void adjustImageViewSize() {
LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
// 空指针校验
if (mLlContainer == null || mIvBackground == null) {
LogUtils.e(TAG, "控件为空");
LogUtils.e(TAG, "adjustImageViewSize: 容器或ImageView未初始化");
return;
}
// 获取LinearLayout尺寸
// 获取容器尺寸
int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight();
if (llWidth == 0 || llHeight == 0) {
postDelayed(new Runnable() {
LogUtils.w(TAG, "adjustImageViewSize: 容器尺寸未初始化,延迟调整");
post(new Runnable() {
@Override
public void run() {
adjustImageViewSize();
}
}, 10);
});
return;
}
// 计算ImageView尺寸保持比例不超出LinearLayout
// 计算ImageView尺寸
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
@@ -227,28 +327,80 @@ public class BackgroundView extends RelativeLayout {
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);
mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIvBackground.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "adjustImageViewSize: 尺寸调整完成,宽=" + ivWidth + ",高=" + ivHeight);
LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
}
/**
* 设置默认透明背景,仅减少引用计数,不删除缓存
*/
private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null);
LogUtils.d(TAG, "=== setDefaultTransparentBackground 启动 ===");
// 清空ImageView
mIvBackground.setImageDrawable(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f;
// 减少引用计数,不删除缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
LogUtils.d(TAG, "setDefaultTransparentBackground: 减少引用计数,路径=" + mCurrentCachedPath);
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
LogUtils.d(TAG, "=== setDefaultTransparentBackground 完成 ===");
}
// ====================================== 重写方法 ======================================
// ====================================== 重写生命周期方法 ======================================
/**
* 重写绘制前强制校验Bitmap有效性防止已回收Bitmap崩溃
*/
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = mIvBackground.getDrawable();
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
Bitmap bitmap = bitmapDrawable.getBitmap();
if (!isBitmapValid(bitmap)) {
LogUtils.e(TAG, "onDraw: 检测到已回收Bitmap清空本地绘制保留全局缓存");
mIvBackground.setImageDrawable(null);
return;
}
}
super.onDraw(canvas);
}
/**
* 重写View从窗口移除时仅减少引用计数不删除全局缓存强制保持策略
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
LogUtils.d(TAG, "=== onDetachedFromWindow 启动 ===");
// 清空ImageView的Drawable释放本地引用
mIvBackground.setImageDrawable(null);
// 减少引用计数,不删除全局缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,路径=" + mCurrentCachedPath);
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
LogUtils.d(TAG, "=== onDetachedFromWindow 完成 ===");
}
/**
* 重写恢复尺寸调整逻辑确保View尺寸变化时正确显示无压缩
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
adjustImageViewSize(); // 尺寸变化时重新调整
LogUtils.d(TAG, "onSizeChanged: 尺寸变化,宽=" + w + ",高=" + h + "调整ImageView尺寸");
adjustImageViewSize();
}
}

View File

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

View File

@@ -0,0 +1,839 @@
package cc.winboll.studio.powerbell.views;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/**
* 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* 适配Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 13:14
*/
public class MainContentView {
// ====================================== 静态常量区(唯一标识,变更类型分类) ======================================
public static final String TAG = "MainContentView";
// 变更类型常量(区分不同控件,精准处理逻辑)
private static final int CHANGE_TYPE_CHARGE_SWITCH = 1;
private static final int CHANGE_TYPE_USAGE_SWITCH = 2;
private static final int CHANGE_TYPE_SERVICE_SWITCH = 3;
private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4;
private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5;
// 电量范围常量
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// ====================================== 内部静态类(临时数据载体,避免外部依赖) ======================================
/**
* 临时配置数据实体(缓存变更信息,取消时恢复)
*/
private static class TempConfigData {
int changeType;
boolean originalBooleanValue;
int originalIntValue;
boolean newBooleanValue;
int newIntValue;
// 构造方法(开关类型)
TempConfigData(int changeType, boolean originalValue, boolean newValue) {
this.changeType = changeType;
this.originalBooleanValue = originalValue;
this.newBooleanValue = newValue;
}
// 构造方法(进度条类型)
TempConfigData(int changeType, int originalValue, int newValue) {
this.changeType = changeType;
this.originalIntValue = originalValue;
this.newIntValue = newValue;
}
}
// ====================================== 事件回调接口(解耦视图与业务,提升扩展性) ======================================
public interface OnViewActionListener {
void onChargeReminderSwitchChanged(boolean isChecked);
void onUsageReminderSwitchChanged(boolean isChecked);
void onServiceSwitchChanged(boolean isChecked);
void onChargeReminderProgressChanged(int progress);
void onUsageReminderProgressChanged(int progress);
}
// ====================================== 成员变量区按功能分类final优先避免混乱 ======================================
// 外部依赖实例(生命周期关联,优先声明)
private Context mContext;
private AppConfigUtils mAppConfigUtils;
private OnViewActionListener mActionListener;
// 视图控件按「布局→开关→文本→进度条→图标」功能归类public控件标注用途
// 基础布局控件
public RelativeLayout mainLayout;
public MemoryCachedBackgroundView backgroundView;
private LinearLayout mllBackgroundView;
// 容器布局控件
public LinearLayout llLeftSeekBar;
public LinearLayout llRightSeekBar;
// 开关控件
public CheckBox cbEnableChargeReminder;
public CheckBox cbEnableUsageReminder;
public Switch swEnableService;
// 文本显示控件
public TextView tvTips;
public TextView tvChargeReminderValue;
public TextView tvUsageReminderValue;
public TextView tvCurrentBatteryValue;
// 进度条控件(使用自定义 VerticalSeekBar
public VerticalSeekBar sbChargeReminder;
public VerticalSeekBar sbUsageReminder;
// 图标显示控件
public ImageView ivCurrentBattery;
public ImageView ivChargeReminderBattery;
public ImageView ivUsageReminderBattery;
// 进度缓存(用于实时计算比值,避免频繁调用 getProgress()
private int mCurrentChargeProgress;
private int mCurrentUsageProgress;
// 内部复用资源(避免重复创建,优化性能)
private BatteryDrawable mCurrentBatteryDrawable;
private BatteryDrawable mChargeReminderBatteryDrawable;
private BatteryDrawable mUsageReminderBatteryDrawable;
// 配置变更确认对话框(单例复用,避免重复创建)
private AlertDialog mConfigConfirmDialog;
private AlertDialog.Builder mDialogBuilder;
// 临时存储变更数据(对话框确认前缓存,取消时恢复)
private TempConfigData mTempConfigData;
// 对话框状态锁(避免快速点击重复弹窗)
private boolean isDialogShowing = false;
// ====================================== 构造方法(初始化入口,逻辑闭环) ======================================
public MainContentView(Context context, View rootView, OnViewActionListener actionListener) {
LogUtils.d(TAG, "【MainContentView】构造器调用 | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener);
// 初始化外部依赖
this.mContext = context;
this.mActionListener = actionListener;
this.mAppConfigUtils = AppConfigUtils.getInstance(context.getApplicationContext());
// 执行核心初始化流程(按顺序执行,避免依赖空指针)
bindViews(rootView);
initBatteryDrawables();
initConfirmDialog();
bindViewListeners();
LogUtils.d(TAG, "【MainContentView】初始化完成");
}
// ====================================== 私有初始化方法(封装内部逻辑,仅暴露入口) ======================================
/**
* 绑定视图控件(显式强转适配 Java7适配 API30 视图加载机制)
* @param rootView 根视图
*/
private void bindViews(View rootView) {
LogUtils.d(TAG, "【bindViews】视图绑定开始 | rootView=" + rootView);
// 基础布局绑定
mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1);
mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview);
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
if (backgroundView.getParent() != null) {
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
LogUtils.d(TAG, "【bindViews】移除背景视图旧父容器");
}
mllBackgroundView.addView(backgroundView);
// 容器布局绑定
llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1);
llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2);
// 开关控件绑定
cbEnableChargeReminder = (CheckBox) rootView.findViewById(R.id.fragmentmainviewCheckBox1);
cbEnableUsageReminder = (CheckBox) rootView.findViewById(R.id.fragmentmainviewCheckBox2);
swEnableService = (Switch) rootView.findViewById(R.id.fragmentandroidviewSwitch1);
// 文本控件绑定
tvTips = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView1);
tvChargeReminderValue = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView2);
tvUsageReminderValue = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView3);
tvCurrentBatteryValue = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView4);
// 进度条控件绑定(自定义 VerticalSeekBar
sbChargeReminder = (VerticalSeekBar) rootView.findViewById(R.id.fragmentandroidviewVerticalSeekBar1);
sbUsageReminder = (VerticalSeekBar) rootView.findViewById(R.id.fragmentandroidviewVerticalSeekBar2);
// 图标控件绑定
ivCurrentBattery = (ImageView) rootView.findViewById(R.id.fragmentandroidviewImageView1);
ivChargeReminderBattery = (ImageView) rootView.findViewById(R.id.fragmentandroidviewImageView3);
ivUsageReminderBattery = (ImageView) rootView.findViewById(R.id.fragmentandroidviewImageView2);
// 初始化进度缓存(从配置读取初始值)
mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue();
mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue();
LogUtils.d(TAG, "【bindViews】进度缓存初始化 | charge=" + mCurrentChargeProgress + " | usage=" + mCurrentUsageProgress);
// 关键视图绑定校验(仅保留核心控件错误日志,精简冗余)
if (mainLayout == null) LogUtils.e(TAG, "【bindViews】mainLayout 绑定失败");
if (backgroundView == null) LogUtils.e(TAG, "【bindViews】backgroundView 绑定失败");
LogUtils.d(TAG, "【bindViews】视图绑定完成");
}
/**
* 初始化电池 Drawable集成 BatteryDrawable默认能量风格适配小米机型渲染
*/
private void initBatteryDrawables() {
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始");
// 当前电量 Drawable颜色从资源读取适配 API30 主题)
int colorCurrent = getResourceColor(R.color.colorCurrent);
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
// 充电提醒 Drawable
int colorCharge = getResourceColor(R.color.colorCharge);
mChargeReminderBatteryDrawable = new BatteryDrawable(colorCharge);
// 耗电提醒 Drawable
int colorUsage = getResourceColor(R.color.colorUsege);
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
}
/**
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
*/
private void initConfirmDialog() {
LogUtils.d(TAG, "【initConfirmDialog】对话框初始化开始");
if (mContext == null) {
LogUtils.e(TAG, "【initConfirmDialog】Context 为空,初始化失败");
return;
}
// 1. 初始化 Builder核心后续通过 Builder 更新消息)
mDialogBuilder = new AlertDialog.Builder(mContext);
mDialogBuilder.setTitle("配置变更确认");
mDialogBuilder.setMessage("是否确认修改当前配置?");
// 确定按钮:保存配置+回调+更新视图
mDialogBuilder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
confirmConfigChange();
dialog.dismiss();
}
});
// 取消按钮:恢复原始配置(补充物理取消按钮,提升用户体验)
mDialogBuilder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
cancelConfigChange();
dialog.dismiss();
}
});
// 对话框外部点击监听:关闭对话框+恢复原始配置
mDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
cancelConfigChange();
dialog.dismiss();
}
});
// 2. 初始化对话框实例(设置可取消,支持外部点击关闭)
mConfigConfirmDialog = mDialogBuilder.create();
mConfigConfirmDialog.setCancelable(true);
mConfigConfirmDialog.setCanceledOnTouchOutside(true);
LogUtils.d(TAG, "【initConfirmDialog】对话框初始化完成");
}
/**
* 绑定视图事件监听Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效)
*/
private void bindViewListeners() {
LogUtils.d(TAG, "【bindViewListeners】事件监听绑定开始");
// 依赖校验,避免空指针
if (mAppConfigUtils == null || mActionListener == null || mDialogBuilder == null) {
LogUtils.e(TAG, "【bindViewListeners】依赖实例为空跳过监听绑定");
return;
}
// 充电提醒进度条监听(使用 VerticalSeekBar 专属接口确保弹窗100%触发)
if (sbChargeReminder != null) {
// 原有:触摸抬起/取消监听(用于配置确认)
sbChargeReminder.setOnVerticalSeekBarTouchListener(new VerticalSeekBar.OnVerticalSeekBarTouchListener() {
@Override
public void onTouchUp(VerticalSeekBar seekBar, int progress) {
int originalValue = mAppConfigUtils.getChargeReminderValue();
// 进度无变化,不处理
if (originalValue == progress) {
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar: 进度无变化,跳过");
return;
}
// 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
}
@Override
public void onTouchCancel(VerticalSeekBar seekBar, int progress) {
// 触摸取消回滚视图进度UI 与配置保持一致)
int originalValue = mAppConfigUtils.getChargeReminderValue();
if (tvChargeReminderValue != null && mChargeReminderBatteryDrawable != null && ivChargeReminderBattery != null) {
mChargeReminderBatteryDrawable.setBatteryValue(originalValue);
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(originalValue + "%");
}
seekBar.setProgress(originalValue);
// 恢复进度缓存
mCurrentChargeProgress = originalValue;
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
}
});
// 新增:实时进度变化监听(用于比值预览)
sbChargeReminder.setOnVerticalSeekBarChangeListener(new VerticalSeekBar.OnVerticalSeekBarChangeListener() {
@Override
public void onProgressChanged(VerticalSeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
mCurrentChargeProgress = progress;
// 同步更新进度文本和电池图标保持UI一致性
if (tvChargeReminderValue != null && mChargeReminderBatteryDrawable != null && ivChargeReminderBattery != null) {
mChargeReminderBatteryDrawable.setBatteryValue(progress);
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(progress + "%");
}
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar实时更新 | 进度=" + progress);
}
}
@Override
public void onStartTrackingTouch(VerticalSeekBar seekBar) {}
@Override
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
});
LogUtils.d(TAG, "【bindViewListeners】充电提醒进度条专属监听绑定完成");
}
// 充电提醒开关监听
if (cbEnableChargeReminder != null) {
cbEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean originalValue = mAppConfigUtils.isChargeReminderEnabled();
boolean newValue = cbEnableChargeReminder.isChecked();
// 状态无变化,不处理
if (originalValue == newValue) return;
// 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "【bindViewListeners】cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
}
});
LogUtils.d(TAG, "【bindViewListeners】充电提醒开关监听绑定完成");
}
// 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口确保弹窗100%触发)
if (sbUsageReminder != null) {
// 原有:触摸抬起/取消监听(用于配置确认)
sbUsageReminder.setOnVerticalSeekBarTouchListener(new VerticalSeekBar.OnVerticalSeekBarTouchListener() {
@Override
public void onTouchUp(VerticalSeekBar seekBar, int progress) {
int originalValue = mAppConfigUtils.getUsageReminderValue();
// 进度无变化,不处理
if (originalValue == progress) {
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar: 进度无变化,跳过");
return;
}
// 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
}
@Override
public void onTouchCancel(VerticalSeekBar seekBar, int progress) {
// 触摸取消回滚视图进度UI 与配置保持一致)
int originalValue = mAppConfigUtils.getUsageReminderValue();
if (tvUsageReminderValue != null && mUsageReminderBatteryDrawable != null && ivUsageReminderBattery != null) {
mUsageReminderBatteryDrawable.setBatteryValue(originalValue);
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(originalValue + "%");
}
seekBar.setProgress(originalValue);
// 恢复进度缓存
mCurrentUsageProgress = originalValue;
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
}
});
// 新增:实时进度变化监听(用于比值预览)
sbUsageReminder.setOnVerticalSeekBarChangeListener(new VerticalSeekBar.OnVerticalSeekBarChangeListener() {
@Override
public void onProgressChanged(VerticalSeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
mCurrentUsageProgress = progress;
// 同步更新进度文本和电池图标保持UI一致性
if (tvUsageReminderValue != null && mUsageReminderBatteryDrawable != null && ivUsageReminderBattery != null) {
mUsageReminderBatteryDrawable.setBatteryValue(progress);
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(progress + "%");
}
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar实时更新 | 进度=" + progress);
}
}
@Override
public void onStartTrackingTouch(VerticalSeekBar seekBar) {}
@Override
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
});
LogUtils.d(TAG, "【bindViewListeners】耗电提醒进度条专属监听绑定完成");
}
// 耗电提醒开关监听
if (cbEnableUsageReminder != null) {
cbEnableUsageReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean originalValue = mAppConfigUtils.isUsageReminderEnabled();
boolean newValue = cbEnableUsageReminder.isChecked();
// 状态无变化,不处理
if (originalValue == newValue) return;
// 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "【bindViewListeners】cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
}
});
LogUtils.d(TAG, "【bindViewListeners】耗电提醒开关监听绑定完成");
}
// 服务总开关监听(核心优化:逻辑与其他控件完全对齐)
if (swEnableService != null) {
swEnableService.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从服务控制Bean读取原始状态确保与实际一致
boolean originalValue = getServiceEnableState();
boolean newValue = ((Switch) v).isChecked();
// 状态无变化,不处理
if (originalValue == newValue) return;
// 缓存变更数据
mTempConfigData = new TempConfigData(CHANGE_TYPE_SERVICE_SWITCH, originalValue, newValue);
// 更新差异化提示语
updateDialogMessageByChangeType();
// 显示确认对话框
showConfigConfirmDialog();
LogUtils.d(TAG, "【bindViewListeners】swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
}
});
LogUtils.d(TAG, "【bindViewListeners】服务总开关监听绑定完成");
}
LogUtils.d(TAG, "【bindViewListeners】所有事件监听绑定完成");
}
// ====================================== 对外暴露核心方法(业务入口,精简参数,明确职责) ======================================
/**
* 更新所有视图数据(从配置读取数据,统一刷新 UI适配 API30 视图更新规范)
* @param frameDrawable 进度条背景 Drawable外部传入适配主题切换
*/
public void updateViewData(Drawable frameDrawable) {
LogUtils.d(TAG, "【updateViewData】视图数据更新开始 | frameDrawable=" + frameDrawable);
if (mAppConfigUtils == null) {
LogUtils.e(TAG, "【updateViewData】AppConfigUtils 为空,跳过更新");
return;
}
// 一次读取所有配置参数,减少工具类调用,提升性能
int chargeVal = mAppConfigUtils.getChargeReminderValue();
int usageVal = mAppConfigUtils.getUsageReminderValue();
int currentVal = mAppConfigUtils.getCurrentBatteryValue();
boolean chargeEnable = mAppConfigUtils.isChargeReminderEnabled();
boolean usageEnable = mAppConfigUtils.isUsageReminderEnabled();
// 从服务控制Bean读取状态确保UI与实际一致
boolean serviceEnable = getServiceEnableState();
// 更新进度缓存
mCurrentChargeProgress = chargeVal;
mCurrentUsageProgress = usageVal;
LogUtils.d(TAG, "【updateViewData】配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable);
// 进度条背景更新
if (frameDrawable != null) {
if (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable);
if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable);
LogUtils.d(TAG, "【updateViewData】进度条背景更新完成");
}
// 当前电量更新(联动 BatteryDrawable实时刷新图标
if (ivCurrentBattery != null && mCurrentBatteryDrawable != null) {
mCurrentBatteryDrawable.setBatteryValue(currentVal);
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
}
if (tvCurrentBatteryValue != null) {
tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent));
tvCurrentBatteryValue.setText(currentVal + "%");
}
LogUtils.d(TAG, "【updateViewData】当前电量更新完成");
// 充电提醒视图更新
if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) {
mChargeReminderBatteryDrawable.setBatteryValue(chargeVal);
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
}
if (tvChargeReminderValue != null) {
tvChargeReminderValue.setTextColor(getResourceColor(R.color.colorCharge));
tvChargeReminderValue.setText(chargeVal + "%");
}
if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal);
if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable);
LogUtils.d(TAG, "【updateViewData】充电提醒视图更新完成");
// 耗电提醒视图更新
if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) {
mUsageReminderBatteryDrawable.setBatteryValue(usageVal);
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
}
if (tvUsageReminderValue != null) {
tvUsageReminderValue.setTextColor(getResourceColor(R.color.colorUsege));
tvUsageReminderValue.setText(usageVal + "%");
}
if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal);
if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable);
LogUtils.d(TAG, "【updateViewData】耗电提醒视图更新完成");
// 服务开关+提示文本更新(确保状态准确)
if (swEnableService != null) {
swEnableService.setChecked(serviceEnable);
swEnableService.setText(mContext.getString(R.string.txt_aboveswitch));
}
if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips));
LogUtils.d(TAG, "【updateViewData】服务开关与提示文本更新完成");
LogUtils.d(TAG, "【updateViewData】所有视图数据更新完成");
}
/**
* 实时更新当前电量(单独抽离,适配电池实时监控场景,优化 API30 UI 响应速度)
* @param value 电量值(自动校准 0-100避免异常值
*/
public void updateCurrentBattery(int value) {
LogUtils.d(TAG, "【updateCurrentBattery】当前电量更新开始 | 原始值=" + value);
// 核心依赖校验
if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) {
LogUtils.e(TAG, "【updateCurrentBattery】视图/Drawable 为空,跳过更新");
return;
}
// 校准电量范围(强制 0-100防止 API30 视图显示异常)
int validValue = Math.max(BATTERY_MIN, Math.min(value, BATTERY_MAX));
// 联动 BatteryDrawable 更新图标,同步文本显示
mCurrentBatteryDrawable.setBatteryValue(validValue);
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
tvCurrentBatteryValue.setText(validValue + "%");
LogUtils.d(TAG, "【updateCurrentBattery】更新完成 | 校准后值=" + validValue);
}
/**
* 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用)
*/
public void releaseResources() {
LogUtils.d(TAG, "【releaseResources】资源释放开始");
// 释放对话框资源(安全销毁,避免内存泄漏)
if (mConfigConfirmDialog != null) {
if (mConfigConfirmDialog.isShowing()) {
mConfigConfirmDialog.dismiss();
}
mConfigConfirmDialog.setOnDismissListener(null);
mConfigConfirmDialog.setOnCancelListener(null);
mConfigConfirmDialog = null;
}
// 释放 Builder
mDialogBuilder = null;
// 释放临时数据
mTempConfigData = null;
// 释放 BatteryDrawable 资源(重点回收绘制资源,避免 OOM
mCurrentBatteryDrawable = null;
mChargeReminderBatteryDrawable = null;
mUsageReminderBatteryDrawable = null;
// 置空视图实例(断开视图引用,辅助 GC 回收)
mainLayout = null;
backgroundView = null;
mllBackgroundView = null;
llLeftSeekBar = null;
llRightSeekBar = null;
cbEnableChargeReminder = null;
cbEnableUsageReminder = null;
swEnableService = null;
tvTips = null;
tvChargeReminderValue = null;
tvUsageReminderValue = null;
tvCurrentBatteryValue = null;
sbChargeReminder = null;
sbUsageReminder = null;
ivCurrentBattery = null;
ivChargeReminderBattery = null;
ivUsageReminderBattery = null;
// 置空外部依赖(断开生命周期关联,杜绝内存泄漏)
mContext = null;
mAppConfigUtils = null;
mActionListener = null;
LogUtils.d(TAG, "【releaseResources】所有资源释放完成");
}
/**
* 设置服务开关启用状态(外部调用,同步 UI 与服务状态,适配 Activity 视图刷新)
* @param enabled 服务启用状态
*/
public void setServiceSwitchChecked(boolean enabled) {
LogUtils.d(TAG, "【setServiceSwitchChecked】服务开关状态设置 | enabled=" + enabled);
if (swEnableService != null) {
swEnableService.setChecked(enabled);
}
}
/**
* 设置服务开关点击状态(外部调用,避免更新 UI 时触发重复回调)
* @param enabled 是否允许点击
*/
public void setServiceSwitchEnabled(boolean enabled) {
LogUtils.d(TAG, "【setServiceSwitchEnabled】服务开关点击状态设置 | enabled=" + enabled);
if (swEnableService != null) {
swEnableService.setEnabled(enabled);
}
}
// ====================================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑) ======================================
/**
* 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗)
*/
private void showConfigConfirmDialog() {
LogUtils.d(TAG, "【showConfigConfirmDialog】对话框显示开始 | isDialogShowing=" + isDialogShowing);
// 对话框状态锁:正在显示则跳过,避免重复触发
if (isDialogShowing) {
LogUtils.d(TAG, "【showConfigConfirmDialog】对话框已显示跳过重复调用");
return;
}
// 基础校验:对话框/上下文/Builder 为空
if (mDialogBuilder == null || mContext == null) {
LogUtils.e(TAG, "【showConfigConfirmDialog】对话框Builder/上下文异常,无法显示");
if (mTempConfigData != null) cancelConfigChange();
return;
}
// Activity 状态校验:避免销毁后弹窗崩溃(适配 API30
Activity activity = (Activity) mContext;
if (activity.isFinishing() || activity.isDestroyed()) {
LogUtils.e(TAG, "【showConfigConfirmDialog】Activity 已销毁,无法显示对话框");
if (mTempConfigData != null) cancelConfigChange();
return;
}
// 重新构建对话框(核心:确保最新消息生效)
mConfigConfirmDialog = mDialogBuilder.create();
// 显示对话框,设置状态锁+关闭监听
mConfigConfirmDialog.show();
isDialogShowing = true;
// 对话框关闭时解锁
mConfigConfirmDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
isDialogShowing = false;
mConfigConfirmDialog.setOnDismissListener(null);
}
});
LogUtils.d(TAG, "【showConfigConfirmDialog】确认对话框显示成功");
}
/**
* 确认配置变更(保存数据+回调监听+更新视图)
*/
private void confirmConfigChange() {
LogUtils.d(TAG, "【confirmConfigChange】配置确认开始 | mTempConfigData=" + mTempConfigData);
if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) {
LogUtils.e(TAG, "【confirmConfigChange】依赖数据为空确认失败");
return;
}
switch (mTempConfigData.changeType) {
// 充电提醒开关
case CHANGE_TYPE_CHARGE_SWITCH:
mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue);
mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "【confirmConfigChange】充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
break;
// 耗电提醒开关
case CHANGE_TYPE_USAGE_SWITCH:
mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue);
mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "【confirmConfigChange】耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
break;
// 服务总开关(核心:持久化配置+触发 Activity 回调)
case CHANGE_TYPE_SERVICE_SWITCH:
// 1. 设置服务启停
if (mTempConfigData.newBooleanValue) {
ControlCenterService.startControlCenterService(mContext);
} else {
ControlCenterService.stopControlCenterService(mContext);
}
// 2. 强制触发 Activity 回调,执行服务启停逻辑
mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "【confirmConfigChange】服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
break;
// 充电提醒进度条
case CHANGE_TYPE_CHARGE_SEEKBAR:
mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue);
mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue);
LogUtils.d(TAG, "【confirmConfigChange】充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
break;
// 耗电提醒进度条
case CHANGE_TYPE_USAGE_SEEKBAR:
mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue);
mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue);
LogUtils.d(TAG, "【confirmConfigChange】耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
break;
default:
LogUtils.w(TAG, "【confirmConfigChange】未知变更类型跳过");
break;
}
// 确认完成,清空临时数据
mTempConfigData = null;
LogUtils.d(TAG, "【confirmConfigChange】配置确认完成");
}
/**
* 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致)
*/
private void cancelConfigChange() {
LogUtils.d(TAG, "【cancelConfigChange】配置取消开始 | mTempConfigData=" + mTempConfigData);
if (mTempConfigData == null || mAppConfigUtils == null) {
LogUtils.e(TAG, "【cancelConfigChange】依赖数据为空取消失败");
return;
}
switch (mTempConfigData.changeType) {
case CHANGE_TYPE_CHARGE_SWITCH:
if (cbEnableChargeReminder != null) {
cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue);
}
LogUtils.d(TAG, "【cancelConfigChange】充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break;
case CHANGE_TYPE_USAGE_SWITCH:
if (cbEnableUsageReminder != null) {
cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue);
}
LogUtils.d(TAG, "【cancelConfigChange】耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break;
case CHANGE_TYPE_SERVICE_SWITCH:
if (swEnableService != null) {
swEnableService.setChecked(mTempConfigData.originalBooleanValue);
}
LogUtils.d(TAG, "【cancelConfigChange】服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break;
case CHANGE_TYPE_CHARGE_SEEKBAR:
if (sbChargeReminder != null) {
sbChargeReminder.setProgress(mTempConfigData.originalIntValue);
}
if (tvChargeReminderValue != null && mChargeReminderBatteryDrawable != null && ivChargeReminderBattery != null) {
mChargeReminderBatteryDrawable.setBatteryValue(mTempConfigData.originalIntValue);
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%");
}
LogUtils.d(TAG, "【cancelConfigChange】充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
break;
case CHANGE_TYPE_USAGE_SEEKBAR:
if (sbUsageReminder != null) {
sbUsageReminder.setProgress(mTempConfigData.originalIntValue);
}
if (tvUsageReminderValue != null && mUsageReminderBatteryDrawable != null && ivUsageReminderBattery != null) {
mUsageReminderBatteryDrawable.setBatteryValue(mTempConfigData.originalIntValue);
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%");
}
LogUtils.d(TAG, "【cancelConfigChange】耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
break;
default:
LogUtils.w(TAG, "【cancelConfigChange】未知变更类型跳过");
break;
}
// 取消完成,清空临时数据
mTempConfigData = null;
LogUtils.d(TAG, "【cancelConfigChange】配置取消完成");
}
/**
* 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效)
*/
private void updateDialogMessageByChangeType() {
LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新开始 | mTempConfigData=" + mTempConfigData);
if (mDialogBuilder == null || mTempConfigData == null) return;
String message;
if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) {
// 服务开关差异化提示语
message = mTempConfigData.newBooleanValue ?
"启用服务后,将后台持续监控电池状态,是否确认?" :
"禁用服务后,电池监控功能将停止,是否确认?";
} else {
// 普通配置默认提示语
message = "是否确认修改当前配置?";
}
// 通过 Builder 设置消息,确保弹窗显示最新内容
mDialogBuilder.setMessage(message);
LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新完成 | message=" + message);
}
// ====================================== 内部工具方法(封装重复逻辑,提升复用性) ======================================
/**
* 获取资源颜色(适配 API30 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针)
* @param colorResId 颜色资源 ID
* @return 校准后的颜色值
*/
private int getResourceColor(int colorResId) {
LogUtils.d(TAG, "【getResourceColor】资源颜色获取 | colorResId=" + colorResId);
// 空指针防御Context 为空返回默认黑色
if (mContext == null) {
LogUtils.e(TAG, "【getResourceColor】Context 为空,返回默认黑色");
return 0xFF000000;
}
// 适配 API30 主题颜色读取
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return mContext.getResources().getColor(colorResId, mContext.getTheme());
} else {
return mContext.getResources().getColor(colorResId);
}
}
/**
* 获取服务启用状态统一从服务控制Bean读取确保全链路状态一致
* @return 服务启用状态true=启用false=禁用)
*/
private boolean getServiceEnableState() {
LogUtils.d(TAG, "【getServiceEnableState】服务状态获取开始");
ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
// 本地无配置时,默认禁用服务(与服务初始化逻辑对齐)
boolean state = serviceBean != null && serviceBean.isEnableService();
LogUtils.d(TAG, "【getServiceEnableState】服务启用状态获取完成 | state=" + state);
return state;
}
}

View File

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

View File

@@ -5,92 +5,234 @@ import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.SeekBar;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 垂直进度条控件,适配 API30支持逆时针旋转0在下100在上
* 修复滑块同步+弹窗触发bug新增实时进度变化监听接口支持拖动时实时回调进度
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 14:11
*/
public class VerticalSeekBar extends SeekBar {
public static final String TAG = VerticalSeekBar.class.getSimpleName();
// ======================== 静态常量 =========================
private static final String TAG = VerticalSeekBar.class.getSimpleName();
public volatile int _mnProgress = -1;
public VerticalSeekBar(Context context) {
super(context);
// ======================== 接口定义(前置,便于外部调用)========================
/**
* 垂直进度条触摸事件回调接口,解决原生 OnSeekBarChangeListener 回调失效问题
* 直接在触摸抬起时回调确保配置变更对话框100%触发
*/
public interface OnVerticalSeekBarTouchListener {
/**
* 触摸抬起时回调(滑块停止滑动,触发弹窗的核心时机)
* @param seekBar 当前垂直进度条实例
* @param progress 最终滑动进度0~100
*/
void onTouchUp(VerticalSeekBar seekBar, int progress);
/**
* 触摸取消时回调(可选,用于异常场景进度回滚)
* @param seekBar 当前垂直进度条实例
* @param progress 取消时的进度
*/
void onTouchCancel(VerticalSeekBar seekBar, int progress);
}
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
/**
* 垂直进度条实时进度变化监听接口
* 支持拖动过程中实时回调进度用于比值预览等实时UI更新场景
*/
public interface OnVerticalSeekBarChangeListener {
/**
* 进度变化时回调
* @param seekBar 当前垂直进度条实例
* @param progress 当前进度0~100
* @param fromUser 是否是用户触摸导致的进度变化
*/
void onProgressChanged(VerticalSeekBar seekBar, int progress, boolean fromUser);
/**
* 开始触摸进度条时回调
* @param seekBar 当前垂直进度条实例
*/
void onStartTrackingTouch(VerticalSeekBar seekBar);
/**
* 停止触摸进度条时回调
* @param seekBar 当前垂直进度条实例
*/
void onStopTrackingTouch(VerticalSeekBar seekBar);
}
// ======================== 成员变量 =========================
// 核心状态当前进度缓存修复滑块同步问题volatile 保证多线程可见性)
private volatile int mProgress = -1;
// 监听接口:触摸事件回调(原有,用于弹窗触发)
private OnVerticalSeekBarTouchListener mTouchListener;
// 监听接口:实时进度变化回调(新增,用于比值计算)
private OnVerticalSeekBarChangeListener mProgressChangeListener;
// ======================== 构造方法 =========================
public VerticalSeekBar(Context context) {
super(context);
initView();
LogUtils.d(TAG, "【构造器1】VerticalSeekBar 初始化完成");
}
public VerticalSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
// 去除冗余的水平阴影
initView();
LogUtils.d(TAG, "【构造器2】VerticalSeekBar 初始化完成");
}
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
LogUtils.d(TAG, "【构造器3】VerticalSeekBar 初始化完成");
}
// ======================== 初始化方法 =========================
private void initView() {
// 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销
setBackgroundDrawable(null);
LogUtils.d(TAG, "【initView】移除默认背景阴影完成视图初始化");
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw);
// ======================== 对外设置方法(监听接口绑定)========================
/**
* 设置触摸事件监听器(给外部调用,如 MainContentView 绑定)
* @param listener 触摸事件回调实例
*/
public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener listener) {
this.mTouchListener = listener;
LogUtils.d(TAG, "【setOnVerticalSeekBarTouchListener】触摸监听器绑定完成");
}
/**
* 设置实时进度变化监听器(给外部调用,如 MainContentView 绑定)
* @param listener 实时进度变化回调实例
*/
public void setOnVerticalSeekBarChangeListener(OnVerticalSeekBarChangeListener listener) {
this.mProgressChangeListener = listener;
LogUtils.d(TAG, "【setOnVerticalSeekBarChangeListener】实时进度监听器绑定完成");
}
// ======================== 重写系统方法(测量/布局/绘制)========================
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
LogUtils.v(TAG, "【onMeasure】垂直测量完成宽=" + getMeasuredHeight() + ",高=" + getMeasuredWidth());
}
protected void onDraw(Canvas c) {
// 0--------100,顺时针旋转,小在上
// c.rotate(+90);
// c.translate(0, -getWidth());
// 0--------100,逆时针旋转,小在下
c.rotate(-90);
c.translate(-getHeight(), 0);
super.onDraw(c);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 调用基类的处理函数
// 该方法可以使得
// SeekBar.OnSeekBarChangeListener
// 的 onStopTrackingTouch 和 onStartTrackingTouch 等函数有效。
boolean handled = super.onTouchEvent(event);
if (handled) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// 0--------100,顺时针旋转,小在上
//_mnProgress = (int)(getMax() * event.getY() / getHeight());
// // 0--------100,逆时针旋转,小在下
_mnProgress = getMax() - (int) (getMax() * event.getY() / getHeight());
_mnProgress = _mnProgress > 100 ? 100 : _mnProgress ;
//LogUtils.d(TAG, "_mnProgress is " + Integer.toString(_mnProgress));
setProgress(_mnProgress);
//onSizeChanged(getWidth(), getHeight(), 0, 0);
break;
case MotionEvent.ACTION_CANCEL:
break;
default :
//LogUtils.d(TAG, "event.getAction() is " + event.getAction());
break;
}
}
return handled;
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw);
LogUtils.v(TAG, "【onSizeChanged】尺寸变化新宽=" + h + ",新高=" + w);
}
// 解决调用setProgress方法时滑块不跟随的bug
@Override
protected void onDraw(Canvas canvas) {
// 逆时针旋转90度平移画布避免绘制偏移0在下100在上
canvas.rotate(-90);
canvas.translate(-getHeight(), 0);
super.onDraw(canvas);
LogUtils.v(TAG, "【onDraw】完成垂直绘制旋转角度=-90°");
}
// ======================== 重写进度设置方法(修复滑块同步+新增实时回调)========================
/**
* 重写进度设置,调用尺寸变化方法强制刷新,解决 setProgress 滑块不跟随问题
* 新增:支持外部调用 setProgress 时触发实时进度回调
*/
@Override
public synchronized void setProgress(int progress) {
super.setProgress(progress);
// 强制触发尺寸变化同步刷新滑块位置核心bug修复逻辑
onSizeChanged(getWidth(), getHeight(), 0, 0);
mProgress = progress;
LogUtils.d(TAG, "【setProgress】进度设置为" + progress + ",滑块同步刷新");
// 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false
if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, progress, false);
LogUtils.v(TAG, "【setProgress】触发实时进度回调fromUser=false");
}
}
// ======================== 重写触摸事件(优化事件透传+实时进度回调)========================
@Override
public boolean onTouchEvent(MotionEvent event) {
// 先调用父类方法,保留原生监听器兼容性,同时强制透传事件
super.onTouchEvent(event);
boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失
boolean fromUser = true; // 标记是否是用户触摸导致的进度变化
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
LogUtils.d(TAG, "【onTouchEvent】触摸按下Y坐标=" + event.getY());
// 触发实时进度监听:开始触摸
if (mProgressChangeListener != null) {
mProgressChangeListener.onStartTrackingTouch(this);
LogUtils.v(TAG, "【onTouchEvent】触发开始触摸回调");
}
break;
case MotionEvent.ACTION_MOVE:
calculateProgress(event.getY());
setProgress(mProgress);
LogUtils.v(TAG, "【onTouchEvent】触摸滑动进度更新为" + mProgress);
// 触发实时进度监听:进度变化
if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
}
break;
case MotionEvent.ACTION_UP:
calculateProgress(event.getY());
setProgress(mProgress);
LogUtils.d(TAG, "【onTouchEvent】触摸抬起进度=" + mProgress + ",触发弹窗回调");
// 触发实时进度监听:进度变化+停止触摸
if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
mProgressChangeListener.onStopTrackingTouch(this);
LogUtils.v(TAG, "【onTouchEvent】触发停止触摸回调");
}
// 核心:调用原有触摸接口,通知外部触发配置变更对话框
if (mTouchListener != null) {
mTouchListener.onTouchUp(this, mProgress);
LogUtils.v(TAG, "【onTouchEvent】触发触摸抬起回调");
}
break;
case MotionEvent.ACTION_CANCEL:
int currentProgress = getProgress();
LogUtils.d(TAG, "【onTouchEvent】触摸取消当前进度=" + currentProgress);
// 触发实时进度监听:停止触摸
if (mProgressChangeListener != null) {
mProgressChangeListener.onStopTrackingTouch(this);
}
// 可选:触摸取消时回调,外部可做进度回滚处理
if (mTouchListener != null) {
mTouchListener.onTouchCancel(this, currentProgress);
LogUtils.v(TAG, "【onTouchEvent】触发触摸取消回调");
}
break;
}
return handled;
}
// ======================== 内部工具方法 =========================
/**
* 计算垂直进度,校准范围 0~100避免异常值
* @param touchY 触摸点Y坐标
*/
private void calculateProgress(float touchY) {
// 核心进度计算公式(逆时针旋转适配)
mProgress = getMax() - (int) (getMax() * touchY / getHeight());
// 校准进度范围,防止超出 0~100兼容 API30 进度边界校验)
mProgress = Math.max(0, Math.min(mProgress, getMax()));
LogUtils.v(TAG, "【calculateProgress】触摸Y=" + touchY + ",计算进度=" + mProgress + ",校准后=" + mProgress);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

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,131 @@
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<LinearLayout
android:orientation="vertical"
<RelativeLayout
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_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:layout_alignParentLeft="true"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundsettingsAButton1"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Received BG"
android:layout_alignParentRight="true"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundsettingsAButton2"/>
</RelativeLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:gravity="right">
<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="#FF3243E2"
android:id="@+id/background_view">
<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/activitybackgroundsettingsAButton3"/>
</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/activitybackgroundsettingsAButton4"/>
<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/activitybackgroundsettingsAButton5"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#B92FABE6">
android:layout_height="wrap_content"
android:gravity="right">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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/activitybackgroundsettingsAButton6"/>
<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="50dp"
android:layout_height="36dp"
android:text="[+~]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundsettingsAButton7"/>
<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"/>
<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/activitybackgroundsettingsAButton8"/>
</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:onClick="onColorPaletteDialog"
android:id="@+id/activitybackgroundsettingsAButton9"/>
<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/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>
<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/activitybackgroundsettingsAButton10"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -21,18 +21,225 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitymainRelativeLayout1"/>
android:id="@+id/activitymainRelativeLayout1">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitymainFrameLayout1"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ll_backgroundview">
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"
android:layout_alignParentBottom="true"/>
</LinearLayout>
<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>
<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>

View File

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

View File

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

@@ -13,11 +13,20 @@
android:id="@+id/tv_dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络后台提示"
android:text="网络图片资源下载"
android:textSize="18sp"
android:textColor="@android:color/black"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_dialog_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="请输入网络图片地址:"
android:textSize="15sp"
android:textColor="@android:color/darker_gray"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
@@ -54,15 +63,6 @@
</LinearLayout>
<TextView
android:id="@+id/tv_dialog_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="应用正在后台使用网络,是否继续允许?"
android:textSize="15sp"
android:textColor="@android:color/darker_gray"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -74,7 +74,7 @@
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:text="关闭返回"
android:textSize="14sp"
android:background="@android:drawable/btn_default_small"
android:layout_marginRight="8dp"/>
@@ -83,7 +83,7 @@
android:id="@+id/btn_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="允许"
android:text="使用图片"
android:textSize="14sp"
android:background="@android:drawable/btn_default_small"/>

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,11 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_log"
android:title="@string/item_logview"/>
<item
android:id="@+id/action_unittestactivity"
android:title="@string/item_mainunittestactivity"/>
android:title="MainUnitTestActivity"/>
<item
android:id="@+id/action_unittest2activity"
android:title="MainUnitTest2Activity"/>
</menu>

View File

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

View File

@@ -58,9 +58,73 @@
<color name="colorUsege">@color/colorRed</color>
<color name="colorCurrent">@color/colorBlue</color>
<color name="colorCharge">@color/colorYellow</color>
<!--CustomSlideToUnlockView控件配置-->
<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>

View File

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

View File

@@ -26,4 +26,18 @@
<item name="android:textSize">@dimen/text_subtitle_size</item>
</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>