Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae5fba531e | |||
| b59c274032 | |||
| 0c86530ca6 | |||
| 9431dbb930 | |||
| f83295f64f | |||
| a71dd7d399 | |||
| 3d48cd39fc | |||
| 8f7fd504e8 | |||
| b20f53d06e | |||
| 0a174f531f | |||
| 7d3b83b522 | |||
| 0ed6dbe6c2 | |||
| 77c6776cf8 | |||
| f134b7c746 | |||
| 795593b3bf | |||
| cda173fa9e | |||
| fe57aee469 | |||
| 3fc0883e0e | |||
| b162eb1a4e | |||
| fdd8b64718 | |||
| 8d4fd95ff2 | |||
| 3b31a6a155 | |||
| 9eb873332c | |||
| 1b07a72236 | |||
| 8328e44b86 | |||
| 981a8dd8cd | |||
| 6bc3cbe446 | |||
| 79e42e72af | |||
| b59076d470 | |||
| 30fbf4f97e | |||
| bc882c5f80 | |||
| 8ae36effdd | |||
| 0cc9b0871e | |||
| d13c44c666 | |||
| f786d06682 | |||
| 168d2e2d60 | |||
| f303a77e28 | |||
| 04b5d062aa | |||
| fce2bf2fb1 | |||
| 48a592cbd2 | |||
| 5fcf0de951 | |||
| f027158d07 | |||
| 852f3d6cd8 | |||
| c2cff1a2bb | |||
| 98dd2a60b8 | |||
| 25e966099f | |||
| 0a69b54dd0 | |||
| e87513b179 | |||
| 186b1656a7 | |||
| 4ce22e8750 | |||
| 5c8505dcbd | |||
| 9c3963822e | |||
| 30532f8e8d | |||
| 2b98149e0f | |||
| 80d9b764ee | |||
| d4f24a9b0c | |||
| f357e353ee | |||
| 937f90aa07 | |||
| 643e91bd71 | |||
| f4c6cea623 | |||
| a0309ce98a | |||
| 931671d85a | |||
| 580fa87773 | |||
| a26276e169 | |||
| a3bf1087aa | |||
| 1e5295df95 | |||
| 8cb9ea5cae | |||
| 8006a05749 | |||
| 3c9a6ec884 | |||
| 8b47043053 | |||
| 202eeed72b | |||
| 4511bf8730 |
@@ -94,8 +94,8 @@ lint-results.html
|
||||
## 忽略 AndroidIDE 临时文件夹
|
||||
.androidide
|
||||
|
||||
## MyWinBoLL 基础应用(避免上传敏感配置)
|
||||
## WinBoLL 基础应用(避免上传敏感配置)
|
||||
/winboll.properties
|
||||
/local.properties
|
||||
#/settings.gradle
|
||||
#/gradle.properties
|
||||
#/gradle.properties
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/system/bin/sh
|
||||
## 合并其他项目分支的模块源码到projects_keeper分支。
|
||||
## 合并其他项目分支的模块源码到projects-keeper分支。
|
||||
|
||||
# ====================== 0. 进入目标目录 ======================
|
||||
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper"
|
||||
@@ -36,7 +36,7 @@ fi
|
||||
|
||||
# ====================== 3. Git 分支检查 ======================
|
||||
CUR_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
TARGET_BRANCH="projects_keeper"
|
||||
TARGET_BRANCH="projects-keeper"
|
||||
|
||||
if [ "$CUR_BRANCH" != "$TARGET_BRANCH" ]; then
|
||||
echo "错误:当前不在 $TARGET_BRANCH 分支!"
|
||||
@@ -72,6 +72,7 @@ libaes
|
||||
libappbase
|
||||
libdebugtemp
|
||||
libgpsrelaysentinel
|
||||
libwinboll
|
||||
local.properties-demo
|
||||
mymessagemanager
|
||||
positions
|
||||
@@ -81,14 +82,13 @@ winboll
|
||||
winboll.properties-demo
|
||||
)
|
||||
|
||||
# ====================== 5. 获取当前目录真实文件列表(兼容过滤 . ..) ======================
|
||||
# ====================== 5. 获取当前目录真实文件列表 ======================
|
||||
REAL_ITEMS=()
|
||||
# 使用固定排序ls,自动过滤 . 和 ..,不会进入比对数组
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" != "." && "$line" != ".." ]]; then
|
||||
REAL_ITEMS+=("$line")
|
||||
fi
|
||||
done < <(LC_COLLATE=C ls -a1 --color=none)
|
||||
done < <(ls -a)
|
||||
|
||||
# ====================== 6. 差异比对函数 ======================
|
||||
check_diff() {
|
||||
@@ -158,7 +158,7 @@ echo -e "## 对象列表结束
|
||||
|
||||
## 合并 APP 项目
|
||||
MERGE_APP_PROJECT_LIST=(
|
||||
WinBoLL
|
||||
DemoAPP
|
||||
)
|
||||
echo -e "#@@@ 开始合并应用型模块源码 @@@#
|
||||
## 目标合并对象列表:"
|
||||
@@ -166,13 +166,14 @@ echo -e "#@@@ 开始合并应用型模块源码 @@@#
|
||||
for item in "${MERGE_APP_PROJECT_LIST[@]}"; do
|
||||
echo "正在合并 $item 项目 ..."
|
||||
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
|
||||
git checkout origin/$item_lower $item_lower
|
||||
git checkout origin/$item_lower $item_lower
|
||||
git add .
|
||||
git commit -m "合并 $item 项目"
|
||||
done
|
||||
|
||||
## 合并 LIB 项目
|
||||
MERGE_LIB_PROJECT_LIST=(
|
||||
WinBoLL
|
||||
APPBase
|
||||
AES
|
||||
)
|
||||
@@ -182,10 +183,10 @@ echo -e "#@@@ 开始合并类库型模块源码 @@@#
|
||||
for item in "${MERGE_LIB_PROJECT_LIST[@]}"; do
|
||||
echo "正在合并 $item 项目 ..."
|
||||
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
|
||||
git checkout origin/$item_lower $item_lower lib$item_lower
|
||||
git checkout origin/$item_lower $item_lower lib$item_lower
|
||||
git add .
|
||||
git commit -m "合并 $item 项目"
|
||||
done
|
||||
|
||||
echo '正在推送 Projects_Keeper 项目'
|
||||
git push
|
||||
git push
|
||||
@@ -38,7 +38,7 @@ if [ "${NOW_BRANCH}" != "${TARGET_BRANCH}" ];then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 目录结构校验白名单(不含 . ..)
|
||||
# 目录结构校验
|
||||
MERGE_OBJECTS_LIST=(
|
||||
.git
|
||||
.gitignore
|
||||
@@ -65,6 +65,7 @@ libaes
|
||||
libappbase
|
||||
libdebugtemp
|
||||
libgpsrelaysentinel
|
||||
libwinboll
|
||||
local.properties-demo
|
||||
mymessagemanager
|
||||
positions
|
||||
@@ -75,13 +76,9 @@ winboll.properties-demo
|
||||
)
|
||||
|
||||
REAL_ITEMS=()
|
||||
# 标准排序ls输出,循环强制过滤 . 和 ..
|
||||
while IFS= read -r line; do
|
||||
# 跳过虚拟目录 . 和 ..
|
||||
if [[ "$line" != "." && "$line" != ".." ]]; then
|
||||
REAL_ITEMS+=("$line")
|
||||
fi
|
||||
done < <(LC_COLLATE=C ls -a1 --color=none)
|
||||
[[ $line != "." && $line != ".." ]] && REAL_ITEMS+=("$line")
|
||||
done < <(ls -a)
|
||||
|
||||
check_diff(){
|
||||
local miss=() extra=()
|
||||
@@ -92,17 +89,7 @@ check_diff(){
|
||||
[[ ! " ${MERGE_OBJECTS_LIST[@]} " =~ " ${i} " ]] && extra+=("$i")
|
||||
done
|
||||
if [[ ${#miss[@]} -gt 0 || ${#extra[@]} -gt 0 ]];then
|
||||
echo "========================================"
|
||||
echo "本地目录结构不一致,终止运行"
|
||||
if [[ ${#miss[@]} -gt 0 ]]; then
|
||||
echo -e "\n缺失条目:"
|
||||
for m in "${miss[@]}"; do echo " $m"; done
|
||||
fi
|
||||
if [[ ${#extra[@]} -gt 0 ]]; then
|
||||
echo -e "\n多余条目:"
|
||||
for e in "${extra[@]}"; do echo " $e"; done
|
||||
fi
|
||||
echo "========================================"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -111,7 +98,7 @@ check_diff
|
||||
echo -e "#@@@ 按时间获取最新标签合并模块源码 @@@#"
|
||||
|
||||
# 应用型模块
|
||||
MERGE_APP_PROJECT_LIST=(WinBoLL)
|
||||
MERGE_APP_PROJECT_LIST=(DemoAPP)
|
||||
echo -e "---------- 应用型模块 ----------"
|
||||
for name in "${MERGE_APP_PROJECT_LIST[@]}";do
|
||||
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
|
||||
@@ -133,7 +120,7 @@ for name in "${MERGE_APP_PROJECT_LIST[@]}";do
|
||||
done
|
||||
|
||||
# 类库模块
|
||||
MERGE_LIB_PROJECT_LIST=(APPBase AES)
|
||||
MERGE_LIB_PROJECT_LIST=(WinBoLL APPBase AES)
|
||||
echo -e "---------- 类库模块 ----------"
|
||||
for name in "${MERGE_LIB_PROJECT_LIST[@]}";do
|
||||
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# ==============================================================================
|
||||
# WinBoLL 应用发布脚本
|
||||
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
|
||||
# 依赖:build.properties、app_update_description.txt(项目根目录下)
|
||||
# 使用:./script_name.sh <APP_NAME>
|
||||
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
# ==============================================================================
|
||||
|
||||
# ==================== 常量定义 ====================
|
||||
# 脚本退出码
|
||||
EXIT_CODE_SUCCESS=0
|
||||
EXIT_CODE_ERR_NO_APP_NAME=2
|
||||
EXIT_CODE_ERR_WORK_DIR=1
|
||||
EXIT_CODE_ERR_GIT_CHECK=1
|
||||
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
|
||||
|
||||
# Gradle 任务(正式发布)
|
||||
GRADLE_TASK_PUBLISH="assembleStageRelease"
|
||||
# Gradle 任务(调试用,注释备用)
|
||||
# GRADLE_TASK_DEBUG="assembleBetaDebug"
|
||||
|
||||
# aapt2本地覆盖参数
|
||||
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
|
||||
# 禁用Gradle守护进程
|
||||
GRADLE_NO_DAEMON="--no-daemon"
|
||||
|
||||
# ==================== 函数定义 ====================
|
||||
# 检查Git源码是否已完全提交(无未提交变更)
|
||||
# 返回值:0=已完全提交,1=存在未提交变更
|
||||
function checkGitSources() {
|
||||
# 配置Git安全目录(解决权限问题)
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
|
||||
# 检查是否有未提交的变更
|
||||
if [[ -n $(git diff --stat) ]]; then
|
||||
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[INFO] Git源码检查通过:所有变更已提交。"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||
# 返回值:1=用户选择是,0=用户选择否
|
||||
function askAddWorkflowsTag() {
|
||||
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
|
||||
if [[ $answer =~ ^[Yy]$ ]]; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 添加WinBoLL正式标签
|
||||
# 参数:$1=应用名称(项目根目录名)
|
||||
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||
function addWinBoLLTag() {
|
||||
local app_name=$1
|
||||
local build_prop_path="${app_name}/build.properties"
|
||||
|
||||
# 从build.properties中提取publishVersion
|
||||
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||
if [[ -z ${publish_version} ]]; then
|
||||
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
|
||||
return 1
|
||||
fi
|
||||
echo "[INFO] 从${build_prop_path}读取到publishVersion:${publish_version}"
|
||||
|
||||
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>)
|
||||
local tag="${app_name}-v${publish_version}"
|
||||
echo "[INFO] 准备添加WinBoLL标签:${tag}"
|
||||
|
||||
# 检查标签是否已存在
|
||||
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||
echo "[ERROR] WinBoLL标签${tag}已存在!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||
echo "[INFO] WinBoLL标签${tag}添加成功!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
|
||||
# 参数:$1=应用名称(项目根目录名)
|
||||
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||
function addWorkflowsTag() {
|
||||
local app_name=$1
|
||||
local build_prop_path="${app_name}/build.properties"
|
||||
|
||||
# 从build.properties中提取baseBetaVersion
|
||||
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||
if [[ -z ${base_beta_version} ]]; then
|
||||
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
|
||||
return 1
|
||||
fi
|
||||
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion:${base_beta_version}"
|
||||
|
||||
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta)
|
||||
local tag="${app_name}-v${base_beta_version}-beta"
|
||||
echo "[INFO] 准备添加Workflows标签:${tag}"
|
||||
|
||||
# 检查标签是否已存在
|
||||
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||
echo "[ERROR] Workflows标签${tag}已存在!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||
echo "[INFO] Workflows标签${tag}添加成功!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==================== 主流程开始 ====================
|
||||
echo "============================================="
|
||||
echo " WinBoLL 应用发布脚本"
|
||||
echo "============================================="
|
||||
|
||||
# 1. 检查应用名称参数是否指定
|
||||
if [ -z "$1" ]; then
|
||||
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
|
||||
exit ${EXIT_CODE_ERR_NO_APP_NAME}
|
||||
fi
|
||||
APP_NAME=$1
|
||||
echo "[INFO] 待发布应用名称:${APP_NAME}"
|
||||
|
||||
# 2. 检查并切换到项目根目录(确保build.properties存在)
|
||||
echo "[INFO] 当前工作目录:$(pwd)"
|
||||
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
|
||||
cd ..
|
||||
echo "[INFO] 切换后工作目录:$(pwd)"
|
||||
fi
|
||||
|
||||
# 验证最终工作目录是否正确
|
||||
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
|
||||
exit ${EXIT_CODE_ERR_WORK_DIR}
|
||||
fi
|
||||
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
|
||||
|
||||
# 3. 检查Git源码状态
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤1:检查Git源码状态"
|
||||
echo "---------------------------------------------"
|
||||
checkGitSources
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] Git源码检查失败,脚本终止!"
|
||||
exit ${EXIT_CODE_ERR_GIT_CHECK}
|
||||
fi
|
||||
|
||||
# 4. 编译Stage Release版本APK(携带aapt2覆盖参数 + --no-daemon)
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤2:编译Stage Release APK"
|
||||
echo "---------------------------------------------"
|
||||
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
|
||||
# 调试用(注释正式任务,启用调试任务)
|
||||
# bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_DEBUG}
|
||||
bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_PUBLISH}
|
||||
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] Gradle编译任务失败!"
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] Stage Release APK编译成功!"
|
||||
|
||||
# 5. 添加WinBoLL正式标签
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤3:添加WinBoLL标签"
|
||||
echo "---------------------------------------------"
|
||||
addWinBoLLTag ${APP_NAME}
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
|
||||
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
|
||||
fi
|
||||
|
||||
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||
# echo "---------------------------------------------"
|
||||
# echo " 步骤4:添加Workflows标签(可选)"
|
||||
# echo "---------------------------------------------"
|
||||
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
|
||||
# askAddWorkflowsTag
|
||||
# nAskAddWorkflowsTag=$?
|
||||
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
|
||||
# addWorkflowsTag ${APP_NAME}
|
||||
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
|
||||
# exit 1
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# 7. 清理更新描述文件
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤5:清理更新描述文件"
|
||||
echo "---------------------------------------------"
|
||||
echo "" > "${APP_NAME}/app_update_description.txt"
|
||||
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
|
||||
|
||||
# 8. 提交并推送源码与标签
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤6:提交并推送源码"
|
||||
echo "---------------------------------------------"
|
||||
git add .
|
||||
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
|
||||
echo "[INFO] 源码提交成功,开始推送..."
|
||||
|
||||
# 推送源码到远程仓库
|
||||
git push origin
|
||||
# 推送标签到远程仓库
|
||||
git push origin --tags
|
||||
|
||||
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[INFO] 源码与标签推送成功!"
|
||||
else
|
||||
echo "[ERROR] 源码与标签推送失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==================== 主流程结束 ====================
|
||||
echo "============================================="
|
||||
echo " WinBoLL 应用发布完成!"
|
||||
echo "============================================="
|
||||
exit ${EXIT_CODE_SUCCESS}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
# aapt2本地覆盖参数
|
||||
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
|
||||
# Gradle禁用守护进程参数
|
||||
GRADLE_NO_DAEMON="--no-daemon"
|
||||
|
||||
# 检查是否指定了将要发布的类库名称
|
||||
# 使用 `-z` 命令检查变量是否为空
|
||||
if [ -z "$1" ]; then
|
||||
echo "No Library name specified : $0"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
## 正式发布使用
|
||||
git pull && bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishReleasePublicationToWinBoLLReleaseRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
|
||||
|
||||
## 调试使用
|
||||
#bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishSnapshotWinBoLLPublicationToWinBoLLSnapshotRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
|
||||
|
||||
@@ -122,6 +122,7 @@ android {
|
||||
// 如果正在调试,就拷贝到 WinBoLL 备份管理文件夹
|
||||
//
|
||||
if(variant.flavorName == "beta"&&variant.buildType.name == "debug"){
|
||||
//File outBuildBckDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/${variant.buildType.name}")
|
||||
File outBuildBckDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}")
|
||||
// 创建目标路径目录
|
||||
if(!outBuildBckDir.exists()) {
|
||||
@@ -129,7 +130,6 @@ android {
|
||||
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
|
||||
}
|
||||
if(outBuildBckDir.exists()) {
|
||||
def targetApkFile = new File(outBuildBckDir, outputFileName)
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outBuildBckDir
|
||||
@@ -138,14 +138,6 @@ android {
|
||||
}
|
||||
println "Output APK (WinBoLLStudio): " + outBuildBckDir.getAbsolutePath() + "/${outputFileName}"
|
||||
}
|
||||
// ========== 设置文件权限为775 ==========
|
||||
if(targetApkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', targetApkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${targetApkFile.absolutePath}"
|
||||
}
|
||||
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['buildCount'] != null)
|
||||
assert (winbollBuildProps['libraryProject'] != null)
|
||||
@@ -168,7 +160,8 @@ android {
|
||||
assert(libraryProjectBuildPropsFile.exists())
|
||||
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
|
||||
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
|
||||
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
|
||||
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
println "\n\n>>> Library Project build.properties saved.\n\n";
|
||||
}
|
||||
@@ -179,12 +172,16 @@ android {
|
||||
//
|
||||
if(variant.flavorName == "stage"&&variant.buildType.name == "release"){
|
||||
// 发布 APK 文件
|
||||
//
|
||||
// 截取版本号的版本字段为短版本名
|
||||
String szVersionName = "${versionName}"
|
||||
String[] szlistTemp = szVersionName.split("-")
|
||||
String szShortVersionName = szlistTemp[0]
|
||||
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
|
||||
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
|
||||
println "CommonTagAPKName is : " + szCommonTagAPKName
|
||||
|
||||
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
|
||||
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/tag/")
|
||||
// 创建目标路径目录
|
||||
if(!outTagDir.exists()) {
|
||||
@@ -195,10 +192,12 @@ android {
|
||||
if(outTagDir.exists()) {
|
||||
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
|
||||
if(targetAPK.exists()) {
|
||||
// 标签版本APK文件已经存在,构建拷贝任务停止
|
||||
assert (!targetAPK.exists())
|
||||
// 可选择删除并继续输出APK文件
|
||||
//delete targetAPK
|
||||
}
|
||||
// 复制完整版APK
|
||||
def fullApkFile = new File(outTagDir, outputFileName)
|
||||
// 复制一个备份
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -207,16 +206,7 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
|
||||
}
|
||||
// 设置权限775。
|
||||
if(fullApkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', fullApkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${fullApkFile.absolutePath}"
|
||||
}
|
||||
|
||||
// 复制短版本名APK
|
||||
def shortApkFile = new File(outTagDir, szCommonTagAPKName)
|
||||
// 复制一个并重命名为短版本名
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -225,14 +215,6 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
|
||||
}
|
||||
// 设置权限775。
|
||||
if(shortApkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', shortApkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${shortApkFile.absolutePath}"
|
||||
}
|
||||
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['stageCount'] != null)
|
||||
assert (winbollBuildProps['publishVersion'] != null)
|
||||
@@ -257,11 +239,14 @@ android {
|
||||
fos.close();
|
||||
|
||||
if(winbollBuildProps['libraryProject'] != "") {
|
||||
// 如果应用 build.properties 文件设置了类库模块项目文件名
|
||||
// 就拷贝一份新的编译标志配置到类库项目文件夹
|
||||
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
|
||||
assert(winbollBuildPropsFile.exists())
|
||||
assert(libraryProjectBuildPropsFile.exists())
|
||||
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
|
||||
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
|
||||
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
|
||||
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
@@ -278,12 +263,17 @@ android {
|
||||
// 如果正在调试发布版,就只生成和输出APK文件,不处理 Git 仓库提交与更新问题。
|
||||
//
|
||||
if(variant.flavorName == "stage"&&variant.buildType.name == "debug"){
|
||||
// 发布 APK 文件
|
||||
//
|
||||
// 截取版本号的版本字段为短版本名
|
||||
String szVersionName = "${versionName}"
|
||||
String[] szlistTemp = szVersionName.split("-")
|
||||
String szShortVersionName = szlistTemp[0]
|
||||
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
|
||||
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
|
||||
println "CommonTagAPKName is : " + szCommonTagAPKName
|
||||
|
||||
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
|
||||
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}/")
|
||||
// 创建目标路径目录
|
||||
if(!outTagDir.exists()) {
|
||||
@@ -294,11 +284,13 @@ android {
|
||||
if(outTagDir.exists()) {
|
||||
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
|
||||
if(targetAPK.exists()) {
|
||||
// 标签版本APK文件已经存在,构建拷贝任务停止
|
||||
println '如果是在调试 Stage 版应用包构建,请删除(注:在debug目录)现有的 Stage 应用包('+targetAPK.getAbsolutePath()+')。再编译一次。'
|
||||
assert (!targetAPK.exists())
|
||||
// 可选择删除并继续输出APK文件
|
||||
//delete targetAPK
|
||||
}
|
||||
// 复制完整版APK
|
||||
def debugFullApk = new File(outTagDir, outputFileName)
|
||||
// 复制一个备份
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -307,16 +299,7 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
|
||||
}
|
||||
// 权限设为775。
|
||||
if(debugFullApk.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', debugFullApk.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${debugFullApk.absolutePath}"
|
||||
}
|
||||
|
||||
// 复制短版本名APK
|
||||
def debugShortApk = new File(outTagDir, szCommonTagAPKName)
|
||||
// 复制一个并重命名为短版本名
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -325,13 +308,8 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
|
||||
}
|
||||
// 权限设为775
|
||||
if(debugShortApk.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', debugShortApk.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${debugShortApk.absolutePath}"
|
||||
}
|
||||
|
||||
//不保存编译标志配置
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,13 +328,6 @@ android {
|
||||
}
|
||||
println "Output APK (Common): " + outCommonDir.getAbsolutePath() + "/${commandAPKName}"
|
||||
}
|
||||
// 额外输出文件设置775权限
|
||||
if(apkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', apkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${apkFile.absolutePath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
15.20
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,97 +0,0 @@
|
||||
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
|
||||
|
||||
# LICENSE-Private-Demo
|
||||
|
||||
# WinBoLL 源码公共转私有继承开发规范守则
|
||||
|
||||
## 核心声明
|
||||
|
||||
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
|
||||
## 一、文件宗旨与风险防控说明
|
||||
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
|
||||
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||
|
||||
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
|
||||
|
||||
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
|
||||
|
||||
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
|
||||
|
||||
## 二、公私分支授权标识文件定义(风控核心依据)
|
||||
|
||||
### 1. 公共开源分支唯一标识
|
||||
|
||||
**文件名:LICENSE**
|
||||
|
||||
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||
|
||||
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
|
||||
|
||||
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
|
||||
|
||||
### 2. 私有开发分支唯一标识
|
||||
|
||||
**文件名:LICENSE-Private**
|
||||
|
||||
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||
|
||||
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
|
||||
|
||||
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
|
||||
|
||||
## 三、分支管理与合并风控规则(强制遵守)
|
||||
|
||||
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
|
||||
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
|
||||
3. **核心合并风控铁则**
|
||||
|
||||
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
|
||||
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
|
||||
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||
|
||||
## 四、公共转私有标准化操作步骤(锁死合并风险)
|
||||
|
||||
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
|
||||
|
||||
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
|
||||
|
||||
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
|
||||
|
||||
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
|
||||
|
||||
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
|
||||
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||
>
|
||||
>
|
||||
|
||||
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
|
||||
|
||||
## 五、人为操作责任界定(核心补充条款)
|
||||
|
||||
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
|
||||
|
||||
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
|
||||
|
||||
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||
|
||||
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
|
||||
## 六、分支状态校验与异常处理
|
||||
|
||||
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
|
||||
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
|
||||
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
## 核心声明
|
||||
|
||||
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
|
||||
## 一、文件宗旨与风险防控说明
|
||||
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
|
||||
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
### 1. 公共开源分支唯一标识
|
||||
|
||||
**文件名:LICENSE**
|
||||
**文件名: LICENSE**
|
||||
|
||||
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
### 2. 私有开发分支唯一标识
|
||||
|
||||
**文件名:LICENSE-Private**
|
||||
**文件名: LICENSE-Private**
|
||||
|
||||
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
|
||||
## 三、分支管理与合并风控规则(强制遵守)
|
||||
|
||||
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
|
||||
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
|
||||
3. **核心合并风控铁则**
|
||||
|
||||
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
|
||||
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
|
||||
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
|
||||
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||
>
|
||||
@@ -84,14 +84,14 @@
|
||||
|
||||
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||
|
||||
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
|
||||
## 六、分支状态校验与异常处理
|
||||
|
||||
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
|
||||
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
|
||||
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
> (注: 文档部分内容可能由 AI 生成)
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
## 核心声明
|
||||
|
||||
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
|
||||
## 一、文件宗旨与风险防控说明
|
||||
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
|
||||
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
### 1. 公共开源分支唯一标识
|
||||
|
||||
**文件名:LICENSE**
|
||||
**文件名: LICENSE**
|
||||
|
||||
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
### 2. 私有开发分支唯一标识
|
||||
|
||||
**文件名:LICENSE-Private**
|
||||
**文件名: LICENSE-Private**
|
||||
|
||||
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
|
||||
## 三、分支管理与合并风控规则(强制遵守)
|
||||
|
||||
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
|
||||
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
|
||||
3. **核心合并风控铁则**
|
||||
|
||||
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
|
||||
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
|
||||
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
|
||||
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||
>
|
||||
@@ -84,14 +84,14 @@
|
||||
|
||||
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||
|
||||
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
|
||||
## 六、分支状态校验与异常处理
|
||||
|
||||
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
|
||||
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
|
||||
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
> (注: 文档部分内容可能由 AI 生成)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Jun 28 20:55:33 HKT 2026
|
||||
stageCount=35
|
||||
#Wed May 27 14:51:29 HKT 2026
|
||||
stageCount=23
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.34
|
||||
publishVersion=15.20.22
|
||||
buildCount=0
|
||||
baseBetaVersion=15.20.35
|
||||
baseBetaVersion=15.20.23
|
||||
|
||||
@@ -37,7 +37,7 @@ public class AboutActivity extends Activity {
|
||||
appInfo.setAppName("APPBase");
|
||||
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package cc.winboll.studio.appbase;
|
||||
|
||||
import cc.winboll.studio.libappbase.CrashActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import cc.winboll.studio.libappbase.BuildConfig;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -24,24 +21,10 @@ public class App extends GlobalApplication {
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
try {
|
||||
super.onCreate();
|
||||
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
} catch (Throwable e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
String stackTraceStr = sw.toString();
|
||||
CrashHandleNotifyUtils.handleUncaughtException(
|
||||
this,
|
||||
getPackageName(),
|
||||
stackTraceStr,
|
||||
CrashActivity.class
|
||||
);
|
||||
}
|
||||
super.onCreate();
|
||||
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,491 +0,0 @@
|
||||
package cc.winboll.studio.libaes.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import cc.winboll.studio.libaes.R;
|
||||
import cc.winboll.studio.libaes.enums.ADsMode;
|
||||
import cc.winboll.studio.libaes.utils.MimoUtils;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import com.miui.zeus.mimo.sdk.ADParams;
|
||||
import com.miui.zeus.mimo.sdk.BannerAd;
|
||||
import com.miui.zeus.mimo.sdk.MimoCustomController;
|
||||
import com.miui.zeus.mimo.sdk.MimoLocation;
|
||||
import com.miui.zeus.mimo.sdk.MimoSdk;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/18 14:41
|
||||
* @Describe WinBoLL 横幅广告类
|
||||
*/
|
||||
public class ADsBannerView extends LinearLayout {
|
||||
|
||||
public static final String TAG = "ADsBannerView";
|
||||
|
||||
|
||||
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
|
||||
private String BANNER_POS_ID_WINBOLL_BETA = "d129ee5a263911f981a6dc7a9802e3e7";
|
||||
private String BANNER_POS_ID_WINBOLL = "4ec30efdb32271765b9a4efac902828b";
|
||||
|
||||
/*
|
||||
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
|
||||
private String BANNER_POS_ID_WINBOLL_BETA = "802e356f1726f9ff39c69308bfd6f06a";
|
||||
private String BANNER_POS_ID_WINBOLL = "802e356f1726f9ff39c69308bfd6f06a";
|
||||
*/
|
||||
|
||||
Context mContext;
|
||||
View mMianView;
|
||||
SharedPreferences mSharedPreferences;
|
||||
ViewGroup mContainer;
|
||||
BannerAd mBannerAd;
|
||||
List<BannerAd> mAllBanners = new ArrayList<>();
|
||||
// 新增:主线程Handler,确保广告操作在主线程执行
|
||||
private Handler mMainHandler;
|
||||
|
||||
public ADsBannerView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public ADsBannerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
void initView(Context context) {
|
||||
this.mContext = context;
|
||||
initMimoSdk(this.mContext);
|
||||
|
||||
// 初始化主线程Handler(关键:确保广告操作在主线程执行)
|
||||
mMainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
this.mMianView = inflate(this.mContext, R.layout.view_adsbanner, null);
|
||||
mContainer = this.mMianView.findViewById(R.id.ads_container);
|
||||
addView(this.mMianView);
|
||||
}
|
||||
|
||||
public void resumeADs(final Activity activity) {
|
||||
// 没有设置米盟广告支持就退出
|
||||
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
|
||||
// 2. 释放之前的广告资源
|
||||
if (mBannerAd != null) {
|
||||
mBannerAd.destroy();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:优化广告请求逻辑(添加生命周期判断 + 主线程执行)
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||||
if (ADsControlView.getAdsModeFromStatic(this.mContext) == ADsMode.MIMO_SDK) {
|
||||
LogUtils.i(TAG, "已设置播放米盟广告,正在播放...");
|
||||
mMainHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 再次校验生命周期,避免延迟执行时Activity已销毁
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||||
fetchAd(activity);
|
||||
}
|
||||
}
|
||||
}, 1000); // 延迟1秒请求广告,提升页面加载体验
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放广告资源(关键:避免内存泄漏和空Context调用)
|
||||
*/
|
||||
public void releaseAdResources() {
|
||||
// 没有设置米盟广告支持就退出
|
||||
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "releaseAdResources()");
|
||||
|
||||
// 移除Handler回调
|
||||
if (mMainHandler != null) {
|
||||
mMainHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
// 销毁所有广告实例
|
||||
if (mAllBanners != null && !mAllBanners.isEmpty()) {
|
||||
for (BannerAd ad : mAllBanners) {
|
||||
if (ad != null) {
|
||||
ad.destroy();
|
||||
}
|
||||
}
|
||||
mAllBanners.clear();
|
||||
}
|
||||
// 置空当前广告引用
|
||||
mBannerAd = null;
|
||||
// 移除广告容器中的视图
|
||||
if (mContainer != null) {
|
||||
mContainer.removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示广告(核心修复:传递安全的Context + 生命周期校验)
|
||||
*/
|
||||
private void showAd(final Activity activity) {
|
||||
// 没有设置米盟广告支持就退出
|
||||
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "showAd()");
|
||||
// 1. 生命周期校验:避免Activity已销毁时操作UI
|
||||
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
|
||||
LogUtils.e(TAG, "showAd: Activity is finishing or destroyed");
|
||||
return;
|
||||
}
|
||||
// 2. 非空校验:广告实例和容器
|
||||
if (mBannerAd == null || mContainer == null) {
|
||||
LogUtils.e(TAG, "showAd: BannerAd or Container is null");
|
||||
return;
|
||||
}
|
||||
// 3. 创建广告容器(使用ApplicationContext避免内存泄漏)
|
||||
final FrameLayout container = new FrameLayout(activity.getApplicationContext());
|
||||
container.setPadding(0, 0, 0, MimoUtils.dpToPx(activity, 10));
|
||||
mContainer.addView(container, new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
// if (mIsBiddingWin) {
|
||||
// mBannerAd.setPrice(getPrice());
|
||||
// }
|
||||
// 4. 显示广告:传递ApplicationContext,避免Activity Context失效
|
||||
mBannerAd.showAd(activity, container, new BannerAd.BannerInteractionListener() {
|
||||
@Override
|
||||
public void onAdClick() {
|
||||
LogUtils.d(TAG, "onAdClick");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdShow() {
|
||||
LogUtils.d(TAG, "onAdShow");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdDismiss() {
|
||||
LogUtils.d(TAG, "onAdDismiss");
|
||||
// 修复:移除容器时校验Activity状态
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
|
||||
mContainer.removeView(container);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderSuccess() {
|
||||
LogUtils.d(TAG, "onRenderSuccess");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderFail(int code, String msg) {
|
||||
LogUtils.e(TAG, "onRenderFail errorCode " + code + " errorMsg " + msg);
|
||||
// 修复:渲染失败时移除容器
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
|
||||
mContainer.removeView(container);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求广告(核心修复:Context安全校验 + 异常捕获 + 资源管理)
|
||||
*/
|
||||
private void fetchAd(final Activity activity) {
|
||||
// 没有设置米盟广告支持就退出
|
||||
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "fetchAd()");
|
||||
// 1. 双重校验:Activity未销毁 + Context非空
|
||||
if (activity == null || activity.isFinishing() || activity.isDestroyed() || activity.getApplicationContext() == null) {
|
||||
LogUtils.e(TAG, "fetchAd: Invalid Context or Activity state");
|
||||
return;
|
||||
}
|
||||
// 2. 释放之前的广告资源,避免内存泄漏
|
||||
if (mBannerAd != null) {
|
||||
mBannerAd.destroy();
|
||||
}
|
||||
// 3. 初始化广告(使用ApplicationContext,避免Activity Context失效)
|
||||
try {
|
||||
mBannerAd = new BannerAd();
|
||||
mAllBanners.add(mBannerAd);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "fetchAd: Init BannerAd failed", e);
|
||||
return;
|
||||
}
|
||||
// 4. 设置下载监听
|
||||
mBannerAd.setDownLoadListener(new BannerAd.BannerDownloadListener() {
|
||||
@Override
|
||||
public void onDownloadStarted() {
|
||||
LogUtils.d(TAG, "onDownloadStarted");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadPaused() {
|
||||
LogUtils.d(TAG, "onDownloadPaused");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadFailed(int errorCode) {
|
||||
String msg = "onDownloadFailed, errorCode = " + errorCode;
|
||||
LogUtils.d(TAG, msg);
|
||||
//ToastUtils.show(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadFinished() {
|
||||
LogUtils.d(TAG, "onDownloadFinished");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgressUpdated(int progress) {
|
||||
LogUtils.d(TAG, "onDownloadProgressUpdated " + progress + "%");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallFailed(int errorCode) {
|
||||
LogUtils.d(TAG, "onInstallFailed, errorCode = " + errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallStart() {
|
||||
LogUtils.d(TAG, "onInstallStart");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallSuccess() {
|
||||
LogUtils.d(TAG, "onInstallSuccess");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadCancel() {
|
||||
LogUtils.d(TAG, "onDownloadCancel");
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 构建广告参数并请求
|
||||
String currentAD_ID = getAD_ID();
|
||||
LogUtils.d(TAG, String.format("currentAD_ID = %s", currentAD_ID));
|
||||
ADParams params = new ADParams.Builder().setUpId(currentAD_ID).build();
|
||||
mBannerAd.loadAd(params, new BannerAd.BannerLoadListener() {
|
||||
@Override
|
||||
public void onBannerAdLoadSuccess() {
|
||||
LogUtils.d(TAG, "onBannerAdLoadSuccess()");
|
||||
// 修复:广告加载成功后校验Activity状态
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||||
showAd(activity);
|
||||
//ToastUtils.show("showAd()");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdLoadFailed(int errorCode, String errorMsg) {
|
||||
String msg = "onAdLoadFailed: errorCode = " + errorCode + ", errorMsg = " + errorMsg;
|
||||
LogUtils.d(TAG, msg);
|
||||
removeAllBanners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void removeAllBanners() {
|
||||
// 没有设置米盟广告支持就退出
|
||||
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:加载失败时移除当前广告实例
|
||||
if (mAllBanners.contains(mBannerAd)) {
|
||||
mAllBanners.remove(mBannerAd);
|
||||
}
|
||||
mBannerAd.destroy();
|
||||
mBannerAd = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前秒数获取广告ID(原逻辑保留)
|
||||
*/
|
||||
private String getAD_ID() {
|
||||
long currentSecond = System.currentTimeMillis() / 1000;
|
||||
return (currentSecond % 2 == 0) ? BANNER_POS_ID :
|
||||
(GlobalApplication.isDebugging() ? BANNER_POS_ID_WINBOLL_BETA : BANNER_POS_ID_WINBOLL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广告价格(原逻辑保留,添加空指针校验)
|
||||
*/
|
||||
// private long getPrice() {
|
||||
// if (mBannerAd == null) {
|
||||
// return 0;
|
||||
// }
|
||||
// Map<String, Object> map = mBannerAd.getMediaExtraInfo();
|
||||
// if (map == null || map.isEmpty() || !map.containsKey("price")) {
|
||||
// LogUtils.w(TAG, "getPrice: media extra info is null or no price key");
|
||||
// return 0;
|
||||
// }
|
||||
// Object priceObj = map.get("price");
|
||||
// if (priceObj instanceof Long) {
|
||||
// return (Long) priceObj;
|
||||
// } else if (priceObj instanceof Integer) {
|
||||
// return ((Integer) priceObj).longValue();
|
||||
// } else {
|
||||
// LogUtils.e(TAG, "getPrice: price type is invalid");
|
||||
// return 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 显示隐私协议弹窗(原逻辑保留,优化Context使用)
|
||||
*/
|
||||
// private void showPrivacy() {
|
||||
// // 校验Activity状态,避免弹窗泄露
|
||||
// if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed()) {
|
||||
// return;
|
||||
// }
|
||||
// ADsMode adsMode = ADsControlView.getAdsModeFromStatic(this.mContext);
|
||||
// if (adsMode == ADsMode.STANDALONE) {
|
||||
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.STANDALONE);
|
||||
// LogUtils.i(TAG, "单机模式,广告已处于不可用状态...");
|
||||
// Toast.makeText(getActivity().getApplicationContext(), "单机模式,广告已处于不可用状态...", Toast.LENGTH_SHORT).show();
|
||||
// return;
|
||||
// } else if (adsMode == ADsMode.MIMO_SDK) {
|
||||
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.MIMO_SDK);
|
||||
// LogUtils.i(TAG, "米盟广告SDK支持模式,现在初始化SDK...");
|
||||
// initMimoSdk();
|
||||
// return;
|
||||
// }
|
||||
// else {
|
||||
// LogUtils.i(TAG, "开始弹出隐私协议...");
|
||||
// AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
// builder.setTitle("用户须知");
|
||||
// builder.setMessage("小米广告SDK隐私政策: https://dev.mi.com/distribute/doc/details?pId=1688, 请复制到浏览器查看");
|
||||
// builder.setIcon(R.drawable.ic_launcher);
|
||||
// builder.setCancelable(false); // 点击对话框以外的区域不消失
|
||||
// builder.setPositiveButton("同意", new DialogInterface.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(DialogInterface dialog, int which) {
|
||||
// getSharedPreferences().edit()
|
||||
// .putString(PRIVACY_VALUE, String.valueOf(1))
|
||||
// .apply();
|
||||
// initMimoSdk();
|
||||
// dialog.dismiss();
|
||||
// }
|
||||
// });
|
||||
// builder.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(DialogInterface dialog, int which) {
|
||||
// getSharedPreferences().edit()
|
||||
// .putString(PRIVACY_VALUE, String.valueOf(0))
|
||||
// .apply();
|
||||
// dialog.dismiss();
|
||||
// }
|
||||
// });
|
||||
// AlertDialog dialog = builder.create();
|
||||
//
|
||||
// // 配置弹窗位置(底部全屏)
|
||||
// Window window = dialog.getWindow();
|
||||
// if (window != null) {
|
||||
// window.setGravity(Gravity.BOTTOM);
|
||||
// WindowManager m = getActivity().getWindowManager();
|
||||
// Display d = m.getDefaultDisplay();
|
||||
// WindowManager.LayoutParams p = window.getAttributes();
|
||||
// p.width = d.getWidth();
|
||||
// window.setAttributes(p);
|
||||
// }
|
||||
// dialog.show();
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 初始化米盟SDK(核心修复:传递ApplicationContext + 异常捕获)
|
||||
*/
|
||||
private void initMimoSdk(Context context) {
|
||||
// 1. 安全获取ApplicationContext,避免Activity Context失效
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
LogUtils.e(TAG, "initMimoSdk: ApplicationContext is null");
|
||||
return;
|
||||
}
|
||||
// 2. 初始化SDK,捕获异常避免崩溃
|
||||
try {
|
||||
MimoSdk.init(appContext, new MimoCustomController() {
|
||||
@Override
|
||||
public boolean isCanUseLocation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MimoLocation getMimoLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCanUseWifiState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean alist() {
|
||||
return true;
|
||||
}
|
||||
}, new MimoSdk.InitCallback() {
|
||||
@Override
|
||||
public void success() {
|
||||
LogUtils.d(TAG, "MimoSdk init success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(int code, String msg) {
|
||||
LogUtils.e(TAG, "MimoSdk init fail, code=" + code + ",msg=" + msg);
|
||||
}
|
||||
});
|
||||
MimoSdk.setDebugOn(true);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "initMimoSdk: init failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取SharedPreferences实例(原逻辑保留,添加空指针校验)
|
||||
*/
|
||||
// SharedPreferences getSharedPreferences() {
|
||||
//// if (mSharedPreferences == null) {
|
||||
//// // 修复:使用ApplicationContext获取SharedPreferences,避免Activity Context泄露
|
||||
//// Context appContext = getActivity().getApplicationContext();
|
||||
//// if (appContext != null) {
|
||||
//// mSharedPreferences = appContext.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
|
||||
//// } else {
|
||||
//// LogUtils.e(TAG, "getSharedPreferences: ApplicationContext is null");
|
||||
//// // 降级方案:若ApplicationContext为空,使用Activity Context(仅作兼容)
|
||||
//// mSharedPreferences = getActivity().getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
|
||||
//// }
|
||||
//// }
|
||||
// return mSharedPreferences;
|
||||
// }
|
||||
}
|
||||
@@ -1,633 +0,0 @@
|
||||
package cc.winboll.studio.libaes.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.text.Html;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import cc.winboll.studio.libaes.R;
|
||||
import cc.winboll.studio.libaes.enums.ADsMode;
|
||||
import cc.winboll.studio.libaes.enums.PrivacyAgreeStatus;
|
||||
import cc.winboll.studio.libaes.utils.WebUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import com.miui.zeus.mimo.sdk.MimoCustomController;
|
||||
import com.miui.zeus.mimo.sdk.MimoLocation;
|
||||
import com.miui.zeus.mimo.sdk.MimoSdk;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/26 17:51
|
||||
* @LastEditTime 2026/01/08 11:00:00 HKT
|
||||
* @Describe 广告模式控制控件(Java 7 兼容,云宝物语模式)
|
||||
* 核心修改:将PopupMenu锚点绑定到view_popmenu_anchor_point控件,菜单精准显示在锚点位置
|
||||
*/
|
||||
public class ADsControlView extends LinearLayout {
|
||||
public static final String TAG = "ADsControlView";
|
||||
|
||||
// SP存储配置
|
||||
private static final String SP_NAME = "ads_control_config";
|
||||
private static final String KEY_SELECTED_MODE = "selected_ads_mode";
|
||||
ADsMode mADsMode;
|
||||
private static final String PRIVACY_VALUE = "privacy_value";
|
||||
PrivacyAgreeStatus mPrivacyAgreeStatus;
|
||||
|
||||
// Handler消息标识
|
||||
private static final int MSG_UPDATE_MODE = 1001;
|
||||
|
||||
// 控件引用
|
||||
private RadioGroup rgADsMode;
|
||||
private RadioButton rbStandalone;
|
||||
private RadioButton rbMimoSDK;
|
||||
private RadioButton rbStoreQrcode;
|
||||
private RelativeLayout rlWinbollStore;
|
||||
private ImageView ivWinbollStoreQrcode;
|
||||
// 新增:锚点控件引用
|
||||
private TextView viewPopmenuAnchorPoint;
|
||||
|
||||
// 外部监听、SP实例、Handler实例
|
||||
private OnAdsModeSelectedListener listener;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private InternalHandler mHandler;
|
||||
private Context mContext;
|
||||
|
||||
// 静态列表:存储所有已创建的控件实例
|
||||
private static final java.util.List<ADsControlView> sControlViews = new java.util.ArrayList<ADsControlView>();
|
||||
|
||||
// 常量定义
|
||||
private static final String WECHAT_STORE_URL = "https://store.weixin.qq.com/shop/b/XhrPkZgoeHo4zug";
|
||||
private static final int MENU_ITEM_OPEN_STORE = 1001;
|
||||
|
||||
// 构造方法(Java 7 兼容)
|
||||
public ADsControlView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public ADsControlView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public ADsControlView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public void setPrivacyAgreeStatus(PrivacyAgreeStatus privacyAgreeStatus) {
|
||||
this.mPrivacyAgreeStatus = privacyAgreeStatus;
|
||||
sharedPreferences.edit().putString(PRIVACY_VALUE, this.mPrivacyAgreeStatus.name()).apply();
|
||||
}
|
||||
|
||||
public PrivacyAgreeStatus getPrivacyAgreeStatus() {
|
||||
String privacyAgreeStatusStr = sharedPreferences.getString(PRIVACY_VALUE, PrivacyAgreeStatus.UN_SIGNED.name());
|
||||
PrivacyAgreeStatus privacyAgreeStatus = PrivacyAgreeStatus.fromString(privacyAgreeStatusStr);
|
||||
return privacyAgreeStatus;
|
||||
}
|
||||
|
||||
public void setADsMode(ADsMode mADsMode) {
|
||||
this.mADsMode = mADsMode;
|
||||
sharedPreferences.edit().putString(KEY_SELECTED_MODE, this.mADsMode.name()).apply();
|
||||
updateStoreQrcodeLayoutVisibility(mADsMode);
|
||||
}
|
||||
|
||||
public ADsMode getADsMode() {
|
||||
String savedModeStr = sharedPreferences.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
|
||||
mADsMode = ADsMode.fromValue(savedModeStr);
|
||||
return mADsMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图、SP、Handler
|
||||
*/
|
||||
private void initView(final Context context) {
|
||||
this.mContext = context;
|
||||
|
||||
// 加载布局
|
||||
LayoutInflater.from(context).inflate(R.layout.view_adscontrol, this, true);
|
||||
|
||||
// 初始化SP
|
||||
sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
// 绑定控件
|
||||
rgADsMode = (RadioGroup) findViewById(R.id.rg_ads_mode);
|
||||
rbStandalone = (RadioButton) findViewById(R.id.rb_standalone);
|
||||
rbMimoSDK = (RadioButton) findViewById(R.id.rb_mimo_sdk);
|
||||
rbStoreQrcode = (RadioButton) findViewById(R.id.rb_store_qrcode);
|
||||
rlWinbollStore = (RelativeLayout) findViewById(R.id.rl_winboll_store);
|
||||
ivWinbollStoreQrcode = (ImageView) findViewById(R.id.iv_winboll_store);
|
||||
// 绑定锚点控件
|
||||
viewPopmenuAnchorPoint = (TextView) findViewById(R.id.view_popmenu_anchor_point);
|
||||
|
||||
// 初始化Handler
|
||||
mHandler = new InternalHandler(Looper.getMainLooper());
|
||||
|
||||
// 核心修改:初始化图片的点击和长按事件(锚点改为view_popmenu_anchor_point)
|
||||
initImageViewClickAndLongClick();
|
||||
|
||||
// 注册控件实例
|
||||
registerControlView(this);
|
||||
|
||||
// 从SP读取初始模式
|
||||
setSelectedMode(getADsMode());
|
||||
|
||||
// 单选组选择事件监听
|
||||
rgADsMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, int checkedId) {
|
||||
if (checkedId == R.id.rb_standalone) {
|
||||
setADsMode(ADsMode.STANDALONE);
|
||||
if (listener != null) listener.onModeSelected(ADsMode.STANDALONE);
|
||||
} else if (checkedId == R.id.rb_mimo_sdk) {
|
||||
handlePrivacyLogic((Activity) context, PrivacyAgreeStatus.UN_SIGNED, new OnPrivacyChangeListener() {
|
||||
@Override
|
||||
public void onAgreePrivacy() {
|
||||
setADsMode(ADsMode.MIMO_SDK);
|
||||
if (listener != null) listener.onModeSelected(ADsMode.MIMO_SDK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisagreePrivacy() {
|
||||
setADsMode(ADsMode.STANDALONE);
|
||||
setSelectedMode(ADsMode.STANDALONE);
|
||||
if (listener != null) listener.onModeSelected(ADsMode.STANDALONE);
|
||||
}
|
||||
});
|
||||
} else if (checkedId == R.id.rb_store_qrcode) {
|
||||
setADsMode(ADsMode.STORE_QRCODE);
|
||||
if (listener != null) listener.onModeSelected(ADsMode.STORE_QRCODE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化图片的点击和长按事件
|
||||
* 核心:将PopupMenu锚点绑定到view_popmenu_anchor_point控件
|
||||
*/
|
||||
private void initImageViewClickAndLongClick() {
|
||||
if (ivWinbollStoreQrcode == null || viewPopmenuAnchorPoint == null) {
|
||||
LogUtils.e(TAG, "initImageViewClickAndLongClick: 控件引用为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 点击事件:简化为提示信息
|
||||
ivWinbollStoreQrcode.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ToastUtils.show("长按图片可打开微信小店");
|
||||
LogUtils.d(TAG, "图片点击:提示用户长按打开微信小店");
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 长按事件:锚点改为view_popmenu_anchor_point
|
||||
ivWinbollStoreQrcode.setOnLongClickListener(new OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
// 计算锚点控件的屏幕坐标(用于菜单位置微调)
|
||||
int[] anchorLocation = new int[2];
|
||||
viewPopmenuAnchorPoint.getLocationOnScreen(anchorLocation);
|
||||
final int anchorX = anchorLocation[0];
|
||||
final int anchorY = anchorLocation[1];
|
||||
|
||||
// 创建PopupMenu,锚点绑定到view_popmenu_anchor_point
|
||||
PopupMenu popupMenu = new PopupMenu(mContext, viewPopmenuAnchorPoint);
|
||||
// 设置菜单重力:相对锚点居中显示
|
||||
popupMenu.setGravity(Gravity.CENTER);
|
||||
|
||||
Menu menu = popupMenu.getMenu();
|
||||
menu.add(Menu.NONE, MENU_ITEM_OPEN_STORE, Menu.NONE, "打开微信小店");
|
||||
|
||||
// 设置菜单点击事件
|
||||
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (item.getItemId() == MENU_ITEM_OPEN_STORE) {
|
||||
WebUtils.openUrlInBrowser(mContext, WECHAT_STORE_URL);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// 反射获取PopupWindow,微调菜单位置(可选)
|
||||
Field popupField = PopupMenu.class.getDeclaredField("mPopup");
|
||||
popupField.setAccessible(true);
|
||||
Object popupObject = popupField.get(popupMenu);
|
||||
if (popupObject instanceof PopupWindow) {
|
||||
final PopupWindow popupWindow = (PopupWindow) popupObject;
|
||||
popupWindow.setAnimationStyle(0); // 关闭默认动画
|
||||
|
||||
// 延迟微调菜单位置(确保布局测量完成)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int menuX = anchorX + viewPopmenuAnchorPoint.getWidth() / 2 - popupWindow.getWidth() / 2;
|
||||
int menuY = anchorY + viewPopmenuAnchorPoint.getHeight() / 2 - popupWindow.getHeight() / 2;
|
||||
if (!popupWindow.isShowing()) {
|
||||
popupWindow.showAtLocation(viewPopmenuAnchorPoint, Gravity.NO_GRAVITY, menuX, menuY);
|
||||
}
|
||||
}
|
||||
}, 30);
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
LogUtils.e(TAG, "反射获取PopupWindow失败", e);
|
||||
}
|
||||
|
||||
// 显示菜单
|
||||
popupMenu.show();
|
||||
LogUtils.d(TAG, "长按图片,菜单锚点为view_popmenu_anchor_point");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 设置控件可交互标识
|
||||
ivWinbollStoreQrcode.setClickable(true);
|
||||
ivWinbollStoreQrcode.setFocusable(true);
|
||||
ivWinbollStoreQrcode.setLongClickable(true);
|
||||
viewPopmenuAnchorPoint.setClickable(false); // 锚点控件不可点击
|
||||
viewPopmenuAnchorPoint.setLongClickable(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从ImageView中提取Bitmap(保留方法,无实际调用)
|
||||
*/
|
||||
private Bitmap getBitmapFromImageView(ImageView imageView) {
|
||||
Drawable drawable = imageView.getDrawable();
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
return ((BitmapDrawable) drawable).getBitmap();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩Bitmap(备用方法,无实际调用)
|
||||
*/
|
||||
private Bitmap compressBitmapBySize(Bitmap src, int maxWidth, int maxHeight) {
|
||||
if (src == null) return null;
|
||||
int width = src.getWidth();
|
||||
int height = src.getHeight();
|
||||
float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
|
||||
int newWidth = (int) (width * scale);
|
||||
int newHeight = (int) (height * scale);
|
||||
return Bitmap.createScaledBitmap(src, newWidth, newHeight, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算Bitmap采样率(备用方法,无实际调用)
|
||||
*/
|
||||
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
final int halfHeight = height / 2;
|
||||
final int halfWidth = width / 2;
|
||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从ImageView反射获取资源ID(备用方法,无实际调用)
|
||||
*/
|
||||
private int getResIdFromImageView(ImageView imageView) {
|
||||
try {
|
||||
Field field = ImageView.class.getDeclaredField("mSrcResource");
|
||||
field.setAccessible(true);
|
||||
return field.getInt(imageView);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
LogUtils.e(TAG, "getResIdFromImageView: 反射失败", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新二维码布局显示状态
|
||||
*/
|
||||
private void updateStoreQrcodeLayoutVisibility(ADsMode mode) {
|
||||
if (rlWinbollStore == null) return;
|
||||
rlWinbollStore.setVisibility(mode == ADsMode.STORE_QRCODE ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理SP中的隐私协议状态
|
||||
*/
|
||||
public static void cleanPrivacyStatus(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "cleanPrivacyStatus: Context is null");
|
||||
return;
|
||||
}
|
||||
SharedPreferences sp = getPrivacySharedPreferences(context);
|
||||
sp.edit().remove(PRIVACY_VALUE).apply();
|
||||
LogUtils.i(TAG, "隐私协议状态清理成功");
|
||||
ToastUtils.show("隐私协议状态已清理");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取隐私协议SP实例
|
||||
*/
|
||||
private static SharedPreferences getPrivacySharedPreferences(Context context) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext != null) {
|
||||
return appContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
return context.getSharedPreferences(PRIVACY_VALUE, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理隐私协议逻辑
|
||||
*/
|
||||
private static void handlePrivacyLogic(final Activity activity, PrivacyAgreeStatus privacyAgreeStatus, final OnPrivacyChangeListener onPrivacyChangeListener) {
|
||||
if (privacyAgreeStatus == PrivacyAgreeStatus.REJECTED) {
|
||||
Toast.makeText(activity.getApplicationContext(), "已拒绝隐私协议,广告已处于不可用状态", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
} else if (privacyAgreeStatus == PrivacyAgreeStatus.AGREED) {
|
||||
initMimoSdkStatic(activity.getApplicationContext());
|
||||
return;
|
||||
} else {
|
||||
AlertDialog dialog = createPrivacyDialog(activity, onPrivacyChangeListener);
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.setGravity(Gravity.BOTTOM);
|
||||
WindowManager m = activity.getWindowManager();
|
||||
Display d = m.getDefaultDisplay();
|
||||
WindowManager.LayoutParams p = window.getAttributes();
|
||||
p.width = d.getWidth();
|
||||
window.setAttributes(p);
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化米盟SDK
|
||||
*/
|
||||
private static void initMimoSdkStatic(Context appContext) {
|
||||
if (appContext == null) return;
|
||||
try {
|
||||
MimoSdk.init(appContext, new MimoCustomController() {
|
||||
@Override
|
||||
public boolean isCanUseLocation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MimoLocation getMimoLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCanUseWifiState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean alist() {
|
||||
return true;
|
||||
}
|
||||
}, new MimoSdk.InitCallback() {
|
||||
@Override
|
||||
public void success() {
|
||||
LogUtils.d(TAG, "米盟SDK初始化成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(int code, String msg) {
|
||||
LogUtils.e(TAG, "米盟SDK初始化失败:" + code + ", " + msg);
|
||||
}
|
||||
});
|
||||
MimoSdk.setDebugOn(true);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "米盟SDK初始化异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法:更新SP中的模式
|
||||
*/
|
||||
public static void updateAdsModeByStatic(Context context, ADsMode mode) {
|
||||
if (context == null || mode == null) return;
|
||||
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
sp.edit().putString(KEY_SELECTED_MODE, mode.name()).apply();
|
||||
InternalHandler.sendUpdateModeMessage(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法:读取SP中的模式
|
||||
*/
|
||||
public static ADsMode getAdsModeFromStatic(Context context) {
|
||||
if (context == null) return ADsMode.STANDALONE;
|
||||
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
String savedModeStr = sp.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
|
||||
return ADsMode.fromValue(savedModeStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册控件实例
|
||||
*/
|
||||
private static void registerControlView(ADsControlView view) {
|
||||
synchronized (sControlViews) {
|
||||
if (!sControlViews.contains(view)) {
|
||||
sControlViews.add(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除控件实例
|
||||
*/
|
||||
private static void unregisterControlView(ADsControlView view) {
|
||||
synchronized (sControlViews) {
|
||||
sControlViews.remove(view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中模式
|
||||
*/
|
||||
private void setSelectedMode(final ADsMode mode) {
|
||||
final ADsMode mode2 = (mode == null) ? ADsMode.STANDALONE : mode;
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
if (mode2 == ADsMode.STANDALONE) {
|
||||
rbStandalone.setChecked(true);
|
||||
} else if (mode2 == ADsMode.MIMO_SDK) {
|
||||
rbMimoSDK.setChecked(true);
|
||||
} else if (mode2 == ADsMode.STORE_QRCODE) {
|
||||
rbStoreQrcode.setChecked(true);
|
||||
}
|
||||
updateStoreQrcodeLayoutVisibility(mode2);
|
||||
} else {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setSelectedMode(mode2);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中模式
|
||||
*/
|
||||
public ADsMode getSelectedMode() {
|
||||
int checkedId = rgADsMode.getCheckedRadioButtonId();
|
||||
if (checkedId == R.id.rb_mimo_sdk) {
|
||||
return ADsMode.MIMO_SDK;
|
||||
} else if (checkedId == R.id.rb_store_qrcode) {
|
||||
return ADsMode.STORE_QRCODE;
|
||||
} else {
|
||||
return ADsMode.STANDALONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置外部监听
|
||||
*/
|
||||
public void setOnAdsModeSelectedListener(OnAdsModeSelectedListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部Handler类
|
||||
*/
|
||||
private static class InternalHandler extends Handler {
|
||||
static volatile InternalHandler _InternalHandler;
|
||||
|
||||
public InternalHandler(Looper looper) {
|
||||
super(looper);
|
||||
_InternalHandler = this;
|
||||
}
|
||||
|
||||
public static void sendUpdateModeMessage(ADsMode mode) {
|
||||
if (mode == null || _InternalHandler == null) return;
|
||||
Message msg = _InternalHandler.obtainMessage();
|
||||
msg.what = MSG_UPDATE_MODE;
|
||||
msg.obj = mode;
|
||||
_InternalHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_UPDATE_MODE) {
|
||||
ADsMode mode = (ADsMode) msg.obj;
|
||||
if (mode == null) return;
|
||||
synchronized (sControlViews) {
|
||||
for (ADsControlView view : sControlViews) {
|
||||
if (view != null && view.isShown() && view.isAttachedToWindow()) {
|
||||
view.setSelectedMode(mode);
|
||||
view.updateStoreQrcodeLayoutVisibility(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生命周期:控件销毁
|
||||
*/
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
if (mHandler != null) {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
unregisterControlView(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部监听接口
|
||||
*/
|
||||
public interface OnAdsModeSelectedListener {
|
||||
void onModeSelected(ADsMode selectedMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私协议监听接口
|
||||
*/
|
||||
public interface OnPrivacyChangeListener {
|
||||
void onAgreePrivacy();
|
||||
void onDisagreePrivacy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建隐私协议对话框
|
||||
*/
|
||||
private static AlertDialog createPrivacyDialog(final Activity activity, final OnPrivacyChangeListener onPrivacyChangeListener) {
|
||||
View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_privacy_agreement, null);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setView(dialogView).setCancelable(false);
|
||||
final AlertDialog dialog = builder.create();
|
||||
|
||||
final TextView tvPrivacyUrl = (TextView) dialogView.findViewById(R.id.tv_privacy_url);
|
||||
Button btnAgree = (Button) dialogView.findViewById(R.id.btn_agree);
|
||||
Button btnDisagree = (Button) dialogView.findViewById(R.id.btn_disagree);
|
||||
|
||||
tvPrivacyUrl.setText(Html.fromHtml("<u>" + tvPrivacyUrl.getText().toString() + "</u>"));
|
||||
tvPrivacyUrl.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String url = tvPrivacyUrl.getText().toString().trim();
|
||||
ToastUtils.show("隐私协议链接:" + url);
|
||||
}
|
||||
});
|
||||
tvPrivacyUrl.setClickable(true);
|
||||
tvPrivacyUrl.setFocusable(true);
|
||||
|
||||
btnAgree.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onPrivacyChangeListener != null) {
|
||||
onPrivacyChangeListener.onAgreePrivacy();
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
btnDisagree.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onPrivacyChangeListener != null) {
|
||||
onPrivacyChangeListener.onDisagreePrivacy();
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Jun 28 20:55:33 HKT 2026
|
||||
stageCount=35
|
||||
#Wed May 27 14:51:29 HKT 2026
|
||||
stageCount=23
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.34
|
||||
publishVersion=15.20.22
|
||||
buildCount=0
|
||||
baseBetaVersion=15.20.35
|
||||
baseBetaVersion=15.20.23
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<activity
|
||||
android:name=".CrashActivity"
|
||||
android:name=".CrashHandler$CrashActivity"
|
||||
android:label="CrashActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:process=":CrashActivity"/>
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
public final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
||||
private static final int MENUITEM_COPY = 0;
|
||||
private static final int MENUITEM_RESTART = 1;
|
||||
|
||||
private String mLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
mLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||
initLayout();
|
||||
}
|
||||
|
||||
private void initLayout() {
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
HorizontalScrollView hw = new HorizontalScrollView(this);
|
||||
hw.setBackgroundColor(0xFFF5F5F5);
|
||||
|
||||
TextView message = new TextView(this);
|
||||
final int padding = dp2px(16);
|
||||
message.setPadding(padding, padding, padding, padding);
|
||||
message.setText(mLog);
|
||||
message.setTextColor(0xFF000000);
|
||||
message.setTextIsSelectable(true);
|
||||
|
||||
hw.addView(message);
|
||||
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
setContentView(contentView);
|
||||
|
||||
getActionBar().setTitle(CrashHandler.TITTLE);
|
||||
getActionBar().setSubtitle(GlobalApplication.getAppName(getApplicationContext()) + " Error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restartApp();
|
||||
}
|
||||
|
||||
private void restartApp() {
|
||||
final Intent intent = getPackageManager()
|
||||
.getLaunchIntentForPackage(getPackageName());
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private int dp2px(final float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
menu.add(0, MENUITEM_COPY, 0, "Copy")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
|
||||
menu.add(0, MENUITEM_RESTART, 0, "Restart")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MENUITEM_COPY:
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case MENUITEM_RESTART:
|
||||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||||
restartApp();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
|
||||
@@ -206,5 +220,111 @@ public final class CrashHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部Activity页面 ======================
|
||||
/**
|
||||
* 基础极简崩溃页面
|
||||
* 保险丝熔断时启动,避免复杂布局二次崩溃
|
||||
*/
|
||||
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
||||
private static final int MENUITEM_COPY = 0;
|
||||
private static final int MENUITEM_RESTART = 1;
|
||||
|
||||
private String mLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||
initLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态初始化布局
|
||||
*/
|
||||
private void initLayout() {
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
HorizontalScrollView hw = new HorizontalScrollView(this);
|
||||
hw.setBackgroundColor(0xFFF5F5F5);
|
||||
|
||||
TextView message = new TextView(this);
|
||||
final int padding = dp2px(16);
|
||||
message.setPadding(padding, padding, padding, padding);
|
||||
message.setText(mLog);
|
||||
message.setTextColor(0xFF000000);
|
||||
message.setTextIsSelectable(true);
|
||||
|
||||
hw.addView(message);
|
||||
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
setContentView(contentView);
|
||||
|
||||
getActionBar().setTitle(TITTLE);
|
||||
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restartApp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启应用
|
||||
*/
|
||||
private void restartApp() {
|
||||
final Intent intent = getPackageManager()
|
||||
.getLaunchIntentForPackage(getPackageName());
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* dp转px
|
||||
*/
|
||||
private int dp2px(final float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
menu.add(0, MENUITEM_COPY, 0, "Copy")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
|
||||
menu.add(0, MENUITEM_RESTART, 0, "Restart")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MENUITEM_COPY:
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case MENUITEM_RESTART:
|
||||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||||
restartApp();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.util.Log;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -129,32 +125,17 @@ public class GlobalApplication extends Application {
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
try {
|
||||
super.onCreate();
|
||||
|
||||
// 初始化单例实例(确保在所有初始化操作前完成)
|
||||
sInstance = this;
|
||||
super.onCreate();
|
||||
// 初始化单例实例(确保在所有初始化操作前完成)
|
||||
sInstance = this;
|
||||
|
||||
restoreDebugStatus();
|
||||
// 初始化基础组件(日志、崩溃处理、Toast)
|
||||
initCoreComponents();
|
||||
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
|
||||
initWinbollHost();
|
||||
restoreDebugStatus();
|
||||
// 初始化基础组件(日志、崩溃处理、Toast)
|
||||
initCoreComponents();
|
||||
// 初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
|
||||
initWinbollHost();
|
||||
|
||||
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
|
||||
} catch (Throwable e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
String stackTraceStr = sw.toString();
|
||||
CrashHandleNotifyUtils.handleUncaughtException(
|
||||
this,
|
||||
getPackageName(),
|
||||
stackTraceStr,
|
||||
CrashActivity.class
|
||||
);
|
||||
}
|
||||
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,7 +190,7 @@ public class GlobalApplication extends Application {
|
||||
*/
|
||||
public static String getAppName(Context context) {
|
||||
if (context == null) {
|
||||
Log.w(TAG, "getAppName: 上下文为空,返回 null");
|
||||
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
|
||||
return null;
|
||||
}
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
@@ -225,7 +206,8 @@ public class GlobalApplication extends Application {
|
||||
return appName;
|
||||
} catch (NameNotFoundException e) {
|
||||
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
|
||||
Log.e(TAG, "获取应用名称失败:包名不存在", e);
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -66,7 +66,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
final Intent intent = new Intent();
|
||||
intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, mCrashLog);
|
||||
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashActivity.class);
|
||||
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashHandler.CrashActivity.class);
|
||||
|
||||
StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
|
||||
StringBuilder sb = new StringBuilder("GlobalCrashActivity onCreate StackTrace");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<!-- Theme removed for compatibility -->
|
||||
android:theme="?attr/themeDebug"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -12,7 +12,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:background="?attr/colorTittleBackgound"
|
||||
android:id="@+id/viewlogRelativeLayoutToolbar">
|
||||
|
||||
<Button
|
||||
@@ -20,8 +20,8 @@
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:text="Clean"
|
||||
android:textColor=" android:textColor="@color/mainWindowTextColor""
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:textColor="?attr/activityTextColor"
|
||||
android:background="?attr/toolbarBackgroundColor"
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/viewlogButtonClean"
|
||||
android:layout_marginLeft="5dp"/>
|
||||
@@ -35,7 +35,7 @@
|
||||
android:layout_toRightOf="@+id/viewlogButtonClean"
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/viewlogTextView1"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:background="?attr/colorTittleBackgound"
|
||||
android:textColor="?attr/toolbarTextColor"/>
|
||||
|
||||
<cc.winboll.studio.libappbase.widget.LogTagSpinner
|
||||
@@ -53,7 +53,7 @@
|
||||
android:layout_toLeftOf="@+id/viewlogButtonCopy"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="Selectable"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:background="?attr/colorTittleBackgound"
|
||||
android:id="@+id/viewlogCheckBoxSelectable"
|
||||
android:padding="@dimen/log_text_padding"
|
||||
android:textColor="?attr/toolbarTextColor"/>
|
||||
@@ -62,8 +62,8 @@
|
||||
android:layout_width="@dimen/log_button_width"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:textColor=" android:textColor="@color/mainWindowTextColor""
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:textColor="?attr/activityTextColor"
|
||||
android:background="?attr/toolbarBackgroundColor"
|
||||
android:text="Copy"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
@@ -79,7 +79,7 @@
|
||||
android:layout_below="@+id/viewlogRelativeLayoutToolbar"
|
||||
android:id="@+id/viewlogLinearLayout1"
|
||||
android:gravity="center_vertical"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor"">
|
||||
android:background="?attr/colorTittleBackgound">
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
@@ -88,7 +88,7 @@
|
||||
android:text="ALL"
|
||||
android:padding="2dp"
|
||||
android:id="@+id/viewlogCheckBox1"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:background="?attr/colorTittleBackgound"
|
||||
android:textColor="?attr/toolbarTextColor"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"/>
|
||||
@@ -99,14 +99,14 @@
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:textColor="?attr/colorText"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:background="?attr/colorTittleBackgound"
|
||||
android:singleLine="true"
|
||||
android:id="@+id/tagsearch_et"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background=" android:background="@color/toolbarBackgroundColor""
|
||||
android:background="?attr/colorTittleBackgound"
|
||||
android:scrollbars="none"
|
||||
android:padding="2dp"
|
||||
android:layout_weight="1.0"
|
||||
@@ -132,7 +132,7 @@
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/colorTextBackgound"
|
||||
android:background="?attr/colorTextBackgound"
|
||||
android:id="@+id/viewlogScrollViewLog">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,37 +0,0 @@
|
||||
# WinBoLL
|
||||
|
||||
#### 介绍
|
||||
WinBoLL 网站浏览器。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh winboll
|
||||
|
||||
#### 使用说明
|
||||
3. Termux应用配置:
|
||||
- 已安装Termux(包名 com.termux );
|
||||
- 执行 echo "allow-external-apps = true" > ~/.termux/termux.properties
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: '../.winboll/winboll_app_build.gradle'
|
||||
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||
|
||||
def genVersionName(def versionName){
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['stageCount'] != null)
|
||||
assert (winbollBuildProps['baseVersion'] != null)
|
||||
// 保存基础版本号
|
||||
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||
//保存编译标志配置
|
||||
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||
fos.close();
|
||||
|
||||
// 返回编译版本号
|
||||
return "${versionName}." + winbollBuildProps['stageCount']
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.mywinboll"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 30
|
||||
//1. Android 官方规则
|
||||
//- versionCode 类型:int 整型
|
||||
//- Java int 最大值:2147483647
|
||||
versionCode 1520000
|
||||
// versionName 更新后需要手动设置
|
||||
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.20"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// 下拉控件
|
||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
// Html 解析
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
// 二维码类库
|
||||
api 'com.google.zxing:core:3.4.1'
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
// FastJSON解析
|
||||
implementation 'com.alibaba:fastjson:1.2.76'
|
||||
|
||||
// AndroidX 类库
|
||||
/*api 'androidx.appcompat:appcompat:1.1.0'
|
||||
//api 'com.google.android.material:material:1.4.0'
|
||||
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'*/
|
||||
|
||||
|
||||
// 米盟
|
||||
//api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
//注意:以下5个库必须要引入
|
||||
//implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
/*
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
*/
|
||||
implementation 'com.termux:terminal-emulator:0.118.0'
|
||||
implementation 'com.termux:terminal-view:0.118.0'
|
||||
implementation 'com.termux:termux-shared:0.118.0'
|
||||
|
||||
// Biometric (指纹识别)
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
api 'cc.winboll.studio:libappbase:15.20.34'
|
||||
api 'cc.winboll.studio:libaes:15.20.17'
|
||||
|
||||
// 备用库 jitpack.io 地址
|
||||
//api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.33'
|
||||
//api 'com.github.ZhanGSKen:libaes:aes-v15.20.16'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed Jul 01 16:36:17 CST 2026
|
||||
stageCount=0
|
||||
libraryProject=
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.0
|
||||
buildCount=11
|
||||
baseBetaVersion=15.20.1
|
||||
@@ -1,137 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# ============================== 基础通用规则 ==============================
|
||||
# 保留系统组件
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
|
||||
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||
-keep class cc.winboll.studio.** { *; }
|
||||
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||
|
||||
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||
-keepclassmembers class * {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# 保留 R 文件(避免资源ID混淆)
|
||||
-keepclassmembers class **.R$* {
|
||||
public static <fields>;
|
||||
}
|
||||
|
||||
# 保留 native 方法(避免JNI调用失败)
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# 保留注解和泛型(避免反射/序列化异常)
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
|
||||
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||
-dontwarn java.lang.invoke.*
|
||||
-dontwarn android.support.v8.renderscript.*
|
||||
-dontwarn java.util.function.**
|
||||
|
||||
# ============================== 第三方框架专项规则 ==============================
|
||||
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-keep class okhttp3.internal.** { *; }
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
|
||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||
<init>();
|
||||
}
|
||||
-dontwarn com.bumptech.glide.**
|
||||
|
||||
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep interface com.google.gson.** { *; }
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
|
||||
-keep class com.miui.zeus.** { *; }
|
||||
-keep interface com.miui.zeus.** { *; }
|
||||
# 保留米盟日志字段(便于广告加载失败排查)
|
||||
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
|
||||
-keep class androidx.recyclerview.** { *; }
|
||||
-keep interface androidx.recyclerview.** { *; }
|
||||
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||
public *;
|
||||
}
|
||||
|
||||
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||
# XXPermissions 18.63
|
||||
-keep class com.hjq.permissions.** { *; }
|
||||
-keep interface com.hjq.permissions.** { *; }
|
||||
|
||||
# ZXing 二维码(核心解析组件)
|
||||
-keep class com.google.zxing.** { *; }
|
||||
-keep class com.journeyapps.zxing.** { *; }
|
||||
|
||||
# Jsoup HTML解析
|
||||
-keep class org.jsoup.** { *; }
|
||||
|
||||
# Pinyin4j 拼音搜索
|
||||
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||
|
||||
# JSch SSH组件
|
||||
-keep class com.jcraft.jsch.** { *; }
|
||||
|
||||
# AndroidX 基础组件
|
||||
-keep class androidx.appcompat.** { *; }
|
||||
-keep interface androidx.appcompat.** { *; }
|
||||
|
||||
# ============================== 优化与调试配置 ==============================
|
||||
# 优化级别(平衡混淆效果与性能)
|
||||
-optimizationpasses 5
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
|
||||
# 调试辅助(保留行号便于崩溃定位)
|
||||
-verbose
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">MyWinBoLL +</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,37 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.mywinboll">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,358 +0,0 @@
|
||||
package cc.winboll.studio.mywinboll;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.CrashActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
try {
|
||||
super.onCreate();
|
||||
|
||||
// 初始化主题样式ID列表
|
||||
AESThemeUtil.init(null);
|
||||
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
} catch (Throwable e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
String stackTraceStr = sw.toString();
|
||||
CrashHandleNotifyUtils.handleUncaughtException(
|
||||
this,
|
||||
getPackageName(),
|
||||
stackTraceStr,
|
||||
CrashActivity.class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||
byte[] buf = new byte[1024 * 8];
|
||||
int len;
|
||||
while ((len = input.read(buf)) != -1) {
|
||||
output.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
public static void write(File file, byte[] data) throws IOException {
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||
FileOutputStream output = new FileOutputStream(file);
|
||||
try {
|
||||
write(input, output);
|
||||
} finally {
|
||||
closeIO(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toString(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
write(input, output);
|
||||
try {
|
||||
return output.toString("UTF-8");
|
||||
} finally {
|
||||
closeIO(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeIO(Closeable... closeables) {
|
||||
for (Closeable closeable : closeables) {
|
||||
try {
|
||||
if (closeable != null) closeable.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CrashHandler {
|
||||
|
||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
private static CrashHandler sInstance;
|
||||
|
||||
private PartCrashHandler mPartCrashHandler;
|
||||
|
||||
public static CrashHandler getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new CrashHandler();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void registerGlobal(Context context) {
|
||||
registerGlobal(context, null);
|
||||
}
|
||||
|
||||
public void registerGlobal(Context context, String crashDir) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||
}
|
||||
|
||||
public void registerPart(Context context) {
|
||||
unregisterPart(context);
|
||||
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||
}
|
||||
|
||||
public void unregisterPart(Context context) {
|
||||
if (mPartCrashHandler != null) {
|
||||
mPartCrashHandler.isRunning.set(false);
|
||||
mPartCrashHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PartCrashHandler implements Runnable {
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||
|
||||
public PartCrashHandler(Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (isRunning.get()) {
|
||||
try {
|
||||
Looper.loop();
|
||||
} catch (final Throwable e) {
|
||||
e.printStackTrace();
|
||||
if (isRunning.get()) {
|
||||
MAIN_HANDLER.post(new Runnable(){
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException)e;
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||
|
||||
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final File mCrashDir;
|
||||
|
||||
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||
this.mContext = context;
|
||||
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||
try {
|
||||
|
||||
String log = buildLog(throwable);
|
||||
writeLog(log);
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||
mContext.startActivity(intent);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
writeLog(e.toString());
|
||||
}
|
||||
|
||||
throwable.printStackTrace();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
|
||||
} catch (Throwable e) {
|
||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildLog(Throwable throwable) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
|
||||
String versionName = "unknown";
|
||||
long versionCode = 0;
|
||||
try {
|
||||
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||
versionName = packageInfo.versionName;
|
||||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||
head.put("Time Of Crash", time);
|
||||
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
||||
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
||||
head.put("Kernel", getKernel());
|
||||
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
||||
head.put("Fingerprint", Build.FINGERPRINT);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for (String key : head.keySet()) {
|
||||
if (builder.length() != 0) builder.append("\n");
|
||||
builder.append(key);
|
||||
builder.append(" : ");
|
||||
builder.append(head.get(key));
|
||||
}
|
||||
|
||||
builder.append("\n\n");
|
||||
builder.append(Log.getStackTraceString(throwable));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void writeLog(String log) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||
try {
|
||||
write(file, log.getBytes("UTF-8"));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getKernel() {
|
||||
try {
|
||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||
} catch (Throwable e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CrashActivity extends Activity {
|
||||
|
||||
private String mLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setTheme(android.R.style.Theme_DeviceDefault);
|
||||
setTitle("App Crash");
|
||||
|
||||
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||
|
||||
TextView textView = new TextView(this);
|
||||
int padding = dp2px(16);
|
||||
textView.setPadding(padding, padding, padding, padding);
|
||||
textView.setText(mLog);
|
||||
textView.setTextIsSelectable(true);
|
||||
textView.setTypeface(Typeface.DEFAULT);
|
||||
textView.setLinksClickable(true);
|
||||
|
||||
horizontalScrollView.addView(textView);
|
||||
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
setContentView(contentView);
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static int dp2px(float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.copy:
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package cc.winboll.studio.mywinboll;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
public static final String TAG = "MainActivity";
|
||||
LogView mLogView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AESThemeUtil.applyAppCompatTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
mLogView = findViewById(R.id.logview);
|
||||
|
||||
ToastUtils.show("onCreate");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mLogView.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
AESThemeUtil.inflateCompatThemeMenu(this, menu);
|
||||
if (GlobalApplication.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
}
|
||||
getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_drawerbase, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
}
|
||||
if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/toolbar"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center_vertical|center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="MyWinBoLL"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<cc.winboll.studio.libappbase.LogView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/logview"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#42A5F5</color>
|
||||
<color name="colorPrimaryDark">#1565C0</color>
|
||||
<color name="colorAccent">#FFD740</color>
|
||||
<color name="mainWindowBackgroundColor">#121212</color>
|
||||
<color name="mainWindowTextColor">#E0E0E0</color>
|
||||
<color name="toolbarTextColor">#FFFFFF</color>
|
||||
<color name="toolbarBackgroundColor">#1E3A5F</color>
|
||||
<color name="debugTextColor">#00E676</color>
|
||||
<color name="colorTextBackgound">#121212</color>
|
||||
</resources>
|
||||
@@ -1,29 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
|
||||
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
||||
<item name="aboutViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
|
||||
<item name="aboutViewTextColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="aboutViewTitleColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="aboutViewDividerColor">?attr/mainWindowTextColor</item>
|
||||
<item name="dialogBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
|
||||
<item name="dialogTextColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
|
||||
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||
<item name="textViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
|
||||
<item name="textViewTextColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="editTextBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
|
||||
<item name="editTextTextColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="scrollViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
|
||||
<item name="activityBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
|
||||
<item name="activityTextColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
|
||||
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
|
||||
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
|
||||
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
|
||||
</style>
|
||||
|
||||
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
|
||||
|
||||
</resources>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#1565C0</color>
|
||||
<color name="colorPrimaryDark">#0D47A1</color>
|
||||
<color name="colorAccent">#FF6F00</color>
|
||||
<color name="mainWindowBackgroundColor">#FAFAFA</color>
|
||||
<color name="mainWindowTextColor">#212121</color>
|
||||
<color name="toolbarTextColor">#FFFFFF</color>
|
||||
<color name="toolbarBackgroundColor">#1565C0</color>
|
||||
<color name="debugTextColor">#616161</color>
|
||||
<color name="colorTextBackgound">#FAFAFA</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">MyWinBoLL</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,37 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
|
||||
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
||||
<item name="aboutViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="aboutViewTextColor">?attr/mainWindowTextColor</item>
|
||||
<item name="aboutViewTitleColor">?attr/mainWindowTextColor</item>
|
||||
<item name="aboutViewDividerColor">?attr/mainWindowDarkTextColor</item>
|
||||
<item name="dialogBackgroundColor">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="dialogTextColor">?attr/mainWindowTextColor</item>
|
||||
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
|
||||
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||
<item name="textViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="textViewTextColor">?attr/mainWindowTextColor</item>
|
||||
<item name="editTextBackgroundColor">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="editTextTextColor">?attr/mainWindowTextColor</item>
|
||||
<item name="scrollViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="activityBackgroundColor">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="activityTextColor">?attr/mainWindowTextColor</item>
|
||||
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
|
||||
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
|
||||
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
|
||||
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
|
||||
</style>
|
||||
|
||||
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
|
||||
<item name="colorTittle">?attr/mainWindowTextColor</item>
|
||||
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
|
||||
<item name="colorText">?attr/debugTextColor</item>
|
||||
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
|
||||
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||
</style>
|
||||
|
||||
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
|
||||
|
||||
</resources>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Put flavor specific strings here -->
|
||||
|
||||
</resources>
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,112 +0,0 @@
|
||||
# PowerBell
|
||||
|
||||
#### 介绍
|
||||
一个接收手机电量信息的应用,当电量值达到设定范围时会提醒用户。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :gradle assembleStageRelease
|
||||
|
||||
#### 使用说明
|
||||
|
||||
在安卓系统中需要设置两个权限允许。
|
||||
1.自启动权限允许。
|
||||
2.省电策略-无限制权限允许。
|
||||
3.设置背景图片需要读写手机存储权限。
|
||||
4.要在锁屏充电的时候提醒,还需要设置允许锁屏通知权限。
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@qq.com>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
||||
|
||||
AndroidManifest.xml详解
|
||||
https://www.jianshu.com/p/3b5b89d4e154
|
||||
|
||||
CrashHandler自定义异常处理
|
||||
https://www.jianshu.com/p/9a3d800a429a
|
||||
|
||||
Android用Intent启动Activity的方法
|
||||
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
|
||||
|
||||
Android开发:最详细的 Toolbar 开发实践总结
|
||||
https://www.jianshu.com/p/79604c3ddcae
|
||||
|
||||
Using the App Toolbar
|
||||
https://guides.codepath.com/android/using-the-app-toolbar
|
||||
|
||||
Android的onCreateOptionsMenu()创建菜单Menu详解
|
||||
https://www.cnblogs.com/spring87/p/4312538.html
|
||||
|
||||
Android通知栏-Notification(通知消息)
|
||||
https://blog.csdn.net/qq_35507234/article/details/90676587
|
||||
|
||||
android之PendingIntent的使用
|
||||
https://blog.csdn.net/qq_16628781/article/details/51548324
|
||||
|
||||
change seekbar color android” Code Answer
|
||||
https://www.codegrepper.com/code-examples/whatever/change+seekbar+color+android
|
||||
|
||||
如何选择开源项目许可证
|
||||
https://www.zhihu.com/question/28292322
|
||||
|
||||
Android最简单的自定义布局Notification
|
||||
https://blog.csdn.net/acesheep_911/article/details/81458784?utm_medium=distribute.wap_relevant.none-task-blog-2~default~baidujs_title~default-0.wap_blog_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3
|
||||
|
||||
Android中通知栏Notification详解以及自定义Notification
|
||||
https://blog.csdn.net/daitu_liang/article/details/50246803
|
||||
|
||||
Android 图像系列: 将本地图片加载到Drawable
|
||||
https://blog.csdn.net/qzone123222/article/details/7930035
|
||||
|
||||
android 从相册选择,Android开发从相册中选取照片
|
||||
https://blog.csdn.net/weixin_42146086/article/details/117570917
|
||||
|
||||
Android 任务栈简介
|
||||
https://blog.csdn.net/qq_34368586/article/details/107653912
|
||||
|
||||
Android用Intent启动Activity的方法
|
||||
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
|
||||
|
||||
Android中使用dimen定义尺寸
|
||||
https://blog.csdn.net/yuzhiboyi/article/details/7696174
|
||||
|
||||
declare-styleable:自定义控件的属性
|
||||
https://blog.csdn.net/congqingbin/article/details/7869730
|
||||
|
||||
安卓自定义滑动解锁控件
|
||||
https://blog.csdn.net/lp506954774/article/details/72677018
|
||||
|
||||
Android 添加菜单和返回按钮
|
||||
https://blog.csdn.net/my_tiantian/article/details/77822173
|
||||
|
||||
Android Button的基本使用
|
||||
https://www.cnblogs.com/yishaochu/p/5783605.html
|
||||
|
||||
Android应用中实现系统“分享”接口
|
||||
https://blog.csdn.net/lowprofile_coding/article/details/37656255
|
||||
|
||||
Android 关于mimeType的使用
|
||||
https://blog.csdn.net/dorytmx/article/details/80972248
|
||||
|
||||
使用GitHub Actions实现Android自动打包apk
|
||||
https://blog.csdn.net/ZZL23333/article/details/115798615?app_version=6.0.0&code=app_1562916241&csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22115798615%22%2C%22source%22%3A%22weixin_38986226%22%7D&uLinkId=usr1mkqgl919blen&utm_source=app
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: '../.winboll/winboll_app_build.gradle'
|
||||
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||
|
||||
def genVersionName(def versionName){
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['stageCount'] != null)
|
||||
assert (winbollBuildProps['baseVersion'] != null)
|
||||
// 保存基础版本号
|
||||
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||
//保存编译标志配置
|
||||
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||
fos.close();
|
||||
|
||||
// 返回编译版本号
|
||||
return "${versionName}." + winbollBuildProps['stageCount']
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
// 确保 Java 7 兼容性(已适配项目技术栈)
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.powerbell"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 30
|
||||
versionCode 7
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.15"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/NOTICE.md'
|
||||
exclude 'META-INF/LICENSE.md'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
//注意:以下5个库必须要引入
|
||||
//api 'androidx.appcompat:appcompat:1.4.1'
|
||||
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
api 'com.google.code.gson:gson:2.8.5'
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
// uCrop 核心依赖(最新稳定版)
|
||||
implementation 'com.github.yalantis:ucrop:2.2.8'
|
||||
// 兼容AndroidX(若项目用AndroidX,必须添加)
|
||||
//implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
// Html 解析
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
// 二维码类库
|
||||
api 'com.google.zxing:core:3.4.1'
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
|
||||
// AndroidX 类库
|
||||
api 'androidx.appcompat:appcompat:1.1.0'
|
||||
api 'com.google.android.material:material:1.4.0'
|
||||
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
api 'cc.winboll.studio:libaes:15.20.21'
|
||||
api 'cc.winboll.studio:libappbase:15.20.34'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.2'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||
|
||||
//api fileTree(dir: 'libs', include: ['*.aar'])
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
// JavaMail(APPMSGMailUtils 发送邮件依赖,Android 专用版)
|
||||
implementation 'com.sun.mail:android-mail:1.6.7'
|
||||
implementation 'com.sun.mail:android-activation:1.6.7'
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Jul 18 10:40:59 HKT 2026
|
||||
stageCount=27
|
||||
libraryProject=
|
||||
baseVersion=15.15
|
||||
publishVersion=15.15.26
|
||||
buildCount=0
|
||||
baseBetaVersion=15.15.27
|
||||
@@ -1,279 +0,0 @@
|
||||
#!/bin/bash
|
||||
# PowerBell软著版本号快速修改+生成脚本
|
||||
# 无需手动改主脚本,输入版本号直接运行
|
||||
|
||||
# 颜色输出函数
|
||||
red_echo() { echo -e "\033[31m$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m$1\033[0m"; }
|
||||
|
||||
# 1. 提示用户输入新版本号
|
||||
blue_echo "==== 请输入软著版本号(格式示例:V15、V15.0.1) ===="
|
||||
read -p "输入版本号:" NEW_VERSION
|
||||
|
||||
# 校验版本号格式(避免特殊符号)
|
||||
if [[ ! $NEW_VERSION =~ ^V[0-9]+(\.[0-9]+)*$ ]]; then
|
||||
red_echo "错误:版本号格式无效!请遵循「V+数字」格式(如V15、V15.0.1),不含特殊符号"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 定义固定配置(仅需修改这里的著作权人,其他无需动)
|
||||
SOFTWARE_NAME="PowerBell"
|
||||
COPYRIGHT_OWNER="张绍建陆丰东海镇云宝软件开发工作室"
|
||||
LINES_PER_PAGE=55
|
||||
|
||||
# 3. 生成主脚本(自动替换新版本号)
|
||||
blue_echo -e "\n==== 生成${NEW_VERSION}版本主脚本 ===="
|
||||
cat > build_copyright_pdf_temp.sh << EOF
|
||||
#!/bin/bash
|
||||
# PowerBell软著PDF生成脚本(版本:$NEW_VERSION)
|
||||
red_echo() { echo -e "\033[31m\$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m\$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m\$1\033[0m"; }
|
||||
|
||||
# 配置项(已自动替换为${NEW_VERSION})
|
||||
SOFTWARE_NAME="$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION="$NEW_VERSION"
|
||||
COPYRIGHT_OWNER="$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE=$LINES_PER_PAGE
|
||||
|
||||
# 步骤1:检查依赖
|
||||
blue_echo "==== 1/7 检查并安装依赖 ===="
|
||||
sudo apt update > /dev/null 2>&1
|
||||
REQUIRED_PKGS=("python3" "wkhtmltopdf" "fonts-wqy-microhei" "pdftk" "poppler-utils")
|
||||
for pkg in "\${REQUIRED_PKGS[@]}"; do
|
||||
if ! dpkg -s "\$pkg" > /dev/null 2>&1; then
|
||||
green_echo "安装依赖:\$pkg"
|
||||
sudo apt install -y "\$pkg" > /dev/null 2>&1
|
||||
fi
|
||||
done
|
||||
|
||||
# 步骤2:生成纯文本源码
|
||||
blue_echo -e "\n==== 2/7 生成纯文本核心源码 ===="
|
||||
cat > generate_source.py << GEN_EOF
|
||||
import os
|
||||
PROJECT_PATH = "./"
|
||||
OUTPUT_TXT = "PowerBell_Core_Source.txt"
|
||||
INCLUDE_EXT = [".java", ".kt"]
|
||||
EXCLUDE_DIRS = ["build", "libs", "test", "androidTest", ".git", ".idea", "gradle", "unittest"]
|
||||
MIN_LINE_COUNT = 3
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
|
||||
def clean_text(text):
|
||||
return ''.join(c for c in text if c.isprintable() or c in "\\n\\r\\t")
|
||||
|
||||
def generate_source_txt():
|
||||
valid_files = []
|
||||
main_dir = os.path.join(PROJECT_PATH, "src", "main")
|
||||
if not os.path.exists(main_dir):
|
||||
print("Error: src/main directory not found!")
|
||||
return
|
||||
for root, dirs, files in os.walk(main_dir):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] in INCLUDE_EXT:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = f.readlines()
|
||||
code_lines = [l for l in lines if l.strip() and not l.strip().startswith("//")]
|
||||
if len(code_lines) >= MIN_LINE_COUNT:
|
||||
valid_files.append(file_path)
|
||||
except:
|
||||
continue
|
||||
valid_files.sort(key=lambda x: os.path.getsize(x), reverse=True)
|
||||
with open(OUTPUT_TXT, "w", encoding="utf-8-sig") as f:
|
||||
f.write(f"\{SOFTWARE_NAME} \{SOFTWARE_VERSION} 核心源码 - 著作权人:\{COPYRIGHT_OWNER}\\n\\n")
|
||||
for idx, file_path in enumerate(valid_files, 1):
|
||||
f.write(f"\\n{'='*60}\\n")
|
||||
f.write(f"文件 \{idx}:\{file_path.replace(PROJECT_PATH, '')}\\n")
|
||||
f.write(f"{'='*60}\\n\\n")
|
||||
try:
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
except UnicodeDecodeError:
|
||||
with open(file_path, "r", encoding="gbk") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
f.write(content)
|
||||
f.write("\\n\\n")
|
||||
except Exception as e:
|
||||
f.write(f"文件读取失败:\{str(e)}\\n\\n")
|
||||
continue
|
||||
print(f"有效源码文件数:\{len(valid_files)}")
|
||||
print(f"纯文本文件路径:\{os.path.abspath(OUTPUT_TXT)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_source_txt()
|
||||
GEN_EOF
|
||||
|
||||
python3 generate_source.py
|
||||
if [ ! -f "PowerBell_Core_Source.txt" ]; then
|
||||
red_echo "纯文本源码生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤3:生成带版本号页眉的HTML
|
||||
blue_echo -e "\n==== 3/7 生成带${NEW_VERSION}页眉的HTML ===="
|
||||
cat > txt2html.py << TXT_EOF
|
||||
import os
|
||||
TXT_FILE = "PowerBell_Core_Source.txt"
|
||||
HTML_FILE = "PowerBell_Source.html"
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE = $LINES_PER_PAGE
|
||||
|
||||
CSS_STYLE = """
|
||||
<style>
|
||||
@page {{
|
||||
size: A4;
|
||||
margin: 10mm 5mm;
|
||||
@top-center {{
|
||||
content: "{} {} - 源代码(著作权人:{})";
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
}}
|
||||
@bottom-center {{
|
||||
content: "页码 " counter(page) " / " counter(pages);
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
}}
|
||||
}}
|
||||
body {{
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
padding: 5mm 0 0 0;
|
||||
counter-reset: code-line;
|
||||
}}
|
||||
.file-header {{
|
||||
background: #f0f0f0;
|
||||
padding: 3px;
|
||||
margin: 6px 0;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}}
|
||||
.code-block {{
|
||||
white-space: pre;
|
||||
margin-left: 8px;
|
||||
line-height: 1.1;
|
||||
counter-increment: code-line;
|
||||
}}
|
||||
.code-block:before {{
|
||||
content: counter(code-line) " ";
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
}}
|
||||
.page-break {{ page-break-after: always; counter-reset: code-line; }}
|
||||
</style>
|
||||
""".format(SOFTWARE_NAME, SOFTWARE_VERSION, COPYRIGHT_OWNER)
|
||||
|
||||
def txt_to_html():
|
||||
with open(TXT_FILE, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
html_content = "<!DOCTYPE html><html><head><meta charset='utf-8'>" + CSS_STYLE + "</head><body>"
|
||||
content_lines = content.split("\\n")[2:]
|
||||
content_clean = "\\n".join(content_lines)
|
||||
blocks = content_clean.split("====")
|
||||
|
||||
line_count = 0
|
||||
for block in blocks:
|
||||
if not block.strip():
|
||||
continue
|
||||
if "文件 " in block and ":" in block:
|
||||
file_header = block.split("\\n")[0].strip() if "\\n" in block else block.strip()
|
||||
html_content += f"<div class='file-header'>\{file_header}</div>"
|
||||
code_part = block.split("\\n")[1:] if "\\n" in block else []
|
||||
block = "\\n".join(code_part)
|
||||
code_lines = block.split("\\n")
|
||||
for line in code_lines:
|
||||
if line.strip() or line_count > 0:
|
||||
line_count += 1
|
||||
html_content += f"<div class='code-block'>\{line}</div>"
|
||||
if line_count >= LINES_PER_PAGE:
|
||||
html_content += "<div class='page-break'></div>"
|
||||
line_count = 0
|
||||
html_content += "</body></html>"
|
||||
with open(HTML_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
print(f"HTML文件路径:\{os.path.abspath(HTML_FILE)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
txt_to_html()
|
||||
TXT_EOF
|
||||
|
||||
python3 txt2html.py
|
||||
if [ ! -f "PowerBell_Source.html" ]; then
|
||||
red_echo "HTML文件生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤4:生成完整PDF
|
||||
blue_echo -e "\n==== 4/7 生成完整PDF(版本:${NEW_VERSION}) ===="
|
||||
wkhtmltopdf --page-size A4 \
|
||||
--margin-top 15mm --margin-bottom 15mm --margin-left 5mm --margin-right 5mm \
|
||||
--encoding utf-8 \
|
||||
--no-images --disable-javascript \
|
||||
--enable-local-file-access \
|
||||
--no-stop-slow-scripts \
|
||||
PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
|
||||
if [ ! -f "PowerBell_soft_full.pdf" ]; then
|
||||
red_echo "完整PDF生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤5:截取60页
|
||||
blue_echo -e "\n==== 5/7 截取前30+后30页 ===="
|
||||
TOTAL_PAGES=\$(pdfinfo PowerBell_soft_full.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "源码完整PDF总页数:\$TOTAL_PAGES 页"
|
||||
|
||||
if [ "\$TOTAL_PAGES" -le 60 ]; then
|
||||
cp PowerBell_soft_full.pdf PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
green_echo "源码不足60页,直接使用完整PDF"
|
||||
else
|
||||
pdftk PowerBell_soft_full.pdf cat 1-30 output PowerBell_前30页.pdf
|
||||
START_PAGE=\$((TOTAL_PAGES - 29))
|
||||
pdftk PowerBell_soft_full.pdf cat \$START_PAGE-\$TOTAL_PAGES output PowerBell_后30页.pdf
|
||||
pdftk PowerBell_前30页.pdf PowerBell_后30页.pdf cat output PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
rm -f PowerBell_前30页.pdf PowerBell_后30页.pdf
|
||||
green_echo "源码超过60页,已截取前30页+后30页合并为60页"
|
||||
fi
|
||||
|
||||
# 步骤6:验证规范
|
||||
blue_echo -e "\n==== 6/7 验证${NEW_VERSION}版本PDF规范 ===="
|
||||
FINAL_PAGES=\$(pdfinfo PowerBell_软著源码_${NEW_VERSION}_60页.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "最终PDF页数:\$FINAL_PAGES 页"
|
||||
green_echo "每页代码行数:\$LINES_PER_PAGE 行(≥50行)"
|
||||
green_echo "页眉信息:$SOFTWARE_NAME $NEW_VERSION - 源代码(著作权人:$COPYRIGHT_OWNER)"
|
||||
|
||||
# 步骤7:清理临时文件
|
||||
blue_echo -e "\n==== 7/7 清理临时文件 ===="
|
||||
rm -f generate_source.py txt2html.py PowerBell_Core_Source.txt PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
green_echo "临时文件清理完成!"
|
||||
|
||||
# 输出结果
|
||||
green_echo -e "\n====================================="
|
||||
green_echo "✅ $SOFTWARE_NAME $NEW_VERSION 软著PDF生成成功!🎉"
|
||||
green_echo "📄 最终文件:\$(pwd)/PowerBell_软著源码_${NEW_VERSION}_60页.pdf"
|
||||
green_echo "💡 可直接提交软著登记,无需手动修改!"
|
||||
green_echo "====================================="
|
||||
EOF
|
||||
|
||||
# 4. 赋予执行权限并运行
|
||||
chmod +x build_copyright_pdf_temp.sh
|
||||
blue_echo -e "\n==== 开始生成${NEW_VERSION}版本PDF ===="
|
||||
./build_copyright_pdf_temp.sh
|
||||
|
||||
# 5. 删除临时主脚本(可选,保留则注释此行)
|
||||
rm -f build_copyright_pdf_temp.sh
|
||||
|
||||
green_echo -e "\n==== 操作完成!${NEW_VERSION}版本PDF已生成 ===="
|
||||
@@ -1,151 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# ============================== 基础通用规则 ==============================
|
||||
# 保留系统组件
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
|
||||
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||
-keep class cc.winboll.studio.** { *; }
|
||||
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||
|
||||
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||
-keepclassmembers class * {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# 保留 R 文件(避免资源ID混淆)
|
||||
-keepclassmembers class **.R$* {
|
||||
public static <fields>;
|
||||
}
|
||||
|
||||
# 保留 native 方法(避免JNI调用失败)
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# 保留注解和泛型(避免反射/序列化异常)
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
|
||||
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||
-dontwarn java.lang.invoke.*
|
||||
-dontwarn android.support.v8.renderscript.*
|
||||
-dontwarn java.util.function.**
|
||||
|
||||
# ============================== 第三方框架专项规则 ==============================
|
||||
# JavaMail / SMTP 邮件发送(APPMSGMailUtils 依赖,防止 R8 混淆导致 SMTP 发送失败)
|
||||
-keep class javax.mail.** { *; }
|
||||
-keep class javax.mail.internet.** { *; }
|
||||
-keep class javax.activation.** { *; }
|
||||
-keep class com.sun.mail.** { *; }
|
||||
-keep class com.sun.mail.util.** { *; }
|
||||
-keep class com.sun.mail.smtp.** { *; }
|
||||
-keep class com.sun.mail.pop3.** { *; }
|
||||
-keep class com.sun.mail.imap.** { *; }
|
||||
-dontwarn javax.mail.**
|
||||
-dontwarn javax.activation.**
|
||||
-dontwarn com.sun.mail.**
|
||||
-dontwarn java.awt.**
|
||||
-dontwarn javax.naming.**
|
||||
-dontwarn javax.net.ssl.**
|
||||
|
||||
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-keep class okhttp3.internal.** { *; }
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
# ============================== 必要补充规则 ==============================
|
||||
# OkHttp 4.4.1 补充规则(Java 7 兼容)
|
||||
-keep class okhttp3.internal.concurrent.** { *; }
|
||||
-keep class okhttp3.internal.connection.** { *; }
|
||||
-dontwarn okhttp3.internal.concurrent.TaskRunner
|
||||
-dontwarn okhttp3.internal.connection.RealCall
|
||||
|
||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||
<init>();
|
||||
}
|
||||
-dontwarn com.bumptech.glide.**
|
||||
|
||||
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep interface com.google.gson.** { *; }
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# RecyclerView 1.0.0
|
||||
-keep class androidx.recyclerview.** { *; }
|
||||
-keep interface androidx.recyclerview.** { *; }
|
||||
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||
public *;
|
||||
}
|
||||
|
||||
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||
# XXPermissions 18.63
|
||||
-keep class com.hjq.permissions.** { *; }
|
||||
-keep interface com.hjq.permissions.** { *; }
|
||||
|
||||
# ZXing 二维码(核心解析组件)
|
||||
-keep class com.google.zxing.** { *; }
|
||||
-keep class com.journeyapps.zxing.** { *; }
|
||||
|
||||
# Jsoup HTML解析
|
||||
-keep class org.jsoup.** { *; }
|
||||
|
||||
# Pinyin4j 拼音搜索
|
||||
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||
|
||||
# JSch SSH组件
|
||||
-keep class com.jcraft.jsch.** { *; }
|
||||
|
||||
# AndroidX 基础组件
|
||||
-keep class androidx.appcompat.** { *; }
|
||||
-keep interface androidx.appcompat.** { *; }
|
||||
|
||||
# ============================== 优化与调试配置 ==============================
|
||||
# 优化级别(平衡混淆效果与性能)
|
||||
-optimizationpasses 5
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
|
||||
# 调试辅助(保留行号便于崩溃定位)
|
||||
-verbose
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
tools:replace="android:icon"
|
||||
android:icon="@drawable/ic_launcher_beta">
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name_cn1">能源钟★</string>
|
||||
<string name="app_name_cn2">泡额呗额☆</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">PowerBell+</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_en1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_en1"
|
||||
android:shortcutLongLabel="@string/switchto_en1"
|
||||
android:shortcutDisabledMessage="@string/en1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_en1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<!--<shortcut
|
||||
android:shortcutId="switchto_cn1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn1"
|
||||
android:shortcutLongLabel="@string/switchto_cn1"
|
||||
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>-->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn2"
|
||||
android:shortcutLongLabel="@string/switchto_cn2"
|
||||
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn2" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_en1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_en1"
|
||||
android:shortcutLongLabel="@string/switchto_en1"
|
||||
android:shortcutDisabledMessage="@string/en1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_en1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn1"
|
||||
android:shortcutLongLabel="@string/switchto_cn1"
|
||||
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<!--<shortcut
|
||||
android:shortcutId="switchto_cn2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn2"
|
||||
android:shortcutLongLabel="@string/switchto_cn2"
|
||||
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn2" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>-->
|
||||
</shortcuts>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<!--<shortcut
|
||||
android:shortcutId="switchto_en1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_en1"
|
||||
android:shortcutLongLabel="@string/switchto_en1"
|
||||
android:shortcutDisabledMessage="@string/en1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_en1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>-->
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn1"
|
||||
android:shortcutLongLabel="@string/switchto_cn1"
|
||||
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn1" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="switchto_cn2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:shortcutShortLabel="@string/switchto_cn2"
|
||||
android:shortcutLongLabel="@string/switchto_cn2"
|
||||
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
|
||||
android:targetPackage="cc.winboll.studio.powerbell.beta"
|
||||
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
|
||||
android:data="switchto_cn2" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -1,332 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.powerbell">
|
||||
|
||||
<!-- 此应用可显示在其他应用上方 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<!-- 运行前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<!-- 运行“specialUse”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
|
||||
<!-- 开机启动 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<!-- 显示通知 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- PACKAGE_USAGE_STATS -->
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
|
||||
|
||||
<!-- BATTERY_STATS -->
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
|
||||
<!-- 计算应用存储空间 -->
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<!-- 请求忽略电池优化 -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- MANAGE_EXTERNAL_STORAGE -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 拍摄照片和视频 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission"/>
|
||||
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false"/>
|
||||
|
||||
<queries>
|
||||
|
||||
<package android:name="com.miui.securitycenter"/>
|
||||
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:process=":main"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme_Default"
|
||||
android:persistent="true"
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityEN1"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmainen1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityCN1"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name_cn1"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn1"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityCN2"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name_cn2"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmaincn2"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.CrashActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.ClearRecordActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.BackgroundSettingsActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:mimeType="image/jpeg"/>
|
||||
|
||||
<data android:mimeType="image/jpg"/>
|
||||
|
||||
<data android:mimeType="image/png"/>
|
||||
|
||||
<data android:mimeType="image/webp"/>
|
||||
|
||||
<data android:mimeType="image/*"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.BatteryReporterActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.PixelPickerActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.BatteryReportActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".unittest.MainUnitTestActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.ShortcutActionActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:process=":main"
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true"/>
|
||||
|
||||
<receiver
|
||||
android:process=":main"
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
<action android:name="android.intent.action.POWER_CONNECTED"/>
|
||||
|
||||
<action android:name="android.intent.action.USER_PRESENT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":main"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="后台核心功能运行、持续保活"/>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":assistant"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="辅助核心功能运行"/>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.TTSPlayService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":main"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<service
|
||||
android:name=".services.ThoughtfulService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":main"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"
|
||||
android:process=":main">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 517 B |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,319 +0,0 @@
|
||||
package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
|
||||
|
||||
/**
|
||||
* 应用全局入口类
|
||||
* 适配:Java7 语法规范 | Android API30 系统版本
|
||||
* 核心策略:极致强制缓存 - 无论内存紧张程度,永不自动清理任何缓存(Bitmap/视图控件/路径记录)
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025-12-29 15:30:00
|
||||
* @LastModified 2026-01-02 19:01:00
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
// ====================================== 常量区 - 置顶排序 (按功能归类) ======================================
|
||||
// 基础日志TAG
|
||||
private static final String TAG = "App";
|
||||
// 缓存保护专用TAG
|
||||
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
|
||||
// 电池无效值常量(修复拼写错误:INVALID_BATTERY_VALUE)
|
||||
private static final int INVALID_BATTERY_VALUE = -1;
|
||||
|
||||
// 组件跳转常量
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
|
||||
// 动作跳转常量
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
|
||||
// ====================================== 静态属性区 - 全局单例/状态 (按核心程度排序) ======================================
|
||||
// 应用单例
|
||||
private static App sApp;
|
||||
|
||||
// 配置与缓存工具 (全局单例)
|
||||
public static AppConfigUtils sAppConfigUtils;
|
||||
private static AppCacheUtils sAppCacheUtils;
|
||||
|
||||
// 资源与视图缓存 (强制驻留,极致缓存核心)
|
||||
public static BackgroundSourceUtils sBackgroundSourceUtils;
|
||||
public static BitmapCacheUtils sBitmapCacheUtils;
|
||||
private static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
|
||||
|
||||
// 系统状态 (电池电量)
|
||||
public static volatile int sQuantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
|
||||
// 系统工具 (通知管理器)
|
||||
private static NotificationManagerUtils sNotificationManagerUtils;
|
||||
|
||||
// ====================================== 成员属性区 - 非静态成员 (广播接收器) ======================================
|
||||
private GlobalApplicationReceiver mGlobalReceiver;
|
||||
|
||||
// ====================================== 公共静态方法 - 单例/工具获取 (对外入口) ======================================
|
||||
/**
|
||||
* 获取应用全局单例实例
|
||||
* @return 应用单例App实例
|
||||
*/
|
||||
public static App getInstance() {
|
||||
LogUtils.d(TAG, "【getInstance】应用单例获取方法调用 | 当前实例:" + sApp);
|
||||
return sApp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置工具类单例实例
|
||||
* @param context 上下文对象
|
||||
* @return 配置工具类AppConfigUtils实例
|
||||
*/
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
String contextClass = context != null ? context.getClass().getSimpleName() : "null";
|
||||
LogUtils.d(TAG, "【getAppConfigUtils】配置工具获取方法调用 | 入参Context类型:" + contextClass);
|
||||
|
||||
if (sAppConfigUtils == null) {
|
||||
sAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "【getAppConfigUtils】配置工具实例为空,已初始化新实例");
|
||||
}
|
||||
return sAppConfigUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存工具类单例实例
|
||||
* @param context 上下文对象
|
||||
* @return 缓存工具类AppCacheUtils实例
|
||||
*/
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
String contextClass = context != null ? context.getClass().getSimpleName() : "null";
|
||||
LogUtils.d(TAG, "【getAppCacheUtils】缓存工具获取方法调用 | 入参Context类型:" + contextClass);
|
||||
|
||||
if (sAppCacheUtils == null) {
|
||||
sAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "【getAppCacheUtils】缓存工具实例为空,已初始化新实例");
|
||||
}
|
||||
return sAppCacheUtils;
|
||||
}
|
||||
|
||||
// ====================================== 公共成员方法 - 业务逻辑 (实例方法) ======================================
|
||||
/**
|
||||
* 清除电池历史数据
|
||||
*/
|
||||
public void clearBatteryHistory() {
|
||||
LogUtils.d(TAG, "【clearBatteryHistory】清除电池历史数据方法调用");
|
||||
if (sAppCacheUtils != null) {
|
||||
sAppCacheUtils.clearBatteryHistory();
|
||||
LogUtils.d(TAG, "【clearBatteryHistory】电池历史数据清除成功");
|
||||
} else {
|
||||
LogUtils.w(TAG, "【clearBatteryHistory】电池历史数据清除失败 | 缓存工具实例sAppCacheUtils为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视图缓存实例
|
||||
* @return 视图缓存MemoryCachedBackgroundView实例
|
||||
*/
|
||||
public MemoryCachedBackgroundView getMemoryCachedBackgroundView() {
|
||||
LogUtils.d(TAG, "【getMemoryCachedBackgroundView】视图缓存获取方法调用 | 当前实例:" + sMemoryCachedBackgroundView);
|
||||
return sMemoryCachedBackgroundView;
|
||||
}
|
||||
|
||||
// ====================================== 公共静态方法 - 业务逻辑 (全局工具方法) ======================================
|
||||
/**
|
||||
* 手动清理所有缓存(仅主动调用生效,符合极致缓存策略)
|
||||
*/
|
||||
public static void manualClearAllCache() {
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存方法调用 | 仅主动触发生效");
|
||||
|
||||
// 清理Bitmap缓存
|
||||
if (sBitmapCacheUtils != null) {
|
||||
sBitmapCacheUtils.clearAllCache();
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】Bitmap缓存已清理");
|
||||
}
|
||||
|
||||
// 仅置空视图缓存引用,不销毁实例(极致缓存策略)
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【manualClearAllCache】视图缓存引用已置空 | 实例保留");
|
||||
sMemoryCachedBackgroundView = null;
|
||||
}
|
||||
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【manualClearAllCache】手动清理缓存操作完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知消息(仅调试模式下生效)
|
||||
* @param title 通知标题
|
||||
* @param content 通知内容
|
||||
*/
|
||||
public static void notifyMessage(String title, String content) {
|
||||
LogUtils.d(TAG, "【notifyMessage】发送通知消息方法调用 | 标题:" + title + " | 内容:" + content);
|
||||
|
||||
boolean canSend = isDebugging() && sApp != null && sNotificationManagerUtils != null;
|
||||
if (canSend) {
|
||||
NotificationMessage message = new NotificationMessage(title, content, "");
|
||||
sNotificationManagerUtils.showMessageNotification(sApp, message);
|
||||
LogUtils.d(TAG, "【notifyMessage】通知消息发送成功");
|
||||
} else {
|
||||
LogUtils.d(TAG, "【notifyMessage】通知消息发送失败 | 条件不满足:调试模式=" + isDebugging() + " | 应用实例=" + (sApp != null) + " | 通知工具=" + (sNotificationManagerUtils != null));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 生命周期方法 - 应用全局生命周期 (重写父类方法) ======================================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "【onCreate】应用启动生命周期方法调用 | 开始初始化应用...");
|
||||
|
||||
// 初始化应用单例
|
||||
sApp = this;
|
||||
LogUtils.d(TAG, "【onCreate】应用单例已初始化");
|
||||
|
||||
// 初始化核心组件
|
||||
initBaseTools();
|
||||
initUtils();
|
||||
initReceiver();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】应用初始化完成 | 极致强制缓存策略已激活");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "【onTerminate】应用终止生命周期方法调用 | 开始释放非缓存资源...");
|
||||
|
||||
// 释放非缓存资源
|
||||
ToastUtils.release();
|
||||
releaseNotificationManager();
|
||||
releaseReceiver();
|
||||
|
||||
// 核心策略:不清理任何缓存
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【onTerminate】极致缓存策略生效 | 所有缓存将保留在内存中");
|
||||
LogUtils.d(TAG, "【onTerminate】非缓存资源释放完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【onTrimMemory】系统内存修剪回调 | 内存等级:" + level + " | 忽略修剪,缓存强制保护");
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
LogUtils.w(CACHE_PROTECT_TAG, "【onLowMemory】系统低内存回调 | 极致缓存策略生效 | 不执行任何缓存清理操作");
|
||||
logDetailedCacheStatus();
|
||||
}
|
||||
|
||||
// ====================================== 私有初始化方法 - 组件初始化 (按依赖顺序排序) ======================================
|
||||
/**
|
||||
* 初始化基础工具类(Activity管理、Toast、通知管理器)
|
||||
*/
|
||||
private void initBaseTools() {
|
||||
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化开始...");
|
||||
WinBoLLActivityManager.init(this);
|
||||
ToastUtils.init(this);
|
||||
sNotificationManagerUtils = new NotificationManagerUtils(this);
|
||||
LogUtils.d(TAG, "【initBaseTools】基础工具类初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化核心工具与缓存(极致强制驻留,缓存核心)
|
||||
*/
|
||||
private void initUtils() {
|
||||
LogUtils.d(TAG, "【initUtils】核心工具与缓存初始化开始 | 极致缓存策略激活");
|
||||
|
||||
// 1. 配置与基础缓存工具初始化
|
||||
sAppConfigUtils = getAppConfigUtils(this);
|
||||
sAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
// 2. 资源与Bitmap缓存工具初始化(永久驻留)
|
||||
sBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
sBackgroundSourceUtils.loadSettings();
|
||||
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "【initUtils】资源与Bitmap缓存工具初始化完成 | 永久驻留内存");
|
||||
|
||||
// 3. 视图缓存初始化(永久驻留,无实例则创建)
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
|
||||
if (sMemoryCachedBackgroundView == null) {
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, sBackgroundSourceUtils.getCurrentBackgroundBean(), true);
|
||||
LogUtils.d(TAG, "【initUtils】视图缓存无现有实例,已创建新实例");
|
||||
}
|
||||
LogUtils.d(TAG, "【initUtils】视图缓存初始化完成 | 永久驻留内存");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局广播接收器
|
||||
*/
|
||||
private void initReceiver() {
|
||||
LogUtils.d(TAG, "【initReceiver】全局广播接收器注册开始...");
|
||||
mGlobalReceiver = new GlobalApplicationReceiver(this);
|
||||
mGlobalReceiver.registerAction();
|
||||
LogUtils.d(TAG, "【initReceiver】全局广播接收器注册完成");
|
||||
}
|
||||
|
||||
// ====================================== 私有释放方法 - 资源释放 (按创建逆序排序) ======================================
|
||||
/**
|
||||
* 释放全局广播接收器
|
||||
*/
|
||||
private void releaseReceiver() {
|
||||
LogUtils.d(TAG, "【releaseReceiver】全局广播接收器释放开始...");
|
||||
if (mGlobalReceiver != null) {
|
||||
mGlobalReceiver.unregisterAction();
|
||||
mGlobalReceiver = null;
|
||||
LogUtils.d(TAG, "【releaseReceiver】全局广播接收器释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放通知管理器资源
|
||||
*/
|
||||
private void releaseNotificationManager() {
|
||||
LogUtils.d(TAG, "【releaseNotificationManager】通知管理器资源释放开始...");
|
||||
if (sNotificationManagerUtils != null) {
|
||||
sNotificationManagerUtils.release();
|
||||
sNotificationManagerUtils = null;
|
||||
LogUtils.d(TAG, "【releaseNotificationManager】通知管理器资源释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 私有辅助方法 - 日志/工具 (辅助功能) ======================================
|
||||
/**
|
||||
* 记录当前缓存详细状态(用于调试监控,极致缓存策略监控)
|
||||
*/
|
||||
private void logDetailedCacheStatus() {
|
||||
LogUtils.d(TAG, "【logDetailedCacheStatus】缓存状态监控日志开始...");
|
||||
|
||||
// Bitmap缓存状态
|
||||
if (sBitmapCacheUtils != null) {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 有效");
|
||||
try {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】Bitmap缓存数量:" + sBitmapCacheUtils.getCacheCount());
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(CACHE_PROTECT_TAG, "【缓存详情】获取Bitmap缓存数量失败", e);
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】BitmapCache - 未初始化");
|
||||
}
|
||||
|
||||
// 视图缓存状态
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 有效");
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存详情】视图实例数量:" + MemoryCachedBackgroundView.getInstanceCount());
|
||||
} else {
|
||||
LogUtils.d(CACHE_PROTECT_TAG, "【缓存状态】ViewCache - 引用已置空(实例可能保留)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,590 +0,0 @@
|
||||
package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.activities.AboutActivity;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
|
||||
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
|
||||
import cc.winboll.studio.powerbell.activities.SettingsActivity;
|
||||
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.models.BatteryStyle;
|
||||
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity;
|
||||
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageUtils;
|
||||
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.views.BatteryStyleView;
|
||||
import cc.winboll.studio.powerbell.views.MainContentView;
|
||||
|
||||
/**
|
||||
* 应用核心主活动
|
||||
* 功能:管理电池监控、背景设置、服务启停、权限申请等核心功能
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | UI与服务状态实时同步
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class MainActivity extends WinBoLLActivity implements MainContentView.OnViewActionListener {
|
||||
|
||||
// ======================== 静态常量区(抽离魔法值,按功能分类)========================
|
||||
public static final String TAG = "MainActivity";
|
||||
private static final int REQUEST_BACKGROUND_SETTINGS_ACTIVITY = 1001;
|
||||
public static final String EXTRA_ISRELOAD_BACKGROUNDVIEW = "EXTRA_ISRELOAD_BACKGROUNDVIEW";
|
||||
public static final String EXTRA_ISRELOAD_ACCENTCOLOR = "EXTRA_ISRELOAD_ACCENTCOLOR";
|
||||
// Handler 消息常量
|
||||
public static final int MSG_RELOAD_APPCONFIG = 0;
|
||||
public static final int MSG_CURRENTVALUEBATTERY = 1;
|
||||
public static final int MSG_LOAD_BACKGROUND = 2;
|
||||
private static final int MSG_UPDATE_SERVICE_SWITCH = 3;
|
||||
private static final int MSG_UPDATE_BATTERYDRAWABLE = 4;
|
||||
|
||||
// ======================== 静态成员区(全局共享,管控生命周期)========================
|
||||
private static MainActivity sMainActivity;
|
||||
private static Handler sGlobalHandler;
|
||||
|
||||
// ======================== 工具类实例区(单例化,避免重复初始化)========================
|
||||
private PermissionUtils mPermissionUtils;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private BackgroundSourceUtils mBgSourceUtils;
|
||||
|
||||
// ======================== 应用核心实例区 =========================
|
||||
private App mApplication;
|
||||
private MainContentView mMainContentView;
|
||||
private ControlCenterServiceBean mServiceControlBean;
|
||||
|
||||
// ======================== 基础视图组件区 =========================
|
||||
private Toolbar mToolbar;
|
||||
private Drawable mFrameDrawable;
|
||||
private Menu mMenu;
|
||||
|
||||
// ======================== 生命周期方法区(按系统调用顺序排列)========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate() 调用 | savedInstanceState: " + savedInstanceState);
|
||||
|
||||
initGlobalHandler();
|
||||
setContentView(R.layout.activity_main);
|
||||
initPermissionUtils();
|
||||
initMainContentView();
|
||||
initCriticalView();
|
||||
initCoreUtilsAsync();
|
||||
|
||||
// 处理首次启动参数
|
||||
handleReloadBackgroundParam(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
LogUtils.d(TAG, "onNewIntent() 调用 | intent: " + intent);
|
||||
// 关键:更新Activity持有的Intent,确保后续获取最新值
|
||||
setIntent(intent);
|
||||
// 统一处理刷新背景参数
|
||||
handleReloadBackgroundParam(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onPostCreate() 调用 | savedInstanceState: " + savedInstanceState);
|
||||
mPermissionUtils.startPermissionRequest(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume() 调用");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
LogUtils.d(TAG, "onPause() 调用");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy() 调用");
|
||||
|
||||
// 释放核心视图
|
||||
if (mMainContentView != null) {
|
||||
mMainContentView.releaseResources();
|
||||
mMainContentView = null;
|
||||
LogUtils.d(TAG, "onDestroy: 核心视图资源已释放");
|
||||
}
|
||||
// 销毁Handler防止内存泄漏
|
||||
if (sGlobalHandler != null) {
|
||||
sGlobalHandler.removeCallbacksAndMessages(null);
|
||||
sGlobalHandler = null;
|
||||
LogUtils.d(TAG, "onDestroy: 全局Handler已销毁");
|
||||
}
|
||||
// 释放Drawable
|
||||
if (mFrameDrawable != null) {
|
||||
mFrameDrawable.setCallback(null);
|
||||
mFrameDrawable = null;
|
||||
LogUtils.d(TAG, "onDestroy: 框架Drawable已释放");
|
||||
}
|
||||
// 置空所有引用,消除内存泄漏风险
|
||||
sMainActivity = null;
|
||||
mPermissionUtils = null;
|
||||
mAppConfigUtils = null;
|
||||
mBgSourceUtils = null;
|
||||
mServiceControlBean = null;
|
||||
mMenu = null;
|
||||
mApplication = null;
|
||||
mToolbar = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "onActivityResult() 调用 | requestCode: " + requestCode + " | resultCode: " + resultCode + " | data: " + data);
|
||||
mPermissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_BACKGROUND_SETTINGS_ACTIVITY && sGlobalHandler != null) {
|
||||
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
|
||||
LogUtils.d(TAG, "onActivityResult: 发送背景加载消息");
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 菜单与导航方法区 ========================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu() 调用 | menu: " + menu);
|
||||
mMenu = menu;
|
||||
AESThemeUtil.inflateThemeMenu(this, menu);
|
||||
|
||||
// 调试模式加载测试菜单
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
|
||||
LogUtils.d(TAG, "onCreateOptionsMenu: 已加载测试菜单");
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
LogUtils.d(TAG, "onOptionsItemSelected() 调用 | itemId: " + item.getItemId());
|
||||
// 主题切换处理
|
||||
if (AESThemeUtil.onThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
Intent mainIntent = new Intent(MainActivity.this, MainActivity.class);
|
||||
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
|
||||
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_ACCENTCOLOR, true);
|
||||
startActivity(mainIntent);
|
||||
return true;
|
||||
}
|
||||
// 开发者功能处理
|
||||
if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
return true;
|
||||
}
|
||||
// 菜单点击事件分发
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
break;
|
||||
case R.id.action_battery_report:
|
||||
startActivity(new Intent(this, BatteryReportActivity.class));
|
||||
break;
|
||||
case R.id.action_clearrecord:
|
||||
startActivity(new Intent(this, ClearRecordActivity.class));
|
||||
break;
|
||||
case R.id.action_changepicture:
|
||||
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_BACKGROUND_SETTINGS_ACTIVITY);
|
||||
break;
|
||||
case R.id.action_unittestactivity:
|
||||
startActivity(new Intent(this, MainUnitTestActivity.class));
|
||||
break;
|
||||
case R.id.action_unittest2activity:
|
||||
startActivity(new Intent(this, MainUnitTest2Activity.class));
|
||||
break;
|
||||
case R.id.action_about:
|
||||
startAboutActivity();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupToolbar() {
|
||||
super.setupToolbar();
|
||||
LogUtils.d(TAG, "setupToolbar() 调用");
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
LogUtils.d(TAG, "setupToolbar: 已隐藏返回按钮");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
LogUtils.d(TAG, "onBackPressed() 调用");
|
||||
moveTaskToBack(true);
|
||||
LogUtils.d(TAG, "onBackPressed: 应用已退至后台");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
LogUtils.d(TAG, "dispatchKeyEvent() 调用 | event: " + event);
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// ======================== 核心初始化方法区 ========================
|
||||
private void initPermissionUtils() {
|
||||
LogUtils.d(TAG, "initPermissionUtils() 调用");
|
||||
mPermissionUtils = PermissionUtils.getInstance();
|
||||
LogUtils.d(TAG, "initPermissionUtils: 权限工具类已初始化");
|
||||
}
|
||||
|
||||
private void initGlobalHandler() {
|
||||
LogUtils.d(TAG, "initGlobalHandler() 调用");
|
||||
if (sGlobalHandler == null) {
|
||||
sGlobalHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
// Activity已销毁则跳过消息处理
|
||||
if (sMainActivity == null || sMainActivity.isFinishing() || sMainActivity.isDestroyed()) {
|
||||
LogUtils.w(TAG, "handleMessage: Activity已销毁,跳过消息 | what: " + msg.what);
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "handleMessage() 调用 | what: " + msg.what);
|
||||
|
||||
switch (msg.what) {
|
||||
case MSG_RELOAD_APPCONFIG:
|
||||
sMainActivity.updateViewData();
|
||||
break;
|
||||
case MSG_CURRENTVALUEBATTERY:
|
||||
if (sMainActivity.mMainContentView != null) {
|
||||
sMainActivity.mMainContentView.updateCurrentBattery(msg.arg1);
|
||||
LogUtils.d(TAG, "handleMessage: 更新当前电量 | value: " + msg.arg1);
|
||||
}
|
||||
break;
|
||||
case MSG_LOAD_BACKGROUND:
|
||||
sMainActivity.reloadBackground();
|
||||
break;
|
||||
case MSG_UPDATE_SERVICE_SWITCH:
|
||||
sMainActivity.updateServiceSwitchUI();
|
||||
break;
|
||||
case MSG_UPDATE_BATTERYDRAWABLE:
|
||||
sMainActivity.updateBatteryDrawable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已创建");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initGlobalHandler: 全局Handler已存在,无需重复创建");
|
||||
}
|
||||
}
|
||||
|
||||
private void initMainContentView() {
|
||||
LogUtils.d(TAG, "initMainContentView() 调用");
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
mMainContentView = new MainContentView(this, rootView, this);
|
||||
LogUtils.d(TAG, "initMainContentView: 核心内容视图已初始化");
|
||||
}
|
||||
|
||||
private void initCriticalView() {
|
||||
LogUtils.d(TAG, "initCriticalView() 调用");
|
||||
sMainActivity = this;
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
if (mToolbar != null) {
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
LogUtils.d(TAG, "initCriticalView: 工具栏已设置标题样式");
|
||||
}
|
||||
LogUtils.d(TAG, "initCriticalView: 工具栏已设置标题样式");
|
||||
}
|
||||
|
||||
private void initCoreUtilsAsync() {
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync() 调用");
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 异步线程启动 | threadId: " + Thread.currentThread().getId());
|
||||
mApplication = (App) getApplication();
|
||||
mAppConfigUtils = AppConfigUtils.getInstance(getApplicationContext());
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
|
||||
|
||||
// 初始化服务控制配置
|
||||
mServiceControlBean = ControlCenterServiceBean.loadBean(getApplicationContext(), ControlCenterServiceBean.class);
|
||||
if (mServiceControlBean == null) {
|
||||
mServiceControlBean = new ControlCenterServiceBean(false);
|
||||
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置不存在,已创建默认配置");
|
||||
}
|
||||
|
||||
// 根据配置启停服务
|
||||
final boolean isServiceEnable = mServiceControlBean.isEnableService();
|
||||
final boolean isServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName());
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务配置状态 | isServiceEnable: " + isServiceEnable + " | isServiceAlive: " + isServiceAlive);
|
||||
|
||||
if (isServiceEnable && !isServiceAlive) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ControlCenterService.startControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已启动");
|
||||
}
|
||||
});
|
||||
} else if (!isServiceEnable && isServiceAlive) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ControlCenterService.stopControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: 服务已停止");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主线程更新UI
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "initCoreUtilsAsync: Activity已销毁,跳过UI更新");
|
||||
return;
|
||||
}
|
||||
// 适配API30,兼容低版本Drawable加载
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame, getTheme());
|
||||
} else {
|
||||
mFrameDrawable = getResources().getDrawable(R.drawable.bg_frame);
|
||||
}
|
||||
updateViewData();
|
||||
sGlobalHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
|
||||
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
|
||||
LogUtils.d(TAG, "initCoreUtilsAsync: UI更新消息已发送");
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// ======================== 视图操作方法区 ========================
|
||||
private void handleReloadBackgroundParam(Intent intent) {
|
||||
LogUtils.d(TAG, "handleReloadBackgroundParam() 调用 | intent: " + intent);
|
||||
if (intent == null) {
|
||||
LogUtils.d(TAG, "handleReloadBackgroundParam: Intent 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isReloadAccentColor = intent.getBooleanExtra(EXTRA_ISRELOAD_ACCENTCOLOR, false);
|
||||
if (isReloadAccentColor) {
|
||||
App.sBackgroundSourceUtils.getCurrentBackgroundBean().setPixelColor(ImageUtils.getColorAccent(this));
|
||||
App.sBackgroundSourceUtils.saveSettings();
|
||||
}
|
||||
|
||||
boolean isReloadBackgroundView = intent.getBooleanExtra(EXTRA_ISRELOAD_BACKGROUNDVIEW, false);
|
||||
if (isReloadBackgroundView) {
|
||||
LogUtils.d(TAG, "handleReloadBackgroundParam: 接收到刷新背景视图指令");
|
||||
reloadBackgroundView();
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadBackgroundView() {
|
||||
LogUtils.d(TAG, "reloadBackgroundView() 调用");
|
||||
mMainContentView.reloadBackgroundView();
|
||||
}
|
||||
|
||||
private void updateViewData() {
|
||||
LogUtils.d(TAG, "updateViewData() 调用");
|
||||
if (mMainContentView == null || mFrameDrawable == null) {
|
||||
LogUtils.e(TAG, "updateViewData: 核心视图或框架背景为空,更新失败");
|
||||
return;
|
||||
}
|
||||
mMainContentView.updateViewData(mFrameDrawable);
|
||||
LogUtils.d(TAG, "updateViewData: 视图数据已更新");
|
||||
}
|
||||
|
||||
void updateBatteryDrawable() {
|
||||
BatteryStyle batteryStyle = BatteryStyleView.getSavedBatteryStyle(this);
|
||||
mMainContentView.updateBatteryDrawable(batteryStyle);
|
||||
}
|
||||
|
||||
public static void sendUpdateBatteryDrawableMessage() {
|
||||
if (sGlobalHandler != null) {
|
||||
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_BATTERYDRAWABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadBackground() {
|
||||
LogUtils.d(TAG, "reloadBackground() 调用");
|
||||
if (mMainContentView == null || mBgSourceUtils == null) {
|
||||
LogUtils.e(TAG, "reloadBackground: 核心视图或背景工具类为空,加载失败");
|
||||
return;
|
||||
}
|
||||
BackgroundBean currentBgBean = mBgSourceUtils.getCurrentBackgroundBean();
|
||||
if (currentBgBean != null) {
|
||||
mMainContentView.backgroundView.loadByBackgroundBean(currentBgBean, true);
|
||||
LogUtils.d(TAG, "reloadBackground: 已加载自定义背景");
|
||||
} else {
|
||||
mMainContentView.backgroundView.setBackgroundResource(R.drawable.default_background);
|
||||
LogUtils.d(TAG, "reloadBackground: 已加载默认背景");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateServiceSwitchUI() {
|
||||
LogUtils.d(TAG, "updateServiceSwitchUI() 调用");
|
||||
if (mMainContentView == null || mServiceControlBean == null) {
|
||||
LogUtils.e(TAG, "updateServiceSwitchUI: 核心视图或服务配置为空,更新失败");
|
||||
return;
|
||||
}
|
||||
boolean configEnabled = mServiceControlBean.isEnableService();
|
||||
mMainContentView.setServiceSwitchEnabled(false);
|
||||
mMainContentView.setServiceSwitchChecked(configEnabled);
|
||||
mMainContentView.setServiceSwitchEnabled(true);
|
||||
LogUtils.d(TAG, "updateServiceSwitchUI: 服务开关已更新 | 状态: " + configEnabled);
|
||||
}
|
||||
|
||||
// ======================== 服务与线程管理方法区 ========================
|
||||
private void toggleServiceEnableState(boolean isEnable) {
|
||||
LogUtils.d(TAG, "toggleServiceEnableState() 调用 | 目标状态: " + isEnable);
|
||||
if (mServiceControlBean == null) {
|
||||
LogUtils.e(TAG, "toggleServiceEnableState: 服务配置为空,切换失败");
|
||||
return;
|
||||
}
|
||||
mServiceControlBean.setIsEnableService(isEnable);
|
||||
ControlCenterServiceBean.saveBean(getApplicationContext(), mServiceControlBean);
|
||||
LogUtils.d(TAG, "toggleServiceEnableState: 服务配置已保存");
|
||||
|
||||
// UI开关联动服务启停
|
||||
if (isEnable) {
|
||||
if (!ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName())) {
|
||||
ControlCenterService.startControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "toggleServiceEnableState: 服务已启动");
|
||||
}
|
||||
} else {
|
||||
ControlCenterService.stopControlCenterService(getApplicationContext());
|
||||
LogUtils.d(TAG, "toggleServiceEnableState: 服务已停止");
|
||||
}
|
||||
|
||||
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_SERVICE_SWITCH);
|
||||
}
|
||||
|
||||
// ======================== 页面跳转方法区 ========================
|
||||
private void startAboutActivity() {
|
||||
LogUtils.d(TAG, "startAboutActivity() 调用");
|
||||
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
APPInfo appInfo = genDefaultAppInfo();
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), aboutIntent, AboutActivity.class);
|
||||
LogUtils.d(TAG, "startAboutActivity: 关于页面已启动");
|
||||
}
|
||||
|
||||
// ======================== 消息发送方法区 ========================
|
||||
private void notifyServiceAppConfigChange() {
|
||||
LogUtils.d(TAG, "notifyServiceAppConfigChange() 调用");
|
||||
ControlCenterService.sendAppConfigStatusUpdateMessage(this);
|
||||
reloadAppConfig();
|
||||
LogUtils.d(TAG, "notifyServiceAppConfigChange: 服务配置已通知更新");
|
||||
}
|
||||
|
||||
public static void reloadAppConfig() {
|
||||
LogUtils.d(TAG, "reloadAppConfig() 调用");
|
||||
if (sGlobalHandler != null) {
|
||||
sGlobalHandler.sendEmptyMessage(MSG_RELOAD_APPCONFIG);
|
||||
LogUtils.d(TAG, "reloadAppConfig: 配置重载消息已发送");
|
||||
} else {
|
||||
LogUtils.w(TAG, "reloadAppConfig: 全局Handler为空,消息发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendCurrentBatteryValueMessage(int value) {
|
||||
LogUtils.d(TAG, "sendCurrentBatteryValueMessage() 调用 | 电量: " + value);
|
||||
if (sGlobalHandler != null) {
|
||||
Message msg = sGlobalHandler.obtainMessage(MSG_CURRENTVALUEBATTERY);
|
||||
msg.arg1 = value;
|
||||
sGlobalHandler.sendMessage(msg);
|
||||
LogUtils.d(TAG, "sendCurrentBatteryValueMessage: 电量消息已发送");
|
||||
} else {
|
||||
LogUtils.w(TAG, "sendCurrentBatteryValueMessage: 全局Handler为空,消息发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 辅助工具方法区 ========================
|
||||
private APPInfo genDefaultAppInfo() {
|
||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||
String branchName = "powerbell";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(R.drawable.ic_launcher);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
|
||||
appInfo.setAppAPKName("PowerBell");
|
||||
appInfo.setAppAPKFolderName("PowerBell");
|
||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||
return appInfo;
|
||||
}
|
||||
|
||||
// ======================== MainContentView 事件回调区 ========================
|
||||
@Override
|
||||
public void onChargeReminderSwitchChanged(boolean isChecked) {
|
||||
LogUtils.d(TAG, "onChargeReminderSwitchChanged() 调用 | isChecked: " + isChecked);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUsageReminderSwitchChanged(boolean isChecked) {
|
||||
LogUtils.d(TAG, "onUsageReminderSwitchChanged() 调用 | isChecked: " + isChecked);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSwitchChanged(boolean isChecked) {
|
||||
LogUtils.d(TAG, "onServiceSwitchChanged() 调用 | isChecked: " + isChecked);
|
||||
toggleServiceEnableState(isChecked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChargeReminderProgressChanged(int progress) {
|
||||
LogUtils.d(TAG, "onChargeReminderProgressChanged() 调用 | progress: " + progress);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUsageReminderProgressChanged(int progress) {
|
||||
LogUtils.d(TAG, "onUsageReminderProgressChanged() 调用 | progress: " + progress);
|
||||
notifyServiceAppConfigChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.models.APPInfo;
|
||||
import cc.winboll.studio.libappbase.views.AboutView;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/13 11:25
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
public class AboutActivity extends WinBoLLActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
private Toolbar mToolbar;
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
// 设置工具栏
|
||||
initToolbar();
|
||||
|
||||
AboutView aboutView = findViewById(R.id.aboutview);
|
||||
aboutView.setAPPInfo(genDefaultAppInfo());
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
}
|
||||
|
||||
private APPInfo genDefaultAppInfo() {
|
||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||
String branchName = "powerbell";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("PowerBell");
|
||||
appInfo.setAppGitOwner("ZhanGSKen");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
|
||||
appInfo.setAppAPKName("PowerBell");
|
||||
appInfo.setAppAPKFolderName("PowerBell");
|
||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||
return appInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,994 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 背景设置页面(支持图片选择、拍照、裁剪、像素拾取、调色板等功能)
|
||||
* 核心:基于强制缓存策略,支持预览与设置提交分离,保留操作状态
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class BackgroundSettingsActivity extends WinBoLLActivity {
|
||||
// ====================== 常量定义(按功能分类排序)======================
|
||||
public static final String TAG = "BackgroundSettingsActivity";
|
||||
|
||||
// 系统版本常量
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
|
||||
// 请求码(按功能分组,从小到大排序)
|
||||
public static final int REQUEST_SELECT_PICTURE = 0;
|
||||
public static final int REQUEST_TAKE_PHOTO = 1;
|
||||
public static final int REQUEST_CROP_IMAGE = 2;
|
||||
private static final int REQUEST_PIXELPICKER = 1001;
|
||||
private static final int REQUEST_CAMERA_PERMISSION = 1004;
|
||||
|
||||
// Bitmap解析常量
|
||||
private static final int BITMAP_MAX_SIZE = 2048;
|
||||
private static final int BITMAP_MAX_SAMPLE_SIZE = 16;
|
||||
|
||||
// ====================== 成员变量(按依赖优先级+功能分类)======================
|
||||
// 工具类实例
|
||||
private BackgroundSourceUtils mBgSourceUtils;
|
||||
private BitmapCacheUtils mBitmapCache;
|
||||
|
||||
// 视图组件
|
||||
private Toolbar mToolbar;
|
||||
private BackgroundView mBackgroundView;
|
||||
|
||||
// 状态标记(volatile保证多线程可见性)
|
||||
private volatile boolean isCommitSettings = false;
|
||||
private volatile boolean isPreviewBackgroundChanged = false;
|
||||
|
||||
// ====================== 生命周期方法(按执行顺序排列)======================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate() 开始初始化");
|
||||
setContentView(R.layout.activity_background_settings);
|
||||
|
||||
// 初始化核心组件
|
||||
initCoreComponents();
|
||||
// 初始化Toolbar与点击事件
|
||||
initToolbar();
|
||||
initClickListeners();
|
||||
LogUtils.d(TAG, "onCreate() 视图与事件绑定完成");
|
||||
|
||||
// 处理分享意图或初始化预览
|
||||
handleIntentOrPreview();
|
||||
// 初始化预览环境并刷新
|
||||
initPreviewEnvironment();
|
||||
|
||||
LogUtils.d(TAG, "onCreate() 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onPostCreate() 执行双重刷新预览");
|
||||
|
||||
// 监听视图布局完成事件
|
||||
mBackgroundView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
// 移除监听,避免重复回调
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
mBackgroundView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
mBackgroundView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
|
||||
// 此时已获取真实宽高
|
||||
int width = mBackgroundView.getWidth();
|
||||
int height = mBackgroundView.getHeight();
|
||||
LogUtils.d(TAG, String.format("onPostCreate() 获取视图尺寸 | width=%d | height=%d", width, height));
|
||||
if (width > 0 && height > 0) {
|
||||
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(BackgroundSettingsActivity.this);
|
||||
appConfigUtils.loadAppConfig();
|
||||
appConfigUtils.mAppConfigBean.setDefaultFrameWidth(width);
|
||||
appConfigUtils.mAppConfigBean.setDefaultFrameHeight(height);
|
||||
appConfigUtils.saveAppConfig();
|
||||
LogUtils.d(TAG, "onPostCreate() 保存默认相框尺寸成功");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, String.format("onActivityResult() | requestCode=%d | resultCode=%d | data=%s",
|
||||
requestCode, resultCode, data != null ? data.toString() : "null"));
|
||||
|
||||
try {
|
||||
if (resultCode != RESULT_OK) {
|
||||
LogUtils.d(TAG, String.format("onActivityResult() 操作取消 | requestCode=%d", requestCode));
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
handleActivityResult(requestCode, data);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("onActivityResult() 异常 | requestCode=%d | 异常信息=%s",
|
||||
requestCode, e.getMessage()));
|
||||
ToastUtils.show("操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
LogUtils.d(TAG, String.format("finish() | isCommitSettings=%b | isPreviewBackgroundChanged=%b",
|
||||
isCommitSettings, isPreviewBackgroundChanged));
|
||||
if (isCommitSettings) {
|
||||
super.finish();
|
||||
} else {
|
||||
handleFinishConfirmation();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 权限回调方法(单独分类)======================
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, String.format("onRequestPermissionsResult() | requestCode=%d | 权限数量=%d | 结果数量=%d",
|
||||
requestCode, permissions.length, grantResults.length));
|
||||
if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||
handleCameraPermissionResult(grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 界面初始化方法(Toolbar + 点击事件)======================
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
}
|
||||
|
||||
private void initClickListeners() {
|
||||
LogUtils.d(TAG, "initClickListeners() 开始绑定按钮点击事件");
|
||||
// 绑定所有按钮点击事件
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton1, onOriginNullClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton2, onReceivedPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton3, onTakePhotoClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton4, onSelectPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton5, onNetworkBackgroundDialog);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton6, onCropPictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton7, onCropFreePictureClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton8, onPixelPickerClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton9, onColorPaletteClickListener);
|
||||
bindClickListener(R.id.activitybackgroundsettingsAButton10, onCleanPixelClickListener);
|
||||
LogUtils.d(TAG, "initClickListeners() 按钮点击事件绑定完成");
|
||||
}
|
||||
|
||||
// 通用按钮绑定工具方法
|
||||
private void bindClickListener(int resId, View.OnClickListener listener) {
|
||||
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d", resId));
|
||||
View view = findViewById(resId);
|
||||
if (view != null) {
|
||||
view.setOnClickListener(listener);
|
||||
LogUtils.d(TAG, String.format("bindClickListener() | resId=%d 绑定成功", resId));
|
||||
} else {
|
||||
LogUtils.e(TAG, String.format("bindClickListener() | 未找到视图:%d", resId));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 按钮点击事件(按功能分类)======================
|
||||
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onOriginNullClickListener() | 取消背景图片");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onOriginNullClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
previewBean.setIsUseBackgroundFile(false);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onSelectPictureClickListener() | 选择图片");
|
||||
launchImageSelector();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onNetworkBackgroundDialog = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onNetworkBackgroundDialog() | 打开网络背景对话框");
|
||||
NetworkBackgroundDialog networkBackgroundDialog = new NetworkBackgroundDialog(BackgroundSettingsActivity.this, new NetworkBackgroundDialog.OnDialogClickListener() {
|
||||
@Override
|
||||
public void onConfirm(String szConfirmFilePath) {
|
||||
LogUtils.d(TAG, String.format("网络背景确认 onConfirm() | 文件路径=%s", szConfirmFilePath));
|
||||
// 拷贝文件到预览数据并启动裁剪
|
||||
if (putUriFileToPreviewSource(new File(szConfirmFilePath))) {
|
||||
startImageCrop(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
LogUtils.d(TAG, "网络背景取消 onCancel()");
|
||||
}
|
||||
});
|
||||
networkBackgroundDialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onCropPictureClickListener() | 固定比例裁剪");
|
||||
startImageCrop(false);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onCropFreePictureClickListener() | 自由裁剪");
|
||||
startImageCrop(true);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onTakePhotoClickListener() | 拍照");
|
||||
// 动态申请相机权限
|
||||
if (ContextCompat.checkSelfPermission(BackgroundSettingsActivity.this, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "拍照准备 | 相机权限未授予,发起申请");
|
||||
ActivityCompat.requestPermissions(
|
||||
BackgroundSettingsActivity.this,
|
||||
new String[]{Manifest.permission.CAMERA},
|
||||
REQUEST_CAMERA_PERMISSION);
|
||||
return;
|
||||
}
|
||||
handleTakePhoto();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onReceivedPictureClickListener() | 恢复收到的图片");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onReceivedPictureClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onPixelPickerClickListener() | 像素拾取");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onPixelPickerClickListener() | 预览Bean为空");
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
return;
|
||||
}
|
||||
String targetImagePath = previewBean.getBackgroundFilePath();
|
||||
File targetFile = new File(targetImagePath);
|
||||
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
LogUtils.e(TAG, String.format("像素拾取失败 | 文件无效:%s", targetImagePath));
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
|
||||
intent.putExtra("imagePath", targetImagePath);
|
||||
startActivityForResult(intent, REQUEST_PIXELPICKER);
|
||||
LogUtils.d(TAG, String.format("像素拾取启动 | 路径:%s", targetImagePath));
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onCleanPixelClickListener() | 清空像素颜色");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onCleanPixelClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
int oldColor = previewBean.getPixelColor();
|
||||
previewBean.setPixelColor(ImageUtils.getColorAccent(BackgroundSettingsActivity.this));
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
ToastUtils.show("像素颜色已清空");
|
||||
LogUtils.d(TAG, String.format("像素清空 | 旧颜色:#%08X", oldColor));
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onColorPaletteClickListener() | 调色板按钮");
|
||||
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "onColorPaletteClickListener() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
int initialColor = previewBean.getPixelColor();
|
||||
LogUtils.d(TAG, String.format("调色板 | 初始颜色:#%08X", initialColor));
|
||||
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
|
||||
@Override
|
||||
public void onColorSelected(int color) {
|
||||
previewBean.setPixelColor(color);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
LogUtils.d(TAG, String.format("颜色选择 | 选中颜色:#%08X", color));
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "调色板 | 对话框已显示");
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 工具方法(通用工具 + 视图工具)======================
|
||||
/**
|
||||
* 生成 FileProvider Uri,适配 Android 7.0+
|
||||
* @param file 目标文件
|
||||
* @return 适配后的Uri,失败返回null
|
||||
*/
|
||||
public Uri getFileProviderUri(File file) {
|
||||
LogUtils.d(TAG, String.format("getFileProviderUri() | 文件路径:%s", (file != null ? file.getAbsolutePath() : "null")));
|
||||
if (file == null) {
|
||||
LogUtils.e(TAG, "getFileProviderUri() | 文件为空");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
|
||||
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
|
||||
} else {
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("getFileProviderUri() | 生成Uri失败:%s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Bitmap 是否有效(未被回收且不为空)
|
||||
* @param bitmap 目标Bitmap
|
||||
* @return 有效返回true,否则false
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
boolean isValid = bitmap != null && !bitmap.isRecycled();
|
||||
LogUtils.d(TAG, String.format("isBitmapValid() | Bitmap有效性校验:%b", isValid));
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
LogUtils.d(TAG, "doubleRefreshPreview() 开始双重刷新预览");
|
||||
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
|
||||
LogUtils.w(TAG, "双重刷新 跳过:对象为空或Activity已结束");
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一重刷新
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBackgroundView.loadByBackgroundBean(previewBean, true);
|
||||
LogUtils.d(TAG, "双重刷新 第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("双重刷新 第一重异常:%s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二重刷新(延迟执行)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBackgroundView.loadByBackgroundBean(previewBean, true);
|
||||
LogUtils.d(TAG, "双重刷新 第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("双重刷新 第二重异常:%s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(按功能分类)======================
|
||||
/**
|
||||
* 初始化核心组件(工具类+视图)
|
||||
*/
|
||||
private void initCoreComponents() {
|
||||
LogUtils.d(TAG, "initCoreComponents() 开始初始化");
|
||||
// 初始化视图
|
||||
mBackgroundView = findViewById(R.id.background_view);
|
||||
if (mBackgroundView == null) {
|
||||
LogUtils.e(TAG, "initCoreComponents() | BackgroundView未找到");
|
||||
}
|
||||
// 初始化工具类
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBitmapCache = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "initCoreComponents() 视图与工具类加载完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理意图或初始化预览
|
||||
*/
|
||||
private void handleIntentOrPreview() {
|
||||
LogUtils.d(TAG, "handleIntentOrPreview() 开始处理");
|
||||
if (handleShareIntent()) {
|
||||
ToastUtils.show("已接收分享图片");
|
||||
LogUtils.d(TAG, "handleIntentOrPreview() | 处理分享意图成功");
|
||||
} else {
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
LogUtils.d(TAG, "handleIntentOrPreview() | 加载当前背景配置");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化预览环境
|
||||
*/
|
||||
private void initPreviewEnvironment() {
|
||||
LogUtils.d(TAG, "initPreviewEnvironment() 开始初始化");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(previewBean);
|
||||
doubleRefreshPreview();
|
||||
LogUtils.d(TAG, "initPreviewEnvironment() 初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分享意图
|
||||
* @return 处理成功返回true,否则false
|
||||
*/
|
||||
private boolean handleShareIntent() {
|
||||
LogUtils.d(TAG, "handleShareIntent() 开始处理");
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
LogUtils.d(TAG, String.format("分享处理 | action:%s,type:%s", action, type));
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
|
||||
showSharePreviewDialog();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示分享图片预览对话框
|
||||
*/
|
||||
private void showSharePreviewDialog() {
|
||||
LogUtils.d(TAG, "showSharePreviewDialog() 开始显示");
|
||||
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener() {
|
||||
@Override
|
||||
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
|
||||
LogUtils.d(TAG, String.format("分享确认 | Uri:%s", uriRecivedPicture.toString()));
|
||||
if (putUriFileToPreviewSource(uriRecivedPicture)) {
|
||||
startImageCrop(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
dlg.show();
|
||||
LogUtils.d(TAG, "分享处理 | 显示图片预览对话框");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片类型
|
||||
* @param mimeType MIME类型
|
||||
* @return 是图片返回true,否则false
|
||||
*/
|
||||
private boolean isImageType(String mimeType) {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
}
|
||||
String lowerMimeType = mimeType.toLowerCase();
|
||||
LogUtils.d(TAG, String.format("isImageType() | mimeType: %s, lowerMimeType: %s", mimeType, lowerMimeType));
|
||||
return lowerMimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
*/
|
||||
private void launchImageSelector() {
|
||||
LogUtils.d(TAG, "launchImageSelector() 启动图片选择器");
|
||||
Intent[] intents = createImageSelectorIntents();
|
||||
Intent validIntent = findValidIntent(intents);
|
||||
|
||||
if (validIntent != null) {
|
||||
launchImageChooser(validIntent);
|
||||
} else {
|
||||
showNoGalleryDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片选择器意图数组
|
||||
* @return 意图数组
|
||||
*/
|
||||
private Intent[] createImageSelectorIntents() {
|
||||
LogUtils.d(TAG, "createImageSelectorIntents() 开始创建");
|
||||
Intent[] intents = new Intent[3];
|
||||
// ACTION_GET_CONTENT
|
||||
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
getContentIntent.setType("image/*");
|
||||
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intents[0] = getContentIntent;
|
||||
|
||||
// ACTION_PICK
|
||||
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
pickIntent.setType("image/*");
|
||||
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intents[1] = pickIntent;
|
||||
|
||||
// ACTION_OPEN_DOCUMENT(API19+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
openDocIntent.setType("image/*");
|
||||
openDocIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
intents[2] = openDocIntent;
|
||||
}
|
||||
LogUtils.d(TAG, "createImageSelectorIntents() 意图数组创建完成");
|
||||
return intents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找有效的意图
|
||||
* @param intents 意图数组
|
||||
* @return 有效意图,无则返回null
|
||||
*/
|
||||
private Intent findValidIntent(Intent[] intents) {
|
||||
LogUtils.d(TAG, "findValidIntent() 开始查找");
|
||||
for (Intent intent : intents) {
|
||||
if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
|
||||
LogUtils.d(TAG, "findValidIntent() | 找到有效意图");
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "findValidIntent() | 无有效意图");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
* @param validIntent 有效意图
|
||||
*/
|
||||
private void launchImageChooser(Intent validIntent) {
|
||||
LogUtils.d(TAG, "launchImageChooser() 启动选择器");
|
||||
Intent chooser = Intent.createChooser(validIntent, "选择图片");
|
||||
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
|
||||
LogUtils.d(TAG, "launchImageChooser() | 启动图片选择");
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示无相册应用提示对话框
|
||||
*/
|
||||
private void showNoGalleryDialog() {
|
||||
LogUtils.d(TAG, "showNoGalleryDialog() | 无相册应用");
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("未找到相册应用,请安装后重试");
|
||||
new AlertDialog.Builder(BackgroundSettingsActivity.this)
|
||||
.setTitle("无图片选择应用")
|
||||
.setMessage("需要安装相册应用才能选择图片")
|
||||
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
launchGalleryMarket();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用商店下载相册
|
||||
*/
|
||||
private void launchGalleryMarket() {
|
||||
LogUtils.d(TAG, "launchGalleryMarket() 启动应用商店");
|
||||
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
|
||||
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
|
||||
if (marketIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(marketIntent);
|
||||
LogUtils.d(TAG, "launchGalleryMarket() | 启动成功");
|
||||
} else {
|
||||
ToastUtils.show("无法打开应用商店");
|
||||
LogUtils.e(TAG, "launchGalleryMarket() | 启动失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理操作取消或失败
|
||||
*/
|
||||
private void handleOperationCancelOrFail() {
|
||||
LogUtils.d(TAG, "handleOperationCancelOrFail() 操作取消或失败");
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
ToastUtils.show("操作取消或失败");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照逻辑(权限通过后执行)
|
||||
*/
|
||||
void handleTakePhoto() {
|
||||
LogUtils.d(TAG, "handleTakePhoto() 开始处理拍照");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "handleTakePhoto() | 预览Bean为空");
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
File takePhotoFile = new File(previewBean.getBackgroundFilePath());
|
||||
if (!takePhotoFile.exists()) {
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
LogUtils.e(TAG, String.format("handleTakePhoto() | 文件不存在:%s", takePhotoFile.getAbsolutePath()));
|
||||
return;
|
||||
}
|
||||
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
try {
|
||||
Uri photoUri = getFileProviderUri(takePhotoFile);
|
||||
if (photoUri == null) {
|
||||
throw new Exception("生成FileProvider Uri失败");
|
||||
}
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
|
||||
LogUtils.d(TAG, String.format("handleTakePhoto() | Uri:%s", photoUri.toString()));
|
||||
} catch (Exception e) {
|
||||
String errMsg = "拍照启动异常:" + e.getMessage();
|
||||
ToastUtils.show(errMsg.substring(0, 20));
|
||||
LogUtils.e(TAG, String.format("handleTakePhoto() | %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理ActivityResult分发
|
||||
* @param requestCode 请求码
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleActivityResult(int requestCode, Intent data) {
|
||||
LogUtils.d(TAG, String.format("handleActivityResult() | 处理请求码:%d", requestCode));
|
||||
switch (requestCode) {
|
||||
case REQUEST_SELECT_PICTURE:
|
||||
handleSelectPictureResult(data);
|
||||
break;
|
||||
case REQUEST_TAKE_PHOTO:
|
||||
handleTakePhotoResult(data);
|
||||
break;
|
||||
case REQUEST_CROP_IMAGE:
|
||||
handleCropImageResult(data);
|
||||
break;
|
||||
case REQUEST_PIXELPICKER:
|
||||
handlePixelPickerResult();
|
||||
break;
|
||||
default:
|
||||
LogUtils.d(TAG, String.format("handleActivityResult() | 未知requestCode:%d", requestCode));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleTakePhotoResult(Intent data) {
|
||||
LogUtils.d(TAG, "handleTakePhotoResult() 处理拍照结果");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "handleTakePhotoResult() | 预览Bean为空");
|
||||
return;
|
||||
}
|
||||
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(false);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
|
||||
startImageCrop(false);
|
||||
LogUtils.d(TAG, "handleTakePhotoResult() | 已启动裁剪");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选图结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleSelectPictureResult(Intent data) {
|
||||
LogUtils.d(TAG, "handleSelectPictureResult() 处理选图结果");
|
||||
Uri selectedImage = data.getData();
|
||||
if (selectedImage == null) {
|
||||
ToastUtils.show("图片Uri为空");
|
||||
LogUtils.e(TAG, "handleSelectPictureResult() | Uri为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, String.format("handleSelectPictureResult() | 系统返回Uri : %s", selectedImage.toString()));
|
||||
|
||||
// 申请持久化权限(API33+)
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
getContentResolver().takePersistableUriPermission(
|
||||
selectedImage,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
LogUtils.d(TAG, "handleSelectPictureResult() | 已添加持久化权限");
|
||||
}
|
||||
|
||||
// 同步文件并启动裁剪
|
||||
if (putUriFileToPreviewSource(selectedImage)) {
|
||||
LogUtils.d(TAG, "handleSelectPictureResult() | 路径绑定完成");
|
||||
startImageCrop(false);
|
||||
} else {
|
||||
ToastUtils.show("图片同步失败");
|
||||
LogUtils.e(TAG, "handleSelectPictureResult() | 文件复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Uri 文件同步到预览 Bean
|
||||
* @param srcUriFile 源Uri
|
||||
* @return 同步成功返回true,否则false
|
||||
*/
|
||||
private boolean putUriFileToPreviewSource(Uri srcUriFile) {
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源Uri:%s", srcUriFile.toString()));
|
||||
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
LogUtils.e(TAG, "putUriFileToPreviewSource() | Uri解析路径为空");
|
||||
return false;
|
||||
}
|
||||
File srcFile = new File(filePath);
|
||||
return putUriFileToPreviewSource(srcFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 File 同步到预览 Bean
|
||||
* @param srcFile 源文件
|
||||
* @return 同步成功返回true,否则false
|
||||
*/
|
||||
private boolean putUriFileToPreviewSource(File srcFile) {
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 源文件:%s", srcFile.getAbsolutePath()));
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
File dstFile = new File(previewBean.getBackgroundFilePath());
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource() | 目标文件:%s", dstFile.getAbsolutePath()));
|
||||
if (FileUtils.copyFile(srcFile, dstFile)) {
|
||||
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件拷贝成功");
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "putUriFileToPreviewSource() | 文件无法拷贝");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪结果
|
||||
* @param data 回调数据
|
||||
*/
|
||||
private void handleCropImageResult(Intent data) {
|
||||
LogUtils.d(TAG, "handleCropImageResult() 处理裁剪结果");
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "handleCropImageResult() | 预览Bean为空");
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
File cropTempFile = new File(previewBean.getBackgroundScaledCompressFilePath());
|
||||
boolean isFileExist = cropTempFile.exists();
|
||||
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
|
||||
long fileSize = isFileExist ? cropTempFile.length() : 0;
|
||||
boolean isCropSuccess = isFileExist && isFileReadable && fileSize > 100;
|
||||
|
||||
if (isCropSuccess) {
|
||||
handleCropSuccess(previewBean, fileSize);
|
||||
} else {
|
||||
handleCropFailure(isFileExist, isFileReadable, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪成功
|
||||
* @param previewBean 预览Bean
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void handleCropSuccess(BackgroundBean previewBean, long fileSize) {
|
||||
LogUtils.d(TAG, String.format("handleCropSuccess() | 裁剪成功,文件大小:%d", fileSize));
|
||||
isPreviewBackgroundChanged = true;
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪失败
|
||||
* @param isFileExist 文件是否存在
|
||||
* @param isFileReadable 文件是否可读
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void handleCropFailure(boolean isFileExist, boolean isFileReadable, long fileSize) {
|
||||
LogUtils.e(TAG, String.format("handleCropFailure() | 裁剪失败,文件状态:存在=%b,可读=%b,大小=%d",
|
||||
isFileExist, isFileReadable, fileSize));
|
||||
handleOperationCancelOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理像素拾取结果
|
||||
*/
|
||||
private void handlePixelPickerResult() {
|
||||
LogUtils.d(TAG, "handlePixelPickerResult() 处理像素拾取结果");
|
||||
doubleRefreshPreview();
|
||||
isPreviewBackgroundChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相机权限申请结果
|
||||
* @param grantResults 权限结果数组
|
||||
*/
|
||||
private void handleCameraPermissionResult(int[] grantResults) {
|
||||
LogUtils.d(TAG, "handleCameraPermissionResult() 处理相机权限结果");
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予成功");
|
||||
handleTakePhoto();
|
||||
} else {
|
||||
LogUtils.d(TAG, "handleCameraPermissionResult() | 相机权限授予失败");
|
||||
ToastUtils.show("相机权限被拒绝,无法拍照");
|
||||
// 引导用户到设置页面开启权限(用户选择不再询问时)
|
||||
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
|
||||
launchAppSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用设置页面
|
||||
*/
|
||||
private void launchAppSettings() {
|
||||
LogUtils.d(TAG, "launchAppSettings() 启动应用设置页面");
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
ToastUtils.show("请在设置中开启相机权限");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Finish确认对话框
|
||||
*/
|
||||
private void handleFinishConfirmation() {
|
||||
LogUtils.d(TAG, "handleFinishConfirmation() 处理Finish确认");
|
||||
if (isPreviewBackgroundChanged) {
|
||||
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onYes() {
|
||||
mBgSourceUtils.commitPreviewSourceToCurrent();
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
Intent mainIntent = new Intent(BackgroundSettingsActivity.this, MainActivity.class);
|
||||
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
|
||||
startActivity(mainIntent);
|
||||
LogUtils.d(TAG, "handleFinishConfirmation() | 确认设置,启动MainActivity并刷新背景");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
LogUtils.d(TAG, "handleFinishConfirmation() | 取消设置,关闭页面");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片裁剪
|
||||
* @param isFreeCrop 是否自由裁剪
|
||||
*/
|
||||
private void startImageCrop(boolean isFreeCrop) {
|
||||
LogUtils.d(TAG, String.format("startImageCrop() | 是否自由裁剪:%b", isFreeCrop));
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "startImageCrop() | 预览Bean为空");
|
||||
ToastUtils.show("裁剪失败:无有效图片");
|
||||
return;
|
||||
}
|
||||
int width = isFreeCrop ? 0 : mBackgroundView.getWidth();
|
||||
int height = isFreeCrop ? 0 : mBackgroundView.getHeight();
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
previewBean,
|
||||
width,
|
||||
height,
|
||||
isFreeCrop,
|
||||
REQUEST_CROP_IMAGE);
|
||||
LogUtils.d(TAG, String.format("startImageCrop() | 目标尺寸:%dx%d", width, height));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,598 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 电池报告页面,统计应用24小时运行时长与电池消耗情况
|
||||
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量(按功能分类) =========================
|
||||
public static final String TAG = "BatteryReportActivity";
|
||||
private static final long ONE_DAY_MS = 24 * 3600 * 1000; // 24小时毫秒数
|
||||
private static final long ONE_MINUTE_MS = 60 * 1000; // 1分钟毫秒数
|
||||
|
||||
// ======================== 成员变量(按依赖优先级+功能分类) =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private RecyclerView rvBatteryReport;
|
||||
private EditText etSearch;
|
||||
|
||||
// 数据与适配器
|
||||
private BatteryReportAdapter adapter;
|
||||
private List<AppBatteryModel> dataList = new ArrayList<>();
|
||||
private List<AppBatteryModel> filteredList = new ArrayList<>();
|
||||
|
||||
// 电池相关
|
||||
private BroadcastReceiver batteryReceiver;
|
||||
private int batteryCapacity = 5400; // 电池容量(mAh)
|
||||
private float lastBatteryPercent = 100.0f; // 上次电池百分比
|
||||
private long lastCheckTime = System.currentTimeMillis(); // 上次检查时间戳
|
||||
|
||||
// 缓存相关
|
||||
private Map<String, Long> appRunTimeCache = new HashMap<>();
|
||||
private Map<String, String> packageToAppNameCache = new HashMap<>();
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法(按执行顺序排列) =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_battery_report);
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化PackageManager
|
||||
mPackageManager = getPackageManager();
|
||||
LogUtils.d(TAG, "【onCreate】基础组件初始化完成");
|
||||
|
||||
// 权限检查(Java7 传统条件判断)
|
||||
if (!hasUsageStatsPermission(this)) {
|
||||
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
|
||||
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
|
||||
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限,引导用户开启");
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
|
||||
loadAllAppPackage();
|
||||
preCacheAllAppNames();
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateInitial24hTotalConsumption();
|
||||
filteredList.addAll(dataList);
|
||||
LogUtils.d(TAG, "【onCreate】数据初始化完成,原始数据量:" + dataList.size());
|
||||
|
||||
// 初始化适配器
|
||||
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
|
||||
rvBatteryReport.setAdapter(adapter);
|
||||
LogUtils.d(TAG, "【onCreate】适配器初始化完成,过滤后数据量:" + filteredList.size());
|
||||
|
||||
// 绑定搜索监听 + 注册电池广播
|
||||
bindSearchListener();
|
||||
registerBatteryReceiver();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// Java7 显式非空判断
|
||||
if (batteryReceiver != null) {
|
||||
unregisterReceiver(batteryReceiver);
|
||||
LogUtils.d(TAG, "【onDestroy】电池广播已注销");
|
||||
}
|
||||
LogUtils.d(TAG, "【onDestroy】BatteryReportActivity 销毁完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化RecyclerView与搜索框
|
||||
etSearch = (EditText) findViewById(R.id.et_search);
|
||||
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
|
||||
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 搜索监听绑定方法 =========================
|
||||
private void bindSearchListener() {
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String keyword = s.toString().trim();
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化:" + keyword);
|
||||
filterAppsByPackageAndName(keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成");
|
||||
}
|
||||
|
||||
// ======================== 电池广播注册方法 =========================
|
||||
private void registerBatteryReceiver() {
|
||||
batteryReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int level = intent.getIntExtra("level", 100);
|
||||
int scale = intent.getIntExtra("scale", 100);
|
||||
float currentPercent = (float) level / scale * 100;
|
||||
LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
|
||||
|
||||
if (currentPercent < lastBatteryPercent) {
|
||||
float dropPercent = lastBatteryPercent - currentPercent;
|
||||
long duration = System.currentTimeMillis() - lastCheckTime;
|
||||
LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration));
|
||||
|
||||
// 更新运行时长并计算耗电
|
||||
appRunTimeCache = getAppRunTime();
|
||||
updateAppRunTimeToModel();
|
||||
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache);
|
||||
}
|
||||
|
||||
// 刷新记录
|
||||
lastBatteryPercent = currentPercent;
|
||||
lastCheckTime = System.currentTimeMillis();
|
||||
}
|
||||
};
|
||||
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成");
|
||||
}
|
||||
|
||||
// ======================== 权限检查方法 =========================
|
||||
/**
|
||||
* 检查是否拥有使用情况访问权限
|
||||
* @param context 上下文
|
||||
* @return 拥有权限返回true,否则返回false
|
||||
*/
|
||||
private boolean hasUsageStatsPermission(Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LogUtils.w(TAG, "【hasUsageStatsPermission】系统版本低于LOLLIPOP,不支持使用情况访问权限");
|
||||
return false;
|
||||
}
|
||||
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
if (manager == null) {
|
||||
LogUtils.e(TAG, "【hasUsageStatsPermission】获取UsageStatsManager失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - ONE_MINUTE_MS;
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
boolean hasPermission = statsList != null && !statsList.isEmpty();
|
||||
LogUtils.d(TAG, "【hasUsageStatsPermission】使用情况访问权限检查结果:" + hasPermission);
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
// ======================== 数据加载与缓存方法 =========================
|
||||
/**
|
||||
* 加载所有应用包名,初始化数据模型
|
||||
*/
|
||||
private void loadAllAppPackage() {
|
||||
List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
dataList.clear();
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用");
|
||||
|
||||
for (ApplicationInfo appInfo : appList) {
|
||||
String packageName = appInfo.packageName;
|
||||
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
|
||||
}
|
||||
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名");
|
||||
}
|
||||
|
||||
/**
|
||||
* 预缓存所有应用名称,减少PackageManager重复调用
|
||||
*/
|
||||
private void preCacheAllAppNames() {
|
||||
packageToAppNameCache.clear();
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
|
||||
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
String appName = getAppNameByPackage(packageName);
|
||||
packageToAppNameCache.put(packageName, appName);
|
||||
}
|
||||
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过包名获取应用名称,带异常处理
|
||||
* @param packageName 应用包名
|
||||
* @return 应用名称,获取失败返回包名
|
||||
*/
|
||||
private String getAppNameByPackage(String packageName) {
|
||||
LogUtils.v(TAG, "【getAppNameByPackage】查询包名:" + packageName);
|
||||
try {
|
||||
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
|
||||
return mPackageManager.getApplicationLabel(appInfo).toString();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
return packageName;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运行时长到数据模型
|
||||
*/
|
||||
private void updateAppRunTimeToModel() {
|
||||
int updateCount = 0;
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
|
||||
model.setRunTime(runTime);
|
||||
if (runTime > 0) {
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成,数据量:" + dataList.size() + ",更新运行时长应用数:" + updateCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用24小时运行时长
|
||||
* @return 应用包名-运行时长(ms)映射
|
||||
*/
|
||||
private Map<String, Long> getAppRunTime() {
|
||||
Map<String, Long> runTimeMap = new HashMap<>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
android.app.usage.UsageStatsManager manager =
|
||||
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - ONE_DAY_MS; // 近24小时
|
||||
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
|
||||
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
|
||||
|
||||
for (android.app.usage.UsageStats stats : statsList) {
|
||||
long runTimeMs = stats.getTotalTimeInForeground();
|
||||
String packageName = stats.getPackageName();
|
||||
runTimeMap.put(packageName, runTimeMs);
|
||||
LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs));
|
||||
if (packageName.equals("aidepro.top")) {
|
||||
LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量:" + runTimeMap.size());
|
||||
return runTimeMap;
|
||||
}
|
||||
|
||||
// ======================== 核心计算方法 =========================
|
||||
/**
|
||||
* 初始化时计算24小时累计耗电(赋值给totalConsumption)
|
||||
* 逻辑:基于24小时运行时长占比,分配当前电池容量的理论24小时消耗
|
||||
*/
|
||||
private void calculateInitial24hTotalConsumption() {
|
||||
long total24hRunTime = 0;
|
||||
// 1. 计算24小时内所有应用总运行时长
|
||||
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
|
||||
total24hRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime));
|
||||
|
||||
// 2. 按运行时长占比分配24小时累计耗电
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
|
||||
|
||||
float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
|
||||
float initialTotalConsumption = batteryCapacity * ratio;
|
||||
model.setTotalConsumption(initialTotalConsumption);
|
||||
LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化:" + initialTotalConsumption + " mAh");
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单次耗电(赋值给consumption)+ 累加至累计耗电
|
||||
* @param dropPercent 电池下降百分比
|
||||
* @param runTimeMap 应用运行时长映射
|
||||
*/
|
||||
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】开始计算,电池下降百分比:" + dropPercent);
|
||||
long totalSingleRunTime = 0;
|
||||
// 1. 计算本次电池下降期间的总运行时长
|
||||
for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
|
||||
totalSingleRunTime += entry.getValue();
|
||||
}
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长:" + formatRunTime(totalSingleRunTime));
|
||||
|
||||
// 2. 遍历计算每个应用的单次耗电并累加
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
|
||||
|
||||
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
|
||||
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
|
||||
model.setConsumption(singleConsumption);
|
||||
|
||||
// 累加至累计耗电
|
||||
float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
|
||||
model.setTotalConsumption(newTotalConsumption);
|
||||
model.setRunTime(appSingleRunTime);
|
||||
|
||||
LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s:单次耗电%.1f mAh,累计耗电%.1f mAh",
|
||||
packageName, singleConsumption, newTotalConsumption));
|
||||
}
|
||||
|
||||
// 3. 按累计耗电降序排序
|
||||
Collections.sort(dataList, new Comparator<AppBatteryModel>() {
|
||||
@Override
|
||||
public int compare(AppBatteryModel m1, AppBatteryModel m2) {
|
||||
return Float.compare(m2.getTotalConsumption(), m1.getTotalConsumption());
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 重新过滤并刷新列表
|
||||
filterAppsByPackageAndName(etSearch.getText().toString().trim());
|
||||
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成,列表已刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 双维度过滤(包名+应用名)
|
||||
* @param keyword 搜索关键词
|
||||
*/
|
||||
private void filterAppsByPackageAndName(String keyword) {
|
||||
filteredList.clear();
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
filteredList.addAll(dataList);
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空,显示全部应用,数量:" + filteredList.size());
|
||||
} else {
|
||||
String lowerKeyword = keyword.toLowerCase();
|
||||
for (AppBatteryModel model : dataList) {
|
||||
String packageName = model.getPackageName();
|
||||
String packageNameLower = packageName.toLowerCase();
|
||||
String appName = packageToAppNameCache.get(packageName);
|
||||
String appNameLower = appName.toLowerCase();
|
||||
|
||||
boolean isMatched = packageNameLower.contains(lowerKeyword) || appNameLower.contains(lowerKeyword);
|
||||
if (isMatched) {
|
||||
filteredList.add(model);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词:" + keyword + ",匹配应用数量:" + filteredList.size());
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 格式化运行时长
|
||||
* @param runTimeMs 运行时长(ms)
|
||||
* @return 格式化后的运行时长字符串
|
||||
*/
|
||||
private String formatRunTime(long runTimeMs) {
|
||||
if (runTimeMs <= 0) {
|
||||
return "0秒";
|
||||
}
|
||||
long seconds = runTimeMs / 1000;
|
||||
long hours = seconds / 3600;
|
||||
long minutes = (seconds % 3600) / 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%d时%d分%d秒", hours, minutes, seconds);
|
||||
} else if (minutes > 0) {
|
||||
return String.format("%d分%d秒", minutes, seconds);
|
||||
} else {
|
||||
return String.format("%d秒", seconds);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:数据模型 =========================
|
||||
/**
|
||||
* 应用电池数据模型
|
||||
* - consumption:单次耗电(两次电池广播间的消耗)
|
||||
* - totalConsumption:累计耗电(24小时初始化值+后续单次累加)
|
||||
* - runTime:运行时长(ms)
|
||||
*/
|
||||
public static class AppBatteryModel {
|
||||
private String packageName; // 应用包名(核心标识)
|
||||
private float consumption; // 单次耗电(mAh)
|
||||
private float totalConsumption;// 累计耗电(mAh)
|
||||
private long runTime; // 运行时长(ms)
|
||||
|
||||
// Java7 显式构造
|
||||
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
|
||||
this.packageName = packageName;
|
||||
this.consumption = consumption;
|
||||
this.totalConsumption = totalConsumption;
|
||||
this.runTime = runTime;
|
||||
}
|
||||
|
||||
// Getter/Setter
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public float getConsumption() {
|
||||
return consumption;
|
||||
}
|
||||
|
||||
public void setConsumption(float consumption) {
|
||||
this.consumption = consumption;
|
||||
}
|
||||
|
||||
public float getTotalConsumption() {
|
||||
return totalConsumption;
|
||||
}
|
||||
|
||||
public void setTotalConsumption(float totalConsumption) {
|
||||
this.totalConsumption = totalConsumption;
|
||||
}
|
||||
|
||||
public long getRunTime() {
|
||||
return runTime;
|
||||
}
|
||||
|
||||
public void setRunTime(long runTime) {
|
||||
this.runTime = runTime;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部类:RecyclerView适配器 =========================
|
||||
/**
|
||||
* 电池报告列表适配器,显示应用名称、累计耗电、运行时长
|
||||
*/
|
||||
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
|
||||
private Context mContext;
|
||||
private List<AppBatteryModel> mDataList;
|
||||
private PackageManager mPm;
|
||||
private Map<String, String> mPackageToNameCache;
|
||||
|
||||
// Java7 显式构造
|
||||
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
|
||||
PackageManager pm, Map<String, String> packageToNameCache) {
|
||||
this.mContext = context;
|
||||
this.mDataList = dataList;
|
||||
this.mPm = pm;
|
||||
this.mPackageToNameCache = packageToNameCache;
|
||||
LogUtils.d(TAG, "【BatteryReportAdapter】适配器构造完成,数据量:" + dataList.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View itemView = LayoutInflater.from(mContext)
|
||||
.inflate(android.R.layout.simple_list_item_2, parent, false);
|
||||
return new ViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
// Java7 显式非空判断
|
||||
if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
|
||||
holder.tvAppName.setText("未知应用");
|
||||
holder.tvConsumption.setText("累计耗电:0.0 mAh | 运行时长:0秒");
|
||||
LogUtils.w(TAG, "【onBindViewHolder】数据异常,位置:" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
AppBatteryModel model = mDataList.get(position);
|
||||
String packageName = model.getPackageName();
|
||||
String appName = "";
|
||||
|
||||
// 优先从缓存获取应用名
|
||||
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
|
||||
appName = mPackageToNameCache.get(packageName);
|
||||
} else {
|
||||
// 缓存无数据时兜底查询
|
||||
try {
|
||||
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
|
||||
appName = mPm.getApplicationLabel(appInfo).toString();
|
||||
if (mPackageToNameCache != null) {
|
||||
mPackageToNameCache.put(packageName, appName);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
appName = packageName;
|
||||
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 显示逻辑:应用名称 + 累计耗电 + 运行时长
|
||||
holder.tvAppName.setText(appName);
|
||||
String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
|
||||
String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
|
||||
model.getTotalConsumption(), runTimeStr);
|
||||
holder.tvConsumption.setText(totalConsumptionText);
|
||||
|
||||
// 显示优化
|
||||
holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
|
||||
holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
|
||||
holder.tvAppName.setTextSize(16);
|
||||
holder.tvConsumption.setTextSize(14);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mDataList == null ? 0 : mDataList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder:绑定系统布局控件
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvAppName; // 应用名称
|
||||
TextView tvConsumption; // 累计耗电 + 运行时长
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvAppName = (TextView) itemView.findViewById(android.R.id.text1);
|
||||
tvConsumption = (TextView) itemView.findViewById(android.R.id.text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量(按功能分类) =========================
|
||||
public static final String TAG = "ClearRecordActivity";
|
||||
private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned.";
|
||||
|
||||
// ======================== 成员变量(按依赖优先级+功能分类) =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private TextView mtvRecordText;
|
||||
private TextView tvAOHPCTCSeekBarMSG;
|
||||
private AOHPCTCSeekBar aOHPCTCSeekBar;
|
||||
|
||||
// 应用与配置
|
||||
private App mApplication;
|
||||
private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法(按执行顺序排列) =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_clearrecord);
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
|
||||
|
||||
// 初始化应用实例
|
||||
mApplication = (App) getApplication();
|
||||
LogUtils.d(TAG, "【onCreate】应用实例初始化完成");
|
||||
|
||||
// 初始化核心逻辑
|
||||
initView();
|
||||
initSeekBar();
|
||||
initRecordText();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化Toolbar与显示文本组件
|
||||
*/
|
||||
private void initView() {
|
||||
// 初始化Toolbar
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回按钮,关闭当前页面");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化显示文本组件
|
||||
tvAOHPCTCSeekBarMSG = (TextView) findViewById(R.id.activityclearrecordTextView1);
|
||||
mtvRecordText = (TextView) findViewById(R.id.activityclearrecordTextView2);
|
||||
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
|
||||
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化滑动清理控件,设置回调监听
|
||||
*/
|
||||
private void initSeekBar() {
|
||||
aOHPCTCSeekBar = (AOHPCTCSeekBar) findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
|
||||
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
|
||||
aOHPCTCSeekBar.setThumbOffset(0);
|
||||
aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
LogUtils.d(TAG, "【onOHPCommit】滑动清理触发,开始执行记录清理逻辑");
|
||||
// 清理电池历史记录
|
||||
mApplication.clearBatteryHistory();
|
||||
// 发送广播更新前台通知
|
||||
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
// 提示清理成功
|
||||
ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS);
|
||||
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成,已发送前台通知更新广播");
|
||||
}
|
||||
});
|
||||
|
||||
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成,回调监听已绑定");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 初始化记录显示文本,根据配置切换带换行/不带换行格式
|
||||
*/
|
||||
void initRecordText() {
|
||||
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
|
||||
String szRecordText;
|
||||
|
||||
// 判空处理:避免空列表导致异常
|
||||
if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
|
||||
szRecordText = getString(R.string.msg_no_battery_record);
|
||||
LogUtils.d(TAG, "【initRecordText】无电池记录数据,显示空记录提示文本");
|
||||
} else {
|
||||
// 根据配置切换显示格式
|
||||
if (mIsShowRecordWithEnter) {
|
||||
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
LogUtils.d(TAG, String.format("【initRecordText】使用带换行格式显示记录,记录数量:%d", listBatteryInfo.size()));
|
||||
} else {
|
||||
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
LogUtils.d(TAG, String.format("【initRecordText】使用无换行格式显示记录,记录数量:%d", listBatteryInfo.size()));
|
||||
}
|
||||
}
|
||||
|
||||
mtvRecordText.setText(szRecordText);
|
||||
LogUtils.d(TAG, "【initRecordText】记录显示文本刷新完成");
|
||||
}
|
||||
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 切换记录显示格式(带换行/不带换行)
|
||||
* @param view 触发事件的Switch控件
|
||||
*/
|
||||
public void onShowRecordWithEnter(View view) {
|
||||
Switch swShowRecordWithEnter = (Switch) view;
|
||||
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
|
||||
LogUtils.d(TAG, String.format("【onShowRecordWithEnter】记录显示格式切换,带换行显示:%b", mIsShowRecordWithEnter));
|
||||
// 刷新记录显示
|
||||
initRecordText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* 像素拾取页面,支持加载图片并拾取指定位置像素颜色,同步至背景配置
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/22 14:15
|
||||
*/
|
||||
public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "PixelPickerActivity";
|
||||
public static final String EXTRA_IMAGE_PATH = "imagePath"; // 图片路径传递键
|
||||
// 提示文本常量
|
||||
private static final String MSG_IMAGE_LOADED = "图片已加载,点击获取像素值";
|
||||
private static final String MSG_NO_IMAGE_PATH = "未找到图片路径";
|
||||
private static final String MSG_IMAGE_LOAD_FAILED = "图片加载失败";
|
||||
private static final String MSG_FILE_NOT_EXIST = "图片文件不存在";
|
||||
private static final String MSG_FILE_NOT_FOUND = "图片文件未找到";
|
||||
private static final String MSG_PIXEL_OUT_OF_RANGE = "像素坐标超出范围";
|
||||
private static final String MSG_TOUCH_OUT_OF_IMAGE = "点击位置超出图片显示范围";
|
||||
private static final String MSG_PIXEL_CALC_FAILED = "计算像素位置失败";
|
||||
private static final String MSG_PIXEL_RECORDED = "已记录像素值";
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
// UI组件
|
||||
private Toolbar mToolbar;
|
||||
private ImageView imageView;
|
||||
private TextView infoText;
|
||||
private ViewGroup imageContainer;
|
||||
private RelativeLayout mainLayout;
|
||||
// 图片与像素数据
|
||||
private Bitmap originalBitmap; // 原始图片Bitmap(用于像素拾取)
|
||||
|
||||
// ======================== 接口实现方法 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pixelpicker);
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化开始");
|
||||
|
||||
// 初始化UI组件
|
||||
initView();
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 加载传递的图片
|
||||
loadImageFromIntent();
|
||||
// 绑定图片触摸事件
|
||||
bindImageTouchListener();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】PixelPickerActivity 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "【onResume】PixelPickerActivity 恢复显示");
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 回收Bitmap资源,避免内存泄漏
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
LogUtils.d(TAG, "【onDestroy】原始图片Bitmap资源已回收");
|
||||
}
|
||||
LogUtils.d(TAG, "【onDestroy】PixelPickerActivity 销毁完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化方法 =========================
|
||||
/**
|
||||
* 初始化所有UI组件
|
||||
*/
|
||||
private void initView() {
|
||||
imageView = findViewById(R.id.imageView);
|
||||
infoText = findViewById(R.id.infoText);
|
||||
imageContainer = findViewById(R.id.imageContainer);
|
||||
mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
|
||||
|
||||
LogUtils.d(TAG, "【initView】UI组件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化工具栏,设置导航与标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 从Intent中获取图片路径并加载图片
|
||||
*/
|
||||
private void loadImageFromIntent() {
|
||||
String imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||
LogUtils.d(TAG, "【loadImageFromIntent】获取到图片路径:" + imagePath);
|
||||
|
||||
if (imagePath != null) {
|
||||
loadImage(imagePath);
|
||||
} else {
|
||||
infoText.setText(MSG_NO_IMAGE_PATH);
|
||||
LogUtils.w(TAG, "【loadImageFromIntent】未获取到图片路径");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定路径的图片
|
||||
* @param imagePath 图片文件路径
|
||||
*/
|
||||
private void loadImage(String imagePath) {
|
||||
try {
|
||||
File file = new File(imagePath);
|
||||
if (file.exists()) {
|
||||
// 解码图片(加载原图)
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = 1;
|
||||
originalBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
|
||||
|
||||
if (originalBitmap != null) {
|
||||
imageView.setImageBitmap(originalBitmap);
|
||||
infoText.setText(MSG_IMAGE_LOADED);
|
||||
LogUtils.d(TAG, "【loadImage】图片加载成功,尺寸:" + originalBitmap.getWidth() + "x" + originalBitmap.getHeight());
|
||||
} else {
|
||||
infoText.setText(MSG_IMAGE_LOAD_FAILED);
|
||||
LogUtils.e(TAG, "【loadImage】图片解码失败");
|
||||
}
|
||||
} else {
|
||||
infoText.setText(MSG_FILE_NOT_EXIST);
|
||||
LogUtils.w(TAG, "【loadImage】图片文件不存在:" + imagePath);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
infoText.setText(MSG_FILE_NOT_FOUND);
|
||||
LogUtils.e(TAG, "【loadImage】图片文件未找到:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示像素颜色信息对话框
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
* @param x 像素X坐标
|
||||
* @param y 像素Y坐标
|
||||
*/
|
||||
private void showPixelDialog(final int pixelColor, int x, int y) {
|
||||
final Dialog dialog = new Dialog(this);
|
||||
dialog.setContentView(R.layout.dialog_pixel);
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// 设置颜色预览与信息展示
|
||||
TextView colorView = dialog.findViewById(R.id.pixelColorView);
|
||||
TextView infoTextView = dialog.findViewById(R.id.colorInfoText);
|
||||
colorView.setBackgroundColor(pixelColor);
|
||||
|
||||
String colorInfo = String.format(
|
||||
"RGB: (%d, %d, %d)\n" +
|
||||
"ARGB: #%08X\n" +
|
||||
"实际像素位置: (%d, %d)",
|
||||
Color.red(pixelColor),
|
||||
Color.green(pixelColor),
|
||||
Color.blue(pixelColor),
|
||||
pixelColor,
|
||||
x, y);
|
||||
infoTextView.setText(colorInfo);
|
||||
LogUtils.d(TAG, "【showPixelDialog】显示像素信息:" + colorInfo);
|
||||
|
||||
// 确定按钮点击事件
|
||||
Button confirmButton = dialog.findViewById(R.id.confirmButton);
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
// 保存像素颜色到背景配置
|
||||
savePixelColor(pixelColor);
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_RECORDED, Toast.LENGTH_SHORT).show();
|
||||
// 同步背景颜色
|
||||
setBackgroundColor();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
LogUtils.d(TAG, "【showPixelDialog】像素对话框已显示");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存拾取的像素颜色到背景配置
|
||||
* @param pixelColor 拾取的像素颜色(ARGB)
|
||||
*/
|
||||
private void savePixelColor(int pixelColor) {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveSettings();
|
||||
LogUtils.d(TAG, "【savePixelColor】像素颜色已保存:#" + Integer.toHexString(pixelColor));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步背景颜色为拾取的像素颜色
|
||||
*/
|
||||
void setBackgroundColor() {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
int pixelColor = bean.getPixelColor();
|
||||
mainLayout.setBackgroundColor(pixelColor);
|
||||
LogUtils.d(TAG, "【setBackgroundColor】背景颜色已同步:#" + Integer.toHexString(pixelColor));
|
||||
}
|
||||
|
||||
// ======================== 事件回调方法 =========================
|
||||
/**
|
||||
* 绑定图片容器的触摸事件,处理像素拾取逻辑
|
||||
*/
|
||||
private void bindImageTouchListener() {
|
||||
imageContainer.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN && originalBitmap != null) {
|
||||
float touchX = event.getX();
|
||||
float touchY = event.getY();
|
||||
LogUtils.v(TAG, "【onTouch】触摸坐标:(" + touchX + ", " + touchY + ")");
|
||||
|
||||
try {
|
||||
// 获取图片在窗口中的位置与尺寸
|
||||
int[] imageLocation = new int[2];
|
||||
imageView.getLocationInWindow(imageLocation);
|
||||
int imageWidth = imageView.getWidth();
|
||||
int imageHeight = imageView.getHeight();
|
||||
LogUtils.v(TAG, "【onTouch】图片显示尺寸:" + imageWidth + "x" + imageHeight + ",位置:(" + imageLocation[0] + ", " + imageLocation[1] + ")");
|
||||
|
||||
// 计算缩放比例
|
||||
float scaleX = (float) originalBitmap.getWidth() / imageWidth;
|
||||
float scaleY = (float) originalBitmap.getHeight() / imageHeight;
|
||||
LogUtils.v(TAG, "【onTouch】图片缩放比例:X=" + scaleX + ",Y=" + scaleY);
|
||||
|
||||
// 调整触摸坐标到图片显示区域坐标系
|
||||
float adjustedX = touchX - imageLocation[0];
|
||||
float adjustedY = touchY - imageLocation[1];
|
||||
LogUtils.v(TAG, "【onTouch】调整后触摸坐标:(" + adjustedX + ", " + adjustedY + ")");
|
||||
|
||||
// 检查是否在图片显示范围内
|
||||
if (adjustedX >= 0 && adjustedX <= imageWidth && adjustedY >= 0 && adjustedY <= imageHeight) {
|
||||
// 计算原始图片的像素坐标
|
||||
int pixelX = (int) (adjustedX * scaleX);
|
||||
int pixelY = (int) (adjustedY * scaleY);
|
||||
LogUtils.v(TAG, "【onTouch】计算后像素坐标:(" + pixelX + ", " + pixelY + ")");
|
||||
|
||||
// 检查像素坐标是否在原始图片范围内
|
||||
if (pixelX >= 0 && pixelX < originalBitmap.getWidth() && pixelY >= 0 && pixelY < originalBitmap.getHeight()) {
|
||||
int pixelColor = originalBitmap.getPixel(pixelX, pixelY);
|
||||
showPixelDialog(pixelColor, pixelX, pixelY);
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_OUT_OF_RANGE, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, "【onTouch】像素坐标超出原始图片范围");
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_TOUCH_OUT_OF_IMAGE, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.w(TAG, "【onTouch】触摸位置超出图片显示范围");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(PixelPickerActivity.this, MSG_PIXEL_CALC_FAILED, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.e(TAG, "【onTouch】计算像素位置失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindImageTouchListener】图片触摸事件已绑定");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
LogUtils.d(TAG, "【onOptionsItemSelected】点击返回菜单");
|
||||
Intent intent = new Intent(this, BackgroundSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
LogUtils.d(TAG, "【onBackPressed】返回键触发,页面关闭");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.SMTPConfigView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
|
||||
import cc.winboll.studio.powerbell.utils.ChargeMailNotifyHelper;
|
||||
import cc.winboll.studio.powerbell.utils.ChargeMailNotifyFloatWindow;
|
||||
import cc.winboll.studio.powerbell.threads.RemindThread;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import android.widget.Switch;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 应用设置窗口,提供应用配置项的统一入口
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025年11月27日14时26分00秒
|
||||
* @LastEditTime 2026-02-28
|
||||
* @Describe 应用设置窗口(主开关联动子开关启用/禁用,主开关关闭则子开关禁用并取消勾选)
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量区 =========================
|
||||
public static final String TAG = "SettingsActivity";
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
// ======================== 成员属性区 =========================
|
||||
private Toolbar mToolbar;
|
||||
private CheckBox cbUsePowerTts; // 用电TTS(主开关)
|
||||
private CheckBox cbChargeTts; // 充电TTS(主开关)
|
||||
private CheckBox cbUseageTtsBattary; // 用电TTS带电量提醒(子开关)
|
||||
private CheckBox cbChargeTtsBattary; // 充电TTS带电量提醒(子开关)
|
||||
private CheckBox cbTtsWhenNotifyBattery; // 👉 新增:通知电量时同时播放TTS
|
||||
private Switch switchChargeMailNotify; // 充电邮件通知主开关
|
||||
private SMTPConfigView smtpConfigView; // SMTP邮件配置视图
|
||||
|
||||
// ======================== 接口实现区 =========================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ======================== 生命周期区 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
LogUtils.d(TAG, "onCreate: 应用设置页面初始化开始");
|
||||
|
||||
initToolbar();
|
||||
initTtsCheckBoxes();
|
||||
initTtsCheckBoxStatus();
|
||||
initSMTPConfigView();
|
||||
initChargeMailNotifySwitch();
|
||||
|
||||
LogUtils.d(TAG, "onCreate: 应用设置页面初始化完成");
|
||||
}
|
||||
|
||||
// ======================== UI初始化区 =========================
|
||||
/**
|
||||
* 初始化顶部工具栏
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar: 工具栏初始化开始");
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "initToolbar-navigationOnClick: 点击导航返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar: 工具栏初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定TTS相关复选框控件
|
||||
*/
|
||||
private void initTtsCheckBoxes() {
|
||||
LogUtils.d(TAG, "initTtsCheckBoxes: TTS复选框绑定开始");
|
||||
cbUsePowerTts = findViewById(R.id.activitysettingsCheckBox1);
|
||||
cbChargeTts = findViewById(R.id.activitysettingsCheckBox2);
|
||||
cbUseageTtsBattary = findViewById(R.id.activitysettingsCheckBox3);
|
||||
cbChargeTtsBattary = findViewById(R.id.activitysettingsCheckBox4);
|
||||
cbTtsWhenNotifyBattery = findViewById(R.id.activitysettingsCheckBox5); // 👉 新增绑定
|
||||
LogUtils.d(TAG, "initTtsCheckBoxes: TTS复选框绑定完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化TTS复选框初始状态
|
||||
*/
|
||||
private void initTtsCheckBoxStatus() {
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: TTS复选框状态初始化开始");
|
||||
ThoughtfulServiceBean bean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
|
||||
if (bean == null) {
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: 未读取到配置Bean,创建新实例");
|
||||
bean = new ThoughtfulServiceBean();
|
||||
}
|
||||
|
||||
boolean useMainOpen = bean.isEnableUsePowerTts();
|
||||
boolean chargeMainOpen = bean.isEnableChargeTts();
|
||||
cbUsePowerTts.setChecked(useMainOpen);
|
||||
cbChargeTts.setChecked(chargeMainOpen);
|
||||
cbUseageTtsBattary.setChecked(bean.isEnableUseageTtsWithBattary());
|
||||
cbChargeTtsBattary.setChecked(bean.isEnableChargeTtsWithBattary());
|
||||
cbTtsWhenNotifyBattery.setChecked(bean.isEnableTtsWhenNotifyBattery()); // 👉 新增赋值
|
||||
cbUseageTtsBattary.setEnabled(useMainOpen);
|
||||
cbChargeTtsBattary.setEnabled(chargeMainOpen);
|
||||
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: 主开关状态-用电TTS:" + useMainOpen + " 充电TTS:" + chargeMainOpen);
|
||||
LogUtils.d(TAG, "initTtsCheckBoxStatus: TTS复选框状态初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化充电邮件通知开关
|
||||
*/
|
||||
private void initChargeMailNotifySwitch() {
|
||||
LogUtils.d(TAG, "initChargeMailNotifySwitch: 充电邮件通知开关初始化开始");
|
||||
switchChargeMailNotify = findViewById(R.id.switchChargeMailNotify);
|
||||
boolean enabled = ChargeMailNotifyHelper.isEnabled(this);
|
||||
switchChargeMailNotify.setChecked(enabled);
|
||||
updateSMTPConfigViewEnabled(enabled);
|
||||
LogUtils.d(TAG, "initChargeMailNotifySwitch: 充电邮件通知开关初始化完成,状态=" + enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化SMTP邮件配置视图
|
||||
*/
|
||||
private void initSMTPConfigView() {
|
||||
LogUtils.d(TAG, "initSMTPConfigView: SMTP邮件配置视图初始化开始");
|
||||
smtpConfigView = findViewById(R.id.smtp_config_view);
|
||||
LogUtils.d(TAG, "initSMTPConfigView: SMTP邮件配置视图初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SMTP配置视图的启用状态
|
||||
*/
|
||||
private void updateSMTPConfigViewEnabled(boolean enabled) {
|
||||
LogUtils.d(TAG, "updateSMTPConfigViewEnabled: 设置SMTPConfigView启用状态=" + enabled);
|
||||
smtpConfigView.setEnabled(enabled);
|
||||
// 递归设置子控件的启用状态
|
||||
setViewAndChildrenEnabled(smtpConfigView, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归设置View及其所有子控件的启用状态
|
||||
*/
|
||||
private void setViewAndChildrenEnabled(View view, boolean enabled) {
|
||||
view.setEnabled(enabled);
|
||||
if (view instanceof android.view.ViewGroup) {
|
||||
android.view.ViewGroup group = (android.view.ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setViewAndChildrenEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 事件响应区 =========================
|
||||
/**
|
||||
* 悬浮窗权限检查入口
|
||||
*/
|
||||
public void onCheckTTSDrawOverlaysPermission(View view) {
|
||||
LogUtils.d(TAG, "onCheckTTSDrawOverlaysPermission: 触发悬浮窗权限检查");
|
||||
canDrawOverlays();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用电TTS主开关点击事件
|
||||
*/
|
||||
public void onEnableUsePowerTts(View view) {
|
||||
boolean isChecked = cbUsePowerTts.isChecked();
|
||||
LogUtils.d(TAG, "onEnableUsePowerTts: 用电TTS主开关点击,切换后状态=" + isChecked);
|
||||
// 主开关联动子开关
|
||||
cbUseageTtsBattary.setEnabled(isChecked);
|
||||
// 保存配置
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableUsePowerTts(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableUsePowerTts: 用电TTS状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电TTS主开关点击事件
|
||||
*/
|
||||
public void onEnableChargeTts(View view) {
|
||||
boolean isChecked = cbChargeTts.isChecked();
|
||||
LogUtils.d(TAG, "onEnableChargeTts: 充电TTS主开关点击,切换后状态=" + isChecked);
|
||||
// 主开关联动子开关
|
||||
cbChargeTtsBattary.setEnabled(isChecked);
|
||||
// 保存配置
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableChargeTts(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableChargeTts: 充电TTS状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用电TTS带电量提醒子开关点击事件
|
||||
*/
|
||||
public void onEnableUseageTtsWithBattary(View view) {
|
||||
boolean isChecked = cbUseageTtsBattary.isChecked();
|
||||
LogUtils.d(TAG, "onEnableUseageTtsWithBattary: 用电TTS电量提醒开关点击,切换后状态=" + isChecked);
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableUseageTtsWithBattary(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableUseageTtsWithBattary: 用电TTS电量提醒状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电TTS带电量提醒子开关点击事件
|
||||
*/
|
||||
public void onEnableChargeTtsWithBattary(View view) {
|
||||
boolean isChecked = cbChargeTtsBattary.isChecked();
|
||||
LogUtils.d(TAG, "onEnableChargeTtsWithBattary: 充电TTS电量提醒开关点击,切换后状态=" + isChecked);
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableChargeTtsWithBattary(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableChargeTtsWithBattary: 充电TTS电量提醒状态保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 👉 新增:允许通知电量消息时同时播放TTS语音
|
||||
*/
|
||||
public void onEnableTtsWhenNotifyBattery(View view) {
|
||||
boolean isChecked = cbTtsWhenNotifyBattery.isChecked();
|
||||
LogUtils.d(TAG, "onEnableTtsWhenNotifyBattery: 通知电量时播TTS开关状态=" + isChecked);
|
||||
ThoughtfulServiceBean bean = getThoughtfulServiceBean();
|
||||
bean.setIsEnableTtsWhenNotifyBattery(isChecked);
|
||||
ThoughtfulServiceBean.saveBean(this, bean);
|
||||
LogUtils.d(TAG, "onEnableTtsWhenNotifyBattery: 保存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 充电邮件通知开关点击事件
|
||||
*/
|
||||
public void onChargeMailNotifySwitchChanged(View view) {
|
||||
boolean isChecked = switchChargeMailNotify.isChecked();
|
||||
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 充电邮件通知开关状态=" + isChecked);
|
||||
ChargeMailNotifyHelper.setEnabled(this, isChecked);
|
||||
updateSMTPConfigViewEnabled(isChecked);
|
||||
if (isChecked) {
|
||||
// 开启时重启ControlCenterService
|
||||
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 开启充电邮件通知,重启ControlCenterService");
|
||||
stopService(new Intent(this, ControlCenterService.class));
|
||||
startService(new Intent(this, ControlCenterService.class));
|
||||
} else {
|
||||
// 关闭时停止RemindThread并关闭悬浮窗
|
||||
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 关闭充电邮件通知,停止RemindThread");
|
||||
RemindThread.stopRemindThread();
|
||||
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 关闭充电邮件通知悬浮窗");
|
||||
ChargeMailNotifyFloatWindow.dismissWindow();
|
||||
}
|
||||
LogUtils.d(TAG, "onChargeMailNotifySwitchChanged: 状态保存完成");
|
||||
}
|
||||
|
||||
// ======================== 工具方法区 =========================
|
||||
/**
|
||||
* 获取配置Bean实例,避免重复代码
|
||||
*/
|
||||
private ThoughtfulServiceBean getThoughtfulServiceBean() {
|
||||
LogUtils.d(TAG, "getThoughtfulServiceBean: 获取配置Bean");
|
||||
ThoughtfulServiceBean bean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
|
||||
if (bean == null) {
|
||||
LogUtils.d(TAG, "getThoughtfulServiceBean: 配置Bean为空,创建新实例");
|
||||
bean = new ThoughtfulServiceBean();
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮窗权限检查与请求
|
||||
*/
|
||||
void canDrawOverlays() {
|
||||
LogUtils.d(TAG, "canDrawOverlays: 悬浮窗权限检查开始");
|
||||
if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
|
||||
LogUtils.d(TAG, "canDrawOverlays: 未开启悬浮窗权限,发起请求");
|
||||
showDrawOverlayRequestDialog();
|
||||
} else {
|
||||
LogUtils.d(TAG, "canDrawOverlays: 悬浮窗权限已开启");
|
||||
ToastUtils.show("悬浮窗权限已开启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示悬浮窗权限请求对话框
|
||||
*/
|
||||
private void showDrawOverlayRequestDialog() {
|
||||
LogUtils.d(TAG, "showDrawOverlayRequestDialog: 显示悬浮窗权限请求弹窗");
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("权限请求")
|
||||
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
|
||||
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
LogUtils.d(TAG, "showDrawOverlayRequestDialog-去设置: 点击跳转权限页面");
|
||||
dialog.dismiss();
|
||||
jumpToDrawOverlaySettings();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
LogUtils.d(TAG, "showDrawOverlayRequestDialog-稍后: 点击取消请求");
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转悬浮窗权限设置页面(反射适配低版本)
|
||||
*/
|
||||
private void jumpToDrawOverlaySettings() {
|
||||
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置页面");
|
||||
try {
|
||||
Class<?> settingsClazz = Settings.class;
|
||||
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||
String action = (String) actionField.get(null);
|
||||
|
||||
Intent intent = new Intent(action);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转权限页面意图已发送");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
|
||||
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.APPPlusUtils;
|
||||
|
||||
/**
|
||||
* 应用快捷方式活动类,处理应用图标快捷菜单的切换请求
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/15 13:45
|
||||
* @Describe 应用快捷方式活动类
|
||||
*/
|
||||
public class ShortcutActionActivity extends Activity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "ShortcutActionActivity";
|
||||
// 快捷指令常量
|
||||
private static final String ACTION_SWITCH_TO_EN1 = "switchto_en1";
|
||||
private static final String ACTION_SWITCH_TO_CN1 = "switchto_cn1";
|
||||
private static final String ACTION_SWITCH_TO_CN2 = "switchto_cn2";
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "【onCreate】ShortcutActionActivity 启动,开始处理快捷方式请求");
|
||||
|
||||
// 处理应用图标快捷菜单的切换请求
|
||||
handleSwitchRequest();
|
||||
|
||||
LogUtils.d(TAG, "【onCreate】快捷方式请求处理完成,关闭活动");
|
||||
finish();
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 处理应用图标快捷菜单的请求,根据意图数据切换应用启动组件
|
||||
*/
|
||||
private void handleSwitchRequest() {
|
||||
Intent intent = getIntent();
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "【handleSwitchRequest】意图为空,无法处理快捷方式请求");
|
||||
return;
|
||||
}
|
||||
|
||||
String dataString = intent.getDataString();
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】获取到快捷指令:" + dataString);
|
||||
|
||||
// 匹配快捷指令并切换组件
|
||||
if (ACTION_SWITCH_TO_EN1.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name) + "图标";
|
||||
ToastUtils.show(toastMsg);
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】已切换至EN1组件:" + App.COMPONENT_EN1);
|
||||
} else if (ACTION_SWITCH_TO_CN1.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name_cn1) + "图标";
|
||||
ToastUtils.show(toastMsg);
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN1组件:" + App.COMPONENT_CN1);
|
||||
} else if (ACTION_SWITCH_TO_CN2.equals(dataString)) {
|
||||
APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
|
||||
String toastMsg = "切换至" + getString(R.string.app_name_cn2) + "图标";
|
||||
ToastUtils.show(toastMsg);
|
||||
LogUtils.d(TAG, "【handleSwitchRequest】已切换至CN2组件:" + App.COMPONENT_CN2);
|
||||
} else {
|
||||
LogUtils.w(TAG, "【handleSwitchRequest】未匹配到有效快捷指令:" + dataString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.AESThemeBean;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/06/19 20:35
|
||||
* @Describe 应用窗口基类,提供主题设置、Activity 管理、工具栏配置、全屏切换等通用功能
|
||||
* 适配 API30,基于 Java7 开发,所有子类需继承此类实现统一窗口行为
|
||||
*/
|
||||
public abstract class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
protected volatile AESThemeBean.ThemeType mThemeType; // 当前主题类型
|
||||
|
||||
// ======================== 接口实现 & 抽象方法 =========================
|
||||
@Override
|
||||
public abstract Activity getActivity();
|
||||
|
||||
@Override
|
||||
public abstract String getTag();
|
||||
|
||||
// ======================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化开始", getTag()));
|
||||
// 初始化主题样式ID列表(远程AAR要求init先于getThemeTypeID调用)
|
||||
initAESThemeStyles();
|
||||
// 初始化主题
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, String.format("【%s-onCreate】窗口基类初始化完成,当前主题:%s", getTag(), mThemeType));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
// 注册到Activity管理器
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
LogUtils.d(TAG, String.format("【%s-onPostCreate】已注册到Activity管理器", getTag()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 从Activity管理器移除
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
LogUtils.d(TAG, String.format("【%s-onDestroy】已从Activity管理器移除", getTag()));
|
||||
}
|
||||
|
||||
// ======================== 主题相关方法 =========================
|
||||
/**
|
||||
* 初始化 AESThemeUtil 内部主题样式ID列表
|
||||
* 远程 AAR 15.20.x 的 AESThemeUtil.getThemeStyleID 依赖此列表进行 ordinal 查找,
|
||||
* 未初始化将导致 IndexOutOfBoundsException
|
||||
*/
|
||||
private void initAESThemeStyles() {
|
||||
ArrayList<Integer> themeStyleIDs = new ArrayList<Integer>();
|
||||
AESThemeBean.fillThemeStyleIDList(themeStyleIDs);
|
||||
AESThemeUtil.init(themeStyleIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题类型
|
||||
* @return 主题类型枚举
|
||||
*/
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
int themeId = AESThemeUtil.getThemeTypeID(getApplicationContext());
|
||||
AESThemeBean.ThemeType themeType = AESThemeBean.getThemeStyleType(themeId);
|
||||
LogUtils.d(TAG, String.format("【%s-getThemeType】获取主题类型,ID:%d,类型:%s", getTag(), themeId, themeType));
|
||||
return themeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题样式
|
||||
*/
|
||||
void setThemeStyle() {
|
||||
int themeId = AESThemeUtil.getThemeTypeID(getApplicationContext());
|
||||
setTheme(themeId);
|
||||
LogUtils.d(TAG, String.format("【%s-setThemeStyle】应用主题样式,ID:%d", getTag(), themeId));
|
||||
}
|
||||
|
||||
// ======================== UI 配置方法 =========================
|
||||
/**
|
||||
* 配置工具栏,显示返回按钮
|
||||
*/
|
||||
public void setupToolbar() {
|
||||
Toolbar mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar != null) {
|
||||
setSupportActionBar(mToolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
LogUtils.d(TAG, String.format("【%s-setupToolbar】工具栏配置完成,已显示返回按钮", getTag()));
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("【%s-setupToolbar】ActionBar为空,无法显示返回按钮", getTag()));
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("【%s-setupToolbar】未找到工具栏控件(ID:toolbar)", getTag()));
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 菜单 & 返回键处理 =========================
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
LogUtils.d(TAG, String.format("【%s-onOptionsItemSelected】点击返回菜单", getTag()));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
LogUtils.d(TAG, String.format("【%s-onBackPressed】触发返回键", getTag()));
|
||||
}
|
||||
|
||||
// ======================== 工具方法 =========================
|
||||
/**
|
||||
* 切换至全屏模式,隐藏状态栏与导航栏
|
||||
* @param activity 目标Activity
|
||||
*/
|
||||
public void changeFullScreen(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】目标Activity为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
Window window = activity.getWindow();
|
||||
if (window == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】窗口为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
View decorView = window.getDecorView();
|
||||
if (decorView == null) {
|
||||
LogUtils.w(TAG, String.format("【%s-changeFullScreen】DecorView为空,无法切换全屏", getTag()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置全屏标志位
|
||||
int flag = decorView.getSystemUiVisibility();
|
||||
flag |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
flag |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
flag |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
||||
flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
decorView.setSystemUiVisibility(flag);
|
||||
// 配置窗口标志位
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
LogUtils.d(TAG, String.format("【%s-changeFullScreen】已切换至全屏模式", getTag()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BatteryData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 电池报告数据适配器,用于RecyclerView展示电池电量、充放电时间数据
|
||||
* 适配 API30,基于 Java7 开发
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:38:55
|
||||
* @Describe 电池报告数据适配器
|
||||
*/
|
||||
public class BatteryAdapter extends RecyclerView.Adapter<BatteryAdapter.ViewHolder> {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "BatteryAdapter";
|
||||
private static final String FORMAT_BATTERY_LEVEL = "%d%%"; // 电量显示格式
|
||||
private static final String PREFIX_DISCHARGE_TIME = "使用时间: "; // 放电时间前缀
|
||||
private static final String PREFIX_CHARGE_TIME = "充电时间: "; // 充电时间前缀
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
private List<BatteryData> dataList = new ArrayList<>(); // 电池数据列表
|
||||
|
||||
// ======================== 构造方法 =========================
|
||||
public BatteryAdapter() {
|
||||
LogUtils.d(TAG, "【BatteryAdapter】适配器初始化,初始数据列表为空");
|
||||
}
|
||||
|
||||
// ======================== 数据操作方法 =========================
|
||||
/**
|
||||
* 更新适配器数据并刷新列表
|
||||
* @param newData 新的电池数据列表
|
||||
*/
|
||||
public void updateData(List<BatteryData> newData) {
|
||||
LogUtils.d(TAG, "【updateData】开始更新数据,新数据列表是否为空:" + (newData == null));
|
||||
// 判空处理,避免空指针
|
||||
if (newData != null) {
|
||||
dataList = newData;
|
||||
LogUtils.d(TAG, "【updateData】数据更新完成,当前数据量:" + dataList.size());
|
||||
} else {
|
||||
dataList.clear();
|
||||
LogUtils.w(TAG, "【updateData】新数据列表为空,已清空本地数据");
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "【updateData】已通知列表刷新");
|
||||
}
|
||||
|
||||
// ======================== RecyclerView 重写方法 =========================
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "【onCreateViewHolder】创建ViewHolder,父容器:" + parent.getContext().getClass().getSimpleName());
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_battery_report, parent, false);
|
||||
ViewHolder viewHolder = new ViewHolder(view);
|
||||
LogUtils.d(TAG, "【onCreateViewHolder】ViewHolder创建完成");
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "【onBindViewHolder】绑定ViewHolder,位置:" + position);
|
||||
// 判空与越界校验
|
||||
if (dataList == null || dataList.isEmpty() || position >= dataList.size()) {
|
||||
LogUtils.w(TAG, "【onBindViewHolder】数据异常,无法绑定视图,位置:" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
BatteryData item = dataList.get(position);
|
||||
// 绑定数据到视图
|
||||
holder.tvLevel.setText(String.format(FORMAT_BATTERY_LEVEL, item.getCurrentLevel()));
|
||||
holder.tvDischargeTime.setText(PREFIX_DISCHARGE_TIME + item.getDischargeTime());
|
||||
holder.tvChargeTime.setText(PREFIX_CHARGE_TIME + item.getChargeTime());
|
||||
|
||||
LogUtils.d(TAG, "【onBindViewHolder】视图绑定完成,位置:" + position + ",电量:" + item.getCurrentLevel() + "%");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = dataList.size();
|
||||
LogUtils.d(TAG, "【getItemCount】获取条目数量:" + count);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ======================== ViewHolder 内部类 =========================
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvLevel; // 电量显示
|
||||
TextView tvDischargeTime; // 放电时间显示
|
||||
TextView tvChargeTime; // 充电时间显示
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
// 初始化视图控件
|
||||
tvLevel = itemView.findViewById(R.id.tvLevel);
|
||||
tvDischargeTime = itemView.findViewById(R.id.tvDischargeTime);
|
||||
tvChargeTime = itemView.findViewById(R.id.tvChargeTime);
|
||||
LogUtils.d(TAG, "【ViewHolder】控件初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
|
||||
/**
|
||||
* 背景图片的接收分享文件后的预览对话框
|
||||
* 适配 API30,基于 Java7 开发,支持分享图片的Uri解析、预览与确认选择
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/25 16:27:53
|
||||
* @Describe 背景图片的接收分享文件后的预览对话框
|
||||
*/
|
||||
public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
// ======================== 静态常量 =========================
|
||||
public static final String TAG = "BackgroundPicturePreviewDialog";
|
||||
private static final String TOAST_MSG_EMPTY_FILE = "接收到的文件为空。"; // 空文件提示文本
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
private Context mContext; // 上下文对象
|
||||
private IOnRecivedPictureListener mIOnRecivedPictureListener; // 图片接收监听
|
||||
private Uri mUriRecivedPicture; // 接收的图片Uri
|
||||
// 控件对象
|
||||
private BackgroundView mBackgroundView; // 背景预览视图
|
||||
private Button dialogbackgroundpicturepreviewButton1; // 取消按钮
|
||||
private Button dialogbackgroundpicturepreviewButton2; // 确认按钮
|
||||
|
||||
// ======================== 接口定义 =========================
|
||||
/**
|
||||
* 图片接收监听接口,用于通知确认选择的图片Uri
|
||||
*/
|
||||
public interface IOnRecivedPictureListener {
|
||||
void onAcceptRecivedPicture(Uri uriRecivedPicture);
|
||||
}
|
||||
|
||||
// ======================== 构造方法 =========================
|
||||
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化开始");
|
||||
// 初始化成员变量
|
||||
mContext = context;
|
||||
mIOnRecivedPictureListener = iOnRecivedPictureListener;
|
||||
|
||||
// 设置布局与控件
|
||||
setContentView(R.layout.dialog_backgroundpicturepreview);
|
||||
initViews();
|
||||
bindButtonClickEvents();
|
||||
|
||||
// 预览接收的图片
|
||||
previewRecivedPicture();
|
||||
LogUtils.d(TAG, "【BackgroundPicturePreviewDialog】对话框初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 视图初始化方法 =========================
|
||||
/**
|
||||
* 初始化对话框内所有控件
|
||||
*/
|
||||
private void initViews() {
|
||||
mBackgroundView = findViewById(R.id.backgroundview);
|
||||
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
|
||||
dialogbackgroundpicturepreviewButton2 = findViewById(R.id.dialogbackgroundpicturepreviewButton2);
|
||||
LogUtils.d(TAG, "【initViews】对话框控件初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 事件绑定方法 =========================
|
||||
/**
|
||||
* 绑定按钮点击事件
|
||||
*/
|
||||
private void bindButtonClickEvents() {
|
||||
// 取消按钮:跳转到主页面并关闭对话框
|
||||
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
LogUtils.d(TAG, "【onClick】点击取消按钮,跳转到主页面");
|
||||
Intent intent = new Intent(mContext, MainActivity.class);
|
||||
mContext.startActivity(intent);
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "【onClick】对话框已关闭");
|
||||
}
|
||||
});
|
||||
|
||||
// 确认按钮:通知监听并关闭对话框
|
||||
dialogbackgroundpicturepreviewButton2.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【onClick】点击确认按钮,通知接收图片");
|
||||
if (mIOnRecivedPictureListener != null && mUriRecivedPicture != null) {
|
||||
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
|
||||
LogUtils.d(TAG, "【onClick】已通知监听,图片Uri:" + mUriRecivedPicture);
|
||||
} else {
|
||||
LogUtils.w(TAG, "【onClick】监听为空或图片Uri无效,无法通知");
|
||||
}
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "【onClick】对话框已关闭");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "【bindButtonClickEvents】按钮点击事件绑定完成");
|
||||
}
|
||||
|
||||
// ======================== 业务逻辑方法 =========================
|
||||
/**
|
||||
* 预览接收的分享图片
|
||||
*/
|
||||
private void previewRecivedPicture() {
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】开始预览接收的图片");
|
||||
// 校验上下文类型
|
||||
if (!(mContext instanceof BackgroundSettingsActivity)) {
|
||||
LogUtils.e(TAG, "【previewRecivedPicture】上下文不是BackgroundSettingsActivity,无法获取图片Uri");
|
||||
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
BackgroundSettingsActivity activity = (BackgroundSettingsActivity) mContext;
|
||||
// 从Intent中获取图片Uri(优先getData,其次EXTRA_STREAM)
|
||||
mUriRecivedPicture = activity.getIntent().getData();
|
||||
if (mUriRecivedPicture == null) {
|
||||
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】从EXTRA_STREAM获取Uri:" + mUriRecivedPicture);
|
||||
} else {
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】从getData获取Uri:" + mUriRecivedPicture);
|
||||
}
|
||||
|
||||
// 解析Uri为文件路径
|
||||
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
|
||||
//App.notifyMessage(TAG, "szSrcImage : " + szSrcImage);
|
||||
if (TextUtils.isEmpty(szSrcImage)) {
|
||||
LogUtils.w(TAG, "【previewRecivedPicture】解析的文件路径为空");
|
||||
Toast.makeText(mContext, TOAST_MSG_EMPTY_FILE, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载图片到预览视图
|
||||
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
|
||||
mBackgroundView.loadImage(nCurrentPixelColor, szSrcImage, true);
|
||||
LogUtils.d(TAG, "【previewRecivedPicture】图片预览完成,文件路径:" + szSrcImage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,733 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import com.a4455jkjh.colorpicker.ColorPickerDialog;
|
||||
import com.a4455jkjh.colorpicker.view.OnColorChangedListener;
|
||||
|
||||
/**
|
||||
* 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
* 适配 API30,基于 Java7 开发,返回 0xAARRGGBB 格式颜色(含透明度)
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/16 11:47
|
||||
* @Describe 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
*/
|
||||
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "ColorPaletteDialog";
|
||||
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值(0-255)
|
||||
private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比(100%,无调节)
|
||||
private static final int BRIGHTNESS_STEP = 5; // 亮度调节步长(每次±5%,精准流畅)
|
||||
private static final int MIN_BRIGHTNESS = 10; // 亮度最小值(10%,避免全黑看不见)
|
||||
private static final int MAX_BRIGHTNESS = 200; // 亮度最大值(200%,避免过曝失真)
|
||||
private static final int MAX_ALPHA_PERCENT = 100; // 透明度最大值(100%=不透明)
|
||||
private static final int MIN_ALPHA_PERCENT = 0; // 透明度最小值(0%=完全透明)
|
||||
private static final String FORMAT_COLOR_HEX = "#%08X"; // 颜色值格式化(AARRGGBB)
|
||||
private static final String FORMAT_PERCENT = "%d%%"; // 百分比格式化(X%)
|
||||
|
||||
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
|
||||
public interface OnColorSelectedListener {
|
||||
void onColorSelected(int color); // 返回0xAARRGGBB格式颜色(含透明度)
|
||||
}
|
||||
|
||||
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
|
||||
// 核心数据:原始基准值(用户输入/选择颜色时更新)+ 实时调节值(亮度/透明度变化时更新)
|
||||
private OnColorSelectedListener mListener; // 颜色选择回调(非空校验)
|
||||
private int mInitialColor; // 初始颜色(传入的默认颜色)
|
||||
private int mCurrentColor; // 当前最终颜色(含亮度+透明度调节)
|
||||
private int mCurrentBrightnessPercent; // 当前亮度百分比(10%-200%)
|
||||
// 透明度:百分比(0-100%,用户直观操作)+ 原始/实时值(0-255,颜色计算用)
|
||||
private int mOriginalAlphaPercent; // 原始透明度百分比(基准值,用户输入/选色时更新)
|
||||
private int mCurrentAlphaPercent; // 实时透明度百分比(调节进度条时更新)
|
||||
private int mOriginalAlpha; // 原始透明度(0-255,基准值)
|
||||
private int mCurrentAlpha; // 实时透明度(0-255,计算用)
|
||||
// RGB:原始基准值+实时调节值
|
||||
private int mOriginalR; // 原始R分量(基准值,用户输入/选色时更新)
|
||||
private int mOriginalG; // 原始G分量(基准值,用户输入/选色时更新)
|
||||
private int mOriginalB; // 原始B分量(基准值,用户输入/选色时更新)
|
||||
private int mCurrentR; // 实时R分量(亮度调节后,同步输入框显示)
|
||||
private int mCurrentG; // 实时G分量(亮度调节后,同步输入框显示)
|
||||
private int mCurrentB; // 实时B分量(亮度调节后,同步输入框显示)
|
||||
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
|
||||
private static volatile boolean isAppSelfUpdatingColor = false;
|
||||
|
||||
// 控件引用
|
||||
private ImageView ivColorPicker; // 颜色预览拾取框
|
||||
private ImageView ivColorScaler; // 颜色渐变拾取框
|
||||
private EditText etR; // R分量输入框(显示实时调节值)
|
||||
private EditText etG; // G分量输入框(显示实时调节值)
|
||||
private EditText etB; // B分量输入框(显示实时调节值)
|
||||
private EditText etColorValue; // 颜色值输入框(#AARRGGBB,显示最终值)
|
||||
private SeekBar sbAlpha; // 透明度调节进度条(0-100%)
|
||||
private TextView tvAlphaValue; // 透明度数值显示(X%)
|
||||
private TextView tvBrightnessMinus;// 亮度减少按钮(-)
|
||||
private TextView tvBrightnessValue;// 亮度数值显示(X%,直观易懂)
|
||||
private TextView tvBrightnessPlus; // 亮度增加按钮(+)
|
||||
private TextView tvConfirm; // 确认按钮
|
||||
private TextView tvCancel; // 取消按钮
|
||||
|
||||
// ====================== 构造方法(初始化核心数据,严格校验) ======================
|
||||
public ColorPaletteDialog(Context context, int initialColor, OnColorSelectedListener listener) {
|
||||
super(context, R.style.CustomDialogStyle);
|
||||
this.mInitialColor = initialColor;
|
||||
this.mListener = listener;
|
||||
|
||||
// 1. 强制回调非空,避免后续空指针(容错)
|
||||
if (mListener == null) {
|
||||
throw new IllegalArgumentException("OnColorSelectedListener can not be null!");
|
||||
}
|
||||
|
||||
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
|
||||
this.mOriginalAlpha = Color.alpha(initialColor);
|
||||
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
this.mCurrentAlpha = mOriginalAlpha;
|
||||
this.mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
|
||||
this.mOriginalR = Color.red(initialColor);
|
||||
this.mOriginalG = Color.green(initialColor);
|
||||
this.mOriginalB = Color.blue(initialColor);
|
||||
this.mCurrentR = mOriginalR;
|
||||
this.mCurrentG = mOriginalG;
|
||||
this.mCurrentB = mOriginalB;
|
||||
|
||||
// 3. 初始化当前状态(默认亮度100%,当前颜色=初始颜色)
|
||||
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
this.mCurrentColor = initialColor;
|
||||
|
||||
LogUtils.d(TAG, String.format("init dialog success | 初始颜色:%s | 原始RGB:%d,%d,%d | 原始透明度:%s | 初始亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, initialColor),
|
||||
mOriginalR, mOriginalG, mOriginalB,
|
||||
String.format(FORMAT_PERCENT, mOriginalAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE); // 隐藏标题栏
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_color_palette, null);
|
||||
setContentView(view);
|
||||
|
||||
// 初始化流程:控件绑定→数据赋值→监听设置→尺寸适配(小米机型优先适配)
|
||||
initViewBind(view);
|
||||
initData();
|
||||
initListener();
|
||||
adjustDialogSize();
|
||||
LogUtils.d(TAG, "dialog create complete | 适配小米API29-30机型");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
// 释放资源,避免内存泄漏(回调引用置空)
|
||||
mListener = null;
|
||||
LogUtils.d(TAG, "dialog dismiss | 释放资源完成");
|
||||
}
|
||||
|
||||
// ====================== 初始化核心方法(职责单一,便于维护) ======================
|
||||
/**
|
||||
* 控件绑定
|
||||
*/
|
||||
private void initViewBind(View view) {
|
||||
ivColorPicker = view.findViewById(R.id.iv_color_picker);
|
||||
ivColorScaler = view.findViewById(R.id.iv_color_scaler);
|
||||
etR = view.findViewById(R.id.et_r);
|
||||
etG = view.findViewById(R.id.et_g);
|
||||
etB = view.findViewById(R.id.et_b);
|
||||
etColorValue = view.findViewById(R.id.et_color_value);
|
||||
sbAlpha = view.findViewById(R.id.sb_alpha);
|
||||
tvAlphaValue = view.findViewById(R.id.tv_alpha_value);
|
||||
tvBrightnessMinus = view.findViewById(R.id.tv_brightness_minus);
|
||||
tvBrightnessValue = view.findViewById(R.id.tv_brightness_value);
|
||||
tvBrightnessPlus = view.findViewById(R.id.tv_brightness_plus);
|
||||
tvConfirm = view.findViewById(R.id.tv_confirm);
|
||||
tvCancel = view.findViewById(R.id.tv_cancel);
|
||||
|
||||
// 控件非空校验(小米低版本容错,绑定失败直接关闭对话框)
|
||||
if (ivColorPicker == null || ivColorScaler == null || etR == null || etG == null || etB == null || etColorValue == null
|
||||
|| sbAlpha == null || tvAlphaValue == null
|
||||
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|
||||
|| tvConfirm == null || tvCancel == null) {
|
||||
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确!");
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "view bind complete | 所有控件绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据初始化(无监听状态下赋值,避免循环回调)
|
||||
*/
|
||||
private void initData() {
|
||||
// 1. 颜色预览(显示当前最终颜色,初始=原始颜色)
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. RGB输入框(显示「实时分量」,初始=原始值)
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB)
|
||||
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
|
||||
|
||||
// 4. 透明度控件(进度条+文本,初始=原始透明度)
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
|
||||
|
||||
// 5. 亮度控件(显示默认100%,初始化按钮状态)
|
||||
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
|
||||
updateBrightnessBtnStatus(); // 禁用边界值按钮
|
||||
|
||||
LogUtils.d(TAG, String.format("init data complete | 原始透明度:%s",
|
||||
String.format(FORMAT_PERCENT, mOriginalAlphaPercent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听初始化
|
||||
*/
|
||||
private void initListener() {
|
||||
// 点击监听(按钮+颜色拾取框)
|
||||
ivColorPicker.setOnClickListener(this);
|
||||
ivColorScaler.setOnClickListener(this);
|
||||
tvConfirm.setOnClickListener(this);
|
||||
tvCancel.setOnClickListener(this);
|
||||
tvBrightnessMinus.setOnClickListener(this);
|
||||
tvBrightnessPlus.setOnClickListener(this);
|
||||
// 透明度进度条监听
|
||||
sbAlpha.setOnSeekBarChangeListener(this);
|
||||
// 输入框监听(RGB+颜色值,避免循环同步)
|
||||
initTextWatcherListener();
|
||||
LogUtils.d(TAG, "all listener init complete | 监听绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框尺寸适配(小米全面屏+软键盘优化,避免输入框被遮挡)
|
||||
*/
|
||||
private void adjustDialogSize() {
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams lp = window.getAttributes();
|
||||
// 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸)
|
||||
lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8);
|
||||
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
// 软键盘适配:小米虚拟导航栏兼容
|
||||
window.setAttributes(lp);
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
|
||||
| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
|
||||
LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 监听子方法(细分类型,逻辑清晰) ======================
|
||||
/**
|
||||
* 输入框文本监听(RGB+颜色值,传入触发ID避免循环同步)
|
||||
*/
|
||||
private void initTextWatcherListener() {
|
||||
// RGB输入框监听(复用方法,减少冗余)
|
||||
setEditTextWatcher(etR, R.id.et_r);
|
||||
setEditTextWatcher(etG, R.id.et_g);
|
||||
setEditTextWatcher(etB, R.id.et_b);
|
||||
|
||||
// 颜色值输入框监听(支持#RRGGBB/#AARRGGBB格式)
|
||||
etColorValue.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
parseColorFromStr(s.toString().trim(), R.id.et_color_value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 透明度进度条监听实现 ======================
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
// 仅处理用户手动拖动进度条(避免应用自身更新时触发)
|
||||
if (fromUser && !isAppSelfUpdatingColor) {
|
||||
updateAlphaBySeekBar(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
/**
|
||||
* 拖动透明度进度条更新颜色
|
||||
*/
|
||||
private synchronized void updateAlphaBySeekBar(int alphaPercent) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
try {
|
||||
// 更新实时透明度(百分比+0-255值)
|
||||
mCurrentAlphaPercent = alphaPercent;
|
||||
mCurrentAlpha = percent2Alpha(alphaPercent);
|
||||
// 重新计算最终颜色(基于当前亮度+新透明度)
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("update alpha by seekbar | 透明度:%s",
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false; // 释放标记
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 颜色核心逻辑 ======================
|
||||
/**
|
||||
* 核心计算:基于原始RGB+当前亮度+当前透明度,计算实时RGB+最终颜色
|
||||
*/
|
||||
private void calculateBrightnessAndUpdate() {
|
||||
// 亮度百分比转调节系数(10%→0.1,100%→1.0,200%→2.0)
|
||||
float brightnessFactor = mCurrentBrightnessPercent / 100.0f;
|
||||
|
||||
// RGB三个分量同时调节(基于原始基准值,避免叠加失真),限制0-255
|
||||
mCurrentR = Math.min(Math.max(Math.round(mOriginalR * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
mCurrentG = Math.min(Math.max(Math.round(mOriginalG * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
mCurrentB = Math.min(Math.max(Math.round(mOriginalB * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
|
||||
// 拼接「实时透明度」+「实时RGB」,得到最终颜色(0xAARRGGBB)
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度减少(每次减5%,最低10%)
|
||||
*/
|
||||
private void decreaseBrightness() {
|
||||
changeBrightness(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度增加(每次加5%,最高200%)
|
||||
*/
|
||||
private void increaseBrightness() {
|
||||
changeBrightness(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度调节核心方法(统一逻辑,加并发控制)
|
||||
*/
|
||||
private synchronized void changeBrightness(boolean isIncrease) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
if (isIncrease) {
|
||||
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return;
|
||||
mCurrentBrightnessPercent += BRIGHTNESS_STEP;
|
||||
} else {
|
||||
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return;
|
||||
mCurrentBrightnessPercent -= BRIGHTNESS_STEP;
|
||||
}
|
||||
// 计算亮度调节后的实时RGB+最终颜色
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("%s brightness | 亮度:%s | 实时RGB:%d,%d,%d",
|
||||
isIncrease ? "increase" : "decrease",
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent),
|
||||
mCurrentR, mCurrentG, mCurrentB));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB,更新原始基准值+实时值)
|
||||
*/
|
||||
private void parseColorFromStr(String colorStr, int triggerViewId) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
if (TextUtils.isEmpty(colorStr)) return;
|
||||
|
||||
// 补全#前缀(兼容用户输入习惯)
|
||||
if (!colorStr.startsWith("#")) {
|
||||
colorStr = "#" + colorStr;
|
||||
}
|
||||
|
||||
// 格式校验(仅支持6位RRGGBB/8位AARRGGBB)
|
||||
if (colorStr.length() != 7 && colorStr.length() != 9) {
|
||||
LogUtils.e(TAG, String.format("parse color failed | 格式错误(需#RRGGBB/#AARRGGBB),输入:%s", colorStr));
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析颜色
|
||||
int parsedColor = Color.parseColor(colorStr);
|
||||
|
||||
// 更新原始基准值与实时值
|
||||
mOriginalAlpha = Color.alpha(parsedColor);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(parsedColor);
|
||||
mOriginalG = Color.green(parsedColor);
|
||||
mOriginalB = Color.blue(parsedColor);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = parsedColor;
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("parse color success | 解析颜色:%s | 透明度:%s | 重置亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, parsedColor),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, String.format("parse color failed | 非法颜色格式,输入:%s", colorStr), e);
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过RGB输入框更新颜色(用户输入后,更新原始基准值+实时值,重置亮度为100%)
|
||||
*/
|
||||
private synchronized void updateColorByRGB(int triggerViewId) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
// 解析用户输入的RGB值(限制0-255,非法输入设为0)
|
||||
int inputR = parseInputValue(etR.getText().toString());
|
||||
int inputG = parseInputValue(etG.getText().toString());
|
||||
int inputB = parseInputValue(etB.getText().toString());
|
||||
|
||||
// 更新原始基准值与实时值
|
||||
mOriginalR = inputR;
|
||||
mOriginalG = inputG;
|
||||
mOriginalB = inputB;
|
||||
mCurrentR = inputR;
|
||||
mCurrentG = inputG;
|
||||
mCurrentB = inputB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("update color by RGB | 新原始RGB:%d,%d,%d | 透明度:%s | 重置亮度:%s",
|
||||
mOriginalR, mOriginalG, mOriginalB,
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, DEFAULT_BRIGHTNESS)));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "update color by RGB failed", e);
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心同步:更新所有控件显示
|
||||
*/
|
||||
private void updateAllViews() {
|
||||
// 1. 同步颜色预览
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. 同步RGB输入框
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 同步颜色值输入框
|
||||
etColorValue.setText(String.format(FORMAT_COLOR_HEX, mCurrentColor));
|
||||
|
||||
// 4. 同步透明度控件
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(String.format(FORMAT_PERCENT, mCurrentAlphaPercent));
|
||||
|
||||
// 5. 同步亮度控件
|
||||
tvBrightnessValue.setText(String.format(FORMAT_PERCENT, mCurrentBrightnessPercent));
|
||||
updateBrightnessBtnStatus();
|
||||
|
||||
LogUtils.d(TAG, String.format("sync all views complete | 最终颜色:%s | 实时RGB:%d,%d,%d | 透明度:%s | 亮度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, mCurrentColor),
|
||||
mCurrentR, mCurrentG, mCurrentB,
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent),
|
||||
String.format(FORMAT_PERCENT, mCurrentBrightnessPercent)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新亮度按钮状态(边界值禁用,提升交互体验)
|
||||
*/
|
||||
private void updateBrightnessBtnStatus() {
|
||||
boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS;
|
||||
boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS;
|
||||
|
||||
tvBrightnessMinus.setEnabled(canMinus);
|
||||
tvBrightnessPlus.setEnabled(canPlus);
|
||||
tvBrightnessMinus.setTextColor(canMinus ? Color.BLACK : Color.parseColor("#CCCCCC"));
|
||||
tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC"));
|
||||
}
|
||||
|
||||
// ====================== 工具方法 ======================
|
||||
/**
|
||||
* 透明度:0-255 → 0-100%
|
||||
*/
|
||||
private int alpha2Percent(int alpha) {
|
||||
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 透明度:0-100% → 0-255
|
||||
*/
|
||||
private int percent2Alpha(int percent) {
|
||||
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析输入值(限制0-255,非法输入返回0)
|
||||
*/
|
||||
private int parseInputValue(String input) {
|
||||
if (TextUtils.isEmpty(input)) return 0;
|
||||
try {
|
||||
int value = Integer.parseInt(input);
|
||||
return Math.min(Math.max(value, 0), MAX_RGB_VALUE);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, String.format("parse input failed | 非法数字,输入:%s", input), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB输入框监听复用
|
||||
*/
|
||||
private void setEditTextWatcher(EditText editText, final int viewId) {
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
updateColorByRGB(viewId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* dp转px(适配小米不同分辨率)
|
||||
*/
|
||||
private int dp2px(float dp) {
|
||||
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示系统颜色选择器(兼容API29-30,无高版本依赖,小米机型适配)
|
||||
*/
|
||||
private void showSystemColorPicker() {
|
||||
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30,支持横向滚动");
|
||||
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
|
||||
builder.setTitle("选择基础颜色");
|
||||
|
||||
// 50种常用颜色:按彩虹光谱顺序排列
|
||||
final int[] systemColors = {
|
||||
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
|
||||
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
|
||||
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
|
||||
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
|
||||
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
|
||||
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
|
||||
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
|
||||
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
|
||||
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
|
||||
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
|
||||
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
|
||||
};
|
||||
|
||||
// 1. 第一级:水平滚动容器
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
|
||||
horizontalScrollView.setHorizontalScrollBarEnabled(true);
|
||||
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
|
||||
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
||||
|
||||
// 2. 第二级:颜色排列容器(横向)
|
||||
LinearLayout colorLayout = new LinearLayout(getContext());
|
||||
colorLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
colorLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
|
||||
|
||||
// 3. 循环添加颜色按钮(内置圆形效果)
|
||||
for (int i = 0; i < systemColors.length; i++) {
|
||||
final int color = systemColors[i];
|
||||
ImageView colorBtn = new ImageView(getContext());
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
|
||||
if (i != systemColors.length - 1) {
|
||||
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
|
||||
}
|
||||
colorBtn.setLayoutParams(lp);
|
||||
|
||||
// 内置圆形背景(白色边框+圆形形状)
|
||||
GradientDrawable circleBg = new GradientDrawable();
|
||||
circleBg.setShape(GradientDrawable.OVAL);
|
||||
circleBg.setColor(color);
|
||||
circleBg.setStroke(dp2px(2), Color.WHITE);
|
||||
colorBtn.setBackground(circleBg);
|
||||
|
||||
colorBtn.setClickable(true);
|
||||
colorBtn.setFocusable(true);
|
||||
|
||||
// 点击事件
|
||||
colorBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
builder.create().dismiss();
|
||||
LogUtils.d(TAG, String.format("select system color | 选择颜色:%s | 透明度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, color),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
colorLayout.addView(colorBtn);
|
||||
}
|
||||
|
||||
// 层级嵌套
|
||||
horizontalScrollView.addView(colorLayout);
|
||||
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
|
||||
}
|
||||
|
||||
// ====================== 点击事件实现 ======================
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
// 所有点击事件均加并发判断
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
if (id == R.id.iv_color_picker) {
|
||||
showSystemColorPicker();
|
||||
} else if (id == R.id.iv_color_scaler) {
|
||||
openColorScalerDialog(mCurrentColor);
|
||||
} else if (id == R.id.tv_confirm) {
|
||||
mListener.onColorSelected(mCurrentColor);
|
||||
LogUtils.d(TAG, String.format("confirm color | 回调颜色:%s",
|
||||
String.format(FORMAT_COLOR_HEX, mCurrentColor)));
|
||||
dismiss();
|
||||
} else if (id == R.id.tv_cancel) {
|
||||
dismiss();
|
||||
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
|
||||
} else if (id == R.id.tv_brightness_minus) {
|
||||
decreaseBrightness();
|
||||
} else if (id == R.id.tv_brightness_plus) {
|
||||
increaseBrightness();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开颜色渐变选择器
|
||||
*/
|
||||
void openColorScalerDialog(int nColor) {
|
||||
LogUtils.d(TAG, String.format("openColorScalerDialog | 初始颜色:%s",
|
||||
String.format(FORMAT_COLOR_HEX, nColor)));
|
||||
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
|
||||
dlg.setOnColorChangedListener(new OnColorChangedListener() {
|
||||
@Override
|
||||
public void beforeColorChanged() {}
|
||||
|
||||
@Override
|
||||
public void onColorChanged(int color) {
|
||||
dlg.currentColorScalerDialogColor = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterColorChanged() {}
|
||||
});
|
||||
dlg.show();
|
||||
}
|
||||
|
||||
// ====================== 内部类 ======================
|
||||
class ColorScalerDialog extends ColorPickerDialog {
|
||||
public int currentColorScalerDialogColor = 0;
|
||||
|
||||
public ColorScalerDialog(Context context, int p) {
|
||||
super(context, p);
|
||||
this.currentColorScalerDialogColor = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
int color = currentColorScalerDialogColor;
|
||||
ToastUtils.show(String.format("选择颜色:%s", String.format(FORMAT_COLOR_HEX, color)));
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, String.format("select scaler color | 选择颜色:%s | 透明度:%s",
|
||||
String.format(FORMAT_COLOR_HEX, color),
|
||||
String.format(FORMAT_PERCENT, mCurrentAlphaPercent)));
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageDownloader;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/19 20:11
|
||||
* @Describe 网络背景使用提示对话框
|
||||
* 继承 AndroidX AlertDialog,绑定自定义布局 dialog_networkbackground.xml
|
||||
* 适配 API30,基于 Java7 开发,支持网络图片下载、预览与回调
|
||||
*/
|
||||
public class NetworkBackgroundDialog extends AlertDialog {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "NetworkBackgroundDialog";
|
||||
private static final int MSG_IMAGE_LOAD_SUCCESS = 1001; // 图片加载成功消息标识
|
||||
private static final int MSG_IMAGE_LOAD_FAILED = 1002; // 图片加载失败消息标识
|
||||
|
||||
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
|
||||
/**
|
||||
* 按钮点击回调接口(Java7 接口实现)
|
||||
*/
|
||||
public interface OnDialogClickListener {
|
||||
void onConfirm(String szConfirmFilePath); // 确认按钮点击,返回图片路径
|
||||
void onCancel(); // 取消按钮点击
|
||||
}
|
||||
|
||||
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
|
||||
// 核心数据
|
||||
private OnDialogClickListener listener; // 按钮点击回调
|
||||
private Context mContext; // 上下文对象
|
||||
private Handler mUiHandler; // 主线程 Handler,用于接收子线程消息更新 UI
|
||||
private String mPreviewFilePath; // 预览图片文件路径
|
||||
private String mPreviewFileUrl; // 预览图片网络 URL
|
||||
private String mDownloadSavedPath; // 下载图片保存路径
|
||||
// 控件引用
|
||||
private TextView tvTitle; // 对话框标题
|
||||
private TextView tvContent; // 对话框内容
|
||||
private Button btnCancel; // 取消按钮
|
||||
private Button btnConfirm; // 确认按钮
|
||||
private Button btnPreview; // 预览按钮
|
||||
private EditText etURL; // URL 输入框
|
||||
private BackgroundView mBackgroundView; // 背景预览视图
|
||||
|
||||
// ====================== 构造方法(Java7 显式构造,按参数重载排序) ======================
|
||||
/**
|
||||
* 基础构造(仅传入 Context)
|
||||
* @param context 上下文
|
||||
*/
|
||||
public NetworkBackgroundDialog(@NonNull Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "NetworkBackgroundDialog: 基础构造初始化");
|
||||
initHandler();
|
||||
initView();
|
||||
setDismissListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* 带回调的构造(便于外部处理点击事件)
|
||||
* @param context 上下文
|
||||
* @param listener 按钮点击回调
|
||||
*/
|
||||
public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) {
|
||||
super(context);
|
||||
this.listener = listener;
|
||||
LogUtils.d(TAG, "NetworkBackgroundDialog: 带回调构造初始化");
|
||||
initHandler();
|
||||
initView();
|
||||
setDismissListener();
|
||||
}
|
||||
|
||||
// ====================== 生命周期相关方法(对话框消失监听、Handler 初始化) ======================
|
||||
/**
|
||||
* 初始化主线程 Handler,用于接收子线程消息并更新 UI
|
||||
*/
|
||||
private void initHandler() {
|
||||
mUiHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
// 对话框已消失时,不再处理 UI 消息
|
||||
if (!isShowing()) {
|
||||
LogUtils.d(TAG, "handleMessage: 对话框已消失,忽略消息");
|
||||
return;
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MSG_IMAGE_LOAD_SUCCESS:
|
||||
// 图片加载成功,获取文件路径并设置背景
|
||||
mDownloadSavedPath = (String) msg.obj;
|
||||
LogUtils.d(TAG, String.format("handleMessage: 图片加载成功,保存路径:%s", mDownloadSavedPath));
|
||||
int nCurrentPixelColor = BackgroundSourceUtils.getInstance(mContext).getCurrentBackgroundBean().getPixelColor();
|
||||
|
||||
mBackgroundView.loadImage(nCurrentPixelColor, mDownloadSavedPath, true);
|
||||
break;
|
||||
case MSG_IMAGE_LOAD_FAILED:
|
||||
// 图片加载失败,设置默认背景
|
||||
LogUtils.e(TAG, "handleMessage: 图片加载失败");
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
ToastUtils.show("图片预览失败,请检查链接");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogUtils.d(TAG, "initHandler: 主线程 Handler 初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对话框消失监听:移除 Handler 消息,避免内存泄漏
|
||||
*/
|
||||
private void setDismissListener() {
|
||||
this.setOnDismissListener(new OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
// 对话框消失时,移除所有未处理的消息和回调
|
||||
if (mUiHandler != null) {
|
||||
mUiHandler.removeCallbacksAndMessages(null);
|
||||
LogUtils.d(TAG, "onDismiss: Handler 消息已清理");
|
||||
}
|
||||
LogUtils.d(TAG, "onDismiss: 对话框已消失");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "setDismissListener: 对话框消失监听已设置");
|
||||
}
|
||||
|
||||
// ====================== 初始化方法(布局、控件、点击事件) ======================
|
||||
/**
|
||||
* 初始化布局和控件
|
||||
*/
|
||||
private void initView() {
|
||||
mContext = this.getContext();
|
||||
// 加载自定义布局
|
||||
View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_networkbackground, null);
|
||||
// 设置对话框内容视图
|
||||
setView(dialogView);
|
||||
|
||||
// 绑定控件
|
||||
tvTitle = (TextView) dialogView.findViewById(R.id.tv_dialog_title);
|
||||
tvContent = (TextView) dialogView.findViewById(R.id.tv_dialog_content);
|
||||
btnCancel = (Button) dialogView.findViewById(R.id.btn_cancel);
|
||||
btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm);
|
||||
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
|
||||
etURL = (EditText) dialogView.findViewById(R.id.et_url);
|
||||
mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
|
||||
|
||||
// 控件非空校验
|
||||
if (tvTitle == null || tvContent == null || btnCancel == null || btnConfirm == null || btnPreview == null
|
||||
|| etURL == null || mBackgroundView == null) {
|
||||
LogUtils.e(TAG, "initView: 控件绑定失败,请检查布局ID是否正确");
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载初始图片
|
||||
mBackgroundView.setBackgroundResource(R.drawable.blank100x100);
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListeners();
|
||||
|
||||
LogUtils.d(TAG, "initView: 布局和控件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置按钮点击监听
|
||||
*/
|
||||
private void setButtonClickListeners() {
|
||||
// 取消按钮:关闭对话框 + 回调外部
|
||||
btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 取消按钮点击");
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.setCurrentSourceToPreview();
|
||||
|
||||
dismiss(); // 关闭对话框
|
||||
if (listener != null) {
|
||||
listener.onCancel();
|
||||
LogUtils.d(TAG, "onClick: 取消回调已执行");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 确认按钮:关闭对话框 + 回调外部
|
||||
btnConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 确认按钮点击");
|
||||
dismiss(); // 关闭对话框
|
||||
if (TextUtils.isEmpty(mDownloadSavedPath)) {
|
||||
ToastUtils.show("未下载图片。");
|
||||
LogUtils.w(TAG, "onClick: 确认失败,未下载图片");
|
||||
return;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onConfirm(mDownloadSavedPath);
|
||||
LogUtils.d(TAG, String.format("onClick: 确认回调已执行,图片路径:%s", mDownloadSavedPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 图片预览按钮:预览输入框地址图片
|
||||
btnPreview.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 预览按钮点击");
|
||||
downloadImageToAlbumAndPreview();
|
||||
}
|
||||
});
|
||||
|
||||
LogUtils.d(TAG, "setButtonClickListeners: 按钮点击监听已设置");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(图片下载、预览) ======================
|
||||
/**
|
||||
* 下载网络图片并预览
|
||||
*/
|
||||
void downloadImageToAlbumAndPreview() {
|
||||
mPreviewFileUrl = etURL.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(mPreviewFileUrl)) {
|
||||
ToastUtils.show("请输入图片URL");
|
||||
LogUtils.w(TAG, "downloadImageToAlbumAndPreview: 图片URL为空");
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, String.format("downloadImageToAlbumAndPreview: 开始下载图片,URL:%s", mPreviewFileUrl));
|
||||
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String savePath) {
|
||||
LogUtils.d(TAG, String.format("onSuccess: 图片下载成功,保存路径:%s", savePath));
|
||||
// 发送消息到主线程,携带图片路径
|
||||
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
|
||||
mUiHandler.sendMessage(successMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
LogUtils.e(TAG, String.format("onFailure: 图片下载失败,错误信息:%s", errorMsg));
|
||||
ToastUtils.show("下载失败:" + errorMsg);
|
||||
// 发送图片加载失败消息
|
||||
Message failMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_FAILED);
|
||||
mUiHandler.sendMessage(failMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件路径设置 BackgroundView 背景(主线程调用)
|
||||
* @param previewFilePath 图片文件路径
|
||||
*/
|
||||
private void previewBackground(String previewFilePath) {
|
||||
if (TextUtils.isEmpty(previewFilePath)) {
|
||||
LogUtils.w(TAG, "previewBackground: 预览文件路径为空");
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
File imageFile = new File(previewFilePath);
|
||||
if (!imageFile.exists()) {
|
||||
ToastUtils.show("图片文件不存在:" + previewFilePath);
|
||||
LogUtils.e(TAG, String.format("previewBackground: 图片文件不存在,路径:%s", previewFilePath));
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览背景
|
||||
mPreviewFilePath = previewFilePath;
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
mBackgroundView.loadByBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
|
||||
LogUtils.d(TAG, String.format("previewBackground: 图片预览成功,路径:%s", previewFilePath));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("previewBackground: 图片预览失败,错误信息:%s", e.getMessage()), e);
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
} finally {
|
||||
// Java7 手动关闭流,避免资源泄漏
|
||||
if (fis != null) {
|
||||
try {
|
||||
fis.close();
|
||||
LogUtils.d(TAG, "previewBackground: 文件输入流已关闭");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, String.format("previewBackground: 关闭文件输入流失败,错误信息:%s", e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外提供方法(灵活适配不同场景) ======================
|
||||
/**
|
||||
* 对外提供方法:修改对话框标题
|
||||
* @param title 标题文本
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
if (tvTitle != null && !TextUtils.isEmpty(title)) {
|
||||
tvTitle.setText(title);
|
||||
LogUtils.d(TAG, String.format("setTitle: 对话框标题已修改为:%s", title));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:修改对话框内容
|
||||
* @param content 内容文本
|
||||
*/
|
||||
public void setContent(String content) {
|
||||
if (tvContent != null && !TextUtils.isEmpty(content)) {
|
||||
tvContent.setText(content);
|
||||
LogUtils.d(TAG, String.format("setContent: 对话框内容已修改为:%s", content));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供方法:设置按钮点击回调(替代带参构造)
|
||||
* @param listener 按钮点击回调
|
||||
*/
|
||||
public void setOnDialogClickListener(OnDialogClickListener listener) {
|
||||
this.listener = listener;
|
||||
LogUtils.d(TAG, "setOnDialogClickListener: 按钮点击回调已设置");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.handlers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.threads.TTSRemindThread;
|
||||
import cc.winboll.studio.powerbell.utils.ChargeMailNotifyHelper;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* 服务通信Handler
|
||||
* 功能:处理电量提醒消息,构建并发送标准化通知
|
||||
* 特性:弱引用防泄漏、参数严格校验、通知格式统一
|
||||
* 适配:Java7 | API30 | 小米手机
|
||||
*/
|
||||
public class ControlCenterServiceHandler extends Handler {
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "ControlCenterServiceHandler";
|
||||
public static final int MSG_REMIND_TEXT = 1001; // 电量提醒消息标识
|
||||
|
||||
// 提醒类型常量
|
||||
private static final String REMIND_TYPE_CHARGE = "+";
|
||||
private static final String REMIND_TYPE_USAGE = "-";
|
||||
|
||||
// 电量范围常量
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// 通知文案常量(抽离魔法值,便于统一修改)
|
||||
private static final String CHARGE_REMIND_TITLE = "充电提醒";
|
||||
private static final String USAGE_REMIND_TITLE = "耗电提醒";
|
||||
private static final String CHARGE_REMIND_CONTENT_FORMAT = "(+)电量已达额定值。当前电量%d%%,%s。";
|
||||
private static final String USAGE_REMIND_CONTENT_FORMAT = "(-)电量低于指定值。当前电量%d%%,%s。";
|
||||
private static final String CHARGE_STATE_CHARGING = "充电中";
|
||||
private static final String CHARGE_STATE_NOT_CHARGING = "未充电";
|
||||
|
||||
// ================================== 成员变量区(弱引用防泄漏,final保证不可变)=================================
|
||||
private final WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
|
||||
// ================================== 构造方法(强制传入服务,初始化弱引用)=================================
|
||||
public ControlCenterServiceHandler(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "构造方法执行 | service=" + (service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ================================== 核心消息处理(重写handleMessage,解析多参数消息)=================================
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
// 解析消息参数:obj=提醒类型(+/-),arg1=当前电量,arg2=充电状态(1=充电/0=未充电)
|
||||
String remindType = (msg.obj != null) ? (String) msg.obj : "";
|
||||
int currentBattery = msg.arg1;
|
||||
boolean isCharging = msg.arg2 == 1;
|
||||
|
||||
LogUtils.d(TAG, "handleMessage: 接收消息 | what=" + msg.what + " | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
|
||||
|
||||
// 弱引用获取服务,避免内存泄漏
|
||||
ControlCenterService service = mwrControlCenterService.get();
|
||||
if (service == null) {
|
||||
LogUtils.e(TAG, "handleMessage: 服务实例已被GC回收,终止消息处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按消息类型分发处理
|
||||
switch (msg.what) {
|
||||
case MSG_REMIND_TEXT:
|
||||
handleRemindMessage(service, remindType, currentBattery, isCharging);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "handleMessage: 未知消息类型,忽略处理 | what=" + msg.what);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 业务辅助方法(构建通知并发送,全链路参数校验)=================================
|
||||
/**
|
||||
* 处理电量提醒消息,构建带电量+充电状态的通知并发送
|
||||
* @param service 控制中心服务实例(已校验非空)
|
||||
* @param remindType 提醒类型(+充电/-耗电)
|
||||
* @param currentBattery 当前电量(0-100)
|
||||
* @param isCharging 充电状态
|
||||
*/
|
||||
private void handleRemindMessage(ControlCenterService service, String remindType, int currentBattery, boolean isCharging) {
|
||||
LogUtils.d(TAG, "handleRemindMessage: 开始处理提醒消息 | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
|
||||
|
||||
// 1. 前置校验:参数有效性
|
||||
if (!REMIND_TYPE_CHARGE.equals(remindType) && !REMIND_TYPE_USAGE.equals(remindType)) {
|
||||
LogUtils.w(TAG, "handleRemindMessage: 提醒类型无效,忽略 | type=" + remindType + " | 允许值:" + REMIND_TYPE_CHARGE + "/" + REMIND_TYPE_USAGE);
|
||||
return;
|
||||
}
|
||||
if (currentBattery < BATTERY_LEVEL_MIN || currentBattery > BATTERY_LEVEL_MAX) {
|
||||
LogUtils.w(TAG, "handleRemindMessage: 电量值超出范围,忽略 | battery=" + currentBattery + " | 允许范围:" + BATTERY_LEVEL_MIN + "-" + BATTERY_LEVEL_MAX);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 构建提醒内容
|
||||
String chargeStateDesc = isCharging ? CHARGE_STATE_CHARGING : CHARGE_STATE_NOT_CHARGING;
|
||||
String content;
|
||||
if (REMIND_TYPE_CHARGE.equals(remindType)) {
|
||||
content = String.format(CHARGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc);
|
||||
} else {
|
||||
content = String.format(USAGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc);
|
||||
}
|
||||
LogUtils.d(TAG, "handleRemindMessage: 提醒内容构建完成 | content=" + content);
|
||||
|
||||
// 3. 根据 ChargeMailNotify 状态选择提醒方式
|
||||
if (ChargeMailNotifyHelper.isEnabled(service)) {
|
||||
// 启用邮件通知时,使用 Toast 代替通知栏
|
||||
LogUtils.d(TAG, "handleRemindMessage: 充电邮件通知已启用,使用 Toast 提醒");
|
||||
ToastUtils.show(content);
|
||||
} else {
|
||||
// 未启用邮件通知时,使用通知栏提醒
|
||||
if (service.getNotificationManager() == null) {
|
||||
LogUtils.e(TAG, "handleRemindMessage: 通知管理工具类未初始化,无法发送提醒");
|
||||
return;
|
||||
}
|
||||
NotificationMessage remindMsg = new NotificationMessage();
|
||||
if (REMIND_TYPE_CHARGE.equals(remindType)) {
|
||||
remindMsg.setTitle(CHARGE_REMIND_TITLE);
|
||||
remindMsg.setRemindMSG("charge_remind");
|
||||
} else {
|
||||
remindMsg.setTitle(USAGE_REMIND_TITLE);
|
||||
remindMsg.setRemindMSG("usage_remind");
|
||||
}
|
||||
remindMsg.setContent(content);
|
||||
LogUtils.d(TAG, "handleRemindMessage: 调用通知工具类发送提醒 | remindMSG=" + remindMsg.getRemindMSG());
|
||||
service.getNotificationManager().showRemindNotification(service, remindMsg);
|
||||
}
|
||||
LogUtils.d(TAG, "handleRemindMessage: 提醒流程执行完毕");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/29 17:24:53
|
||||
* @Describe 应用运行参数类
|
||||
* 适配 API30,支持 Serializable 持久化、Parcelable Intent 传递、JSON 序列化/反序列化
|
||||
* 包含耗电提醒、充电提醒、电量检测、铃声提醒、相框尺寸等核心配置
|
||||
*/
|
||||
public class AppConfigBean extends BaseBean implements Serializable, Parcelable {
|
||||
// ====================== 静态常量区(首屏可见,统一管理) ======================
|
||||
// 序列化版本号(Serializable 必备,避免反序列化失败)
|
||||
private static final long serialVersionUID = 1L;
|
||||
// 日志标签(全局统一)
|
||||
transient public static final String TAG = "AppConfigBean";
|
||||
// 字段校验常量(统一阈值,避免硬编码)
|
||||
private static final int MIN_INTERVAL = 500; // 最小检测间隔(ms)
|
||||
private static final int MIN_REMIND_INTERVAL = 1000;// 最小提醒间隔(ms)
|
||||
private static final int BATTERY_MIN = 0; // 电量最小值
|
||||
private static final int BATTERY_MAX = 100; // 电量最大值
|
||||
private static final int INVALID_BATTERY = -1; // 无效电量标识
|
||||
private static final int DEFAULT_FRAME_WIDTH = 500; // 默认相框宽度(px)
|
||||
private static final int DEFAULT_FRAME_HEIGHT = 500;// 默认相框高度(px)
|
||||
|
||||
// ====================== 成员变量区(按功能分类:提醒配置→电量状态→检测配置→相框配置) ======================
|
||||
// 耗电提醒配置
|
||||
boolean isEnableUsageReminder = false; // 耗电提醒开关
|
||||
int usageReminderValue = 45; // 耗电提醒阈值(0-100)
|
||||
// 充电提醒配置
|
||||
boolean isEnableChargeReminder = false;// 充电提醒开关
|
||||
int chargeReminderValue = 100; // 充电提醒阈值(0-100)
|
||||
// 铃声提醒配置
|
||||
int reminderIntervalTime = 5000; // 铃声提醒间隔(ms)
|
||||
// 电量状态
|
||||
boolean isCharging = false; // 是否充电
|
||||
// 电量检测配置
|
||||
int batteryDetectInterval = 2000; // 电量检测间隔(ms,适配 RemindThread)
|
||||
// 相框配置
|
||||
int defaultFrameWidth = DEFAULT_FRAME_WIDTH; // 默认相框宽度(px)
|
||||
int defaultFrameHeight = DEFAULT_FRAME_HEIGHT;// 默认相框高度(px)
|
||||
|
||||
// ====================== 构造方法(初始化默认配置,强化默认值校验) ======================
|
||||
public AppConfigBean() {
|
||||
setChargeReminderValue(100);
|
||||
setEnableChargeReminder(false);
|
||||
setUsageReminderValue(10);
|
||||
setEnableUsageReminder(false);
|
||||
setReminderIntervalTime(5000);
|
||||
setBatteryDetectInterval(1000);
|
||||
setDefaultFrameWidth(DEFAULT_FRAME_WIDTH);
|
||||
setDefaultFrameHeight(DEFAULT_FRAME_HEIGHT);
|
||||
LogUtils.d(TAG, "AppConfigBean() 构造器执行 | 默认配置初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法(Setter/Getter,按字段功能分类,补充调试日志) ======================
|
||||
// --------------- 充电状态相关 ---------------
|
||||
public void setIsCharging(boolean isCharging) {
|
||||
this.isCharging = isCharging;
|
||||
LogUtils.d(TAG, String.format("setIsCharging() 执行 | 充电状态=%b", isCharging));
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
// --------------- 耗电提醒配置相关 ---------------
|
||||
public void setEnableUsageReminder(boolean isEnableUsageReminder) {
|
||||
this.isEnableUsageReminder = isEnableUsageReminder;
|
||||
LogUtils.d(TAG, String.format("setEnableUsageReminder() 执行 | 耗电提醒开关=%b", isEnableUsageReminder));
|
||||
}
|
||||
|
||||
public boolean isEnableUsageReminder() {
|
||||
return isEnableUsageReminder;
|
||||
}
|
||||
|
||||
public void setUsageReminderValue(int usageReminderValue) {
|
||||
this.usageReminderValue = Math.min(Math.max(usageReminderValue, BATTERY_MIN), BATTERY_MAX);
|
||||
LogUtils.d(TAG, String.format("setUsageReminderValue() 执行 | 最终阈值=%d | 输入值=%d", this.usageReminderValue, usageReminderValue));
|
||||
}
|
||||
|
||||
public int getUsageReminderValue() {
|
||||
return usageReminderValue;
|
||||
}
|
||||
|
||||
// --------------- 充电提醒配置相关 ---------------
|
||||
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
this.isEnableChargeReminder = isEnableChargeReminder;
|
||||
LogUtils.d(TAG, String.format("setEnableChargeReminder() 执行 | 充电提醒开关=%b", isEnableChargeReminder));
|
||||
}
|
||||
|
||||
public boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public void setChargeReminderValue(int chargeReminderValue) {
|
||||
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, BATTERY_MIN), BATTERY_MAX);
|
||||
LogUtils.d(TAG, String.format("setChargeReminderValue() 执行 | 最终阈值=%d | 输入值=%d", this.chargeReminderValue, chargeReminderValue));
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
// --------------- 铃声提醒配置相关 ---------------
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
this.reminderIntervalTime = Math.max(reminderIntervalTime, MIN_REMIND_INTERVAL);
|
||||
LogUtils.d(TAG, String.format("setReminderIntervalTime() 执行 | 最终间隔=%dms | 输入值=%dms", this.reminderIntervalTime, reminderIntervalTime));
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return reminderIntervalTime;
|
||||
}
|
||||
|
||||
// --------------- 电量检测配置相关 ---------------
|
||||
public void setBatteryDetectInterval(int batteryDetectInterval) {
|
||||
this.batteryDetectInterval = Math.max(batteryDetectInterval, MIN_INTERVAL);
|
||||
LogUtils.d(TAG, String.format("setBatteryDetectInterval() 执行 | 最终间隔=%dms | 输入值=%dms", this.batteryDetectInterval, batteryDetectInterval));
|
||||
}
|
||||
|
||||
public int getBatteryDetectInterval() {
|
||||
return batteryDetectInterval;
|
||||
}
|
||||
|
||||
// --------------- 相框配置相关 ---------------
|
||||
public void setDefaultFrameWidth(int defaultFrameWidth) {
|
||||
this.defaultFrameWidth = defaultFrameWidth;
|
||||
LogUtils.d(TAG, String.format("setDefaultFrameWidth() 执行 | 最终宽度=%dpx | 输入值=%dpx", this.defaultFrameWidth, defaultFrameWidth));
|
||||
}
|
||||
|
||||
public int getDefaultFrameWidth() {
|
||||
return defaultFrameWidth;
|
||||
}
|
||||
|
||||
public void setDefaultFrameHeight(int defaultFrameHeight) {
|
||||
this.defaultFrameHeight = defaultFrameHeight;
|
||||
LogUtils.d(TAG, String.format("setDefaultFrameHeight() 执行 | 最终高度=%dpx | 输入值=%dpx", this.defaultFrameHeight, defaultFrameHeight));
|
||||
}
|
||||
|
||||
public int getDefaultFrameHeight() {
|
||||
return defaultFrameHeight;
|
||||
}
|
||||
|
||||
// ====================== 父类重写方法(JSON 序列化/反序列化,兼容旧配置) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter() 执行 | 开始JSON序列化");
|
||||
|
||||
// 原有字段序列化
|
||||
jsonWriter.name("isEnableUsageReminder").value(isEnableUsageReminder());
|
||||
jsonWriter.name("usageReminderValue").value(getUsageReminderValue());
|
||||
jsonWriter.name("isEnableChargeReminder").value(isEnableChargeReminder());
|
||||
jsonWriter.name("chargeReminderValue").value(getChargeReminderValue());
|
||||
jsonWriter.name("reminderIntervalTime").value(getReminderIntervalTime());
|
||||
jsonWriter.name("isCharging").value(isCharging());
|
||||
// 新增字段序列化(检测配置)
|
||||
jsonWriter.name("batteryDetectInterval").value(getBatteryDetectInterval());
|
||||
// 新增字段序列化(相框配置)
|
||||
jsonWriter.name("defaultFrameWidth").value(getDefaultFrameWidth());
|
||||
jsonWriter.name("defaultFrameHeight").value(getDefaultFrameHeight());
|
||||
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter() 完成 | JSON序列化成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader() 执行 | 开始JSON反序列化");
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
jsonReader.beginObject();
|
||||
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
// 兼容拼写错误字段(isEnableUsegeReminder → isEnableUsageReminder)
|
||||
if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) {
|
||||
bean.setEnableUsageReminder(jsonReader.nextBoolean());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isEnableUsageReminder()));
|
||||
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
|
||||
bean.setUsageReminderValue(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getUsageReminderValue()));
|
||||
} else if (name.equals("isEnableChargeReminder")) {
|
||||
bean.setEnableChargeReminder(jsonReader.nextBoolean());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isEnableChargeReminder()));
|
||||
} else if (name.equals("chargeReminderValue")) {
|
||||
bean.setChargeReminderValue(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getChargeReminderValue()));
|
||||
} else if (name.equals("reminderIntervalTime")) {
|
||||
bean.setReminderIntervalTime(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getReminderIntervalTime()));
|
||||
} else if (name.equals("isCharging")) {
|
||||
bean.setIsCharging(jsonReader.nextBoolean());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%b", name, bean.isCharging()));
|
||||
} else if (name.equals("batteryDetectInterval")) {
|
||||
bean.setBatteryDetectInterval(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getBatteryDetectInterval()));
|
||||
} else if (name.equals("defaultFrameWidth")) {
|
||||
bean.setDefaultFrameWidth(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getDefaultFrameWidth()));
|
||||
} else if (name.equals("defaultFrameHeight")) {
|
||||
bean.setDefaultFrameHeight(jsonReader.nextInt());
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader() 读取字段 | %s=%d", name, bean.getDefaultFrameHeight()));
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader() 跳过未知字段 | %s", name));
|
||||
}
|
||||
}
|
||||
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader() 完成 | JSON反序列化成功");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口实现(API30 Intent 传递必备) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0; // 无特殊内容描述,固定返回0
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
LogUtils.d(TAG, "writeToParcel() 执行 | 开始Parcel序列化");
|
||||
// 按成员变量顺序写入,boolean 转 byte 存储
|
||||
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0));
|
||||
dest.writeInt(usageReminderValue);
|
||||
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0));
|
||||
dest.writeInt(chargeReminderValue);
|
||||
dest.writeInt(reminderIntervalTime);
|
||||
dest.writeByte((byte) (isCharging ? 1 : 0));
|
||||
dest.writeInt(batteryDetectInterval);
|
||||
dest.writeInt(defaultFrameWidth);
|
||||
dest.writeInt(defaultFrameHeight);
|
||||
LogUtils.d(TAG, "writeToParcel() 完成 | Parcel序列化成功");
|
||||
}
|
||||
|
||||
// 反序列化 Creator(必须 public static final 修饰,Java7 适配)
|
||||
public static final Parcelable.Creator<AppConfigBean> CREATOR = new Parcelable.Creator<AppConfigBean>() {
|
||||
@Override
|
||||
public AppConfigBean createFromParcel(Parcel source) {
|
||||
LogUtils.d(TAG, "createFromParcel() 执行 | 开始Parcel反序列化");
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
// 按 writeToParcel 顺序读取
|
||||
bean.isEnableUsageReminder = source.readByte() != 0;
|
||||
bean.usageReminderValue = source.readInt();
|
||||
bean.isEnableChargeReminder = source.readByte() != 0;
|
||||
bean.chargeReminderValue = source.readInt();
|
||||
bean.reminderIntervalTime = source.readInt();
|
||||
bean.isCharging = source.readByte() != 0;
|
||||
bean.batteryDetectInterval = source.readInt();
|
||||
bean.defaultFrameWidth = source.readInt();
|
||||
bean.defaultFrameHeight = source.readInt();
|
||||
LogUtils.d(TAG, "createFromParcel() 完成 | Parcel反序列化成功");
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigBean[] newArray(int size) {
|
||||
return new AppConfigBean[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 11:52:28
|
||||
* @Describe 应用背景图片数据类
|
||||
* 适配 API30,支持 Serializable 持久化、JSON 序列化/反序列化
|
||||
* 存储正式/预览背景配置,包含原图、压缩图、裁剪比例、像素颜色等核心字段
|
||||
*/
|
||||
public class BackgroundBean extends BaseBean implements Serializable {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
// 日志标签(全局统一,替换 Log 为 LogUtils)
|
||||
public static final String TAG = "BackgroundBean";
|
||||
// 兼容旧字段常量(统一管理,避免硬编码)
|
||||
private static final String OLD_FIELD_USE_SCALED_COMPRESS = "isUseScaledCompress";
|
||||
// 字段默认值常量(统一管理,避免魔法值)
|
||||
private static final int DEFAULT_DIMENSION = 100; // 默认宽高
|
||||
private static final int MIN_DIMENSION = 1; // 最小宽高
|
||||
|
||||
// ====================== 成员变量(按功能分类:原图配置→压缩图配置→控制字段→裁剪配置→像素颜色) ======================
|
||||
// 原图配置
|
||||
private String backgroundFileName = ""; // 背景图片文件名
|
||||
private String backgroundFilePath = ""; // 背景图片完整路径
|
||||
private String backgroundFileInfo = ""; // 图片信息(Uri、网络地址等)
|
||||
// 压缩图配置
|
||||
private String backgroundScaledCompressFileName = ""; // 压缩后背景图片文件名
|
||||
private String backgroundScaledCompressFilePath = ""; // 压缩后背景图片完整路径
|
||||
// 控制字段
|
||||
private boolean isUseBackgroundFile = false; // 是否启用背景图片
|
||||
private boolean isUseBackgroundScaledCompressFile = false; // 是否启用压缩背景图(重命名:原isUseScaledCompress)
|
||||
// 裁剪配置
|
||||
private int backgroundWidth = DEFAULT_DIMENSION; // 背景图宽度
|
||||
private int backgroundHeight = DEFAULT_DIMENSION; // 背景图高度
|
||||
// 像素颜色
|
||||
private int pixelColor = 0xFFFFFFFF; // 拾取的像素颜色(纯色背景用)
|
||||
|
||||
// ====================== 构造方法(无参构造,JSON反序列化必备) ======================
|
||||
/**
|
||||
* 无参构造器(必须,JSON反序列化时需默认构造器)
|
||||
*/
|
||||
public BackgroundBean() {
|
||||
LogUtils.d(TAG, "BackgroundBean: 无参构造初始化完成");
|
||||
}
|
||||
|
||||
// ====================== Getter/Setter 方法(按功能分类,补充调试日志,强化校验) ======================
|
||||
// --------------- 原图配置相关 ---------------
|
||||
public String getBackgroundFileName() {
|
||||
return backgroundFileName;
|
||||
}
|
||||
|
||||
public void setBackgroundFileName(String backgroundFileName) {
|
||||
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName;
|
||||
LogUtils.d(TAG, String.format("setBackgroundFileName: 背景文件名设置为 %s", this.backgroundFileName));
|
||||
}
|
||||
|
||||
public String getBackgroundFilePath() {
|
||||
return backgroundFilePath;
|
||||
}
|
||||
|
||||
public void setBackgroundFilePath(String backgroundFilePath) {
|
||||
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath;
|
||||
LogUtils.d(TAG, String.format("setBackgroundFilePath: 背景文件路径设置为 %s", this.backgroundFilePath));
|
||||
}
|
||||
|
||||
public String getBackgroundFileInfo() {
|
||||
return backgroundFileInfo;
|
||||
}
|
||||
|
||||
public void setBackgroundFileInfo(String backgroundFileInfo) {
|
||||
this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo;
|
||||
LogUtils.d(TAG, String.format("setBackgroundFileInfo: 背景文件信息设置为 %s", this.backgroundFileInfo));
|
||||
}
|
||||
|
||||
// --------------- 控制字段相关 ---------------
|
||||
public boolean isUseBackgroundFile() {
|
||||
return isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
LogUtils.d(TAG, String.format("setIsUseBackgroundFile: 是否启用背景图设置为 %b", isUseBackgroundFile));
|
||||
}
|
||||
|
||||
// --------------- 压缩图配置相关 ---------------
|
||||
public String getBackgroundScaledCompressFileName() {
|
||||
return backgroundScaledCompressFileName;
|
||||
}
|
||||
|
||||
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
|
||||
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName;
|
||||
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFileName: 压缩背景文件名设置为 %s", this.backgroundScaledCompressFileName));
|
||||
}
|
||||
|
||||
public String getBackgroundScaledCompressFilePath() {
|
||||
return backgroundScaledCompressFilePath;
|
||||
}
|
||||
|
||||
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
|
||||
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath;
|
||||
LogUtils.d(TAG, String.format("setBackgroundScaledCompressFilePath: 压缩背景文件路径设置为 %s", this.backgroundScaledCompressFilePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名:原isUseScaledCompress → 新isUseBackgroundScaledCompressFile(Getter/Setter同步修改)
|
||||
* 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆
|
||||
*/
|
||||
public boolean isUseBackgroundScaledCompressFile() {
|
||||
return isUseBackgroundScaledCompressFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) {
|
||||
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
|
||||
LogUtils.d(TAG, String.format("setIsUseBackgroundScaledCompressFile: 是否启用压缩背景图设置为 %b", isUseBackgroundScaledCompressFile));
|
||||
}
|
||||
|
||||
// --------------- 裁剪配置相关 ---------------
|
||||
public int getBackgroundWidth() {
|
||||
return backgroundWidth;
|
||||
}
|
||||
|
||||
public void setBackgroundWidth(int backgroundWidth) {
|
||||
this.backgroundWidth = backgroundWidth < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundWidth;
|
||||
LogUtils.d(TAG, String.format("setBackgroundWidth: 背景宽度设置为 %d(输入值:%d)", this.backgroundWidth, backgroundWidth));
|
||||
}
|
||||
|
||||
public int getBackgroundHeight() {
|
||||
return backgroundHeight;
|
||||
}
|
||||
|
||||
public void setBackgroundHeight(int backgroundHeight) {
|
||||
this.backgroundHeight = backgroundHeight < MIN_DIMENSION ? DEFAULT_DIMENSION : backgroundHeight;
|
||||
LogUtils.d(TAG, String.format("setBackgroundHeight: 背景高度设置为 %d(输入值:%d)", this.backgroundHeight, backgroundHeight));
|
||||
}
|
||||
|
||||
// --------------- 像素颜色相关 ---------------
|
||||
public int getPixelColor() {
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
public void setPixelColor(int pixelColor) {
|
||||
this.pixelColor = pixelColor;
|
||||
LogUtils.d(TAG, String.format("setPixelColor: 像素颜色设置为 0x%08X", pixelColor));
|
||||
}
|
||||
|
||||
// ====================== 序列化/反序列化方法(适配重命名字段,兼容旧版本,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = BackgroundBean.class.getName();
|
||||
LogUtils.d(TAG, String.format("getName: 类名标识为 %s", className));
|
||||
return className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化:同步重命名字段(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile)
|
||||
* 确保新字段能正常持久化,同时兼容旧版本JSON(保留旧字段写入,避免旧版本读取异常)
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BackgroundBean bean = this;
|
||||
// 原图配置序列化
|
||||
jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName());
|
||||
jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath());
|
||||
jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo());
|
||||
// 控制字段序列化
|
||||
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
|
||||
// 压缩图配置序列化
|
||||
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
|
||||
jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath());
|
||||
// 关键:新字段序列化(核心)
|
||||
jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 兼容旧版本:保留旧字段名写入(避免旧版本Bean读取时缺失字段)
|
||||
jsonWriter.name(OLD_FIELD_USE_SCALED_COMPRESS).value(bean.isUseBackgroundScaledCompressFile());
|
||||
// 裁剪配置与像素颜色序列化
|
||||
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
|
||||
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
|
||||
jsonWriter.name("pixelColor").value(bean.getPixelColor());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成,已兼容旧字段");
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化:同步处理重命名字段(兼容旧版本JSON,新旧字段都能读取)
|
||||
* 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效)
|
||||
*/
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BackgroundBean bean = new BackgroundBean();
|
||||
jsonReader.beginObject();
|
||||
// 临时变量:存储旧字段值(用于兼容)
|
||||
boolean tempUseScaledCompress = false;
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
switch (name) {
|
||||
case "backgroundFileName":
|
||||
bean.setBackgroundFileName(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundFilePath":
|
||||
bean.setBackgroundFilePath(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundFileInfo":
|
||||
bean.setBackgroundFileInfo(jsonReader.nextString());
|
||||
break;
|
||||
case "isUseBackgroundFile":
|
||||
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
|
||||
break;
|
||||
case "backgroundScaledCompressFileName":
|
||||
bean.setBackgroundScaledCompressFileName(jsonReader.nextString());
|
||||
break;
|
||||
case "backgroundScaledCompressFilePath":
|
||||
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
|
||||
break;
|
||||
case "isUseBackgroundScaledCompressFile":
|
||||
// 关键:读取新字段(优先)
|
||||
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取新字段 isUseBackgroundScaledCompressFile 完成");
|
||||
break;
|
||||
case OLD_FIELD_USE_SCALED_COMPRESS:
|
||||
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
|
||||
tempUseScaledCompress = jsonReader.nextBoolean();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取旧字段 isUseScaledCompress 完成");
|
||||
break;
|
||||
case "backgroundWidth":
|
||||
bean.setBackgroundWidth(jsonReader.nextInt());
|
||||
break;
|
||||
case "backgroundHeight":
|
||||
bean.setBackgroundHeight(jsonReader.nextInt());
|
||||
break;
|
||||
case "pixelColor":
|
||||
bean.setPixelColor(jsonReader.nextInt());
|
||||
break;
|
||||
default:
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段 %s", name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
// 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充
|
||||
if (!bean.isUseBackgroundScaledCompressFile()) {
|
||||
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 旧字段值已填充到新字段");
|
||||
}
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== 辅助方法(重置配置、配置校验,补充调试日志) ======================
|
||||
/**
|
||||
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
|
||||
*/
|
||||
public void resetBackgroundConfig() {
|
||||
this.backgroundFileName = "";
|
||||
this.backgroundFilePath = "";
|
||||
this.backgroundScaledCompressFileName = "";
|
||||
this.backgroundScaledCompressFilePath = "";
|
||||
this.backgroundFileInfo = "";
|
||||
this.isUseBackgroundFile = false;
|
||||
this.isUseBackgroundScaledCompressFile = false;
|
||||
this.backgroundWidth = DEFAULT_DIMENSION;
|
||||
this.backgroundHeight = DEFAULT_DIMENSION;
|
||||
LogUtils.d(TAG, "resetBackgroundConfig: 背景配置已重置为默认值");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验)
|
||||
* 同步使用重命名字段判断压缩图是否启用
|
||||
* @return true-配置有效(可显示背景图),false-配置无效
|
||||
*/
|
||||
public boolean isBackgroundConfigValid() {
|
||||
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
|
||||
if (!isUseBackgroundFile) {
|
||||
LogUtils.d(TAG, "isBackgroundConfigValid: 未启用背景图,配置无效");
|
||||
return false;
|
||||
}
|
||||
// 原图校验:路径非空 或 文件名非空
|
||||
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
|
||||
// 压缩图校验:启用压缩图时,路径/文件名需非空
|
||||
boolean isCompressValid = true;
|
||||
if (isUseBackgroundScaledCompressFile()) {
|
||||
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
|
||||
}
|
||||
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
|
||||
boolean isValid = isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
|
||||
LogUtils.d(TAG, String.format("isBackgroundConfigValid: 背景配置有效性为 %b(启用压缩图:%b,原图有效:%b,压缩图有效:%b)",
|
||||
isValid, isUseBackgroundScaledCompressFile(), isOriginalValid, isCompressValid));
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 14:30:51
|
||||
* @Describe 电池报告数据模型
|
||||
* 适配 API30,存储当前电量、放电时间、充电时间核心数据
|
||||
* 支持参数校验与调试日志输出
|
||||
*/
|
||||
public class BatteryData {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "BatteryData";
|
||||
// 字段校验常量(避免硬编码,统一管理)
|
||||
private static final int BATTERY_MIN = 0;
|
||||
private static final int BATTERY_MAX = 100;
|
||||
private static final String EMPTY_TIME = "00:00:00";
|
||||
|
||||
// ====================== 成员变量(按功能分类:电量→时间) ======================
|
||||
private int currentLevel; // 当前电池电量(0-100)
|
||||
private String dischargeTime; // 放电时间
|
||||
private String chargeTime; // 充电时间
|
||||
|
||||
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
|
||||
/**
|
||||
* 无参构造器(适配 JSON 反序列化、反射实例化场景)
|
||||
*/
|
||||
public BatteryData() {
|
||||
this.currentLevel = BATTERY_MIN;
|
||||
this.dischargeTime = EMPTY_TIME;
|
||||
this.chargeTime = EMPTY_TIME;
|
||||
LogUtils.d(TAG, "BatteryData: 无参构造初始化完成,默认值已设置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造器(核心构造,初始化所有字段)
|
||||
* @param currentLevel 当前电量(0-100)
|
||||
* @param dischargeTime 放电时间
|
||||
* @param chargeTime 充电时间
|
||||
*/
|
||||
public BatteryData(int currentLevel, String dischargeTime, String chargeTime) {
|
||||
// 电量范围校验(0-100,异常值置为0)
|
||||
this.currentLevel = currentLevel >= BATTERY_MIN && currentLevel <= BATTERY_MAX
|
||||
? currentLevel : BATTERY_MIN;
|
||||
// 时间字段防 null(空值置为默认空时间)
|
||||
this.dischargeTime = dischargeTime == null ? EMPTY_TIME : dischargeTime;
|
||||
this.chargeTime = chargeTime == null ? EMPTY_TIME : chargeTime;
|
||||
|
||||
// 调试日志:输出入参与最终赋值结果
|
||||
LogUtils.d(TAG, String.format("BatteryData: 带参构造初始化完成 | 当前电量:%d(输入:%d)| 放电时间:%s(输入:%s)| 充电时间:%s(输入:%s)",
|
||||
this.currentLevel, currentLevel,
|
||||
this.dischargeTime, dischargeTime,
|
||||
this.chargeTime, chargeTime));
|
||||
}
|
||||
|
||||
// ====================== Getter 方法(按成员变量顺序排列,补充日志可选) ======================
|
||||
/**
|
||||
* 获取当前电池电量
|
||||
* @return 当前电量(0-100)
|
||||
*/
|
||||
public int getCurrentLevel() {
|
||||
return currentLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取放电时间
|
||||
* @return 放电时间
|
||||
*/
|
||||
public String getDischargeTime() {
|
||||
return dischargeTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充电时间
|
||||
* @return 充电时间
|
||||
*/
|
||||
public String getChargeTime() {
|
||||
return chargeTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 电池信息数据模型
|
||||
* 适配 API30,存储电量时间戳与电量值,支持 JSON 序列化/反序列化
|
||||
* 修复字段拼写错误,补充数据校验与调试日志
|
||||
*/
|
||||
public class BatteryInfoBean extends BaseBean implements Serializable {
|
||||
// ====================== 静态常量(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "BatteryInfoBean";
|
||||
// 字段校验常量(避免硬编码,统一管理)
|
||||
private static final int BATTERY_MIN = 0;
|
||||
private static final int BATTERY_MAX = 100;
|
||||
private static final long DEFAULT_TIMESTAMP = 0L;
|
||||
private static final int DEFAULT_BATTERY_VALUE = 0;
|
||||
|
||||
// ====================== 成员变量(修复拼写错误:battetyValue → batteryValue) ======================
|
||||
private long timeStamp; // 记录电量的时间戳
|
||||
private int batteryValue; // 电量值(0-100)
|
||||
|
||||
// ====================== 构造方法(按参数重载排序,补充校验与日志) ======================
|
||||
/**
|
||||
* 无参构造器(JSON 反序列化、反射实例化必备)
|
||||
*/
|
||||
public BatteryInfoBean() {
|
||||
this.timeStamp = DEFAULT_TIMESTAMP;
|
||||
this.batteryValue = DEFAULT_BATTERY_VALUE;
|
||||
LogUtils.d(TAG, "BatteryInfoBean: 无参构造初始化完成,默认时间戳:" + timeStamp + ",默认电量:" + batteryValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参构造器(核心构造,初始化所有字段)
|
||||
* @param timeStamp 电量记录时间戳
|
||||
* @param batteryValue 电量值(0-100)
|
||||
*/
|
||||
public BatteryInfoBean(long timeStamp, int batteryValue) {
|
||||
this.timeStamp = timeStamp;
|
||||
// 电量范围校验(0-100,异常值置为默认值)
|
||||
this.batteryValue = batteryValue >= BATTERY_MIN && batteryValue <= BATTERY_MAX
|
||||
? batteryValue : DEFAULT_BATTERY_VALUE;
|
||||
LogUtils.d(TAG, String.format("BatteryInfoBean: 带参构造初始化完成 | 时间戳:%d | 电量:%d(输入:%d)",
|
||||
this.timeStamp, this.batteryValue, batteryValue));
|
||||
}
|
||||
|
||||
// ====================== Setter/Getter 方法(按成员变量顺序排列,修复拼写错误,补充日志) ======================
|
||||
/**
|
||||
* 设置电量记录时间戳
|
||||
* @param timeStamp 时间戳
|
||||
*/
|
||||
public void setTimeStamp(long timeStamp) {
|
||||
this.timeStamp = timeStamp;
|
||||
LogUtils.d(TAG, "setTimeStamp: 时间戳设置为 " + timeStamp);
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置电量值(修复拼写错误:battetyValue → batteryValue)
|
||||
* @param batteryValue 电量值(0-100)
|
||||
*/
|
||||
public void setBatteryValue(int batteryValue) {
|
||||
this.batteryValue = batteryValue >= BATTERY_MIN && batteryValue <= BATTERY_MAX
|
||||
? batteryValue : DEFAULT_BATTERY_VALUE;
|
||||
LogUtils.d(TAG, String.format("setBatteryValue: 电量设置为 %d(输入:%d)",
|
||||
this.batteryValue, batteryValue));
|
||||
}
|
||||
|
||||
public int getBatteryValue() {
|
||||
return batteryValue;
|
||||
}
|
||||
|
||||
// ====================== JSON 序列化/反序列化方法(修复字段拼写错误,补充调试日志) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = BatteryInfoBean.class.getName();
|
||||
LogUtils.d(TAG, "getName: 类名标识为 " + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BatteryInfoBean bean = this;
|
||||
jsonWriter.name("timeStamp").value(bean.getTimeStamp());
|
||||
// 修复 JSON 字段名拼写错误:battetyValue → batteryValue
|
||||
jsonWriter.name("batteryValue").value(bean.getBatteryValue());
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON 序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BatteryInfoBean bean = new BatteryInfoBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
switch (name) {
|
||||
case "timeStamp":
|
||||
bean.setTimeStamp(jsonReader.nextLong());
|
||||
break;
|
||||
case "batteryValue":
|
||||
bean.setBatteryValue(jsonReader.nextInt());
|
||||
break;
|
||||
// 兼容旧字段名(battetyValue),避免旧配置解析失败
|
||||
case "battetyValue":
|
||||
int oldBatteryValue = jsonReader.nextInt();
|
||||
bean.setBatteryValue(oldBatteryValue);
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 读取旧字段 battetyValue,已兼容为 batteryValue,值:" + oldBatteryValue);
|
||||
break;
|
||||
default:
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知字段 " + name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON 反序列化完成 | 时间戳:" + bean.getTimeStamp() + ",电量:" + bean.getBatteryValue());
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
/**
|
||||
* 电池绘制样式枚举 (单选选项)
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public enum BatteryStyle {
|
||||
ENERGY_STYLE, // 能量样式
|
||||
ZEBRA_STYLE, // 条纹样式
|
||||
POINT_STYLE // 点阵样式
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 15:55
|
||||
* @Describe 服务控制参数模型
|
||||
* 适配 API30,管理服务启用状态,支持 Serializable 持久化、Parcelable 组件传递、JSON 序列化解析
|
||||
*/
|
||||
public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable {
|
||||
// ====================== 静态常量(置顶统一管理,避免魔法值) ======================
|
||||
//private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
|
||||
private static final String TAG = "ControlCenterServiceBean";
|
||||
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService"; // JSON 字段常量,避免硬编码
|
||||
|
||||
// ====================== 核心成员变量(私有封装,规范命名) ======================
|
||||
private boolean isEnableService = false; // 服务启用状态:true=启用,false=禁用
|
||||
|
||||
// ====================== Parcelable 静态创建器(必须 public static final,适配 API30 组件传递) ======================
|
||||
public static final Parcelable.Creator<ControlCenterServiceBean> CREATOR = new Parcelable.Creator<ControlCenterServiceBean>() {
|
||||
@Override
|
||||
public ControlCenterServiceBean createFromParcel(Parcel source) {
|
||||
boolean isEnable = source.readByte() != 0;
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnable);
|
||||
LogUtils.d(TAG, String.format("createFromParcel: 反序列化完成,isEnableService=%b", isEnable));
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlCenterServiceBean[] newArray(int size) {
|
||||
LogUtils.d(TAG, String.format("newArray: 创建数组,长度=%d", size));
|
||||
return new ControlCenterServiceBean[size];
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 构造方法(无参+有参,满足不同初始化场景) ======================
|
||||
/**
|
||||
* 无参构造(JSON解析、反射创建必备)
|
||||
*/
|
||||
public ControlCenterServiceBean() {
|
||||
this.isEnableService = false;
|
||||
LogUtils.d(TAG, "无参构造:初始化服务状态为禁用(false)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 有参构造(指定服务启用状态)
|
||||
* @param isEnableService 服务启用状态
|
||||
*/
|
||||
public ControlCenterServiceBean(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
LogUtils.d(TAG, String.format("有参构造:初始化服务状态,isEnableService=%b", isEnableService));
|
||||
}
|
||||
|
||||
// ====================== Getter/Setter 方法(封装成员变量,控制访问) ======================
|
||||
public boolean isEnableService() {
|
||||
LogUtils.d(TAG, String.format("isEnableService: 当前状态=%b", isEnableService));
|
||||
return isEnableService;
|
||||
}
|
||||
|
||||
public void setIsEnableService(boolean isEnableService) {
|
||||
LogUtils.d(TAG, String.format("setIsEnableService: 旧状态=%b,新状态=%b", this.isEnableService, isEnableService));
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
// ====================== 父类 BaseBean 方法重写(核心业务逻辑:JSON 序列化/反序列化) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = ControlCenterServiceBean.class.getName();
|
||||
LogUtils.d(TAG, String.format("getName: 返回类名=%s", className));
|
||||
return className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 JSON(适配数据持久化/网络传输)
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_SERVICE).value(this.isEnableService);
|
||||
LogUtils.d(TAG, String.format("writeThisToJsonWriter: 序列化完成,%s=%b", JSON_FIELD_IS_ENABLE_SERVICE, this.isEnableService));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 反序列化创建对象(适配数据恢复)
|
||||
*/
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (JSON_FIELD_IS_ENABLE_SERVICE.equals(fieldName)) {
|
||||
boolean isEnable = jsonReader.nextBoolean();
|
||||
bean.setIsEnableService(isEnable);
|
||||
LogUtils.d(TAG, String.format("readBeanFromJsonReader: 读取字段,%s=%b", fieldName, isEnable));
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, String.format("readBeanFromJsonReader: 跳过未知字段=%s", fieldName));
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口方法实现(适配 Intent 组件间传递,Java7 适配) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
LogUtils.d(TAG, "describeContents: 返回内容描述符=0");
|
||||
return 0; // 无特殊内容(如文件描述符),返回0即可(API30 标准实现)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 Parcel(Intent 传递必备,Java7 适配:用 byte 存储 boolean)
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
byte flag = (byte) (this.isEnableService ? 1 : 0);
|
||||
dest.writeByte(flag);
|
||||
LogUtils.d(TAG, String.format("writeToParcel: 序列化完成,isEnableService=%b(存储为byte=%d)", this.isEnableService, flag));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 通知数据模型
|
||||
* 适配 API30,统一存储通知标题、内容、标识信息,支持各组件数据传递
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递
|
||||
*/
|
||||
public class NotificationMessage {
|
||||
// ====================== 静态常量(统一管理) ======================
|
||||
private static final String TAG = "NotificationMessage";
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
// ====================== 核心成员变量(按业务逻辑排序) ======================
|
||||
private String title; // 通知标题
|
||||
private String content; // 通知内容
|
||||
private String remindMSG; // 通知标识(区分服务运行/充电/耗电)
|
||||
|
||||
// ====================== 构造方法(无参+全参,满足不同初始化场景) ======================
|
||||
/**
|
||||
* 无参构造器(反射实例化、JSON反序列化必备)
|
||||
*/
|
||||
public NotificationMessage() {
|
||||
this.title = EMPTY_STRING;
|
||||
this.content = EMPTY_STRING;
|
||||
this.remindMSG = EMPTY_STRING;
|
||||
LogUtils.d(TAG, "无参构造:初始化通知数据模型,默认值为空字符串");
|
||||
}
|
||||
|
||||
/**
|
||||
* 全参构造器(直接传参创建实例,简化调用)
|
||||
* @param title 通知标题
|
||||
* @param content 通知内容
|
||||
* @param remindMSG 通知标识
|
||||
*/
|
||||
public NotificationMessage(String title, String content, String remindMSG) {
|
||||
this.title = title == null ? EMPTY_STRING : title;
|
||||
this.content = content == null ? EMPTY_STRING : content;
|
||||
this.remindMSG = remindMSG == null ? EMPTY_STRING : remindMSG;
|
||||
LogUtils.d(TAG, String.format("全参构造:初始化完成 | 标题:%s | 内容:%s | 标识:%s",
|
||||
this.title, this.content, this.remindMSG));
|
||||
}
|
||||
|
||||
// ====================== Setter 方法(补充空值防护与调试日志) ======================
|
||||
public void setTitle(String title) {
|
||||
this.title = title == null ? EMPTY_STRING : title;
|
||||
LogUtils.d(TAG, String.format("setTitle:通知标题设置为「%s」", this.title));
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content == null ? EMPTY_STRING : content;
|
||||
LogUtils.d(TAG, String.format("setContent:通知内容设置为「%s」", this.content));
|
||||
}
|
||||
|
||||
public void setRemindMSG(String remindMSG) {
|
||||
this.remindMSG = remindMSG == null ? EMPTY_STRING : remindMSG;
|
||||
LogUtils.d(TAG, String.format("setRemindMSG:通知标识设置为「%s」", this.remindMSG));
|
||||
}
|
||||
|
||||
// ====================== Getter 方法(按成员变量顺序排列) ======================
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public String getRemindMSG() {
|
||||
return remindMSG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* TTS 语音播放文本内容实体类
|
||||
* 适配:Java7 语法规范 | Android API30 系统版本
|
||||
* 特性:实现序列化接口,支持跨页面/进程传递,属性默认值初始化
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/29 19:13
|
||||
*/
|
||||
public class TTSSpeakTextBean implements Serializable {
|
||||
|
||||
// ====================================== 常量区 - 置顶排序 ======================================
|
||||
/** 日志TAG 瞬态修饰,不参与序列化,减少序列化体积 */
|
||||
transient public static final String TAG = "TTSSpeakTextBean";
|
||||
|
||||
// ====================================== 成员属性区 - 业务属性排序 ======================================
|
||||
/** 延迟播放时长 单位:毫秒,默认值0:无延迟播放 */
|
||||
public int mnDelay = 0;
|
||||
/** TTS语音播放文本内容,默认值空字符串:防止空指针 */
|
||||
public String mszSpeakContent = "";
|
||||
|
||||
// ====================================== 构造方法区 - 无参+有参 完整实现 ======================================
|
||||
/**
|
||||
* 无参构造方法
|
||||
* Java7序列化规范必备 + 兼容反射实例化场景
|
||||
*/
|
||||
public TTSSpeakTextBean() {
|
||||
LogUtils.d(TAG, "【无参构造】TTSSpeakTextBean 实例化,使用默认值 | 延迟:" + mnDelay + " | 文本:" + mszSpeakContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 有参构造方法【主构造】
|
||||
* @param nDelay 延迟播放时长(ms)
|
||||
* @param szSpeakContent 语音播放文本内容
|
||||
*/
|
||||
public TTSSpeakTextBean(int nDelay, String szSpeakContent) {
|
||||
LogUtils.d(TAG, "【有参构造】TTSSpeakTextBean 实例化,入参 | 延迟:" + nDelay + " | 文本:" + szSpeakContent);
|
||||
this.mnDelay = nDelay;
|
||||
this.mszSpeakContent = szSpeakContent;
|
||||
LogUtils.d(TAG, "【有参构造】赋值完成 | 最终延迟:" + this.mnDelay + " | 最终文本:" + this.mszSpeakContent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/29 20:59
|
||||
* @Describe 贴心服务配置实体类 (适配API30 / Java7)
|
||||
*/
|
||||
public class ThoughtfulServiceBean extends BaseBean implements Parcelable, Serializable {
|
||||
|
||||
// ====================== 常量区 - 置顶统一管理 ======================
|
||||
public static final String TAG = ThoughtfulServiceBean.class.getSimpleName();
|
||||
private static final long serialVersionUID = 1L; // Serializable 序列化兼容必备
|
||||
// JSON序列化字段常量 杜绝硬编码
|
||||
public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS = "isEnableChargeTts";
|
||||
public static final String JSON_FIELD_IS_ENABLE_USE_POWER_TTS = "isEnableUsePowerTts";
|
||||
public static final String JSON_FIELD_IS_ENABLE_USAGE_TTS_WITH_BATTERY = "isEnableUseageTtsWithBattary";
|
||||
public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS_WITH_BATTERY = "isEnableChargeTtsWithBattary";
|
||||
// 👉 新增:通知电量消息时同时播放TTS
|
||||
public static final String JSON_FIELD_IS_ENABLE_TTS_WHEN_NOTIFY_BATTERY = "isEnableTtsWhenNotifyBattery";
|
||||
|
||||
// ====================== 核心成员变量 - 私有封装 ======================
|
||||
private boolean isEnableChargeTts = false; // 是否启用 充电TTS贴心语音服务
|
||||
private boolean isEnableUsePowerTts = false; // 是否启用 用电TTS贴心语音服务
|
||||
private boolean isEnableUseageTtsWithBattary = false; // 用电TTS加入电量提醒
|
||||
private boolean isEnableChargeTtsWithBattary = false; // 充电TTS加入电量提醒
|
||||
private boolean isEnableTtsWhenNotifyBattery = false; // 👉 允许通知电量消息时同时播放TTS语音
|
||||
|
||||
// ====================== Parcelable 静态创建器 ======================
|
||||
public static final Creator<ThoughtfulServiceBean> CREATOR = new Creator<ThoughtfulServiceBean>() {
|
||||
@Override
|
||||
public ThoughtfulServiceBean createFromParcel(Parcel source) {
|
||||
return new ThoughtfulServiceBean(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThoughtfulServiceBean[] newArray(int size) {
|
||||
LogUtils.d(TAG, "newArray: 初始化数组,size = " + size);
|
||||
return new ThoughtfulServiceBean[size];
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 构造方法区 ======================
|
||||
public ThoughtfulServiceBean() {
|
||||
LogUtils.d(TAG, "ThoughtfulServiceBean: 无参构造,初始化默认禁用所有TTS服务");
|
||||
}
|
||||
|
||||
public ThoughtfulServiceBean(boolean isEnableChargeTts, boolean isEnableUsePowerTts,
|
||||
boolean isEnableUseageTtsWithBattary, boolean isEnableChargeTtsWithBattary,
|
||||
boolean isEnableTtsWhenNotifyBattery) {
|
||||
this.isEnableChargeTts = isEnableChargeTts;
|
||||
this.isEnableUsePowerTts = isEnableUsePowerTts;
|
||||
this.isEnableUseageTtsWithBattary = isEnableUseageTtsWithBattary;
|
||||
this.isEnableChargeTtsWithBattary = isEnableChargeTtsWithBattary;
|
||||
this.isEnableTtsWhenNotifyBattery = isEnableTtsWhenNotifyBattery;
|
||||
LogUtils.d(TAG, "ThoughtfulServiceBean: 全参构造 | 充电TTS=" + isEnableChargeTts + " | 用电TTS=" + isEnableUsePowerTts
|
||||
+ " | 用电TTS加电量=" + isEnableUseageTtsWithBattary + " | 充电TTS加电量=" + isEnableChargeTtsWithBattary
|
||||
+ " | 通知电量时播TTS=" + isEnableTtsWhenNotifyBattery);
|
||||
}
|
||||
|
||||
private ThoughtfulServiceBean(Parcel in) {
|
||||
this.isEnableChargeTts = in.readByte() != 0;
|
||||
this.isEnableUsePowerTts = in.readByte() != 0;
|
||||
this.isEnableUseageTtsWithBattary = in.readByte() != 0;
|
||||
this.isEnableChargeTtsWithBattary = in.readByte() != 0;
|
||||
this.isEnableTtsWhenNotifyBattery = in.readByte() != 0; // 👉 新增反序列化
|
||||
LogUtils.d(TAG, "ThoughtfulServiceBean: Parcel构造解析完成");
|
||||
}
|
||||
|
||||
// ====================== Getter/Setter 方法区 ======================
|
||||
public boolean isEnableChargeTts() {
|
||||
return isEnableChargeTts;
|
||||
}
|
||||
|
||||
public void setIsEnableChargeTts(boolean isEnableChargeTts) {
|
||||
LogUtils.d(TAG, "setIsEnableChargeTts: 旧值=" + this.isEnableChargeTts + " | 新值=" + isEnableChargeTts);
|
||||
this.isEnableChargeTts = isEnableChargeTts;
|
||||
}
|
||||
|
||||
public boolean isEnableUsePowerTts() {
|
||||
return isEnableUsePowerTts;
|
||||
}
|
||||
|
||||
public void setIsEnableUsePowerTts(boolean isEnableUsePowerTts) {
|
||||
LogUtils.d(TAG, "setIsEnableUsePowerTts: 旧值=" + this.isEnableUsePowerTts + " | 新值=" + isEnableUsePowerTts);
|
||||
this.isEnableUsePowerTts = isEnableUsePowerTts;
|
||||
}
|
||||
|
||||
public boolean isEnableUseageTtsWithBattary() {
|
||||
return isEnableUseageTtsWithBattary;
|
||||
}
|
||||
|
||||
public void setIsEnableUseageTtsWithBattary(boolean isEnableUseageTtsWithBattary) {
|
||||
LogUtils.d(TAG, "setIsEnableUseageTtsWithBattary: 旧值=" + this.isEnableUseageTtsWithBattary + " | 新值=" + isEnableUseageTtsWithBattary);
|
||||
this.isEnableUseageTtsWithBattary = isEnableUseageTtsWithBattary;
|
||||
}
|
||||
|
||||
public boolean isEnableChargeTtsWithBattary() {
|
||||
return isEnableChargeTtsWithBattary;
|
||||
}
|
||||
|
||||
public void setIsEnableChargeTtsWithBattary(boolean isEnableChargeTtsWithBattary) {
|
||||
LogUtils.d(TAG, "setIsEnableChargeTtsWithBattary: 旧值=" + this.isEnableChargeTtsWithBattary + " | 新值=" + isEnableChargeTtsWithBattary);
|
||||
this.isEnableChargeTtsWithBattary = isEnableChargeTtsWithBattary;
|
||||
}
|
||||
|
||||
// 👉 新增:通知电量时播放TTS
|
||||
public boolean isEnableTtsWhenNotifyBattery() {
|
||||
return isEnableTtsWhenNotifyBattery;
|
||||
}
|
||||
|
||||
public void setIsEnableTtsWhenNotifyBattery(boolean isEnableTtsWhenNotifyBattery) {
|
||||
LogUtils.d(TAG, "setIsEnableTtsWhenNotifyBattery: 旧值=" + this.isEnableTtsWhenNotifyBattery + " | 新值=" + isEnableTtsWhenNotifyBattery);
|
||||
this.isEnableTtsWhenNotifyBattery = isEnableTtsWhenNotifyBattery;
|
||||
}
|
||||
|
||||
// ====================== 重写父类 BaseBean 核心方法 ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = ThoughtfulServiceBean.class.getName();
|
||||
LogUtils.d(TAG, "getName: 返回当前实体类名 = " + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS).value(this.isEnableChargeTts);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_USE_POWER_TTS).value(this.isEnableUsePowerTts);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_USAGE_TTS_WITH_BATTERY).value(this.isEnableUseageTtsWithBattary);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS_WITH_BATTERY).value(this.isEnableChargeTtsWithBattary);
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_TTS_WHEN_NOTIFY_BATTERY).value(this.isEnableTtsWhenNotifyBattery); // 👉 新增JSON写入
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成,所有TTS服务状态已写入");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
ThoughtfulServiceBean bean = new ThoughtfulServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
switch (fieldName) {
|
||||
case JSON_FIELD_IS_ENABLE_CHARGE_TTS:
|
||||
bean.setIsEnableChargeTts(jsonReader.nextBoolean());
|
||||
break;
|
||||
case JSON_FIELD_IS_ENABLE_USE_POWER_TTS:
|
||||
bean.setIsEnableUsePowerTts(jsonReader.nextBoolean());
|
||||
break;
|
||||
case JSON_FIELD_IS_ENABLE_USAGE_TTS_WITH_BATTERY:
|
||||
bean.setIsEnableUseageTtsWithBattary(jsonReader.nextBoolean());
|
||||
break;
|
||||
case JSON_FIELD_IS_ENABLE_CHARGE_TTS_WITH_BATTERY:
|
||||
bean.setIsEnableChargeTtsWithBattary(jsonReader.nextBoolean());
|
||||
break;
|
||||
case JSON_FIELD_IS_ENABLE_TTS_WHEN_NOTIFY_BATTERY:
|
||||
bean.setIsEnableTtsWhenNotifyBattery(jsonReader.nextBoolean()); // 👉 新增JSON读取
|
||||
break;
|
||||
default:
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段 = " + fieldName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成,生成实体对象");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口方法 ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeByte((byte) (isEnableChargeTts ? 1 : 0));
|
||||
dest.writeByte((byte) (isEnableUsePowerTts ? 1 : 0));
|
||||
dest.writeByte((byte) (isEnableUseageTtsWithBattary ? 1 : 0));
|
||||
dest.writeByte((byte) (isEnableChargeTtsWithBattary ? 1 : 0));
|
||||
dest.writeByte((byte) (isEnableTtsWhenNotifyBattery ? 1 : 0)); // 👉 新增Parcel写入
|
||||
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成,所有TTS服务状态已写入");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.receivers;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
import cc.winboll.studio.powerbell.services.ThoughtfulService;
|
||||
|
||||
/**
|
||||
* 控制中心广播接收器
|
||||
* 功能:监听电池状态变化、前台通知更新、配置变更指令
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:23
|
||||
* @Describe 统一处理系统与应用内广播,同步电池状态与配置,保障多线程数据一致性
|
||||
*/
|
||||
public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
// ====================== 静态常量区 ======================
|
||||
public static final String TAG = "ControlCenterServiceReceiver";
|
||||
|
||||
public static final String ACTION_UPDATE_FOREGROUND_NOTIFICATION = "cc.winboll.studio.powerbell.action.ACTION_UPDATE_FOREGROUND_NOTIFICATION";
|
||||
public static final String ACTION_APPCONFIG_CHANGED = "cc.winboll.studio.powerbell.action.ACTION_APPCONFIG_CHANGED";
|
||||
public static final String EXTRA_APP_CONFIG_BEAN = "extra_app_config_bean";
|
||||
|
||||
private static final int BROADCAST_PRIORITY = IntentFilter.SYSTEM_HIGH_PRIORITY - 10;
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
private static final int INVALID_BATTERY = -1;
|
||||
|
||||
// ====================== 静态状态(防抖动 + 防重复播报) ======================
|
||||
private static volatile int sLastBatteryLevel = INVALID_BATTERY;
|
||||
private static volatile boolean sIsCharging = false;
|
||||
|
||||
// 【新增】防重复触发:3秒内只允许一次播报(关键修复)
|
||||
private static final long MIN_TRIGGER_INTERVAL = 3000;
|
||||
private static long sLastTriggerTime = 0;
|
||||
|
||||
// ====================== 成员变量 ======================
|
||||
private WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
private boolean isRegistered = false;
|
||||
|
||||
// ====================== 构造 ======================
|
||||
public ControlCenterServiceReceiver(ControlCenterService service) {
|
||||
LogUtils.d(TAG, String.format("ControlCenterServiceReceiver() 构造 | 服务:%s",
|
||||
service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ====================== 广播入口 ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent != null ? intent.getAction() : "null";
|
||||
LogUtils.d(TAG, String.format("onReceive() | Action=%s", action));
|
||||
|
||||
if (context == null || intent == null || action == null) {
|
||||
LogUtils.e(TAG, "onReceive() 终止:参数无效");
|
||||
return;
|
||||
}
|
||||
|
||||
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
|
||||
if (service == null || service.isDestroyed()) {
|
||||
LogUtils.e(TAG, "onReceive() 终止:服务已销毁");
|
||||
unregisterAction(context);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case Intent.ACTION_BATTERY_CHANGED:
|
||||
handleBatteryStateChanged(service, intent);
|
||||
break;
|
||||
case ACTION_UPDATE_FOREGROUND_NOTIFICATION:
|
||||
handleUpdateForegroundNotification(service);
|
||||
break;
|
||||
case ACTION_APPCONFIG_CHANGED:
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "未知Action:" + action);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 电池状态(核心修复区) ======================
|
||||
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged() 解析电池状态");
|
||||
try {
|
||||
boolean currentCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
|
||||
LogUtils.d(TAG, String.format("当前:充电=%b | 电量=%d%%", currentCharging, currentBatteryLevel));
|
||||
|
||||
// ================ 【关键修复1】状态没变直接跳过 ================
|
||||
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "状态无变化,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
// ================ 【关键修复2】只有 真正插拔充电 才播报 ================
|
||||
boolean isRealPlugSwitch = (currentCharging != sIsCharging);
|
||||
boolean isBatteryValid = (sLastBatteryLevel != INVALID_BATTERY);
|
||||
|
||||
// ================ 【关键修复3】防抖动:3秒内不重复触发 ================
|
||||
long now = System.currentTimeMillis();
|
||||
boolean canTrigger = (now - sLastTriggerTime >= MIN_TRIGGER_INTERVAL);
|
||||
|
||||
if (isRealPlugSwitch && isBatteryValid && canTrigger) {
|
||||
LogUtils.d(TAG, "检测到充电状态切换 → 执行TTS提醒");
|
||||
|
||||
// 更新触发时间
|
||||
sLastTriggerTime = now;
|
||||
|
||||
// 执行播报
|
||||
if (currentCharging) {
|
||||
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.CHARGE_STATE);
|
||||
} else {
|
||||
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.DISCHARGE_STATE);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
sIsCharging = currentCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
|
||||
// 同步配置
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 配置同步 ======================
|
||||
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate() 同步配置");
|
||||
try {
|
||||
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
|
||||
if (latestConfig == null) {
|
||||
LogUtils.e(TAG, "配置为空,终止");
|
||||
return;
|
||||
}
|
||||
|
||||
App.sQuantityOfElectricity = sLastBatteryLevel;
|
||||
latestConfig.setIsCharging(sIsCharging);
|
||||
service.notifyAppConfigUpdate(latestConfig);
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 通知更新 ======================
|
||||
private void handleUpdateForegroundNotification(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification() 更新通知");
|
||||
try {
|
||||
NotificationManagerUtils notifyUtils = service.getNotificationManager();
|
||||
NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
|
||||
|
||||
if (notifyUtils == null || notifyMsg == null) {
|
||||
LogUtils.e(TAG, "通知工具或消息为空");
|
||||
return;
|
||||
}
|
||||
notifyUtils.updateForegroundServiceNotify(notifyMsg);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "更新通知异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 注册/注销 ======================
|
||||
public void registerAction(Context context) {
|
||||
if (context == null || isRegistered) return;
|
||||
try {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(ACTION_UPDATE_FOREGROUND_NOTIFICATION);
|
||||
filter.addAction(ACTION_APPCONFIG_CHANGED);
|
||||
filter.setPriority(BROADCAST_PRIORITY);
|
||||
context.registerReceiver(this, filter);
|
||||
isRegistered = true;
|
||||
LogUtils.d(TAG, "广播注册完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "注册异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterAction(Context context) {
|
||||
if (context == null || !isRegistered) return;
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
isRegistered = false;
|
||||
LogUtils.d(TAG, "广播已注销");
|
||||
} catch (Exception e) {
|
||||
LogUtils.w(TAG, "注销异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 释放 ======================
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release() 释放资源");
|
||||
if (mwrControlCenterService != null) {
|
||||
mwrControlCenterService.clear();
|
||||
mwrControlCenterService = null;
|
||||
}
|
||||
// 重置静态状态
|
||||
sLastBatteryLevel = INVALID_BATTERY;
|
||||
sIsCharging = false;
|
||||
sLastTriggerTime = 0; // 【新增】重置防抖时间
|
||||
}
|
||||
|
||||
// ====================== Getter ======================
|
||||
public static int getLastBatteryLevel() {
|
||||
return sLastBatteryLevel;
|
||||
}
|
||||
|
||||
public static boolean isLastCharging() {
|
||||
return sIsCharging;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.receivers;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:13
|
||||
* @Describe 全局应用广播接收器
|
||||
* 功能:监听系统电池状态变化,同步状态到配置工具类,通知页面更新
|
||||
* 适配:Java7 | API30 | 内存泄漏防护
|
||||
*/
|
||||
public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "GlobalApplicationReceiver";
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
private static volatile int sLastBatteryLevel = -1; // 历史电量(0-100)
|
||||
private static volatile boolean sLastIsCharging = false; // 历史充电状态
|
||||
|
||||
// ====================== 成员变量区(按功能分层,移除冗余的mCurrentReceiver) ======================
|
||||
private App mGlobalApplication;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
|
||||
// ====================== 构造方法(强化参数校验,初始化核心依赖) ======================
|
||||
public GlobalApplicationReceiver(App globalApplication) {
|
||||
LogUtils.d(TAG, String.format("构造接收器 | App实例:%s", globalApplication));
|
||||
if (globalApplication == null) {
|
||||
LogUtils.e(TAG, "构造失败:App实例为空");
|
||||
throw new IllegalArgumentException("App cannot be null");
|
||||
}
|
||||
this.mGlobalApplication = globalApplication;
|
||||
this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
|
||||
LogUtils.d(TAG, String.format("构造完成 | AppConfigUtils:%s", mAppConfigUtils));
|
||||
}
|
||||
|
||||
// ====================== 广播核心接收逻辑(入口方法,过滤电池状态广播) ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent != null ? intent.getAction() : "null";
|
||||
LogUtils.d(TAG, String.format("onReceive: 接收广播 | 上下文:%s | Action:%s", context, action));
|
||||
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null || action == null) {
|
||||
LogUtils.e(TAG, "onReceive: 参数无效,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅处理电池状态变化广播
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
|
||||
handleBatteryStateChanged(context, intent);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面) ======================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param context 上下文
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(Context context, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态");
|
||||
try {
|
||||
// 1. 解析当前电池状态(复用工具类,二次校验电量范围)
|
||||
boolean currentIsCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 当前状态 | 充电=%b | 电量=%d%%", currentIsCharging, currentBatteryLevel));
|
||||
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 同步最新状态到配置工具类
|
||||
if (mAppConfigUtils != null) {
|
||||
if (currentIsCharging != sLastIsCharging) {
|
||||
mAppConfigUtils.setCharging(currentIsCharging);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步充电状态 | %b", currentIsCharging));
|
||||
}
|
||||
if (currentBatteryLevel != sLastBatteryLevel) {
|
||||
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 同步电量 | %d%%", currentBatteryLevel));
|
||||
}
|
||||
} else {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: AppConfigUtils为空,同步失败");
|
||||
}
|
||||
|
||||
// 4. 执行状态变化后的业务逻辑
|
||||
// 记录电量变化时间
|
||||
if (App.getAppCacheUtils(context) != null) {
|
||||
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
|
||||
}
|
||||
// 通知MainActivity更新电量
|
||||
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
|
||||
LogUtils.d(TAG, String.format("handleBatteryStateChanged: 发送电量更新消息到MainActivity | %d%%", currentBatteryLevel));
|
||||
|
||||
// 5. 更新历史状态缓存
|
||||
sLastIsCharging = currentIsCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 广播注册/注销(强化容错,避免重复操作) ======================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
*/
|
||||
public void registerAction() {
|
||||
LogUtils.d(TAG, "registerAction: 注册广播");
|
||||
if (mGlobalApplication == null) {
|
||||
LogUtils.e(TAG, "注册失败:App实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先注销再注册,避免重复注册异常
|
||||
unregisterAction();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
mGlobalApplication.registerReceiver(this, filter);
|
||||
LogUtils.d(TAG, "registerAction: 广播注册成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器
|
||||
*/
|
||||
public void unregisterAction() {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播");
|
||||
if (mGlobalApplication == null) {
|
||||
LogUtils.e(TAG, "注销失败:App实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mGlobalApplication.unregisterReceiver(this);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 资源释放方法(主动释放,彻底避免内存泄漏) ======================
|
||||
/**
|
||||
* 释放接收器资源,供App销毁时调用
|
||||
*/
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release: 释放接收器资源");
|
||||
// 注销广播
|
||||
unregisterAction();
|
||||
// 置空引用,帮助GC回收
|
||||
mGlobalApplication = null;
|
||||
mAppConfigUtils = null;
|
||||
// 重置静态状态缓存
|
||||
sLastBatteryLevel = -1;
|
||||
sLastIsCharging = false;
|
||||
LogUtils.d(TAG, "release: 资源释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.receivers;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/06 15:01:39
|
||||
* @Describe 应用核心广播接收器
|
||||
* 功能:监听开机完成广播,实现服务开机自启
|
||||
* 适配:Java7 | API30 | 服务启动兼容性处理
|
||||
*/
|
||||
public class MainReceiver extends BroadcastReceiver {
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
public static final String TAG = "MainReceiver";
|
||||
// 系统广播Action常量
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
// API版本常量(适配前台服务启动要求)
|
||||
private static final int API_LEVEL_26 = 26;
|
||||
|
||||
// ====================== 静态状态标记(volatile保证多线程可见性) ======================
|
||||
// 历史电量值,用于校验电量变化(暂未使用,保留扩展能力)
|
||||
private static volatile int sLastBatteryLevel = -1;
|
||||
|
||||
// ====================== 广播核心接收逻辑(入口方法,分Action处理) ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null) {
|
||||
LogUtils.e(TAG, "onReceive: 上下文或意图为空,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
LogUtils.d(TAG, String.format("onReceive: 接收广播 | Action:%s", action));
|
||||
|
||||
// 仅处理开机完成广播
|
||||
if (ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
handleBootCompleted(context);
|
||||
} else {
|
||||
LogUtils.w(TAG, String.format("onReceive: 忽略未知Action:%s", action));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 业务处理方法(处理开机完成广播,实现服务自启) ======================
|
||||
/**
|
||||
* 处理开机完成广播,自动启动控制中心服务
|
||||
* @param context 上下文
|
||||
*/
|
||||
private void handleBootCompleted(Context context) {
|
||||
LogUtils.d(TAG, "handleBootCompleted: 开始处理开机完成广播");
|
||||
try {
|
||||
// 1. 校验服务启用状态
|
||||
boolean isServiceEnabled = App.getAppConfigUtils(context).isServiceEnabled();
|
||||
LogUtils.d(TAG, String.format("handleBootCompleted: 服务启用状态:%b", isServiceEnabled));
|
||||
if (!isServiceEnabled) {
|
||||
LogUtils.d(TAG, "handleBootCompleted: 服务未启用,跳过自启");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 校验服务是否已运行
|
||||
String serviceClassName = ControlCenterService.class.getName();
|
||||
boolean isServiceAlive = ServiceUtils.isServiceAlive(context.getApplicationContext(), serviceClassName);
|
||||
LogUtils.d(TAG, String.format("handleBootCompleted: 服务运行状态:%b", isServiceAlive));
|
||||
if (isServiceAlive) {
|
||||
LogUtils.d(TAG, "handleBootCompleted: 服务已运行,无需重复启动");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 按API版本启动服务(适配前台服务要求)
|
||||
Intent serviceIntent = new Intent(context, ControlCenterService.class);
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
|
||||
context.startForegroundService(serviceIntent);
|
||||
LogUtils.d(TAG, "handleBootCompleted: 启动前台服务(API >= 26)");
|
||||
} else {
|
||||
context.startService(serviceIntent);
|
||||
LogUtils.d(TAG, "handleBootCompleted: 启动普通服务(API < 26)");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "handleBootCompleted: 服务自启处理完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBootCompleted: 服务自启失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.services;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
|
||||
/**
|
||||
* 电池提醒核心服务进程守护类
|
||||
* 功能:监听主服务 {@link ControlCenterService} 存活状态,异常断开时自动重启并绑定
|
||||
* 适配:Java7 | API30 | 前台服务启动规则 | 服务绑定稳定性保障
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Describe 守护服务:保障ControlCenterService持续运行
|
||||
*/
|
||||
public class AssistantService extends Service {
|
||||
// ====================== 静态常量区(置顶归类,消除魔法值) ======================
|
||||
private static final String TAG = "AssistantService";
|
||||
// 服务返回策略常量
|
||||
private static final int SERVICE_RETURN_STICKY = START_STICKY;
|
||||
// 服务绑定标记常量
|
||||
private static final int BIND_FLAG = Context.BIND_IMPORTANT;
|
||||
// API版本常量(适配前台服务启动要求)
|
||||
private static final int API_LEVEL_26 = Build.VERSION_CODES.O;
|
||||
|
||||
// ====================== 成员变量区(按功能分层,volatile保证多线程可见性) ======================
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private volatile boolean mIsThreadAlive;
|
||||
|
||||
// ====================== 内部类(服务连接状态监听,前置定义便于引用) ======================
|
||||
/**
|
||||
* 服务连接状态监听器
|
||||
* 主服务连接成功时记录状态,断开时自动重连
|
||||
*/
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
String className = name != null ? name.getClassName() : "null";
|
||||
LogUtils.d(TAG, String.format("onServiceConnected: 主服务连接成功 | 组件名=%s | Binder=%s", className, service));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
String className = name != null ? name.getClassName() : "null";
|
||||
LogUtils.d(TAG, String.format("onServiceDisconnected: 主服务连接断开 | 组件名=%s", className));
|
||||
// 主服务断开且配置启用时,重新唤醒绑定
|
||||
if (mAppConfigUtils != null && mAppConfigUtils.isServiceEnabled()) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected: 配置启用,尝试重新唤醒并绑定主服务");
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 服务生命周期方法(按执行顺序排列:onCreate→onStartCommand→onBind→onDestroy) ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, String.format("onCreate: 守护服务启动 | 进程ID=%d", android.os.Process.myPid()));
|
||||
|
||||
// 初始化配置工具类,添加空指针防护
|
||||
mAppConfigUtils = App.getAppConfigUtils(this);
|
||||
if (mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "onCreate: AppConfigUtils初始化失败,守护服务无法工作");
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化服务连接对象
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
LogUtils.d(TAG, "onCreate: ServiceConnection初始化完成");
|
||||
}
|
||||
|
||||
// 初始化运行状态,执行核心守护逻辑
|
||||
mIsThreadAlive = false;
|
||||
run();
|
||||
LogUtils.d(TAG, String.format("onCreate: 守护服务初始化完成 | 服务启用状态=%b", mAppConfigUtils.isServiceEnabled()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, String.format("onStartCommand: 守护服务触发重启 | flags=%d | startId=%d", flags, startId));
|
||||
// 配置工具类为空时,直接返回非粘性策略
|
||||
if (mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "onStartCommand: AppConfigUtils未初始化,终止服务");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
run();
|
||||
int returnFlag = mAppConfigUtils.isServiceEnabled() ? SERVICE_RETURN_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, String.format("onStartCommand: 处理完成 | 返回策略=%s", returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT"));
|
||||
return returnFlag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, String.format("onBind: 服务绑定请求 | intent=%s", intent));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁流程启动");
|
||||
|
||||
// 重置运行状态,终止守护逻辑
|
||||
mIsThreadAlive = false;
|
||||
|
||||
// 解绑主服务,添加异常捕获防止重复解绑崩溃
|
||||
unbindMainService();
|
||||
|
||||
// 置空工具类引用,帮助GC回收
|
||||
mAppConfigUtils = null;
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁完成");
|
||||
}
|
||||
|
||||
// ====================== 核心业务逻辑(守护主服务存活) ======================
|
||||
/**
|
||||
* 执行守护逻辑:检查主服务状态,按需唤醒并绑定
|
||||
* 前置条件:mAppConfigUtils 必须初始化完成
|
||||
*/
|
||||
private void run() {
|
||||
boolean isServiceEnabled = mAppConfigUtils.isServiceEnabled();
|
||||
LogUtils.d(TAG, String.format("run: 执行守护逻辑 | 配置启用=%b | 线程存活=%b", isServiceEnabled, mIsThreadAlive));
|
||||
if (isServiceEnabled) {
|
||||
if (!mIsThreadAlive) {
|
||||
mIsThreadAlive = true;
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "run: 服务未启用,跳过守护逻辑");
|
||||
// 服务未启用时,重置线程状态
|
||||
mIsThreadAlive = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒主服务并建立绑定,确保主服务持续运行
|
||||
* 适配 API26+ 前台服务启动规则,避免系统限制导致启动失败
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
// 检查主服务存活状态
|
||||
String mainServiceName = ControlCenterService.class.getName();
|
||||
boolean isMainServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), mainServiceName);
|
||||
LogUtils.d(TAG, String.format("wakeupAndBindMain: 主服务存活状态=%b", isMainServiceAlive));
|
||||
|
||||
// 主服务未存活时,按需启动(区分API版本)
|
||||
if (!isMainServiceAlive) {
|
||||
Intent mainServiceIntent = new Intent(AssistantService.this, ControlCenterService.class);
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_26) {
|
||||
startForegroundService(mainServiceIntent);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: API26+ 以前台服务方式启动主服务");
|
||||
} else {
|
||||
startService(mainServiceIntent);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 以普通服务方式启动主服务");
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定主服务,监听连接状态,添加结果日志
|
||||
Intent bindIntent = new Intent(AssistantService.this, ControlCenterService.class);
|
||||
boolean bindResult = bindService(bindIntent, mMyServiceConnection, BIND_FLAG);
|
||||
LogUtils.d(TAG, String.format("wakeupAndBindMain: 绑定主服务结果=%b | 绑定标记=BIND_IMPORTANT", bindResult));
|
||||
}
|
||||
|
||||
// ====================== 辅助工具方法(拆分独立逻辑,提高可维护性) ======================
|
||||
/**
|
||||
* 解绑主服务,包含异常捕获与状态日志
|
||||
*/
|
||||
private void unbindMainService() {
|
||||
if (mMyServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mMyServiceConnection);
|
||||
LogUtils.d(TAG, "unbindMainService: 已成功解绑ControlCenterService");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, String.format("unbindMainService: 解绑服务失败,服务未绑定 | %s", e.getMessage()));
|
||||
}
|
||||
mMyServiceConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||