Compare commits

..

85 Commits

Author SHA1 Message Date
d03c105ccd <powerbell>APK 15.14.48 release Publish. 2026-01-06 19:10:13 +08:00
5d58b3b20d 更新基础类库 2026-01-06 19:08:26 +08:00
32864c72b1 <powerbell>APK 15.14.47 release Publish. 2026-01-06 15:13:36 +08:00
a490ab3ff5 调整应用设置窗口显示信息 2026-01-06 15:12:05 +08:00
a5c5cc91e1 更新编译配置 2026-01-06 15:09:09 +08:00
5a9d068fc0 Merge branch 'winboll' into powerbell 2026-01-06 14:56:29 +08:00
cce4a643e7 编译环境参数调整 2026-01-06 14:08:30 +08:00
8405097573 Merge branch 'winboll' into powerbell 2026-01-06 12:52:47 +08:00
3d69d4da09 <powerbell>APK 15.14.46 release Publish. 2026-01-05 19:22:25 +08:00
c98685440d 添加云宝物语小店广告。 2026-01-05 19:20:19 +08:00
7183338c97 更新广支持,添加云宝小店广告。 2026-01-05 19:10:02 +08:00
61cfa7f3ff 添加微信支付测试窗口 2026-01-04 17:05:12 +08:00
cb9ad1dc57 添加企业微信测试码 2026-01-03 11:16:35 +08:00
e18ed1b0fd Merge branch 'winboll' into powerbell 2026-01-03 10:20:49 +08:00
98c334f442 移除GitHub工作流配置文件 2026-01-03 10:19:43 +08:00
9d42a3e1e9 源码整理 2026-01-02 19:08:57 +08:00
411af44303 源码整理 2026-01-02 18:53:54 +08:00
9afb072351 源码整理 2026-01-02 18:34:05 +08:00
b0c91f3ee4 <powerbell>APK 15.14.45 release Publish. 2025-12-31 20:53:46 +08:00
b21d5ecdb5 Merge remote-tracking branch 'origin/winboll' into powerbell 2025-12-31 20:47:20 +08:00
47c328cd25 <winboll>APK 15.11.8 release Publish. 2025-12-31 20:28:06 +08:00
7134d4e1c8 源码整理 2025-12-31 20:25:15 +08:00
a98c9e4214 <powerbell>APK 15.14.44 release Publish. 2025-12-30 20:07:05 +08:00
cc7bbcf2a6 <powerbell>Start New Stage Version. 2025-12-30 20:06:28 +08:00
8da6162632 应用设置UI界面优化。 2025-12-30 20:05:28 +08:00
02c135fd8c <powerbell>APK 15.14.43 release Publish. 2025-12-30 18:46:42 +08:00
2f42334f19 修复TTS语音服务在应用重启时,会播放的问题,TTS语音服务设定在充电状态与放电状态切换时播放。 2025-12-30 18:45:10 +08:00
10348d2c8d <powerbell>APK 15.14.42 release Publish. 2025-12-30 18:16:29 +08:00
21cdc219c8 更新TTS语音服务朗读文本。 2025-12-30 18:14:27 +08:00
3ebf87c642 <powerbell>APK 15.14.41 release Publish. 2025-12-29 21:40:10 +08:00
347a4040cd <powerbell>Start New Stage Version. 2025-12-29 21:37:48 +08:00
6fd86a2742 添加TTS贴心服务,以免在充电时设置了服务提醒却不知道。 2025-12-29 21:36:34 +08:00
798357aedd 源码整理 2025-12-29 09:45:01 +08:00
9124303fd3 <powerbell>APK 15.14.40 release Publish. 2025-12-28 20:43:38 +08:00
414541093a <powerbell>Start New Stage Version. 2025-12-28 20:42:24 +08:00
13265be66e 添加点阵能量块风格。 2025-12-28 20:41:19 +08:00
ebd32adb68 <powerbell>APK 15.14.39 release Publish. 2025-12-28 13:54:20 +08:00
56a65cd10a 能量风格绘图方法切换回老式算法,修复能量条间隔缝隙问题。 2025-12-28 13:52:51 +08:00
e379684002 <powerbell>APK 15.14.38 release Publish. 2025-12-28 13:16:31 +08:00
4077ac18f6 <powerbell>Start New Stage Version. 2025-12-28 13:15:17 +08:00
278e690795 能量条绘图风格调试完成。 2025-12-28 13:14:24 +08:00
e81fc65b90 BatteryStyleView控件调试中。。。 2025-12-27 21:19:20 +08:00
abd956d7d0 添加BatteryStyleView控件,添加能量与斑马绘图风格。 2025-12-27 21:12:54 +08:00
fca17908b2 <powerbell>APK 15.14.37 release Publish. 2025-12-27 14:48:34 +08:00
39a3a5aeb0 更新黑白主题风格。 2025-12-27 14:45:13 +08:00
2e30f577b5 <powerbell>APK 15.14.36 release Publish. 2025-12-26 21:00:10 +08:00
dcb5355233 添加在切换主题时把主题基准色配置为背景图背景像素底色。 2025-12-26 20:58:31 +08:00
d1ced7ac63 <powerbell>APK 15.14.35 release Publish. 2025-12-26 20:13:09 +08:00
bf0cf23144 <powerbell>Start New Stage Version. 2025-12-26 20:11:34 +08:00
890b32ceae 修复像素拾取窗口菜单栏风格未统一问题。 2025-12-26 20:10:28 +08:00
c347d51c84 简化代码使用全局变量 2025-12-26 20:04:25 +08:00
7278e9f22f 源码整理 2025-12-26 19:47:06 +08:00
4e98c8d699 优化函数使用方式 2025-12-26 18:56:24 +08:00
3ec1bbe264 源码整理 2025-12-26 18:31:25 +08:00
f4a2a1585d 源码整理 2025-12-26 18:00:24 +08:00
35f4aa8730 源码整理 2025-12-26 17:42:30 +08:00
5b1d160dac <powerbell>APK 15.14.34 release Publish. 2025-12-26 02:58:19 +08:00
db87ba51e3 修复接收分享图片后的主界面更新问题。 2025-12-26 02:56:11 +08:00
fd6e852061 优化任务进程配置 2025-12-26 02:53:41 +08:00
8ff4b9e1f4 修复全局应用资源缓存不同步问题 2025-12-26 02:20:21 +08:00
78a2d85150 20251226_002506_076 正在调试设置窗口返回时的界面刷新。。。 2025-12-26 00:25:50 +08:00
fd2d1d17ed 注释调试码 2025-12-25 20:15:25 +08:00
37f3091103 <powerbell>APK 15.14.33 release Publish. 2025-12-25 20:10:33 +08:00
6dc5f05702 调整图片分享接收后的窗口切换逻辑。 2025-12-25 20:08:36 +08:00
77df2558b3 <powerbell>APK 15.14.32 release Publish. 2025-12-25 16:51:19 +08:00
315541f2d2 添加权限申请配置属性 2025-12-25 16:50:02 +08:00
f52bb54d22 <powerbell>APK 15.14.31 release Publish. 2025-12-25 16:31:07 +08:00
0742164f65 调整资源进程配置 2025-12-25 16:27:59 +08:00
fc785b9258 <powerbell>APK 15.14.30 release Publish. 2025-12-25 16:08:51 +08:00
0abeb982d2 增加自启成功概率 2025-12-25 16:04:23 +08:00
81f0e5c56e 视图缓存控件调试成功。 2025-12-25 16:00:17 +08:00
4fcce7edb3 20251225_153508_537 2025-12-25 15:35:12 +08:00
59f5eae136 <powerbell>APK 15.14.29 release Publish. 2025-12-24 21:23:33 +08:00
bf9479c53f 源码整理,最新像素背景位图缓存功能调试完成。 2025-12-24 21:21:38 +08:00
f4a485f1ff 线程提醒电量问题调试完成 2025-12-24 21:07:54 +08:00
80da7677f8 源码整理 2025-12-24 20:27:30 +08:00
e1c3c8f072 位图大小设置调试完成 2025-12-24 20:18:59 +08:00
2238d632f7 源码整理 2025-12-24 19:20:17 +08:00
558bc16013 位图平铺方式调试结束 2025-12-24 19:17:56 +08:00
7d50453e34 位图类处理模块重构 2025-12-24 17:28:55 +08:00
f771225830 <powerbell>APK 15.14.28 release Publish. 2025-12-24 12:22:06 +08:00
72794dc6e5 改进图片像素背景的绘图方法。 2025-12-24 12:19:48 +08:00
2ffdb26f69 图片加载逻辑优化,正在查询哪个类在使用背景像素。。。 2025-12-24 00:12:34 +08:00
778a1bc98e 添加应用版本号说明 2025-12-15 12:42:50 +08:00
bb94f87597 更新说明书 2025-12-08 00:56:14 +08:00
67 changed files with 4815 additions and 1471 deletions

View File

@@ -1,87 +0,0 @@
name: Android CI
# 触发器
on:
push:
tags:
- *-beta
pull_request:
tags:
- *-beta
jobs:
build:
runs-on: ubuntu-latest
# 设置 JDK 环境
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# 获取应用打包秘钥库
- name: Checkout Android Keystore
uses: actions/checkout@v3
with:
repository: zhangsken/keystore # 存储应用打包用的 keystore 的仓库(格式:用户名/仓库名)
token: ${{ secrets.APP_SECRET_TOKEN_1 }} # 连接仓库的 token , 需要单独配置
path: keystore # 仓库的根目录名
# 打包 Stage Release 版本应用
- name: Build with Gradle
run: bash ./gradlew assembleBetaRelease
# 创建release
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.APP_SECRET_TOKEN_1 }}
# GitHub 会自动创建 GITHUB_TOKEN 密码以在工作流程中使用。
# 您可以使用 GITHUB_TOKEN 在工作流程运行中进行身份验证。
# 当您启用 GitHub Actions 时GitHub 在您的仓库中安装 GitHub 应用程序。
# GITHUB_TOKEN 密码是一种 GitHub 应用程序 安装访问令牌。
# 您可以使用安装访问令牌代表仓库中安装的 GitHub 应用程序 进行身份验证。
# 令牌的权限仅限于包含您的工作流程的仓库。 更多信息请参阅“GITHUB_TOKEN 的权限”。
# 在每个作业开始之前, GitHub 将为作业提取安装访问令牌。 令牌在作业完成后过期。
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
# 获取 APK 版本号
- name: Get Version Name
uses: actions/github-script@v3
id: get-version
with:
script: |
const str=process.env.GITHUB_REF;
return str.substring(str.indexOf("v"));
result-encoding: string
# 上传至 Release 的资源
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.APP_SECRET_TOKEN_1 }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # 上传网址,无需改动
#asset_path: app/build/outputs/apk/release/app-release.apk # 上传路径(Release)
asset_path: app/build/outputs/apk/beta/release/app-beta-release.apk # 上传路径(WinBoll Stage Release)
asset_name: WinBoll-${{steps.get-version.outputs.result}}0.apk # 资源名
asset_content_type: application/vnd.android.package-archiv # 资源类型
# 存档打包的文件
- name: Archive production artifacts
uses: actions/upload-artifact@v2
with:
name: build
path: app/build/outputs # 将打包之后的文件全部上传(里面会有混淆的 map 文件)

View File

@@ -1,166 +1,223 @@
#!/usr/bin/bash
# ==============================================================================
# WinBoLL 应用发布脚本
# 功能检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
# 依赖build.properties、app_update_description.txt项目根目录下
# 使用:./script_name.sh <APP_NAME>
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
# ==============================================================================
# 检查是否指定了将要发布的应用名称
# 使用 `-z` 命令检查变量是否为空
# ==================== 常量定义 ====================
# 脚本退出码
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"
# ==================== 函数定义 ====================
# 检查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 "No APP name specified : $0"
exit 2
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
exit ${EXIT_CODE_ERR_NO_APP_NAME}
fi
APP_NAME=$1
echo "[INFO] 待发布应用名称:${APP_NAME}"
## 定义相关函数
## 检查 Git 源码是否完全提交了完全提交就返回0
function checkGitSources {
#local input="$1"
#echo "The string is: $input"
git config --global --add safe.directory `pwd`
if [[ -n $(git diff --stat) ]]
then
local result="Source is no commit completely."
echo $result
# 脚本调试时使用
#return 0
# 正式检查源码时使用
return 1
fi
local result="Git Source Check OK."
echo $result
return 0
}
function askAddWorkflowsTag {
read answer
if [[ $answer =~ ^[Yy]$ ]]; then
#echo "You chose yes."
return 1
else
#echo "You chose no."
return 0
fi
}
function addWinBoLLTag {
# 就读取脚本 .winboll/winboll_app_build.gradle 生成的 publishVersion。
# 如果文件中有 publishVersion 这一项,
# 使用grep找到包含"publishVersion="的那一行然后用awk提取其后的值
PUBLISH_VERSION=$(grep -o "publishVersion=.*" $1/build.properties | awk -F '=' '{print $2}')
echo "< $1/build.properties publishVersion : ${PUBLISH_VERSION} >"
## 设新的 WinBoLL 标签
# 脚本调试时使用
#tag="projectname-v7.6.4-test1"
# 正式设置标签时使用
tag=$1"-v"${PUBLISH_VERSION}
echo "< WinBoLL Tag To: $tag >";
# 检查是否已经添加了 WinBoLL Tag
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
echo -e "< WinBoLL Tag ${tag} exist! >"
return 1 # WinBoLL标签重复
fi
# 添加WinBoLL标签
git tag -a ${tag} -F $1/app_update_description.txt
return 0
}
function addWorkflowsTag {
# 就读取脚本 .winboll/winboll_app_build.gradle 生成的 baseBetaVersion。
# 如果文件中有 baseBetaVersion 这一项,
# 使用grep找到包含"baseBetaVersion="的那一行然后用awk提取其后的值
BASE_BETA_VERSION=$(grep -o "baseBetaVersion=.*" $1/build.properties | awk -F '=' '{print $2}')
echo "< $1/build.properties baseBetaVersion : ${BASE_BETA_VERSION} >"
## 设新的 workflows 标签
# 脚本调试时使用
#tag="projectname-v7.6.4-beta"
# 正式设置标签时使用
tag=$1"-v"${BASE_BETA_VERSION}-beta
echo "< Workflows Tag To: $tag >";
# 检查是否已经添加了工作流 Tag
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
echo -e "< Github Workflows Tag ${tag} exist! >"
return 1 # 工作流标签重复
fi
# 添加工作流标签
git tag -a ${tag} -F $1/app_update_description.txt
return 0
}
## 开始执行脚本
echo -e "Current dir : \n"`pwd`
# 检查当前目录是否是项目根目录
if [[ -e $1/build.properties ]]; then
echo "The $1/build.properties file exists."
echo -e "Work dir correctly."
else
echo "The $1/build.properties file does not exist."
echo "尝试进入根目录"
# 进入项目根目录
# 2. 检查并切换到项目根目录确保build.properties存在
echo "[INFO] 当前工作目录:$(pwd)"
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties尝试切换到上级目录..."
cd ..
echo "[INFO] 切换后工作目录:$(pwd)"
fi
## 本脚本需要在项目根目录下执行
echo -e "Current dir : \n"`pwd`
# 检查当前目录是否是项目根目录
if [[ -e $1/build.properties ]]; then
echo "The $1/build.properties file exists."
echo -e "Work dir correctly."
# 验证最终工作目录是否正确
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
echo "---------------------------------------------"
echo " 步骤2编译Stage Release APK"
echo "---------------------------------------------"
echo "[INFO] 开始执行Gradle任务${GRADLE_TASK_PUBLISH}"
# 调试用(注释正式任务,启用调试任务)
# bash gradlew :${APP_NAME}:${GRADLE_TASK_DEBUG}
bash gradlew :${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 "The $1/build.properties file does not exist."
echo -e "Work dir error."
echo "[ERROR] 源码与标签推送失败!"
exit 1
fi
# 检查源码状态
result=$(checkGitSources)
if [[ $? -eq 0 ]]; then
echo $result
# 如果Git已经提交了所有代码就执行标签和应用发布操作
# ==================== 主流程结束 ====================
echo "============================================="
echo " WinBoLL 应用发布完成!"
echo "============================================="
exit ${EXIT_CODE_SUCCESS}
# 预先询问是否添加工作流标签
#echo "Add Github Workflows Tag? (yes/No)"
#result=$(askAddWorkflowsTag)
#nAskAddWorkflowsTag=$?
#echo $result
# 发布应用
echo "Publishing WinBoLL APK ..."
# 脚本调试时使用
#bash gradlew :$1:assembleBetaDebug
# 正式发布
bash gradlew :$1:assembleStageRelease
echo "Publishing WinBoLL APK OK."
# 添加 WinBoLL 标签
result=$(addWinBoLLTag $1)
echo $result
if [[ $? -eq 0 ]]; then
echo $result
# WinBoLL 标签添加成功
else
echo -e "${0}: addWinBoLLTag $1\n${result}\nAdd WinBoLL tag cancel."
exit 1 # addWinBoLLTag 异常
fi
# 添加 GitHub 工作流标签
#if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
# 如果用户选择添加工作流标签
#result=$(addWorkflowsTag $1)
#if [[ $? -eq 0 ]]; then
# echo $result
# 工作流标签添加成功
#else
#echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
#exit 1 # addWorkflowsTag 异常
#fi
#fi
## 清理更新描述文件内容
echo "" > $1/app_update_description.txt
# 设置新版本开发参数配置
# 提交配置
git add .
git commit -m "<$1>Start New Stage Version."
echo "Push sources to git repositories ..."
# 推送源码到所有仓库
git push origin && git push origin --tags
else
echo -e "${0}: checkGitSources\n${result}\nShell cancel."
exit 1 # checkGitSources 异常
fi

View File

@@ -64,6 +64,11 @@ android {
dimension "WinBoLLApp"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
// 应用包输出配置
//

View File

@@ -5,10 +5,11 @@
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/APPBase> ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/appbase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
## WinBoLL 提问
同样是 /sdcard 目录,在开发 Android 应用时,
能否实现手机编译与电脑编译的源码同步。
@@ -154,3 +155,11 @@ $ bash gradlew assembleBetaDebug
$ bash gradlew assembleStageDebug
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
# 应用版本号命名方式
## statge 渠道
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
APPBase_15.7.0
## beta 渠道
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
APPBase_15.9.6-beta8_5413

View File

@@ -100,12 +100,15 @@ allprojects {
}
subprojects {
// 1. 对纯 Java 模块的 JavaCompile 任务配置(升级为 Java 11
tasks.withType(JavaCompile) {
options.compilerArgs << "-parameters"
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// 可选:确保编码一致
options.encoding = "UTF-8"
}
}
}
}
task clean(type: Delete) {

View File

@@ -19,12 +19,12 @@ def genVersionName(def versionName){
android {
// 1. compileSdkVersion必须 ≥ targetSdkVersion建议直接等于 targetSdkVersion30
compileSdkVersion 30
// 2. buildToolsVersion需匹配 compileSdkVersion建议使用 30.x.x 最新稳定版(无需高于 compileSdkVersion
buildToolsVersion "30.0.3" // 这是 30 对应的最新稳定版,避免使用 beta 版
// 关键:改为你已安装的 SDK 32≥ targetSdkVersion 30兼容已安装环境
compileSdkVersion 32
// 直接使用已安装的构建工具 33.0.3(无需修改)
buildToolsVersion "33.0.3"
defaultConfig {
applicationId "cc.winboll.studio.powerbell"
minSdkVersion 23
@@ -83,12 +83,12 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libaes:15.12.7'
//api 'cc.winboll.studio:libappbase:15.12.2'
api 'cc.winboll.studio:libaes:15.12.12'
api 'cc.winboll.studio:libappbase:15.14.2'
// WinBoLL备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:AES:aes-v15.12.7'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Dec 23 21:02:16 HKT 2025
stageCount=28
#Tue Jan 06 19:10:13 HKT 2026
stageCount=49
libraryProject=
baseVersion=15.14
publishVersion=15.14.27
publishVersion=15.14.48
buildCount=0
baseBetaVersion=15.14.28
baseBetaVersion=15.14.49

View File

@@ -4,53 +4,84 @@
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"
@@ -62,14 +93,13 @@
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"
@@ -77,13 +107,19 @@
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
@@ -93,13 +129,19 @@
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
@@ -109,124 +151,175 @@
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=".controlcenterservice"
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=".assistantservice">
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: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"/>
@@ -234,4 +327,3 @@
</application>
</manifest>

View File

@@ -5,20 +5,32 @@ 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;
/**
* 应用全局入口类适配Android API 30基于Java 7编写
* 应用全局入口类
* 适配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 {
// ===================== 常量定义区(按功能分类排序) =====================
public static final String TAG = "App";
// ====================================== 常量区 - 置顶排序 (按功能归类) ======================================
// 基础日志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";
@@ -30,237 +42,279 @@ public class App extends GlobalApplication {
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
// 缓存防护常量
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
// ====================================== 静态属性区 - 全局单例/状态 (按核心程度排序) ======================================
// 应用单例
private static App sApp;
// ===================== 静态属性区(按工具类优先级排序) =====================
// 数据配置工具
private static AppConfigUtils sAppConfigUtils;
// 配置与缓存工具 (全局单例)
public static AppConfigUtils sAppConfigUtils;
private static AppCacheUtils sAppCacheUtils;
// 全局Bitmap缓存工具极致强制保持一旦初始化永不销毁
// 资源与视图缓存 (强制驻留,极致缓存核心)
public static BackgroundSourceUtils sBackgroundSourceUtils;
public static BitmapCacheUtils sBitmapCacheUtils;
private static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// 全局视图控件缓存工具(极致强制保持:一旦初始化,永不销毁)
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// 系统状态 (电池电量)
public static volatile int sQuantityOfElectricity = INVALID_BATTERY_VALUE;
// ===================== 成员属性区(按生命周期关联度排序) =====================
// 全局广播接收器
// 系统工具 (通知管理器)
private static NotificationManagerUtils sNotificationManagerUtils;
// ====================================== 成员属性区 - 非静态成员 (广播接收器) ======================================
private GlobalApplicationReceiver mGlobalReceiver;
// 通知管理工具
private NotificationManagerUtils mNotificationManager;
// ===================== 公共静态方法区(工具类实例获取) =====================
// ====================================== 公共静态方法 - 单例/工具获取 (对外入口) ======================================
/**
* 获取应用配置工具实例
* 获取应用全局单例实例
* @return 应用单例App实例
*/
public static App getInstance() {
LogUtils.d(TAG, "【getInstance】应用单例获取方法调用 | 当前实例:" + sApp);
return sApp;
}
/**
* 获取配置工具类单例实例
* @param context 上下文对象
* @return 配置工具类AppConfigUtils实例
*/
public static AppConfigUtils getAppConfigUtils(Context context) {
LogUtils.d(TAG, "getAppConfigUtils() 调用传入Context类型" + (context != null ? context.getClass().getSimpleName() : "null"));
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()AppConfigUtils实例已初始化");
LogUtils.d(TAG, "getAppConfigUtils】配置工具实例为空,已初始化新实例");
}
return sAppConfigUtils;
}
/**
* 获取应用缓存工具实例
* 获取缓存工具类单例实例
* @param context 上下文对象
* @return 缓存工具类AppCacheUtils实例
*/
public static AppCacheUtils getAppCacheUtils(Context context) {
LogUtils.d(TAG, "getAppCacheUtils() 调用传入Context类型" + (context != null ? context.getClass().getSimpleName() : "null"));
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()AppCacheUtils实例已初始化");
LogUtils.d(TAG, "getAppCacheUtils】缓存工具实例为空,已初始化新实例");
}
return sAppCacheUtils;
}
// ===================== 公共成员方法区(业务功能) =====================
// ====================================== 公共成员方法 - 业务逻辑 (实例方法) ======================================
/**
* 清除电池历史数据
*/
public void clearBatteryHistory() {
LogUtils.d(TAG, "clearBatteryHistory() 调用");
LogUtils.d(TAG, "clearBatteryHistory】清除电池历史数据方法调用");
if (sAppCacheUtils != null) {
sAppCacheUtils.clearBatteryHistory();
LogUtils.d(TAG, "clearBatteryHistory()电池历史数据清除");
LogUtils.d(TAG, "clearBatteryHistory电池历史数据清除成功");
} else {
LogUtils.w(TAG, "clearBatteryHistory()AppCacheUtils未初始化,清除失败");
LogUtils.w(TAG, "clearBatteryHistory】电池历史数据清除失败 | 缓存工具实例sAppCacheUtils为空");
}
}
/**
* 手动清理所有缓存(带严格权限控制,仅主动调用生效)
* 极致强制缓存策略下,仅提供手动清理入口,永不自动调用
* 获取视图缓存实例
* @return 视图缓存MemoryCachedBackgroundView实例
*/
public MemoryCachedBackgroundView getMemoryCachedBackgroundView() {
LogUtils.d(TAG, "【getMemoryCachedBackgroundView】视图缓存获取方法调用 | 当前实例:" + sMemoryCachedBackgroundView);
return sMemoryCachedBackgroundView;
}
// ====================================== 公共静态方法 - 业务逻辑 (全局工具方法) ======================================
/**
* 手动清理所有缓存(仅主动调用生效,符合极致缓存策略)
*/
public static void manualClearAllCache() {
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存调用(极致强制缓存策略下,需谨慎使用)");
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存方法调用 | 仅主动触发生效");
// 清理Bitmap缓存
if (sBitmapCacheUtils != null) {
sBitmapCacheUtils.clearAllCache();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存已手动清理");
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】Bitmap缓存已清理");
}
// 清理视图控件缓存(仅清除静态引用,不销毁实例)
// 仅置空视图缓存引用,不销毁实例(极致缓存策略)
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存实例保持,仅清除静态引用");
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】视图缓存引用已置空 | 实例保留");
sMemoryCachedBackgroundView = null;
}
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存完成(部分缓存实例仍可能保留在内存中)");
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() 应用启动,开始初始化");
LogUtils.d(TAG, "onCreate应用启动生命周期方法调用 | 开始初始化应用...");
// 初始化调试模式
// 初始化应用单例与调试模式
sApp = this;
setIsDebugging(BuildConfig.DEBUG);
LogUtils.d(TAG, "onCreate() 调试模式:" + BuildConfig.DEBUG);
LogUtils.d(TAG, "onCreate】应用单例已初始化 | 调试模式:" + BuildConfig.DEBUG);
// 初始化基础工具
// 初始化核心组件
initBaseTools();
// 初始化工具类实例(核心:极致强制缓存,永不销毁)
initUtils();
// 初始化广播接收器
initReceiver();
LogUtils.d(TAG, "onCreate() 应用初始化完成极致强制缓存策略已启用");
LogUtils.d(TAG, "onCreate应用初始化完成 | 极致强制缓存策略已激活");
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "onTerminate() 应用终止开始释放非缓存资源");
LogUtils.d(TAG, "onTerminate应用终止生命周期方法调用 | 开始释放非缓存资源...");
// 释放Toast工具
// 释放非缓存资源
ToastUtils.release();
LogUtils.d(TAG, "onTerminate()Toast工具已释放");
// 释放通知工具
releaseNotificationManager();
// 释放广播接收器
releaseReceiver();
// 核心修改:应用终止时也不清理缓存,保持静态实例
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 应用终止,极致强制缓存策略生效,不清理任何缓存");
LogUtils.d(TAG, "onTerminate() 非缓存资源释放完成,缓存实例保持");
// 核心策略:不清理任何缓存
LogUtils.w(CACHE_PROTECT_TAG, "【onTerminate】极致缓存策略生效 | 所有缓存将保留在内存中");
LogUtils.d(TAG, "【onTerminate】非缓存资源释放完成");
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
// 极致强制缓存:禁止任何缓存清理操作,仅记录日志
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onTrimMemory() 调用内存等级level" + level + ",极致强制保持所有缓存");
// 记录详细缓存状态,不执行任何清理
LogUtils.w(CACHE_PROTECT_TAG, "【onTrimMemory】系统内存修剪回调 | 内存等级:" + level + " | 忽略修剪,缓存强制保护");
logDetailedCacheStatus();
}
@Override
public void onLowMemory() {
super.onLowMemory();
// 极致强制缓存:低内存时也不清理任何缓存
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onLowMemory() 调用,极致强制保持所有缓存");
// 记录详细缓存状态,不执行任何清理
LogUtils.w(CACHE_PROTECT_TAG, "【onLowMemory】系统低内存回调 | 极致缓存策略生效 | 不执行任何缓存清理操作");
logDetailedCacheStatus();
}
// ===================== 私有初始化方法区(按初始化顺序排序) =====================
// ====================================== 私有初始化方法 - 组件初始化 (按依赖顺序排序) ======================================
/**
* 初始化基础工具Activity管理、Toast
* 初始化基础工具Activity管理、Toast、通知管理器
*/
private void initBaseTools() {
LogUtils.d(TAG, "initBaseTools() 开始初始化基础工具");
LogUtils.d(TAG, "initBaseTools】基础工具类初始化开始...");
WinBoLLActivityManager.init(this);
ToastUtils.init(this);
LogUtils.d(TAG, "initBaseTools() 基础工具初始化完成");
sNotificationManagerUtils = new NotificationManagerUtils(this);
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化完成");
}
/**
* 初始化工具类实例(核心:极致强制缓存,一旦初始化永不销毁
* 初始化核心工具与缓存(极致强制驻留,缓存核心
*/
private void initUtils() {
LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用极致强制缓存策略");
LogUtils.d(TAG, "initUtils】核心工具与缓存初始化开始 | 极致缓存策略激活");
// 1. 配置与基础缓存工具初始化
sAppConfigUtils = getAppConfigUtils(this);
sAppCacheUtils = getAppCacheUtils(this);
// 极致强制初始化Bitmap缓存工具(必初始化,永不销毁
// 2. 资源与Bitmap缓存工具初始化(永久驻留
sBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this);
sBackgroundSourceUtils.loadSettings();
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "initUtils() Bitmap缓存工具初始化(极致强制保持,永不销毁)");
LogUtils.d(TAG, "initUtils】资源与Bitmap缓存工具初始化完成 | 永久驻留内存");
// 极致强制初始化视图控件缓存工具(必初始化,永不销毁
// 3. 视图缓存初始化(永久驻留,无实例则创建
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(极致强制保持,永不销毁)");
mNotificationManager = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initUtils() 工具类初始化完成,极致强制缓存策略已生效");
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() 开始初始化广播接收器");
LogUtils.d(TAG, "initReceiver】全局广播接收器注册开始...");
mGlobalReceiver = new GlobalApplicationReceiver(this);
mGlobalReceiver.registerAction();
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成");
LogUtils.d(TAG, "initReceiver】全局广播接收器注册完成");
}
// ===================== 私有释放方法区(按资源重要性排序) =====================
// ====================================== 私有释放方法 - 资源释放 (按创建逆序排序) ======================================
/**
* 释放广播接收器资源
* 释放全局广播接收器
*/
private void releaseReceiver() {
LogUtils.d(TAG, "releaseReceiver() 开始释放广播接收器");
LogUtils.d(TAG, "releaseReceiver】全局广播接收器释放开始...");
if (mGlobalReceiver != null) {
mGlobalReceiver.unregisterAction();
mGlobalReceiver = null;
LogUtils.d(TAG, "releaseReceiver() 广播接收器资源已释放");
} else {
LogUtils.d(TAG, "releaseReceiver() 广播接收器未初始化,无需释放");
LogUtils.d(TAG, "releaseReceiver】全局广播接收器释放完成");
}
}
/**
* 释放通知管理工具资源
* 释放通知管理资源
*/
private void releaseNotificationManager() {
LogUtils.d(TAG, "releaseNotificationManager() 开始释放通知工具");
if (mNotificationManager != null) {
mNotificationManager.release();
mNotificationManager = null;
LogUtils.d(TAG, "releaseNotificationManager() 通知工具资源释放");
} else {
LogUtils.d(TAG, "releaseNotificationManager() 通知工具未初始化,无需释放");
LogUtils.d(TAG, "releaseNotificationManager】通知管理器资源释放开始...");
if (sNotificationManagerUtils != null) {
sNotificationManagerUtils.release();
sNotificationManagerUtils = null;
LogUtils.d(TAG, "releaseNotificationManager】通知管理器资源释放完成");
}
}
// ===================== 私有工具方法区(辅助功能) =====================
// ====================================== 私有辅助方法 - 日志/工具 (辅助功能) ======================================
/**
* 记录详细缓存状态(用于调试监控极致强制缓存效果
* 记录当前缓存详细状态(用于调试监控极致缓存策略监控
*/
private void logDetailedCacheStatus() {
LogUtils.d(TAG, "logDetailedCacheStatus() 开始记录详细缓存状态");
LogUtils.d(TAG, "logDetailedCacheStatus】缓存状态监控日志开始...");
// Bitmap缓存状态
if (sBitmapCacheUtils != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存工具实例有效极致强制保持");
// 假设BitmapCacheUtils有获取缓存数量的方法
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 有效");
try {
int cacheCount = sBitmapCacheUtils.getCacheCount();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量" + cacheCount);
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】Bitmap缓存数量" + sBitmapCacheUtils.getCacheCount());
} catch (Exception e) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量获取失败(不影响缓存),异常信息:" + e.getMessage());
LogUtils.e(CACHE_PROTECT_TAG, "【缓存详情】获取Bitmap缓存数量失败", e);
}
} else {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 未初始化");
}
// 视图控件缓存状态
// 视图缓存状态
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存工具实例有效(极致强制保持)");
// 记录视图实例总数
int viewInstanceCount = MemoryCachedBackgroundView.getInstanceCount();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件实例总数:" + viewInstanceCount);
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 有效");
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】视图实例数量:" + MemoryCachedBackgroundView.getInstanceCount());
} else {
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 引用已置空(实例可能保留)");
}
LogUtils.d(TAG, "logDetailedCacheStatus() 详细缓存状态记录完成,所有缓存均极致强制保持");
}
}

