Compare commits

..

72 Commits

Author SHA1 Message Date
ZhanGSKen ae5fba531e <aes>APK 15.20.18 release Publish. 2026-07-11 20:07:00 +08:00
MiMo b59c274032 feat: 状态栏颜色跟随主题 colorPrimaryDark 属性动态变化
- 修改 AESTheme 主题 android:statusBarColor 从固定颜色改为引用 ?attr/colorPrimaryDark
- 同步修改 MyDebugActivityTheme 主题状态栏颜色配置
- 覆盖日间模式和夜间模式 (values/ values-night/)
- 主题切换时状态栏颜色自动跟随各主题定义的 colorPrimaryDark 值

修改文件:
  libaes/src/main/res/values/styles.xml
  libaes/src/main/res/values-night/styles.xml
  aes/src/main/res/values/styles.xml
  aes/src/main/res/values-night/styles.xml
2026-07-11 20:03:05 +08:00
ZhanGSKen 0c86530ca6 <libaes>Library Release 15.20.17 2026-06-29 11:29:33 +08:00
ZhanGSKen 9431dbb930 <aes>APK 15.20.17 release Publish. 2026-06-29 11:28:11 +08:00
GPT-OSS_20B f83295f64f feat: update libappbase dependency to latest 15.20.34
* Incremented libappbase version from 15.20.33 to 15.20.34 in libaes module.
  The latest release of libappbase is 15.20.34 and includes bug fixes
  for Android API 30 compatibility and several performance improvements.
* The change keeps Java 7 source/target levels, compileSdk/targetSdk
  unchanged and is fully backward‑compatible.
* No other project files were modified.
2026-06-29 11:24:07 +08:00
ZhanGSKen a71dd7d399 更新Git仓库地址 2026-06-24 08:44:36 +08:00
ZhanGSKen 3d48cd39fc <libaes>Library Release 15.20.16 2026-06-24 04:26:08 +08:00
ZhanGSKen 8f7fd504e8 <aes>APK 15.20.16 release Publish. 2026-06-24 04:25:55 +08:00
ZhanGSKen b20f53d06e 更新类库,编译调试。 2026-06-24 04:22:50 +08:00
ZhanGSKen 0a174f531f 移除米盟SDK依赖,统一类库使用https://jitpack.io/仓库 2026-06-08 21:20:10 +08:00
Studio 7d3b83b522 <libaes>Library Release 15.20.15 2026-06-03 20:29:53 +08:00
Studio 0ed6dbe6c2 <aes>APK 15.20.15 release Publish. 2026-06-03 20:29:39 +08:00
BigPickle 77c6776cf8 fix: 修复CrashHandler崩溃处理引用,改用CrashActivity直接调用
- App.java: CrashHandler.CrashActivity → CrashActivity,简化崩溃处理逻辑
- libaes/build.gradle: libappbase依赖版本 15.20.25 → 15.20.26
2026-06-03 20:27:13 +08:00
Studio f134b7c746 <libaes>Library Release 15.20.14 2026-06-03 07:07:35 +08:00
Studio 795593b3bf <aes>APK 15.20.14 release Publish. 2026-06-03 07:07:27 +08:00
Studio cda173fa9e <libaes>Library Release 15.20.13 2026-06-03 07:05:51 +08:00
Studio fe57aee469 <aes>APK 15.20.13 release Publish. 2026-06-03 07:05:34 +08:00
ZhanGSKen 3fc0883e0e 源码整理,更新类库 2026-06-03 07:03:06 +08:00
ZhanGSKen b162eb1a4e 优化应用创建函数,提高应用调试能力 2026-06-03 07:00:58 +08:00
STUDIO fdd8b64718 <libaes>Library Release 15.20.12 2026-06-02 08:54:36 +08:00
STUDIO 8d4fd95ff2 <aes>APK 15.20.12 release Publish. 2026-06-02 08:54:20 +08:00
ZhanGSKen 3b31a6a155 fix: AToolbar/ASupportToolbar 暗黑模式主题适配
AESAToolbar/AESASupportToolbar: @color 改为 ?attr 引用,
使渐变色跟随当前主题变体而非固定静态色。
所有主题暗黑模式配色采用头尾对调策略:
primary 保持不变,primaryDark ↔ accent 互换。
同步更新 values-night/colors.xml 静态颜色。
2026-06-02 08:50:35 +08:00
STUDIO 9eb873332c <libaes>Library Release 15.20.11 2026-06-02 02:23:32 +08:00
STUDIO 1b07a72236 <aes>APK 15.20.11 release Publish. 2026-06-02 02:23:15 +08:00
ZhanGSKen 8328e44b86 黑白主题着重提升黑色的专注感官,减少视觉流失感官色彩。 2026-06-02 02:18:42 +08:00
ZhanGSKen 981a8dd8cd fix: AESThemeUtil.init(null) NullPointerException 闪退修复
AESThemeUtil.init(null) 传入 null 时,Java 值传递导致
fillThemeStyleIDList 内部 new ArrayList 仅赋值给局部变量,
调用方 themeStyleIDList 仍为 null,随后的 addAll(null) 抛 NPE。

在 init() 中先创建新 ArrayList 再传入 fillThemeStyleIDList 以修复。
2026-06-02 01:59:15 +08:00
ZhanGSKen 6bc3cbe446 Merge branch 'aes' into merge 2026-06-02 01:54:28 +08:00
ZhanGSKen 79e42e72af 移除winboll项目 2026-06-02 01:54:03 +08:00
ZhanGSKen b59076d470 提交AESUtils init 初始化方案 2026-06-02 00:39:17 +08:00
ZhanGSKen 30fbf4f97e 主题功能重构,AESThemeUtil.init函数初始化未设置。 2026-05-28 21:27:13 +08:00
ZhanGSKen bc882c5f80 <libaes>Library Release 15.20.10 2026-05-27 20:22:57 +08:00
ZhanGSKen 8ae36effdd <aes>APK 15.20.10 release Publish. 2026-05-27 20:22:45 +08:00
BigPickle 0cc9b0871e fix: ASupportToolbar/AToolbar 控件颜色不跟随主题切换
- AESASupportToolbar/AESAToolbar 样式的颜色引用从 @color/ 改为
  ?attr/,使其通过主题属性解析,随主题切换而变化
- AESTheme 基主题新增 colorPrimary/colorPrimaryDark/colorAccent
  显式定义,保持默认绿色外观不变
2026-05-27 20:19:54 +08:00
ZhanGSKen d13c44c666 菜单排列重组 2026-05-27 19:35:01 +08:00
ZhanGSKen f786d06682 Merge branch 'aes' into merge 2026-05-27 15:21:52 +08:00
ZhanGSKen 168d2e2d60 Merge remote-tracking branch 'origin/projects_keeper_tag' into merge 2026-05-27 15:21:47 +08:00
ZhanGSKen f303a77e28 更新基础类库 2026-05-27 15:16:12 +08:00
ZhanGSKen 04b5d062aa Merge branch 'aes' into merge 2026-05-24 10:59:01 +08:00
ZhanGSKen fce2bf2fb1 Merge remote-tracking branch 'origin/projects_keeper_tag' into merge 2026-05-24 10:58:54 +08:00
ZhanGSKen 48a592cbd2 <libaes>Library Release 15.20.9 2026-05-24 10:49:00 +08:00
ZhanGSKen 5fcf0de951 <aes>APK 15.20.9 release Publish. 2026-05-24 10:48:48 +08:00
ZhanGSKen f027158d07 更新APPBase类库 2026-05-24 10:46:55 +08:00
ZhanGSKen 852f3d6cd8 <libaes>Library Release 15.20.8 2026-05-20 20:05:04 +08:00
ZhanGSKen c2cff1a2bb <aes>APK 15.20.8 release Publish. 2026-05-20 20:04:49 +08:00
ZhanGSKen 98dd2a60b8 添加网站https://jitpack.io接口编译配置 2026-05-20 20:02:37 +08:00
ZhanGSKen 25e966099f Merge remote-tracking branch 'origin/projects_keeper_tag' into aes 2026-05-20 19:56:45 +08:00
ZhanGSKen 0a69b54dd0 Merge branch 'aes' into merge 2026-05-20 12:09:20 +08:00
ZhanGSKen e87513b179 Merge branch 'projects_keeper_tag' into merge 2026-05-20 12:08:49 +08:00
ZhanGSKen 186b1656a7 更新联系邮箱 2026-05-20 11:19:18 +08:00
ZhanGSKen 4ce22e8750 更换APPBase类库 2026-05-20 03:19:52 +08:00
ZhanGSKen 5c8505dcbd <libaes>Library Release 15.20.7 2026-05-19 20:39:08 +08:00
ZhanGSKen 9c3963822e <aes>APK 15.20.7 release Publish. 2026-05-19 20:38:13 +08:00
ZhanGSKen 30532f8e8d 版本号对齐APPBase。 2026-05-19 20:26:18 +08:00
ZhanGSKen 2b98149e0f Merge branch 'aes' into merge 2026-05-19 19:45:12 +08:00
ZhanGSKen 80d9b764ee Merge branch 'projects_keeper_tag' into merge 2026-05-19 19:44:19 +08:00
ZhanGSKen d4f24a9b0c Merge branch 'aes' into merge 2026-05-19 19:30:17 +08:00
ZhanGSKen f357e353ee Merge branch 'projects_keeper_tag' into merge 2026-05-19 19:30:12 +08:00
ZhanGSKen 937f90aa07 <libaes>Library Release 15.20.6 2026-05-19 01:52:54 +08:00
ZhanGSKen 643e91bd71 <aes>APK 15.20.6 release Publish. 2026-05-19 01:52:47 +08:00
ZhanGSKen f4c6cea623 <libaes>Library Release 15.20.5 2026-05-19 01:48:56 +08:00
ZhanGSKen a0309ce98a <aes>APK 15.20.5 release Publish. 2026-05-19 01:48:43 +08:00
ZhanGSKen 931671d85a <libaes>Library Release 15.20.4 2026-05-17 20:53:51 +08:00
ZhanGSKen 580fa87773 <aes>APK 15.20.4 release Publish. 2026-05-17 20:53:39 +08:00
LaizyBoy a26276e169 fix: 将libaes模块debugTextColor从红色改为灰色
- libaes普通模式debugTextColor改为#FF808080(灰色)
- libaes深色模式debugTextColor改为#FFAAAAAA(浅灰)
2026-05-17 20:49:19 +08:00
LaizyBoy a3bf1087aa fix: 修复libaes模块Java 7兼容性问题及XML注释语法错误
- 为libaes添加compileOptions,指定Java 7源码与目标兼容级别
- 修复view_adscontrol.xml中XML注释包含非法"--"及缺少关闭符的问题
- 更新aes与libaes的build.properties编译计数
2026-05-17 20:30:41 +08:00
ZhanGSKen 1e5295df95 Merge keeper tag branch sources. 2026-05-17 20:15:50 +08:00
ZhanGSKen 8cb9ea5cae 清理源码冗余模块 2026-05-17 20:07:28 +08:00
ZhanGSKen 8006a05749 恢复winboll/winboll分支的基础类库资料,以便项目初始化时的基础控件使用的分析辅助。 2026-05-13 20:52:54 +08:00
ZhanGSKen 3c9a6ec884 修改示例模板的编译配置文件,设置版本号模板。 2026-05-13 20:21:55 +08:00
ZhanGSKen 8b47043053 更新 LICENSE-Private-Demo
字符更新
2026-05-13 19:51:47 +08:00
ZhanGSKen 202eeed72b 更新 LICENSE-Private-Demo_docs/LICENSE-Private-Demo
字符更新
2026-05-13 19:48:38 +08:00
ZhanGSKen 4511bf8730 添加WinBoLL-Demo使用的WinBoLL基础类库的基线型号配置文件。整理创建winboll衍生分支所要具备的基础文件。 2026-05-13 19:25:51 +08:00
339 changed files with 536 additions and 35988 deletions
+2 -2
View File
@@ -94,8 +94,8 @@ lint-results.html
## 忽略 AndroidIDE 临时文件夹
.androidide
## MyWinBoLL 基础应用(避免上传敏感配置)
## WinBoLL 基础应用(避免上传敏感配置)
/winboll.properties
/local.properties
#/settings.gradle
#/gradle.properties
#/gradle.properties
@@ -1,5 +1,5 @@
#!/system/bin/sh
## 合并其他项目分支的模块源码到projects_keeper分支。
## 合并其他项目分支的模块源码到projects-keeper分支。
# ====================== 0. 进入目标目录 ======================
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper"
@@ -36,7 +36,7 @@ fi
# ====================== 3. Git 分支检查 ======================
CUR_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
TARGET_BRANCH="projects_keeper"
TARGET_BRANCH="projects-keeper"
if [ "$CUR_BRANCH" != "$TARGET_BRANCH" ]; then
echo "错误:当前不在 $TARGET_BRANCH 分支!"
@@ -72,6 +72,7 @@ libaes
libappbase
libdebugtemp
libgpsrelaysentinel
libwinboll
local.properties-demo
mymessagemanager
positions
@@ -81,14 +82,13 @@ winboll
winboll.properties-demo
)
# ====================== 5. 获取当前目录真实文件列表(兼容过滤 . .. ======================
# ====================== 5. 获取当前目录真实文件列表 ======================
REAL_ITEMS=()
# 使用固定排序ls,自动过滤 . 和 ..,不会进入比对数组
while IFS= read -r line; do
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(LC_COLLATE=C ls -a1 --color=none)
done < <(ls -a)
# ====================== 6. 差异比对函数 ======================
check_diff() {
@@ -158,7 +158,7 @@ echo -e "## 对象列表结束
## 合并 APP 项目
MERGE_APP_PROJECT_LIST=(
WinBoLL
DemoAPP
)
echo -e "#@@@ 开始合并应用型模块源码 @@@#
## 目标合并对象列表:"
@@ -166,13 +166,14 @@ echo -e "#@@@ 开始合并应用型模块源码 @@@#
for item in "${MERGE_APP_PROJECT_LIST[@]}"; do
echo "正在合并 $item 项目 ..."
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
git checkout origin/$item_lower $item_lower
git checkout origin/$item_lower $item_lower
git add .
git commit -m "合并 $item 项目"
done
## 合并 LIB 项目
MERGE_LIB_PROJECT_LIST=(
WinBoLL
APPBase
AES
)
@@ -182,10 +183,10 @@ echo -e "#@@@ 开始合并类库型模块源码 @@@#
for item in "${MERGE_LIB_PROJECT_LIST[@]}"; do
echo "正在合并 $item 项目 ..."
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
git checkout origin/$item_lower $item_lower lib$item_lower
git checkout origin/$item_lower $item_lower lib$item_lower
git add .
git commit -m "合并 $item 项目"
done
echo '正在推送 Projects_Keeper 项目'
git push
git push
@@ -38,7 +38,7 @@ if [ "${NOW_BRANCH}" != "${TARGET_BRANCH}" ];then
exit 1
fi
# 目录结构校验白名单(不含 . ..
# 目录结构校验
MERGE_OBJECTS_LIST=(
.git
.gitignore
@@ -65,6 +65,7 @@ libaes
libappbase
libdebugtemp
libgpsrelaysentinel
libwinboll
local.properties-demo
mymessagemanager
positions
@@ -75,13 +76,9 @@ winboll.properties-demo
)
REAL_ITEMS=()
# 标准排序ls输出,循环强制过滤 . 和 ..
while IFS= read -r line; do
# 跳过虚拟目录 . 和 ..
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(LC_COLLATE=C ls -a1 --color=none)
[[ $line != "." && $line != ".." ]] && REAL_ITEMS+=("$line")
done < <(ls -a)
check_diff(){
local miss=() extra=()
@@ -92,17 +89,7 @@ check_diff(){
[[ ! " ${MERGE_OBJECTS_LIST[@]} " =~ " ${i} " ]] && extra+=("$i")
done
if [[ ${#miss[@]} -gt 0 || ${#extra[@]} -gt 0 ]];then
echo "========================================"
echo "本地目录结构不一致,终止运行"
if [[ ${#miss[@]} -gt 0 ]]; then
echo -e "\n缺失条目:"
for m in "${miss[@]}"; do echo " $m"; done
fi
if [[ ${#extra[@]} -gt 0 ]]; then
echo -e "\n多余条目:"
for e in "${extra[@]}"; do echo " $e"; done
fi
echo "========================================"
exit 1
fi
}
@@ -111,7 +98,7 @@ check_diff
echo -e "#@@@ 按时间获取最新标签合并模块源码 @@@#"
# 应用型模块
MERGE_APP_PROJECT_LIST=(WinBoLL)
MERGE_APP_PROJECT_LIST=(DemoAPP)
echo -e "---------- 应用型模块 ----------"
for name in "${MERGE_APP_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
@@ -133,7 +120,7 @@ for name in "${MERGE_APP_PROJECT_LIST[@]}";do
done
# 类库模块
MERGE_LIB_PROJECT_LIST=(APPBase AES)
MERGE_LIB_PROJECT_LIST=(WinBoLL APPBase AES)
echo -e "---------- 类库模块 ----------"
for name in "${MERGE_LIB_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
-228
View File
@@ -1,228 +0,0 @@
#!/usr/bin/bash
# ==============================================================================
# WinBoLL 应用发布脚本
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
# 依赖:build.properties、app_update_description.txt(项目根目录下)
# 使用:./script_name.sh <APP_NAME>
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
# ==============================================================================
# ==================== 常量定义 ====================
# 脚本退出码
EXIT_CODE_SUCCESS=0
EXIT_CODE_ERR_NO_APP_NAME=2
EXIT_CODE_ERR_WORK_DIR=1
EXIT_CODE_ERR_GIT_CHECK=1
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
# Gradle 任务(正式发布)
GRADLE_TASK_PUBLISH="assembleStageRelease"
# Gradle 任务(调试用,注释备用)
# GRADLE_TASK_DEBUG="assembleBetaDebug"
# aapt2本地覆盖参数
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
# 禁用Gradle守护进程
GRADLE_NO_DAEMON="--no-daemon"
# ==================== 函数定义 ====================
# 检查Git源码是否已完全提交(无未提交变更)
# 返回值:0=已完全提交,1=存在未提交变更
function checkGitSources() {
# 配置Git安全目录(解决权限问题)
git config --global --add safe.directory "$(pwd)"
# 检查是否有未提交的变更
if [[ -n $(git diff --stat) ]]; then
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
return 1
fi
echo "[INFO] Git源码检查通过:所有变更已提交。"
return 0
}
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
# 返回值:1=用户选择是,0=用户选择否
function askAddWorkflowsTag() {
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
if [[ $answer =~ ^[Yy]$ ]]; then
return 1
else
return 0
fi
}
# 添加WinBoLL正式标签
# 参数:$1=应用名称(项目根目录名)
# 返回值:0=标签添加成功,1=标签已存在/添加失败
function addWinBoLLTag() {
local app_name=$1
local build_prop_path="${app_name}/build.properties"
# 从build.properties中提取publishVersion
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
if [[ -z ${publish_version} ]]; then
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
return 1
fi
echo "[INFO] 从${build_prop_path}读取到publishVersion${publish_version}"
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>
local tag="${app_name}-v${publish_version}"
echo "[INFO] 准备添加WinBoLL标签:${tag}"
# 检查标签是否已存在
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
echo "[ERROR] WinBoLL标签${tag}已存在!"
return 1
fi
# 添加带注释的标签(注释来自app_update_description.txt
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
echo "[INFO] WinBoLL标签${tag}添加成功!"
return 0
}
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
# 参数:$1=应用名称(项目根目录名)
# 返回值:0=标签添加成功,1=标签已存在/添加失败
function addWorkflowsTag() {
local app_name=$1
local build_prop_path="${app_name}/build.properties"
# 从build.properties中提取baseBetaVersion
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
if [[ -z ${base_beta_version} ]]; then
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
return 1
fi
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion${base_beta_version}"
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta
local tag="${app_name}-v${base_beta_version}-beta"
echo "[INFO] 准备添加Workflows标签:${tag}"
# 检查标签是否已存在
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
echo "[ERROR] Workflows标签${tag}已存在!"
return 1
fi
# 添加带注释的标签(注释来自app_update_description.txt
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
echo "[INFO] Workflows标签${tag}添加成功!"
return 0
}
# ==================== 主流程开始 ====================
echo "============================================="
echo " WinBoLL 应用发布脚本"
echo "============================================="
# 1. 检查应用名称参数是否指定
if [ -z "$1" ]; then
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
exit ${EXIT_CODE_ERR_NO_APP_NAME}
fi
APP_NAME=$1
echo "[INFO] 待发布应用名称:${APP_NAME}"
# 2. 检查并切换到项目根目录(确保build.properties存在)
echo "[INFO] 当前工作目录:$(pwd)"
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
cd ..
echo "[INFO] 切换后工作目录:$(pwd)"
fi
# 验证最终工作目录是否正确
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
exit ${EXIT_CODE_ERR_WORK_DIR}
fi
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
# 3. 检查Git源码状态
echo "---------------------------------------------"
echo " 步骤1:检查Git源码状态"
echo "---------------------------------------------"
checkGitSources
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] Git源码检查失败,脚本终止!"
exit ${EXIT_CODE_ERR_GIT_CHECK}
fi
# 4. 编译Stage Release版本APK(携带aapt2覆盖参数 + --no-daemon
echo "---------------------------------------------"
echo " 步骤2:编译Stage Release APK"
echo "---------------------------------------------"
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
# 调试用(注释正式任务,启用调试任务)
# bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_DEBUG}
bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_PUBLISH}
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] Gradle编译任务失败!"
exit 1
fi
echo "[INFO] Stage Release APK编译成功!"
# 5. 添加WinBoLL正式标签
echo "---------------------------------------------"
echo " 步骤3:添加WinBoLL标签"
echo "---------------------------------------------"
addWinBoLLTag ${APP_NAME}
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
fi
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
# echo "---------------------------------------------"
# echo " 步骤4:添加Workflows标签(可选)"
# echo "---------------------------------------------"
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
# askAddWorkflowsTag
# nAskAddWorkflowsTag=$?
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
# addWorkflowsTag ${APP_NAME}
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
# exit 1
# fi
# fi
# 7. 清理更新描述文件
echo "---------------------------------------------"
echo " 步骤5:清理更新描述文件"
echo "---------------------------------------------"
echo "" > "${APP_NAME}/app_update_description.txt"
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
# 8. 提交并推送源码与标签
echo "---------------------------------------------"
echo " 步骤6:提交并推送源码"
echo "---------------------------------------------"
git add .
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
echo "[INFO] 源码提交成功,开始推送..."
# 推送源码到远程仓库
git push origin
# 推送标签到远程仓库
git push origin --tags
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
echo "[INFO] 源码与标签推送成功!"
else
echo "[ERROR] 源码与标签推送失败!"
exit 1
fi
# ==================== 主流程结束 ====================
echo "============================================="
echo " WinBoLL 应用发布完成!"
echo "============================================="
exit ${EXIT_CODE_SUCCESS}
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/bash
# aapt2本地覆盖参数
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
# Gradle禁用守护进程参数
GRADLE_NO_DAEMON="--no-daemon"
# 检查是否指定了将要发布的类库名称
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ]; then
echo "No Library name specified : $0"
exit 2
fi
## 正式发布使用
git pull && bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishReleasePublicationToWinBoLLReleaseRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
## 调试使用
#bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishSnapshotWinBoLLPublicationToWinBoLLSnapshotRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
+27 -56
View File
@@ -122,6 +122,7 @@ android {
// 如果正在调试,就拷贝到 WinBoLL 备份管理文件夹
//
if(variant.flavorName == "beta"&&variant.buildType.name == "debug"){
//File outBuildBckDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/${variant.buildType.name}")
File outBuildBckDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}")
// 创建目标路径目录
if(!outBuildBckDir.exists()) {
@@ -129,7 +130,6 @@ android {
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
}
if(outBuildBckDir.exists()) {
def targetApkFile = new File(outBuildBckDir, outputFileName)
copy{
from file.outputFile
into outBuildBckDir
@@ -138,14 +138,6 @@ android {
}
println "Output APK (WinBoLLStudio): " + outBuildBckDir.getAbsolutePath() + "/${outputFileName}"
}
// ========== 设置文件权限为775 ==========
if(targetApkFile.exists()){
exec {
commandLine 'chmod', '775', targetApkFile.absolutePath
}
println "Set file permission to 775 : ${targetApkFile.absolutePath}"
}
// 检查编译标志位配置
assert (winbollBuildProps['buildCount'] != null)
assert (winbollBuildProps['libraryProject'] != null)
@@ -168,7 +160,8 @@ android {
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
println "\n\n>>> Library Project build.properties saved.\n\n";
}
@@ -179,12 +172,16 @@ android {
//
if(variant.flavorName == "stage"&&variant.buildType.name == "release"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/tag/")
// 创建目标路径目录
if(!outTagDir.exists()) {
@@ -195,10 +192,12 @@ android {
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在,构建拷贝任务停止
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制完整版APK
def fullApkFile = new File(outTagDir, outputFileName)
// 复制一个备份
copy{
from file.outputFile
into outTagDir
@@ -207,16 +206,7 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 设置权限775。
if(fullApkFile.exists()){
exec {
commandLine 'chmod', '775', fullApkFile.absolutePath
}
println "Set file permission to 775 : ${fullApkFile.absolutePath}"
}
// 复制短版本名APK
def shortApkFile = new File(outTagDir, szCommonTagAPKName)
// 复制一个并重命名为短版本名
copy{
from file.outputFile
into outTagDir
@@ -225,14 +215,6 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
// 设置权限775。
if(shortApkFile.exists()){
exec {
commandLine 'chmod', '775', shortApkFile.absolutePath
}
println "Set file permission to 775 : ${shortApkFile.absolutePath}"
}
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['publishVersion'] != null)
@@ -257,11 +239,14 @@ android {
fos.close();
if(winbollBuildProps['libraryProject'] != "") {
// 如果应用 build.properties 文件设置了类库模块项目文件名
// 就拷贝一份新的编译标志配置到类库项目文件夹
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
assert(winbollBuildPropsFile.exists())
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
@@ -278,12 +263,17 @@ android {
// 如果正在调试发布版,就只生成和输出APK文件,不处理 Git 仓库提交与更新问题。
//
if(variant.flavorName == "stage"&&variant.buildType.name == "debug"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}/")
// 创建目标路径目录
if(!outTagDir.exists()) {
@@ -294,11 +284,13 @@ android {
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在,构建拷贝任务停止
println '如果是在调试 Stage 版应用包构建,请删除(注:在debug目录)现有的 Stage 应用包('+targetAPK.getAbsolutePath()+')。再编译一次。'
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制完整版APK
def debugFullApk = new File(outTagDir, outputFileName)
// 复制一个备份
copy{
from file.outputFile
into outTagDir
@@ -307,16 +299,7 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 权限设为775。
if(debugFullApk.exists()){
exec {
commandLine 'chmod', '775', debugFullApk.absolutePath
}
println "Set file permission to 775 : ${debugFullApk.absolutePath}"
}
// 复制短版本名APK
def debugShortApk = new File(outTagDir, szCommonTagAPKName)
// 复制一个并重命名为短版本名
copy{
from file.outputFile
into outTagDir
@@ -325,13 +308,8 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
// 权限设为775
if(debugShortApk.exists()){
exec {
commandLine 'chmod', '775', debugShortApk.absolutePath
}
println "Set file permission to 775 : ${debugShortApk.absolutePath}"
}
//不保存编译标志配置
}
}
@@ -350,13 +328,6 @@ android {
}
println "Output APK (Common): " + outCommonDir.getAbsolutePath() + "/${commandAPKName}"
}
// 额外输出文件设置775权限
if(apkFile.exists()){
exec {
commandLine 'chmod', '775', apkFile.absolutePath
}
println "Set file permission to 775 : ${apkFile.absolutePath}"
}
}
}
+1
View File
@@ -0,0 +1 @@
15.20
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-97
View File
@@ -1,97 +0,0 @@
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
# LICENSE-Private-Demo
# WinBoLL 源码公共转私有继承开发规范守则
## 核心声明
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
## 二、公私分支授权标识文件定义(风控核心依据)
### 1. 公共开源分支唯一标识
**文件名:LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
### 2. 私有开发分支唯一标识
**文件名:LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
## 四、公共转私有标准化操作步骤(锁死合并风险)
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
>
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
## 五、人为操作责任界定(核心补充条款)
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注:文档部分内容可能由 AI 生成)
+14 -14
View File
@@ -6,11 +6,11 @@
## 核心声明
本文档**唯一核心设计目的**通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
@@ -24,7 +24,7 @@
### 1. 公共开源分支唯一标识
**文件名LICENSE**
**文件名: LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
@@ -34,7 +34,7 @@
### 2. 私有开发分支唯一标识
**文件名LICENSE-Private**
**文件名: LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
@@ -44,15 +44,15 @@
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支允许正常拉取、同步上游更新,不影响私有开发迭代
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
@@ -68,7 +68,7 @@
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
@@ -84,14 +84,14 @@
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支仅存在 `LICENSE-Private`,无 `LICENSE`
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注文档部分内容可能由 AI 生成)
> (注: 文档部分内容可能由 AI 生成)
+14 -14
View File
@@ -6,11 +6,11 @@
## 核心声明
本文档**唯一核心设计目的**通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
@@ -24,7 +24,7 @@
### 1. 公共开源分支唯一标识
**文件名LICENSE**
**文件名: LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
@@ -34,7 +34,7 @@
### 2. 私有开发分支唯一标识
**文件名LICENSE-Private**
**文件名: LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
@@ -44,15 +44,15 @@
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支允许正常拉取、同步上游更新,不影响私有开发迭代
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
@@ -68,7 +68,7 @@
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
@@ -84,14 +84,14 @@
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支仅存在 `LICENSE-Private`,无 `LICENSE`
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注文档部分内容可能由 AI 生成)
> (注: 文档部分内容可能由 AI 生成)
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 28 20:55:33 HKT 2026
stageCount=35
#Wed May 27 14:51:29 HKT 2026
stageCount=23
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.34
publishVersion=15.20.22
buildCount=0
baseBetaVersion=15.20.35
baseBetaVersion=15.20.23
@@ -37,7 +37,7 @@ public class AboutActivity extends Activity {
appInfo.setAppName("APPBase");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitName("APPBase");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
@@ -1,11 +1,8 @@
package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import cc.winboll.studio.libappbase.BuildConfig;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -24,24 +21,10 @@ public class App extends GlobalApplication {
*/
@Override
public void onCreate() {
try {
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
}
/**
@@ -1,491 +0,0 @@
package cc.winboll.studio.libaes.views;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.enums.ADsMode;
import cc.winboll.studio.libaes.utils.MimoUtils;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import com.miui.zeus.mimo.sdk.ADParams;
import com.miui.zeus.mimo.sdk.BannerAd;
import com.miui.zeus.mimo.sdk.MimoCustomController;
import com.miui.zeus.mimo.sdk.MimoLocation;
import com.miui.zeus.mimo.sdk.MimoSdk;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/18 14:41
* @Describe WinBoLL 横幅广告类
*/
public class ADsBannerView extends LinearLayout {
public static final String TAG = "ADsBannerView";
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL_BETA = "d129ee5a263911f981a6dc7a9802e3e7";
private String BANNER_POS_ID_WINBOLL = "4ec30efdb32271765b9a4efac902828b";
/*
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL_BETA = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL = "802e356f1726f9ff39c69308bfd6f06a";
*/
Context mContext;
View mMianView;
SharedPreferences mSharedPreferences;
ViewGroup mContainer;
BannerAd mBannerAd;
List<BannerAd> mAllBanners = new ArrayList<>();
// 新增:主线程Handler,确保广告操作在主线程执行
private Handler mMainHandler;
public ADsBannerView(Context context) {
super(context);
initView(context);
}
public ADsBannerView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context);
}
void initView(Context context) {
this.mContext = context;
initMimoSdk(this.mContext);
// 初始化主线程Handler(关键:确保广告操作在主线程执行)
mMainHandler = new Handler(Looper.getMainLooper());
this.mMianView = inflate(this.mContext, R.layout.view_adsbanner, null);
mContainer = this.mMianView.findViewById(R.id.ads_container);
addView(this.mMianView);
}
public void resumeADs(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
// 2. 释放之前的广告资源
if (mBannerAd != null) {
mBannerAd.destroy();
}
return;
}
// 修复:优化广告请求逻辑(添加生命周期判断 + 主线程执行)
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
if (ADsControlView.getAdsModeFromStatic(this.mContext) == ADsMode.MIMO_SDK) {
LogUtils.i(TAG, "已设置播放米盟广告,正在播放...");
mMainHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 再次校验生命周期,避免延迟执行时Activity已销毁
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
fetchAd(activity);
}
}
}, 1000); // 延迟1秒请求广告,提升页面加载体验
}
}
}
/**
* 释放广告资源(关键:避免内存泄漏和空Context调用)
*/
public void releaseAdResources() {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "releaseAdResources()");
// 移除Handler回调
if (mMainHandler != null) {
mMainHandler.removeCallbacksAndMessages(null);
}
// 销毁所有广告实例
if (mAllBanners != null && !mAllBanners.isEmpty()) {
for (BannerAd ad : mAllBanners) {
if (ad != null) {
ad.destroy();
}
}
mAllBanners.clear();
}
// 置空当前广告引用
mBannerAd = null;
// 移除广告容器中的视图
if (mContainer != null) {
mContainer.removeAllViews();
}
}
/**
* 显示广告(核心修复:传递安全的Context + 生命周期校验)
*/
private void showAd(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "showAd()");
// 1. 生命周期校验:避免Activity已销毁时操作UI
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
LogUtils.e(TAG, "showAd: Activity is finishing or destroyed");
return;
}
// 2. 非空校验:广告实例和容器
if (mBannerAd == null || mContainer == null) {
LogUtils.e(TAG, "showAd: BannerAd or Container is null");
return;
}
// 3. 创建广告容器(使用ApplicationContext避免内存泄漏)
final FrameLayout container = new FrameLayout(activity.getApplicationContext());
container.setPadding(0, 0, 0, MimoUtils.dpToPx(activity, 10));
mContainer.addView(container, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
));
// if (mIsBiddingWin) {
// mBannerAd.setPrice(getPrice());
// }
// 4. 显示广告:传递ApplicationContext,避免Activity Context失效
mBannerAd.showAd(activity, container, new BannerAd.BannerInteractionListener() {
@Override
public void onAdClick() {
LogUtils.d(TAG, "onAdClick");
}
@Override
public void onAdShow() {
LogUtils.d(TAG, "onAdShow");
}
@Override
public void onAdDismiss() {
LogUtils.d(TAG, "onAdDismiss");
// 修复:移除容器时校验Activity状态
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
mContainer.removeView(container);
}
}
@Override
public void onRenderSuccess() {
LogUtils.d(TAG, "onRenderSuccess");
}
@Override
public void onRenderFail(int code, String msg) {
LogUtils.e(TAG, "onRenderFail errorCode " + code + " errorMsg " + msg);
// 修复:渲染失败时移除容器
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
mContainer.removeView(container);
}
}
});
}
/**
* 请求广告(核心修复:Context安全校验 + 异常捕获 + 资源管理)
*/
private void fetchAd(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "fetchAd()");
// 1. 双重校验:Activity未销毁 + Context非空
if (activity == null || activity.isFinishing() || activity.isDestroyed() || activity.getApplicationContext() == null) {
LogUtils.e(TAG, "fetchAd: Invalid Context or Activity state");
return;
}
// 2. 释放之前的广告资源,避免内存泄漏
if (mBannerAd != null) {
mBannerAd.destroy();
}
// 3. 初始化广告(使用ApplicationContext,避免Activity Context失效)
try {
mBannerAd = new BannerAd();
mAllBanners.add(mBannerAd);
} catch (Exception e) {
LogUtils.e(TAG, "fetchAd: Init BannerAd failed", e);
return;
}
// 4. 设置下载监听
mBannerAd.setDownLoadListener(new BannerAd.BannerDownloadListener() {
@Override
public void onDownloadStarted() {
LogUtils.d(TAG, "onDownloadStarted");
}
@Override
public void onDownloadPaused() {
LogUtils.d(TAG, "onDownloadPaused");
}
@Override
public void onDownloadFailed(int errorCode) {
String msg = "onDownloadFailed, errorCode = " + errorCode;
LogUtils.d(TAG, msg);
//ToastUtils.show(msg);
}
@Override
public void onDownloadFinished() {
LogUtils.d(TAG, "onDownloadFinished");
}
@Override
public void onDownloadProgressUpdated(int progress) {
LogUtils.d(TAG, "onDownloadProgressUpdated " + progress + "%");
}
@Override
public void onInstallFailed(int errorCode) {
LogUtils.d(TAG, "onInstallFailed, errorCode = " + errorCode);
}
@Override
public void onInstallStart() {
LogUtils.d(TAG, "onInstallStart");
}
@Override
public void onInstallSuccess() {
LogUtils.d(TAG, "onInstallSuccess");
}
@Override
public void onDownloadCancel() {
LogUtils.d(TAG, "onDownloadCancel");
}
});
// 5. 构建广告参数并请求
String currentAD_ID = getAD_ID();
LogUtils.d(TAG, String.format("currentAD_ID = %s", currentAD_ID));
ADParams params = new ADParams.Builder().setUpId(currentAD_ID).build();
mBannerAd.loadAd(params, new BannerAd.BannerLoadListener() {
@Override
public void onBannerAdLoadSuccess() {
LogUtils.d(TAG, "onBannerAdLoadSuccess()");
// 修复:广告加载成功后校验Activity状态
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
showAd(activity);
//ToastUtils.show("showAd()");
}
}
@Override
public void onAdLoadFailed(int errorCode, String errorMsg) {
String msg = "onAdLoadFailed: errorCode = " + errorCode + ", errorMsg = " + errorMsg;
LogUtils.d(TAG, msg);
removeAllBanners();
}
});
}
void removeAllBanners() {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
// 修复:加载失败时移除当前广告实例
if (mAllBanners.contains(mBannerAd)) {
mAllBanners.remove(mBannerAd);
}
mBannerAd.destroy();
mBannerAd = null;
}
/**
* 根据当前秒数获取广告ID(原逻辑保留)
*/
private String getAD_ID() {
long currentSecond = System.currentTimeMillis() / 1000;
return (currentSecond % 2 == 0) ? BANNER_POS_ID :
(GlobalApplication.isDebugging() ? BANNER_POS_ID_WINBOLL_BETA : BANNER_POS_ID_WINBOLL);
}
/**
* 获取广告价格(原逻辑保留,添加空指针校验)
*/
// private long getPrice() {
// if (mBannerAd == null) {
// return 0;
// }
// Map<String, Object> map = mBannerAd.getMediaExtraInfo();
// if (map == null || map.isEmpty() || !map.containsKey("price")) {
// LogUtils.w(TAG, "getPrice: media extra info is null or no price key");
// return 0;
// }
// Object priceObj = map.get("price");
// if (priceObj instanceof Long) {
// return (Long) priceObj;
// } else if (priceObj instanceof Integer) {
// return ((Integer) priceObj).longValue();
// } else {
// LogUtils.e(TAG, "getPrice: price type is invalid");
// return 0;
// }
// }
/**
* 显示隐私协议弹窗(原逻辑保留,优化Context使用)
*/
// private void showPrivacy() {
// // 校验Activity状态,避免弹窗泄露
// if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed()) {
// return;
// }
// ADsMode adsMode = ADsControlView.getAdsModeFromStatic(this.mContext);
// if (adsMode == ADsMode.STANDALONE) {
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.STANDALONE);
// LogUtils.i(TAG, "单机模式,广告已处于不可用状态...");
// Toast.makeText(getActivity().getApplicationContext(), "单机模式,广告已处于不可用状态...", Toast.LENGTH_SHORT).show();
// return;
// } else if (adsMode == ADsMode.MIMO_SDK) {
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.MIMO_SDK);
// LogUtils.i(TAG, "米盟广告SDK支持模式,现在初始化SDK...");
// initMimoSdk();
// return;
// }
// else {
// LogUtils.i(TAG, "开始弹出隐私协议...");
// AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// builder.setTitle("用户须知");
// builder.setMessage("小米广告SDK隐私政策: https://dev.mi.com/distribute/doc/details?pId=1688, 请复制到浏览器查看");
// builder.setIcon(R.drawable.ic_launcher);
// builder.setCancelable(false); // 点击对话框以外的区域不消失
// builder.setPositiveButton("同意", new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// getSharedPreferences().edit()
// .putString(PRIVACY_VALUE, String.valueOf(1))
// .apply();
// initMimoSdk();
// dialog.dismiss();
// }
// });
// builder.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// getSharedPreferences().edit()
// .putString(PRIVACY_VALUE, String.valueOf(0))
// .apply();
// dialog.dismiss();
// }
// });
// AlertDialog dialog = builder.create();
//
// // 配置弹窗位置(底部全屏)
// Window window = dialog.getWindow();
// if (window != null) {
// window.setGravity(Gravity.BOTTOM);
// WindowManager m = getActivity().getWindowManager();
// Display d = m.getDefaultDisplay();
// WindowManager.LayoutParams p = window.getAttributes();
// p.width = d.getWidth();
// window.setAttributes(p);
// }
// dialog.show();
// }
// }
/**
* 初始化米盟SDK(核心修复:传递ApplicationContext + 异常捕获)
*/
private void initMimoSdk(Context context) {
// 1. 安全获取ApplicationContext,避免Activity Context失效
Context appContext = context.getApplicationContext();
if (appContext == null) {
LogUtils.e(TAG, "initMimoSdk: ApplicationContext is null");
return;
}
// 2. 初始化SDK,捕获异常避免崩溃
try {
MimoSdk.init(appContext, new MimoCustomController() {
@Override
public boolean isCanUseLocation() {
return true;
}
@Override
public MimoLocation getMimoLocation() {
return null;
}
@Override
public boolean isCanUseWifiState() {
return true;
}
@Override
public boolean alist() {
return true;
}
}, new MimoSdk.InitCallback() {
@Override
public void success() {
LogUtils.d(TAG, "MimoSdk init success");
}
@Override
public void fail(int code, String msg) {
LogUtils.e(TAG, "MimoSdk init fail, code=" + code + ",msg=" + msg);
}
});
MimoSdk.setDebugOn(true);
} catch (Exception e) {
LogUtils.e(TAG, "initMimoSdk: init failed", e);
}
}
/**
* 获取SharedPreferences实例(原逻辑保留,添加空指针校验)
*/
// SharedPreferences getSharedPreferences() {
//// if (mSharedPreferences == null) {
//// // 修复:使用ApplicationContext获取SharedPreferences,避免Activity Context泄露
//// Context appContext = getActivity().getApplicationContext();
//// if (appContext != null) {
//// mSharedPreferences = appContext.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
//// } else {
//// LogUtils.e(TAG, "getSharedPreferences: ApplicationContext is null");
//// // 降级方案:若ApplicationContext为空,使用Activity Context(仅作兼容)
//// mSharedPreferences = getActivity().getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
//// }
//// }
// return mSharedPreferences;
// }
}
@@ -1,633 +0,0 @@
package cc.winboll.studio.libaes.views;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.Html;
import android.util.AttributeSet;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.PopupWindow;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.enums.ADsMode;
import cc.winboll.studio.libaes.enums.PrivacyAgreeStatus;
import cc.winboll.studio.libaes.utils.WebUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import com.miui.zeus.mimo.sdk.MimoCustomController;
import com.miui.zeus.mimo.sdk.MimoLocation;
import com.miui.zeus.mimo.sdk.MimoSdk;
import java.lang.reflect.Field;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 17:51
* @LastEditTime 2026/01/08 11:00:00 HKT
* @Describe 广告模式控制控件(Java 7 兼容,云宝物语模式)
* 核心修改:将PopupMenu锚点绑定到view_popmenu_anchor_point控件,菜单精准显示在锚点位置
*/
public class ADsControlView extends LinearLayout {
public static final String TAG = "ADsControlView";
// SP存储配置
private static final String SP_NAME = "ads_control_config";
private static final String KEY_SELECTED_MODE = "selected_ads_mode";
ADsMode mADsMode;
private static final String PRIVACY_VALUE = "privacy_value";
PrivacyAgreeStatus mPrivacyAgreeStatus;
// Handler消息标识
private static final int MSG_UPDATE_MODE = 1001;
// 控件引用
private RadioGroup rgADsMode;
private RadioButton rbStandalone;
private RadioButton rbMimoSDK;
private RadioButton rbStoreQrcode;
private RelativeLayout rlWinbollStore;
private ImageView ivWinbollStoreQrcode;
// 新增:锚点控件引用
private TextView viewPopmenuAnchorPoint;
// 外部监听、SP实例、Handler实例
private OnAdsModeSelectedListener listener;
private SharedPreferences sharedPreferences;
private InternalHandler mHandler;
private Context mContext;
// 静态列表:存储所有已创建的控件实例
private static final java.util.List<ADsControlView> sControlViews = new java.util.ArrayList<ADsControlView>();
// 常量定义
private static final String WECHAT_STORE_URL = "https://store.weixin.qq.com/shop/b/XhrPkZgoeHo4zug";
private static final int MENU_ITEM_OPEN_STORE = 1001;
// 构造方法(Java 7 兼容)
public ADsControlView(Context context) {
super(context);
initView(context);
}
public ADsControlView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
@SuppressWarnings("deprecation")
public ADsControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public void setPrivacyAgreeStatus(PrivacyAgreeStatus privacyAgreeStatus) {
this.mPrivacyAgreeStatus = privacyAgreeStatus;
sharedPreferences.edit().putString(PRIVACY_VALUE, this.mPrivacyAgreeStatus.name()).apply();
}
public PrivacyAgreeStatus getPrivacyAgreeStatus() {
String privacyAgreeStatusStr = sharedPreferences.getString(PRIVACY_VALUE, PrivacyAgreeStatus.UN_SIGNED.name());
PrivacyAgreeStatus privacyAgreeStatus = PrivacyAgreeStatus.fromString(privacyAgreeStatusStr);
return privacyAgreeStatus;
}
public void setADsMode(ADsMode mADsMode) {
this.mADsMode = mADsMode;
sharedPreferences.edit().putString(KEY_SELECTED_MODE, this.mADsMode.name()).apply();
updateStoreQrcodeLayoutVisibility(mADsMode);
}
public ADsMode getADsMode() {
String savedModeStr = sharedPreferences.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
mADsMode = ADsMode.fromValue(savedModeStr);
return mADsMode;
}
/**
* 初始化视图、SP、Handler
*/
private void initView(final Context context) {
this.mContext = context;
// 加载布局
LayoutInflater.from(context).inflate(R.layout.view_adscontrol, this, true);
// 初始化SP
sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
// 绑定控件
rgADsMode = (RadioGroup) findViewById(R.id.rg_ads_mode);
rbStandalone = (RadioButton) findViewById(R.id.rb_standalone);
rbMimoSDK = (RadioButton) findViewById(R.id.rb_mimo_sdk);
rbStoreQrcode = (RadioButton) findViewById(R.id.rb_store_qrcode);
rlWinbollStore = (RelativeLayout) findViewById(R.id.rl_winboll_store);
ivWinbollStoreQrcode = (ImageView) findViewById(R.id.iv_winboll_store);
// 绑定锚点控件
viewPopmenuAnchorPoint = (TextView) findViewById(R.id.view_popmenu_anchor_point);
// 初始化Handler
mHandler = new InternalHandler(Looper.getMainLooper());
// 核心修改:初始化图片的点击和长按事件(锚点改为view_popmenu_anchor_point
initImageViewClickAndLongClick();
// 注册控件实例
registerControlView(this);
// 从SP读取初始模式
setSelectedMode(getADsMode());
// 单选组选择事件监听
rgADsMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
if (checkedId == R.id.rb_standalone) {
setADsMode(ADsMode.STANDALONE);
if (listener != null) listener.onModeSelected(ADsMode.STANDALONE);
} else if (checkedId == R.id.rb_mimo_sdk) {
handlePrivacyLogic((Activity) context, PrivacyAgreeStatus.UN_SIGNED, new OnPrivacyChangeListener() {
@Override
public void onAgreePrivacy() {
setADsMode(ADsMode.MIMO_SDK);
if (listener != null) listener.onModeSelected(ADsMode.MIMO_SDK);
}
@Override
public void onDisagreePrivacy() {
setADsMode(ADsMode.STANDALONE);
setSelectedMode(ADsMode.STANDALONE);
if (listener != null) listener.onModeSelected(ADsMode.STANDALONE);
}
});
} else if (checkedId == R.id.rb_store_qrcode) {
setADsMode(ADsMode.STORE_QRCODE);
if (listener != null) listener.onModeSelected(ADsMode.STORE_QRCODE);
}
}
});
}
/**
* 初始化图片的点击和长按事件
* 核心:将PopupMenu锚点绑定到view_popmenu_anchor_point控件
*/
private void initImageViewClickAndLongClick() {
if (ivWinbollStoreQrcode == null || viewPopmenuAnchorPoint == null) {
LogUtils.e(TAG, "initImageViewClickAndLongClick: 控件引用为空");
return;
}
// 1. 点击事件:简化为提示信息
ivWinbollStoreQrcode.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ToastUtils.show("长按图片可打开微信小店");
LogUtils.d(TAG, "图片点击:提示用户长按打开微信小店");
}
});
// 2. 长按事件:锚点改为view_popmenu_anchor_point
ivWinbollStoreQrcode.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
// 计算锚点控件的屏幕坐标(用于菜单位置微调)
int[] anchorLocation = new int[2];
viewPopmenuAnchorPoint.getLocationOnScreen(anchorLocation);
final int anchorX = anchorLocation[0];
final int anchorY = anchorLocation[1];
// 创建PopupMenu,锚点绑定到view_popmenu_anchor_point
PopupMenu popupMenu = new PopupMenu(mContext, viewPopmenuAnchorPoint);
// 设置菜单重力:相对锚点居中显示
popupMenu.setGravity(Gravity.CENTER);
Menu menu = popupMenu.getMenu();
menu.add(Menu.NONE, MENU_ITEM_OPEN_STORE, Menu.NONE, "打开微信小店");
// 设置菜单点击事件
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == MENU_ITEM_OPEN_STORE) {
WebUtils.openUrlInBrowser(mContext, WECHAT_STORE_URL);
return true;
}
return false;
}
});
try {
// 反射获取PopupWindow,微调菜单位置(可选)
Field popupField = PopupMenu.class.getDeclaredField("mPopup");
popupField.setAccessible(true);
Object popupObject = popupField.get(popupMenu);
if (popupObject instanceof PopupWindow) {
final PopupWindow popupWindow = (PopupWindow) popupObject;
popupWindow.setAnimationStyle(0); // 关闭默认动画
// 延迟微调菜单位置(确保布局测量完成)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
int menuX = anchorX + viewPopmenuAnchorPoint.getWidth() / 2 - popupWindow.getWidth() / 2;
int menuY = anchorY + viewPopmenuAnchorPoint.getHeight() / 2 - popupWindow.getHeight() / 2;
if (!popupWindow.isShowing()) {
popupWindow.showAtLocation(viewPopmenuAnchorPoint, Gravity.NO_GRAVITY, menuX, menuY);
}
}
}, 30);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
LogUtils.e(TAG, "反射获取PopupWindow失败", e);
}
// 显示菜单
popupMenu.show();
LogUtils.d(TAG, "长按图片,菜单锚点为view_popmenu_anchor_point");
return true;
}
});
// 设置控件可交互标识
ivWinbollStoreQrcode.setClickable(true);
ivWinbollStoreQrcode.setFocusable(true);
ivWinbollStoreQrcode.setLongClickable(true);
viewPopmenuAnchorPoint.setClickable(false); // 锚点控件不可点击
viewPopmenuAnchorPoint.setLongClickable(false);
}
/**
* 从ImageView中提取Bitmap(保留方法,无实际调用)
*/
private Bitmap getBitmapFromImageView(ImageView imageView) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
return null;
}
/**
* 压缩Bitmap(备用方法,无实际调用)
*/
private Bitmap compressBitmapBySize(Bitmap src, int maxWidth, int maxHeight) {
if (src == null) return null;
int width = src.getWidth();
int height = src.getHeight();
float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
int newWidth = (int) (width * scale);
int newHeight = (int) (height * scale);
return Bitmap.createScaledBitmap(src, newWidth, newHeight, true);
}
/**
* 计算Bitmap采样率(备用方法,无实际调用)
*/
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* 从ImageView反射获取资源ID(备用方法,无实际调用)
*/
private int getResIdFromImageView(ImageView imageView) {
try {
Field field = ImageView.class.getDeclaredField("mSrcResource");
field.setAccessible(true);
return field.getInt(imageView);
} catch (NoSuchFieldException | IllegalAccessException e) {
LogUtils.e(TAG, "getResIdFromImageView: 反射失败", e);
return 0;
}
}
/**
* 更新二维码布局显示状态
*/
private void updateStoreQrcodeLayoutVisibility(ADsMode mode) {
if (rlWinbollStore == null) return;
rlWinbollStore.setVisibility(mode == ADsMode.STORE_QRCODE ? View.VISIBLE : View.GONE);
}
/**
* 清理SP中的隐私协议状态
*/
public static void cleanPrivacyStatus(Context context) {
if (context == null) {
LogUtils.e(TAG, "cleanPrivacyStatus: Context is null");
return;
}
SharedPreferences sp = getPrivacySharedPreferences(context);
sp.edit().remove(PRIVACY_VALUE).apply();
LogUtils.i(TAG, "隐私协议状态清理成功");
ToastUtils.show("隐私协议状态已清理");
}
/**
* 获取隐私协议SP实例
*/
private static SharedPreferences getPrivacySharedPreferences(Context context) {
Context appContext = context.getApplicationContext();
if (appContext != null) {
return appContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
}
return context.getSharedPreferences(PRIVACY_VALUE, Context.MODE_PRIVATE);
}
/**
* 处理隐私协议逻辑
*/
private static void handlePrivacyLogic(final Activity activity, PrivacyAgreeStatus privacyAgreeStatus, final OnPrivacyChangeListener onPrivacyChangeListener) {
if (privacyAgreeStatus == PrivacyAgreeStatus.REJECTED) {
Toast.makeText(activity.getApplicationContext(), "已拒绝隐私协议,广告已处于不可用状态", Toast.LENGTH_SHORT).show();
return;
} else if (privacyAgreeStatus == PrivacyAgreeStatus.AGREED) {
initMimoSdkStatic(activity.getApplicationContext());
return;
} else {
AlertDialog dialog = createPrivacyDialog(activity, onPrivacyChangeListener);
Window window = dialog.getWindow();
if (window != null) {
window.setGravity(Gravity.BOTTOM);
WindowManager m = activity.getWindowManager();
Display d = m.getDefaultDisplay();
WindowManager.LayoutParams p = window.getAttributes();
p.width = d.getWidth();
window.setAttributes(p);
}
dialog.show();
}
}
/**
* 初始化米盟SDK
*/
private static void initMimoSdkStatic(Context appContext) {
if (appContext == null) return;
try {
MimoSdk.init(appContext, new MimoCustomController() {
@Override
public boolean isCanUseLocation() {
return true;
}
@Override
public MimoLocation getMimoLocation() {
return null;
}
@Override
public boolean isCanUseWifiState() {
return true;
}
@Override
public boolean alist() {
return true;
}
}, new MimoSdk.InitCallback() {
@Override
public void success() {
LogUtils.d(TAG, "米盟SDK初始化成功");
}
@Override
public void fail(int code, String msg) {
LogUtils.e(TAG, "米盟SDK初始化失败:" + code + ", " + msg);
}
});
MimoSdk.setDebugOn(true);
} catch (Exception e) {
LogUtils.e(TAG, "米盟SDK初始化异常", e);
}
}
/**
* 静态方法:更新SP中的模式
*/
public static void updateAdsModeByStatic(Context context, ADsMode mode) {
if (context == null || mode == null) return;
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_SELECTED_MODE, mode.name()).apply();
InternalHandler.sendUpdateModeMessage(mode);
}
/**
* 静态方法:读取SP中的模式
*/
public static ADsMode getAdsModeFromStatic(Context context) {
if (context == null) return ADsMode.STANDALONE;
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String savedModeStr = sp.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
return ADsMode.fromValue(savedModeStr);
}
/**
* 注册控件实例
*/
private static void registerControlView(ADsControlView view) {
synchronized (sControlViews) {
if (!sControlViews.contains(view)) {
sControlViews.add(view);
}
}
}
/**
* 移除控件实例
*/
private static void unregisterControlView(ADsControlView view) {
synchronized (sControlViews) {
sControlViews.remove(view);
}
}
/**
* 设置选中模式
*/
private void setSelectedMode(final ADsMode mode) {
final ADsMode mode2 = (mode == null) ? ADsMode.STANDALONE : mode;
if (Looper.myLooper() == Looper.getMainLooper()) {
if (mode2 == ADsMode.STANDALONE) {
rbStandalone.setChecked(true);
} else if (mode2 == ADsMode.MIMO_SDK) {
rbMimoSDK.setChecked(true);
} else if (mode2 == ADsMode.STORE_QRCODE) {
rbStoreQrcode.setChecked(true);
}
updateStoreQrcodeLayoutVisibility(mode2);
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
setSelectedMode(mode2);
}
});
}
}
/**
* 获取选中模式
*/
public ADsMode getSelectedMode() {
int checkedId = rgADsMode.getCheckedRadioButtonId();
if (checkedId == R.id.rb_mimo_sdk) {
return ADsMode.MIMO_SDK;
} else if (checkedId == R.id.rb_store_qrcode) {
return ADsMode.STORE_QRCODE;
} else {
return ADsMode.STANDALONE;
}
}
/**
* 设置外部监听
*/
public void setOnAdsModeSelectedListener(OnAdsModeSelectedListener listener) {
this.listener = listener;
}
/**
* 内部Handler类
*/
private static class InternalHandler extends Handler {
static volatile InternalHandler _InternalHandler;
public InternalHandler(Looper looper) {
super(looper);
_InternalHandler = this;
}
public static void sendUpdateModeMessage(ADsMode mode) {
if (mode == null || _InternalHandler == null) return;
Message msg = _InternalHandler.obtainMessage();
msg.what = MSG_UPDATE_MODE;
msg.obj = mode;
_InternalHandler.sendMessage(msg);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_UPDATE_MODE) {
ADsMode mode = (ADsMode) msg.obj;
if (mode == null) return;
synchronized (sControlViews) {
for (ADsControlView view : sControlViews) {
if (view != null && view.isShown() && view.isAttachedToWindow()) {
view.setSelectedMode(mode);
view.updateStoreQrcodeLayoutVisibility(mode);
}
}
}
}
}
}
/**
* 生命周期:控件销毁
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
unregisterControlView(this);
}
/**
* 外部监听接口
*/
public interface OnAdsModeSelectedListener {
void onModeSelected(ADsMode selectedMode);
}
/**
* 隐私协议监听接口
*/
public interface OnPrivacyChangeListener {
void onAgreePrivacy();
void onDisagreePrivacy();
}
/**
* 创建隐私协议对话框
*/
private static AlertDialog createPrivacyDialog(final Activity activity, final OnPrivacyChangeListener onPrivacyChangeListener) {
View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_privacy_agreement, null);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(dialogView).setCancelable(false);
final AlertDialog dialog = builder.create();
final TextView tvPrivacyUrl = (TextView) dialogView.findViewById(R.id.tv_privacy_url);
Button btnAgree = (Button) dialogView.findViewById(R.id.btn_agree);
Button btnDisagree = (Button) dialogView.findViewById(R.id.btn_disagree);
tvPrivacyUrl.setText(Html.fromHtml("<u>" + tvPrivacyUrl.getText().toString() + "</u>"));
tvPrivacyUrl.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String url = tvPrivacyUrl.getText().toString().trim();
ToastUtils.show("隐私协议链接:" + url);
}
});
tvPrivacyUrl.setClickable(true);
tvPrivacyUrl.setFocusable(true);
btnAgree.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onPrivacyChangeListener != null) {
onPrivacyChangeListener.onAgreePrivacy();
}
dialog.dismiss();
}
});
btnDisagree.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onPrivacyChangeListener != null) {
onPrivacyChangeListener.onDisagreePrivacy();
}
dialog.dismiss();
}
});
return dialog;
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 28 20:55:33 HKT 2026
stageCount=35
#Wed May 27 14:51:29 HKT 2026
stageCount=23
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.34
publishVersion=15.20.22
buildCount=0
baseBetaVersion=15.20.35
baseBetaVersion=15.20.23
+1 -1
View File
@@ -8,7 +8,7 @@
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".CrashActivity"
android:name=".CrashHandler$CrashActivity"
android:label="CrashActivity"
android:launchMode="singleInstance"
android:process=":CrashActivity"/>
@@ -1,109 +0,0 @@
package cc.winboll.studio.libappbase;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
public final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
private static final int MENUITEM_COPY = 0;
private static final int MENUITEM_RESTART = 1;
private String mLog;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
mLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
initLayout();
}
private void initLayout() {
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(0xFFF5F5F5);
TextView message = new TextView(this);
final int padding = dp2px(16);
message.setPadding(padding, padding, padding, padding);
message.setText(mLog);
message.setTextColor(0xFF000000);
message.setTextIsSelectable(true);
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
getActionBar().setTitle(CrashHandler.TITTLE);
getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()) + " Error");
}
@Override
public void onBackPressed() {
restartApp();
}
private void restartApp() {
final Intent intent = getPackageManager()
.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
menu.add(0, MENUITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(0, MENUITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
@Override
public boolean onMenuItemClick(final MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restartApp();
break;
default:
break;
}
return false;
}
}
@@ -1,13 +1,27 @@
package cc.winboll.studio.libappbase;
import android.app.Activity;
import android.app.Application;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
@@ -206,5 +220,111 @@ public final class CrashHandler {
}
}
// ====================== 内部Activity页面 ======================
/**
* 基础极简崩溃页面
* 保险丝熔断时启动,避免复杂布局二次崩溃
*/
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
private static final int MENUITEM_COPY = 0;
private static final int MENUITEM_RESTART = 1;
private String mLog;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
initLayout();
}
/**
* 动态初始化布局
*/
private void initLayout() {
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(0xFFF5F5F5);
TextView message = new TextView(this);
final int padding = dp2px(16);
message.setPadding(padding, padding, padding, padding);
message.setText(mLog);
message.setTextColor(0xFF000000);
message.setTextIsSelectable(true);
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
getActionBar().setTitle(TITTLE);
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
}
@Override
public void onBackPressed() {
restartApp();
}
/**
* 重启应用
*/
private void restartApp() {
final Intent intent = getPackageManager()
.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* dp转px
*/
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
menu.add(0, MENUITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(0, MENUITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
@Override
public boolean onMenuItemClick(final MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restartApp();
break;
default:
break;
}
return false;
}
}
}
@@ -6,10 +6,6 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -129,32 +125,17 @@ public class GlobalApplication extends Application {
*/
@Override
public void onCreate() {
try {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
restoreDebugStatus();
// 初始化基础组件(日志、崩溃处理、Toast)
initCoreComponents();
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
restoreDebugStatus();
// 初始化基础组件(日志、崩溃处理、Toast)
initCoreComponents();
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
}
/**
@@ -209,7 +190,7 @@ public class GlobalApplication extends Application {
*/
public static String getAppName(Context context) {
if (context == null) {
Log.w(TAG, "getAppName: 上下文为空,返回 null");
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
return null;
}
PackageManager packageManager = context.getPackageManager();
@@ -225,7 +206,8 @@ public class GlobalApplication extends Application {
return appName;
} catch (NameNotFoundException e) {
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
Log.e(TAG, "获取应用名称失败:包名不存在", e);
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace();
}
return null;
@@ -66,7 +66,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
final Intent intent = new Intent();
intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, mCrashLog);
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashActivity.class);
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashHandler.CrashActivity.class);
StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder("GlobalCrashActivity onCreate StackTrace");
+13 -13
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<!-- Theme removed for compatibility -->
android:theme="?attr/themeDebug"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -12,7 +12,7 @@
android:layout_width="match_parent"
android:layout_height="34dp"
android:layout_alignParentTop="true"
android:background=" android:background="@color/toolbarBackgroundColor""
android:background="?attr/colorTittleBackgound"
android:id="@+id/viewlogRelativeLayoutToolbar">
<Button
@@ -20,8 +20,8 @@
android:layout_height="@dimen/log_button_height"
android:textSize="@dimen/log_text_size"
android:text="Clean"
android:textColor=" android:textColor="@color/mainWindowTextColor""
android:background=" android:background="@color/toolbarBackgroundColor""
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:layout_centerVertical="true"
android:id="@+id/viewlogButtonClean"
android:layout_marginLeft="5dp"/>
@@ -35,7 +35,7 @@
android:layout_toRightOf="@+id/viewlogButtonClean"
android:layout_centerVertical="true"
android:id="@+id/viewlogTextView1"
android:background=" android:background="@color/toolbarBackgroundColor""
android:background="?attr/colorTittleBackgound"
android:textColor="?attr/toolbarTextColor"/>
<cc.winboll.studio.libappbase.widget.LogTagSpinner
@@ -53,7 +53,7 @@
android:layout_toLeftOf="@+id/viewlogButtonCopy"
android:layout_centerVertical="true"
android:text="Selectable"
android:background=" android:background="@color/toolbarBackgroundColor""
android:background="?attr/colorTittleBackgound"
android:id="@+id/viewlogCheckBoxSelectable"
android:padding="@dimen/log_text_padding"
android:textColor="?attr/toolbarTextColor"/>
@@ -62,8 +62,8 @@
android:layout_width="@dimen/log_button_width"
android:layout_height="@dimen/log_button_height"
android:textSize="@dimen/log_text_size"
android:textColor=" android:textColor="@color/mainWindowTextColor""
android:background=" android:background="@color/toolbarBackgroundColor""
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:text="Copy"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
@@ -79,7 +79,7 @@
android:layout_below="@+id/viewlogRelativeLayoutToolbar"
android:id="@+id/viewlogLinearLayout1"
android:gravity="center_vertical"
android:background=" android:background="@color/toolbarBackgroundColor"">
android:background="?attr/colorTittleBackgound">
<CheckBox
android:layout_width="wrap_content"
@@ -88,7 +88,7 @@
android:text="ALL"
android:padding="2dp"
android:id="@+id/viewlogCheckBox1"
android:background=" android:background="@color/toolbarBackgroundColor""
android:background="?attr/colorTittleBackgound"
android:textColor="?attr/toolbarTextColor"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"/>
@@ -99,14 +99,14 @@
android:layout_height="@dimen/log_button_height"
android:textSize="@dimen/log_text_size"
android:textColor="?attr/colorText"
android:background=" android:background="@color/toolbarBackgroundColor""
android:background="?attr/colorTittleBackgound"
android:singleLine="true"
android:id="@+id/tagsearch_et"/>
<HorizontalScrollView
android:layout_width="0dp"
android:layout_height="match_parent"
android:background=" android:background="@color/toolbarBackgroundColor""
android:background="?attr/colorTittleBackgound"
android:scrollbars="none"
android:padding="2dp"
android:layout_weight="1.0"
@@ -132,7 +132,7 @@
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorTextBackgound"
android:background="?attr/colorTextBackgound"
android:id="@+id/viewlogScrollViewLog">
<TextView
-1
View File
@@ -1 +0,0 @@
/build
-37
View File
@@ -1,37 +0,0 @@
# WinBoLL
#### 介绍
WinBoLL 网站浏览器。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh winboll
#### 使用说明
3. Termux应用配置:
- 已安装Termux(包名 com.termux );
- 执行  echo "allow-external-apps = true" > ~/.termux/termux.properties
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档
-1
View File
@@ -1 +0,0 @@
-110
View File
@@ -1,110 +0,0 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.mywinboll"
minSdkVersion 26
targetSdkVersion 30
//1. Android 官方规则
//-  versionCode  类型:int 整型
//- Java int 最大值:2147483647
versionCode 1520000
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.20"
if(true) {
versionName = genVersionName("${versionName}")
}
}
}
dependencies {
api 'com.google.code.gson:gson:2.10.1'
// 下拉控件
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
// AndroidX 类库
/*api 'androidx.appcompat:appcompat:1.1.0'
//api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'*/
// 米盟
//api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意:以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
/*
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
implementation 'com.termux:terminal-emulator:0.118.0'
implementation 'com.termux:terminal-view:0.118.0'
implementation 'com.termux:termux-shared:0.118.0'
// Biometric (指纹识别)
implementation 'androidx.biometric:biometric:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libappbase:15.20.34'
api 'cc.winboll.studio:libaes:15.20.17'
// 备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.33'
//api 'com.github.ZhanGSKen:libaes:aes-v15.20.16'
api fileTree(dir: 'libs', include: ['*.jar'])
}
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Jul 01 16:36:17 CST 2026
stageCount=0
libraryProject=
baseVersion=15.20
publishVersion=15.20.0
buildCount=11
baseBetaVersion=15.20.1
-137
View File
@@ -1,137 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类(避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件(避免资源ID混淆)
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法(避免JNI调用失败)
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
-keep class com.miui.zeus.** { *; }
-keep interface com.miui.zeus.** { *; }
# 保留米盟日志字段(便于广告加载失败排查)
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
public static final java.lang.String TAG;
}
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MyWinBoLL +</string>
</resources>
-37
View File
@@ -1,37 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.mywinboll">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>
@@ -1,358 +0,0 @@
package cc.winboll.studio.mywinboll;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
try {
super.onCreate();
// 初始化主题样式ID列表
AESThemeUtil.init(null);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
}
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
public void registerGlobal(Context context, String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
}
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
}
}
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
e.printStackTrace();
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable(){
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
try {
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
writeLog(e.toString());
}
throwable.printStackTrace();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) builder.append("\n");
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(Log.getStackTraceString(throwable));
return builder.toString();
}
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
} catch (Throwable e) {
e.printStackTrace();
}
}
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.copy:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}
@@ -1,62 +0,0 @@
package cc.winboll.studio.mywinboll;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
LogView mLogView;
@Override
protected void onCreate(Bundle savedInstanceState) {
AESThemeUtil.applyAppCompatTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mLogView = findViewById(R.id.logview);
ToastUtils.show("onCreate");
}
@Override
protected void onResume() {
super.onResume();
mLogView.start();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
AESThemeUtil.inflateCompatThemeMenu(this, menu);
if (GlobalApplication.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
}
getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_drawerbase, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) {
recreate();
}
if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
@@ -1,43 +0,0 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:gravity="center_vertical|center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MyWinBoLL"
android:textAppearance="?android:attr/textAppearanceLarge"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/logview"/>
</LinearLayout>
</LinearLayout>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#42A5F5</color>
<color name="colorPrimaryDark">#1565C0</color>
<color name="colorAccent">#FFD740</color>
<color name="mainWindowBackgroundColor">#121212</color>
<color name="mainWindowTextColor">#E0E0E0</color>
<color name="toolbarTextColor">#FFFFFF</color>
<color name="toolbarBackgroundColor">#1E3A5F</color>
<color name="debugTextColor">#00E676</color>
<color name="colorTextBackgound">#121212</color>
</resources>
@@ -1,29 +0,0 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowDarkTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowDarkTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowDarkTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#1565C0</color>
<color name="colorPrimaryDark">#0D47A1</color>
<color name="colorAccent">#FF6F00</color>
<color name="mainWindowBackgroundColor">#FAFAFA</color>
<color name="mainWindowTextColor">#212121</color>
<color name="toolbarTextColor">#FFFFFF</color>
<color name="toolbarBackgroundColor">#1565C0</color>
<color name="debugTextColor">#616161</color>
<color name="colorTextBackgound">#FAFAFA</color>
</resources>
@@ -1,4 +0,0 @@
<resources>
<string name="app_name">MyWinBoLL</string>
</resources>
-37
View File
@@ -1,37 +0,0 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowDarkTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
<item name="colorTittle">?attr/mainWindowTextColor</item>
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>
-1
View File
@@ -1 +0,0 @@
/build
-112
View File
@@ -1,112 +0,0 @@
# PowerBell
#### 介绍
一个接收手机电量信息的应用,当电量值达到设定范围时会提醒用户。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 gradle assembleStageRelease
#### 使用说明
在安卓系统中需要设置两个权限允许。
1.自启动权限允许。
2.省电策略-无限制权限允许。
3.设置背景图片需要读写手机存储权限。
4.要在锁屏充电的时候提醒,还需要设置允许锁屏通知权限。
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@qq.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档
AndroidManifest.xml详解
https://www.jianshu.com/p/3b5b89d4e154
CrashHandler自定义异常处理
https://www.jianshu.com/p/9a3d800a429a
Android用Intent启动Activity的方法
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
Android开发:最详细的 Toolbar 开发实践总结
https://www.jianshu.com/p/79604c3ddcae
Using the App Toolbar
https://guides.codepath.com/android/using-the-app-toolbar
Android的onCreateOptionsMenu()创建菜单Menu详解
https://www.cnblogs.com/spring87/p/4312538.html
Android通知栏-Notification(通知消息)
https://blog.csdn.net/qq_35507234/article/details/90676587
android之PendingIntent的使用
https://blog.csdn.net/qq_16628781/article/details/51548324
change seekbar color android” Code Answer
https://www.codegrepper.com/code-examples/whatever/change+seekbar+color+android
如何选择开源项目许可证
https://www.zhihu.com/question/28292322
Android最简单的自定义布局Notification
https://blog.csdn.net/acesheep_911/article/details/81458784?utm_medium=distribute.wap_relevant.none-task-blog-2~default~baidujs_title~default-0.wap_blog_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3
Android中通知栏Notification详解以及自定义Notification
https://blog.csdn.net/daitu_liang/article/details/50246803
Android 图像系列: 将本地图片加载到Drawable
https://blog.csdn.net/qzone123222/article/details/7930035
android 从相册选择,Android开发从相册中选取照片
https://blog.csdn.net/weixin_42146086/article/details/117570917
Android 任务栈简介
https://blog.csdn.net/qq_34368586/article/details/107653912
Android用Intent启动Activity的方法
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
Android中使用dimen定义尺寸
https://blog.csdn.net/yuzhiboyi/article/details/7696174
declare-styleable:自定义控件的属性
https://blog.csdn.net/congqingbin/article/details/7869730
安卓自定义滑动解锁控件
https://blog.csdn.net/lp506954774/article/details/72677018
Android 添加菜单和返回按钮
https://blog.csdn.net/my_tiantian/article/details/77822173
Android Button的基本使用
https://www.cnblogs.com/yishaochu/p/5783605.html
Android应用中实现系统“分享”接口
https://blog.csdn.net/lowprofile_coding/article/details/37656255
Android 关于mimeType的使用
https://blog.csdn.net/dorytmx/article/details/80972248
使用GitHub Actions实现Android自动打包apk
https://blog.csdn.net/ZZL23333/article/details/115798615?app_version=6.0.0&code=app_1562916241&csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22115798615%22%2C%22source%22%3A%22weixin_38986226%22%7D&uLinkId=usr1mkqgl919blen&utm_source=app
-1
View File
@@ -1 +0,0 @@
-100
View File
@@ -1,100 +0,0 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
// 确保 Java 7 兼容性(已适配项目技术栈)
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
applicationId "cc.winboll.studio.powerbell"
minSdkVersion 26
targetSdkVersion 30
versionCode 7
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.15"
if(true) {
versionName = genVersionName("${versionName}")
}
}
packagingOptions {
exclude 'META-INF/NOTICE.md'
exclude 'META-INF/LICENSE.md'
}
}
dependencies {
//注意:以下5个库必须要引入
//api 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// uCrop 核心依赖(最新稳定版)
implementation 'com.github.yalantis:ucrop:2.2.8'
// 兼容AndroidX(若项目用AndroidX,必须添加)
//implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// AndroidX 类库
api 'androidx.appcompat:appcompat:1.1.0'
api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.20.21'
api 'cc.winboll.studio:libappbase:15.20.34'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.15.2'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])
// JavaMailAPPMSGMailUtils 发送邮件依赖,Android 专用版)
implementation 'com.sun.mail:android-mail:1.6.7'
implementation 'com.sun.mail:android-activation:1.6.7'
}
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Jul 18 10:40:59 HKT 2026
stageCount=27
libraryProject=
baseVersion=15.15
publishVersion=15.15.26
buildCount=0
baseBetaVersion=15.15.27
-279
View File
@@ -1,279 +0,0 @@
#!/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已生成 ===="
-151
View File
@@ -1,151 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类(避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件(避免资源ID混淆)
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法(避免JNI调用失败)
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# JavaMail / SMTP 邮件发送(APPMSGMailUtils 依赖,防止 R8 混淆导致 SMTP 发送失败)
-keep class javax.mail.** { *; }
-keep class javax.mail.internet.** { *; }
-keep class javax.activation.** { *; }
-keep class com.sun.mail.** { *; }
-keep class com.sun.mail.util.** { *; }
-keep class com.sun.mail.smtp.** { *; }
-keep class com.sun.mail.pop3.** { *; }
-keep class com.sun.mail.imap.** { *; }
-dontwarn javax.mail.**
-dontwarn javax.activation.**
-dontwarn com.sun.mail.**
-dontwarn java.awt.**
-dontwarn javax.naming.**
-dontwarn javax.net.ssl.**
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# ============================== 必要补充规则 ==============================
# OkHttp 4.4.1 补充规则(Java 7 兼容)
-keep class okhttp3.internal.concurrent.** { *; }
-keep class okhttp3.internal.connection.** { *; }
-dontwarn okhttp3.internal.concurrent.TaskRunner
-dontwarn okhttp3.internal.connection.RealCall
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# RecyclerView 1.0.0
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
tools:replace="android:icon"
android:icon="@drawable/ic_launcher_beta">
</application>
</manifest>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name_cn1">能源钟★</string>
<string name="app_name_cn2">泡额呗额☆</string>
</resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">PowerBell+</string>
</resources>
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<shortcut
android:shortcutId="switchto_en1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_en1"
android:shortcutLongLabel="@string/switchto_en1"
android:shortcutDisabledMessage="@string/en1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_en1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<!--<shortcut
android:shortcutId="switchto_cn1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn1"
android:shortcutLongLabel="@string/switchto_cn1"
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>-->
<shortcut
android:shortcutId="switchto_cn2"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn2"
android:shortcutLongLabel="@string/switchto_cn2"
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn2" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<shortcut
android:shortcutId="switchto_en1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_en1"
android:shortcutLongLabel="@string/switchto_en1"
android:shortcutDisabledMessage="@string/en1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_en1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="switchto_cn1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn1"
android:shortcutLongLabel="@string/switchto_cn1"
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<!--<shortcut
android:shortcutId="switchto_cn2"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn2"
android:shortcutLongLabel="@string/switchto_cn2"
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn2" />
<categories android:name="android.shortcut.conversation" />
</shortcut>-->
</shortcuts>
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<!--<shortcut
android:shortcutId="switchto_en1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_en1"
android:shortcutLongLabel="@string/switchto_en1"
android:shortcutDisabledMessage="@string/en1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_en1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>-->
<shortcut
android:shortcutId="switchto_cn1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn1"
android:shortcutLongLabel="@string/switchto_cn1"
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="switchto_cn2"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn2"
android:shortcutLongLabel="@string/switchto_cn2"
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn2" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>
-332
View File
@@ -1,332 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- 此应用可显示在其他应用上方 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 运行“specialUse”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- 显示通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- PACKAGE_USAGE_STATS -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<!-- 请求忽略电池优化 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<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.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"
android:process=":main"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme_Default"
android:persistent="true"
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:process=":main"
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:launchMode="singleTask"/>
<activity-alias
android:name=".MainActivityEN1"
android:targetActivity=".MainActivity"
android:exported="true"
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
android:name=".MainActivityCN1"
android:targetActivity=".MainActivity"
android:exported="true"
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
android:name=".MainActivityCN2"
android:targetActivity=".MainActivity"
android:exported="true"
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:process=":main"
android:name=".activities.CrashActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask"
android:exported="false"/>
<activity
android:process=":main"
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:process=":main"
android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.SettingsActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
android:exported="false"/>
<activity
android:process=":main"
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"/>
<receiver
android:process=":main"
android:name=".receivers.MainReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.POWER_CONNECTED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>
<service
android:name=".services.ControlCenterService"
android:priority="1000"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活"/>
</service>
<service
android:name=".services.AssistantService"
android:enabled="true"
android:exported="false"
android:process=":assistant"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行"/>
</service>
<service
android:name=".services.TTSPlayService"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<service
android:name=".services.ThoughtfulService"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
android:process=":main">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
</application>
</manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

@@ -1,319 +0,0 @@
package cc.winboll.studio.powerbell;
import android.content.Context;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
/**
* 应用全局入口类
* 适配:Java7 语法规范 | Android API30 系统版本
* 核心策略:极致强制缓存 - 无论内存紧张程度,永不自动清理任何缓存(Bitmap/视图控件/路径记录)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025-12-29 15:30:00
* @LastModified 2026-01-02 19:01:00
*/
public class App extends GlobalApplication {
// ====================================== 常量区 - 置顶排序 (按功能归类) ======================================
// 基础日志TAG
private static final String TAG = "App";
// 缓存保护专用TAG
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
// 电池无效值常量(修复拼写错误:INVALID_BATTERY_VALUE
private static final int INVALID_BATTERY_VALUE = -1;
// 组件跳转常量
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";
// ====================================== 静态属性区 - 全局单例/状态 (按核心程度排序) ======================================
// 应用单例
private static App sApp;
// 配置与缓存工具 (全局单例)
public static AppConfigUtils sAppConfigUtils;
private static AppCacheUtils sAppCacheUtils;
// 资源与视图缓存 (强制驻留,极致缓存核心)
public static BackgroundSourceUtils sBackgroundSourceUtils;
public static BitmapCacheUtils sBitmapCacheUtils;
private static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// 系统状态 (电池电量)
public static volatile int sQuantityOfElectricity = INVALID_BATTERY_VALUE;
// 系统工具 (通知管理器)
private static NotificationManagerUtils sNotificationManagerUtils;
// ====================================== 成员属性区 - 非静态成员 (广播接收器) ======================================
private GlobalApplicationReceiver mGlobalReceiver;
// ====================================== 公共静态方法 - 单例/工具获取 (对外入口) ======================================
/**
* 获取应用全局单例实例
* @return 应用单例App实例
*/
public static App getInstance() {
LogUtils.d(TAG, "【getInstance】应用单例获取方法调用 | 当前实例:" + sApp);
return sApp;
}
/**
* 获取配置工具类单例实例
* @param context 上下文对象
* @return 配置工具类AppConfigUtils实例
*/
public static AppConfigUtils getAppConfigUtils(Context context) {
String contextClass = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "【getAppConfigUtils】配置工具获取方法调用 | 入参Context类型:" + contextClass);
if (sAppConfigUtils == null) {
sAppConfigUtils = AppConfigUtils.getInstance(context);
LogUtils.d(TAG, "【getAppConfigUtils】配置工具实例为空,已初始化新实例");
}
return sAppConfigUtils;
}
/**
* 获取缓存工具类单例实例
* @param context 上下文对象
* @return 缓存工具类AppCacheUtils实例
*/
public static AppCacheUtils getAppCacheUtils(Context context) {
String contextClass = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "【getAppCacheUtils】缓存工具获取方法调用 | 入参Context类型:" + contextClass);
if (sAppCacheUtils == null) {
sAppCacheUtils = AppCacheUtils.getInstance(context);
LogUtils.d(TAG, "【getAppCacheUtils】缓存工具实例为空,已初始化新实例");
}
return sAppCacheUtils;
}
// ====================================== 公共成员方法 - 业务逻辑 (实例方法) ======================================
/**
* 清除电池历史数据
*/
public void clearBatteryHistory() {
LogUtils.d(TAG, "【clearBatteryHistory】清除电池历史数据方法调用");
if (sAppCacheUtils != null) {
sAppCacheUtils.clearBatteryHistory();
LogUtils.d(TAG, "【clearBatteryHistory】电池历史数据清除成功");
} else {
LogUtils.w(TAG, "【clearBatteryHistory】电池历史数据清除失败 | 缓存工具实例sAppCacheUtils为空");
}
}
/**
* 获取视图缓存实例
* @return 视图缓存MemoryCachedBackgroundView实例
*/
public MemoryCachedBackgroundView getMemoryCachedBackgroundView() {
LogUtils.d(TAG, "【getMemoryCachedBackgroundView】视图缓存获取方法调用 | 当前实例:" + sMemoryCachedBackgroundView);
return sMemoryCachedBackgroundView;
}
// ====================================== 公共静态方法 - 业务逻辑 (全局工具方法) ======================================
/**
* 手动清理所有缓存(仅主动调用生效,符合极致缓存策略)
*/
public static void manualClearAllCache() {
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存方法调用 | 仅主动触发生效");
// 清理Bitmap缓存
if (sBitmapCacheUtils != null) {
sBitmapCacheUtils.clearAllCache();
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】Bitmap缓存已清理");
}
// 仅置空视图缓存引用,不销毁实例(极致缓存策略)
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】视图缓存引用已置空 | 实例保留");
sMemoryCachedBackgroundView = null;
}
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存操作完成");
}
/**
* 发送通知消息(仅调试模式下生效)
* @param title 通知标题
* @param content 通知内容
*/
public static void notifyMessage(String title, String content) {
LogUtils.d(TAG, "【notifyMessage】发送通知消息方法调用 | 标题:" + title + " | 内容:" + content);
boolean canSend = isDebugging() && sApp != null && sNotificationManagerUtils != null;
if (canSend) {
NotificationMessage message = new NotificationMessage(title, content, "");
sNotificationManagerUtils.showMessageNotification(sApp, message);
LogUtils.d(TAG, "【notifyMessage】通知消息发送成功");
} else {
LogUtils.d(TAG, "【notifyMessage】通知消息发送失败 | 条件不满足:调试模式=" + isDebugging() + " | 应用实例=" + (sApp != null) + " | 通知工具=" + (sNotificationManagerUtils != null));
}
}
// ====================================== 生命周期方法 - 应用全局生命周期 (重写父类方法) ======================================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "【onCreate】应用启动生命周期方法调用 | 开始初始化应用...");
// 初始化应用单例
sApp = this;
LogUtils.d(TAG, "【onCreate】应用单例已初始化");
// 初始化核心组件
initBaseTools();
initUtils();
initReceiver();
LogUtils.d(TAG, "【onCreate】应用初始化完成 | 极致强制缓存策略已激活");
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "【onTerminate】应用终止生命周期方法调用 | 开始释放非缓存资源...");
// 释放非缓存资源
ToastUtils.release();
releaseNotificationManager();
releaseReceiver();
// 核心策略:不清理任何缓存
LogUtils.w(CACHE_PROTECT_TAG, "【onTerminate】极致缓存策略生效 | 所有缓存将保留在内存中");
LogUtils.d(TAG, "【onTerminate】非缓存资源释放完成");
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
LogUtils.w(CACHE_PROTECT_TAG, "【onTrimMemory】系统内存修剪回调 | 内存等级:" + level + " | 忽略修剪,缓存强制保护");
logDetailedCacheStatus();
}
@Override
public void onLowMemory() {
super.onLowMemory();
LogUtils.w(CACHE_PROTECT_TAG, "【onLowMemory】系统低内存回调 | 极致缓存策略生效 | 不执行任何缓存清理操作");
logDetailedCacheStatus();
}
// ====================================== 私有初始化方法 - 组件初始化 (按依赖顺序排序) ======================================
/**
* 初始化基础工具类(Activity管理、Toast、通知管理器)
*/
private void initBaseTools() {
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化开始...");
WinBoLLActivityManager.init(this);
ToastUtils.init(this);
sNotificationManagerUtils = new NotificationManagerUtils(this);
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化完成");
}
/**
* 初始化核心工具与缓存(极致强制驻留,缓存核心)
*/
private void initUtils() {
LogUtils.d(TAG, "【initUtils】核心工具与缓存初始化开始 | 极致缓存策略激活");
// 1. 配置与基础缓存工具初始化
sAppConfigUtils = getAppConfigUtils(this);
sAppCacheUtils = getAppCacheUtils(this);
// 2. 资源与Bitmap缓存工具初始化(永久驻留)
sBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this);
sBackgroundSourceUtils.loadSettings();
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "【initUtils】资源与Bitmap缓存工具初始化完成 | 永久驻留内存");
// 3. 视图缓存初始化(永久驻留,无实例则创建)
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
if (sMemoryCachedBackgroundView == null) {
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, sBackgroundSourceUtils.getCurrentBackgroundBean(), true);
LogUtils.d(TAG, "【initUtils】视图缓存无现有实例,已创建新实例");
}
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】全局广播接收器释放完成");
}
}
/**
* 释放通知管理器资源
*/
private void releaseNotificationManager() {
LogUtils.d(TAG, "【releaseNotificationManager】通知管理器资源释放开始...");
if (sNotificationManagerUtils != null) {
sNotificationManagerUtils.release();
sNotificationManagerUtils = null;
LogUtils.d(TAG, "【releaseNotificationManager】通知管理器资源释放完成");
}
}
// ====================================== 私有辅助方法 - 日志/工具 (辅助功能) ======================================
/**
* 记录当前缓存详细状态(用于调试监控,极致缓存策略监控)
*/
private void logDetailedCacheStatus() {
LogUtils.d(TAG, "【logDetailedCacheStatus】缓存状态监控日志开始...");
// Bitmap缓存状态
if (sBitmapCacheUtils != null) {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 有效");
try {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】Bitmap缓存数量:" + sBitmapCacheUtils.getCacheCount());
} catch (Exception e) {
LogUtils.e(CACHE_PROTECT_TAG, "【缓存详情】获取Bitmap缓存数量失败", e);
}
} else {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 未初始化");
}
// 视图缓存状态
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 有效");
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】视图实例数量:" + MemoryCachedBackgroundView.getInstanceCount());
} else {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 引用已置空(实例可能保留)");
}
}
}
@@ -1,590 +0,0 @@
package cc.winboll.studio.powerbell;
import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.activities.AboutActivity;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
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.models.BackgroundBean;
import cc.winboll.studio.powerbell.models.BatteryStyle;
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.ImageUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.views.BatteryStyleView;
import cc.winboll.studio.powerbell.views.MainContentView;
/**
* 应用核心主活动
* 功能:管理电池监控、背景设置、服务启停、权限申请等核心功能
* 适配:Java7 | API30 | 内存泄漏防护 | UI与服务状态实时同步
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class MainActivity extends WinBoLLActivity implements MainContentView.OnViewActionListener {
// ======================== 静态常量区(抽离魔法值,按功能分类)========================
public static final String TAG = "MainActivity";
private static final int REQUEST_BACKGROUND_SETTINGS_ACTIVITY = 1001;
public static final String EXTRA_ISRELOAD_BACKGROUNDVIEW = "EXTRA_ISRELOAD_BACKGROUNDVIEW";
public static final String EXTRA_ISRELOAD_ACCENTCOLOR = "EXTRA_ISRELOAD_ACCENTCOLOR";
// Handler 消息常量
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;
private static final int MSG_UPDATE_BATTERYDRAWABLE = 4;
// ======================== 静态成员区(全局共享,管控生命周期)========================
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 Drawable mFrameDrawable;
private Menu mMenu;
// ======================== 生命周期方法区(按系统调用顺序排列)========================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate() 调用 | savedInstanceState: " + savedInstanceState);
initGlobalHandler();
setContentView(R.layout.activity_main);
initPermissionUtils();
initMainContentView();
initCriticalView();
initCoreUtilsAsync();
// 处理首次启动参数
handleReloadBackgroundParam(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
LogUtils.d(TAG, "onNewIntent() 调用 | intent: " + intent);
// 关键:更新Activity持有的Intent,确保后续获取最新值
setIntent(intent);
// 统一处理刷新背景参数
handleReloadBackgroundParam(intent);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "onPostCreate() 调用 | savedInstanceState: " + savedInstanceState);
mPermissionUtils.startPermissionRequest(this);
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume() 调用");
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause() 调用");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy() 调用");
// 释放核心视图
if (mMainContentView != null) {
mMainContentView.releaseResources();
mMainContentView = null;
LogUtils.d(TAG, "onDestroy: 核心视图资源已释放");
}
// 销毁Handler防止内存泄漏
if (sGlobalHandler != null) {
sGlobalHandler.removeCallbacksAndMessages(null);
sGlobalHandler = null;
LogUtils.d(TAG, "onDestroy: 全局Handler已销毁");
}
// 释放Drawable
if (mFrameDrawable != null) {
mFrameDrawable.setCallback(null);
mFrameDrawable = null;
LogUtils.d(TAG, "onDestroy: 框架Drawable已释放");
}
// 置空所有引用,消除内存泄漏风险
sMainActivity = null;
mPermissionUtils = null;
mAppConfigUtils = null;
mBgSourceUtils = null;
mServiceControlBean = null;
mMenu = null;
mApplication = null;
mToolbar = 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_BACKGROUND_SETTINGS_ACTIVITY && sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
LogUtils.d(TAG, "onActivityResult: 发送背景加载消息");
}
}
// ======================== 菜单与导航方法区 ========================
@Override
public boolean onCreateOptionsMenu(Menu menu) {
LogUtils.d(TAG, "onCreateOptionsMenu() 调用 | menu: " + menu);
mMenu = menu;
AESThemeUtil.inflateThemeMenu(this, menu);
// 调试模式加载测试菜单
if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
LogUtils.d(TAG, "onCreateOptionsMenu: 已加载测试菜单");
}
getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
LogUtils.d(TAG, "onOptionsItemSelected() 调用 | itemId: " + item.getItemId());
// 主题切换处理
if (AESThemeUtil.onThemeItemSelected(this, item)) {
recreate();
Intent mainIntent = new Intent(MainActivity.this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_ACCENTCOLOR, true);
startActivity(mainIntent);
return true;
}
// 开发者功能处理
if (DevelopUtils.onDevelopItemSelected(this, item)) {
return true;
}
// 菜单点击事件分发
switch (item.getItemId()) {
case R.id.action_settings:
startActivity(new Intent(this, SettingsActivity.class));
break;
case R.id.action_battery_report:
startActivity(new Intent(this, BatteryReportActivity.class));
break;
case R.id.action_clearrecord:
startActivity(new Intent(this, ClearRecordActivity.class));
break;
case R.id.action_changepicture:
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_BACKGROUND_SETTINGS_ACTIVITY);
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:
startAboutActivity();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void setupToolbar() {
super.setupToolbar();
LogUtils.d(TAG, "setupToolbar() 调用");
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
LogUtils.d(TAG, "setupToolbar: 已隐藏返回按钮");
}
}
@Override
public void onBackPressed() {
LogUtils.d(TAG, "onBackPressed() 调用");
moveTaskToBack(true);
LogUtils.d(TAG, "onBackPressed: 应用已退至后台");
}
@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();
LogUtils.d(TAG, "initPermissionUtils: 权限工具类已初始化");
}
private void initGlobalHandler() {
LogUtils.d(TAG, "initGlobalHandler() 调用");
if (sGlobalHandler == null) {
sGlobalHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// Activity已销毁则跳过消息处理
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();
break;
case MSG_UPDATE_SERVICE_SWITCH:
sMainActivity.updateServiceSwitchUI();
break;
case MSG_UPDATE_BATTERYDRAWABLE:
sMainActivity.updateBatteryDrawable();
break;
}
}
};
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已创建");
} else {
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已存在,无需重复创建");
}
}
private void initMainContentView() {
LogUtils.d(TAG, "initMainContentView() 调用");
View rootView = findViewById(android.R.id.content);
mMainContentView = new MainContentView(this, rootView, this);
LogUtils.d(TAG, "initMainContentView: 核心内容视图已初始化");
}
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);
LogUtils.d(TAG, "initCriticalView: 工具栏已设置标题样式");
}
LogUtils.d(TAG, "initCriticalView: 工具栏已设置标题样式");
}
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);
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置不存在,已创建默认配置");
}
// 根据配置启停服务
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());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已启动");
}
});
} else if (!isServiceEnable && isServiceAlive) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ControlCenterService.stopControlCenterService(getApplicationContext());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已停止");
}
});
}
// 主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isFinishing() || isDestroyed()) {
LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁,跳过UI更新");
return;
}
// 适配API30,兼容低版本Drawable加载
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);
LogUtils.d(TAG, "initCoreUtilsAsync: UI更新消息已发送");
}
});
}
}).start();
}
// ======================== 视图操作方法区 ========================
private void handleReloadBackgroundParam(Intent intent) {
LogUtils.d(TAG, "handleReloadBackgroundParam() 调用 | intent: " + intent);
if (intent == null) {
LogUtils.d(TAG, "handleReloadBackgroundParam: Intent 为空");
return;
}
boolean isReloadAccentColor = intent.getBooleanExtra(EXTRA_ISRELOAD_ACCENTCOLOR, false);
if (isReloadAccentColor) {
App.sBackgroundSourceUtils.getCurrentBackgroundBean().setPixelColor(ImageUtils.getColorAccent(this));
App.sBackgroundSourceUtils.saveSettings();
}
boolean isReloadBackgroundView = intent.getBooleanExtra(EXTRA_ISRELOAD_BACKGROUNDVIEW, false);
if (isReloadBackgroundView) {
LogUtils.d(TAG, "handleReloadBackgroundParam: 接收到刷新背景视图指令");
reloadBackgroundView();
}
}
private void reloadBackgroundView() {
LogUtils.d(TAG, "reloadBackgroundView() 调用");
mMainContentView.reloadBackgroundView();
}
private void updateViewData() {
LogUtils.d(TAG, "updateViewData() 调用");
if (mMainContentView == null || mFrameDrawable == null) {
LogUtils.e(TAG, "updateViewData: 核心视图或框架背景为空,更新失败");
return;
}
mMainContentView.updateViewData(mFrameDrawable);
LogUtils.d(TAG, "updateViewData: 视图数据已更新");
}
void updateBatteryDrawable() {
BatteryStyle batteryStyle = BatteryStyleView.getSavedBatteryStyle(this);
mMainContentView.updateBatteryDrawable(batteryStyle);
}
public static void sendUpdateBatteryDrawableMessage() {
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_BATTERYDRAWABLE);
}
}
private void reloadBackground() {
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, true);
LogUtils.d(TAG, "reloadBackground: 已加载自定义背景");
} else {
mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background);
LogUtils.d(TAG, "reloadBackground: 已加载默认背景");
}
}
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);
LogUtils.d(TAG, "updateServiceSwitchUI: 服务开关已更新 | 状态: " + configEnabled);
}
// ======================== 服务与线程管理方法区 ========================
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);
LogUtils.d(TAG, "toggleServiceEnableState: 服务配置已保存");
// UI开关联动服务启停
if (isEnable) {
if (!ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName())) {
ControlCenterService.startControlCenterService(getApplicationContext());
LogUtils.d(TAG, "toggleServiceEnableState: 服务已启动");
}
} else {
ControlCenterService.stopControlCenterService(getApplicationContext());
LogUtils.d(TAG, "toggleServiceEnableState: 服务已停止");
}
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
}
// ======================== 页面跳转方法区 ========================
private void startAboutActivity() {
LogUtils.d(TAG, "startAboutActivity() 调用");
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
APPInfo appInfo = genDefaultAppInfo();
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class);
LogUtils.d(TAG, "startAboutActivity: 关于页面已启动");
}
// ======================== 消息发送方法区 ========================
private void notifyServiceAppConfigChange() {
LogUtils.d(TAG, "notifyServiceAppConfigChange() 调用");
ControlCenterService.sendAppConfigStatusUpdateMessage(this);
reloadAppConfig();
LogUtils.d(TAG, "notifyServiceAppConfigChange: 服务配置已通知更新");
}
public static void reloadAppConfig() {
LogUtils.d(TAG, "reloadAppConfig() 调用");
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_RELOAD_APPCONFIG);
LogUtils.d(TAG, "reloadAppConfig: 配置重载消息已发送");
} 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);
LogUtils.d(TAG, "sendCurrentBatteryValueMessage: 电量消息已发送");
} 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(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
appInfo.setAppAPKName("PowerBell");
appInfo.setAppAPKFolderName("PowerBell");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
// ======================== MainContentView 事件回调区 ========================
@Override
public void onChargeReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onChargeReminderSwitchChanged() 调用 | isChecked: " + isChecked);
notifyServiceAppConfigChange();
}
@Override
public void onUsageReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onUsageReminderSwitchChanged() 调用 | isChecked: " + isChecked);
notifyServiceAppConfigChange();
}
@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();
}
}
@@ -1,81 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
import cc.winboll.studio.powerbell.R;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/13 11:25
* @Describe 应用介绍窗口
*/
public class AboutActivity extends WinBoLLActivity {
public static final String TAG = "AboutActivity";
private Toolbar mToolbar;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
// 设置工具栏
initToolbar();
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
finish();
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
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_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("PowerBell");
appInfo.setAppGitOwner("ZhanGSKen");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
appInfo.setAppAPKName("PowerBell");
appInfo.setAppAPKFolderName("PowerBell");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}
@@ -1,994 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.provider.Settings;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
/**
* 背景设置页面(支持图片选择、拍照、裁剪、像素拾取、调色板等功能)
* 核心:基于强制缓存策略,支持预览与设置提交分离,保留操作状态
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class BackgroundSettingsActivity extends WinBoLLActivity {
// ====================== 常量定义(按功能分类排序)======================
public static final String TAG = "BackgroundSettingsActivity";
// 系统版本常量
private static final int SDK_VERSION_TIRAMISU = 33;
// 请求码(按功能分组,从小到大排序)
public static final int REQUEST_SELECT_PICTURE = 0;
public static final int REQUEST_TAKE_PHOTO = 1;
public static final int REQUEST_CROP_IMAGE = 2;
private static final int REQUEST_PIXELPICKER = 1001;
private static final int REQUEST_CAMERA_PERMISSION = 1004;
// Bitmap解析常量
private static final int BITMAP_MAX_SIZE = 2048;
private static final int BITMAP_MAX_SAMPLE_SIZE = 16;
// ====================== 成员变量(按依赖优先级+功能分类)======================
// 工具类实例
private BackgroundSourceUtils mBgSourceUtils;
private BitmapCacheUtils mBitmapCache;
// 视图组件
private Toolbar mToolbar;
private BackgroundView mBackgroundView;
// 状态标记(volatile保证多线程可见性)
private volatile boolean isCommitSettings = false;
private volatile boolean isPreviewBackgroundChanged = false;
// ====================== 生命周期方法(按执行顺序排列)======================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate() 开始初始化");
setContentView(R.layout.activity_background_settings);
// 初始化核心组件
initCoreComponents();
// 初始化Toolbar与点击事件
initToolbar();
initClickListeners();
LogUtils.d(TAG, "onCreate() 视图与事件绑定完成");
// 处理分享意图或初始化预览
handleIntentOrPreview();
// 初始化预览环境并刷新
initPreviewEnvironment();
LogUtils.d(TAG, "onCreate() 初始化完成");
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "onPostCreate() 执行双重刷新预览");
// 监听视图布局完成事件
mBackgroundView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除监听,避免重复回调
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mBackgroundView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mBackgroundView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// 此时已获取真实宽高
int width = mBackgroundView.getWidth();
int height = mBackgroundView.getHeight();
LogUtils.d(TAG, String.format("onPostCreate() 获取视图尺寸 | width=%d | height=%d", width, height));
if (width > 0 && height > 0) {
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(BackgroundSettingsActivity.this);
appConfigUtils.loadAppConfig();
appConfigUtils.mAppConfigBean.setDefaultFrameWidth(width);
appConfigUtils.mAppConfigBean.setDefaultFrameHeight(height);
appConfigUtils.saveAppConfig();
LogUtils.d(TAG, "onPostCreate() 保存默认相框尺寸成功");
doubleRefreshPreview();
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, String.format("onActivityResult() | requestCode=%d | resultCode=%d | data=%s",
requestCode, resultCode, data != null ? data.toString() : "null"));
try {
if (resultCode != RESULT_OK) {
LogUtils.d(TAG, String.format("onActivityResult() 操作取消 | requestCode=%d", requestCode));
handleOperationCancelOrFail();
return;
}
handleActivityResult(requestCode, data);
} catch (Exception e) {
LogUtils.e(TAG, String.format("onActivityResult() 异常 | requestCode=%d | 异常信息=%s",
requestCode, e.getMessage()));
ToastUtils.show("操作失败");
}
}
@Override
public void finish() {
LogUtils.d(TAG, String.format("finish() | isCommitSettings=%b | isPreviewBackgroundChanged=%b",
isCommitSettings, isPreviewBackgroundChanged));
if (isCommitSettings) {
super.finish();
} else {
handleFinishConfirmation();
}
}
// ====================== 权限回调方法(单独分类)======================
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, String.format("onRequestPermissionsResult() | requestCode=%d | 权限数量=%d | 结果数量=%d",
requestCode, permissions.length, grantResults.length));
if (requestCode == REQUEST_CAMERA_PERMISSION) {
handleCameraPermissionResult(grantResults);
}
}
// ====================== 界面初始化方法(Toolbar + 点击事件)======================
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
finish();
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
private void initClickListeners() {
LogUtils.d(TAG, "initClickListeners() 开始绑定按钮点击事件");
// 绑定所有按钮点击事件
bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton3, onTakePhotoClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton4, onSelectPictureClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton5, onNetworkBackgroundDialog);
bindClickListener(R.id.activitybackgroundsettingsAButton6, onCropPictureClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton7, onCropFreePictureClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener);
LogUtils.d(TAG, "initClickListeners() 按钮点击事件绑定完成");
}
// 通用按钮绑定工具方法
private void bindClickListener(int resId, View.OnClickListener listener) {
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d", resId));
View view = findViewById(resId);
if (view != null) {
view.setOnClickListener(listener);
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d 绑定成功", resId));
} else {
LogUtils.e(TAG, String.format("bindClickListener() | 未找到视图:%d", resId));
}
}
// ====================== 按钮点击事件(按功能分类)======================
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onOriginNullClickListener() | 取消背景图片");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "onOriginNullClickListener() | 预览Bean为空");
return;
}
previewBean.setIsUseBackgroundFile(false);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
};
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onSelectPictureClickListener() | 选择图片");
launchImageSelector();
}
};
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onNetworkBackgroundDialog() | 打开网络背景对话框");
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener() {
@Override
public void onConfirm(String szConfirmFilePath) {
LogUtils.d(TAG, String.format("网络背景确认 onConfirm() | 文件路径=%s", szConfirmFilePath));
// 拷贝文件到预览数据并启动裁剪
if (putUriFileToPreviewSource(new File(szConfirmFilePath))) {
startImageCrop(false);
}
}
@Override
public void onCancel() {
LogUtils.d(TAG, "网络背景取消 onCancel()");
}
});
networkBackgroundDialog.show();
}
};
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onCropPictureClickListener() | 固定比例裁剪");
startImageCrop(false);
}
};
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onCropFreePictureClickListener() | 自由裁剪");
startImageCrop(true);
}
};
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onTakePhotoClickListener() | 拍照");
// 动态申请相机权限
if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "拍照准备 | 相机权限未授予,发起申请");
ActivityCompat.requestPermissions(
BackgroundSettingsActivity.this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
return;
}
handleTakePhoto();
}
};
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onReceivedPictureClickListener() | 恢复收到的图片");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "onReceivedPictureClickListener() | 预览Bean为空");
return;
}
previewBean.setIsUseBackgroundFile(true);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
};
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onPixelPickerClickListener() | 像素拾取");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "onPixelPickerClickListener() | 预览Bean为空");
ToastUtils.show("无有效图片可拾取像素");
return;
}
String targetImagePath = previewBean.getBackgroundFilePath();
File targetFile = new File(targetImagePath);
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
ToastUtils.show("无有效图片可拾取像素");
LogUtils.e(TAG, String.format("像素拾取失败 | 文件无效:%s", targetImagePath));
return;
}
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", targetImagePath);
startActivityForResult(intent, REQUEST_PIXELPICKER);
LogUtils.d(TAG, String.format("像素拾取启动 | 路径:%s", targetImagePath));
}
};
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onCleanPixelClickListener() | 清空像素颜色");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "onCleanPixelClickListener() | 预览Bean为空");
return;
}
int oldColor = previewBean.getPixelColor();
previewBean.setPixelColor(ImageUtils.getColorAccent(BackgroundSettingsActivity.this));
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, String.format("像素清空 | 旧颜色:#%08X", oldColor));
}
};
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onColorPaletteClickListener() | 调色板按钮");
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "onColorPaletteClickListener() | 预览Bean为空");
return;
}
int initialColor = previewBean.getPixelColor();
LogUtils.d(TAG, String.format("调色板 | 初始颜色:#%08X", initialColor));
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
@Override
public void onColorSelected(int color) {
previewBean.setPixelColor(color);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
LogUtils.d(TAG, String.format("颜色选择 | 选中颜色:#%08X", color));
}
});
dialog.show();
LogUtils.d(TAG, "调色板 | 对话框已显示");
}
};
// ====================== 工具方法(通用工具 + 视图工具)======================
/**
* 生成 FileProvider Uri,适配 Android 7.0+
* @param file 目标文件
* @return 适配后的Uri,失败返回null
*/
public Uri getFileProviderUri(File file) {
LogUtils.d(TAG, String.format("getFileProviderUri() | 文件路径:%s", (file != null ? file.getAbsolutePath() : "null")));
if (file == null) {
LogUtils.e(TAG, "getFileProviderUri() | 文件为空");
return null;
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
} else {
return Uri.fromFile(file);
}
} catch (Exception e) {
LogUtils.e(TAG, String.format("getFileProviderUri() | 生成Uri失败:%s", e.getMessage()));
return null;
}
}
/**
* 校验 Bitmap 是否有效(未被回收且不为空)
* @param bitmap 目标Bitmap
* @return 有效返回true,否则false
*/
private boolean isBitmapValid(Bitmap bitmap) {
boolean isValid = bitmap != null && !bitmap.isRecycled();
LogUtils.d(TAG, String.format("isBitmapValid() | Bitmap有效性校验:%b", isValid));
return isValid;
}
/**
* 双重刷新预览,确保背景加载最新数据
*/
private void doubleRefreshPreview() {
LogUtils.d(TAG, "doubleRefreshPreview() 开始双重刷新预览");
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
LogUtils.w(TAG, "双重刷新 跳过:对象为空或Activity已结束");
return;
}
// 第一重刷新
try {
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBackgroundView.loadByBackgroundBean(previewBean, true);
LogUtils.d(TAG, "双重刷新 第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("双重刷新 第一重异常:%s", e.getMessage()));
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
try {
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBackgroundView.loadByBackgroundBean(previewBean, true);
LogUtils.d(TAG, "双重刷新 第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("双重刷新 第二重异常:%s", e.getMessage()));
}
}
}
}, 200);
}
// ====================== 业务逻辑方法(按功能分类)======================
/**
* 初始化核心组件(工具类+视图)
*/
private void initCoreComponents() {
LogUtils.d(TAG, "initCoreComponents() 开始初始化");
// 初始化视图
mBackgroundView = findViewById(R.id.background_view);
if (mBackgroundView == null) {
LogUtils.e(TAG, "initCoreComponents() | BackgroundView未找到");
}
// 初始化工具类
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
mBitmapCache = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "initCoreComponents() 视图与工具类加载完成");
}
/**
* 处理意图或初始化预览
*/
private void handleIntentOrPreview() {
LogUtils.d(TAG, "handleIntentOrPreview() 开始处理");
if (handleShareIntent()) {
ToastUtils.show("已接收分享图片");
LogUtils.d(TAG, "handleIntentOrPreview() | 处理分享意图成功");
} else {
mBgSourceUtils.setCurrentSourceToPreview();
LogUtils.d(TAG, "handleIntentOrPreview() | 加载当前背景配置");
}
}
/**
* 初始化预览环境
*/
private void initPreviewEnvironment() {
LogUtils.d(TAG, "initPreviewEnvironment() 开始初始化");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean);
doubleRefreshPreview();
LogUtils.d(TAG, "initPreviewEnvironment() 初始化完成");
}
/**
* 处理分享意图
* @return 处理成功返回true,否则false
*/
private boolean handleShareIntent() {
LogUtils.d(TAG, "handleShareIntent() 开始处理");
Intent intent = getIntent();
if (intent != null) {
String action = intent.getAction();
String type = intent.getType();
LogUtils.d(TAG, String.format("分享处理 | action%stype%s", action, type));
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
showSharePreviewDialog();
return true;
}
}
return false;
}
/**
* 显示分享图片预览对话框
*/
private void showSharePreviewDialog() {
LogUtils.d(TAG, "showSharePreviewDialog() 开始显示");
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener() {
@Override
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
LogUtils.d(TAG, String.format("分享确认 | Uri%s", uriRecivedPicture.toString()));
if (putUriFileToPreviewSource(uriRecivedPicture)) {
startImageCrop(false);
}
}
});
dlg.show();
LogUtils.d(TAG, "分享处理 | 显示图片预览对话框");
}
/**
* 判断是否为图片类型
* @param mimeType MIME类型
* @return 是图片返回true,否则false
*/
private boolean isImageType(String mimeType) {
if (mimeType == null) {
return false;
}
String lowerMimeType = mimeType.toLowerCase();
LogUtils.d(TAG, String.format("isImageType() | mimeType: %s, lowerMimeType: %s", mimeType, lowerMimeType));
return lowerMimeType.startsWith("image/");
}
/**
* 启动图片选择器
*/
private void launchImageSelector() {
LogUtils.d(TAG, "launchImageSelector() 启动图片选择器");
Intent[] intents = createImageSelectorIntents();
Intent validIntent = findValidIntent(intents);
if (validIntent != null) {
launchImageChooser(validIntent);
} else {
showNoGalleryDialog();
}
}
/**
* 创建图片选择器意图数组
* @return 意图数组
*/
private Intent[] createImageSelectorIntents() {
LogUtils.d(TAG, "createImageSelectorIntents() 开始创建");
Intent[] intents = new Intent[3];
// ACTION_GET_CONTENT
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
getContentIntent.setType("image/*");
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intents[0] = getContentIntent;
// ACTION_PICK
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
pickIntent.setType("image/*");
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intents[1] = pickIntent;
// ACTION_OPEN_DOCUMENTAPI19+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openDocIntent.setType("image/*");
openDocIntent.addCategory(Intent.CATEGORY_OPENABLE);
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intents[2] = openDocIntent;
}
LogUtils.d(TAG, "createImageSelectorIntents() 意图数组创建完成");
return intents;
}
/**
* 查找有效的意图
* @param intents 意图数组
* @return 有效意图,无则返回null
*/
private Intent findValidIntent(Intent[] intents) {
LogUtils.d(TAG, "findValidIntent() 开始查找");
for (Intent intent : intents) {
if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
LogUtils.d(TAG, "findValidIntent() | 找到有效意图");
return intent;
}
}
LogUtils.d(TAG, "findValidIntent() | 无有效意图");
return null;
}
/**
* 启动图片选择器
* @param validIntent 有效意图
*/
private void launchImageChooser(Intent validIntent) {
LogUtils.d(TAG, "launchImageChooser() 启动选择器");
Intent chooser = Intent.createChooser(validIntent, "选择图片");
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
LogUtils.d(TAG, "launchImageChooser() | 启动图片选择");
}
/**
* 显示无相册应用提示对话框
*/
private void showNoGalleryDialog() {
LogUtils.d(TAG, "showNoGalleryDialog() | 无相册应用");
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("未找到相册应用,请安装后重试");
new AlertDialog.Builder(BackgroundSettingsActivity.this)
.setTitle("无图片选择应用")
.setMessage("需要安装相册应用才能选择图片")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
launchGalleryMarket();
}
})
.setNegativeButton("取消", null)
.show();
}
});
}
/**
* 启动应用商店下载相册
*/
private void launchGalleryMarket() {
LogUtils.d(TAG, "launchGalleryMarket() 启动应用商店");
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
if (marketIntent.resolveActivity(getPackageManager()) != null) {
startActivity(marketIntent);
LogUtils.d(TAG, "launchGalleryMarket() | 启动成功");
} else {
ToastUtils.show("无法打开应用商店");
LogUtils.e(TAG, "launchGalleryMarket() | 启动失败");
}
}
/**
* 处理操作取消或失败
*/
private void handleOperationCancelOrFail() {
LogUtils.d(TAG, "handleOperationCancelOrFail() 操作取消或失败");
mBgSourceUtils.setCurrentSourceToPreview();
ToastUtils.show("操作取消或失败");
doubleRefreshPreview();
}
/**
* 处理拍照逻辑(权限通过后执行)
*/
void handleTakePhoto() {
LogUtils.d(TAG, "handleTakePhoto() 开始处理拍照");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "handleTakePhoto() | 预览Bean为空");
ToastUtils.show("拍照文件创建失败");
return;
}
File takePhotoFile = new File(previewBean.getBackgroundFilePath());
if (!takePhotoFile.exists()) {
ToastUtils.show("拍照文件创建失败");
LogUtils.e(TAG, String.format("handleTakePhoto() | 文件不存在:%s", takePhotoFile.getAbsolutePath()));
return;
}
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
try {
Uri photoUri = getFileProviderUri(takePhotoFile);
if (photoUri == null) {
throw new Exception("生成FileProvider Uri失败");
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
LogUtils.d(TAG, String.format("handleTakePhoto() | Uri%s", photoUri.toString()));
} catch (Exception e) {
String errMsg = "拍照启动异常:" + e.getMessage();
ToastUtils.show(errMsg.substring(0, 20));
LogUtils.e(TAG, String.format("handleTakePhoto() | %s", e.getMessage()));
}
}
/**
* 处理ActivityResult分发
* @param requestCode 请求码
* @param data 回调数据
*/
private void handleActivityResult(int requestCode, Intent data) {
LogUtils.d(TAG, String.format("handleActivityResult() | 处理请求码:%d", requestCode));
switch (requestCode) {
case REQUEST_SELECT_PICTURE:
handleSelectPictureResult(data);
break;
case REQUEST_TAKE_PHOTO:
handleTakePhotoResult(data);
break;
case REQUEST_CROP_IMAGE:
handleCropImageResult(data);
break;
case REQUEST_PIXELPICKER:
handlePixelPickerResult();
break;
default:
LogUtils.d(TAG, String.format("handleActivityResult() | 未知requestCode%d", requestCode));
break;
}
}
/**
* 处理拍照结果
* @param data 回调数据
*/
private void handleTakePhotoResult(Intent data) {
LogUtils.d(TAG, "handleTakePhotoResult() 处理拍照结果");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "handleTakePhotoResult() | 预览Bean为空");
return;
}
previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(false);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
startImageCrop(false);
LogUtils.d(TAG, "handleTakePhotoResult() | 已启动裁剪");
}
/**
* 处理选图结果
* @param data 回调数据
*/
private void handleSelectPictureResult(Intent data) {
LogUtils.d(TAG, "handleSelectPictureResult() 处理选图结果");
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("图片Uri为空");
LogUtils.e(TAG, "handleSelectPictureResult() | Uri为空");
return;
}
LogUtils.d(TAG, String.format("handleSelectPictureResult() | 系统返回Uri : %s", selectedImage.toString()));
// 申请持久化权限(API33+
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
getContentResolver().takePersistableUriPermission(
selectedImage,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
LogUtils.d(TAG, "handleSelectPictureResult() | 已添加持久化权限");
}
// 同步文件并启动裁剪
if (putUriFileToPreviewSource(selectedImage)) {
LogUtils.d(TAG, "handleSelectPictureResult() | 路径绑定完成");
startImageCrop(false);
} else {
ToastUtils.show("图片同步失败");
LogUtils.e(TAG, "handleSelectPictureResult() | 文件复制失败");
}
}
/**
* 将 Uri 文件同步到预览 Bean
* @param srcUriFile 源Uri
* @return 同步成功返回true,否则false
*/
private boolean putUriFileToPreviewSource(Uri srcUriFile) {
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源Uri%s", srcUriFile.toString()));
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
if (TextUtils.isEmpty(filePath)) {
LogUtils.e(TAG, "putUriFileToPreviewSource() | Uri解析路径为空");
return false;
}
File srcFile = new File(filePath);
return putUriFileToPreviewSource(srcFile);
}
/**
* 将 File 同步到预览 Bean
* @param srcFile 源文件
* @return 同步成功返回true,否则false
*/
private boolean putUriFileToPreviewSource(File srcFile) {
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源文件:%s", srcFile.getAbsolutePath()));
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
File dstFile = new File(previewBean.getBackgroundFilePath());
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 目标文件:%s", dstFile.getAbsolutePath()));
if (FileUtils.copyFile(srcFile, dstFile)) {
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件拷贝成功");
return true;
}
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件无法拷贝");
return false;
}
/**
* 处理裁剪结果
* @param data 回调数据
*/
private void handleCropImageResult(Intent data) {
LogUtils.d(TAG, "handleCropImageResult() 处理裁剪结果");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "handleCropImageResult() | 预览Bean为空");
handleOperationCancelOrFail();
return;
}
File cropTempFile = new File(previewBean.getBackgroundScaledCompressFilePath());
boolean isFileExist = cropTempFile.exists();
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
long fileSize = isFileExist ? cropTempFile.length() : 0;
boolean isCropSuccess = isFileExist && isFileReadable && fileSize > 100;
if (isCropSuccess) {
handleCropSuccess(previewBean, fileSize);
} else {
handleCropFailure(isFileExist, isFileReadable, fileSize);
}
}
/**
* 处理裁剪成功
* @param previewBean 预览Bean
* @param fileSize 文件大小
*/
private void handleCropSuccess(BackgroundBean previewBean, long fileSize) {
LogUtils.d(TAG, String.format("handleCropSuccess() | 裁剪成功,文件大小:%d", fileSize));
isPreviewBackgroundChanged = true;
previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
}
/**
* 处理裁剪失败
* @param isFileExist 文件是否存在
* @param isFileReadable 文件是否可读
* @param fileSize 文件大小
*/
private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) {
LogUtils.e(TAG, String.format("handleCropFailure() | 裁剪失败,文件状态:存在=%b,可读=%b,大小=%d",
isFileExist, isFileReadable, fileSize));
handleOperationCancelOrFail();
}
/**
* 处理像素拾取结果
*/
private void handlePixelPickerResult() {
LogUtils.d(TAG, "handlePixelPickerResult() 处理像素拾取结果");
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
/**
* 处理相机权限申请结果
* @param grantResults 权限结果数组
*/
private void handleCameraPermissionResult(int[] grantResults) {
LogUtils.d(TAG, "handleCameraPermissionResult() 处理相机权限结果");
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予成功");
handleTakePhoto();
} else {
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予失败");
ToastUtils.show("相机权限被拒绝,无法拍照");
// 引导用户到设置页面开启权限(用户选择不再询问时)
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
launchAppSettings();
}
}
}
/**
* 启动应用设置页面
*/
private void launchAppSettings() {
LogUtils.d(TAG, "launchAppSettings() 启动应用设置页面");
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
ToastUtils.show("请在设置中开启相机权限");
}
/**
* 处理Finish确认对话框
*/
private void handleFinishConfirmation() {
LogUtils.d(TAG, "handleFinishConfirmation() 处理Finish确认");
if (isPreviewBackgroundChanged) {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
public void onYes() {
mBgSourceUtils.commitPreviewSourceToCurrent();
isCommitSettings = true;
finish();
Intent mainIntent = new Intent(BackgroundSettingsActivity.this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
startActivity(mainIntent);
LogUtils.d(TAG, "handleFinishConfirmation() | 确认设置,启动MainActivity并刷新背景");
}
@Override
public void onNo() {
isCommitSettings = true;
finish();
LogUtils.d(TAG, "handleFinishConfirmation() | 取消设置,关闭页面");
}
});
} else {
isCommitSettings = true;
finish();
}
}
/**
* 启动图片裁剪
* @param isFreeCrop 是否自由裁剪
*/
private void startImageCrop(boolean isFreeCrop) {
LogUtils.d(TAG, String.format("startImageCrop() | 是否自由裁剪:%b", isFreeCrop));
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "startImageCrop() | 预览Bean为空");
ToastUtils.show("裁剪失败:无有效图片");
return;
}
int width = isFreeCrop ? 0 : mBackgroundView.getWidth();
int height = isFreeCrop ? 0 : mBackgroundView.getHeight();
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
previewBean,
width,
height,
isFreeCrop,
REQUEST_CROP_IMAGE);
LogUtils.d(TAG, String.format("startImageCrop() | 目标尺寸:%dx%d", width, height));
}
}
@@ -1,598 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 电池报告页面,统计应用24小时运行时长与电池消耗情况
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
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分钟毫秒数
// ======================== 成员变量(按依赖优先级+功能分类) =========================
// UI组件
private Toolbar mToolbar;
private RecyclerView rvBatteryReport;
private EditText etSearch;
// 数据与适配器
private BatteryReportAdapter adapter;
private List<AppBatteryModel> dataList = new ArrayList<>();
private List<AppBatteryModel> filteredList = new ArrayList<>();
// 电池相关
private BroadcastReceiver batteryReceiver;
private int batteryCapacity = 5400; // 电池容量(mAh
private float lastBatteryPercent = 100.0f; // 上次电池百分比
private long lastCheckTime = System.currentTimeMillis(); // 上次检查时间戳
// 缓存相关
private Map<String, Long> appRunTimeCache = new HashMap<>();
private Map<String, String> packageToAppNameCache = new HashMap<>();
private PackageManager mPackageManager;
// ======================== 接口实现方法 =========================
@Override
public Activity getActivity() {
return this;
}
@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 初始化开始");
// 初始化UI组件
initView();
// 初始化PackageManager
mPackageManager = getPackageManager();
LogUtils.d(TAG, "【onCreate】基础组件初始化完成");
// 权限检查(Java7 传统条件判断)
if (!hasUsageStatsPermission(this)) {
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限,引导用户开启");
return;
}
// 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
loadAllAppPackage();
preCacheAllAppNames();
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateInitial24hTotalConsumption();
filteredList.addAll(dataList);
LogUtils.d(TAG, "【onCreate】数据初始化完成,原始数据量:" + dataList.size());
// 初始化适配器
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
rvBatteryReport.setAdapter(adapter);
LogUtils.d(TAG, "【onCreate】适配器初始化完成,过滤后数据量:" + filteredList.size());
// 绑定搜索监听 + 注册电池广播
bindSearchListener();
registerBatteryReceiver();
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
// 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) {
String keyword = s.toString().trim();
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化:" + keyword);
filterAppsByPackageAndName(keyword);
}
@Override
public void afterTextChanged(Editable s) {}
});
LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成");
}
// ======================== 电池广播注册方法 =========================
private void registerBatteryReceiver() {
batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra("level", 100);
int scale = intent.getIntExtra("scale", 100);
float currentPercent = (float) level / scale * 100;
LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
if (currentPercent < lastBatteryPercent) {
float dropPercent = lastBatteryPercent - currentPercent;
long duration = System.currentTimeMillis() - lastCheckTime;
LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration));
// 更新运行时长并计算耗电
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache);
}
// 刷新记录
lastBatteryPercent = currentPercent;
lastCheckTime = System.currentTimeMillis();
}
};
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成");
}
// ======================== 权限检查方法 =========================
/**
* 检查是否拥有使用情况访问权限
* @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, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用");
for (ApplicationInfo appInfo : appList) {
String packageName = appInfo.packageName;
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
}
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名");
}
/**
* 预缓存所有应用名称,减少PackageManager重复调用
*/
private void preCacheAllAppNames() {
packageToAppNameCache.clear();
LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
String appName = getAppNameByPackage(packageName);
packageToAppNameCache.put(packageName, appName);
}
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
}
/**
* 通过包名获取应用名称,带异常处理
* @param packageName 应用包名
* @return 应用名称,获取失败返回包名
*/
private String getAppNameByPackage(String packageName) {
LogUtils.v(TAG, "【getAppNameByPackage】查询包名:" + packageName);
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
return mPackageManager.getApplicationLabel(appInfo).toString();
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
return packageName;
} catch (Exception e) {
LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
return packageName;
}
}
/**
* 更新运行时长到数据模型
*/
private void updateAppRunTimeToModel() {
int updateCount = 0;
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
model.setRunTime(runTime);
if (runTime > 0) {
updateCount++;
}
}
LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成,数据量:" + dataList.size() + ",更新运行时长应用数:" + updateCount);
}
/**
* 获取应用24小时运行时长
* @return 应用包名-运行时长(ms)映射
*/
private Map<String, Long> getAppRunTime() {
Map<String, Long> runTimeMap = new HashMap<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
long endTime = System.currentTimeMillis();
long startTime = endTime - ONE_DAY_MS; // 近24小时
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
for (android.app.usage.UsageStats stats : statsList) {
long runTimeMs = stats.getTotalTimeInForeground();
String packageName = stats.getPackageName();
runTimeMap.put(packageName, runTimeMs);
LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs));
if (packageName.equals("aidepro.top")) {
LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果");
}
}
} catch (Exception e) {
LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败:" + e.getMessage());
}
}
LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量:" + runTimeMap.size());
return runTimeMap;
}
// ======================== 核心计算方法 =========================
/**
* 初始化时计算24小时累计耗电(赋值给totalConsumption
* 逻辑:基于24小时运行时长占比,分配当前电池容量的理论24小时消耗
*/
private void calculateInitial24hTotalConsumption() {
long total24hRunTime = 0;
// 1. 计算24小时内所有应用总运行时长
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
total24hRunTime += entry.getValue();
}
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime));
// 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;
model.setTotalConsumption(initialTotalConsumption);
LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化:" + initialTotalConsumption + " mAh");
}
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
}
/**
* 计算单次耗电(赋值给consumption)+ 累加至累计耗电
* @param dropPercent 电池下降百分比
* @param runTimeMap 应用运行时长映射
*/
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】开始计算,电池下降百分比:" + dropPercent);
long totalSingleRunTime = 0;
// 1. 计算本次电池下降期间的总运行时长
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);
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
model.setConsumption(singleConsumption);
// 累加至累计耗电
float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
model.setTotalConsumption(newTotalConsumption);
model.setRunTime(appSingleRunTime);
LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s:单次耗电%.1f mAh,累计耗电%.1f mAh",
packageName, singleConsumption, newTotalConsumption));
}
// 3. 按累计耗电降序排序
Collections.sort(dataList, new Comparator<AppBatteryModel>() {
@Override
public int compare(AppBatteryModel m1, AppBatteryModel m2) {
return Float.compare(m2.getTotalConsumption(), m1.getTotalConsumption());
}
});
// 4. 重新过滤并刷新列表
filterAppsByPackageAndName(etSearch.getText().toString().trim());
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);
if (isMatched) {
filteredList.add(model);
}
}
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词:" + keyword + ",匹配应用数量:" + filteredList.size());
}
adapter.notifyDataSetChanged();
}
// ======================== 工具方法 =========================
/**
* 格式化运行时长
* @param runTimeMs 运行时长(ms
* @return 格式化后的运行时长字符串
*/
private String formatRunTime(long runTimeMs) {
if (runTimeMs <= 0) {
return "0秒";
}
long seconds = runTimeMs / 1000;
long hours = seconds / 3600;
long minutes = (seconds % 3600) / 60;
seconds = seconds % 60;
if (hours > 0) {
return String.format("%d时%d分%d秒", hours, minutes, seconds);
} else if (minutes > 0) {
return String.format("%d分%d秒", minutes, seconds);
} else {
return String.format("%d秒", seconds);
}
}
// ======================== 内部类:数据模型 =========================
/**
* 应用电池数据模型
* - consumption:单次耗电(两次电池广播间的消耗)
* - totalConsumption:累计耗电(24小时初始化值+后续单次累加)
* - runTime:运行时长(ms
*/
public static class AppBatteryModel {
private String packageName; // 应用包名(核心标识)
private float consumption; // 单次耗电(mAh
private float totalConsumption;// 累计耗电(mAh
private long runTime; // 运行时长(ms
// Java7 显式构造
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
this.packageName = packageName;
this.consumption = consumption;
this.totalConsumption = totalConsumption;
this.runTime = runTime;
}
// Getter/Setter
public String getPackageName() {
return packageName;
}
public float getConsumption() {
return consumption;
}
public void setConsumption(float consumption) {
this.consumption = consumption;
}
public float getTotalConsumption() {
return totalConsumption;
}
public void setTotalConsumption(float totalConsumption) {
this.totalConsumption = totalConsumption;
}
public long getRunTime() {
return runTime;
}
public void setRunTime(long runTime) {
this.runTime = runTime;
}
}
// ======================== 内部类:RecyclerView适配器 =========================
/**
* 电池报告列表适配器,显示应用名称、累计耗电、运行时长
*/
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
private Context mContext;
private List<AppBatteryModel> mDataList;
private PackageManager mPm;
private Map<String, String> mPackageToNameCache;
// Java7 显式构造
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
PackageManager pm, Map<String, String> packageToNameCache) {
this.mContext = context;
this.mDataList = dataList;
this.mPm = pm;
this.mPackageToNameCache = packageToNameCache;
LogUtils.d(TAG, "【BatteryReportAdapter】适配器构造完成,数据量:" + dataList.size());
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(mContext)
.inflate(android.R.layout.simple_list_item_2, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// 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;
}
AppBatteryModel model = mDataList.get(position);
String packageName = model.getPackageName();
String appName = "";
// 优先从缓存获取应用名
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
appName = mPackageToNameCache.get(packageName);
} else {
// 缓存无数据时兜底查询
try {
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
appName = mPm.getApplicationLabel(appInfo).toString();
if (mPackageToNameCache != null) {
mPackageToNameCache.put(packageName, appName);
}
} catch (PackageManager.NameNotFoundException e) {
appName = packageName;
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
} catch (Exception e) {
appName = packageName;
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
}
}
// 显示逻辑:应用名称 + 累计耗电 + 运行时长
holder.tvAppName.setText(appName);
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);
}
@Override
public int getItemCount() {
return mDataList == null ? 0 : mDataList.size();
}
/**
* ViewHolder:绑定系统布局控件
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvAppName; // 应用名称
TextView tvConsumption; // 累计耗电 + 运行时长
public ViewHolder(View itemView) {
super(itemView);
tvAppName = (TextView) itemView.findViewById(android.R.id.text1);
tvConsumption = (TextView) itemView.findViewById(android.R.id.text2);
}
}
}
}
@@ -1,166 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Switch;
import android.widget.TextView;
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.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;
/**
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
* 适配 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;
private TextView mtvRecordText;
private TextView tvAOHPCTCSeekBarMSG;
private AOHPCTCSeekBar aOHPCTCSeekBar;
// 应用与配置
private App mApplication;
private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
// ======================== 接口实现方法 =========================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
// ======================== 生命周期方法(按执行顺序排列) =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clearrecord);
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
// 初始化应用实例
mApplication = (App) getApplication();
LogUtils.d(TAG, "【onCreate】应用实例初始化完成");
// 初始化核心逻辑
initView();
initSeekBar();
initRecordText();
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
}
// ======================== UI初始化方法 =========================
/**
* 初始化Toolbar与显示文本组件
*/
private void initView() {
// 初始化Toolbar
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
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();
}
});
// 初始化显示文本组件
tvAOHPCTCSeekBarMSG = (TextView) findViewById(R.id.activityclearrecordTextView1);
mtvRecordText = (TextView) findViewById(R.id.activityclearrecordTextView2);
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
/**
* 初始化滑动清理控件,设置回调监听
*/
private void initSeekBar() {
aOHPCTCSeekBar = (AOHPCTCSeekBar) findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
aOHPCTCSeekBar.setThumbOffset(0);
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();
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, String.format("【initRecordText】使用带换行格式显示记录,记录数量:%d", listBatteryInfo.size()));
} else {
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
LogUtils.d(TAG, String.format("【initRecordText】使用无换行格式显示记录,记录数量:%d", listBatteryInfo.size()));
}
}
mtvRecordText.setText(szRecordText);
LogUtils.d(TAG, "【initRecordText】记录显示文本刷新完成");
}
// ======================== 事件回调方法 =========================
/**
* 切换记录显示格式(带换行/不带换行)
* @param view 触发事件的Switch控件
*/
public void onShowRecordWithEnter(View view) {
Switch swShowRecordWithEnter = (Switch) view;
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
LogUtils.d(TAG, String.format("【onShowRecordWithEnter】记录显示格式切换,带换行显示:%b", mIsShowRecordWithEnter));
// 刷新记录显示
initRecordText();
}
}
@@ -1,346 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
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 = "已记录像素值";
// ======================== 成员变量 =========================
// UI组件
private Toolbar mToolbar;
private ImageView imageView;
private TextView infoText;
private ViewGroup imageContainer;
private RelativeLayout mainLayout;
// 图片与像素数据
private Bitmap originalBitmap; // 原始图片Bitmap(用于像素拾取)
// ======================== 接口实现方法 =========================
@Override
public Activity getActivity() {
return this;
}
@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() {
imageView = findViewById(R.id.imageView);
infoText = findViewById(R.id.infoText);
imageContainer = findViewById(R.id.imageContainer);
mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
/**
* 初始化工具栏,设置导航与标题
*/
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
finish();
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
// ======================== 业务逻辑方法 =========================
/**
* 从Intent中获取图片路径并加载图片
*/
private void loadImageFromIntent() {
String imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
LogUtils.d(TAG, "【loadImageFromIntent】获取到图片路径:" + imagePath);
if (imagePath != null) {
loadImage(imagePath);
} else {
infoText.setText(MSG_NO_IMAGE_PATH);
LogUtils.w(TAG, "【loadImageFromIntent】未获取到图片路径");
}
}
/**
* 加载指定路径的图片
* @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);
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());
}
}
/**
* 显示像素颜色信息对话框
* @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)",
Color.red(pixelColor),
Color.green(pixelColor),
Color.blue(pixelColor),
pixelColor,
x, y);
infoTextView.setText(colorInfo);
LogUtils.d(TAG, "【showPixelDialog】显示像素信息:" + colorInfo);
// 确定按钮点击事件
Button confirmButton = dialog.findViewById(R.id.confirmButton);
confirmButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
// 保存像素颜色到背景配置
savePixelColor(pixelColor);
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_RECORDED, Toast.LENGTH_SHORT).show();
// 同步背景颜色
setBackgroundColor();
}
});
dialog.show();
LogUtils.d(TAG, "【showPixelDialog】像素对话框已显示");
}
/**
* 保存拾取的像素颜色到背景配置
* @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));
}
// ======================== 事件回调方法 =========================
/**
* 绑定图片容器的触摸事件,处理像素拾取逻辑
*/
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 + ")");
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) {
LogUtils.d(TAG, "【onOptionsItemSelected】点击返回菜单");
Intent intent = new Intent(this, BackgroundSettingsActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
super.onBackPressed();
setResult(RESULT_OK);
finish();
LogUtils.d(TAG, "【onBackPressed】返回键触发,页面关闭");
}
}
@@ -1,363 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.SMTPConfigView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import cc.winboll.studio.powerbell.utils.ChargeMailNotifyHelper;
import cc.winboll.studio.powerbell.utils.ChargeMailNotifyFloatWindow;
import cc.winboll.studio.powerbell.threads.RemindThread;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import android.widget.Switch;
import java.lang.reflect.Field;
/**
* 应用设置窗口,提供应用配置项的统一入口
* 适配 API30,基于 Java7 开发
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025年11月27日14时26分00秒
* @LastEditTime 2026-02-28
* @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 CheckBox cbUsePowerTts; // 用电TTS(主开关)
private CheckBox cbChargeTts; // 充电TTS(主开关)
private CheckBox cbUseageTtsBattary; // 用电TTS带电量提醒(子开关)
private CheckBox cbChargeTtsBattary; // 充电TTS带电量提醒(子开关)
private CheckBox cbTtsWhenNotifyBattery; // 👉 新增:通知电量时同时播放TTS
private Switch switchChargeMailNotify; // 充电邮件通知主开关
private SMTPConfigView smtpConfigView; // SMTP邮件配置视图
// ======================== 接口实现区 =========================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
// ======================== 生命周期区 =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
LogUtils.d(TAG, "onCreate: 应用设置页面初始化开始");
initToolbar();
initTtsCheckBoxes();
initTtsCheckBoxStatus();
initSMTPConfigView();
initChargeMailNotifySwitch();
LogUtils.d(TAG, "onCreate: 应用设置页面初始化完成");
}
// ======================== UI初始化区 =========================
/**
* 初始化顶部工具栏
*/
private void initToolbar() {
LogUtils.d(TAG, "initToolbar: 工具栏初始化开始");
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, "initToolbar-navigationOnClick: 点击导航返回按钮");
finish();
}
});
LogUtils.d(TAG, "initToolbar: 工具栏初始化完成");
}
/**
* 绑定TTS相关复选框控件
*/
private void initTtsCheckBoxes() {
LogUtils.d(TAG, "initTtsCheckBoxes: TTS复选框绑定开始");
cbUsePowerTts = findViewById(R.id.activitysettingsCheckBox1);
cbChargeTts = findViewById(R.id.activitysettingsCheckBox2);
cbUseageTtsBattary = findViewById(R.id.activitysettingsCheckBox3);
cbChargeTtsBattary = findViewById(R.id.activitysettingsCheckBox4);
cbTtsWhenNotifyBattery = findViewById(R.id.activitysettingsCheckBox5); // 👉 新增绑定
LogUtils.d(TAG, "initTtsCheckBoxes: TTS复选框绑定完成");
}
/**
* 初始化TTS复选框初始状态
*/
private void initTtsCheckBoxStatus() {
LogUtils.d(TAG, "initTtsCheckBoxStatus: TTS复选框状态初始化开始");
ThoughtfulServiceBean bean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (bean == null) {
LogUtils.d(TAG, "initTtsCheckBoxStatus: 未读取到配置Bean,创建新实例");
bean = new ThoughtfulServiceBean();
}
boolean useMainOpen = bean.isEnableUsePowerTts();
boolean chargeMainOpen = bean.isEnableChargeTts();
cbUsePowerTts.setChecked(useMainOpen);
cbChargeTts.setChecked(chargeMainOpen);
cbUseageTtsBattary.setChecked(bean.isEnableUseageTtsWithBattary());
cbChargeTtsBattary.setChecked(bean.isEnableChargeTtsWithBattary());
cbTtsWhenNotifyBattery.setChecked(bean.isEnableTtsWhenNotifyBattery()); // 👉 新增赋值
cbUseageTtsBattary.setEnabled(useMainOpen);
cbChargeTtsBattary.setEnabled(chargeMainOpen);
LogUtils.d(TAG, "initTtsCheckBoxStatus: 主开关状态-用电TTS" + useMainOpen + " 充电TTS" + chargeMainOpen);
LogUtils.d(TAG, "initTtsCheckBoxStatus: TTS复选框状态初始化完成");
}
/**
* 初始化充电邮件通知开关
*/
private void initChargeMailNotifySwitch() {
LogUtils.d(TAG, "initChargeMailNotifySwitch: 充电邮件通知开关初始化开始");
switchChargeMailNotify = findViewById(R.id.switchChargeMailNotify);
boolean enabled = ChargeMailNotifyHelper.isEnabled(this);
switchChargeMailNotify.setChecked(enabled);
updateSMTPConfigViewEnabled(enabled);
LogUtils.d(TAG, "initChargeMailNotifySwitch: 充电邮件通知开关初始化完成,状态=" + enabled);
}
/**
* 初始化SMTP邮件配置视图
*/
private void initSMTPConfigView() {
LogUtils.d(TAG, "initSMTPConfigView: SMTP邮件配置视图初始化开始");
smtpConfigView = findViewById(R.id.smtp_config_view);
LogUtils.d(TAG, "initSMTPConfigView: SMTP邮件配置视图初始化完成");
}
/**
* 更新SMTP配置视图的启用状态
*/
private void updateSMTPConfigViewEnabled(boolean enabled) {
LogUtils.d(TAG, "updateSMTPConfigViewEnabled: 设置SMTPConfigView启用状态=" + enabled);
smtpConfigView.setEnabled(enabled);
// 递归设置子控件的启用状态
setViewAndChildrenEnabled(smtpConfigView, enabled);
}
/**
* 递归设置View及其所有子控件的启用状态
*/
private void setViewAndChildrenEnabled(View view, boolean enabled) {
view.setEnabled(enabled);
if (view instanceof android.view.ViewGroup) {
android.view.ViewGroup group = (android.view.ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
setViewAndChildrenEnabled(group.getChildAt(i), enabled);
}
}
}
// ======================== 事件响应区 =========================
/**
* 悬浮窗权限检查入口
*/
public void onCheckTTSDrawOverlaysPermission(View view) {
LogUtils.d(TAG, "onCheckTTSDrawOverlaysPermission: 触发悬浮窗权限检查");
canDrawOverlays();
}
/**
* 用电TTS主开关点击事件
*/
public void onEnableUsePowerTts(View view) {
boolean isChecked = cbUsePowerTts.isChecked();
LogUtils.d(TAG, "onEnableUsePowerTts: 用电TTS主开关点击,切换后状态=" + isChecked);
// 主开关联动子开关
cbUseageTtsBattary.setEnabled(isChecked);
// 保存配置
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
bean.setIsEnableUsePowerTts(isChecked);
ThoughtfulServiceBean.saveBean(this, bean);
LogUtils.d(TAG, "onEnableUsePowerTts: 用电TTS状态保存完成");
}
/**
* 充电TTS主开关点击事件
*/
public void onEnableChargeTts(View view) {
boolean isChecked = cbChargeTts.isChecked();
LogUtils.d(TAG, "onEnableChargeTts: 充电TTS主开关点击,切换后状态=" + isChecked);
// 主开关联动子开关
cbChargeTtsBattary.setEnabled(isChecked);
// 保存配置
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
bean.setIsEnableChargeTts(isChecked);
ThoughtfulServiceBean.saveBean(this, bean);
LogUtils.d(TAG, "onEnableChargeTts: 充电TTS状态保存完成");
}
/**
* 用电TTS带电量提醒子开关点击事件
*/
public void onEnableUseageTtsWithBattary(View view) {
boolean isChecked = cbUseageTtsBattary.isChecked();
LogUtils.d(TAG, "onEnableUseageTtsWithBattary: 用电TTS电量提醒开关点击,切换后状态=" + isChecked);
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
bean.setIsEnableUseageTtsWithBattary(isChecked);
ThoughtfulServiceBean.saveBean(this, bean);
LogUtils.d(TAG, "onEnableUseageTtsWithBattary: 用电TTS电量提醒状态保存完成");
}
/**
* 充电TTS带电量提醒子开关点击事件
*/
public void onEnableChargeTtsWithBattary(View view) {
boolean isChecked = cbChargeTtsBattary.isChecked();
LogUtils.d(TAG, "onEnableChargeTtsWithBattary: 充电TTS电量提醒开关点击,切换后状态=" + isChecked);
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
bean.setIsEnableChargeTtsWithBattary(isChecked);
ThoughtfulServiceBean.saveBean(this, bean);
LogUtils.d(TAG, "onEnableChargeTtsWithBattary: 充电TTS电量提醒状态保存完成");
}
/**
* 👉 新增:允许通知电量消息时同时播放TTS语音
*/
public void onEnableTtsWhenNotifyBattery(View view) {
boolean isChecked = cbTtsWhenNotifyBattery.isChecked();
LogUtils.d(TAG, "onEnableTtsWhenNotifyBattery: 通知电量时播TTS开关状态=" + isChecked);
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
bean.setIsEnableTtsWhenNotifyBattery(isChecked);
ThoughtfulServiceBean.saveBean(this, bean);
LogUtils.d(TAG, "onEnableTtsWhenNotifyBattery: 保存完成");
}
/**
* 充电邮件通知开关点击事件
*/
public void onChargeMailNotifySwitchChanged(View view) {
boolean isChecked = switchChargeMailNotify.isChecked();
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 充电邮件通知开关状态=" + isChecked);
ChargeMailNotifyHelper.setEnabled(this, isChecked);
updateSMTPConfigViewEnabled(isChecked);
if (isChecked) {
// 开启时重启ControlCenterService
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 开启充电邮件通知,重启ControlCenterService");
stopService(new Intent(this, ControlCenterService.class));
startService(new Intent(this, ControlCenterService.class));
} else {
// 关闭时停止RemindThread并关闭悬浮窗
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 关闭充电邮件通知,停止RemindThread");
RemindThread.stopRemindThread();
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 关闭充电邮件通知悬浮窗");
ChargeMailNotifyFloatWindow.dismissWindow();
}
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 状态保存完成");
}
// ======================== 工具方法区 =========================
/**
* 获取配置Bean实例,避免重复代码
*/
private ThoughtfulServiceBean getThoughtfulServiceBean() {
LogUtils.d(TAG, "getThoughtfulServiceBean: 获取配置Bean");
ThoughtfulServiceBean bean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (bean == null) {
LogUtils.d(TAG, "getThoughtfulServiceBean: 配置Bean为空,创建新实例");
bean = new ThoughtfulServiceBean();
}
return bean;
}
/**
* 悬浮窗权限检查与请求
*/
void canDrawOverlays() {
LogUtils.d(TAG, "canDrawOverlays: 悬浮窗权限检查开始");
if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
LogUtils.d(TAG, "canDrawOverlays: 未开启悬浮窗权限,发起请求");
showDrawOverlayRequestDialog();
} else {
LogUtils.d(TAG, "canDrawOverlays: 悬浮窗权限已开启");
ToastUtils.show("悬浮窗权限已开启");
}
}
/**
* 显示悬浮窗权限请求对话框
*/
private void showDrawOverlayRequestDialog() {
LogUtils.d(TAG, "showDrawOverlayRequestDialog: 显示悬浮窗权限请求弹窗");
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
LogUtils.d(TAG, "showDrawOverlayRequestDialog-去设置: 点击跳转权限页面");
dialog.dismiss();
jumpToDrawOverlaySettings();
}
})
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
LogUtils.d(TAG, "showDrawOverlayRequestDialog-稍后: 点击取消请求");
dialog.dismiss();
}
})
.create();
if (dialog.getWindow() != null) {
dialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
dialog.show();
}
/**
* 跳转悬浮窗权限设置页面(反射适配低版本)
*/
private void jumpToDrawOverlaySettings() {
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置页面");
try {
Class<?> settingsClazz = Settings.class;
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
String action = (String) actionField.get(null);
Intent intent = new Intent(action);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转权限页面意图已发送");
} catch (Exception e) {
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
}
}
}
@@ -1,75 +0,0 @@
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;
/**
* 应用快捷方式活动类,处理应用图标快捷菜单的切换请求
* 适配 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);
LogUtils.d(TAG, "【onCreate】ShortcutActionActivity 启动,开始处理快捷方式请求");
// 处理应用图标快捷菜单的切换请求
handleSwitchRequest();
LogUtils.d(TAG, "【onCreate】快捷方式请求处理完成,关闭活动");
finish();
}
// ======================== 业务逻辑方法 =========================
/**
* 处理应用图标快捷菜单的请求,根据意图数据切换应用启动组件
*/
private void handleSwitchRequest() {
Intent intent = getIntent();
if (intent == null) {
LogUtils.w(TAG, "【handleSwitchRequest】意图为空,无法处理快捷方式请求");
return;
}
String dataString = intent.getDataString();
LogUtils.d(TAG, "【handleSwitchRequest】获取到快捷指令:" + dataString);
// 匹配快捷指令并切换组件
if (ACTION_SWITCH_TO_EN1.equals(dataString)) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
String toastMsg = "切换至" + getString(R.string.app_name) + "图标";
ToastUtils.show(toastMsg);
LogUtils.d(TAG, "【handleSwitchRequest】已切换至EN1组件:" + App.COMPONENT_EN1);
} else if (ACTION_SWITCH_TO_CN1.equals(dataString)) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
String toastMsg = "切换至" + getString(R.string.app_name_cn1) + "图标";
ToastUtils.show(toastMsg);
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN1组件:" + App.COMPONENT_CN1);
} else if (ACTION_SWITCH_TO_CN2.equals(dataString)) {
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
String toastMsg = "切换至" + getString(R.string.app_name_cn2) + "图标";
ToastUtils.show(toastMsg);
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN2组件:" + App.COMPONENT_CN2);
} else {
LogUtils.w(TAG, "【handleSwitchRequest】未匹配到有效快捷指令:" + dataString);
}
}
}
@@ -1,176 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
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.R;
import java.util.ArrayList;
/**
* @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";
// ======================== 成员变量 =========================
protected volatile AESThemeBean.ThemeType mThemeType; // 当前主题类型
// ======================== 接口实现 & 抽象方法 =========================
@Override
public abstract Activity getActivity();
@Override
public abstract String getTag();
// ======================== 生命周期方法 =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化开始", getTag()));
// 初始化主题样式ID列表(远程AAR要求init先于getThemeTypeID调用)
initAESThemeStyles();
// 初始化主题
mThemeType = getThemeType();
setThemeStyle();
super.onCreate(savedInstanceState);
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化完成,当前主题:%s", getTag(), mThemeType));
}
@Override
protected void onStart() {
super.onStart();
}
@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()));
}
// ======================== 主题相关方法 =========================
/**
* 初始化 AESThemeUtil 内部主题样式ID列表
* 远程 AAR 15.20.x 的 AESThemeUtil.getThemeStyleID 依赖此列表进行 ordinal 查找,
* 未初始化将导致 IndexOutOfBoundsException
*/
private void initAESThemeStyles() {
ArrayList<Integer> themeStyleIDs = new ArrayList<Integer>();
AESThemeBean.fillThemeStyleIDList(themeStyleIDs);
AESThemeUtil.init(themeStyleIDs);
}
/**
* 获取当前主题类型
* @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 配置方法 =========================
/**
* 配置工具栏,显示返回按钮
*/
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
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
LogUtils.d(TAG, String.format("【%s-onOptionsItemSelected】点击返回菜单", getTag()));
return true;
}
return super.onOptionsItemSelected(item);
}
@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) {
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;
flag |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
decorView.setSystemUiVisibility(flag);
// 配置窗口标志位
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
LogUtils.d(TAG, String.format("【%s-changeFullScreen】已切换至全屏模式", getTag()));
}
}
@@ -1,107 +0,0 @@
package cc.winboll.studio.powerbell.adapters;
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.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 static final String FORMAT_BATTERY_LEVEL = "%d%%"; // 电量显示格式
private static final String PREFIX_DISCHARGE_TIME = "使用时间: "; // 放电时间前缀
private static final String PREFIX_CHARGE_TIME = "充电时间: "; // 充电时间前缀
// ======================== 成员变量 =========================
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);
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(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() {
int count = dataList.size();
LogUtils.d(TAG, "【getItemCount】获取条目数量:" + count);
return count;
}
// ======================== ViewHolder 内部类 =========================
static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvLevel; // 电量显示
TextView tvDischargeTime; // 放电时间显示
TextView tvChargeTime; // 充电时间显示
ViewHolder(View itemView) {
super(itemView);
// 初始化视图控件
tvLevel = itemView.findViewById(R.id.tvLevel);
tvDischargeTime = itemView.findViewById(R.id.tvDischargeTime);
tvChargeTime = itemView.findViewById(R.id.tvChargeTime);
LogUtils.d(TAG, "【ViewHolder】控件初始化完成");
}
}
}
@@ -1,153 +0,0 @@
package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
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;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
/**
* 背景图片的接收分享文件后的预览对话框
* 适配 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 = "接收到的文件为空。"; // 空文件提示文本
// ======================== 成员变量 =========================
private Context mContext; // 上下文对象
private IOnRecivedPictureListener mIOnRecivedPictureListener; // 图片接收监听
private Uri mUriRecivedPicture; // 接收的图片Uri
// 控件对象
private BackgroundView mBackgroundView; // 背景预览视图
private Button dialogbackgroundpicturepreviewButton1; // 取消按钮
private Button dialogbackgroundpicturepreviewButton2; // 确认按钮
// ======================== 接口定义 =========================
/**
* 图片接收监听接口,用于通知确认选择的图片Uri
*/
public interface IOnRecivedPictureListener {
void onAcceptRecivedPicture(Uri uriRecivedPicture);
}
// ======================== 构造方法 =========================
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
super(context);
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化开始");
// 初始化成员变量
mContext = context;
mIOnRecivedPictureListener = iOnRecivedPictureListener;
// 设置布局与控件
setContentView(R.layout.dialog_backgroundpicturepreview);
initViews();
bindButtonClickEvents();
// 预览接收的图片
previewRecivedPicture();
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化完成");
}
// ======================== 视图初始化方法 =========================
/**
* 初始化对话框内所有控件
*/
private void initViews() {
mBackgroundView = findViewById(R.id.backgroundview);
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
dialogbackgroundpicturepreviewButton2 = findViewById(R.id.dialogbackgroundpicturepreviewButton2);
LogUtils.d(TAG, "【initViews】对话框控件初始化完成");
}
// ======================== 事件绑定方法 =========================
/**
* 绑定按钮点击事件
*/
private void bindButtonClickEvents() {
// 取消按钮:跳转到主页面并关闭对话框
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LogUtils.d(TAG, "【onClick】点击取消按钮,跳转到主页面");
Intent intent = new Intent(mContext, MainActivity.class);
mContext.startActivity(intent);
dismiss();
LogUtils.d(TAG, "【onClick】对话框已关闭");
}
});
// 确认按钮:通知监听并关闭对话框
dialogbackgroundpicturepreviewButton2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【onClick】点击确认按钮,通知接收图片");
if (mIOnRecivedPictureListener != null && mUriRecivedPicture != null) {
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
LogUtils.d(TAG, "【onClick】已通知监听,图片Uri:" + mUriRecivedPicture);
} else {
LogUtils.w(TAG, "【onClick】监听为空或图片Uri无效,无法通知");
}
dismiss();
LogUtils.d(TAG, "【onClick】对话框已关闭");
}
});
LogUtils.d(TAG, "【bindButtonClickEvents】按钮点击事件绑定完成");
}
// ======================== 业务逻辑方法 =========================
/**
* 预览接收的分享图片
*/
private void previewRecivedPicture() {
LogUtils.d(TAG, "【previewRecivedPicture】开始预览接收的图片");
// 校验上下文类型
if (!(mContext instanceof BackgroundSettingsActivity)) {
LogUtils.e(TAG, "【previewRecivedPicture】上下文不是BackgroundSettingsActivity,无法获取图片Uri");
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
dismiss();
return;
}
BackgroundSettingsActivity activity = (BackgroundSettingsActivity) mContext;
// 从Intent中获取图片Uri(优先getData,其次EXTRA_STREAM
mUriRecivedPicture = activity.getIntent().getData();
if (mUriRecivedPicture == null) {
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
LogUtils.d(TAG, "【previewRecivedPicture】从EXTRA_STREAM获取Uri" + mUriRecivedPicture);
} else {
LogUtils.d(TAG, "【previewRecivedPicture】从getData获取Uri" + mUriRecivedPicture);
}
// 解析Uri为文件路径
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
//App.notifyMessage(TAG, "szSrcImage : " + szSrcImage);
if (TextUtils.isEmpty(szSrcImage)) {
LogUtils.w(TAG, "【previewRecivedPicture】解析的文件路径为空");
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
dismiss();
return;
}
// 加载图片到预览视图
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
mBackgroundView.loadImage(nCurrentPixelColor, szSrcImage, true);
LogUtils.d(TAG, "【previewRecivedPicture】图片预览完成,文件路径:" + szSrcImage);
}
}
@@ -1,733 +0,0 @@
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;
}
}
}
}
}
@@ -1,340 +0,0 @@
package cc.winboll.studio.powerbell.dialogs;
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;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
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;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 20:11
* @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; // 图片加载失败消息标识
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
/**
* 按钮点击回调接口(Java7 接口实现)
*/
public interface OnDialogClickListener {
void onConfirm(String szConfirmFilePath); // 确认按钮点击,返回图片路径
void onCancel(); // 取消按钮点击
}
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
// 核心数据
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)
* @param context 上下文
*/
public NetworkBackgroundDialog(@NonNull Context context) {
super(context);
LogUtils.d(TAG, "NetworkBackgroundDialog: 基础构造初始化");
initHandler();
initView();
setDismissListener();
}
/**
* 带回调的构造(便于外部处理点击事件)
* @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() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 对话框已消失时,不再处理 UI 消息
if (!isShowing()) {
LogUtils.d(TAG, "handleMessage: 对话框已消失,忽略消息");
return;
}
switch (msg.what) {
case MSG_IMAGE_LOAD_SUCCESS:
// 图片加载成功,获取文件路径并设置背景
mDownloadSavedPath = (String) msg.obj;
LogUtils.d(TAG, String.format("handleMessage: 图片加载成功,保存路径:%s", mDownloadSavedPath));
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
mBackgroundView.loadImage(nCurrentPixelColor, mDownloadSavedPath, true);
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 初始化完成");
}
/**
* 设置对话框消失监听:移除 Handler 消息,避免内存泄漏
*/
private void setDismissListener() {
this.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
// 对话框消失时,移除所有未处理的消息和回调
if (mUiHandler != null) {
mUiHandler.removeCallbacksAndMessages(null);
LogUtils.d(TAG, "onDismiss: 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);
// 设置对话框内容视图
setView(dialogView);
// 绑定控件
tvTitle = (TextView) dialogView.findViewById(R.id.tv_dialog_title);
tvContent = (TextView) dialogView.findViewById(R.id.tv_dialog_content);
btnCancel = (Button) dialogView.findViewById(R.id.btn_cancel);
btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm);
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);
// 控件非空校验
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: 布局和控件初始化完成");
}
/**
* 设置按钮点击监听
*/
private void setButtonClickListeners() {
// 取消按钮:关闭对话框 + 回调外部
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onClick: 取消按钮点击");
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.setCurrentSourceToPreview();
dismiss(); // 关闭对话框
if (listener != null) {
listener.onCancel();
LogUtils.d(TAG, "onClick: 取消回调已执行");
}
}
});
// 确认按钮:关闭对话框 + 回调外部
btnConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onClick: 确认按钮点击");
dismiss(); // 关闭对话框
if (TextUtils.isEmpty(mDownloadSavedPath)) {
ToastUtils.show("未下载图片。");
LogUtils.w(TAG, "onClick: 确认失败,未下载图片");
return;
}
if (listener != null) {
listener.onConfirm(mDownloadSavedPath);
LogUtils.d(TAG, String.format("onClick: 确认回调已执行,图片路径:%s", mDownloadSavedPath));
}
}
});
// 图片预览按钮:预览输入框地址图片
btnPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onClick: 预览按钮点击");
downloadImageToAlbumAndPreview();
}
});
LogUtils.d(TAG, "setButtonClickListeners: 按钮点击监听已设置");
}
// ====================== 业务逻辑方法(图片下载、预览) ======================
/**
* 下载网络图片并预览
*/
void downloadImageToAlbumAndPreview() {
mPreviewFileUrl = etURL.getText().toString().trim();
if (TextUtils.isEmpty(mPreviewFileUrl)) {
ToastUtils.show("请输入图片URL");
LogUtils.w(TAG, "downloadImageToAlbumAndPreview: 图片URL为空");
return;
}
LogUtils.d(TAG, String.format("downloadImageToAlbumAndPreview: 开始下载图片,URL%s", mPreviewFileUrl));
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback() {
@Override
public void onSuccess(String savePath) {
LogUtils.d(TAG, String.format("onSuccess: 图片下载成功,保存路径:%s", savePath));
// 发送消息到主线程,携带图片路径
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
mUiHandler.sendMessage(successMsg);
}
@Override
public void onFailure(String errorMsg) {
LogUtils.e(TAG, String.format("onFailure: 图片下载失败,错误信息:%s", errorMsg));
ToastUtils.show("下载失败:" + errorMsg);
// 发送图片加载失败消息
Message failMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_FAILED);
mUiHandler.sendMessage(failMsg);
}
});
}
/**
* 根据文件路径设置 BackgroundView 背景(主线程调用)
* @param 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: 按钮点击回调已设置");
}
}
@@ -1,136 +0,0 @@
package cc.winboll.studio.powerbell.handlers;
import android.os.Handler;
import android.os.Message;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.threads.TTSRemindThread;
import cc.winboll.studio.powerbell.utils.ChargeMailNotifyHelper;
import java.lang.ref.WeakReference;
/**
* 服务通信Handler
* 功能:处理电量提醒消息,构建并发送标准化通知
* 特性:弱引用防泄漏、参数严格校验、通知格式统一
* 适配:Java7 | API30 | 小米手机
*/
public class ControlCenterServiceHandler extends Handler {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "ControlCenterServiceHandler";
public static final int MSG_REMIND_TEXT = 1001; // 电量提醒消息标识
// 提醒类型常量
private static final String REMIND_TYPE_CHARGE = "+";
private static final String REMIND_TYPE_USAGE = "-";
// 电量范围常量
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) {
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:
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 (!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. 构建提醒内容
String chargeStateDesc = isCharging ? CHARGE_STATE_CHARGING : CHARGE_STATE_NOT_CHARGING;
String content;
if (REMIND_TYPE_CHARGE.equals(remindType)) {
content = String.format(CHARGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc);
} else {
content = String.format(USAGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc);
}
LogUtils.d(TAG, "handleRemindMessage: 提醒内容构建完成 | content=" + content);
// 3. 根据 ChargeMailNotify 状态选择提醒方式
if (ChargeMailNotifyHelper.isEnabled(service)) {
// 启用邮件通知时,使用 Toast 代替通知栏
LogUtils.d(TAG, "handleRemindMessage: 充电邮件通知已启用,使用 Toast 提醒");
ToastUtils.show(content);
} else {
// 未启用邮件通知时,使用通知栏提醒
if (service.getNotificationManager() == null) {
LogUtils.e(TAG, "handleRemindMessage: 通知管理工具类未初始化,无法发送提醒");
return;
}
NotificationMessage remindMsg = new NotificationMessage();
if (REMIND_TYPE_CHARGE.equals(remindType)) {
remindMsg.setTitle(CHARGE_REMIND_TITLE);
remindMsg.setRemindMSG("charge_remind");
} else {
remindMsg.setTitle(USAGE_REMIND_TITLE);
remindMsg.setRemindMSG("usage_remind");
}
remindMsg.setContent(content);
LogUtils.d(TAG, "handleRemindMessage: 调用通知工具类发送提醒 | remindMSG=" + remindMsg.getRemindMSG());
service.getNotificationManager().showRemindNotification(service, remindMsg);
}
LogUtils.d(TAG, "handleRemindMessage: 提醒流程执行完毕");
}
}
@@ -1,274 +0,0 @@
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.models.libs1520000.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;
// 日志标签(全局统一)
transient public static final String TAG = "AppConfigBean";
// 字段校验常量(统一阈值,避免硬编码)
private static final int MIN_INTERVAL = 500; // 最小检测间隔(ms
private static final int MIN_REMIND_INTERVAL = 1000;// 最小提醒间隔(ms
private static final int BATTERY_MIN = 0; // 电量最小值
private static final int BATTERY_MAX = 100; // 电量最大值
private static final int INVALID_BATTERY = -1; // 无效电量标识
private static final int DEFAULT_FRAME_WIDTH = 500; // 默认相框宽度(px
private static final int DEFAULT_FRAME_HEIGHT = 500;// 默认相框高度(px
// ====================== 成员变量区(按功能分类:提醒配置→电量状态→检测配置→相框配置) ======================
// 耗电提醒配置
boolean isEnableUsageReminder = false; // 耗电提醒开关
int usageReminderValue = 45; // 耗电提醒阈值(0-100
// 充电提醒配置
boolean isEnableChargeReminder = false;// 充电提醒开关
int chargeReminderValue = 100; // 充电提醒阈值(0-100
// 铃声提醒配置
int reminderIntervalTime = 5000; // 铃声提醒间隔(ms
// 电量状态
boolean isCharging = false; // 是否充电
// 电量检测配置
int batteryDetectInterval = 2000; // 电量检测间隔(ms,适配 RemindThread
// 相框配置
int defaultFrameWidth = DEFAULT_FRAME_WIDTH; // 默认相框宽度(px
int defaultFrameHeight = DEFAULT_FRAME_HEIGHT;// 默认相框高度(px
// ====================== 构造方法(初始化默认配置,强化默认值校验) ======================
public AppConfigBean() {
setChargeReminderValue(100);
setEnableChargeReminder(false);
setUsageReminderValue(10);
setEnableUsageReminder(false);
setReminderIntervalTime(5000);
setBatteryDetectInterval(1000);
setDefaultFrameWidth(DEFAULT_FRAME_WIDTH);
setDefaultFrameHeight(DEFAULT_FRAME_HEIGHT);
LogUtils.d(TAG, "AppConfigBean() 构造器执行 | 默认配置初始化完成");
}
// ====================== 核心业务方法(Setter/Getter,按字段功能分类,补充调试日志) ======================
// --------------- 充电状态相关 ---------------
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;
}
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;
}
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;
}
// --------------- 铃声提醒配置相关 ---------------
public void setReminderIntervalTime(int reminderIntervalTime) {
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
LogUtils.d(TAG, String.format("setReminderIntervalTime() 执行 | 最终间隔=%dms | 输入值=%dms", this.reminderIntervalTime, reminderIntervalTime));
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
// --------------- 电量检测配置相关 ---------------
public void setBatteryDetectInterval(int batteryDetectInterval) {
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 执行 | 最终间隔=%dms | 输入值=%dms", this.batteryDetectInterval, batteryDetectInterval));
}
public int getBatteryDetectInterval() {
return batteryDetectInterval;
}
// --------------- 相框配置相关 ---------------
public void setDefaultFrameWidth(int defaultFrameWidth) {
this.defaultFrameWidth = defaultFrameWidth;
LogUtils.d(TAG, String.format("setDefaultFrameWidth() 执行 | 最终宽度=%dpx | 输入值=%dpx", this.defaultFrameWidth, defaultFrameWidth));
}
public int getDefaultFrameWidth() {
return defaultFrameWidth;
}
public void setDefaultFrameHeight(int defaultFrameHeight) {
this.defaultFrameHeight = defaultFrameHeight;
LogUtils.d(TAG, String.format("setDefaultFrameHeight() 执行 | 最终高度=%dpx | 输入值=%dpx", this.defaultFrameHeight, defaultFrameHeight));
}
public int getDefaultFrameHeight() {
return defaultFrameHeight;
}
// ====================== 父类重写方法(JSON 序列化/反序列化,兼容旧配置) ======================
@Override
public String getName() {
return AppConfigBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
LogUtils.d(TAG, "writeThisToJsonWriter() 执行 | 开始JSON序列化");
// 原有字段序列化
jsonWriter.name("isEnableUsageReminder").value(isEnableUsageReminder());
jsonWriter.name("usageReminderValue").value(getUsageReminderValue());
jsonWriter.name("isEnableChargeReminder").value(isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(getChargeReminderValue());
jsonWriter.name("reminderIntervalTime").value(getReminderIntervalTime());
jsonWriter.name("isCharging").value(isCharging());
// 新增字段序列化(检测配置)
jsonWriter.name("batteryDetectInterval").value(getBatteryDetectInterval());
// 新增字段序列化(相框配置)
jsonWriter.name("defaultFrameWidth").value(getDefaultFrameWidth());
jsonWriter.name("defaultFrameHeight").value(getDefaultFrameHeight());
LogUtils.d(TAG, "writeThisToJsonWriter() 完成 | JSON序列化成功");
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader() 执行 | 开始JSON反序列化");
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());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isEnableUsageReminder()));
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
bean.setUsageReminderValue(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getUsageReminderValue()));
} else if (name.equals("isEnableChargeReminder")) {
bean.setEnableChargeReminder(jsonReader.nextBoolean());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isEnableChargeReminder()));
} else if (name.equals("chargeReminderValue")) {
bean.setChargeReminderValue(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getChargeReminderValue()));
} else if (name.equals("reminderIntervalTime")) {
bean.setReminderIntervalTime(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getReminderIntervalTime()));
} else if (name.equals("isCharging")) {
bean.setIsCharging(jsonReader.nextBoolean());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isCharging()));
} else if (name.equals("batteryDetectInterval")) {
bean.setBatteryDetectInterval(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getBatteryDetectInterval()));
} else if (name.equals("defaultFrameWidth")) {
bean.setDefaultFrameWidth(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getDefaultFrameWidth()));
} else if (name.equals("defaultFrameHeight")) {
bean.setDefaultFrameHeight(jsonReader.nextInt());
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getDefaultFrameHeight()));
} else {
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) {
LogUtils.d(TAG, "writeToParcel() 执行 | 开始Parcel序列化");
// 按成员变量顺序写入,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(batteryDetectInterval);
dest.writeInt(defaultFrameWidth);
dest.writeInt(defaultFrameHeight);
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) {
LogUtils.d(TAG, "createFromParcel() 执行 | 开始Parcel反序列化");
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.batteryDetectInterval = source.readInt();
bean.defaultFrameWidth = source.readInt();
bean.defaultFrameHeight = source.readInt();
LogUtils.d(TAG, "createFromParcel() 完成 | Parcel反序列化成功");
return bean;
}
@Override
public AppConfigBean[] newArray(int size) {
return new AppConfigBean[size];
}
};
}
@@ -1,296 +0,0 @@
package cc.winboll.studio.powerbell.models;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.models.libs1520000.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 应用背景图片数据类
* 适配 API30,支持 Serializable 持久化、JSON 序列化/反序列化
* 存储正式/预览背景配置,包含原图、压缩图、裁剪比例、像素颜色等核心字段
*/
public class BackgroundBean extends BaseBean implements Serializable {
// ====================== 静态常量(首屏可见,统一管理) ======================
// 日志标签(全局统一,替换 Log 为 LogUtils
public static final String TAG = "BackgroundBean";
// 兼容旧字段常量(统一管理,避免硬编码)
private static final String OLD_FIELD_USE_SCALED_COMPRESS = "isUseScaledCompress";
// 字段默认值常量(统一管理,避免魔法值)
private static final int DEFAULT_DIMENSION = 100; // 默认宽高
private static final int MIN_DIMENSION = 1; // 最小宽高
// ====================== 成员变量(按功能分类:原图配置→压缩图配置→控制字段→裁剪配置→像素颜色) ======================
// 原图配置
private String backgroundFileName = ""; // 背景图片文件名
private String backgroundFilePath = ""; // 背景图片完整路径
private String backgroundFileInfo = ""; // 图片信息(Uri、网络地址等)
// 压缩图配置
private String backgroundScaledCompressFileName = ""; // 压缩后背景图片文件名
private String backgroundScaledCompressFilePath = ""; // 压缩后背景图片完整路径
// 控制字段
private boolean isUseBackgroundFile = false; // 是否启用背景图片
private boolean isUseBackgroundScaledCompressFile = false; // 是否启用压缩背景图(重命名:原isUseScaledCompress
// 裁剪配置
private int backgroundWidth = DEFAULT_DIMENSION; // 背景图宽度
private int backgroundHeight = DEFAULT_DIMENSION; // 背景图高度
// 像素颜色
private int pixelColor = 0xFFFFFFFF; // 拾取的像素颜色(纯色背景用)
// ====================== 构造方法(无参构造,JSON反序列化必备) ======================
/**
* 无参构造器(必须,JSON反序列化时需默认构造器)
*/
public BackgroundBean() {
LogUtils.d(TAG, "BackgroundBean: 无参构造初始化完成");
}
// ====================== Getter/Setter 方法(按功能分类,补充调试日志,强化校验) ======================
// --------------- 原图配置相关 ---------------
public String getBackgroundFileName() {
return backgroundFileName;
}
public void setBackgroundFileName(String backgroundFileName) {
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName;
LogUtils.d(TAG, String.format("setBackgroundFileName: 背景文件名设置为 %s", this.backgroundFileName));
}
public String getBackgroundFilePath() {
return backgroundFilePath;
}
public void setBackgroundFilePath(String backgroundFilePath) {
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath;
LogUtils.d(TAG, String.format("setBackgroundFilePath: 背景文件路径设置为 %s", this.backgroundFilePath));
}
public String getBackgroundFileInfo() {
return backgroundFileInfo;
}
public void setBackgroundFileInfo(String backgroundFileInfo) {
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;
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFileName: 压缩背景文件名设置为 %s", this.backgroundScaledCompressFileName));
}
public String getBackgroundScaledCompressFilePath() {
return backgroundScaledCompressFilePath;
}
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath;
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFilePath: 压缩背景文件路径设置为 %s", this.backgroundScaledCompressFilePath));
}
/**
* 重命名:原isUseScaledCompress → 新isUseBackgroundScaledCompressFileGetter/Setter同步修改)
* 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆
*/
public boolean isUseBackgroundScaledCompressFile() {
return isUseBackgroundScaledCompressFile;
}
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 < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundWidth;
LogUtils.d(TAG, String.format("setBackgroundWidth: 背景宽度设置为 %d(输入值:%d)", this.backgroundWidth, backgroundWidth));
}
public int getBackgroundHeight() {
return backgroundHeight;
}
public void setBackgroundHeight(int 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() {
String className = BackgroundBean.class.getName();
LogUtils.d(TAG, String.format("getName: 类名标识为 %s", className));
return className;
}
/**
* 序列化:同步重命名字段(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile
* 确保新字段能正常持久化,同时兼容旧版本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("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(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 序列化完成,已兼容旧字段");
}
/**
* 反序列化:同步处理重命名字段(兼容旧版本JSON,新旧字段都能读取)
* 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效)
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
BackgroundBean bean = new BackgroundBean();
jsonReader.beginObject();
// 临时变量:存储旧字段值(用于兼容)
boolean tempUseScaledCompress = false;
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
switch (name) {
case "backgroundFileName":
bean.setBackgroundFileName(jsonReader.nextString());
break;
case "backgroundFilePath":
bean.setBackgroundFilePath(jsonReader.nextString());
break;
case "backgroundFileInfo":
bean.setBackgroundFileInfo(jsonReader.nextString());
break;
case "isUseBackgroundFile":
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
break;
case "backgroundScaledCompressFileName":
bean.setBackgroundScaledCompressFileName(jsonReader.nextString());
break;
case "backgroundScaledCompressFilePath":
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
break;
case "isUseBackgroundScaledCompressFile":
// 关键:读取新字段(优先)
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 isUseBackgroundScaledCompressFile 完成");
break;
case OLD_FIELD_USE_SCALED_COMPRESS:
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
tempUseScaledCompress = jsonReader.nextBoolean();
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 isUseScaledCompress 完成");
break;
case "backgroundWidth":
bean.setBackgroundWidth(jsonReader.nextInt());
break;
case "backgroundHeight":
bean.setBackgroundHeight(jsonReader.nextInt());
break;
case "pixelColor":
bean.setPixelColor(jsonReader.nextInt());
break;
default:
jsonReader.skipValue();
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
break;
}
}
jsonReader.endObject();
// 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充
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.backgroundScaledCompressFileName = "";
this.backgroundScaledCompressFilePath = "";
this.backgroundFileInfo = "";
this.isUseBackgroundFile = false;
this.isUseBackgroundScaledCompressFile = false;
this.backgroundWidth = DEFAULT_DIMENSION;
this.backgroundHeight = DEFAULT_DIMENSION;
LogUtils.d(TAG, "resetBackgroundConfig: 背景配置已重置为默认值");
}
/**
* 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验)
* 同步使用重命名字段判断压缩图是否启用
* @return true-配置有效(可显示背景图),false-配置无效
*/
public boolean isBackgroundConfigValid() {
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
if (!isUseBackgroundFile) {
LogUtils.d(TAG, "isBackgroundConfigValid: 未启用背景图,配置无效");
return false;
}
// 原图校验:路径非空 或 文件名非空
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
// 压缩图校验:启用压缩图时,路径/文件名需非空
boolean isCompressValid = true;
if (isUseBackgroundScaledCompressFile()) {
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
}
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
boolean isValid = isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
LogUtils.d(TAG, String.format("isBackgroundConfigValid: 背景配置有效性为 %b(启用压缩图:%b,原图有效:%b,压缩图有效:%b)",
isValid, isUseBackgroundScaledCompressFile(), isOriginalValid, isCompressValid));
return isValid;
}
}
@@ -1,82 +0,0 @@
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;
}
}
@@ -1,130 +0,0 @@
package cc.winboll.studio.powerbell.models;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.models.libs1520000.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;
}
}
@@ -1,12 +0,0 @@
package cc.winboll.studio.powerbell.models;
/**
* 电池绘制样式枚举 (单选选项)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public enum BatteryStyle {
ENERGY_STYLE, // 能量样式
ZEBRA_STYLE, // 条纹样式
POINT_STYLE // 点阵样式
}
@@ -1,131 +0,0 @@
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.models.libs1520000.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));
}
}
@@ -1,75 +0,0 @@
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;
}
}
@@ -1,47 +0,0 @@
package cc.winboll.studio.powerbell.models;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.Serializable;
/**
* TTS 语音播放文本内容实体类
* 适配:Java7 语法规范 | Android API30 系统版本
* 特性:实现序列化接口,支持跨页面/进程传递,属性默认值初始化
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:13
*/
public class TTSSpeakTextBean implements Serializable {
// ====================================== 常量区 - 置顶排序 ======================================
/** 日志TAG 瞬态修饰,不参与序列化,减少序列化体积 */
transient public static final String TAG = "TTSSpeakTextBean";
// ====================================== 成员属性区 - 业务属性排序 ======================================
/** 延迟播放时长 单位:毫秒,默认值0:无延迟播放 */
public int mnDelay = 0;
/** TTS语音播放文本内容,默认值空字符串:防止空指针 */
public String mszSpeakContent = "";
// ====================================== 构造方法区 - 无参+有参 完整实现 ======================================
/**
* 无参构造方法
* Java7序列化规范必备 + 兼容反射实例化场景
*/
public TTSSpeakTextBean() {
LogUtils.d(TAG, "【无参构造】TTSSpeakTextBean 实例化,使用默认值 | 延迟:" + mnDelay + " | 文本:" + mszSpeakContent);
}
/**
* 有参构造方法【主构造】
* @param nDelay 延迟播放时长(ms)
* @param szSpeakContent 语音播放文本内容
*/
public TTSSpeakTextBean(int nDelay, String szSpeakContent) {
LogUtils.d(TAG, "【有参构造】TTSSpeakTextBean 实例化,入参 | 延迟:" + nDelay + " | 文本:" + szSpeakContent);
this.mnDelay = nDelay;
this.mszSpeakContent = szSpeakContent;
LogUtils.d(TAG, "【有参构造】赋值完成 | 最终延迟:" + this.mnDelay + " | 最终文本:" + this.mszSpeakContent);
}
}
@@ -1,196 +0,0 @@
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.models.libs1520000.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 20:59
* @Describe 贴心服务配置实体类 (适配API30 / Java7)
*/
public class ThoughtfulServiceBean extends BaseBean implements Parcelable, Serializable {
// ====================== 常量区 - 置顶统一管理 ======================
public static final String TAG = ThoughtfulServiceBean.class.getSimpleName();
private static final long serialVersionUID = 1L; // Serializable 序列化兼容必备
// JSON序列化字段常量 杜绝硬编码
public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS = "isEnableChargeTts";
public static final String JSON_FIELD_IS_ENABLE_USE_POWER_TTS = "isEnableUsePowerTts";
public static final String JSON_FIELD_IS_ENABLE_USAGE_TTS_WITH_BATTERY = "isEnableUseageTtsWithBattary";
public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS_WITH_BATTERY = "isEnableChargeTtsWithBattary";
// 👉 新增:通知电量消息时同时播放TTS
public static final String JSON_FIELD_IS_ENABLE_TTS_WHEN_NOTIFY_BATTERY = "isEnableTtsWhenNotifyBattery";
// ====================== 核心成员变量 - 私有封装 ======================
private boolean isEnableChargeTts = false; // 是否启用 充电TTS贴心语音服务
private boolean isEnableUsePowerTts = false; // 是否启用 用电TTS贴心语音服务
private boolean isEnableUseageTtsWithBattary = false; // 用电TTS加入电量提醒
private boolean isEnableChargeTtsWithBattary = false; // 充电TTS加入电量提醒
private boolean isEnableTtsWhenNotifyBattery = false; // 👉 允许通知电量消息时同时播放TTS语音
// ====================== Parcelable 静态创建器 ======================
public static final Creator<ThoughtfulServiceBean> CREATOR = new Creator<ThoughtfulServiceBean>() {
@Override
public ThoughtfulServiceBean createFromParcel(Parcel source) {
return new ThoughtfulServiceBean(source);
}
@Override
public ThoughtfulServiceBean[] newArray(int size) {
LogUtils.d(TAG, "newArray: 初始化数组,size = " + size);
return new ThoughtfulServiceBean[size];
}
};
// ====================== 构造方法区 ======================
public ThoughtfulServiceBean() {
LogUtils.d(TAG, "ThoughtfulServiceBean: 无参构造,初始化默认禁用所有TTS服务");
}
public ThoughtfulServiceBean(boolean isEnableChargeTts, boolean isEnableUsePowerTts,
boolean isEnableUseageTtsWithBattary, boolean isEnableChargeTtsWithBattary,
boolean isEnableTtsWhenNotifyBattery) {
this.isEnableChargeTts = isEnableChargeTts;
this.isEnableUsePowerTts = isEnableUsePowerTts;
this.isEnableUseageTtsWithBattary = isEnableUseageTtsWithBattary;
this.isEnableChargeTtsWithBattary = isEnableChargeTtsWithBattary;
this.isEnableTtsWhenNotifyBattery = isEnableTtsWhenNotifyBattery;
LogUtils.d(TAG, "ThoughtfulServiceBean: 全参构造 | 充电TTS=" + isEnableChargeTts + " | 用电TTS=" + isEnableUsePowerTts
+ " | 用电TTS加电量=" + isEnableUseageTtsWithBattary + " | 充电TTS加电量=" + isEnableChargeTtsWithBattary
+ " | 通知电量时播TTS=" + isEnableTtsWhenNotifyBattery);
}
private ThoughtfulServiceBean(Parcel in) {
this.isEnableChargeTts = in.readByte() != 0;
this.isEnableUsePowerTts = in.readByte() != 0;
this.isEnableUseageTtsWithBattary = in.readByte() != 0;
this.isEnableChargeTtsWithBattary = in.readByte() != 0;
this.isEnableTtsWhenNotifyBattery = in.readByte() != 0; // 👉 新增反序列化
LogUtils.d(TAG, "ThoughtfulServiceBean: Parcel构造解析完成");
}
// ====================== Getter/Setter 方法区 ======================
public boolean isEnableChargeTts() {
return isEnableChargeTts;
}
public void setIsEnableChargeTts(boolean isEnableChargeTts) {
LogUtils.d(TAG, "setIsEnableChargeTts: 旧值=" + this.isEnableChargeTts + " 新值=" + isEnableChargeTts);
this.isEnableChargeTts = isEnableChargeTts;
}
public boolean isEnableUsePowerTts() {
return isEnableUsePowerTts;
}
public void setIsEnableUsePowerTts(boolean isEnableUsePowerTts) {
LogUtils.d(TAG, "setIsEnableUsePowerTts: 旧值=" + this.isEnableUsePowerTts + " 新值=" + isEnableUsePowerTts);
this.isEnableUsePowerTts = isEnableUsePowerTts;
}
public boolean isEnableUseageTtsWithBattary() {
return isEnableUseageTtsWithBattary;
}
public void setIsEnableUseageTtsWithBattary(boolean isEnableUseageTtsWithBattary) {
LogUtils.d(TAG, "setIsEnableUseageTtsWithBattary: 旧值=" + this.isEnableUseageTtsWithBattary + " 新值=" + isEnableUseageTtsWithBattary);
this.isEnableUseageTtsWithBattary = isEnableUseageTtsWithBattary;
}
public boolean isEnableChargeTtsWithBattary() {
return isEnableChargeTtsWithBattary;
}
public void setIsEnableChargeTtsWithBattary(boolean isEnableChargeTtsWithBattary) {
LogUtils.d(TAG, "setIsEnableChargeTtsWithBattary: 旧值=" + this.isEnableChargeTtsWithBattary + " 新值=" + isEnableChargeTtsWithBattary);
this.isEnableChargeTtsWithBattary = isEnableChargeTtsWithBattary;
}
// 👉 新增:通知电量时播放TTS
public boolean isEnableTtsWhenNotifyBattery() {
return isEnableTtsWhenNotifyBattery;
}
public void setIsEnableTtsWhenNotifyBattery(boolean isEnableTtsWhenNotifyBattery) {
LogUtils.d(TAG, "setIsEnableTtsWhenNotifyBattery: 旧值=" + this.isEnableTtsWhenNotifyBattery + " 新值=" + isEnableTtsWhenNotifyBattery);
this.isEnableTtsWhenNotifyBattery = isEnableTtsWhenNotifyBattery;
}
// ====================== 重写父类 BaseBean 核心方法 ======================
@Override
public String getName() {
String className = ThoughtfulServiceBean.class.getName();
LogUtils.d(TAG, "getName: 返回当前实体类名 = " + className);
return className;
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS).value(this.isEnableChargeTts);
jsonWriter.name(JSON_FIELD_IS_ENABLE_USE_POWER_TTS).value(this.isEnableUsePowerTts);
jsonWriter.name(JSON_FIELD_IS_ENABLE_USAGE_TTS_WITH_BATTERY).value(this.isEnableUseageTtsWithBattary);
jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS_WITH_BATTERY).value(this.isEnableChargeTtsWithBattary);
jsonWriter.name(JSON_FIELD_IS_ENABLE_TTS_WHEN_NOTIFY_BATTERY).value(this.isEnableTtsWhenNotifyBattery); // 👉 新增JSON写入
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成,所有TTS服务状态已写入");
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
ThoughtfulServiceBean bean = new ThoughtfulServiceBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
switch (fieldName) {
case JSON_FIELD_IS_ENABLE_CHARGE_TTS:
bean.setIsEnableChargeTts(jsonReader.nextBoolean());
break;
case JSON_FIELD_IS_ENABLE_USE_POWER_TTS:
bean.setIsEnableUsePowerTts(jsonReader.nextBoolean());
break;
case JSON_FIELD_IS_ENABLE_USAGE_TTS_WITH_BATTERY:
bean.setIsEnableUseageTtsWithBattary(jsonReader.nextBoolean());
break;
case JSON_FIELD_IS_ENABLE_CHARGE_TTS_WITH_BATTERY:
bean.setIsEnableChargeTtsWithBattary(jsonReader.nextBoolean());
break;
case JSON_FIELD_IS_ENABLE_TTS_WHEN_NOTIFY_BATTERY:
bean.setIsEnableTtsWhenNotifyBattery(jsonReader.nextBoolean()); // 👉 新增JSON读取
break;
default:
jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段 = " + fieldName);
break;
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成,生成实体对象");
return bean;
}
// ====================== Parcelable 接口方法 ======================
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (isEnableChargeTts ? 1 : 0));
dest.writeByte((byte) (isEnableUsePowerTts ? 1 : 0));
dest.writeByte((byte) (isEnableUseageTtsWithBattary ? 1 : 0));
dest.writeByte((byte) (isEnableChargeTtsWithBattary ? 1 : 0));
dest.writeByte((byte) (isEnableTtsWhenNotifyBattery ? 1 : 0)); // 👉 新增Parcel写入
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成,所有TTS服务状态已写入");
}
}
@@ -1,227 +0,0 @@
package cc.winboll.studio.powerbell.receivers;
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.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;
import cc.winboll.studio.powerbell.services.ThoughtfulService;
/**
* 控制中心广播接收器
* 功能:监听电池状态变化、前台通知更新、配置变更指令
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/19 20:23
* @Describe 统一处理系统与应用内广播,同步电池状态与配置,保障多线程数据一致性
*/
public class ControlCenterServiceReceiver extends BroadcastReceiver {
// ====================== 静态常量区 ======================
public static final String TAG = "ControlCenterServiceReceiver";
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";
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;
private static final int INVALID_BATTERY = -1;
// ====================== 静态状态(防抖动 + 防重复播报) ======================
private static volatile int sLastBatteryLevel = INVALID_BATTERY;
private static volatile boolean sIsCharging = false;
// 【新增】防重复触发:3秒内只允许一次播报(关键修复)
private static final long MIN_TRIGGER_INTERVAL = 3000;
private static long sLastTriggerTime = 0;
// ====================== 成员变量 ======================
private WeakReference<ControlCenterService> mwrControlCenterService;
private boolean isRegistered = false;
// ====================== 构造 ======================
public ControlCenterServiceReceiver(ControlCenterService service) {
LogUtils.d(TAG, String.format("ControlCenterServiceReceiver() 构造 | 服务:%s",
service != null ? service.getClass().getSimpleName() : "null"));
this.mwrControlCenterService = new WeakReference<>(service);
}
// ====================== 广播入口 ======================
@Override
public void onReceive(Context context, Intent intent) {
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() 终止:参数无效");
return;
}
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
if (service == null || service.isDestroyed()) {
LogUtils.e(TAG, "onReceive() 终止:服务已销毁");
unregisterAction(context);
return;
}
switch (action) {
case Intent.ACTION_BATTERY_CHANGED:
handleBatteryStateChanged(service, intent);
break;
case ACTION_UPDATE_FOREGROUND_NOTIFICATION:
handleUpdateForegroundNotification(service);
break;
case ACTION_APPCONFIG_CHANGED:
handleNotifyAppConfigUpdate(service);
break;
default:
LogUtils.w(TAG, "未知Action" + action);
}
}
// ====================== 电池状态(核心修复区) ======================
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
LogUtils.d(TAG, "handleBatteryStateChanged() 解析电池状态");
try {
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("当前:充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
// ================ 【关键修复1】状态没变直接跳过 ================
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
LogUtils.d(TAG, "状态无变化,跳过");
return;
}
// ================ 【关键修复2】只有 真正插拔充电 才播报 ================
boolean isRealPlugSwitch = (currentCharging != sIsCharging);
boolean isBatteryValid = (sLastBatteryLevel != INVALID_BATTERY);
// ================ 【关键修复3】防抖动:3秒内不重复触发 ================
long now = System.currentTimeMillis();
boolean canTrigger = (now - sLastTriggerTime >= MIN_TRIGGER_INTERVAL);
if (isRealPlugSwitch && isBatteryValid && canTrigger) {
LogUtils.d(TAG, "检测到充电状态切换 → 执行TTS提醒");
// 更新触发时间
sLastTriggerTime = now;
// 执行播报
if (currentCharging) {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.CHARGE_STATE);
} else {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.DISCHARGE_STATE);
}
}
// 更新缓存
sIsCharging = currentCharging;
sLastBatteryLevel = currentBatteryLevel;
// 同步配置
handleNotifyAppConfigUpdate(service);
} catch (Exception e) {
LogUtils.e(TAG, "handleBatteryStateChanged 异常", e);
}
}
// ====================== 配置同步 ======================
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
LogUtils.d(TAG, "handleNotifyAppConfigUpdate() 同步配置");
try {
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "配置为空,终止");
return;
}
App.sQuantityOfElectricity = sLastBatteryLevel;
latestConfig.setIsCharging(sIsCharging);
service.notifyAppConfigUpdate(latestConfig);
} catch (Exception e) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate 异常", e);
}
}
// ====================== 通知更新 ======================
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, "通知工具或消息为空");
return;
}
notifyUtils.updateForegroundServiceNotify(notifyMsg);
} catch (Exception e) {
LogUtils.e(TAG, "更新通知异常", e);
}
}
// ====================== 注册/注销 ======================
public void registerAction(Context context) {
if (context == null || isRegistered) 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, "广播注册完成");
} catch (Exception e) {
LogUtils.e(TAG, "注册异常", e);
}
}
public void unregisterAction(Context context) {
if (context == null || !isRegistered) return;
try {
context.unregisterReceiver(this);
isRegistered = false;
LogUtils.d(TAG, "广播已注销");
} catch (Exception e) {
LogUtils.w(TAG, "注销异常", e);
}
}
// ====================== 释放 ======================
public void release() {
LogUtils.d(TAG, "release() 释放资源");
if (mwrControlCenterService != null) {
mwrControlCenterService.clear();
mwrControlCenterService = null;
}
// 重置静态状态
sLastBatteryLevel = INVALID_BATTERY;
sIsCharging = false;
sLastTriggerTime = 0; // 【新增】重置防抖时间
}
// ====================== Getter ======================
public static int getLastBatteryLevel() {
return sLastBatteryLevel;
}
public static boolean isLastCharging() {
return sIsCharging;
}
}
@@ -1,180 +0,0 @@
package cc.winboll.studio.powerbell.receivers;
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.MainActivity;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;
/**
* @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;
// ====================== 静态状态标记(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) {
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) {
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;
}
// 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为空,同步失败");
}
// 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);
}
}
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
/**
* 注册广播接收器
*/
public void registerAction() {
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: 资源释放完成");
}
}
@@ -1,92 +0,0 @@
package cc.winboll.studio.powerbell.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
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.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;
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
// 历史电量值,用于校验电量变化(暂未使用,保留扩展能力)
private static volatile int sLastBatteryLevel = -1;
// ====================== 广播核心接收逻辑(入口方法,分Action处理) ======================
@Override
public void onReceive(Context context, Intent intent) {
// 基础参数校验
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);
}
}
}
@@ -1,189 +0,0 @@
package cc.winboll.studio.powerbell.services;
import android.app.Service;
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.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 static final String TAG = "AssistantService";
// 服务返回策略常量
private static final int SERVICE_RETURN_STICKY = START_STICKY;
// 服务绑定标记常量
private static final int BIND_FLAG = Context.BIND_IMPORTANT;
// API版本常量(适配前台服务启动要求)
private static final int API_LEVEL_26 = Build.VERSION_CODES.O;
// ====================== 成员变量区(按功能分层,volatile保证多线程可见性) ======================
private AppConfigUtils mAppConfigUtils;
private MyServiceConnection mMyServiceConnection;
private volatile boolean mIsThreadAlive;
// ====================== 内部类(服务连接状态监听,前置定义便于引用) ======================
/**
* 服务连接状态监听器
* 主服务连接成功时记录状态,断开时自动重连
*/
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
String className = name != null ? name.getClassName() : "null";
LogUtils.d(TAG, String.format("onServiceConnected: 主服务连接成功 | 组件名=%s | Binder=%s", className, service));
}
@Override
public void onServiceDisconnected(ComponentName name) {
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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More