View File

@@ -26,54 +26,63 @@ 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_READ_MEDIA_IMAGES = 1001;
private static final int REQUEST_BACKGROUND_SETTINGS_ACTIVITY = 1001;
public static final String EXTRA_ISRELOAD_BACKGROUNDVIEW = "EXTRA_ISRELOAD_BACKGROUNDVIEW";
public static final String EXTRA_ISRELOAD_ACCENTCOLOR = "EXTRA_ISRELOAD_ACCENTCOLOR";
private static final long DELAY_LOAD_NON_CRITICAL = 500L;
// 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 ViewStub mAdsViewStub;
private ADsBannerView mADsBannerView;
private Drawable mFrameDrawable;
private Menu mMenu;
// ======================== 生命周期方法(按系统调用顺序排列)========================
// ======================== 生命周期方法(按系统调用顺序排列)========================
@Override
public Activity getActivity() {
return this;
@@ -87,7 +96,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate() | savedInstanceState=" + savedInstanceState);
LogUtils.d(TAG, "onCreate() 调用 | savedInstanceState: " + savedInstanceState);
initGlobalHandler();
setContentView(R.layout.activity_main);
@@ -96,61 +105,75 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
initCriticalView();
initCoreUtilsAsync();
loadNonCriticalViewDelayed();
// 处理首次启动参数
handleReloadBackgroundParam(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
LogUtils.d(TAG, "onNewIntent() 调用 | intent: " + intent);
// 关键更新Activity持有的Intent确保后续获取最新值
setIntent(intent);
// 统一处理刷新背景参数
handleReloadBackgroundParam(intent);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "onPostCreate() | savedInstanceState=" + savedInstanceState);
LogUtils.d(TAG, "onPostCreate() 调用 | savedInstanceState: " + savedInstanceState);
mPermissionUtils.startPermissionRequest(this);
LogUtils.d(TAG, "onPostCreate: 发起权限申请");
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume()");
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
}
LogUtils.d(TAG, "onResume() 调用");
if (mADsBannerView != null) {
mADsBannerView.resumeADs(this);
LogUtils.d(TAG, "onResume: 广告视图已恢复");
}
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause()");
LogUtils.d(TAG, "onPause() 调用");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy()");
LogUtils.d(TAG, "onDestroy() 调用");
// 释放广告资源
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
mADsBannerView = null;
LogUtils.d(TAG, "onDestroy: 广告资源已释放");
}
// 释放核心视图
if (mMainContentView != null) {
mMainContentView.releaseResources();
mMainContentView = null;
LogUtils.d(TAG, "onDestroy: 核心视图资源已释放");
}
// 销毁Handler防止内存泄漏
// 销毁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;
@@ -165,22 +188,27 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@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);
LogUtils.d(TAG, "onActivityResult() 调用 | requestCode: " + requestCode + " | resultCode: " + resultCode + " | data: " + data);
mPermissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
if (requestCode == REQUEST_READ_MEDIA_IMAGES && sGlobalHandler != null) {
if (requestCode == REQUEST_BACKGROUND_SETTINGS_ACTIVITY && sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
LogUtils.d(TAG, "onActivityResult: 发送背景加载消息");
}
}
// ======================== 菜单与导航方法 ========================
// ======================== 菜单与导航方法 ========================
@Override
public boolean onCreateOptionsMenu(Menu menu) {
LogUtils.d(TAG, "onCreateOptionsMenu() | menu=" + menu);
LogUtils.d(TAG, "onCreateOptionsMenu() 调用 | menu: " + menu);
mMenu = menu;
AESThemeUtil.inflateMenu(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;
@@ -188,14 +216,21 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override
public boolean onOptionsItemSelected(MenuItem item) {
LogUtils.d(TAG, "onOptionsItemSelected() | itemId=" + item.getItemId());
LogUtils.d(TAG, "onOptionsItemSelected() 调用 | itemId: " + item.getItemId());
// 主题切换处理
if (AESThemeUtil.onAppThemeItemSelected(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));
@@ -207,12 +242,12 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
startActivity(new Intent(this, ClearRecordActivity.class));
break;
case R.id.action_changepicture:
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_READ_MEDIA_IMAGES);
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_BACKGROUND_SETTINGS_ACTIVITY);
break;
case R.id.action_unittestactivity:
startActivity(new Intent(this, MainUnitTestActivity.class));
break;
case R.id.action_unittest2activity:
case R.id.action_unittest2activity:
startActivity(new Intent(this, MainUnitTest2Activity.class));
break;
case R.id.action_about:
@@ -227,41 +262,46 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override
public void setupToolbar() {
super.setupToolbar();
LogUtils.d(TAG, "setupToolbar()");
LogUtils.d(TAG, "setupToolbar() 调用");
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
LogUtils.d(TAG, "setupToolbar: 已隐藏返回按钮");
}
}
@Override
public void onBackPressed() {
LogUtils.d(TAG, "onBackPressed()");
LogUtils.d(TAG, "onBackPressed() 调用");
moveTaskToBack(true);
LogUtils.d(TAG, "onBackPressed: 应用已退至后台");
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
LogUtils.d(TAG, "dispatchKeyEvent() | event=" + event);
LogUtils.d(TAG, "dispatchKeyEvent() 调用 | event: " + event);
return super.dispatchKeyEvent(event);
}
// ======================== 核心初始化方法 ========================
// ======================== 核心初始化方法 ========================
private void initPermissionUtils() {
LogUtils.d(TAG, "initPermissionUtils()");
LogUtils.d(TAG, "initPermissionUtils() 调用");
mPermissionUtils = PermissionUtils.getInstance();
LogUtils.d(TAG, "initPermissionUtils: 权限工具类已初始化");
}
private void initGlobalHandler() {
LogUtils.d(TAG, "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);
LogUtils.w(TAG, "handleMessage: Activity已销毁跳过消息 | what: " + msg.what);
return;
}
LogUtils.d(TAG, "handleMessage() | what=" + msg.what);
LogUtils.d(TAG, "handleMessage() 调用 | what: " + msg.what);
switch (msg.what) {
case MSG_RELOAD_APPCONFIG:
sMainActivity.updateViewData();
@@ -269,45 +309,53 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
case MSG_CURRENTVALUEBATTERY:
if (sMainActivity.mMainContentView != null) {
sMainActivity.mMainContentView.updateCurrentBattery(msg.arg1);
LogUtils.d(TAG, "handleMessage: 更新当前电量 | value=" + msg.arg1);
LogUtils.d(TAG, "handleMessage: 更新当前电量 | value: " + msg.arg1);
}
break;
case MSG_LOAD_BACKGROUND:
sMainActivity.reloadBackground();
sMainActivity.setMainLayoutBackgroundColor();
break;
case MSG_UPDATE_SERVICE_SWITCH:
sMainActivity.updateServiceSwitchUI();
break;
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()");
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()");
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: 工具栏已设置标题样式");
}
mAdsViewStub = findViewById(R.id.stub_ads_banner);
LogUtils.d(TAG, "initCriticalView: 广告ViewStub已获取");
}
private void initCoreUtilsAsync() {
LogUtils.d(TAG, "initCoreUtilsAsync()");
LogUtils.d(TAG, "initCoreUtilsAsync() 调用");
new Thread(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId=" + Thread.currentThread().getId());
LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId: " + Thread.currentThread().getId());
mApplication = (App) getApplication();
mAppConfigUtils = AppConfigUtils.getInstance(getApplicationContext());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
@@ -317,17 +365,20 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
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);
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) {
@@ -335,6 +386,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
@Override
public void run() {
ControlCenterService.stopControlCenterService(getApplicationContext());
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已停止");
}
});
}
@@ -347,7 +399,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁跳过UI更新");
return;
}
// 加载框架背景适配API23+
// 适配API30兼容低版本Drawable加载
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame, getTheme());
} else {
@@ -356,6 +408,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
updateViewData();
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
LogUtils.d(TAG, "initCoreUtilsAsync: UI更新消息已发送");
}
});
}
@@ -363,7 +416,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
}
private void loadNonCriticalViewDelayed() {
LogUtils.d(TAG, "loadNonCriticalViewDelayed() | 延迟时长=" + DELAY_LOAD_NON_CRITICAL + "ms");
LogUtils.d(TAG, "loadNonCriticalViewDelayed() 调用 | 延迟时长: " + DELAY_LOAD_NON_CRITICAL + "ms");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
@@ -376,9 +429,34 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
}, DELAY_LOAD_NON_CRITICAL);
}
// ======================== 视图操作方法 ========================
// ======================== 视图操作方法 ========================
private void handleReloadBackgroundParam(Intent intent) {
LogUtils.d(TAG, "handleReloadBackgroundParam() 调用 | intent: " + intent);
if (intent == null) {
LogUtils.d(TAG, "handleReloadBackgroundParam: Intent 为空");
return;
}
boolean isReloadAccentColor = intent.getBooleanExtra(EXTRA_ISRELOAD_ACCENTCOLOR, false);
if (isReloadAccentColor) {
App.sBackgroundSourceUtils.getCurrentBackgroundBean().setPixelColor(ImageUtils.getColorAccent(this));
App.sBackgroundSourceUtils.saveSettings();
}
boolean isReloadBackgroundView = intent.getBooleanExtra(EXTRA_ISRELOAD_BACKGROUNDVIEW, false);
if (isReloadBackgroundView) {
LogUtils.d(TAG, "handleReloadBackgroundParam: 接收到刷新背景视图指令");
reloadBackgroundView();
}
}
private void reloadBackgroundView() {
LogUtils.d(TAG, "reloadBackgroundView() 调用");
mMainContentView.reloadBackgroundView();
}
private void loadAdsView() {
LogUtils.d(TAG, "loadAdsView()");
LogUtils.d(TAG, "loadAdsView() 调用");
if (mAdsViewStub == null) {
LogUtils.e(TAG, "loadAdsView: 广告ViewStub为空加载失败");
return;
@@ -386,47 +464,51 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
if (mADsBannerView == null) {
View adsView = mAdsViewStub.inflate();
mADsBannerView = adsView.findViewById(R.id.adsbanner);
LogUtils.d(TAG, "loadAdsView: 广告视图已加载");
} else {
LogUtils.d(TAG, "loadAdsView: 广告视图已存在,无需重复加载");
}
}
private void updateViewData() {
LogUtils.d(TAG, "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()");
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);
mMainContentView.backgroundView.loadByBackgroundBean(currentBgBean, true);
LogUtils.d(TAG, "reloadBackground: 已加载自定义背景");
} else {
mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background);
}
}
private void setMainLayoutBackgroundColor() {
LogUtils.d(TAG, "setMainLayoutBackgroundColor()");
if (isFinishing() || isDestroyed() || mMainContentView == null || mBgSourceUtils == null) {
LogUtils.e(TAG, "setMainLayoutBackgroundColor: 上下文无效,设置失败");
return;
}
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
if (currentBgBean != null) {
mMainContentView.mainLayout.setBackgroundColor(currentBgBean.getPixelColor());
LogUtils.d(TAG, "setMainLayoutBackgroundColor: 主布局背景色设置完成 | color=" + currentBgBean.getPixelColor());
LogUtils.d(TAG, "reloadBackground: 已加载默认背景");
}
}
private void updateServiceSwitchUI() {
LogUtils.d(TAG, "updateServiceSwitchUI()");
LogUtils.d(TAG, "updateServiceSwitchUI() 调用");
if (mMainContentView == null || mServiceControlBean == null) {
LogUtils.e(TAG, "updateServiceSwitchUI: 核心视图或服务配置为空,更新失败");
return;
@@ -435,69 +517,77 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
mMainContentView.setServiceSwitchEnabled(false);
mMainContentView.setServiceSwitchChecked(configEnabled);
mMainContentView.setServiceSwitchEnabled(true);
LogUtils.d(TAG, "updateServiceSwitchUI: 服务开关已更新 | 状态: " + configEnabled);
}
// ======================== 服务与线程管理方法 ========================
// ======================== 服务与线程管理方法 ========================
private void toggleServiceEnableState(boolean isEnable) {
LogUtils.d(TAG, "toggleServiceEnableState() | 目标状态=" + 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()");
LogUtils.d(TAG, "startAboutActivity() 调用");
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
APPInfo appInfo = genDefaultAppInfo();
aboutIntent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo);
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class);
LogUtils.d(TAG, "startAboutActivity: 关于页面已启动");
}
// ======================== 消息发送方法 ========================
// ======================== 消息发送方法 ========================
private void notifyServiceAppConfigChange() {
LogUtils.d(TAG, "notifyServiceAppConfigChange()");
LogUtils.d(TAG, "notifyServiceAppConfigChange() 调用");
ControlCenterService.sendAppConfigStatusUpdateMessage(this);
reloadAppConfig();
LogUtils.d(TAG, "notifyServiceAppConfigChange: 服务配置已通知更新");
}
public static void reloadAppConfig() {
LogUtils.d(TAG, "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);
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()");
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "powerbell";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
@@ -510,37 +600,38 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
appInfo.setAppAPKName("PowerBell");
appInfo.setAppAPKFolderName("PowerBell");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
// ======================== MainContentView 事件回调 ========================
// ======================== MainContentView 事件回调 ========================
@Override
public void onChargeReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onChargeReminderSwitchChanged() | isChecked=" + isChecked);
LogUtils.d(TAG, "onChargeReminderSwitchChanged() 调用 | isChecked: " + isChecked);
notifyServiceAppConfigChange();
}
@Override
public void onUsageReminderSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onUsageReminderSwitchChanged() | isChecked=" + isChecked);
LogUtils.d(TAG, "onUsageReminderSwitchChanged() 调用 | isChecked: " + isChecked);
notifyServiceAppConfigChange();
}
@Override
public void onServiceSwitchChanged(boolean isChecked) {
LogUtils.d(TAG, "onServiceSwitchChanged() | isChecked=" + isChecked);
LogUtils.d(TAG, "onServiceSwitchChanged() 调用 | isChecked: " + isChecked);
toggleServiceEnableState(isChecked);
}
@Override
public void onChargeReminderProgressChanged(int progress) {
LogUtils.d(TAG, "onChargeReminderProgressChanged() | progress=" + progress);
LogUtils.d(TAG, "onChargeReminderProgressChanged() 调用 | progress: " + progress);
notifyServiceAppConfigChange();
}
@Override
public void onUsageReminderProgressChanged(int progress) {
LogUtils.d(TAG, "onUsageReminderProgressChanged() | progress=" + progress);
LogUtils.d(TAG, "onUsageReminderProgressChanged() 调用 | progress: " + progress);
notifyServiceAppConfigChange();
}
}

View File

@@ -15,6 +15,7 @@ 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;
@@ -24,11 +25,13 @@ 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;
@@ -38,18 +41,25 @@ 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;
@@ -58,9 +68,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
// 工具类实例
private BackgroundSourceUtils mBgSourceUtils;
private BitmapCacheUtils mBitmapCache;
// 视图组件
private Toolbar mToolbar;
private BackgroundView mBackgroundView;
// 状态标记volatile保证多线程可见性
private volatile boolean isCommitSettings = false;
private volatile boolean isPreviewBackgroundChanged = false;
@@ -79,52 +91,81 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "【生命周期】onCreate 开始初始化");
LogUtils.d(TAG, "onCreate() 开始初始化");
setContentView(R.layout.activity_background_settings);
// 初始化核心组件
initCoreComponents();
// 初始化界面与事件
// 初始化Toolbar与点击事件
initToolbar();
initClickListeners();
LogUtils.d(TAG, "【初始化】界面与事件绑定完成");
LogUtils.d(TAG, "onCreate() 视图与事件绑定完成");
// 处理分享意图或初始化预览
handleIntentOrPreview();
// 初始化预览环境并刷新
initPreviewEnvironment();
LogUtils.d(TAG, "【生命周期】onCreate 初始化完成");
LogUtils.d(TAG, "onCreate() 初始化完成");
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "【生命周期】onPostCreate 执行双重刷新预览");
doubleRefreshPreview();
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, "【回调触发】requestCode" + requestCode + "resultCode" + resultCode);
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, "【回调处理】结果非RESULT_OK执行取消逻辑");
LogUtils.d(TAG, String.format("onActivityResult() 操作取消 | requestCode=%d", requestCode));
handleOperationCancelOrFail();
return;
}
handleActivityResult(requestCode, data);
} catch (Exception e) {
LogUtils.e(TAG, "【回调异常】requestCode" + requestCode + "异常信息" + e.getMessage());
LogUtils.e(TAG, String.format("onActivityResult() 异常 | requestCode=%d | 异常信息=%s",
requestCode, e.getMessage()));
ToastUtils.show("操作失败");
}
}
@Override
public void finish() {
LogUtils.d(TAG, "【生命周期】finish 触发isCommitSettings" + isCommitSettings + "isPreviewBackgroundChanged" + isPreviewBackgroundChanged);
LogUtils.d(TAG, String.format("finish() | isCommitSettings=%b | isPreviewBackgroundChanged=%b",
isCommitSettings, isPreviewBackgroundChanged));
if (isCommitSettings) {
super.finish();
} else {
@@ -136,7 +177,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, "【权限回调】requestCode" + requestCode + ",权限数量:" + permissions.length);
LogUtils.d(TAG, String.format("onRequestPermissionsResult() | requestCode=%d | 权限数量=%d | 结果数量=%d",
requestCode, permissions.length, grantResults.length));
if (requestCode == REQUEST_CAMERA_PERMISSION) {
handleCameraPermissionResult(grantResults);
}
@@ -144,9 +186,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
// ====================== 界面初始化方法Toolbar + 点击事件)======================
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "【初始化异常】Toolbar未找到");
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
@@ -156,15 +199,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏点击返回按钮");
LogUtils.d(TAG, "导航栏 点击返回按钮");
finish();
}
});
LogUtils.d(TAG, "【界面初始化】Toolbar 配置完成");
LogUtils.d(TAG, "initToolbar() 配置完成");
}
private void initClickListeners() {
LogUtils.d(TAG, "【界面初始化】开始绑定按钮点击事件");
LogUtils.d(TAG, "initClickListeners() 开始绑定按钮点击事件");
// 绑定所有按钮点击事件
bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener);
@@ -176,16 +219,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener);
bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener);
LogUtils.d(TAG, "【界面初始化】按钮点击事件绑定完成");
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, "【绑定异常】未找到视图:" + resId);
LogUtils.e(TAG, String.format("bindClickListener() | 未找到视图:%d", resId));
}
}
@@ -193,10 +238,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】取消背景图片");
LogUtils.d(TAG, "onOriginNullClickListener() | 取消背景图片");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空");
LogUtils.e(TAG, "onOriginNullClickListener() | 预览Bean为空");
return;
}
previewBean.setIsUseBackgroundFile(false);
@@ -209,17 +254,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】选择图片");
LogUtils.d(TAG, "onSelectPictureClickListener() | 选择图片");
launchImageSelector();
}
};
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
@Override
public void onClick(View v) {
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener(){
LogUtils.d(TAG, "onNetworkBackgroundDialog() | 打开网络背景对话框");
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener() {
@Override
public void onConfirm(String szConfirmFilePath) {
LogUtils.d(TAG, String.format("网络背景确认 onConfirm() | 文件路径=%s", szConfirmFilePath));
// 拷贝文件到预览数据并启动裁剪
if (putUriFileToPreviewSource(new File(szConfirmFilePath))) {
startImageCrop(false);
@@ -228,16 +275,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override
public void onCancel() {
LogUtils.d(TAG, "网络背景取消 onCancel()");
}
});
networkBackgroundDialog.show();
networkBackgroundDialog.show();
}
};
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】固定比例裁剪");
LogUtils.d(TAG, "onCropPictureClickListener() | 固定比例裁剪");
startImageCrop(false);
}
};
@@ -245,7 +293,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】自由裁剪");
LogUtils.d(TAG, "onCropFreePictureClickListener() | 自由裁剪");
startImageCrop(true);
}
};
@@ -253,11 +301,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】拍照");
LogUtils.d(TAG, "onTakePhotoClickListener() | 拍照");
// 动态申请相机权限
if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "拍照准备相机权限未授予,发起申请");
LogUtils.d(TAG, "拍照准备 | 相机权限未授予,发起申请");
ActivityCompat.requestPermissions(
BackgroundSettingsActivity.this,
new String[]{Manifest.permission.CAMERA},
@@ -271,10 +319,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】恢复收到的图片");
LogUtils.d(TAG, "onReceivedPictureClickListener() | 恢复收到的图片");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空");
LogUtils.e(TAG, "onReceivedPictureClickListener() | 预览Bean为空");
return;
}
previewBean.setIsUseBackgroundFile(true);
@@ -287,10 +335,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】像素拾取");
LogUtils.d(TAG, "onPixelPickerClickListener() | 像素拾取");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空");
LogUtils.e(TAG, "onPixelPickerClickListener() | 预览Bean为空");
ToastUtils.show("无有效图片可拾取像素");
return;
}
@@ -298,23 +346,23 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
File targetFile = new File(targetImagePath);
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
ToastUtils.show("无有效图片可拾取像素");
LogUtils.e(TAG, "像素拾取失败文件无效:" + targetImagePath);
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, "像素拾取启动路径:" + targetImagePath);
LogUtils.d(TAG, String.format("像素拾取启动 | 路径:%s", targetImagePath));
}
};
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
LogUtils.d(TAG, "onCleanPixelClickListener() | 清空像素颜色");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空");
LogUtils.e(TAG, "onCleanPixelClickListener() | 预览Bean为空");
return;
}
int oldColor = previewBean.getPixelColor();
@@ -323,21 +371,21 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, "【像素清空】旧颜色:" + String.format("#%08X", oldColor));
LogUtils.d(TAG, String.format("像素清空 | 旧颜色:#%08X", oldColor));
}
};
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】调色板按钮");
LogUtils.d(TAG, "onColorPaletteClickListener() | 调色板按钮");
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【操作异常】预览Bean为空");
LogUtils.e(TAG, "onColorPaletteClickListener() | 预览Bean为空");
return;
}
int initialColor = previewBean.getPixelColor();
LogUtils.d(TAG, "【调色板】初始颜色:" + String.format("#%08X", initialColor));
LogUtils.d(TAG, String.format("调色板 | 初始颜色:#%08X", initialColor));
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
@Override
public void onColorSelected(int color) {
@@ -345,11 +393,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
LogUtils.d(TAG, "【颜色选择】选中颜色:" + String.format("#%08X", color));
LogUtils.d(TAG, String.format("颜色选择 | 选中颜色:#%08X", color));
}
});
dialog.show();
LogUtils.d(TAG, "调色板对话框已显示");
LogUtils.d(TAG, "调色板 | 对话框已显示");
}
};
@@ -360,9 +408,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 适配后的Uri失败返回null
*/
public Uri getFileProviderUri(File file) {
LogUtils.d(TAG, "【工具方法】生成FileProvider Uri文件路径:" + (file != null ? file.getAbsolutePath() : "null"));
LogUtils.d(TAG, String.format("getFileProviderUri() | 文件路径:%s", (file != null ? file.getAbsolutePath() : "null")));
if (file == null) {
LogUtils.e(TAG, "【工具异常】文件为空");
LogUtils.e(TAG, "getFileProviderUri() | 文件为空");
return null;
}
try {
@@ -373,7 +421,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
return Uri.fromFile(file);
}
} catch (Exception e) {
LogUtils.e(TAG, "【工具异常】生成Uri失败" + e.getMessage());
LogUtils.e(TAG, String.format("getFileProviderUri() | 生成Uri失败%s", e.getMessage()));
return null;
}
}
@@ -385,7 +433,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
*/
private boolean isBitmapValid(Bitmap bitmap) {
boolean isValid = bitmap != null && !bitmap.isRecycled();
LogUtils.d(TAG, "【工具方法】Bitmap有效性校验" + isValid);
LogUtils.d(TAG, String.format("isBitmapValid() | Bitmap有效性校验%b", isValid));
return isValid;
}
@@ -393,9 +441,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 双重刷新预览,确保背景加载最新数据
*/
private void doubleRefreshPreview() {
LogUtils.d(TAG, "【工具方法】开始双重刷新预览");
LogUtils.d(TAG, "doubleRefreshPreview() 开始双重刷新预览");
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
LogUtils.w(TAG, "双重刷新跳过对象为空或Activity已结束");
LogUtils.w(TAG, "双重刷新 跳过对象为空或Activity已结束");
return;
}
@@ -404,10 +452,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBackgroundView.loadByBackgroundBean(previewBean, true);
mBackgroundView.setBackgroundColor(previewBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成");
LogUtils.d(TAG, "双重刷新 第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, "双重刷新第一重异常:" + e.getMessage());
LogUtils.e(TAG, String.format("双重刷新 第一重异常:%s", e.getMessage()));
return;
}
@@ -420,10 +467,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBackgroundView.loadByBackgroundBean(previewBean, true);
mBackgroundView.setBackgroundColor(previewBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
LogUtils.d(TAG, "双重刷新 第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "双重刷新第二重异常:" + e.getMessage());
LogUtils.e(TAG, String.format("双重刷新 第二重异常:%s", e.getMessage()));
}
}
}
@@ -435,27 +481,30 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 初始化核心组件(工具类+视图)
*/
private void initCoreComponents() {
LogUtils.d(TAG, "initCoreComponents() 开始初始化");
// 初始化视图
mBackgroundView = findViewById(R.id.background_view);
if (mBackgroundView == null) {
LogUtils.e(TAG, "【初始化异常】BackgroundView未找到");
LogUtils.e(TAG, "initCoreComponents() | BackgroundView未找到");
}
// 初始化工具类
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
mBitmapCache = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "【初始化】视图与工具类加载完成");
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, "【预览初始化】加载当前背景配置");
LogUtils.d(TAG, "handleIntentOrPreview() | 加载当前背景配置");
}
}
@@ -463,9 +512,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 初始化预览环境
*/
private void initPreviewEnvironment() {
LogUtils.d(TAG, "initPreviewEnvironment() 开始初始化");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean);
doubleRefreshPreview();
LogUtils.d(TAG, "initPreviewEnvironment() 初始化完成");
}
/**
@@ -473,11 +524,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @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, "分享处理action" + action + "type" + type);
LogUtils.d(TAG, String.format("分享处理 | action%stype%s", action, type));
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
showSharePreviewDialog();
return true;
@@ -490,17 +542,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 显示分享图片预览对话框
*/
private void showSharePreviewDialog() {
LogUtils.d(TAG, "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, "分享处理显示图片预览对话框");
LogUtils.d(TAG, "分享处理 | 显示图片预览对话框");
}
/**
@@ -509,19 +562,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 是图片返回true否则false
*/
private boolean isImageType(String mimeType) {
if (mimeType == null) {
return false;
}
String lowerMimeType = mimeType.toLowerCase();
LogUtils.d("isImageType", "mimeType: " + mimeType + ", lowerMimeType: " + lowerMimeType);
return lowerMimeType.startsWith("image/");
}
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, "【业务逻辑】启动图片选择器");
LogUtils.d(TAG, "launchImageSelector() 启动图片选择器");
Intent[] intents = createImageSelectorIntents();
Intent validIntent = findValidIntent(intents);
@@ -537,6 +590,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 意图数组
*/
private Intent[] createImageSelectorIntents() {
LogUtils.d(TAG, "createImageSelectorIntents() 开始创建");
Intent[] intents = new Intent[3];
// ACTION_GET_CONTENT
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
@@ -559,6 +613,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intents[2] = openDocIntent;
}
LogUtils.d(TAG, "createImageSelectorIntents() 意图数组创建完成");
return intents;
}
@@ -568,11 +623,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @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;
}
@@ -581,17 +639,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @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, "【选图意图】启动图片选择");
LogUtils.d(TAG, "launchImageChooser() | 启动图片选择");
}
/**
* 显示无相册应用提示对话框
*/
private void showNoGalleryDialog() {
LogUtils.d(TAG, "【选图意图】无相册应用");
LogUtils.d(TAG, "showNoGalleryDialog() | 无相册应用");
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -615,12 +674,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 启动应用商店下载相册
*/
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() | 启动失败");
}
}
@@ -628,8 +690,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 处理操作取消或失败
*/
private void handleOperationCancelOrFail() {
LogUtils.d(TAG, "handleOperationCancelOrFail() 操作取消或失败");
mBgSourceUtils.setCurrentSourceToPreview();
LogUtils.d(TAG, "【业务逻辑】操作取消或失败,恢复预览");
ToastUtils.show("操作取消或失败");
doubleRefreshPreview();
}
@@ -638,10 +700,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 处理拍照逻辑(权限通过后执行)
*/
void handleTakePhoto() {
LogUtils.d(TAG, "【业务逻辑】开始处理拍照");
LogUtils.d(TAG, "handleTakePhoto() 开始处理拍照");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【拍照失败】预览Bean为空");
LogUtils.e(TAG, "handleTakePhoto() | 预览Bean为空");
ToastUtils.show("拍照文件创建失败");
return;
}
@@ -649,7 +711,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
File takePhotoFile = new File(previewBean.getBackgroundFilePath());
if (!takePhotoFile.exists()) {
ToastUtils.show("拍照文件创建失败");
LogUtils.e(TAG, "【拍照失败】文件不存在:" + takePhotoFile.getAbsolutePath());
LogUtils.e(TAG, String.format("handleTakePhoto() | 文件不存在:%s", takePhotoFile.getAbsolutePath()));
return;
}
@@ -661,11 +723,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
LogUtils.d(TAG, "【拍照启动】Uri" + photoUri.toString());
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, "【拍照失败】" + e.getMessage());
LogUtils.e(TAG, String.format("handleTakePhoto() | %s", e.getMessage()));
}
}
@@ -675,6 +737,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @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);
@@ -689,7 +752,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
handlePixelPickerResult();
break;
default:
LogUtils.d(TAG, "【回调忽略】未知requestCode" + requestCode);
LogUtils.d(TAG, String.format("handleActivityResult() | 未知requestCode%d", requestCode));
break;
}
}
@@ -699,10 +762,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据
*/
private void handleTakePhotoResult(Intent data) {
LogUtils.d(TAG, "【业务逻辑】处理拍照结果");
LogUtils.d(TAG, "handleTakePhotoResult() 处理拍照结果");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【拍照结果处理】预览Bean为空");
LogUtils.e(TAG, "handleTakePhotoResult() | 预览Bean为空");
return;
}
@@ -712,7 +775,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
doubleRefreshPreview();
startImageCrop(false);
LogUtils.d(TAG, "【拍照完成】已启动裁剪");
LogUtils.d(TAG, "handleTakePhotoResult() | 已启动裁剪");
}
/**
@@ -720,30 +783,30 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据
*/
private void handleSelectPictureResult(Intent data) {
LogUtils.d(TAG, "【业务逻辑】处理选图结果");
LogUtils.d(TAG, "handleSelectPictureResult() 处理选图结果");
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("图片Uri为空");
LogUtils.e(TAG, "【选图结果】Uri为空");
LogUtils.e(TAG, "handleSelectPictureResult() | Uri为空");
return;
}
LogUtils.d(TAG, "【选图回调】系统返回Uri : " + selectedImage.toString());
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, "【选图权限】已添加持久化权限");
LogUtils.d(TAG, "handleSelectPictureResult() | 已添加持久化权限");
}
// 同步文件并启动裁剪
if (putUriFileToPreviewSource(selectedImage)) {
LogUtils.d(TAG, "【选图同步】路径绑定完成");
LogUtils.d(TAG, "handleSelectPictureResult() | 路径绑定完成");
startImageCrop(false);
} else {
ToastUtils.show("图片同步失败");
LogUtils.e(TAG, "【选图同步】文件复制失败");
LogUtils.e(TAG, "handleSelectPictureResult() | 文件复制失败");
}
}
@@ -753,9 +816,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @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, "【选图同步】Uri解析路径为空");
LogUtils.e(TAG, "putUriFileToPreviewSource() | Uri解析路径为空");
return false;
}
File srcFile = new File(filePath);
@@ -768,16 +832,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @return 同步成功返回true否则false
*/
private boolean putUriFileToPreviewSource(File srcFile) {
LogUtils.d(TAG, "【选图同步】源文件:" + srcFile.getAbsolutePath());
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源文件:%s", srcFile.getAbsolutePath()));
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
File dstFile = new File(previewBean.getBackgroundFilePath());
LogUtils.d(TAG, "【选图同步】目标文件:" + dstFile.getAbsolutePath());
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 目标文件:%s", dstFile.getAbsolutePath()));
if (FileUtils.copyFile(srcFile, dstFile)) {
LogUtils.d(TAG, "【选图同步】文件拷贝成功");
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件拷贝成功");
return true;
}
LogUtils.d(TAG, "【选图同步】文件无法拷贝");
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件无法拷贝");
return false;
}
@@ -786,10 +850,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param data 回调数据
*/
private void handleCropImageResult(Intent data) {
LogUtils.d(TAG, "【业务逻辑】处理裁剪结果");
LogUtils.d(TAG, "handleCropImageResult() 处理裁剪结果");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【裁剪结果处理】预览Bean为空");
LogUtils.e(TAG, "handleCropImageResult() | 预览Bean为空");
handleOperationCancelOrFail();
return;
}
@@ -813,8 +877,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param fileSize 文件大小
*/
private void handleCropSuccess(BackgroundBean previewBean, long fileSize) {
LogUtils.d(TAG, String.format("handleCropSuccess() | 裁剪成功,文件大小:%d", fileSize));
isPreviewBackgroundChanged = true;
LogUtils.d(TAG, "【裁剪结果】裁剪成功,文件大小:" + fileSize);
previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings();
@@ -828,15 +892,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param fileSize 文件大小
*/
private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) {
LogUtils.e(TAG, String.format("handleCropFailure() | 裁剪失败,文件状态:存在=%b可读=%b大小=%d",
isFileExist, isFileReadable, fileSize));
handleOperationCancelOrFail();
LogUtils.e(TAG, "【裁剪结果】裁剪失败,文件状态:存在=" + isFileExist + ",可读=" + isFileReadable + ",大小=" + fileSize);
}
/**
* 处理像素拾取结果
*/
private void handlePixelPickerResult() {
LogUtils.d(TAG, "【业务逻辑】处理像素拾取结果");
LogUtils.d(TAG, "handlePixelPickerResult() 处理像素拾取结果");
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
@@ -846,11 +911,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @param grantResults 权限结果数组
*/
private void handleCameraPermissionResult(int[] grantResults) {
LogUtils.d(TAG, "handleCameraPermissionResult() 处理相机权限结果");
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "【权限申请】相机权限授予成功");
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予成功");
handleTakePhoto();
} else {
LogUtils.d(TAG, "【权限申请】相机权限授予失败");
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予失败");
ToastUtils.show("相机权限被拒绝,无法拍照");
// 引导用户到设置页面开启权限(用户选择不再询问时)
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
@@ -863,18 +929,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* 启动应用设置页面
*/
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("请在设置中开启相机权限");
LogUtils.d(TAG, "【权限引导】启动应用设置页面");
}
/**
* 处理Finish确认对话框
*/
private void handleFinishConfirmation() {
LogUtils.d(TAG, "handleFinishConfirmation() 处理Finish确认");
if (isPreviewBackgroundChanged) {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
@@ -882,12 +949,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
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 {
@@ -901,9 +973,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
* @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, "【裁剪启动】预览Bean为空");
LogUtils.e(TAG, "startImageCrop() | 预览Bean为空");
ToastUtils.show("裁剪失败:无有效图片");
return;
}
@@ -915,7 +988,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
height,
isFreeCrop,
REQUEST_CROP_IMAGE);
LogUtils.d(TAG, "【裁剪启动】是否自由裁剪:" + isFreeCrop + ",目标尺寸:" + width + "x" + height);
LogUtils.d(TAG, String.format("startImageCrop() | 目标尺寸:%dx%d", width, height));
}
}

View File

@@ -34,32 +34,34 @@ import java.util.Map;
/**
* 电池报告页面统计应用24小时运行时长与电池消耗情况
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/22 13:21
* @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分钟毫秒数
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<AppBatteryModel>();
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
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 int batteryCapacity = 5400; // 电池容量mAh
private float lastBatteryPercent = 100.0f; // 上次电池百分比
private long lastCheckTime = System.currentTimeMillis(); // 上次检查时间戳
// 缓存相关
private Map<String, Long> appRunTimeCache = new HashMap<String, Long>();
private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
private Map<String, Long> appRunTimeCache = new HashMap<>();
private Map<String, String> packageToAppNameCache = new HashMap<>();
private PackageManager mPackageManager;
// ======================== 接口实现方法 =========================
@@ -73,7 +75,7 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
return TAG;
}
// ======================== 生命周期方法 =========================
// ======================== 生命周期方法(按执行顺序排列) =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -84,6 +86,7 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
initView();
// 初始化PackageManager
mPackageManager = getPackageManager();
LogUtils.d(TAG, "【onCreate】基础组件初始化完成");
// 权限检查Java7 传统条件判断)
if (!hasUsageStatsPermission(this)) {
@@ -100,15 +103,15 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
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());
LogUtils.d(TAG, "【onCreate】适配器初始化完成过滤后数据量:" + filteredList.size());
// 绑定搜索监听
// 绑定搜索监听 + 注册电池广播
bindSearchListener();
// 注册电池广播
registerBatteryReceiver();
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
@@ -156,8 +159,9 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化" + s.toString());
filterAppsByPackageAndName(s.toString());
String keyword = s.toString().trim();
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化" + keyword);
filterAppsByPackageAndName(keyword);
}
@Override
@@ -180,11 +184,14 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
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();
}
@@ -229,15 +236,12 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
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;
// 初始化:单次耗电=0累计耗电=0运行时长=0
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
}
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成共添加" + dataList.size() + "个包名");
}
@@ -253,7 +257,6 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
String appName = getAppNameByPackage(packageName);
packageToAppNameCache.put(packageName, appName);
}
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成共缓存" + packageToAppNameCache.size() + "个应用名称");
}
@@ -263,6 +266,7 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
* @return 应用名称,获取失败返回包名
*/
private String getAppNameByPackage(String packageName) {
LogUtils.v(TAG, "【getAppNameByPackage】查询包名" + packageName);
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
return mPackageManager.getApplicationLabel(appInfo).toString();
@@ -296,7 +300,7 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
* @return 应用包名-运行时长ms映射
*/
private Map<String, Long> getAppRunTime() {
Map<String, Long> runTimeMap = new HashMap<String, Long>();
Map<String, Long> runTimeMap = new HashMap<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
android.app.usage.UsageStatsManager manager =
@@ -341,7 +345,6 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
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);
@@ -351,11 +354,12 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
}
/**
* 计算单次耗电赋值给consumption+ 累加至累计耗电totalConsumption = totalConsumption + consumption
* 计算单次耗电赋值给consumption+ 累加至累计耗电
* @param dropPercent 电池下降百分比
* @param runTimeMap 应用运行时长映射
* @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()) {
@@ -363,28 +367,25 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
}
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长" + formatRunTime(totalSingleRunTime));
// 2. 遍历计算每个应用的单次耗电”并“累加至累计”
// 2. 遍历计算每个应用的单次耗电并累加
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
// 步骤1计算本次单次耗电
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
model.setConsumption(singleConsumption);
// 步骤2累加单次耗电到累计耗电
// 累加至累计耗电
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. 按累计耗电排序(从高到低)
// 3. 按累计耗电降序排序
Collections.sort(dataList, new Comparator<AppBatteryModel>() {
@Override
public int compare(AppBatteryModel m1, AppBatteryModel m2) {
@@ -392,8 +393,8 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
}
});
// 4. 重新应用过滤并刷新列表
filterAppsByPackageAndName(etSearch.getText().toString());
// 4. 重新过滤并刷新列表
filterAppsByPackageAndName(etSearch.getText().toString().trim());
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成列表已刷新");
}
@@ -414,9 +415,7 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
String appName = packageToAppNameCache.get(packageName);
String appNameLower = appName.toLowerCase();
boolean isMatched = packageNameLower.contains(lowerKeyword)
|| appNameLower.contains(lowerKeyword);
boolean isMatched = packageNameLower.contains(lowerKeyword) || appNameLower.contains(lowerKeyword);
if (isMatched) {
filteredList.add(model);
}
@@ -456,7 +455,6 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
* - consumption单次耗电两次电池广播间的消耗
* - totalConsumption累计耗电24小时初始化值+后续单次累加)
* - runTime运行时长ms
* - packageName应用包名
*/
public static class AppBatteryModel {
private String packageName; // 应用包名(核心标识)
@@ -519,6 +517,7 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
this.mDataList = dataList;
this.mPm = pm;
this.mPackageToNameCache = packageToNameCache;
LogUtils.d(TAG, "【BatteryReportAdapter】适配器构造完成数据量" + dataList.size());
}
@Override

View File

@@ -22,19 +22,20 @@ import java.util.ArrayList;
/**
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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; // 记录是否带换行显示
@@ -50,7 +51,7 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
return TAG;
}
// ======================== 生命周期方法 =========================
// ======================== 生命周期方法(按执行顺序排列) =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -59,11 +60,11 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
// 初始化应用实例
mApplication = (App) getApplication();
// 初始化UI组件
LogUtils.d(TAG, "【onCreate】应用实例初始化完成");
// 初始化核心逻辑
initView();
// 初始化滑动清理控件
initSeekBar();
// 初始化记录显示文本
initRecordText();
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
@@ -75,7 +76,7 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
*/
private void initView() {
// 初始化Toolbar
mToolbar = findViewById(R.id.toolbar);
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
@@ -83,29 +84,30 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
LogUtils.d(TAG, "【导航栏】点击返回按钮,关闭当前页面");
finish();
}
});
// 初始化显示文本组件
tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
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 = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
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】滑动清理触发");
LogUtils.d(TAG, "【onOHPCommit】滑动清理触发,开始执行记录清理逻辑");
// 清理电池历史记录
mApplication.clearBatteryHistory();
// 发送广播更新前台通知
@@ -114,10 +116,11 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
initRecordText();
// 提示清理成功
ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS);
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成已发送更新广播");
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成已发送前台通知更新广播");
}
});
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成");
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成回调监听已绑定");
}
// ======================== 业务逻辑方法 =========================
@@ -131,20 +134,20 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
// 判空处理:避免空列表导致异常
if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
szRecordText = getString(R.string.msg_no_battery_record);
LogUtils.d(TAG, "【initRecordText】无电池记录数据");
LogUtils.d(TAG, "【initRecordText】无电池记录数据,显示空记录提示文本");
} else {
// 根据配置切换显示格式
if (mIsShowRecordWithEnter) {
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
LogUtils.d(TAG, "【initRecordText】使用带换行格式显示记录数量" + listBatteryInfo.size());
LogUtils.d(TAG, String.format("【initRecordText】使用带换行格式显示记录记录数量:%d", listBatteryInfo.size()));
} else {
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
LogUtils.d(TAG, "【initRecordText】使用无换行格式显示记录数量" + listBatteryInfo.size());
LogUtils.d(TAG, String.format("【initRecordText】使用无换行格式显示记录记录数量:%d", listBatteryInfo.size()));
}
}
mtvRecordText.setText(szRecordText);
LogUtils.d(TAG, "【initRecordText】记录显示文本初始化完成");
LogUtils.d(TAG, "【initRecordText】记录显示文本刷新完成");
}
// ======================== 事件回调方法 =========================
@@ -155,7 +158,7 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
public void onShowRecordWithEnter(View view) {
Switch swShowRecordWithEnter = (Switch) view;
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
LogUtils.d(TAG, "【onShowRecordWithEnter】记录显示格式切换带换行" + mIsShowRecordWithEnter);
LogUtils.d(TAG, String.format("【onShowRecordWithEnter】记录显示格式切换带换行显示:%b", mIsShowRecordWithEnter));
// 刷新记录显示
initRecordText();
}

View File

@@ -16,6 +16,7 @@ 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;
@@ -49,7 +50,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
// ======================== 成员变量 =========================
// UI组件
private AToolbar mAToolbar;
private Toolbar mToolbar;
private ImageView imageView;
private TextView infoText;
private ViewGroup imageContainer;
@@ -112,7 +113,6 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
* 初始化所有UI组件
*/
private void initView() {
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
imageView = findViewById(R.id.imageView);
infoText = findViewById(R.id.infoText);
imageContainer = findViewById(R.id.imageContainer);
@@ -125,17 +125,24 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
* 初始化工具栏,设置导航与标题
*/
private void initToolbar() {
setActionBar(mAToolbar);
mAToolbar.setSubtitle(R.string.subtitle_activity_pixelpicker);
getActionBar().setDisplayHomeAsUpEnabled(true);
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
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, "导航栏点击返回");
LogUtils.d(TAG, "导航栏 点击返回按钮");
finish();
}
});
LogUtils.d(TAG, "initToolbar】工具栏初始化完成");
LogUtils.d(TAG, "initToolbar() 配置完成");
}
// ======================== 业务逻辑方法 =========================

View File

@@ -1,12 +1,24 @@
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.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import java.lang.reflect.Field;
/**
* 应用设置窗口,提供应用配置项的统一入口
@@ -44,6 +56,13 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
// 初始化工具栏
initToolbar();
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
((CheckBox)findViewById(R.id.activitysettingsCheckBox1)).setChecked(thoughtfulServiceBean.isEnableUsePowerTts());
((CheckBox)findViewById(R.id.activitysettingsCheckBox2)).setChecked(thoughtfulServiceBean.isEnableChargeTts());
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
}
@@ -70,5 +89,96 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
});
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
}
public void onCheckTTSDrawOverlaysPermission(View view) {
canDrawOverlays();
}
public void onEnableChargeTts(View view) {
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
thoughtfulServiceBean.setIsEnableChargeTts(((CheckBox)view).isChecked());
ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
}
public void onEnableUsePowerTts(View view) {
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
thoughtfulServiceBean.setIsEnableUsePowerTts(((CheckBox)view).isChecked());
ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
}
/**
* 悬浮窗权限检查与请求
*/
void canDrawOverlays() {
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
// API6.0+校验权限
if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
showDrawOverlayRequestDialog();
} else {
ToastUtils.show("悬浮窗权限已开启");
}
}
/**
* 显示悬浮窗权限请求对话框
*/
private void showDrawOverlayRequestDialog() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
jumpToDrawOverlaySettings();
}
})
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
// 解决对话框焦点问题
if (dialog.getWindow() != null) {
dialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
dialog.show();
}
/**
* 跳转悬浮窗权限设置页面(反射适配低版本)
*/
private void jumpToDrawOverlaySettings() {
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
try {
// 反射获取设置页面Action避免高版本API依赖
Class<?> settingsClazz = Settings.class;
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
String action = (String) actionField.get(null);
// 跳转当前应用权限设置页
Intent intent = new Intent(action);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception e) {
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -9,9 +9,11 @@ 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;
@@ -134,6 +136,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
// 解析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();
@@ -142,7 +145,8 @@ public class BackgroundPicturePreviewDialog extends Dialog {
}
// 加载图片到预览视图
mBackgroundView.loadImage(szSrcImage);
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
mBackgroundView.loadImage(nCurrentPixelColor, szSrcImage, true);
LogUtils.d(TAG, "【previewRecivedPicture】图片预览完成文件路径" + szSrcImage);
}
}

View File

@@ -107,7 +107,9 @@ public class NetworkBackgroundDialog extends AlertDialog {
// 图片加载成功,获取文件路径并设置背景
mDownloadSavedPath = (String) msg.obj;
LogUtils.d(TAG, String.format("handleMessage: 图片加载成功,保存路径:%s", mDownloadSavedPath));
mBackgroundView.loadImage(mDownloadSavedPath);
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
mBackgroundView.loadImage(nCurrentPixelColor, mDownloadSavedPath, true);
break;
case MSG_IMAGE_LOAD_FAILED:
// 图片加载失败,设置默认背景

View File

@@ -10,26 +10,28 @@ import java.io.IOException;
import java.io.Serializable;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类
* 适配 API30支持 Serializable 持久化、Parcelable Intent 传递、JSON 序列化/反序列化
* 包含耗电提醒、充电提醒、电量检测、铃声提醒等核心配置
* 包含耗电提醒、充电提醒、电量检测、铃声提醒、相框尺寸等核心配置
*/
public class AppConfigBean extends BaseBean implements Serializable, Parcelable {
// ====================== 静态常量(首屏可见,统一管理) ======================
// ====================== 静态常量(首屏可见,统一管理) ======================
// 序列化版本号Serializable 必备,避免反序列化失败)
private static final long serialVersionUID = 1L;
// 日志标签(全局统一,替换 Log 为 LogUtils
// 日志标签(全局统一)
transient public static final String TAG = "AppConfigBean";
// 字段校验常量(统一阈值,避免硬编码)
private static final int MIN_INTERVAL = 500; // 最小检测间隔ms
private static final int MIN_REMIND_INTERVAL = 1000; // 最小提醒间隔ms
private static final int BATTERY_MIN = 0; // 电量最小值
private static final int BATTERY_MAX = 100; // 电量最大值
private static final int INVALID_BATTERY = -1; // 无效电量标识
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
@@ -40,9 +42,11 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
int reminderIntervalTime = 5000; // 铃声提醒间隔ms
// 电量状态
boolean isCharging = false; // 是否充电
int currentBatteryValue = INVALID_BATTERY; // 当前电池电量(统一命名,替代原 currentValue
// 电量检测配置
int batteryDetectInterval = 2000; // 电量检测间隔ms适配 RemindThread
// 相框配置
int defaultFrameWidth = DEFAULT_FRAME_WIDTH; // 默认相框宽度px
int defaultFrameHeight = DEFAULT_FRAME_HEIGHT;// 默认相框高度px
// ====================== 构造方法(初始化默认配置,强化默认值校验) ======================
public AppConfigBean() {
@@ -51,55 +55,17 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
setUsageReminderValue(10);
setEnableUsageReminder(false);
setReminderIntervalTime(5000);
setBatteryDetectInterval(1000); // 默认检测间隔1秒
setCurrentBatteryValue(INVALID_BATTERY); // 初始化无效电量标识
LogUtils.d(TAG, "AppConfigBean: 初始化默认配置完成");
setBatteryDetectInterval(1000);
setDefaultFrameWidth(DEFAULT_FRAME_WIDTH);
setDefaultFrameHeight(DEFAULT_FRAME_HEIGHT);
LogUtils.d(TAG, "AppConfigBean() 构造器执行 | 默认配置初始化完成");
}
// ====================== 核心业务方法Setter/Getter按字段功能分类补充调试日志 ======================
// --------------- 电量状态相关 ---------------
/**
* 设置当前电池电量Receiver 监听电池变化时调用)
* @param currentBatteryValue 当前电量0-100
*/
public void setCurrentBatteryValue(int currentBatteryValue) {
this.currentBatteryValue = (currentBatteryValue >= BATTERY_MIN && currentBatteryValue <= BATTERY_MAX)
? currentBatteryValue : INVALID_BATTERY;
LogUtils.d(TAG, String.format("setCurrentBatteryValue: 当前电量设置为 %d输入值%d",
this.currentBatteryValue, currentBatteryValue));
}
/**
* 获取当前电池电量RemindThread 同步配置时调用)
* @return 当前电量0-100 或 INVALID_BATTERY
*/
public int getCurrentBatteryValue() {
return currentBatteryValue;
}
// --------------- 铃声提醒配置相关 ---------------
/**
* 设置铃声提醒间隔
* @param reminderIntervalTime 提醒间隔ms不小于 MIN_REMIND_INTERVAL
*/
public void setReminderIntervalTime(int reminderIntervalTime) {
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
LogUtils.d(TAG, String.format("setReminderIntervalTime: 提醒间隔设置为 %dms输入值%d",
this.reminderIntervalTime, reminderIntervalTime));
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
// --------------- 充电状态相关 ---------------
/**
* 设置是否充电
* @param isCharging 充电状态
*/
public void setIsCharging(boolean isCharging) {
this.isCharging = isCharging;
LogUtils.d(TAG, String.format("setIsCharging: 充电状态设置为 %b", isCharging));
LogUtils.d(TAG, String.format("setIsCharging() 执行 | 充电状态=%b", isCharging));
}
public boolean isCharging() {
@@ -109,21 +75,16 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
// --------------- 耗电提醒配置相关 ---------------
public void setEnableUsageReminder(boolean isEnableUsageReminder) {
this.isEnableUsageReminder = isEnableUsageReminder;
LogUtils.d(TAG, String.format("setEnableUsageReminder: 耗电提醒开关设置为 %b", isEnableUsageReminder));
LogUtils.d(TAG, String.format("setEnableUsageReminder() 执行 | 耗电提醒开关=%b", isEnableUsageReminder));
}
public boolean isEnableUsageReminder() {
return isEnableUsageReminder;
}
/**
* 设置耗电提醒阈值
* @param usageReminderValue 阈值0-100
*/
public void setUsageReminderValue(int usageReminderValue) {
this.usageReminderValue = Math.min(Math.max(usageReminderValue, BATTERY_MIN), BATTERY_MAX);
LogUtils.d(TAG, String.format("setUsageReminderValue: 耗电提醒阈值设置为 %d输入值%d",
this.usageReminderValue, usageReminderValue));
LogUtils.d(TAG, String.format("setUsageReminderValue() 执行 | 最终阈值=%d | 输入值=%d", this.usageReminderValue, usageReminderValue));
}
public int getUsageReminderValue() {
@@ -133,43 +94,62 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
// --------------- 充电提醒配置相关 ---------------
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
this.isEnableChargeReminder = isEnableChargeReminder;
LogUtils.d(TAG, String.format("setEnableChargeReminder: 充电提醒开关设置为 %b", isEnableChargeReminder));
LogUtils.d(TAG, String.format("setEnableChargeReminder() 执行 | 充电提醒开关=%b", isEnableChargeReminder));
}
public boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
/**
* 设置充电提醒阈值
* @param chargeReminderValue 阈值0-100
*/
public void setChargeReminderValue(int chargeReminderValue) {
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, BATTERY_MIN), BATTERY_MAX);
LogUtils.d(TAG, String.format("setChargeReminderValue: 充电提醒阈值设置为 %d输入值%d",
this.chargeReminderValue, chargeReminderValue));
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;
}
// --------------- 电量检测配置相关 ---------------
/**
* 设置电量检测间隔
* @param batteryDetectInterval 检测间隔ms不小于 MIN_INTERVAL
*/
public void setBatteryDetectInterval(int batteryDetectInterval) {
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
LogUtils.d(TAG, String.format("setBatteryDetectInterval: 检测间隔设置为 %dms输入值%d",
this.batteryDetectInterval, batteryDetectInterval));
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 执行 | 最终间隔=%dms | 输入值=%dms", this.batteryDetectInterval, batteryDetectInterval));
}
public int getBatteryDetectInterval() {
return batteryDetectInterval;
}
// ====================== JSON 序列化/反序列化(兼容旧配置,补充调试日志) ======================
// --------------- 相框配置相关 ---------------
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();
@@ -178,62 +158,72 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
AppConfigBean bean = this;
LogUtils.d(TAG, "writeThisToJsonWriter() 执行 | 开始JSON序列化");
// 原有字段序列化
jsonWriter.name("isEnableUsageReminder").value(bean.isEnableUsageReminder());
jsonWriter.name("usageReminderValue").value(bean.getUsageReminderValue());
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
jsonWriter.name("reminderIntervalTime").value(bean.getReminderIntervalTime());
jsonWriter.name("isCharging").value(bean.isCharging());
// 兼容旧字段 currentValue同步新字段 currentBatteryValue
jsonWriter.name("currentBatteryValue").value(bean.getCurrentBatteryValue());
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue());
// 新增字段序列化
jsonWriter.name("batteryDetectInterval").value(bean.getBatteryDetectInterval());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成");
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());
} else if (name.equals("currentValue")) {
// 优先读取旧字段,兼容历史配置
bean.setCurrentBatteryValue(jsonReader.nextInt());
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 currentValue 完成");
} else if (name.equals("currentBatteryValue")) {
// 新字段覆盖旧字段,保证数据最新
bean.setCurrentBatteryValue(jsonReader.nextInt());
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 currentBatteryValue 完成");
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));
LogUtils.w(TAG, String.format("readBeanFromJsonReader() 跳过未知字段 | %s", name));
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化");
LogUtils.d(TAG, "readBeanFromJsonReader() 完成 | JSON反序列化成");
return bean;
}
// ====================== Parcelable 接口实现API30 Intent 传递必备,补充调试日志 ======================
// ====================== Parcelable 接口实现API30 Intent 传递必备) ======================
@Override
public int describeContents() {
return 0; // 无特殊内容描述固定返回0
@@ -241,6 +231,7 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
@Override
public void writeToParcel(Parcel dest, int flags) {
LogUtils.d(TAG, "writeToParcel() 执行 | 开始Parcel序列化");
// 按成员变量顺序写入boolean 转 byte 存储
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0));
dest.writeInt(usageReminderValue);
@@ -248,15 +239,17 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
dest.writeInt(chargeReminderValue);
dest.writeInt(reminderIntervalTime);
dest.writeByte((byte) (isCharging ? 1 : 0));
dest.writeInt(currentBatteryValue);
dest.writeInt(batteryDetectInterval);
LogUtils.d(TAG, "writeToParcel: Parcel 序列化完成");
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;
@@ -265,9 +258,10 @@ public class AppConfigBean extends BaseBean implements Serializable, Parcelable
bean.chargeReminderValue = source.readInt();
bean.reminderIntervalTime = source.readInt();
bean.isCharging = source.readByte() != 0;
bean.currentBatteryValue = source.readInt();
bean.batteryDetectInterval = source.readInt();
LogUtils.d(TAG, "createFromParcel: Parcel 反序列化完成");
bean.defaultFrameWidth = source.readInt();
bean.defaultFrameHeight = source.readInt();
LogUtils.d(TAG, "createFromParcel() 完成 | Parcel反序列化成功");
return bean;
}

View File

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

View File

@@ -17,7 +17,7 @@ import cc.winboll.studio.libappbase.LogUtils;
*/
public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable {
// ====================== 静态常量(置顶统一管理,避免魔法值) ======================
private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
//private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
private static final String TAG = "ControlCenterServiceBean";
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService"; // JSON 字段常量,避免硬编码

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.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;
@@ -12,13 +13,15 @@ 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;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/19 20:23
* @Describe 控制中心广播接收器
* 控制中心广播接收器
* 功能:监听电池状态变化、前台通知更新、配置变更指令
* 适配Java7 | API30 | 内存泄漏防护 | 多线程状态同步
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/19 20:23
* @Describe 统一处理系统与应用内广播,同步电池状态与配置,保障多线程数据一致性
*/
public class ControlCenterServiceReceiver extends BroadcastReceiver {
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
@@ -33,9 +36,10 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
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; // 无效电量标识
// ====================== 静态状态标记volatile保证多线程可见性 ======================
private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见)
private static volatile int sLastBatteryLevel = INVALID_BATTERY; // 上次电量(多线程可见)
private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见)
// ====================== 成员变量区(弱引用防泄漏,按功能分层) ======================
@@ -44,26 +48,27 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
// ====================== 构造方法(初始化弱引用,避免服务强引用泄漏) ======================
public ControlCenterServiceReceiver(ControlCenterService service) {
LogUtils.d(TAG, String.format("构造接收器 | 服务实例:%s", service != null ? service.getClass().getSimpleName() : "null"));
this.mwrControlCenterService = new WeakReference<>(service);
LogUtils.d(TAG, String.format("ControlCenterServiceReceiver() 构造 | 服务实例:%s",
service != null ? service.getClass().getSimpleName() : "null"));
this.mwrControlCenterService = new WeakReference<ControlCenterService>(service);
}
// ====================== 广播核心接收逻辑入口方法分Action分发处理 ======================
@Override
public void onReceive(Context context, Intent intent) {
String action = intent != null ? intent.getAction() : "null";
LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action%s", action));
LogUtils.d(TAG, String.format("onReceive() 执行 | 接收广播 Action%s", action));
// 基础参数校验
if (context == null || intent == null || action == null) {
LogUtils.e(TAG, "onReceive: 参数无效context=" + context + " | intent=" + intent + ",终止处理");
LogUtils.e(TAG, "onReceive() 终止 | 参数无效context=" + context + " | intent=" + intent + "");
return;
}
// 弱引用获取服务,双重校验服务有效性
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
if (service == null || service.isDestroyed()) {
LogUtils.e(TAG, "onReceive: 服务已销毁或为空,注销广播");
LogUtils.e(TAG, "onReceive() 终止 | 服务已销毁或为空,执行注销");
unregisterAction(context);
return;
}
@@ -77,14 +82,14 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
handleUpdateForegroundNotification(service);
break;
case ACTION_APPCONFIG_CHANGED:
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播");
LogUtils.d(TAG, "onReceive() 分发 | 处理配置更新广播");
handleNotifyAppConfigUpdate(service);
break;
default:
LogUtils.w(TAG, String.format("onReceive: 未知Action=%s", action));
LogUtils.w(TAG, String.format("onReceive() 警告 | 未知Action=%s", action));
}
LogUtils.d(TAG, "onReceive: 广播处理完成");
LogUtils.d(TAG, "onReceive() 完成 | 广播处理结束");
}
// ====================== 业务处理方法(按功能拆分,强化容错与日志) ======================
@@ -94,19 +99,29 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param intent 电池状态广播意图
*/
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
LogUtils.d(TAG, "handleBatteryStateChanged() 执行 | 解析电池状态");
try {
// 1. 解析并校验当前电池状态
boolean currentCharging = BatteryUtils.isCharging(intent);
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
LogUtils.d(TAG, String.format("handleBatteryStateChanged() 解析 | 充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
// 2. 状态无变化则跳过,减少无效运算
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态无变化,跳过处理");
LogUtils.d(TAG, "handleBatteryStateChanged() 跳过 | 电池状态无变化");
return;
}
// 在插拔充电线时,执行贴心服务
if(currentCharging != sIsCharging && sLastBatteryLevel != INVALID_BATTERY) {
//App.notifyMessage(TAG, String.format("sLastBatteryLevel %d", sLastBatteryLevel));
if(currentCharging) {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.CHARGE_STATE);
} else {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.DISCHARGE_STATE);
}
}
// 3. 更新静态缓存状态,保证多线程可见
sIsCharging = currentCharging;
@@ -115,9 +130,10 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
// 4. 同步缓存状态到配置
handleNotifyAppConfigUpdate(service);
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 处理成功 | 缓存电量=%d%% | 缓存充电状态=%b", sLastBatteryLevel, sIsCharging));
LogUtils.d(TAG, String.format("handleBatteryStateChanged() 完成 | 缓存电量=%d%% | 缓存充电状态=%b",
sLastBatteryLevel, sIsCharging));
} catch (Exception e) {
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
LogUtils.e(TAG, "handleBatteryStateChanged() 失败", e);
}
}
@@ -126,26 +142,26 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param service 控制中心服务实例
*/
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置");
LogUtils.d(TAG, "handleNotifyAppConfigUpdate() 执行 | 同步缓存状态到配置");
try {
// 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 最新配置为空,终止处理");
LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
return;
}
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=%d | 耗电阈值=%d",
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate() 加载 | 充电阈值=%d | 耗电阈值=%d",
latestConfig.getChargeReminderValue(), latestConfig.getUsageReminderValue()));
// 同步缓存的电池状态到配置
latestConfig.setCurrentBatteryValue(sLastBatteryLevel);
App.sQuantityOfElectricity = sLastBatteryLevel;
latestConfig.setIsCharging(sIsCharging);
service.notifyAppConfigUpdate(latestConfig);
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=%d%% | 充电状态=%b", sLastBatteryLevel, sIsCharging));
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成");
LogUtils.d(TAG, String.format("handleNotifyAppConfigUpdate() 完成 | 缓存电量=%d%% | 充电状态=%b",
sLastBatteryLevel, sIsCharging));
} catch (Exception e) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 处理失败", e);
LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 失败", e);
}
}
@@ -154,21 +170,22 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param service 控制中心服务实例
*/
private void handleUpdateForegroundNotification(ControlCenterService service) {
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知");
LogUtils.d(TAG, "handleUpdateForegroundNotification() 执行 | 更新前台通知");
try {
NotificationManagerUtils notifyUtils = service.getNotificationManager();
NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
// 非空校验,避免空指针
if (notifyUtils == null || notifyMsg == null) {
LogUtils.e(TAG, String.format("handleUpdateForegroundNotification: 通知工具类或消息为空notifyUtils=%s | notifyMsg=%s", notifyUtils, notifyMsg));
LogUtils.e(TAG, String.format("handleUpdateForegroundNotification() 终止 | 通知工具类或消息为空notifyUtils=%s | notifyMsg=%s",
notifyUtils, notifyMsg));
return;
}
notifyUtils.updateForegroundServiceNotify(notifyMsg);
LogUtils.d(TAG, String.format("handleUpdateForegroundNotification: 前台通知更新成功 | 标题=%s", notifyMsg.getTitle()));
LogUtils.d(TAG, String.format("handleUpdateForegroundNotification() 完成 | 标题=%s", notifyMsg.getTitle()));
} catch (Exception e) {
LogUtils.e(TAG, "handleUpdateForegroundNotification: 处理失败", e);
LogUtils.e(TAG, "handleUpdateForegroundNotification() 失败", e);
}
}
@@ -178,9 +195,9 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param context 上下文
*/
public void registerAction(Context context) {
LogUtils.d(TAG, "registerAction: 注册广播接收器");
LogUtils.d(TAG, "registerAction() 执行 | 注册广播接收器");
if (context == null || isRegistered) {
LogUtils.e(TAG, "registerAction: 上下文为空或已注册,注册失败");
LogUtils.e(TAG, "registerAction() 失败 | 上下文为空或已注册");
return;
}
@@ -193,9 +210,9 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
context.registerReceiver(this, filter);
isRegistered = true;
LogUtils.d(TAG, String.format("registerAction: 广播注册成功 | 优先级=%d", BROADCAST_PRIORITY));
LogUtils.d(TAG, String.format("registerAction() 完成 | 优先级=%d", BROADCAST_PRIORITY));
} catch (Exception e) {
LogUtils.e(TAG, "registerAction: 注册失败", e);
LogUtils.e(TAG, "registerAction() 失败", e);
}
}
@@ -204,20 +221,20 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* @param context 上下文
*/
public void unregisterAction(Context context) {
LogUtils.d(TAG, "unregisterAction: 注销广播接收器");
LogUtils.d(TAG, "unregisterAction() 执行 | 注销广播接收器");
if (context == null || !isRegistered) {
LogUtils.e(TAG, "unregisterAction: 上下文为空或未注册,注销失败");
LogUtils.e(TAG, "unregisterAction() 失败 | 上下文为空或未注册");
return;
}
try {
context.unregisterReceiver(this);
isRegistered = false;
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
LogUtils.d(TAG, "unregisterAction() 完成 | 广播注销成功");
} catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
LogUtils.w(TAG, "unregisterAction() 警告 | 广播未注册,跳过注销");
} catch (Exception e) {
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
LogUtils.e(TAG, "unregisterAction() 失败", e);
}
}
@@ -226,17 +243,17 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
* 主动释放资源,避免内存泄漏
*/
public void release() {
LogUtils.d(TAG, "release: 释放广播接收器资源");
LogUtils.d(TAG, "release() 执行 | 释放广播接收器资源");
// 清空弱引用帮助GC回收
if (mwrControlCenterService != null) {
mwrControlCenterService.clear();
mwrControlCenterService = null;
LogUtils.d(TAG, "release: 弱引用已清空");
LogUtils.d(TAG, "release() 步骤 | 弱引用已清空");
}
// 重置静态状态缓存
sLastBatteryLevel = -1;
sIsCharging = false;
LogUtils.d(TAG, "release: 静态状态缓存已重置");
LogUtils.d(TAG, "release() 完成 | 静态状态缓存已重置");
}
/**

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package cc.winboll.studio.powerbell.threads;
import android.content.Context;
import android.os.Message;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import java.lang.ref.WeakReference;
@@ -26,7 +27,6 @@ public class RemindThread extends Thread {
private static final long THREAD_JOIN_TIMEOUT = 1000L;
// 状态常量
private static final int INVALID_BATTERY_VALUE = -1;
private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100;
@@ -55,16 +55,15 @@ public class RemindThread extends Thread {
private volatile long sleepTime;
private volatile int chargeReminderValue;
private volatile int usageReminderValue;
private volatile int quantityOfElectricity;
private volatile boolean isCharging;
// ====================== 私有构造器(禁止外部实例化) ======================
private RemindThread(Context context, ControlCenterServiceHandler handler) {
LogUtils.d(TAG, String.format("构造器调用 | context=%s | handler=%s", context, handler));
LogUtils.d(TAG, String.format("RemindThread() 构造器调用 | context=%s | handler=%s", context, handler));
this.mContext = context.getApplicationContext();
this.mwrControlCenterServiceHandler = new WeakReference<>(handler);
resetThreadStateInternal();
LogUtils.d(TAG, String.format("构造完成 | threadId=%d | 初始状态重置成功", getId()));
LogUtils.d(TAG, String.format("RemindThread() 构造完成 | threadId=%d | 初始状态重置成功", getId()));
}
// ====================== 对外公开静态接口(多实例列表管理) ======================
@@ -77,7 +76,7 @@ public class RemindThread extends Thread {
* @return true: 启动成功false: 入参非法
*/
public static boolean startRemindThreadWithAppConfig(Context context, ControlCenterServiceHandler handler, AppConfigBean config) {
LogUtils.d(TAG, String.format("startRemindThreadWithAppConfig调用 | context=%s | handler=%s | config=%s", context, handler, config));
LogUtils.d(TAG, String.format("startRemindThreadWithAppConfig() 调用 | context=%s | handler=%s | config=%s", context, handler, config));
// 入参严格校验
if (context == null || handler == null || config == null) {
@@ -108,55 +107,12 @@ public class RemindThread extends Thread {
return true;
}
/**
* 启动提醒线程,同步电池状态信息
* 逻辑:停止所有旧线程 → 创建新线程 → 同步电池状态 → 加入列表管理
* @param context 上下文(非空)
* @param handler 服务处理器(非空)
* @param isCharging 充电状态
* @param batteryLevel 当前电量
* @return true: 启动成功false: 入参非法
*/
public static boolean startRemindThreadWithBatteryInfo(Context context, ControlCenterServiceHandler handler, boolean isCharging, int batteryLevel) {
LogUtils.d(TAG, String.format("startRemindThreadWithBatteryInfo调用 | context=%s | handler=%s | isCharging=%b | batteryLevel=%d", context, handler, isCharging, batteryLevel));
// 入参严格校验
if (context == null || handler == null) {
LogUtils.e(TAG, String.format("启动失败:入参为空 | context=%s | handler=%s", context, handler));
return false;
}
// 初始化线程列表(双重校验锁)
if (sRemindThreadList == null) {
synchronized (RemindThread.class) {
if (sRemindThreadList == null) {
sRemindThreadList = new ArrayList<RemindThread>();
LogUtils.d(TAG, "线程列表初始化完成");
}
}
}
// 停止所有旧线程
stopAllOldThreadsInternal();
// 创建并启动新线程
RemindThread newRemindThread = new RemindThread(context, handler);
// 同步电池状态(范围校验)
newRemindThread.isCharging = isCharging;
newRemindThread.quantityOfElectricity = Math.min(Math.max(batteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
newRemindThread.isExist = false;
newRemindThread.start();
sRemindThreadList.add(newRemindThread);
LogUtils.d(TAG, String.format("新线程启动成功 | threadId=%d | 电池状态同步完成(电量=%d充电=%b", newRemindThread.getId(), newRemindThread.quantityOfElectricity, newRemindThread.isCharging));
return true;
}
/**
* 安全停止所有线程,清空列表
*/
public static void stopRemindThread() {
int listSize = sRemindThreadList != null ? sRemindThreadList.size() : 0;
LogUtils.d(TAG, String.format("stopRemindThread调用 | 列表存在=%b | 列表大小=%d", sRemindThreadList != null, listSize));
LogUtils.d(TAG, String.format("stopRemindThread() 调用 | 列表存在=%b | 列表大小=%d", sRemindThreadList != null, listSize));
if (sRemindThreadList == null || sRemindThreadList.isEmpty()) {
LogUtils.w(TAG, "停止失败:线程列表为空");
return;
@@ -194,7 +150,7 @@ public class RemindThread extends Thread {
// ====================== 线程核心运行逻辑 ======================
@Override
public void run() {
LogUtils.d(TAG, String.format("run执行 | threadId=%d | 状态=%s", getId(), getState()));
LogUtils.d(TAG, String.format("run() 执行 | threadId=%d | 状态=%s", getId(), getState()));
// 初始化提醒状态(加锁保护,避免多线程竞争)
synchronized (mRemindLock) {
@@ -213,21 +169,21 @@ public class RemindThread extends Thread {
if (isExist) break;
// 电量有效性校验非0-100视为无效退出电量提醒线程
if (quantityOfElectricity < BATTERY_LEVEL_MIN || quantityOfElectricity > BATTERY_LEVEL_MAX) {
LogUtils.w(TAG, String.format("电量无效,退出电量提醒线程 | 当前电量=%d | threadId=%d", quantityOfElectricity, getId()));
if (App.sQuantityOfElectricity < BATTERY_LEVEL_MIN || App.sQuantityOfElectricity > BATTERY_LEVEL_MAX) {
LogUtils.w(TAG, String.format("电量无效,退出电量提醒线程 | 当前电量=%d | threadId=%d", App.sQuantityOfElectricity, getId()));
break;
}
// 充电/耗电提醒触发逻辑
boolean chargeRemindTrigger = isCharging && isEnableChargeReminder && quantityOfElectricity >= chargeReminderValue;
boolean usageRemindTrigger = !isCharging && isEnableUsageReminder && quantityOfElectricity <= usageReminderValue;
boolean chargeRemindTrigger = isCharging && isEnableChargeReminder && App.sQuantityOfElectricity >= chargeReminderValue;
boolean usageRemindTrigger = !isCharging && isEnableUsageReminder && App.sQuantityOfElectricity <= usageReminderValue;
if (chargeRemindTrigger) {
LogUtils.d(TAG, String.format("触发充电提醒 | 当前电量=%d ≥ 阈值=%d | threadId=%d", quantityOfElectricity, chargeReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_CHARGE, quantityOfElectricity, isCharging);
LogUtils.d(TAG, String.format("触发充电提醒 | 当前电量=%d ≥ 阈值=%d | threadId=%d", App.sQuantityOfElectricity, chargeReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_CHARGE, App.sQuantityOfElectricity, isCharging);
} else if (usageRemindTrigger) {
LogUtils.d(TAG, String.format("触发耗电提醒 | 当前电量=%d ≤ 阈值=%d | threadId=%d", quantityOfElectricity, usageReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_USAGE, quantityOfElectricity, isCharging);
LogUtils.d(TAG, String.format("触发耗电提醒 | 当前电量=%d ≤ 阈值=%d | threadId=%d", App.sQuantityOfElectricity, usageReminderValue, getId()));
sendNotificationMessageInternal(REMIND_TYPE_USAGE, App.sQuantityOfElectricity, isCharging);
} else {
LogUtils.d(TAG, String.format("未有合适类型提醒,退出提醒线程 | threadId=%d", getId()));
break;
@@ -237,14 +193,14 @@ public class RemindThread extends Thread {
safeSleepInternal(sleepTime);
} catch (Exception e) {
LogUtils.e(TAG, String.format("循环运行异常,退出电量提醒线程 | 当前电量=%d | threadId=%d", quantityOfElectricity, getId()), e);
LogUtils.e(TAG, String.format("循环运行异常,退出电量提醒线程 | 当前电量=%d | threadId=%d", App.sQuantityOfElectricity, getId()), e);
break;
}
}
// 循环退出,清理状态
cleanThreadStateInternal();
LogUtils.d(TAG, String.format("run结束 | threadId=%d", getId()));
LogUtils.d(TAG, String.format("run() 结束 | threadId=%d", getId()));
}
// ====================== 内部业务辅助方法 ======================
@@ -255,7 +211,7 @@ public class RemindThread extends Thread {
* @param isCharging 充电状态
*/
private void sendNotificationMessageInternal(String type, int battery, boolean isCharging) {
LogUtils.d(TAG, String.format("sendNotificationMessageInternal调用 | 类型=%s | 电量=%d | isCharging=%b | threadId=%d", type, battery, isCharging, getId()));
LogUtils.d(TAG, String.format("sendNotificationMessageInternal() 调用 | 类型=%s | 电量=%d | isCharging=%b | threadId=%d", type, battery, isCharging, getId()));
// 前置状态校验
if (isExist || !isReminding) {
LogUtils.d(TAG, String.format("消息发送跳过:线程已退出或提醒关闭 | threadId=%d", getId()));
@@ -292,7 +248,7 @@ public class RemindThread extends Thread {
* @param millis 休眠时长(ms)
*/
private void safeSleepInternal(long millis) {
LogUtils.d(TAG, String.format("safeSleepInternal调用 | 休眠时长=%dms | threadId=%d", millis, getId()));
LogUtils.d(TAG, String.format("safeSleepInternal() 调用 | 休眠时长=%dms | threadId=%d", millis, getId()));
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
@@ -305,7 +261,7 @@ public class RemindThread extends Thread {
* 重置线程初始状态(构造器专用)
*/
private void resetThreadStateInternal() {
LogUtils.d(TAG, String.format("resetThreadStateInternal调用 | threadId=%d", getId()));
LogUtils.d(TAG, String.format("resetThreadStateInternal() 调用 | threadId=%d", getId()));
// 状态标记初始化
isExist = false;
isReminding = false;
@@ -315,7 +271,6 @@ public class RemindThread extends Thread {
sleepTime = MIN_SLEEP_TIME;
chargeReminderValue = -1;
usageReminderValue = -1;
quantityOfElectricity = INVALID_BATTERY_VALUE;
isCharging = false;
LogUtils.d(TAG, String.format("线程初始状态重置完成 | threadId=%d", getId()));
}
@@ -324,10 +279,9 @@ public class RemindThread extends Thread {
* 清理线程运行状态(循环退出时调用)
*/
private void cleanThreadStateInternal() {
LogUtils.d(TAG, String.format("cleanThreadStateInternal调用 | threadId=%d", getId()));
LogUtils.d(TAG, String.format("cleanThreadStateInternal() 调用 | threadId=%d", getId()));
isReminding = false;
isExist = true;
quantityOfElectricity = INVALID_BATTERY_VALUE;
// 中断当前线程(如果存活)
if (isAlive()) {
interrupt();
@@ -341,10 +295,9 @@ public class RemindThread extends Thread {
* @param config 应用配置Bean
*/
public void setAppConfigBean(AppConfigBean config) {
LogUtils.d(TAG, String.format("setAppConfigBean调用 | config=%s | threadId=%d", config, getId()));
LogUtils.d(TAG, String.format("setAppConfigBean() 调用 | config=%s | threadId=%d", config, getId()));
if (config == null) {
LogUtils.e(TAG, String.format("配置同步失败配置Bean为空 | threadId=%d", getId()));
quantityOfElectricity = INVALID_BATTERY_VALUE;
return;
}
@@ -354,12 +307,12 @@ public class RemindThread extends Thread {
chargeReminderValue = Math.min(Math.max(config.getChargeReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
usageReminderValue = Math.min(Math.max(config.getUsageReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
sleepTime = Math.max(config.getBatteryDetectInterval(), MIN_SLEEP_TIME);
quantityOfElectricity = (config.getCurrentBatteryValue() >= BATTERY_LEVEL_MIN && config.getCurrentBatteryValue() <= BATTERY_LEVEL_MAX)
? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE;
// sQuantityOfElectricity = (config.getCurrentBatteryValue() >= BATTERY_LEVEL_MIN && config.getCurrentBatteryValue() <= BATTERY_LEVEL_MAX)
// ? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE;
isCharging = config.isCharging();
LogUtils.d(TAG, String.format("配置同步完成 | 休眠时间=%dms | 充电提醒=%b | 耗电提醒=%b | 当前电量=%d | 充电阈值=%d | 耗电阈值=%d | threadId=%d",
sleepTime, isEnableChargeReminder, isEnableUsageReminder, quantityOfElectricity, chargeReminderValue, usageReminderValue, getId()));
sleepTime, isEnableChargeReminder, isEnableUsageReminder, App.sQuantityOfElectricity, chargeReminderValue, usageReminderValue, getId()));
}
/**
@@ -368,13 +321,13 @@ public class RemindThread extends Thread {
*/
private boolean isRunning() {
boolean running = !isExist && isAlive();
LogUtils.d(TAG, String.format("isRunning调用 | 运行中=%b | 退出标记=%b | 存活=%b | threadId=%d", running, isExist, isAlive(), getId()));
LogUtils.d(TAG, String.format("isRunning() 调用 | 运行中=%b | 退出标记=%b | 存活=%b | threadId=%d", running, isExist, isAlive(), getId()));
return running;
}
// ====================== Getter/Setter按需开放 ======================
public void setIsExist(boolean isExist) {
LogUtils.d(TAG, String.format("setIsExist调用 | isExist=%b | threadId=%d", isExist, getId()));
LogUtils.d(TAG, String.format("setIsExist() 调用 | isExist=%b | threadId=%d", isExist, getId()));
this.isExist = isExist;
}
@@ -386,16 +339,16 @@ public class RemindThread extends Thread {
@Override
public String toString() {
return "RemindThread{" +
"threadId=" + getId() +
", threadName='" + getName() + '\'' +
", isRunning=" + isRunning() +
", isReminding=" + isReminding +
", chargeThreshold=" + chargeReminderValue +
", usageThreshold=" + usageReminderValue +
", currentBattery=" + quantityOfElectricity +
", isCharging=" + isCharging +
", sleepTime=" + sleepTime + "ms" +
'}';
"threadId=" + getId() +
", threadName='" + getName() + '\'' +
", isRunning=" + isRunning() +
", isReminding=" + isReminding +
", chargeThreshold=" + chargeReminderValue +
", usageThreshold=" + usageReminderValue +
", currentBattery=" + App.sQuantityOfElectricity +
", isCharging=" + isCharging +
", sleepTime=" + sleepTime + "ms" +
'}';
}
}

View File

@@ -14,6 +14,7 @@ import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
@@ -104,7 +105,8 @@ public class MainUnitTest2Activity extends AppCompatActivity {
mllBackgroundView = (LinearLayout) findViewById(R.id.ll_backgroundview);
// 创建MemoryCachedBackgroundView单例并添加到布局
mMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, "", false);
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(this).getCurrentBackgroundBean().getPixelColor();
mMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
mllBackgroundView.addView(mMemoryCachedBackgroundView);
LogUtils.d(TAG, "initViewAndEvent内存缓存背景视图实例创建并添加完成");
@@ -217,7 +219,8 @@ public class MainUnitTest2Activity extends AppCompatActivity {
LogUtils.d(TAG, String.format("handleCropResult裁剪回调处理 | resultCode=%d", resultCode));
if (resultCode == RESULT_OK) {
if (isFileValid(mPrivateCropImageFile)) {
mMemoryCachedBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(this).getCurrentBackgroundBean().getPixelColor();
mMemoryCachedBackgroundView.loadImage(nCurrentPixelColor, mPrivateCropImageFile.getAbsolutePath(), true);
LogUtils.d(TAG, String.format("handleCropResult裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
@@ -243,7 +246,7 @@ public class MainUnitTest2Activity extends AppCompatActivity {
// 第一重刷新
try {
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
//mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第一重异常 | %s", e.getMessage()));
@@ -257,7 +260,7 @@ public class MainUnitTest2Activity extends AppCompatActivity {
if (mMemoryCachedBackgroundView != null && !isFinishing()) {
try {
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
//mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "doubleRefreshPreview【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, String.format("doubleRefreshPreview【双重刷新】第二重异常 | %s", e.getMessage()));

View File

@@ -207,25 +207,25 @@ public class MainUnitTestActivity extends AppCompatActivity {
* @param resultCode 裁剪结果码
*/
private void handleCropResult(int resultCode) {
LogUtils.d(TAG, String.format("handleCropResult裁剪回调处理 | resultCode=%d", resultCode));
if (resultCode == RESULT_OK) {
if (isFileValid(mPrivateCropImageFile)) {
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
LogUtils.d(TAG, String.format("handleCropResult裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview();
} else {
LogUtils.e(TAG, "handleCropResult裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效");
}
} else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "handleCropResult裁剪取消");
ToastUtils.show("裁剪已取消");
} else {
LogUtils.e(TAG, String.format("handleCropResult裁剪失败 | resultCode异常=%d", resultCode));
ToastUtils.show("裁剪失败");
}
// LogUtils.d(TAG, String.format("handleCropResult裁剪回调处理 | resultCode=%d", resultCode));
// if (resultCode == RESULT_OK) {
// if (isFileValid(mPrivateCropImageFile)) {
// mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
// LogUtils.d(TAG, String.format("handleCropResult裁剪成功 | 加载裁剪图=%s", mPrivateCropImageFile.getAbsolutePath()));
// ToastUtils.show("裁剪成功");
// mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
// doubleRefreshPreview();
// } else {
// LogUtils.e(TAG, "handleCropResult裁剪成功但输出文件无效");
// ToastUtils.show("裁剪失败:输出文件无效");
// }
// } else if (resultCode == RESULT_CANCELED) {
// LogUtils.d(TAG, "handleCropResult裁剪取消");
// ToastUtils.show("裁剪已取消");
// } else {
// LogUtils.e(TAG, String.format("handleCropResult裁剪失败 | resultCode异常=%d", resultCode));
// ToastUtils.show("裁剪失败");
// }
}
/**

View File

@@ -5,6 +5,7 @@ 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.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.threads.RemindThread;
/**
* 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
@@ -38,10 +39,10 @@ public class AppConfigUtils {
*/
public static AppConfigUtils getInstance(Context context) {
String contextType = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, String.format("getInstance调用 | 传入Context类型=%s", contextType));
LogUtils.d(TAG, String.format("getInstance() 调用 | 传入Context类型=%s", contextType));
if (context == null) {
LogUtils.e(TAG, "getInstance失败Context不能为空");
LogUtils.e(TAG, "getInstance() 失败Context不能为空");
throw new IllegalArgumentException("Context cannot be null");
}
@@ -49,12 +50,12 @@ public class AppConfigUtils {
synchronized (AppConfigUtils.class) {
if (sInstance == null) {
sInstance = new AppConfigUtils(context);
LogUtils.d(TAG, "getInstance单例实例创建成功");
LogUtils.d(TAG, "getInstance():单例实例创建成功");
}
}
}
LogUtils.d(TAG, "getInstance单例实例获取成功");
LogUtils.d(TAG, "getInstance():单例实例获取成功");
return sInstance;
}
@@ -63,12 +64,12 @@ public class AppConfigUtils {
* @param context 上下文内部转换为ApplicationContext
*/
private AppConfigUtils(Context context) {
LogUtils.d(TAG, "AppConfigUtils构造方法调用");
LogUtils.d(TAG, "AppConfigUtils() 构造方法调用");
this.mContext = context.getApplicationContext();
this.mApplication = (App) context.getApplicationContext();
mAppConfigBean = new AppConfigBean();
loadAppConfig(); // 加载持久化配置
LogUtils.d(TAG, "AppConfigUtils构造完成配置初始化成功");
LogUtils.d(TAG, "AppConfigUtils() 构造完成,配置初始化成功");
}
// ======================== 核心配置持久化方法区(加载+保存)========================
@@ -77,17 +78,17 @@ public class AppConfigUtils {
* @return 加载后的应用配置Bean
*/
public AppConfigBean loadAppConfig() {
LogUtils.d(TAG, "loadAppConfig调用:开始加载应用配置");
LogUtils.d(TAG, "loadAppConfig() 调用 | 开始加载应用配置");
AppConfigBean savedAppBean = (AppConfigBean) AppConfigBean.loadBean(mContext, AppConfigBean.class);
if (savedAppBean != null) {
mAppConfigBean = savedAppBean;
LogUtils.d(TAG, String.format("loadAppConfig成功 | 充电阈值=%d%% | 耗电阈值=%d%%",
mAppConfigBean.getChargeReminderValue(), mAppConfigBean.getUsageReminderValue()));
LogUtils.d(TAG, String.format("loadAppConfig() 成功 | 充电阈值=%d%% | 耗电阈值=%d%%",
mAppConfigBean.getChargeReminderValue(), mAppConfigBean.getUsageReminderValue()));
} else {
mAppConfigBean = new AppConfigBean();
AppConfigBean.saveBean(mContext, mAppConfigBean);
LogUtils.d(TAG, "loadAppConfig无已保存配置使用默认值并持久化");
LogUtils.d(TAG, "loadAppConfig():无已保存配置,使用默认值并持久化");
}
return mAppConfigBean;
@@ -96,9 +97,9 @@ public class AppConfigUtils {
/**
* 保存应用配置(内部核心方法,直接持久化)
*/
private void saveAppConfig() {
public void saveAppConfig() {
AppConfigBean.saveBean(mContext, mAppConfigBean);
LogUtils.d(TAG, "saveAppConfig应用配置保存成功");
LogUtils.d(TAG, "saveAppConfig():应用配置保存成功");
}
// ======================== 充电提醒配置方法区(开关+阈值)========================
@@ -107,16 +108,16 @@ public class AppConfigUtils {
* @param isEnabled 目标状态true=开启false=关闭)
*/
public void setChargeReminderEnabled(final boolean isEnabled) {
LogUtils.d(TAG, String.format("setChargeReminderEnabled调用 | 传入状态=%b", isEnabled));
LogUtils.d(TAG, String.format("setChargeReminderEnabled() 调用 | 传入状态=%b", isEnabled));
if (isEnabled == mAppConfigBean.isEnableChargeReminder()) {
LogUtils.d(TAG, "setChargeReminderEnabled充电提醒状态无变化无需操作");
LogUtils.d(TAG, "setChargeReminderEnabled():充电提醒状态无变化,无需操作");
return;
}
mAppConfigBean.setEnableChargeReminder(isEnabled);
saveAppConfig();
LogUtils.d(TAG, String.format("setChargeReminderEnabled成功 | 充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
LogUtils.d(TAG, String.format("setChargeReminderEnabled() 成功 | 充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
}
/**
@@ -125,7 +126,7 @@ public class AppConfigUtils {
*/
public boolean isChargeReminderEnabled() {
boolean isEnabled = mAppConfigBean.isEnableChargeReminder();
LogUtils.d(TAG, String.format("isChargeReminderEnabled获取充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
LogUtils.d(TAG, String.format("isChargeReminderEnabled():获取充电提醒状态=%s", isEnabled ? "开启" : "关闭"));
return isEnabled;
}
@@ -134,17 +135,17 @@ public class AppConfigUtils {
* @param value 目标阈值
*/
public void setChargeReminderValue(final int value) {
LogUtils.d(TAG, String.format("setChargeReminderValue调用 | 传入阈值=%d", value));
LogUtils.d(TAG, String.format("setChargeReminderValue() 调用 | 传入阈值=%d", value));
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getChargeReminderValue()) {
LogUtils.d(TAG, "setChargeReminderValue充电提醒阈值无变化无需操作");
LogUtils.d(TAG, "setChargeReminderValue():充电提醒阈值无变化,无需操作");
return;
}
mAppConfigBean.setChargeReminderValue(calibratedValue);
saveAppConfig();
LogUtils.d(TAG, String.format("setChargeReminderValue成功 | 充电提醒阈值=%d%%", calibratedValue));
LogUtils.d(TAG, String.format("setChargeReminderValue() 成功 | 充电提醒阈值=%d%%", calibratedValue));
}
/**
@@ -153,7 +154,7 @@ public class AppConfigUtils {
*/
public int getChargeReminderValue() {
int value = mAppConfigBean.getChargeReminderValue();
LogUtils.d(TAG, String.format("getChargeReminderValue获取充电提醒阈值=%d%%", value));
LogUtils.d(TAG, String.format("getChargeReminderValue():获取充电提醒阈值=%d%%", value));
return value;
}
@@ -163,16 +164,16 @@ public class AppConfigUtils {
* @param isEnabled 目标状态true=开启false=关闭)
*/
public void setUsageReminderEnabled(final boolean isEnabled) {
LogUtils.d(TAG, String.format("setUsageReminderEnabled调用 | 传入状态=%b", isEnabled));
LogUtils.d(TAG, String.format("setUsageReminderEnabled() 调用 | 传入状态=%b", isEnabled));
if (isEnabled == mAppConfigBean.isEnableUsageReminder()) {
LogUtils.d(TAG, "setUsageReminderEnabled耗电提醒状态无变化无需操作");
LogUtils.d(TAG, "setUsageReminderEnabled():耗电提醒状态无变化,无需操作");
return;
}
mAppConfigBean.setEnableUsageReminder(isEnabled);
saveAppConfig();
LogUtils.d(TAG, String.format("setUsageReminderEnabled成功 | 耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
LogUtils.d(TAG, String.format("setUsageReminderEnabled() 成功 | 耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
}
/**
@@ -181,7 +182,7 @@ public class AppConfigUtils {
*/
public boolean isUsageReminderEnabled() {
boolean isEnabled = mAppConfigBean.isEnableUsageReminder();
LogUtils.d(TAG, String.format("isUsageReminderEnabled获取耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
LogUtils.d(TAG, String.format("isUsageReminderEnabled():获取耗电提醒状态=%s", isEnabled ? "开启" : "关闭"));
return isEnabled;
}
@@ -190,17 +191,17 @@ public class AppConfigUtils {
* @param value 目标阈值
*/
public void setUsageReminderValue(final int value) {
LogUtils.d(TAG, String.format("setUsageReminderValue调用 | 传入阈值=%d", value));
LogUtils.d(TAG, String.format("setUsageReminderValue() 调用 | 传入阈值=%d", value));
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getUsageReminderValue()) {
LogUtils.d(TAG, "setUsageReminderValue耗电提醒阈值无变化无需操作");
LogUtils.d(TAG, "setUsageReminderValue():耗电提醒阈值无变化,无需操作");
return;
}
mAppConfigBean.setUsageReminderValue(calibratedValue);
saveAppConfig();
LogUtils.d(TAG, String.format("setUsageReminderValue成功 | 耗电提醒阈值=%d%%", calibratedValue));
LogUtils.d(TAG, String.format("setUsageReminderValue() 成功 | 耗电提醒阈值=%d%%", calibratedValue));
}
/**
@@ -209,7 +210,7 @@ public class AppConfigUtils {
*/
public int getUsageReminderValue() {
int value = mAppConfigBean.getUsageReminderValue();
LogUtils.d(TAG, String.format("getUsageReminderValue获取耗电提醒阈值=%d%%", value));
LogUtils.d(TAG, String.format("getUsageReminderValue():获取耗电提醒阈值=%d%%", value));
return value;
}
@@ -219,15 +220,15 @@ public class AppConfigUtils {
* @param isCharging 充电状态true=充电中false=未充电)
*/
public void setCharging(boolean isCharging) {
LogUtils.d(TAG, String.format("setCharging调用 | 传入状态=%b", isCharging));
LogUtils.d(TAG, String.format("setCharging() 调用 | 传入状态=%b", isCharging));
if (isCharging == mAppConfigBean.isCharging()) {
LogUtils.d(TAG, "setCharging充电状态无变化无需操作");
LogUtils.d(TAG, "setCharging():充电状态无变化,无需操作");
return;
}
mAppConfigBean.setIsCharging(isCharging);
LogUtils.d(TAG, String.format("setCharging成功 | 充电状态=%s", isCharging ? "充电中" : "未充电"));
LogUtils.d(TAG, String.format("setCharging() 成功 | 充电状态=%s", isCharging ? "充电中" : "未充电"));
}
/**
@@ -236,7 +237,7 @@ public class AppConfigUtils {
*/
public boolean isCharging() {
boolean isCharging = mAppConfigBean.isCharging();
LogUtils.d(TAG, String.format("isCharging获取充电状态=%s", isCharging ? "充电中" : "未充电"));
LogUtils.d(TAG, String.format("isCharging():获取充电状态=%s", isCharging ? "充电中" : "未充电"));
return isCharging;
}
@@ -245,16 +246,16 @@ public class AppConfigUtils {
* @param value 当前电量
*/
public void setCurrentBatteryValue(int value) {
LogUtils.d(TAG, String.format("setCurrentBatteryValue调用 | 传入电量=%d", value));
LogUtils.d(TAG, String.format("setCurrentBatteryValue() 调用 | 传入电量=%d", value));
int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
if (calibratedValue == mAppConfigBean.getCurrentBatteryValue()) {
LogUtils.d(TAG, "setCurrentBatteryValue电池电量无变化无需操作");
if (calibratedValue == App.sQuantityOfElectricity) {
LogUtils.d(TAG, "setCurrentBatteryValue():电池电量无变化,无需操作");
return;
}
mAppConfigBean.setCurrentBatteryValue(calibratedValue);
LogUtils.d(TAG, String.format("setCurrentBatteryValue成功 | 电池电量=%d%%", calibratedValue));
App.sQuantityOfElectricity = calibratedValue;
LogUtils.d(TAG, String.format("setCurrentBatteryValue() 成功 | 电池电量=%d%%", calibratedValue));
}
/**
@@ -262,8 +263,8 @@ public class AppConfigUtils {
* @return 当前电池电量0-100
*/
public int getCurrentBatteryValue() {
int value = mAppConfigBean.getCurrentBatteryValue();
LogUtils.d(TAG, String.format("getCurrentBatteryValue获取电池电量=%d%%", value));
int value = App.sQuantityOfElectricity;
LogUtils.d(TAG, String.format("getCurrentBatteryValue():获取电池电量=%d%%", value));
return value;
}
@@ -273,17 +274,17 @@ public class AppConfigUtils {
* @param interval 目标间隔单位ms
*/
public void setReminderIntervalTime(final int interval) {
LogUtils.d(TAG, String.format("setReminderIntervalTime调用 | 传入间隔=%dms", interval));
LogUtils.d(TAG, String.format("setReminderIntervalTime() 调用 | 传入间隔=%dms", interval));
final int calibratedInterval = Math.max(interval, MIN_INTERVAL_TIME);
if (calibratedInterval == mAppConfigBean.getReminderIntervalTime()) {
LogUtils.d(TAG, "setReminderIntervalTime提醒间隔无变化无需操作");
LogUtils.d(TAG, "setReminderIntervalTime():提醒间隔无变化,无需操作");
return;
}
mAppConfigBean.setReminderIntervalTime(calibratedInterval);
saveAppConfig();
LogUtils.d(TAG, String.format("setReminderIntervalTime成功 | 提醒间隔=%dms", calibratedInterval));
LogUtils.d(TAG, String.format("setReminderIntervalTime() 成功 | 提醒间隔=%dms", calibratedInterval));
}
/**
@@ -292,7 +293,7 @@ public class AppConfigUtils {
*/
public int getReminderIntervalTime() {
int interval = mAppConfigBean.getReminderIntervalTime();
LogUtils.d(TAG, String.format("getReminderIntervalTime获取提醒间隔=%dms", interval));
LogUtils.d(TAG, String.format("getReminderIntervalTime():获取提醒间隔=%dms", interval));
return interval;
}
@@ -301,17 +302,17 @@ public class AppConfigUtils {
* @param interval 目标间隔单位ms
*/
public void setBatteryDetectInterval(final int interval) {
LogUtils.d(TAG, String.format("setBatteryDetectInterval调用 | 传入间隔=%dms", interval));
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 调用 | 传入间隔=%dms", interval));
final int calibratedInterval = Math.max(interval, MIN_DETECT_INTERVAL);
if (calibratedInterval == mAppConfigBean.getBatteryDetectInterval()) {
LogUtils.d(TAG, "setBatteryDetectInterval检测间隔无变化无需操作");
LogUtils.d(TAG, "setBatteryDetectInterval():检测间隔无变化,无需操作");
return;
}
mAppConfigBean.setBatteryDetectInterval(calibratedInterval);
saveAppConfig();
LogUtils.d(TAG, String.format("setBatteryDetectInterval成功 | 电量检测间隔=%dms", calibratedInterval));
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 成功 | 电量检测间隔=%dms", calibratedInterval));
}
/**
@@ -320,7 +321,7 @@ public class AppConfigUtils {
*/
public int getBatteryDetectInterval() {
int interval = mAppConfigBean.getBatteryDetectInterval();
LogUtils.d(TAG, String.format("getBatteryDetectInterval获取电量检测间隔=%dms", interval));
LogUtils.d(TAG, String.format("getBatteryDetectInterval():获取电量检测间隔=%dms", interval));
return interval;
}
@@ -330,16 +331,16 @@ public class AppConfigUtils {
* @return 服务开关状态true=开启false=关闭)
*/
public boolean isServiceEnabled() {
LogUtils.d(TAG, "isServiceEnabled调用:开始获取服务开关状态");
LogUtils.d(TAG, "isServiceEnabled() 调用 | 开始获取服务开关状态");
ControlCenterServiceBean savedServiceBean = (ControlCenterServiceBean) ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
if (savedServiceBean != null) {
boolean isEnabled = savedServiceBean.isEnableService();
LogUtils.d(TAG, String.format("isServiceEnabled服务开关状态=%b", isEnabled));
LogUtils.d(TAG, String.format("isServiceEnabled():服务开关状态=%b", isEnabled));
return isEnabled;
} else {
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false));
LogUtils.d(TAG, "isServiceEnabled无已保存服务配置默认关闭并持久化");
LogUtils.d(TAG, "isServiceEnabled():无已保存服务配置,默认关闭并持久化");
return false;
}
}
@@ -349,9 +350,9 @@ public class AppConfigUtils {
* @param isServiceEnabled 目标状态true=开启false=关闭)
*/
public void setIsServiceEnabled(boolean isServiceEnabled) {
LogUtils.d(TAG, String.format("setIsServiceEnabled调用 | 传入状态=%b", isServiceEnabled));
LogUtils.d(TAG, String.format("setIsServiceEnabled() 调用 | 传入状态=%b", isServiceEnabled));
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(isServiceEnabled));
LogUtils.d(TAG, String.format("setIsServiceEnabled成功 | 服务开关状态=%b", isServiceEnabled));
LogUtils.d(TAG, String.format("setIsServiceEnabled() 成功 | 服务开关状态=%b", isServiceEnabled));
}
}

View File

@@ -51,6 +51,7 @@ public class BitmapCacheUtils {
*/
private BitmapCacheUtils() {
LogUtils.d(TAG, "【BitmapCacheUtils】单例构造开始");
//App.notifyMessage(TAG, "【BitmapCacheUtils】单例构造开始");
// 使用 ConcurrentHashMap 保证线程安全,避免手动同步
mHardCacheMap = new ConcurrentHashMap<>();
mRefCountMap = new ConcurrentHashMap<>();

View File

@@ -78,7 +78,7 @@ public class ImageCropUtils {
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
//uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
// 4. 启动裁剪
@@ -129,7 +129,7 @@ public class ImageCropUtils {
// 3. 初始化 uCrop + 强制 PNG 配置
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
//uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
// 4. 启动裁剪
@@ -296,7 +296,8 @@ public class ImageCropUtils {
// 裁剪模式配置(自由裁剪/固定比例)
options.setFreeStyleCropEnabled(isFreeCrop);
options.withAspectRatio(aspectX, aspectY);
// 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
@@ -305,7 +306,8 @@ public class ImageCropUtils {
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
// 通用 UI 配置(保持原有风格)
options.setHideBottomControls(true); // 隐藏底部控制栏
//options.setHideBottomControls(true); // 隐藏底部控制栏
options.setToolbarTitle("图片裁剪");
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));

View File

@@ -4,7 +4,10 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import java.io.File;
@@ -15,44 +18,51 @@ import java.io.OutputStream;
/**
* 图片处理工具类(质量压缩专用)
* 功能:对图片进行JPEG质量压缩并将压缩结果覆盖源文件获取主题colorPrimary颜色值
* 功能:1. 图片JPEG质量压缩覆盖源文件2. 获取主题colorAccent颜色3. 位图纯色背景合成
* 适配Java 7 + Android API 30
* 核心逻辑:Bitmap.compress 质量压缩 + FileChannel 高效文件复制;主题属性解析
* 核心逻辑:
* - 压缩Bitmap.compress + FileChannel 高效文件复制
* - 主题颜色TypedArray 解析主题属性
* - 位图合成Canvas 绘制(支持透明通道)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class ImageUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = ImageUtils.class.getSimpleName();
public static final String TAG = "ImageUtils";
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
private static final int MIN_COMPRESS_QUALITY = 0;
private static final int MAX_COMPRESS_QUALITY = 100;
private static final int[] COLOR_ACCENT_ATTR = new int[]{R.attr.colorAccent}; // colorPrimary属性数组
private static final int[] COLOR_ACCENT_ATTR = new int[]{R.attr.colorAccent}; // colorAccent属性数组
private static final int DEFAULT_COLOR = Color.parseColor("#FFFFFFFF"); // 默认返回颜色
// ================================== 新增Accent 颜色获取方法 =================================
// ================================== 主题颜色获取方法 =================================
/**
* 从当前应用主题中获取 colorAccent 颜色值
* @param context 上下文,用于获取主题与资源
* @return 解析到的 colorAccent 颜色值,解析失败返回默认白色#FFFFFFFF
*/
public static int getColorAccent(Context context) {
// 方法调用日志
LogUtils.d(TAG, "【getColorAccent】方法调用");
// 参数校验
if (context == null) {
LogUtils.e(TAG, "【getColorAccent】参数异常Context为空,返回默认颜色");
return Color.parseColor("#FFFFFFFF");
LogUtils.e(TAG, "【getColorAccent】参数异常Context为空");
return DEFAULT_COLOR;
}
TypedArray typedArray = null;
try {
// 从当前主题解析 colorAccent 属性
// 从主题解析属性
typedArray = context.obtainStyledAttributes(COLOR_ACCENT_ATTR);
int colorAccent = typedArray.getColor(0, Color.parseColor("#FFFFFFFF"));
int colorAccent = typedArray.getColor(0, DEFAULT_COLOR);
LogUtils.d(TAG, String.format("【getColorAccent】解析成功 | colorAccent=0x%08X", colorAccent));
return colorAccent;
} catch (Exception e) {
LogUtils.e(TAG, "【getColorAccent】解析失败返回默认颜色", e);
return Color.parseColor("#FFFFFFFF");
return DEFAULT_COLOR;
} finally {
// 回收TypedArray资源避免内存泄漏
// 回收资源
if (typedArray != null) {
typedArray.recycle();
LogUtils.d(TAG, "【getColorAccent】TypedArray资源已回收");
@@ -60,17 +70,107 @@ public class ImageUtils {
}
}
// ================================== 核心工具方法(图片质量压缩)=================================
// ================================== 位图合成方法 =================================
/**
* 在纯色背景上绘制前景位图实现FIT_CENTER居中效果
* @param bgColor 背景颜色
* @param originalFrameW 目标画布宽度
* @param originalFrameH 目标画布高度
* @param fgBitmap 前景位图
* @return 合成后的位图失败返回null
*/
public static Bitmap drawBitmapOnSolidBackground(final int bgColor, int originalFrameW, int originalFrameH, Bitmap fgBitmap) {
// 方法调用及入参日志
LogUtils.d(TAG, String.format("【drawBitmapOnSolidBackground】方法调用 | 背景色=0x%08X | 目标尺寸=%dx%d | 前景位图=%s",
bgColor, originalFrameW, originalFrameH,
(fgBitmap != null ? fgBitmap.getWidth() + "x" + fgBitmap.getHeight() : "null")));
// 1. 严格参数校验
if (fgBitmap == null || fgBitmap.isRecycled()) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】参数异常前景Bitmap为空或已回收");
return null;
}
if (fgBitmap.getWidth() <= 0 || fgBitmap.getHeight() <= 0) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】参数异常前景Bitmap尺寸无效");
return null;
}
if (originalFrameW <= 0 || originalFrameH <= 0) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】参数异常原画框尺寸无效宽高必须大于0");
return null;
}
// 2. 强制画布尺寸为目标尺寸
int canvasW = originalFrameW;
int canvasH = originalFrameH;
LogUtils.d(TAG, String.format("【drawBitmapOnSolidBackground】画布尺寸已确定%dx%d", canvasW, canvasH));
// 3. 创建结果位图ARGB_8888支持透明通道
Bitmap resultBitmap = Bitmap.createBitmap(canvasW, canvasH, Bitmap.Config.ARGB_8888);
if (resultBitmap == null) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】创建结果Bitmap失败");
return null;
}
// 4. 画布绘制
Canvas canvas = new Canvas(resultBitmap);
try {
// 4.1 绘制纯色背景
Paint bgPaint = new Paint();
bgPaint.setColor(bgColor);
bgPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, canvasW, canvasH, bgPaint);
// 4.2 计算前景 FIT_CENTER 变换参数(等比缩放至完全放入画布,居中显示)
float fgWidth = fgBitmap.getWidth();
float fgHeight = fgBitmap.getHeight();
float scaleX = (float) canvasW / fgWidth;
float scaleY = (float) canvasH / fgHeight;
float scale = Math.min(scaleX, scaleY); // 取最小比例,保证完全放入画布
// 4.3 计算缩放后前景尺寸
float scaledW = fgWidth * scale;
float scaledH = fgHeight * scale;
// 4.4 计算居中位置(缩放后居中,无裁剪)
float translateX = (canvasW - scaledW) / 2f;
float translateY = (canvasH - scaledH) / 2f;
// 4.5 构建变换矩阵(缩放+平移,实现 FIT_CENTER 效果)
Matrix matrix = new Matrix();
matrix.postScale(scale, scale); // 等比缩放
matrix.postTranslate(translateX, translateY); // 居中平移
// 4.6 绘制前景(保留透明通道,抗锯齿)
Paint fgPaint = new Paint();
fgPaint.setAntiAlias(true);
fgPaint.setDither(true);
canvas.drawBitmap(fgBitmap, matrix, fgPaint);
LogUtils.d(TAG, String.format("【drawBitmapOnSolidBackground】合成成功 | 缩放比例=%.2f | 居中位置=(%.1f,%.1f) | 效果=FIT_CENTER",
scale, translateX, translateY));
return resultBitmap;
} catch (Exception e) {
LogUtils.e(TAG, "【drawBitmapOnSolidBackground】合成失败", e);
if (resultBitmap != null && !resultBitmap.isRecycled()) {
resultBitmap.recycle();
}
return null;
}
}
// ================================== 核心压缩方法 =================================
/**
* 图片质量压缩JPEG格式压缩后覆盖源文件
* @param context 上下文(备用,当前逻辑未直接使用
* @param context 上下文(备用)
* @param srcImagePath 源图片文件路径(非空,文件需存在)
* @param dstImagePath 压缩后临时保存路径(非空,用于存储压缩中间文件
* @param dstImagePath 压缩后临时保存路径(非空)
* @param compressQuality 压缩质量0-100数值越小压缩率越高
*/
public static void bitmapCompress(Context context, String srcImagePath, String dstImagePath, int compressQuality) {
LogUtils.d(TAG, String.format("【bitmapCompress】调用开始 | 源路径=%s | 临时路径=%s | 压缩质量=%d",
// 方法调用及入参日志
LogUtils.d(TAG, String.format("【bitmapCompress】方法调用 | 源路径=%s | 临时路径=%s | 压缩质量=%d",
srcImagePath, dstImagePath, compressQuality));
// 1. 前置参数校验
if (srcImagePath == null || srcImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常源文件路径为空");
@@ -103,7 +203,7 @@ public class ImageUtils {
LogUtils.d(TAG, String.format("【bitmapCompress】Bitmap解码成功 | 尺寸=%dx%d",
compressBitmap.getWidth(), compressBitmap.getHeight()));
// 3. 创建临时压缩文件
// 3. 创建临时压缩文件目录
File dstFile = new File(dstImagePath);
File dstParentDir = dstFile.getParentFile();
if (dstParentDir != null && !dstParentDir.exists()) {
@@ -131,7 +231,7 @@ public class ImageUtils {
} catch (IOException e) {
LogUtils.e(TAG, "【bitmapCompress】IO异常", e);
} finally {
// 6. 资源释放:关闭输出流
// 6. 关闭输出流
if (outputStream != null) {
try {
outputStream.close();
@@ -139,7 +239,7 @@ public class ImageUtils {
LogUtils.e(TAG, "【bitmapCompress】输出流关闭失败", e);
}
}
// 7. 资源释放:回收Bitmap
// 7. 回收Bitmap
if (compressBitmap != null && !compressBitmap.isRecycled()) {
compressBitmap.recycle();
LogUtils.d(TAG, "【bitmapCompress】Bitmap资源已回收");

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BatteryStyle;
/**
* 电池电量Drawable适配API30兼容小米机型支持能量/条纹两种绘制风格切换
@@ -31,7 +32,7 @@ public class BatteryDrawable extends Drawable {
private final Paint mBatteryPaint;
// 业务控制变量
private int mBatteryValue = -1; // 当前电量0-100-1=未初始化)
private boolean mIsEnergyStyle = true; // 绘制风格true=能量false=条纹)
private BatteryStyle mBatteryStyle = BatteryStyle.ENERGY_STYLE; // 绘制风格true=能量false=条纹)
// ====================================== 构造方法(重载适配,优先暴露常用构造) ======================================
/**
@@ -49,13 +50,16 @@ public class BatteryDrawable extends Drawable {
* @param batteryColor 电量显示颜色
* @param isEnergyStyle 是否启用能量风格
*/
public BatteryDrawable(int batteryColor, boolean isEnergyStyle) {
LogUtils.d(TAG, "【BatteryDrawable】构造器2调用 | 颜色=" + Integer.toHexString(batteryColor) + " | 风格=" + (isEnergyStyle ? "能量" : "条纹"));
public BatteryDrawable(int batteryColor, BatteryStyle batteryStyle) {
mBatteryPaint = new Paint();
mIsEnergyStyle = isEnergyStyle;
mBatteryStyle = batteryStyle;
initPaintConfig(batteryColor);
}
public void setIsEnergyStyle(BatteryStyle batteryStyle) {
this.mBatteryStyle = batteryStyle;
}
// ====================================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================================
/**
* 初始化画笔配置适配API30渲染特性优化小米机型兼容性
@@ -74,8 +78,7 @@ public class BatteryDrawable extends Drawable {
// ====================================== 核心绘制方法Drawable抽象方法优先级最高 ======================================
@Override
public void draw(Canvas canvas) {
LogUtils.d(TAG, "【draw】绘制开始 | 当前电量=" + mBatteryValue + " | 风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
// 未初始化/异常电量,直接跳过,避免无效绘制
// 未初始化/异常电量,直接跳过,避免无效绘制
if (mBatteryValue < 0) {
LogUtils.w(TAG, "【draw】电量未初始化跳过绘制");
return;
@@ -99,10 +102,12 @@ public class BatteryDrawable extends Drawable {
LogUtils.d(TAG, "【draw】绘制参数校准 | 左边界=" + left + " | 右边界=" + right + " | 高度=" + drawHeight);
// 按风格执行绘制
if (mIsEnergyStyle) {
if (mBatteryStyle == BatteryStyle.ENERGY_STYLE) {
drawEnergyStyle(canvas, validBattery, left, right, drawHeight);
} else {
drawStripeStyle(canvas, validBattery, left, right, drawHeight);
} else if (mBatteryStyle == BatteryStyle.ZEBRA_STYLE) {
drawZebraStyle(canvas, validBattery, left, right, drawHeight);
} else if (mBatteryStyle == BatteryStyle.POINT_STYLE) {
drawPointStyle(canvas, validBattery, left, right, drawHeight);
}
LogUtils.d(TAG, "【draw】绘制完成");
}
@@ -118,9 +123,25 @@ public class BatteryDrawable extends Drawable {
*/
private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制开始 | 电量=" + battery);
int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
// int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
// canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
// LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 绘制耗电电量提醒值电量
// 能量绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
//for (int i = 0; i < mnValue; i ++) {
nBottom = nHeight;
nTop = nHeight - (nHeight * mBatteryValue / 100);
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
}
/**
@@ -131,15 +152,75 @@ public class BatteryDrawable extends Drawable {
* @param right 右边界
* @param height 绘制高度
*/
private void drawStripeStyle(Canvas canvas, int battery, int left, int right, int height) {
private void drawZebraStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分)
// 从底部向上绘制对应电量条纹
for (int i = 0; i < battery; i++) {
int bottom = height - (stripeHeight * i);
int top = bottom - stripeHeight;
canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
}
// int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分)
// // 从底部向上绘制对应电量条纹
// for (int i = 0; i < battery; i++) {
// int bottom = height - (stripeHeight * i);
// int top = bottom - stripeHeight;
// canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
// }
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 意兴阑珊绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
for (int i = 0; i < mBatteryValue; i ++) {
nBottom = (nHeight * (100 - i) / 100) - mnDx;
nTop = nBottom + mnDx;
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
}
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
}
/**
* 点阵风格绘制
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/
private void drawPointStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 意兴阑珊绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
int nLineWidth = nRight - nLeft;
int radius_horizontal = (nLineWidth / 10) / 2;
int radius_vertical = mnDx/2;
int radius = Math.min(radius_horizontal, radius_vertical);
for (int i = 0; i < mBatteryValue; i ++) {
nBottom = (nHeight * (100 - i) / 100) - mnDx;
nTop = nBottom + mnDx;
//canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
for (int j = 0; j < 10; j++) {
// cx, cy 圆心坐标radius 半径paint 画笔
int cx = radius_horizontal + radius_horizontal * j * 2;
int cy = nTop + radius_vertical;
canvas.drawCircle(cx, cy, radius, mBatteryPaint);
}
}
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
}
@@ -159,9 +240,8 @@ public class BatteryDrawable extends Drawable {
* 切换绘制风格
* @param isEnergyStyle true=能量风格false=条纹风格
*/
public void switchDrawStyle(boolean isEnergyStyle) {
LogUtils.d(TAG, "【switchDrawStyle】风格切换 | 旧风格=" + (mIsEnergyStyle ? "能量" : "条纹") + " | 新风格=" + (isEnergyStyle ? "能量" : "条纹"));
mIsEnergyStyle = isEnergyStyle;
public void setDrawStyle(BatteryStyle batteryStyle) {
mBatteryStyle = batteryStyle;
invalidateSelf();
LogUtils.d(TAG, "【switchDrawStyle】已触发重绘");
}
@@ -188,12 +268,10 @@ public class BatteryDrawable extends Drawable {
return mBatteryValue;
}
/**
* 获取当前绘制风格
* @return true=能量风格false=条纹风格
*/
public boolean isEnergyStyle() {
return mIsEnergyStyle;
public BatteryStyle getEnergyStyle() {
return mBatteryStyle;
}
// ====================================== Drawable抽象方法必须实现精简逻辑 ======================================

View File

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

View File

@@ -15,8 +15,11 @@ import android.widget.RelativeLayout;
import android.widget.Switch;
import android.widget.TextView;
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.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.utils.AppConfigUtils;
@@ -25,6 +28,7 @@ import cc.winboll.studio.powerbell.utils.AppConfigUtils;
* 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* 适配Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
* 修复updateBatteryDrawable() 电池样式切换后重绘失效问题
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 13:14
*/
@@ -41,7 +45,7 @@ public class MainContentView {
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// ====================================== 内部静态类(临时数据载体,避免外部依赖 ======================================
// ====================================== 内部缓存类(解耦,避免冗余 ======================================
/**
* 临时配置数据实体(缓存变更信息,取消时恢复)
*/
@@ -87,6 +91,8 @@ public class MainContentView {
public RelativeLayout mainLayout;
public MemoryCachedBackgroundView backgroundView;
private LinearLayout mllBackgroundView;
private volatile BatteryStyle mBatteryStyle = BatteryStyle.ENERGY_STYLE;
// 容器布局控件
public LinearLayout llLeftSeekBar;
public LinearLayout llRightSeekBar;
@@ -131,6 +137,7 @@ public class MainContentView {
this.mContext = context;
this.mActionListener = actionListener;
this.mAppConfigUtils = AppConfigUtils.getInstance(context.getApplicationContext());
mBatteryStyle = BatteryStyleView.getSavedBatteryStyle(context);
// 执行核心初始化流程(按顺序执行,避免依赖空指针)
bindViews(rootView);
@@ -151,13 +158,20 @@ public class MainContentView {
// 基础布局绑定
mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1);
mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview);
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
backgroundView = App.getInstance().getMemoryCachedBackgroundView();
if (backgroundView == null) {
App.sBackgroundSourceUtils.loadSettings();
BackgroundBean backgroundBean = App.sBackgroundSourceUtils.getCurrentBackgroundBean();
backgroundView = App.getInstance().getMemoryCachedBackgroundView().getInstance(mContext, backgroundBean, true);
}
if (backgroundView.getParent() != null) {
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
LogUtils.d(TAG, "【bindViews】移除背景视图旧父容器");
}
mllBackgroundView.addView(backgroundView);
// 容器布局绑定
// 容器布局绑定
llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1);
llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2);
// 开关控件绑定
@@ -188,23 +202,72 @@ public class MainContentView {
LogUtils.d(TAG, "【bindViews】视图绑定完成");
}
public void reloadBackgroundView() {
if (backgroundView != null) {
App.sBackgroundSourceUtils.loadSettings();
BackgroundBean backgroundBean = App.sBackgroundSourceUtils.getCurrentBackgroundBean();
backgroundView.loadByBackgroundBean(backgroundBean, true);
}
}
/**
* 初始化电池 Drawable集成 BatteryDrawable默认能量风格适配小米机型渲染
*/
private void initBatteryDrawables() {
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始");
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始 | style="+mBatteryStyle.name());
// 当前电量 Drawable颜色从资源读取适配 API30 主题)
int colorCurrent = getResourceColor(R.color.colorCurrent);
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
mCurrentBatteryDrawable.setDrawStyle(mBatteryStyle);
// 充电提醒 Drawable
int colorCharge = getResourceColor(R.color.colorCharge);
mChargeReminderBatteryDrawable = new BatteryDrawable(colorCharge);
// 耗电提醒 Drawable
mChargeReminderBatteryDrawable.setDrawStyle(mBatteryStyle);
// 耗电提醒 Drawable
int colorUsage = getResourceColor(R.color.colorUsege);
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
mUsageReminderBatteryDrawable.setDrawStyle(mBatteryStyle);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
}
/**
* ✅ 核心修复:电池样式切换+强制重绘刷新 完整版
* 修复点1重新创建Drawable后重新给ImageView赋值Drawable
* 修复点2重置所有Drawable的电量值保证样式切换后数值不变
* 修复点3调用ImageView.invalidate()强制触发重绘API30必加
* 修复点4Drawable.invalidateSelf() 双保险刷新绘制内容
* @param batteryStyle 切换后的电池样式
*/
public void updateBatteryDrawable(BatteryStyle batteryStyle) {
if(batteryStyle == null || batteryStyle == mBatteryStyle){
LogUtils.d(TAG, "【updateBatteryDrawable】样式无变化跳过刷新");
return;
}
// 1. 更新样式标记
mBatteryStyle = batteryStyle;
// 2. 重新初始化Drawable并设置新样式
initBatteryDrawables();
// 3. 重置所有Drawable的电量值 → 保证样式切换后数值不变
mCurrentBatteryDrawable.setBatteryValue(mAppConfigUtils.getCurrentBatteryValue());
mChargeReminderBatteryDrawable.setBatteryValue(mCurrentChargeProgress);
mUsageReminderBatteryDrawable.setBatteryValue(mCurrentUsageProgress);
// 4. 重新给ImageView赋值Drawable → 核心修复:之前缺失这一步
if(ivCurrentBattery != null) ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
if(ivChargeReminderBattery != null) ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
if(ivUsageReminderBattery != null) ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
// 5. Drawable自身刷新 → 双保险
mCurrentBatteryDrawable.invalidateSelf();
mChargeReminderBatteryDrawable.invalidateSelf();
mUsageReminderBatteryDrawable.invalidateSelf();
// 6. ✅ API30关键修复ImageView强制重绘解决绘制缓存不刷新问题
if(ivCurrentBattery != null) ivCurrentBattery.invalidate();
if(ivChargeReminderBattery != null) ivChargeReminderBattery.invalidate();
if(ivUsageReminderBattery != null) ivUsageReminderBattery.invalidate();
LogUtils.d(TAG, "【updateBatteryDrawable】样式切换完成"+mBatteryStyle.name() + " | 重绘触发成功");
ToastUtils.show("电池样式已切换为:"+mBatteryStyle.name());
}
/**
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
*/

View File

@@ -6,14 +6,16 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/**
* 单实例缓存版背景视图控件基于Java7- 强制缓存版
* 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片
* 新增SP持久化最后加载路径、获取最后加载实例功能
* 强制缓存策略:无论内存是否紧张,不自动清理任何缓存实例和路径记录
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/21 20:43
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class MemoryCachedBackgroundView extends BackgroundView {
// ====================================== 静态常量区TAG + SP相关常量 ======================================
@@ -33,19 +35,19 @@ public class MemoryCachedBackgroundView extends BackgroundView {
private MemoryCachedBackgroundView(Context context) {
super(context);
sInstanceCount++;
LogUtils.d(TAG, "构造器1】创建MemoryCachedBackgroundView实例,当前实例总数" + sInstanceCount);
LogUtils.d(TAG, String.format("构造器1启动 | 创建实例,当前实例总数=%d", sInstanceCount));
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
sInstanceCount++;
LogUtils.d(TAG, "构造器2】创建MemoryCachedBackgroundView实例,当前实例总数" + sInstanceCount);
LogUtils.d(TAG, String.format("构造器2启动 | 创建实例,当前实例总数=%d", sInstanceCount));
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
sInstanceCount++;
LogUtils.d(TAG, "构造器3】创建MemoryCachedBackgroundView实例,当前实例总数" + sInstanceCount);
LogUtils.d(TAG, String.format("构造器3启动 | 创建实例,当前实例总数=%d", sInstanceCount));
}
// ====================================== 核心静态方法:获取/创建缓存实例(强制缓存版) ======================================
@@ -56,36 +58,27 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @param isReload 是否强制重新加载图片(路径匹配时仍刷新)
* @return 缓存/新创建的MemoryCachedBackgroundView实例
*/
public static MemoryCachedBackgroundView getInstance(Context context, String imagePath, boolean isReload) {
LogUtils.d(TAG, "getInstance调用 | 图片路径:" + imagePath + " | 是否重载" + isReload);
// 空路径校验
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【getInstance】图片路径为空创建空实例");
return new MemoryCachedBackgroundView(context);
}
// 1. 路径匹配缓存 → 判断是否强制重载
if (imagePath.equals(sCachedImagePath) && sCachedView != null) {
LogUtils.d(TAG, "【getInstance】路径已缓存当前缓存实例有效");
if (isReload) {
LogUtils.d(TAG, "【getInstance】强制重载图片 | 路径:" + imagePath);
sCachedView.loadImage(imagePath);
} else {
LogUtils.d(TAG, "【getInstance】使用缓存实例无需重载 | 路径:" + imagePath);
}
return sCachedView;
}
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心:保留旧实例,仅更新引用)
LogUtils.d(TAG, "【getInstance】路径未缓存新建实例保留旧实例 | 路径:" + imagePath);
String oldPath = sCachedImagePath;
public static MemoryCachedBackgroundView getInstance(Context context, BackgroundBean bean, boolean isReload) {
LogUtils.d(TAG, String.format("getInstance 调用 | BackgroundBean=%s | 是否重载=%b", bean.toString(), isReload));
//App.notifyMessage(TAG, String.format("getInstance 调用 | BackgroundBean=%s | 是否重载=%b", bean.toString(), isReload));
sCachedView = new MemoryCachedBackgroundView(context);
sCachedImagePath = imagePath;
sCachedView.loadImage(imagePath);
LogUtils.d(TAG, "getInstance已更新当前缓存实例,旧实例路径" + oldPath + "(强制保持)");
sCachedView.loadByBackgroundBean(bean, isReload);
saveLastLoadImagePath(context, getBackgroundBeanImagePath(bean));
LogUtils.d(TAG, String.format("getInstance: 已更新当前缓存实例,旧实例路径=%s强制保持", getBackgroundBeanImagePath(bean)));
//App.notifyMessage(TAG, String.format("getInstance: 已更新当前缓存实例,旧实例路径=%s强制保持", getBackgroundBeanImagePath(bean)));
return sCachedView;
}
static String getBackgroundBeanImagePath(BackgroundBean bean) {
if (bean.isUseBackgroundFile()) {
if (bean.isUseBackgroundScaledCompressFile()) {
return bean.getBackgroundScaledCompressFilePath();
}
return bean.getBackgroundFilePath();
}
return "";
}
// ====================================== 新增功能:获取最后加载的实例(强制缓存版) ======================================
/**
* 获取最后一次loadImage的路径对应的实例强制保持所有实例
@@ -94,28 +87,20 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 最后加载路径对应的实例
*/
public static MemoryCachedBackgroundView getLastInstance(Context context) {
LogUtils.d(TAG, "getLastInstance调用");
LogUtils.d(TAG, "getLastInstance 调用");
//App.notifyMessage(TAG, "getLastInstance 调用");
// 1. 从SP获取最后加载的路径强制保持不自动删除
String lastPath = getLastLoadImagePath(context);
if (TextUtils.isEmpty(lastPath)) {
LogUtils.e(TAG, "【getLastInstance】无最后加载路径创建空实例");
return new MemoryCachedBackgroundView(context);
}
// 2. 路径匹配当前缓存 → 直接返回
sCachedImagePath = getLastLoadImagePath(context);
String lastPath = getBackgroundBeanImagePath(App.sBackgroundSourceUtils.getCurrentBackgroundBean());
//App.notifyMessage(TAG, String.format("sCachedImagePath : %s", sCachedImagePath));
//App.notifyMessage(TAG, String.format("lastPath : %s", lastPath));
if (lastPath.equals(sCachedImagePath) && sCachedView != null) {
LogUtils.d(TAG, "getLastInstance使用最后路径缓存实例 | 路径" + lastPath);
LogUtils.d(TAG, String.format("getLastInstance: 使用最后路径缓存实例 | 路径=%s", lastPath));
//App.notifyMessage(TAG, String.format("getLastInstance: 使用最后路径缓存实例 | 路径=%s", lastPath));
return sCachedView;
}
// 3. 路径不匹配 → 新建实例并更新缓存(保留旧实例)
LogUtils.d(TAG, "【getLastInstance】最后路径未缓存新建实例并加载保留旧实例 | 路径:" + lastPath);
String oldPath = sCachedImagePath;
sCachedView = new MemoryCachedBackgroundView(context);
sCachedImagePath = lastPath;
sCachedView.loadImage(lastPath);
LogUtils.d(TAG, "【getLastInstance】已更新最后路径实例旧实例路径" + oldPath + "(强制保持)");
return sCachedView;
//App.notifyMessage(TAG, "getLastInstance 返回 null");
return null;
}
// ====================================== 工具方法SP持久化最后加载路径强制保持版 ======================================
@@ -126,12 +111,12 @@ public class MemoryCachedBackgroundView extends BackgroundView {
*/
private static void saveLastLoadImagePath(Context context, String imagePath) {
if (TextUtils.isEmpty(imagePath) || context == null) {
LogUtils.w(TAG, "saveLastLoadImagePath路径或上下文为空,跳过保存");
LogUtils.w(TAG, "saveLastLoadImagePath: 路径或上下文为空,跳过保存");
return;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_LAST_LOAD_IMAGE_PATH, imagePath).apply();
LogUtils.d(TAG, "saveLastLoadImagePath已保存最后路径(强制保持) | 路径" + imagePath);
LogUtils.d(TAG, String.format("saveLastLoadImagePath: 已保存最后路径(强制保持) | 路径=%s", imagePath));
}
/**
@@ -141,12 +126,12 @@ public class MemoryCachedBackgroundView extends BackgroundView {
*/
public static String getLastLoadImagePath(Context context) {
if (context == null) {
LogUtils.e(TAG, "getLastLoadImagePath上下文为空返回null");
LogUtils.e(TAG, "getLastLoadImagePath: 上下文为空返回null");
return null;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String lastPath = sp.getString(KEY_LAST_LOAD_IMAGE_PATH, null);
LogUtils.d(TAG, "getLastLoadImagePath获取最后路径(强制保持) | 路径" + lastPath);
LogUtils.d(TAG, String.format("getLastLoadImagePath: 获取最后路径(强制保持) | 路径=%s", lastPath));
return lastPath;
}
@@ -155,8 +140,8 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* 清除当前缓存实例和路径(强制缓存策略:仅日志,不实际清理)
*/
public static void clearCache() {
LogUtils.w(TAG, "clearCache调用(强制缓存策略:不实际清理缓存) | 当前缓存路径" + sCachedImagePath);
LogUtils.d(TAG, "clearCache强制缓存策略生效,未清除任何实例和路径");
LogUtils.w(TAG, String.format("clearCache 调用(强制缓存策略:不实际清理缓存) | 当前缓存路径=%s", sCachedImagePath));
LogUtils.d(TAG, "clearCache: 强制缓存策略生效,未清除任何实例和路径");
}
/**
@@ -164,20 +149,20 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @param imagePath 图片路径
*/
public static void removeCache(String imagePath) {
LogUtils.w(TAG, "removeCache调用(强制缓存策略:不实际清理缓存) | 图片路径" + imagePath);
LogUtils.w(TAG, String.format("removeCache 调用(强制缓存策略:不实际清理缓存) | 图片路径=%s", imagePath));
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "removeCache图片路径为空,清除失败");
LogUtils.e(TAG, "removeCache: 图片路径为空,清除失败");
return;
}
LogUtils.d(TAG, "removeCache强制缓存策略生效,未清除任何实例和路径");
LogUtils.d(TAG, "removeCache: 强制缓存策略生效,未清除任何实例和路径");
}
/**
* 清除所有缓存(强制缓存策略:仅日志,不实际清理)
*/
public static void clearAllCache() {
LogUtils.w(TAG, "clearAllCache调用(强制缓存策略:不实际清理缓存)");
LogUtils.d(TAG, "clearAllCache强制缓存策略生效未清除任何实例、路径和SP记录");
LogUtils.w(TAG, "clearAllCache 调用(强制缓存策略:不实际清理缓存)");
LogUtils.d(TAG, "clearAllCache: 强制缓存策略生效未清除任何实例、路径和SP记录");
}
/**
@@ -186,7 +171,7 @@ public class MemoryCachedBackgroundView extends BackgroundView {
*/
public static boolean hasCache() {
boolean hasCache = sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
LogUtils.d(TAG, "hasCache缓存存在状态" + hasCache);
LogUtils.d(TAG, String.format("hasCache 调用 | 缓存存在状态=%b", hasCache));
return hasCache;
}
@@ -195,8 +180,8 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @param context 上下文
*/
public static void clearLastLoadImagePath(Context context) {
LogUtils.w(TAG, "clearLastLoadImagePath调用强制缓存策略不实际清理SP记录");
LogUtils.d(TAG, "clearLastLoadImagePath强制缓存策略生效未清除SP中最后路径记录");
LogUtils.w(TAG, "clearLastLoadImagePath 调用强制缓存策略不实际清理SP记录");
LogUtils.d(TAG, "clearLastLoadImagePath: 强制缓存策略生效未清除SP中最后路径记录");
}
// ====================================== 辅助方法:从缓存获取上下文 ======================================
@@ -204,30 +189,22 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* 从缓存实例中获取上下文用于无外部上下文时的SP操作
* @return 上下文实例无则返回null
*/
private static Context getContextFromCache() {
Context context = sCachedView != null ? sCachedView.getContext() : null;
LogUtils.d(TAG, "getContextFromCache从缓存获取上下文" + context);
return context;
}
// private static Context getContextFromCache() {
// Context context = sCachedView != null ? sCachedView.getContext() : null;
// LogUtils.d(TAG, String.format("getContextFromCache 调用 | 从缓存获取上下文=%s", context));
// return context;
// }
// ====================================== 重写父类方法:增强日志+SP持久化强制保持版 ======================================
@Override
public void loadImage(String imagePath) {
LogUtils.d(TAG, "【loadImage】调用 | 图片路径:" + imagePath);
super.loadImage(imagePath);
// 保存最后加载路径到SP强制保持不自动删除
saveLastLoadImagePath(getContext(), imagePath);
}
@Override
public void loadByBackgroundBean(BackgroundBean bean) {
LogUtils.d(TAG, "loadByBackgroundBean调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()));
LogUtils.d(TAG, String.format("loadByBackgroundBean 调用 | BackgroundBean=%s", (bean == null ? "null" : bean.toString())));
super.loadByBackgroundBean(bean);
}
@Override
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
LogUtils.d(TAG, "loadByBackgroundBean调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
LogUtils.d(TAG, String.format("loadByBackgroundBean 调用 | BackgroundBean=%s | 是否刷新=%b", (bean == null ? "null" : bean.toString()), isRefresh));
super.loadByBackgroundBean(bean, isRefresh);
}
@@ -237,7 +214,7 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 实例总数
*/
public static int getInstanceCount() {
LogUtils.d(TAG, "getInstanceCount调用 | 当前实例总数" + sInstanceCount);
LogUtils.d(TAG, String.format("getInstanceCount 调用 | 当前实例总数=%d", sInstanceCount));
return sInstanceCount;
}
}

View File

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

View File

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

View File

@@ -113,8 +113,9 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
@@ -127,7 +128,7 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView2"
android:layout_weight="1.0"/>
@@ -135,7 +136,7 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
@@ -150,7 +151,7 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView1"
android:layout_weight="1.0"/>
@@ -158,8 +159,9 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
@@ -172,7 +174,7 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView3"
android:layout_weight="1.0"/>

View File

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

View File

@@ -13,25 +13,130 @@
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<LinearLayout
android:orientation="horizontal"
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
android:layout_height="0dp"
android:layout_weight="1.0">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CheckPermission"
android:padding="10dp"
android:onClick="onCheckPermission"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame"
android:padding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TTS语音服务设置"
android:paddingLeft="10dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="&lt;仅限在切换充电状态时发送的TTS语音提醒。用于提醒用户当前的服务设置状态。&gt;"
android:textSize="12sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用电提醒启用时的TTS贴心服务"
android:onClick="onEnableUsePowerTts"
android:id="@+id/activitysettingsCheckBox1"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="充电提醒启用时的TTS贴心服务"
android:onClick="onEnableChargeTts"
android:id="@+id/activitysettingsCheckBox2"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="检查TTS语音悬浮窗权限"
android:padding="10dp"
android:onClick="onCheckTTSDrawOverlaysPermission"
android:layout_marginRight="10dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:orientation="vertical"
android:background="@drawable/bg_frame"
android:padding="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="电量图表绘制风格设置:"
android:paddingLeft="10dp"/>
</LinearLayout>
<cc.winboll.studio.powerbell.views.BatteryStyleView
android:id="@+id/battery_style_view"
android:layout_width="match_parent"
android:layout_height="1200dp"
app:batteryPreviewColor="@color/colorPrimary"
app:previewBatteryValue="100"
app:defaultSelectedStyle="zebra_style"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,11 +19,11 @@ def genVersionName(def versionName){
android {
// 1. compileSdkVersion必须 ≥ targetSdkVersion建议直接等于 targetSdkVersion30
compileSdkVersion 30
// 关键:改为你已安装的 SDK 32≥ targetSdkVersion 30兼容已安装环境
compileSdkVersion 32
// 2. buildToolsVersion需匹配 compileSdkVersion建议使用 30.x.x 最新稳定版(无需高于 compileSdkVersion
buildToolsVersion "30.0.3" // 这是 30 对应的最新稳定版,避免使用 beta 版
// 直接使用已安装的构建工具 33.0.3(无需修改
buildToolsVersion "33.0.3"
defaultConfig {
applicationId "cc.winboll.studio.winboll"
@@ -43,6 +43,12 @@ android {
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
}
}
}
dependencies {
@@ -63,6 +69,10 @@ dependencies {
api 'io.github.medyo:android-about-page:2.0.0'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// OkHttp网络请求
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
// AndroidX 类库
api 'androidx.appcompat:appcompat:1.1.0'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Dec 07 03:42:32 HKT 2025
stageCount=8
#Tue Jan 06 06:07:46 GMT 2026
stageCount=9
libraryProject=
baseVersion=15.11
publishVersion=15.11.7
buildCount=0
baseBetaVersion=15.11.8
publishVersion=15.11.8
buildCount=10
baseBetaVersion=15.11.9

Binary file not shown.

View File

@@ -278,6 +278,10 @@
<activity android:name="cc.winboll.studio.winboll.activities.SettingsActivity"/>
<activity android:name="cc.winboll.studio.winboll.unittest.TestWeWorkSpecSDK"/>
<activity android:name="cc.winboll.studio.winboll.activities.WXPayActivity"/>
</application>
</manifest>
</manifest>

View File

@@ -1,6 +1,6 @@
package cc.winboll.studio.winboll;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@@ -8,20 +8,17 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import cc.winboll.studio.libaes.activitys.AboutActivity;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.models.DrawerMenuBean;
import cc.winboll.studio.libaes.unittests.TestAButtonFragment;
import cc.winboll.studio.libaes.unittests.TestViewPageFragment;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.winboll.R;
import cc.winboll.studio.winboll.activities.SettingsActivity;
import cc.winboll.studio.winboll.activities.WXPayActivity;
import cc.winboll.studio.winboll.fragments.BrowserFragment;
import java.util.ArrayList;
import android.content.Intent;
import cc.winboll.studio.libaes.activitys.AboutActivity;
public class MainActivity extends DrawerFragmentActivity {
@@ -152,6 +149,8 @@ public class MainActivity extends DrawerFragmentActivity {
}
} else if (nItemId == R.id.item_settings) {
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
} else if (nItemId == R.id.item_wxpayactivity) {
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), WXPayActivity.class);
} else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) {
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
APPInfo appInfo = genDefaultAPPInfo();

View File

@@ -0,0 +1,28 @@
package cc.winboll.studio.winboll;
/**
* 微信支付配置类
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/07
*/
public class WxPayConfig {
// ========== 核心修改点:替换为你的服务端地址 ==========
// 服务端IP/域名 + 端口Docker部署的服务端需确保安卓端可访问
public static final String BASE_URL = "https://wxpay.winboll.cc";
// 统一下单接口路径(对应服务端的测试接口)
public static final String CREATE_ORDER_URL = BASE_URL + "/pay/createOrder";
// 订单查询接口路径
public static final String QUERY_ORDER_URL = BASE_URL + "/pay/queryOrder";
// ========== 固定支付配置 ==========
public static final String ORDER_BODY = "定额测试支付"; // 商品描述
public static final int TOTAL_FEE = 1; // 固定金额1分沙箱环境推荐
public static final String TRADE_TYPE = "NATIVE"; // 支付类型:二维码
// ========== 轮询配置 ==========
public static final long POLL_INTERVAL = 10000; // 轮询间隔10秒
public static final long POLL_TIMEOUT = 45000; // 轮询超时45秒
}

View File

@@ -0,0 +1,211 @@
package cc.winboll.studio.winboll.activities;
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.winboll.R;
import cc.winboll.studio.winboll.WxPayConfig;
import cc.winboll.studio.winboll.utils.SpecUtil;
import cc.winboll.studio.winboll.utils.WxPayApi;
import cc.winboll.studio.winboll.utils.ZXingUtils;
import java.util.Timer;
import java.util.TimerTask;
/**
* 主界面:生成二维码 + 轮询查询支付结果
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/07
*/
public class WXPayActivity extends WinBoLLActivity implements IWinBoLLActivity {
private static final String TAG = "WXPayActivity";
// Handler消息标识
private static final int MSG_POLL_TIMEOUT = 1001;
private static final int MSG_POLL_SUCCESS = 1002;
private static final int MSG_POLL_FAILED = 1003;
private ImageView mIvQrCode;
private TextView mTvOrderNo;
private TextView mTvPayStatus;
private Button mBtnCreateOrder;
private String mOutTradeNo; // 商户订单号
private Timer mPollTimer; // 轮询定时器
private long mPollStartTime; // 轮询开始时间
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
private Handler mPollHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_POLL_TIMEOUT:
stopPoll();
mTvPayStatus.setText("支付状态:轮询超时");
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.darker_gray));
Toast.makeText(WXPayActivity.this, "轮询超时,请手动查询", Toast.LENGTH_SHORT).show();
break;
case MSG_POLL_SUCCESS:
boolean isPaySuccess = (boolean) msg.obj;
String tradeState = (String) msg.getData().getString("tradeState");
stopPoll();
if (isPaySuccess) {
mTvPayStatus.setText("支付状态:支付成功 ✅");
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark));
Toast.makeText(WXPayActivity.this, "支付成功!", Toast.LENGTH_SHORT).show();
} else {
mTvPayStatus.setText("支付状态:" + tradeState);
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
}
break;
case MSG_POLL_FAILED:
String errorMsg = (String) msg.obj;
mTvPayStatus.setText("查询失败:" + errorMsg);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wxpay);
initView();
initListener();
}
private void initView() {
mIvQrCode = findViewById(R.id.iv_qrcode);
mTvOrderNo = findViewById(R.id.tv_order_no);
mTvPayStatus = findViewById(R.id.tv_pay_status);
mBtnCreateOrder = findViewById(R.id.btn_create_order);
}
private void initListener() {
mBtnCreateOrder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createOrder();
}
});
}
/**
* 统一下单,生成二维码
*/
private void createOrder() {
mBtnCreateOrder.setEnabled(false);
mTvPayStatus.setText("支付状态:生成订单中...");
mIvQrCode.setImageBitmap(null);
WxPayApi.createOrder(new WxPayApi.OnCreateOrderCallback() {
@Override
public void onSuccess(String outTradeNo, String codeUrl) {
mOutTradeNo = outTradeNo;
mTvOrderNo.setText("商户订单号:" + outTradeNo);
mTvPayStatus.setText("支付状态:未支付,请扫码");
// 生成二维码
Bitmap qrCodeBitmap = ZXingUtils.createQRCodeBitmap(codeUrl, 250, 250);
if (qrCodeBitmap != null) {
mIvQrCode.setImageBitmap(qrCodeBitmap);
// 开始轮询查询支付结果
startPoll();
} else {
mTvPayStatus.setText("支付状态:生成二维码失败");
mBtnCreateOrder.setEnabled(true);
}
}
@Override
public void onFailure(String errorMsg) {
SpecUtil.WWSpecLogError(TAG, "统一下单失败:" + errorMsg);
mTvPayStatus.setText("生成订单失败:" + errorMsg);
mBtnCreateOrder.setEnabled(true);
Toast.makeText(WXPayActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
}
});
}
/**
* 开始轮询查询支付结果
*/
private void startPoll() {
stopPoll(); // 先停止之前的轮询
mPollStartTime = System.currentTimeMillis();
mPollTimer = new Timer();
mPollTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 检查是否超时
long elapsedTime = System.currentTimeMillis() - mPollStartTime;
if (elapsedTime >= WxPayConfig.POLL_TIMEOUT) {
mPollHandler.sendEmptyMessage(MSG_POLL_TIMEOUT);
return;
}
// 查询订单状态
WxPayApi.queryOrder(mOutTradeNo, new WxPayApi.OnQueryOrderCallback() {
@Override
public void onSuccess(boolean isPaySuccess, String tradeState) {
Message msg = Message.obtain();
msg.what = MSG_POLL_SUCCESS;
msg.obj = isPaySuccess;
Bundle bundle = new Bundle();
bundle.putString("tradeState", tradeState);
msg.setData(bundle);
mPollHandler.sendMessage(msg);
}
@Override
public void onFailure(String errorMsg) {
Message msg = Message.obtain();
msg.what = MSG_POLL_FAILED;
msg.obj = errorMsg;
mPollHandler.sendMessage(msg);
}
});
}
}, 0, WxPayConfig.POLL_INTERVAL);
}
/**
* 停止轮询
*/
private void stopPoll() {
if (mPollTimer != null) {
mPollTimer.cancel();
mPollTimer = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopPoll();
}
}

View File

@@ -0,0 +1,311 @@
package cc.winboll.studio.winboll.unittest;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.winboll.R;
import cc.winboll.studio.winboll.activities.WinBoLLActivity;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/03 10:52
* @Describe 企业微信SDK接口测试基础调试版
* 包含SDK初始化、基础接口调用、日志输出、主线程回调处理
*/
public class TestWeWorkSpecSDK extends WinBoLLActivity implements IWinBoLLActivity, View.OnClickListener {
public static final String TAG = "TestWeWorkSpecSDK";
// ------------------- 企业微信SDK配置常量需替换为实际项目参数 -------------------
// 企业微信 CorpID从企业微信管理后台获取
private static final String CORP_ID = "wwb37c73f34c722852";
// 应用 AgentID从企业微信应用管理后台获取
private static final String AGENT_ID = "your_agent_id_here";
// 应用 Secret从企业微信应用管理后台获取注意保密
private static final String APP_SECRET = "your_app_secret_here";
// ------------------- Handler消息标识主线程处理SDK回调 -------------------
private static final int MSG_SDK_INIT_SUCCESS = 1001;
private static final int MSG_SDK_INIT_FAILED = 1002;
private static final int MSG_GET_CORP_INFO_SUCCESS = 1003;
private static final int MSG_GET_CORP_INFO_FAILED = 1004;
// ------------------- 控件声明 -------------------
private Button mBtnInitSDK;
private Button mBtnGetCorpInfo;
private Button mBtnCheckAuth;
// ------------------- 主线程Handler处理SDK异步回调 -------------------
private Handler mWeWorkHandler;
@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_test_weworkspecsdk);
// 初始化控件
initViews();
// 绑定点击事件
initEvents();
// 初始化Handler主线程处理回调更新UI
initHandler();
// 初始化SDK可选启动时自动初始化或点击按钮初始化
// initWeWorkSDK();
}
/**
* 初始化控件Java 7 显式绑定)
*/
private void initViews() {
mBtnInitSDK = (Button) findViewById(R.id.btn_init_sdk);
mBtnGetCorpInfo = (Button) findViewById(R.id.btn_get_corp_info);
mBtnCheckAuth = (Button) findViewById(R.id.btn_check_auth);
}
/**
* 绑定点击事件Java 7 匿名内部类)
*/
private void initEvents() {
mBtnInitSDK.setOnClickListener(this);
mBtnGetCorpInfo.setOnClickListener(this);
mBtnCheckAuth.setOnClickListener(this);
}
/**
* 初始化主线程Handler处理SDK异步回调安全更新UI
*/
private void initHandler() {
mWeWorkHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_SDK_INIT_SUCCESS:
showToast("企业微信SDK初始化成功");
LogUtils.d(TAG, "SDK初始化成功");
break;
case MSG_SDK_INIT_FAILED:
String initError = (String) msg.obj;
showToast("SDK初始化失败" + initError);
LogUtils.e(TAG, "SDK初始化失败" + initError);
break;
case MSG_GET_CORP_INFO_SUCCESS:
String corpInfo = (String) msg.obj;
showToast("获取企业信息成功");
LogUtils.d(TAG, "企业信息:" + corpInfo);
break;
case MSG_GET_CORP_INFO_FAILED:
String corpError = (String) msg.obj;
showToast("获取企业信息失败:" + corpError);
LogUtils.e(TAG, "获取企业信息失败:" + corpError);
break;
default:
break;
}
}
};
}
// ------------------- 企业微信SDK核心接口调用 -------------------
/**
* 初始化企业微信SDK异步操作通过Handler回调结果
*/
private void initWeWorkSDK() {
showToast("开始初始化企业微信SDK...");
// 模拟SDK异步初始化实际项目中替换为企业微信SDK的真实初始化接口
new Thread(new Runnable() {
@Override
public void run() {
try {
// 真实SDK初始化逻辑示例
// WeWorkSDK.init(TestWeWorkSpecSDK.this, CORP_ID, AGENT_ID, new WeWorkSDKCallback() {
// @Override
// public void onSuccess() {
// mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
// }
//
// @Override
// public void onFailure(String errorMsg) {
// Message msg = Message.obtain();
// msg.what = MSG_SDK_INIT_FAILED;
// msg.obj = errorMsg;
// mWeWorkHandler.sendMessage(msg);
// }
// });
// 调试模拟休眠1秒模拟异步初始化
Thread.sleep(1000);
// 模拟初始化成功如需测试失败替换为发送MSG_SDK_INIT_FAILED
mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
// 模拟初始化失败
// Message msg = Message.obtain();
// msg.what = MSG_SDK_INIT_FAILED;
// msg.obj = "CorpID或AgentID错误";
// mWeWorkHandler.sendMessage(msg);
} catch (InterruptedException e) {
e.printStackTrace();
Message msg = Message.obtain();
msg.what = MSG_SDK_INIT_FAILED;
msg.obj = "线程中断:" + e.getMessage();
mWeWorkHandler.sendMessage(msg);
}
}
}).start();
}
/**
* 获取企业基本信息异步操作需先初始化SDK
*/
private void getCorpInfo() {
if (!isSDKInitialized()) {
showToast("请先初始化SDK");
return;
}
showToast("开始获取企业信息...");
// 模拟SDK异步获取企业信息实际项目中替换为真实接口
new Thread(new Runnable() {
@Override
public void run() {
try {
// 真实SDK接口示例
// WeWorkSDK.getCorpInfo(APP_SECRET, new CorpInfoCallback() {
// @Override
// public void onSuccess(CorpInfo info) {
// Message msg = Message.obtain();
// msg.what = MSG_GET_CORP_INFO_SUCCESS;
// msg.obj = "企业名称:" + info.getCorpName() + "企业ID" + info.getCorpId();
// mWeWorkHandler.sendMessage(msg);
// }
//
// @Override
// public void onFailure(String errorMsg) {
// Message msg = Message.obtain();
// msg.what = MSG_GET_CORP_INFO_FAILED;
// msg.obj = errorMsg;
// mWeWorkHandler.sendMessage(msg);
// }
// });
// 调试模拟休眠1秒模拟异步获取
Thread.sleep(1000);
// 模拟获取成功
Message successMsg = Message.obtain();
successMsg.what = MSG_GET_CORP_INFO_SUCCESS;
successMsg.obj = "企业名称WinBoLL Studio企业ID" + CORP_ID;
mWeWorkHandler.sendMessage(successMsg);
// 模拟获取失败
// Message failMsg = Message.obtain();
// failMsg.what = MSG_GET_CORP_INFO_FAILED;
// failMsg.obj = "AppSecret错误或权限不足";
// mWeWorkHandler.sendMessage(failMsg);
} catch (InterruptedException e) {
e.printStackTrace();
Message msg = Message.obtain();
msg.what = MSG_GET_CORP_INFO_FAILED;
msg.obj = "线程中断:" + e.getMessage();
mWeWorkHandler.sendMessage(msg);
}
}
}).start();
}
/**
* 检查当前用户是否已授权(同步操作,示例)
*/
private void checkAuthStatus() {
if (!isSDKInitialized()) {
showToast("请先初始化SDK");
return;
}
// 真实SDK接口示例
// boolean isAuthorized = WeWorkSDK.isAuthorized();
// 调试模拟默认返回true
boolean isAuthorized = true;
if (isAuthorized) {
showToast("用户已授权");
LogUtils.d(TAG, "当前用户已授权企业微信应用");
} else {
showToast("用户未授权,请先授权");
LogUtils.d(TAG, "当前用户未授权企业微信应用");
// 真实项目中可调用授权接口:
// WeWorkSDK.requestAuth(TestWeWorkSpecSDK.this, new AuthCallback() {
// @Override
// public void onSuccess(String code) {
// showToast("授权成功code" + code);
// }
//
// @Override
// public void onFailure(String errorMsg) {
// showToast("授权失败:" + errorMsg);
// }
// });
}
}
// ------------------- 工具方法 -------------------
/**
* 检查SDK是否已初始化模拟方法实际项目中替换为SDK的真实状态检查
*/
private boolean isSDKInitialized() {
// 真实SDK可通过静态方法检查状态
// return WeWorkSDK.isInitialized();
// 调试模拟假设Handler不为空即表示已初始化
return mWeWorkHandler != null;
}
/**
* 显示Toast提示Java 7 简化封装)
*/
private void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
// ------------------- 点击事件处理 -------------------
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_init_sdk) {
initWeWorkSDK();
} else if (id == R.id.btn_get_corp_info) {
getCorpInfo();
} else if (id == R.id.btn_check_auth) {
checkAuthStatus();
}
}
// ------------------- 生命周期管理 -------------------
@Override
protected void onDestroy() {
super.onDestroy();
// 释放Handler资源避免内存泄漏
if (mWeWorkHandler != null) {
mWeWorkHandler.removeCallbacksAndMessages(null);
mWeWorkHandler = null;
}
// 真实SDK需调用销毁方法
// WeWorkSDK.destroy();
}
}

View File

@@ -0,0 +1,83 @@
package cc.winboll.studio.winboll.utils;
import android.os.Handler;
import android.os.Looper;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* OkHttp网络请求工具类
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/07
*/
public class OkHttpUtil {
private static OkHttpClient sOkHttpClient;
private static Handler sMainHandler = new Handler(Looper.getMainLooper());
static {
sOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
}
/**
* GET请求
* @param url 请求地址
* @param callback 回调
*/
public static void get(String url, final OnResultCallback callback) {
Request request = new Request.Builder()
.url(url)
.get()
.build();
sOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
sMainHandler.post(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onFailure(e.getMessage());
}
}
});
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
final String result = response.body().string();
sMainHandler.post(new Runnable() {
@Override
public void run() {
if (callback != null) {
if (response.isSuccessful()) {
callback.onSuccess(result);
} else {
callback.onFailure("请求失败:" + response.code());
}
}
}
});
}
});
}
/**
* 回调接口
*/
public interface OnResultCallback {
void onSuccess(String result);
void onFailure(String errorMsg);
}
}

View File

@@ -0,0 +1,26 @@
package cc.winboll.studio.winboll.utils;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 日志工具类(适配项目规范)
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/07
*/
public class SpecUtil {
private static final boolean isDebug = true;
public static void WWSpecLogInfo(String tag, String msg) {
if (isDebug) {
LogUtils.i(tag, msg);
}
}
public static void WWSpecLogError(String tag, String msg) {
if (isDebug) {
LogUtils.e(tag, msg);
}
}
}

View File

@@ -0,0 +1,100 @@
package cc.winboll.studio.winboll.utils;
import cc.winboll.studio.winboll.WxPayConfig;
import com.alibaba.fastjson.JSONObject;
/**
* 微信支付服务端接口封装
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/07
*/
public class WxPayApi {
/**
* 统一下单(生成二维码)
* @param callback 回调
*/
public static void createOrder(final OnCreateOrderCallback callback) {
// 拼接请求参数服务端测试接口需支持GET传参若为POST需修改为表单/JSON
String url = WxPayConfig.CREATE_ORDER_URL +
"?body=" + WxPayConfig.ORDER_BODY +
"&totalFee=" + WxPayConfig.TOTAL_FEE +
"&tradeType=" + WxPayConfig.TRADE_TYPE;
OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
@Override
public void onSuccess(String result) {
try {
JSONObject jsonObject = JSONObject.parseObject(result);
String outTradeNo = jsonObject.getString("out_trade_no");
String codeUrl = jsonObject.getString("code_url");
if (callback != null) {
callback.onSuccess(outTradeNo, codeUrl);
}
} catch (Exception e) {
if (callback != null) {
callback.onFailure("解析统一下单结果失败:" + e.getMessage());
}
}
}
@Override
public void onFailure(String errorMsg) {
if (callback != null) {
callback.onFailure("统一下单请求失败:" + errorMsg);
}
}
});
}
/**
* 订单查询
* @param outTradeNo 商户订单号
* @param callback 回调
*/
public static void queryOrder(String outTradeNo, final OnQueryOrderCallback callback) {
String url = WxPayConfig.QUERY_ORDER_URL + "?outTradeNo=" + outTradeNo;
OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
@Override
public void onSuccess(String result) {
try {
JSONObject jsonObject = JSONObject.parseObject(result);
String tradeState = jsonObject.getString("trade_state");
boolean isSuccess = "SUCCESS".equals(tradeState);
if (callback != null) {
callback.onSuccess(isSuccess, tradeState);
}
} catch (Exception e) {
if (callback != null) {
callback.onFailure("解析订单查询结果失败:" + e.getMessage());
}
}
}
@Override
public void onFailure(String errorMsg) {
if (callback != null) {
callback.onFailure("订单查询请求失败:" + errorMsg);
}
}
});
}
/**
* 统一下单回调接口
*/
public interface OnCreateOrderCallback {
void onSuccess(String outTradeNo, String codeUrl);
void onFailure(String errorMsg);
}
/**
* 订单查询回调接口
*/
public interface OnQueryOrderCallback {
void onSuccess(boolean isPaySuccess, String tradeState);
void onFailure(String errorMsg);
}
}

View File

@@ -0,0 +1,75 @@
package cc.winboll.studio.winboll.utils;
import android.graphics.Bitmap;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.journeyapps.barcodescanner.BarcodeEncoder;
import java.util.HashMap;
import java.util.Map;
/**
* ZXing二维码生成工具类
* 依赖com.google.zxing:core:3.4.1 + com.journeyapps:zxing-android-embedded:3.6.0
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/07
*/
public class ZXingUtils {
/**
* 生成二维码Bitmap核心方法使用journeyapps工具类
* @param content 内容如微信支付的code_url
* @param width 二维码宽度px
* @param height 二维码高度px
* @return 二维码Bitmap失败返回null
*/
public static Bitmap createQRCodeBitmap(String content, int width, int height) {
// 1. 入参合法性校验
if (content == null || content.trim().isEmpty()) {
return null;
}
if (width <= 0 || height <= 0) {
return null;
}
// 2. 配置二维码参数
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 字符编码
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 高容错级别H级可容忍30%遮挡)
hints.put(EncodeHintType.MARGIN, 1); // 边距值越小二维码越紧凑建议1-4
try {
// 3. 生成BitMatrix二维码矩阵
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(
content,
BarcodeFormat.QR_CODE,
width,
height,
hints
);
// 4. 转换BitMatrix为Bitmap关键使用journeyapps的BarcodeEncoder
BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
return barcodeEncoder.createBitmap(bitMatrix);
} catch (WriterException e) {
e.printStackTrace();
return null;
}
}
/**
* 重载方法:生成正方形二维码(宽度=高度)
* @param content 内容
* @param size 二维码边长px
* @return 二维码Bitmap
*/
public static Bitmap createQRCodeBitmap(String content, int size) {
return createQRCodeBitmap(content, size, size);
}
}

View File

@@ -0,0 +1,210 @@
package com.tencent.wework;
import java.util.Map;
import java.util.HashMap;;
/**
* @warning: 1. 不要修改成员变量名native方法内有反射调用
* 2. 调用本地方法需保持包结构本工具需放在包com.tencent.wework内
* 3. 不允许继承类名和函数名均不可修改会影响本地方法的引用详见javah生成本地方法头文件
*/
public final class SpecCallbackSDK {
/**
* @description 调用本地方法后实例化的对象指针
*/
private long specCallbackSDKptr = 0;
public long GetPtr() { return specCallbackSDKptr; }
/**
* @description: 回包的headers
*/
private Map<String, String> responseHeaders;
public Map<String, String> GetResponseHeaders() { return responseHeaders; }
/**
* @description: 回包的加密后的body
*/
private String responseBody;
public String GetResponseBody() { return responseBody; }
/**
* @description: 每个请求构造一个SpecCallbackSDK示例,
* SpecCallbackSDK仅持有headers和body的引用,
* 因此需保证headers和body的生存期比SpecCallbackSDK长
* @param method: 请求方法GET/POST
* @param headers: 请求header
* @param body: 请求body
* @example:
* SpecCallbackSDK sdk = new SpecCallbackSDK(method, headers, body);
* if (sdk.IsOk()) {
* String corpid = sdk.GetCorpId();
* String agentid = sdk.GetAgentId();
* String call_type = sdk.GetCallType();
* String data = sdk.GetData();
* //do something...
* }
* String response = ...;
* sdk.BuildResponseHeaderBody(response);
* Map<String, String> responseHeaders = sdk.GetResponseHeaders();
* String body = sdk.GetResponseBody();
* //do response
*
* @return errorcode 示例如下:
* -920001: 未设置请求方法
* -920002: 未设置请求header
* -920003: 未设置请求body
* */
public SpecCallbackSDK(String method, Map<String, String> headers, String body) {
try {
specCallbackSDKptr = NewCallbackSDK(method, headers, body);
} catch (Exception e) {
SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage());
}
}
private native long NewCallbackSDK(String method, Map<String, String> headers, String body);
/**
* @usage 在Java对象的内存回收前析构C++对象
*/
@Override
protected void finalize() throws Throwable {
DeleteCPPInstance(specCallbackSDKptr);
super.finalize();
}
private native void DeleteCPPInstance(long specCallbackSDKptr);
/**
* @description: 判断构造函数中传入的请求是否解析成功
* @return: 成功与否
* */
public boolean IsOk() {
return IsOk(specCallbackSDKptr);
}
private native boolean IsOk(long specCallbackSDKptr);
/**
* @description: 获取请求的企业
* @require: 仅当IsOk() == true可调用
* @return: corpid
* */
public String GetCorpId() {
return GetCorpId(specCallbackSDKptr);
}
private native String GetCorpId(long specCallbackSDKptr);
/**
* @description: 获取请求的应用
* @require: 仅当IsOk() == true可调用
* @return: agentid
* */
public long GetAgentId() {
return GetAgentId(specCallbackSDKptr);
}
private native long GetAgentId(long specCallbackSDKptr);
/**
* @description: 获取请求的类型
* @require: 仅当IsOk() == true可调用
* @return: 1 - 来自[应用调用专区]的请求
* 2 - 来自企业微信的回调事件
* */
public long GetCallType() {
return GetCallType(specCallbackSDKptr);
}
private native long GetCallType(long specCallbackSDKptr);
/**
* @description: 获取请求数据
* @require: 仅当IsOk() == true可调用
* @return: 请求数据,根据call_type可能是:
* - 企业微信回调事件
* - [应用调用专区]接口中的request_data
* */
public String GetData() {
return GetData(specCallbackSDKptr);
}
private native String GetData(long specCallbackSDKptr);
/**
* @description: 是否异步请求
* @require: 仅当IsOk() == true可调用
* @return: 是否异步请求
* */
public boolean GetIsAsync() {
return GetIsAsync(specCallbackSDKptr);
}
private native boolean GetIsAsync(long specCallbackSDKptr);
/**
* @description: 获取请求的job_info,
* @require: 仅当IsOk() == true可调用
* @return: job_info,无需理解内容,
* 在同一个请求上下文中使用SpecSDK的时候传入
* */
public String GetJobInfo() {
return GetJobInfo(specCallbackSDKptr);
}
private native String GetJobInfo(long specCallbackSDKptr);
/**
* @description: 获取请求的ability_id,[应用调用专区]接口时指定
* @require: 仅当IsOk() == true可调用
* @return: ability_id
* */
public String GetAbilityId() {
return GetAbilityId(specCallbackSDKptr);
}
private native String GetAbilityId(long specCallbackSDKptr);
/**
* @description: 获取请求的notify_id,用于[应用同步调用专区程序]接口
* @require: 仅当IsOk() == true可调用
* @return: notify_id
* */
public String GetNotifyId() {
return GetNotifyId(specCallbackSDKptr);
}
private native String GetNotifyId(long specCallbackSDKptr);
/**
* @description: 对返回包计算签名&加密
* @param response: 待加密的回包明文.如果IsOk()==false,传入空串即可
* @note 本接口的执行问题可查看日志
* */
public void BuildResponseHeaderBody(String response) {
try {
responseHeaders = new HashMap<String, String>();
responseBody = "";
BuildResponseHeaderBody(specCallbackSDKptr, response);
} catch (Exception e) {
SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage());
}
}
private native void BuildResponseHeaderBody(long specCallbackSDKptr, String response);
// 静态代码块内还无法调用native日志函数这里的日志在管理系统无法查询
static {
try {
Class.forName("com.tencent.wework.SpecUtil");
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.exit(1);
}
}
}

View File

@@ -0,0 +1,163 @@
package com.tencent.wework;
/**
* @warning: 1. 不要修改成员变量名native方法内有反射调用
* 2. 调用本地方法需保持包结构本工具需放在包com.tencent.wework内
* 3. 不允许继承类名和函数名均不可修改会影响本地方法的引用详见javah生成本地方法头文件
*/
public final class SpecSDK {
/**
* @description 调用本地方法后实例化的对象指针
*/
private long specSDKptr = 0;
/**
* @usage invoke的请求
* @example "{\"limit\":1}
*/
private String request;
public void SetRequest(String request) {
this.request = request;
}
/**
* @usage 访问上一次invoke的结果
*/
private String response;
public String GetResponse() {
return response;
}
/**
* @param corpid: 企业corpid必选参数
* @param agentid: 应用id必选参数
* @param ability_id: 能力ID可选参数
* @param job_info: job_info可选参数
* */
public SpecSDK(String corpId, long agentId) {
specSDKptr = NewSDK1(corpId, agentId);
}
private native long NewSDK1(String corpId, long agentId);
public SpecSDK(String corpId, long agentId, String abilityId) {
specSDKptr = NewSDK2(corpId, agentId, abilityId);
}
private native long NewSDK2(String corpId, long agentId, String abilityId);
public SpecSDK(String corpId, long agentId, String abilityId, String jobInfo) {
specSDKptr = NewSDK3(corpId, agentId, abilityId, jobInfo);
}
private native long NewSDK3(String corpId, long agentId, String abilityId, String jobInfo);
/**
* @description 使用callback的请求来初始化
* @param callback_sdk: 要求IsOk()==true
* @return C++内部指针创建失败时指针仍为0并输出错误日志
* */
public SpecSDK(SpecCallbackSDK callbackSDK) {
specSDKptr = NewSDK4(callbackSDK.GetPtr());
}
private native long NewSDK4(long callbackSDK);
/**
* @usage 在Java对象的内存回收前析构C++对象
*/
@Override
protected void finalize() throws Throwable {
DeleteCPPInstance(specSDKptr);
super.finalize();
}
private native void DeleteCPPInstance(long specSDKptr);
/**
* @description 用于在专区内调用企业微信接口
* @param api_name 接口名
* @param request json格式的请求数据
* @param response json格式的返回数据
* @return errorcode 参考如下:
* 0: 成功
* -910001: SDK没有初始化
* -910002: 没有设置请求体
* -910003: 没有设置请求的API
* -910004: 在SDK成员内找不到成员"response",注意lib内有反射机制,不要修改成员变量名
* -910005: 使用未初始化的callback初始化SDK
* -910006: invoke调用失败,应检查日志查看具体原因
* -910007: 响应体为空
* @note 当返回0时,表示没有网络或请求协议层面或调用方法的失败,
* 调用方需继续检查response中的errcode字段确保业务层面的成功
*
* @usage 当前版本sdk支持的接口列表,每个接口的具体协议请查看企业微信文档:
* https://developer.work.weixin.qq.com/document/path/91201
*
* +--------------------------------+--------------------------------+
* |接口名 |描述 |
* |--------------------------------|--------------------------------|
* |program_async_job_call_back |上报异步任务结果 |
* |sync_msg |获取会话记录 |
* |get_group_chat |获取内部群信息 |
* |get_agree_status_single |获取单聊会话同意情况 |
* |get_agree_status_room |获取群聊会话同意情况 |
* |set_hide_sensitiveinfo_config |设置成员会话组件敏感信息隐藏配置|
* |get_hide_sensitiveinfo_config |获取成员会话组件敏感信息隐藏配置|
* |search_chat |会话名称搜索 |
* |search_msg |会话消息搜索 |
* |create_rule |新增关键词规则 |
* |get_rule_list |获取关键词列表 |
* |get_rule_detail |获取关键词规则详情 |
* |update_rule |修改关键词规则 |
* |delete_rule |删除关键词规则 |
* |get_hit_msg_list |获取命中关键词规则的会话记录 |
* |create_sentiment_task |创建情感分析任务 |
* |get_sentiment_result |获取情感分析结果 |
* |create_summary_task |创建摘要提取任务 |
* |get_summary_result |获取摘要提取结果 |
* |create_customer_tag_task |创建标签匹配任务 |
* |get_customer_tag_result |获取标签任务结果 |
* |create_recommend_dialog_task |创建话术推荐任务 |
* |get_recommend_dialog_result |获取话术推荐结果 |
* |create_private_task |创建自定义模型任务 |
* |get_private_task_result |获取自定义模型结果 |
* |(废弃)document_list |获取知识集列表 |
* |create_spam_task |会话反垃圾创建分析任务 |
* |get_spam_result |会话反垃圾获取任务结果 |
* |create_chatdata_export_job |创建会话内容导出任务 |
* |get_chatdata_export_job_status |获取会话内容导出任务结果 |
* |spec_notify_app |专区通知应用 |
* |create_program_task |创建自定义程序任务 |
* |get_program_task_result |获取自定义程序结果 |
* |knowledge_base_list |获取企业授权给应用的知识集列表 |
* |knowledge_base_create |创建知识集 |
* |knowledge_base_detail |获取知识集详情 |
* |knowledge_base_add_doc |添加知识集內容 |
* |knowledge_base_remove_doc |删除知识集內容 |
* |knowledge_base_modify_name |修改知识集名称 |
* |knowledge_base_delete |删除知识集 |
* |search_contact_or_customer |员工或者客户名称搜索 |
* |create_ww_model_task |创建企微通用模型任务 |
* |get_ww_model_result |获取企微通用模型结果 |
* |get_msg_list_by_page_id |page_id获取消息列表 |
* +-----------------------------------------------------------------+
* */
public int Invoke(String apiName) {
return Invoke(specSDKptr, apiName, request);
}
private native int Invoke(long sdk, String apiName, String request);
// 静态代码块内还无法调用native日志函数这里的日志在管理系统无法查询
static {
try {
Class.forName("com.tencent.wework.SpecUtil");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,171 @@
package com.tencent.wework;
//import java.lang.management.ManagementFactory;
//import java.lang.management.RuntimeMXBean;
/**
* @warning: 1. 不要修改成员变量名native方法内有反射调用
* 2. 调用本地方法需保持包结构本工具需放在包com.tencent.wework内
* 3. 不允许继承类名和函数名均不可修改会影响本地方法的引用详见javah生成本地方法头文件
* 4. 使用其他工具打印的日志将无法被查询如需使用SLF4j风格的日志或性能更好的日志框架
* 请自行封装SpecUtil.SpecLog或SpecUtil.SpecLogNative方法
*
* @usage: 1. 获取SDK的版本号
* 2. 打印三个级别的日志
* 3. 开启调试模式
*/
public final class SpecUtil {
/**
* @description SDK版本号
* @usage 可用于校对不同SDK版本或后续针对不同的SDK版本添加业务逻辑
*/
private static final String SDK_VERSION = "1.4.0";
public static String GetSDKVersion() {
return SDK_VERSION;
}
/**
* @description 正确的包名SDK必须存放在"com.tencent.wework"下,否则会影响本地方法的调用
*/
private static final String EXPECTED_PACKAGE_NAME = "com.tencent.wework";
public static String GetExpectedPackageName() {
return EXPECTED_PACKAGE_NAME;
}
private static final String LINE_SEPERATOR = System.getProperty("line.separator");
public static void WWSpecLogInfo(String... args) {
SpecLog('I', args);
}
public static void WWSpecLogError(String... args) {
SpecLog('E', args);
}
public static void WWSpecLogDebug(String... args) {
SpecLog('D', args);
}
public static void WWSpecLogInfoWithReqId(String reqId, String... args) {
SpecLogWithReqId(reqId, 'I', args);
}
public static void WWSpecLogErrorWithReqId(String reqId, String... args) {
SpecLogWithReqId(reqId, 'E', args);
}
public static void WWSpecLogDebugWithReqId(String reqId, String... args) {
SpecLogWithReqId(reqId, 'D', args);
}
/**
* @usage 打印标准日志
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询其他框架的日志仅能本地查看
* @param logLevel 日志级别使用char传递目前支持I——INFO、E——ERROR、D——DEBUG
* @param args 自定义参数
*/
public static void SpecLog(char logLevel, String... args) {
StackTraceElement element = Thread.currentThread().getStackTrace()[3];
SpecLogNative(
logLevel,
element.getFileName(),
element.getLineNumber(),
String.join(",", args).replace(LINE_SEPERATOR, " ")
);
}
/**
* @usage 打印标准日志
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询其他框架的日志仅能本地查看
* @param reqid 请求id
* @param logLevel 日志级别使用char传递目前支持I——INFO、E——ERROR、D——DEBUG
* @param args 自定义参数
*/
public static void SpecLogWithReqId(String reqId, char logLevel, String... args) {
StackTraceElement element = Thread.currentThread().getStackTrace()[3];
SpecLogNativeWithReqId(
reqId,
logLevel,
element.getFileName(),
element.getLineNumber(),
String.join(",", args).replace(LINE_SEPERATOR, " ")
);
}
/**
* @usage 打印标准日志
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询其他框架的日志仅能本地查看
* 如需SLF4J风格的接口或对日志性能有进一步需求开发者可以自行封装该函数
* @param logLevel 日志级别使用char传递目前支持I——INFO、E——ERROR、D——DEBUG
* @param fileName 文件名(类名)
* @param lineNumber 行号
* @param argsString 自定义参数
*/
public static native void SpecLogNative(char logLevel, String fileName, int lineNumber, String argsString);
/**
* @usage 打印标准日志
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询其他框架的日志仅能本地查看
* 如需SLF4J风格的接口或对日志性能有进一步需求开发者可以自行封装该函数
* @param reqid 请求id
* @param logLevel 日志级别使用char传递目前支持I——INFO、E——ERROR、D——DEBUG
* @param fileName 文件名(类名)
* @param lineNumber 行号
* @param argsString 自定义参数
*/
public static native void SpecLogNativeWithReqId(String reqId, char logLevel, String fileName, int lineNumber, String argsString);
/**
* @usage 开启调试模式,进程级别开关
* @param debugToken 调试凭证,在管理端获取
* @param accessToken 应用access token
* @return 是否开启成功
*/
public static boolean SpecOpenDebugMode(String debugToken, String accessToken) {
return SpecOpenDebugModeNative(debugToken, accessToken);
}
private static native boolean SpecOpenDebugModeNative(String debugToken, String accessToken);
/**
* @usage 生成notify id。用户可调用本接口生成notify id也可完全自定义生成
* @return 新的notify id支持纳秒级隔离内部异常时会输出日志并返回空串
* @note 1. 用户可先生成notify id将其与回调数据关联存储后再使用该notify id通知应用
* 从而保证回调数据被请求时已存储完毕
*/
public static String GenerateNotifyId() {
return GenerateNotifyIdNative();
}
private static native String GenerateNotifyIdNative();
static {
// 检查包名
String packageName = SpecUtil.class.getPackage().getName();
if (!EXPECTED_PACKAGE_NAME.equals(packageName)) {
// 静态代码块内还无法调用native日志函数这里的日志在管理系统无法查询
System.out.println("SpecUtil class must be in package com.tencent.wework");
System.exit(1);
}
// 加载so库
try {
System.loadLibrary("WeWorkSpecSDK");
} catch (UnsatisfiedLinkError e) {
System.out.println("libWeWorkSpecSDK.so not found in java.library.path");
e.printStackTrace();
System.exit(1);
} catch (Exception e) {
System.out.println("unexpected exception: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
SpecUtil.WWSpecLogInfo("SDK init done", "packageName=" + packageName, "SDK_VERSION=" + SDK_VERSION);
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp"
android:gravity="center_horizontal">
<Button
android:id="@+id/btn_init_sdk"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="初始化企业微信SDK"
android:layout_marginBottom="10dp"/>
<Button
android:id="@+id/btn_get_corp_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="获取企业基本信息"
android:layout_marginBottom="10dp"/>
<Button
android:id="@+id/btn_check_auth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="检查用户授权状态"/>
</LinearLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="定额支付测试0.01元)"
android:textSize="20sp"/>
<ImageView
android:id="@+id/iv_qrcode"
android:layout_width="250dp"
android:layout_height="250dp"
android:scaleType="fitXY"/>
<TextView
android:id="@+id/tv_order_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="商户订单号:"
android:textSize="16sp"/>
<TextView
android:id="@+id/tv_pay_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="支付状态:未支付"
android:textSize="16sp"
android:textColor="@android:color/black"/>
<Button
android:id="@+id/btn_create_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="生成支付二维码"/>
</LinearLayout>

View File

@@ -7,4 +7,7 @@
<item
android:id="@+id/item_settings"
android:title="Settings"/>
<item
android:id="@+id/item_wxpayactivity"
android:title="WXPayActivity"/>
</menu>