Compare commits
10 Commits
powerbell-
...
winboll
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a9199f1be | |||
| 89aefd6354 | |||
| cce4a643e7 | |||
| 61cfa7f3ff | |||
| cb9ad1dc57 | |||
| 98c334f442 | |||
| 47c328cd25 | |||
| 7134d4e1c8 | |||
| 778a1bc98e | |||
| bb94f87597 |
87
.github/workflows/android.yml
vendored
87
.github/workflows/android.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: Android CI
|
||||
|
||||
# 触发器
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- *-beta
|
||||
pull_request:
|
||||
tags:
|
||||
- *-beta
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# 设置 JDK 环境
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
# 获取应用打包秘钥库
|
||||
- name: Checkout Android Keystore
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: zhangsken/keystore # 存储应用打包用的 keystore 的仓库(格式:用户名/仓库名)
|
||||
token: ${{ secrets.APP_SECRET_TOKEN_1 }} # 连接仓库的 token , 需要单独配置
|
||||
path: keystore # 仓库的根目录名
|
||||
|
||||
# 打包 Stage Release 版本应用
|
||||
- name: Build with Gradle
|
||||
run: bash ./gradlew assembleBetaRelease
|
||||
# 创建release
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.APP_SECRET_TOKEN_1 }}
|
||||
# GitHub 会自动创建 GITHUB_TOKEN 密码以在工作流程中使用。
|
||||
# 您可以使用 GITHUB_TOKEN 在工作流程运行中进行身份验证。
|
||||
# 当您启用 GitHub Actions 时,GitHub 在您的仓库中安装 GitHub 应用程序。
|
||||
# GITHUB_TOKEN 密码是一种 GitHub 应用程序 安装访问令牌。
|
||||
# 您可以使用安装访问令牌代表仓库中安装的 GitHub 应用程序 进行身份验证。
|
||||
# 令牌的权限仅限于包含您的工作流程的仓库。 更多信息请参阅“GITHUB_TOKEN 的权限”。
|
||||
# 在每个作业开始之前, GitHub 将为作业提取安装访问令牌。 令牌在作业完成后过期。
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
# 获取 APK 版本号
|
||||
- name: Get Version Name
|
||||
uses: actions/github-script@v3
|
||||
id: get-version
|
||||
with:
|
||||
script: |
|
||||
const str=process.env.GITHUB_REF;
|
||||
return str.substring(str.indexOf("v"));
|
||||
result-encoding: string
|
||||
# 上传至 Release 的资源
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.APP_SECRET_TOKEN_1 }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # 上传网址,无需改动
|
||||
#asset_path: app/build/outputs/apk/release/app-release.apk # 上传路径(Release)
|
||||
asset_path: app/build/outputs/apk/beta/release/app-beta-release.apk # 上传路径(WinBoll Stage Release)
|
||||
asset_name: WinBoll-${{steps.get-version.outputs.result}}0.apk # 资源名
|
||||
asset_content_type: application/vnd.android.package-archiv # 资源类型
|
||||
|
||||
# 存档打包的文件
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: app/build/outputs # 将打包之后的文件全部上传(里面会有混淆的 map 文件)
|
||||
@@ -1,166 +1,223 @@
|
||||
#!/usr/bin/bash
|
||||
# ==============================================================================
|
||||
# WinBoLL 应用发布脚本
|
||||
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
|
||||
# 依赖:build.properties、app_update_description.txt(项目根目录下)
|
||||
# 使用:./script_name.sh <APP_NAME>
|
||||
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
# ==============================================================================
|
||||
|
||||
# 检查是否指定了将要发布的应用名称
|
||||
# 使用 `-z` 命令检查变量是否为空
|
||||
# ==================== 常量定义 ====================
|
||||
# 脚本退出码
|
||||
EXIT_CODE_SUCCESS=0
|
||||
EXIT_CODE_ERR_NO_APP_NAME=2
|
||||
EXIT_CODE_ERR_WORK_DIR=1
|
||||
EXIT_CODE_ERR_GIT_CHECK=1
|
||||
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
|
||||
|
||||
# Gradle 任务(正式发布)
|
||||
GRADLE_TASK_PUBLISH="assembleStageRelease"
|
||||
# Gradle 任务(调试用,注释备用)
|
||||
# GRADLE_TASK_DEBUG="assembleBetaDebug"
|
||||
|
||||
# ==================== 函数定义 ====================
|
||||
# 检查Git源码是否已完全提交(无未提交变更)
|
||||
# 返回值:0=已完全提交,1=存在未提交变更
|
||||
function checkGitSources() {
|
||||
# 配置Git安全目录(解决权限问题)
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
|
||||
# 检查是否有未提交的变更
|
||||
if [[ -n $(git diff --stat) ]]; then
|
||||
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[INFO] Git源码检查通过:所有变更已提交。"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||
# 返回值:1=用户选择是,0=用户选择否
|
||||
function askAddWorkflowsTag() {
|
||||
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
|
||||
if [[ $answer =~ ^[Yy]$ ]]; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 添加WinBoLL正式标签
|
||||
# 参数:$1=应用名称(项目根目录名)
|
||||
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||
function addWinBoLLTag() {
|
||||
local app_name=$1
|
||||
local build_prop_path="${app_name}/build.properties"
|
||||
|
||||
# 从build.properties中提取publishVersion
|
||||
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||
if [[ -z ${publish_version} ]]; then
|
||||
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
|
||||
return 1
|
||||
fi
|
||||
echo "[INFO] 从${build_prop_path}读取到publishVersion:${publish_version}"
|
||||
|
||||
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>)
|
||||
local tag="${app_name}-v${publish_version}"
|
||||
echo "[INFO] 准备添加WinBoLL标签:${tag}"
|
||||
|
||||
# 检查标签是否已存在
|
||||
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||
echo "[ERROR] WinBoLL标签${tag}已存在!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||
echo "[INFO] WinBoLL标签${tag}添加成功!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
|
||||
# 参数:$1=应用名称(项目根目录名)
|
||||
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||
function addWorkflowsTag() {
|
||||
local app_name=$1
|
||||
local build_prop_path="${app_name}/build.properties"
|
||||
|
||||
# 从build.properties中提取baseBetaVersion
|
||||
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||
if [[ -z ${base_beta_version} ]]; then
|
||||
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
|
||||
return 1
|
||||
fi
|
||||
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion:${base_beta_version}"
|
||||
|
||||
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta)
|
||||
local tag="${app_name}-v${base_beta_version}-beta"
|
||||
echo "[INFO] 准备添加Workflows标签:${tag}"
|
||||
|
||||
# 检查标签是否已存在
|
||||
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||
echo "[ERROR] Workflows标签${tag}已存在!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||
echo "[INFO] Workflows标签${tag}添加成功!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==================== 主流程开始 ====================
|
||||
echo "============================================="
|
||||
echo " WinBoLL 应用发布脚本"
|
||||
echo "============================================="
|
||||
|
||||
# 1. 检查应用名称参数是否指定
|
||||
if [ -z "$1" ]; then
|
||||
echo "No APP name specified : $0"
|
||||
exit 2
|
||||
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
|
||||
exit ${EXIT_CODE_ERR_NO_APP_NAME}
|
||||
fi
|
||||
APP_NAME=$1
|
||||
echo "[INFO] 待发布应用名称:${APP_NAME}"
|
||||
|
||||
## 定义相关函数
|
||||
## 检查 Git 源码是否完全提交了,完全提交就返回0
|
||||
function checkGitSources {
|
||||
#local input="$1"
|
||||
#echo "The string is: $input"
|
||||
git config --global --add safe.directory `pwd`
|
||||
if [[ -n $(git diff --stat) ]]
|
||||
then
|
||||
local result="Source is no commit completely."
|
||||
echo $result
|
||||
# 脚本调试时使用
|
||||
#return 0
|
||||
# 正式检查源码时使用
|
||||
return 1
|
||||
fi
|
||||
local result="Git Source Check OK."
|
||||
echo $result
|
||||
return 0
|
||||
}
|
||||
|
||||
function askAddWorkflowsTag {
|
||||
read answer
|
||||
if [[ $answer =~ ^[Yy]$ ]]; then
|
||||
#echo "You chose yes."
|
||||
return 1
|
||||
else
|
||||
#echo "You chose no."
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
function addWinBoLLTag {
|
||||
# 就读取脚本 .winboll/winboll_app_build.gradle 生成的 publishVersion。
|
||||
# 如果文件中有 publishVersion 这一项,
|
||||
# 使用grep找到包含"publishVersion="的那一行,然后用awk提取其后的值
|
||||
PUBLISH_VERSION=$(grep -o "publishVersion=.*" $1/build.properties | awk -F '=' '{print $2}')
|
||||
echo "< $1/build.properties publishVersion : ${PUBLISH_VERSION} >"
|
||||
## 设新的 WinBoLL 标签
|
||||
# 脚本调试时使用
|
||||
#tag="projectname-v7.6.4-test1"
|
||||
# 正式设置标签时使用
|
||||
tag=$1"-v"${PUBLISH_VERSION}
|
||||
echo "< WinBoLL Tag To: $tag >";
|
||||
# 检查是否已经添加了 WinBoLL Tag
|
||||
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
|
||||
echo -e "< WinBoLL Tag ${tag} exist! >"
|
||||
return 1 # WinBoLL标签重复
|
||||
fi
|
||||
# 添加WinBoLL标签
|
||||
git tag -a ${tag} -F $1/app_update_description.txt
|
||||
return 0
|
||||
}
|
||||
|
||||
function addWorkflowsTag {
|
||||
# 就读取脚本 .winboll/winboll_app_build.gradle 生成的 baseBetaVersion。
|
||||
# 如果文件中有 baseBetaVersion 这一项,
|
||||
# 使用grep找到包含"baseBetaVersion="的那一行,然后用awk提取其后的值
|
||||
BASE_BETA_VERSION=$(grep -o "baseBetaVersion=.*" $1/build.properties | awk -F '=' '{print $2}')
|
||||
echo "< $1/build.properties baseBetaVersion : ${BASE_BETA_VERSION} >"
|
||||
## 设新的 workflows 标签
|
||||
# 脚本调试时使用
|
||||
#tag="projectname-v7.6.4-beta"
|
||||
# 正式设置标签时使用
|
||||
tag=$1"-v"${BASE_BETA_VERSION}-beta
|
||||
echo "< Workflows Tag To: $tag >";
|
||||
# 检查是否已经添加了工作流 Tag
|
||||
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
|
||||
echo -e "< Github Workflows Tag ${tag} exist! >"
|
||||
return 1 # 工作流标签重复
|
||||
fi
|
||||
# 添加工作流标签
|
||||
git tag -a ${tag} -F $1/app_update_description.txt
|
||||
return 0
|
||||
}
|
||||
|
||||
## 开始执行脚本
|
||||
echo -e "Current dir : \n"`pwd`
|
||||
# 检查当前目录是否是项目根目录
|
||||
if [[ -e $1/build.properties ]]; then
|
||||
echo "The $1/build.properties file exists."
|
||||
echo -e "Work dir correctly."
|
||||
else
|
||||
echo "The $1/build.properties file does not exist."
|
||||
echo "尝试进入根目录"
|
||||
# 进入项目根目录
|
||||
# 2. 检查并切换到项目根目录(确保build.properties存在)
|
||||
echo "[INFO] 当前工作目录:$(pwd)"
|
||||
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
|
||||
cd ..
|
||||
echo "[INFO] 切换后工作目录:$(pwd)"
|
||||
fi
|
||||
## 本脚本需要在项目根目录下执行
|
||||
echo -e "Current dir : \n"`pwd`
|
||||
# 检查当前目录是否是项目根目录
|
||||
if [[ -e $1/build.properties ]]; then
|
||||
echo "The $1/build.properties file exists."
|
||||
echo -e "Work dir correctly."
|
||||
|
||||
# 验证最终工作目录是否正确
|
||||
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
|
||||
exit ${EXIT_CODE_ERR_WORK_DIR}
|
||||
fi
|
||||
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
|
||||
|
||||
# 3. 检查Git源码状态
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤1:检查Git源码状态"
|
||||
echo "---------------------------------------------"
|
||||
checkGitSources
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] Git源码检查失败,脚本终止!"
|
||||
exit ${EXIT_CODE_ERR_GIT_CHECK}
|
||||
fi
|
||||
|
||||
# 4. 编译Stage Release版本APK
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤2:编译Stage Release APK"
|
||||
echo "---------------------------------------------"
|
||||
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
|
||||
# 调试用(注释正式任务,启用调试任务)
|
||||
# bash gradlew :${APP_NAME}:${GRADLE_TASK_DEBUG}
|
||||
bash gradlew :${APP_NAME}:${GRADLE_TASK_PUBLISH}
|
||||
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] Gradle编译任务失败!"
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] Stage Release APK编译成功!"
|
||||
|
||||
# 5. 添加WinBoLL正式标签
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤3:添加WinBoLL标签"
|
||||
echo "---------------------------------------------"
|
||||
addWinBoLLTag ${APP_NAME}
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
|
||||
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
|
||||
fi
|
||||
|
||||
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||
# echo "---------------------------------------------"
|
||||
# echo " 步骤4:添加Workflows标签(可选)"
|
||||
# echo "---------------------------------------------"
|
||||
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
|
||||
# askAddWorkflowsTag
|
||||
# nAskAddWorkflowsTag=$?
|
||||
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
|
||||
# addWorkflowsTag ${APP_NAME}
|
||||
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
|
||||
# exit 1
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# 7. 清理更新描述文件
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤5:清理更新描述文件"
|
||||
echo "---------------------------------------------"
|
||||
echo "" > "${APP_NAME}/app_update_description.txt"
|
||||
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
|
||||
|
||||
# 8. 提交并推送源码与标签
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤6:提交并推送源码"
|
||||
echo "---------------------------------------------"
|
||||
git add .
|
||||
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
|
||||
echo "[INFO] 源码提交成功,开始推送..."
|
||||
|
||||
# 推送源码到远程仓库
|
||||
git push origin
|
||||
# 推送标签到远程仓库
|
||||
git push origin --tags
|
||||
|
||||
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[INFO] 源码与标签推送成功!"
|
||||
else
|
||||
echo "The $1/build.properties file does not exist."
|
||||
echo -e "Work dir error."
|
||||
echo "[ERROR] 源码与标签推送失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查源码状态
|
||||
result=$(checkGitSources)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo $result
|
||||
# 如果Git已经提交了所有代码就执行标签和应用发布操作
|
||||
# ==================== 主流程结束 ====================
|
||||
echo "============================================="
|
||||
echo " WinBoLL 应用发布完成!"
|
||||
echo "============================================="
|
||||
exit ${EXIT_CODE_SUCCESS}
|
||||
|
||||
# 预先询问是否添加工作流标签
|
||||
#echo "Add Github Workflows Tag? (yes/No)"
|
||||
#result=$(askAddWorkflowsTag)
|
||||
#nAskAddWorkflowsTag=$?
|
||||
#echo $result
|
||||
|
||||
# 发布应用
|
||||
echo "Publishing WinBoLL APK ..."
|
||||
# 脚本调试时使用
|
||||
#bash gradlew :$1:assembleBetaDebug
|
||||
# 正式发布
|
||||
bash gradlew :$1:assembleStageRelease
|
||||
echo "Publishing WinBoLL APK OK."
|
||||
|
||||
# 添加 WinBoLL 标签
|
||||
result=$(addWinBoLLTag $1)
|
||||
echo $result
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo $result
|
||||
# WinBoLL 标签添加成功
|
||||
else
|
||||
echo -e "${0}: addWinBoLLTag $1\n${result}\nAdd WinBoLL tag cancel."
|
||||
exit 1 # addWinBoLLTag 异常
|
||||
fi
|
||||
|
||||
# 添加 GitHub 工作流标签
|
||||
#if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
|
||||
# 如果用户选择添加工作流标签
|
||||
#result=$(addWorkflowsTag $1)
|
||||
#if [[ $? -eq 0 ]]; then
|
||||
# echo $result
|
||||
# 工作流标签添加成功
|
||||
#else
|
||||
#echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
|
||||
#exit 1 # addWorkflowsTag 异常
|
||||
#fi
|
||||
#fi
|
||||
|
||||
## 清理更新描述文件内容
|
||||
echo "" > $1/app_update_description.txt
|
||||
|
||||
# 设置新版本开发参数配置
|
||||
# 提交配置
|
||||
git add .
|
||||
git commit -m "<$1>Start New Stage Version."
|
||||
echo "Push sources to git repositories ..."
|
||||
# 推送源码到所有仓库
|
||||
git push origin && git push origin --tags
|
||||
else
|
||||
echo -e "${0}: checkGitSources\n${result}\nShell cancel."
|
||||
exit 1 # checkGitSources 异常
|
||||
fi
|
||||
|
||||
@@ -64,6 +64,11 @@ android {
|
||||
dimension "WinBoLLApp"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
// 应用包输出配置
|
||||
//
|
||||
|
||||
17
README.md
17
README.md
@@ -5,10 +5,11 @@
|
||||
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/APPBase> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/appbase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
|
||||
## WinBoLL 提问
|
||||
同样是 /sdcard 目录,在开发 Android 应用时,
|
||||
能否实现手机编译与电脑编译的源码同步。
|
||||
@@ -154,3 +155,11 @@ $ bash gradlew assembleBetaDebug
|
||||
$ bash gradlew assembleStageDebug
|
||||
|
||||
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
|
||||
# 应用版本号命名方式
|
||||
## statge 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
||||
如:APPBase_15.7.0
|
||||
## beta 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
||||
如:APPBase_15.9.6-beta8_5413
|
||||
|
||||
10
build.gradle
10
build.gradle
@@ -26,6 +26,7 @@ buildscript {
|
||||
//mavenLocal()
|
||||
}
|
||||
dependencies {
|
||||
// 适配MIUI12
|
||||
classpath 'com.android.tools.build:gradle:7.2.1' // 对应 compileSdkVersion 32
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@@ -100,12 +101,15 @@ allprojects {
|
||||
}
|
||||
|
||||
subprojects {
|
||||
// 1. 对纯 Java 模块的 JavaCompile 任务配置(升级为 Java 11)
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-parameters"
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
// 可选:确保编码一致
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
@@ -29,11 +29,11 @@ android {
|
||||
applicationId "cc.winboll.studio.powerbell"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 7
|
||||
versionCode 6
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.14"
|
||||
versionName "15.11"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
@@ -56,12 +56,7 @@ dependencies {
|
||||
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
|
||||
@@ -82,13 +77,8 @@ dependencies {
|
||||
//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.12.0'
|
||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
|
||||
implementation 'cc.winboll.studio:libaes:15.11.6'
|
||||
implementation 'cc.winboll.studio:libappbase:15.11.0'
|
||||
|
||||
//api fileTree(dir: 'libs', include: ['*.aar'])
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Dec 13 13:05:44 GMT 2025
|
||||
stageCount=0
|
||||
#Wed Nov 26 16:27:33 HKT 2025
|
||||
stageCount=9
|
||||
libraryProject=
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.0
|
||||
buildCount=1
|
||||
baseBetaVersion=15.14.1
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.8
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.9
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.powerbell">
|
||||
|
||||
<!-- 只能在前台获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<!-- 只有在前台运行时才能获取大致位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<!-- 拍摄照片和视频 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
@@ -137,14 +143,14 @@
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ClearRecordActivity"
|
||||
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.BackgroundSettingsActivity"
|
||||
android:name="cc.winboll.studio.powerbell.activities.BackgroundPictureActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
@@ -184,14 +190,14 @@
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".services.ControlCenterService"
|
||||
android:name="cc.winboll.studio.powerbell.services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".controlcenterservice"/>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:name="cc.winboll.studio.powerbell.services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".assistantservice"/>
|
||||
@@ -200,13 +206,15 @@
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".activities.BatteryReporterActivity"/>
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/>
|
||||
|
||||
<activity android:name=".activities.PixelPickerActivity"/>
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
|
||||
|
||||
<activity android:name=".activities.BatteryReportActivity"/>
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
|
||||
|
||||
<activity android:name=".unittest.MainUnitTestActivity"/>
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -220,17 +228,8 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<activity android:name=".activities.ShortcutActionActivity"/>
|
||||
|
||||
<activity android:name=".activities.SettingsActivity"/>
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 100 B |
@@ -2,37 +2,28 @@ package cc.winboll.studio.powerbell;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import android.view.Gravity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
|
||||
import cc.winboll.studio.powerbell.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.PermissionUtils;
|
||||
import java.io.File;
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
public static final String TAG = "App";
|
||||
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
|
||||
public static final String TAG = "GlobalApplication";
|
||||
|
||||
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";
|
||||
|
||||
// 数据配置存储工具
|
||||
static AppConfigUtils _mAppConfigUtils;
|
||||
static AppCacheUtils _mAppCacheUtils;
|
||||
// 新增:全局 Bitmap 缓存工具(常驻内存)
|
||||
public static BitmapCacheUtils _mBitmapCacheUtils;
|
||||
|
||||
GlobalApplicationReceiver mReceiver;
|
||||
static String szTempDir = "";
|
||||
|
||||
@@ -43,72 +34,40 @@ public class App extends GlobalApplication {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
// 初始化活动窗口管理
|
||||
WinBoLLActivityManager.init(this);
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
|
||||
// 临时文件夹初始化(保持原有逻辑)
|
||||
// 临时文件夹方案1
|
||||
// 获取Pictures文件夹路径(Android 10及以上推荐使用MediaStore,此处为传统方式)
|
||||
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
// 定义目标文件路径(在Pictures目录下创建"PowerBell"子文件夹及文件)
|
||||
File powerBellDir = new File(picturesDir, "PowerBell");
|
||||
|
||||
// 临时文件夹方案2 <图片保存失败>
|
||||
// 获取Pictures文件夹路径(Android 10及以上推荐使用MediaStore,此处为传统方式)
|
||||
//File powerBellDir = getExternalFilesDir("TempDir");
|
||||
|
||||
// 先创建文件夹(如果不存在)
|
||||
if (!powerBellDir.exists()) {
|
||||
powerBellDir.mkdirs();
|
||||
}
|
||||
szTempDir = powerBellDir.getAbsolutePath();
|
||||
|
||||
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.toast_custom_view);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
// 设置数据配置存储工具
|
||||
_mAppConfigUtils = getAppConfigUtils(this);
|
||||
_mAppCacheUtils = getAppCacheUtils(this);
|
||||
// 初始化全局 Bitmap 缓存工具(关键:App 启动时初始化,常驻内存)
|
||||
_mBitmapCacheUtils = BitmapCacheUtils.getInstance();
|
||||
|
||||
mReceiver = new GlobalApplicationReceiver(this);
|
||||
mReceiver.registerAction();
|
||||
|
||||
// ======================== 新增:异步预加载背景图 ========================
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 1. 获取背景源工具类实例
|
||||
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(App.this);
|
||||
if (bgSourceUtils == null) {
|
||||
LogUtils.e(TAG, "preloadBitmap: BackgroundSourceUtils 实例为空");
|
||||
return;
|
||||
}
|
||||
// 2. 获取当前背景Bean
|
||||
BackgroundBean bgBean = bgSourceUtils.getCurrentBackgroundBean();
|
||||
if (bgBean == null || !bgBean.isUseBackgroundFile()) {
|
||||
LogUtils.d(TAG, "preloadBitmap: 无有效背景文件,跳过预加载");
|
||||
return;
|
||||
}
|
||||
// 3. 获取背景图路径(优先取压缩图路径)
|
||||
String bgPath = bgBean.isUseBackgroundScaledCompressFile()
|
||||
? bgBean.getBackgroundScaledCompressFilePath()
|
||||
: bgBean.getBackgroundFilePath();
|
||||
// 4. 预加载到全局缓存
|
||||
if (_mBitmapCacheUtils != null) {
|
||||
_mBitmapCacheUtils.cacheBitmap(bgPath);
|
||||
LogUtils.d(TAG, "preloadBitmap: 应用启动时预加载成功 - " + bgPath);
|
||||
} else {
|
||||
LogUtils.e(TAG, "preloadBitmap: 全局 BitmapCacheUtils 未初始化");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "preloadBitmap: 预加载失败 - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}, 1000); // 延迟1秒执行,避免阻塞应用初始化
|
||||
// ======================== 预加载逻辑结束 ========================
|
||||
}
|
||||
|
||||
// 保持原有方法不变
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
if (_mAppConfigUtils == null) {
|
||||
_mAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
@@ -127,14 +86,12 @@ public class App extends GlobalApplication {
|
||||
_mAppCacheUtils.clearBatteryHistory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
// 可选:App 终止时清空 Bitmap 缓存,释放内存
|
||||
if (_mBitmapCacheUtils != null) {
|
||||
_mBitmapCacheUtils.clearAllCache();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/25 01:16:32
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libaes.views.AboutView;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
public class AboutActivity extends Activity {
|
||||
|
||||
Context mContext;
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
mContext = this;
|
||||
|
||||
// 初始化工具栏
|
||||
AToolbar mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
setActionBar(mAToolbar);
|
||||
mAToolbar.setSubtitle(getString(R.string.text_about));
|
||||
//mAToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
AboutView aboutView = CreateAboutView();
|
||||
// 在 Activity 的 onCreate 或其他生命周期方法中调用
|
||||
LinearLayout llRoot = findViewById(R.id.root_ll);
|
||||
//layout.setOrientation(LinearLayout.VERTICAL);
|
||||
// 创建布局参数(宽度和高度)
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
llRoot.addView(aboutView, params);
|
||||
|
||||
}
|
||||
|
||||
public AboutView CreateAboutView() {
|
||||
String szBranchName = "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("APPBase");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
|
||||
appInfo.setAppAPKName("PowerBell");
|
||||
appInfo.setAppAPKFolderName("PowerBell");
|
||||
return new AboutView(mContext, appInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.RelativeLayout;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
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.beans.BackgroundPictureBean;
|
||||
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class BackgroundPictureActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
|
||||
|
||||
public static final String TAG = "BackgroundPictureActivity";
|
||||
public BackgroundPictureUtils mBackgroundPictureUtils;
|
||||
|
||||
// 图片选择请求码
|
||||
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 STORAGE_PERMISSION_REQUEST = 100;
|
||||
|
||||
private AToolbar mAToolbar;
|
||||
private File mfBackgroundDir; // 背景图片存储文件夹
|
||||
private File mfPictureDir; // 拍照与剪裁临时文件夹
|
||||
private File mfTakePhoto; // 拍照文件
|
||||
private File mfRecivedPicture; // 接收的图片文件
|
||||
private File mfTempCropPicture; // 剪裁临时文件
|
||||
private File mfRecivedCropPicture; // 剪裁后的目标文件
|
||||
|
||||
private String preViewFileBackgroundView = "";
|
||||
BackgroundView bvPreviewBackground;
|
||||
boolean isCommitSettings = false;
|
||||
|
||||
// 静态变量
|
||||
public static String _mszRecivedCropPicture = "RecivedCrop.jpg";
|
||||
private static String _mszCommonFileType = "jpeg";
|
||||
private int mnPictureCompress = 100;
|
||||
private static String _RecivedPictureFileName;
|
||||
|
||||
@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_backgroundpicture);
|
||||
initEnv();
|
||||
|
||||
// 初始化工具类和文件夹
|
||||
mBackgroundPictureUtils = BackgroundPictureUtils.getInstance(this);
|
||||
mfBackgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir());
|
||||
if (!mfBackgroundDir.exists()) {
|
||||
mfBackgroundDir.mkdirs();
|
||||
}
|
||||
|
||||
mfPictureDir = new File(App.getTempDirPath());
|
||||
if (!mfPictureDir.exists()) {
|
||||
mfPictureDir.mkdirs();
|
||||
}
|
||||
|
||||
// 初始化文件对象
|
||||
mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg");
|
||||
mfTempCropPicture = new File(mfPictureDir, "TempCrop.jpg");
|
||||
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
mfRecivedCropPicture = new File(mfBackgroundDir, _mszRecivedCropPicture);
|
||||
|
||||
// 初始化工具栏
|
||||
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
setActionBar(mAToolbar);
|
||||
mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish(); // 点击导航栏返回按钮,触发 finish()
|
||||
}
|
||||
});
|
||||
|
||||
// 设置按钮点击事件
|
||||
findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
|
||||
|
||||
updatePreviewBackground();
|
||||
|
||||
// 处理分享的图片
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
|
||||
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this);
|
||||
dlg.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void initEnv() {
|
||||
LogUtils.d(TAG, "initEnv()");
|
||||
_RecivedPictureFileName = "Recived.data";
|
||||
}
|
||||
|
||||
public static String getBackgroundFileName() {
|
||||
return _mszRecivedCropPicture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcceptRecivedPicture(String szPreRecivedPictureName) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
|
||||
utils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
|
||||
utils.saveData();
|
||||
|
||||
File sourceFile = new File(utils.getBackgroundDir(), szPreRecivedPictureName);
|
||||
if (FileUtils.copyFile(sourceFile, mfRecivedPicture)) {
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("图片复制失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新背景图片预览
|
||||
*/
|
||||
public void updatePreviewBackground() {
|
||||
LogUtils.d(TAG, "updatePreviewBackground");
|
||||
//ImageView ivPreviewBackground = (ImageView) findViewById(R.id.activitybackgroundpictureImageView1);
|
||||
bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1);
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
|
||||
utils.loadBackgroundPictureBean();
|
||||
|
||||
boolean isUseBackgroundFile = utils.getBackgroundPictureBean().isUseBackgroundFile();
|
||||
if (isUseBackgroundFile && mfRecivedCropPicture.exists()) {
|
||||
//try {
|
||||
String filePath = utils.getBackgroundDir() + getBackgroundFileName();
|
||||
preViewFileBackgroundView = filePath;
|
||||
bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView);
|
||||
/*Drawable drawable = FileUtils.getImageDrawable(filePath);
|
||||
if (drawable != null) {
|
||||
//drawable.setAlpha(120);
|
||||
//bvPreviewBackground.setImageDrawable(drawable);
|
||||
}*/
|
||||
//ToastUtils.show("背景图片已更新");
|
||||
// } catch (IOException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// ToastUtils.show("背景图片加载失败");
|
||||
// }
|
||||
} else {
|
||||
ToastUtils.show("未使用背景图片");
|
||||
preViewFileBackgroundView = "";
|
||||
bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView);
|
||||
// Drawable drawable = getResources().getDrawable(R.drawable.blank10x10);
|
||||
// if (drawable != null) {
|
||||
// drawable.setAlpha(120);
|
||||
// bvPreviewBackground.setImageDrawable(drawable);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 点击事件监听器
|
||||
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
bean.setIsUseBackgroundFile(false);
|
||||
utils.saveData();
|
||||
updatePreviewBackground();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (checkAndRequestStoragePermission()) {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
startActivityForResult(intent, REQUEST_SELECT_PICTURE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
File fCheck = new File(mfBackgroundDir, getBackgroundFileName());
|
||||
if (fCheck.exists()) {
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("没有可剪裁的图片");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
File fCheck = new File(mfBackgroundDir, getBackgroundFileName());
|
||||
if (fCheck.exists()) {
|
||||
startCropImageActivity(true);
|
||||
} else {
|
||||
ToastUtils.show("没有可剪裁的图片");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onTakePhotoClickListener");
|
||||
LogUtils.d(TAG, "mfTakePhoto : " + mfTakePhoto.getPath());
|
||||
|
||||
if (mfTakePhoto.exists()) {
|
||||
mfTakePhoto.delete();
|
||||
}
|
||||
try {
|
||||
mfTakePhoto.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkAndRequestStoragePermission()) {
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
utils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
|
||||
utils.saveData();
|
||||
updatePreviewBackground();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 从文件路径启动像素拾取活动
|
||||
//String imagePath = "/storage/emulated/0/DCIM/Camera/sample.jpg";
|
||||
String imagePath = mfRecivedCropPicture.toString();
|
||||
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
|
||||
intent.putExtra("imagePath", imagePath);
|
||||
startActivity(intent);
|
||||
//App.getWinBoLLActivityManager().startWinBoLLActivity(getActivity(), intent, PixelPickerActivity.class);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
bean.setPixelColor(0);
|
||||
utils.saveData();
|
||||
setBackgroundColor();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 压缩图片并保存到接收文件
|
||||
*/
|
||||
void compressQualityToRecivedPicture(Bitmap bitmap) {
|
||||
OutputStream outStream = null;
|
||||
try {
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
if (!mfRecivedPicture.exists()) {
|
||||
mfRecivedPicture.createNewFile();
|
||||
}
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(mfRecivedPicture);
|
||||
outStream = new BufferedOutputStream(fos);
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
|
||||
outStream.flush();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
ToastUtils.show("图片压缩失败");
|
||||
} finally {
|
||||
if (outStream != null) {
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片裁剪活动
|
||||
* @param isCropFree 是否自由裁剪
|
||||
*/
|
||||
public void startCropImageActivity(boolean isCropFree) {
|
||||
LogUtils.d(TAG, "startCropImageActivity");
|
||||
BackgroundPictureBean bean = mBackgroundPictureUtils.loadBackgroundPictureBean();
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture);
|
||||
LogUtils.d(TAG, "uri : " + uri.toString());
|
||||
|
||||
if (mfTempCropPicture.exists()) {
|
||||
mfTempCropPicture.delete();
|
||||
}
|
||||
try {
|
||||
mfTempCropPicture.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
ToastUtils.show("剪裁临时文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Uri cropOutPutUri = Uri.fromFile(mfTempCropPicture);
|
||||
LogUtils.d(TAG, "mfTempCropPicture : " + mfTempCropPicture.getPath());
|
||||
|
||||
Intent intent = new Intent("com.android.camera.action.CROP");
|
||||
intent.setDataAndType(uri, "image/" + _mszCommonFileType);
|
||||
intent.putExtra("crop", "true");
|
||||
intent.putExtra("noFaceDetection", true);
|
||||
|
||||
if (!isCropFree) {
|
||||
intent.putExtra("aspectX", bean.getBackgroundWidth());
|
||||
intent.putExtra("aspectY", bean.getBackgroundHeight());
|
||||
}
|
||||
|
||||
intent.putExtra("return-data", true);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri);
|
||||
intent.putExtra("scale", true);
|
||||
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
startActivityForResult(intent, REQUEST_CROP_IMAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存剪裁后的Bitmap(优化版)
|
||||
*/
|
||||
private void saveCropBitmap(Bitmap bitmap) {
|
||||
if (bitmap == null) {
|
||||
ToastUtils.show("剪裁图片为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 内存优化:大图片自动缩放
|
||||
Bitmap scaledBitmap = bitmap;
|
||||
if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB
|
||||
float scale = 1.0f;
|
||||
while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) {
|
||||
scale -= 0.2f; // 每次缩小20%
|
||||
if (scale < 0.2f) break; // 最小缩放到20%
|
||||
scaledBitmap = scaleBitmap(scaledBitmap, scale);
|
||||
}
|
||||
if (scaledBitmap != bitmap) {
|
||||
bitmap.recycle(); // 回收原Bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// 优化:创建保存目录
|
||||
File backgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir());
|
||||
if (!backgroundDir.exists()) {
|
||||
if (!backgroundDir.mkdirs()) {
|
||||
ToastUtils.show("无法创建保存目录");
|
||||
if (scaledBitmap != bitmap) scaledBitmap.recycle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File saveFile = new File(backgroundDir, getBackgroundFileName());
|
||||
|
||||
// 优化:检查文件是否可写
|
||||
if (saveFile.exists() && !saveFile.canWrite()) {
|
||||
if (!saveFile.delete()) {
|
||||
ToastUtils.show("无法删除旧文件");
|
||||
if (scaledBitmap != bitmap) scaledBitmap.recycle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(saveFile);
|
||||
boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
|
||||
fos.flush();
|
||||
if (success) {
|
||||
ToastUtils.show("保存成功");
|
||||
// 更新数据
|
||||
mBackgroundPictureUtils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
|
||||
updatePreviewBackground();
|
||||
} else {
|
||||
ToastUtils.show("图片压缩保存失败");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
LogUtils.e(TAG, "文件未找到" + e);
|
||||
ToastUtils.show("保存失败:文件路径错误");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "写入异常" + e);
|
||||
ToastUtils.show("保存失败:磁盘可能已满或路径错误");
|
||||
} finally {
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "流关闭异常" + e);
|
||||
}
|
||||
}
|
||||
if (scaledBitmap != null && !scaledBitmap.isRecycled()) {
|
||||
scaledBitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放Bitmap
|
||||
*/
|
||||
private Bitmap scaleBitmap(Bitmap original, float scale) {
|
||||
if (original == null) {
|
||||
return null;
|
||||
}
|
||||
int width = (int) (original.getWidth() * scale);
|
||||
int height = (int) (original.getHeight() * scale);
|
||||
return Bitmap.createScaledBitmap(original, width, height, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享图片
|
||||
*/
|
||||
void sharePicture() {
|
||||
Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture);
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
shareIntent.setType("image/" + _mszCommonFileType);
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivity(Intent.createChooser(shareIntent, "Share Image"));
|
||||
}
|
||||
|
||||
public static File getRecivedPictureFile(Context context) {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(context);
|
||||
utils.loadBackgroundPictureBean();
|
||||
return new File(utils.getBackgroundDir(), _RecivedPictureFileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) {
|
||||
try {
|
||||
Uri selectedImage = data.getData();
|
||||
LogUtils.d(TAG, "Uri is : " + selectedImage.toString());
|
||||
File fSrcImage = new File(UriUtil.getFilePathFromUri(this, selectedImage));
|
||||
mfRecivedPicture = getRecivedPictureFile(this);
|
||||
if (FileUtils.copyFile(fSrcImage, mfRecivedPicture)) {
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("图片复制失败,请重试");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "选择图片异常" + e);
|
||||
ToastUtils.show("选择图片失败:" + e.getMessage());
|
||||
}
|
||||
} else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) {
|
||||
LogUtils.d(TAG, "REQUEST_TAKE_PHOTO");
|
||||
Bundle extras = data.getExtras();
|
||||
if (extras != null) {
|
||||
Bitmap imageBitmap = (Bitmap) extras.get("data");
|
||||
if (imageBitmap != null) {
|
||||
compressQualityToRecivedPicture(imageBitmap);
|
||||
startCropImageActivity(false);
|
||||
} else {
|
||||
ToastUtils.show("拍照图片为空");
|
||||
}
|
||||
} else {
|
||||
ToastUtils.show("拍照数据获取失败");
|
||||
}
|
||||
} else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) {
|
||||
LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE");
|
||||
try {
|
||||
Bitmap cropBitmap = null;
|
||||
// 方案1:通过Intent获取剪裁后的Bitmap
|
||||
if (data != null && data.hasExtra("data")) {
|
||||
cropBitmap = data.getParcelableExtra("data");
|
||||
} else if (mfTempCropPicture.exists()) {
|
||||
cropBitmap = BitmapFactory.decodeFile(mfTempCropPicture.getPath());
|
||||
} else {
|
||||
ToastUtils.show("剪裁文件不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cropBitmap != null) {
|
||||
saveCropBitmap(cropBitmap);
|
||||
} else {
|
||||
ToastUtils.show("获取剪裁图片失败");
|
||||
}
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "内存溢出" + e);
|
||||
ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "剪裁保存异常" + e);
|
||||
ToastUtils.show("保存失败:" + e.getMessage());
|
||||
}/* finally {
|
||||
// 安全删除临时文件
|
||||
if (mfTempCropPicture.exists()) {
|
||||
mfTempCropPicture.delete();
|
||||
}
|
||||
}*/
|
||||
} else if (resultCode != RESULT_OK) {
|
||||
LogUtils.d(TAG, "操作取消或失败,requestCode: " + requestCode);
|
||||
ToastUtils.show("操作已取消");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类型是否为图片
|
||||
*/
|
||||
private boolean isImageType(String type) {
|
||||
return type.startsWith("image/") || "image/jpeg".equals(type) ||
|
||||
"image/jpg".equals(type) || "image/png".equals(type) ||
|
||||
"image/webp".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并申请存储权限
|
||||
*/
|
||||
private boolean checkAndRequestStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
STORAGE_PERMISSION_REQUEST);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == STORAGE_PERMISSION_REQUEST) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
ToastUtils.show("存储权限已获取");
|
||||
} else {
|
||||
ToastUtils.show("需要存储权限才能保存图片");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setBackgroundColor() {
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
int nPixelColor = bean.getPixelColor();
|
||||
RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1);
|
||||
mainLayout.setBackgroundColor(nPixelColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
setBackgroundColor();
|
||||
}
|
||||
|
||||
public void onNetworkBackgroundDialog(View view) {
|
||||
// 在需要显示对话框的地方(如网络状态监听回调中)
|
||||
NetworkBackgroundDialog dialog = new NetworkBackgroundDialog(this, new NetworkBackgroundDialog.OnDialogClickListener() {
|
||||
@Override
|
||||
public void onConfirm() {
|
||||
ToastUtils.show("onConfirm");
|
||||
// 处理确认逻辑(如允许后台网络使用)
|
||||
LogUtils.d("MainActivity", "用户允许后台网络使用");
|
||||
// 执行具体业务:如开启后台网络请求服务
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
ToastUtils.show("onCancel");
|
||||
// 处理取消逻辑(如禁止后台网络使用)
|
||||
LogUtils.d("MainActivity", "用户禁止后台网络使用");
|
||||
// 执行具体业务:如关闭后台网络请求
|
||||
}
|
||||
});
|
||||
|
||||
// 可选:修改对话框标题和内容(适配自定义场景)
|
||||
dialog.setTitle("网络图片下载对话框");
|
||||
dialog.setContent("是否下载地址中的图片资源,作为应用背景图片?");
|
||||
|
||||
// 显示对话框
|
||||
dialog.show();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写finish方法,确保所有退出场景都触发Toast
|
||||
*/
|
||||
@Override
|
||||
public void finish() {
|
||||
if (!isCommitSettings) {
|
||||
YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
bvPreviewBackground.saveToBackgroundSources(preViewFileBackgroundView);
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
super.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,885 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.FileProvider;
|
||||
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.dialogs.BackgroundPicturePreviewDialog;
|
||||
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
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.PermissionUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
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_READ_MEDIA = 1001;
|
||||
|
||||
// ====================== 成员变量 ======================
|
||||
private BackgroundSourceUtils mBgSourceUtils;
|
||||
private PermissionUtils mPermissionUtils;
|
||||
private BitmapCacheUtils mBitmapCache;
|
||||
|
||||
private Toolbar mToolbar;
|
||||
private BackgroundView mBackgroundView;
|
||||
private File mfTakePhoto;
|
||||
volatile boolean isCommitSettings = false;
|
||||
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);
|
||||
setContentView(R.layout.activity_background_settings);
|
||||
LogUtils.d(TAG, "【生命周期】onCreate 开始初始化");
|
||||
|
||||
// 初始化视图与工具类
|
||||
mBackgroundView = findViewById(R.id.background_view);
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
mBgSourceUtils.loadSettings();
|
||||
mPermissionUtils = PermissionUtils.getInstance();
|
||||
mBitmapCache = BitmapCacheUtils.getInstance();
|
||||
|
||||
// 初始化临时文件与目录
|
||||
File tempDir = new File(App.getTempDirPath());
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs();
|
||||
}
|
||||
mfTakePhoto = new File(tempDir, "TakePhoto.jpg");
|
||||
|
||||
File selectTempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp");
|
||||
if (!selectTempDir.exists()) {
|
||||
selectTempDir.mkdirs();
|
||||
LogUtils.d(TAG, "【目录初始化】选图临时目录创建完成:" + selectTempDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 初始化界面与事件
|
||||
initToolbar();
|
||||
initClickListeners();
|
||||
|
||||
// 处理分享意图或初始化预览
|
||||
if (handleShareIntent()) {
|
||||
ToastUtils.show("handleShareIntent");
|
||||
} else {
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
}
|
||||
|
||||
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(mBgSourceUtils.getPreviewBackgroundBean());
|
||||
doubleRefreshPreview();
|
||||
LogUtils.d(TAG, "【生命周期】onCreate 初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
doubleRefreshPreview();
|
||||
LogUtils.d(TAG, "【生命周期】onPostCreate 执行双重刷新");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode);
|
||||
|
||||
try {
|
||||
if (requestCode == PermissionUtils.REQUEST_READ_MEDIA_IMAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
handleStoragePermissionCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultCode != RESULT_OK) {
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_SELECT_PICTURE:
|
||||
handleSelectPictureResult(resultCode, data);
|
||||
break;
|
||||
case REQUEST_TAKE_PHOTO:
|
||||
handleTakePhotoResult(resultCode, data);
|
||||
break;
|
||||
case REQUEST_CROP_IMAGE:
|
||||
handleCropImageResult(requestCode, resultCode, data);
|
||||
break;
|
||||
default:
|
||||
LogUtils.d(TAG, "【回调忽略】未知requestCode");
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【回调异常】" + e.getMessage());
|
||||
ToastUtils.show("操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
LogUtils.d(TAG, "【权限回调】转发处理 requestCode:" + requestCode);
|
||||
mPermissionUtils.handleStoragePermissionResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
LogUtils.d(TAG, "【生命周期】finish 触发,isCommitSettings:" + isCommitSettings + ",isPreviewBackgroundChanged:" + isPreviewBackgroundChanged);
|
||||
if (isCommitSettings) {
|
||||
setResult(RESULT_OK);
|
||||
super.finish();
|
||||
} else {
|
||||
if (isPreviewBackgroundChanged) {
|
||||
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onYes() {
|
||||
mBgSourceUtils.commitPreviewSourceToCurrent();
|
||||
isCommitSettings = true;
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
isCommitSettings = true;
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setResult(RESULT_OK);
|
||||
isCommitSettings = true;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 界面初始化方法 ======================
|
||||
private void 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, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initClickListeners() {
|
||||
LogUtils.d(TAG, "【界面初始化】绑定按钮点击事件");
|
||||
findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
|
||||
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
|
||||
}
|
||||
|
||||
// ====================== 按钮点击事件 ======================
|
||||
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】取消背景图片");
|
||||
BackgroundBean previewBackgroundBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
previewBackgroundBean.setIsUseBackgroundFile(false);
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】选择图片");
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
if (mPermissionUtils.checkAndRequestMediaImagesPermission(BackgroundSettingsActivity.this, REQUEST_READ_MEDIA)) {
|
||||
launchImageSelector();
|
||||
}
|
||||
} else {
|
||||
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
|
||||
launchImageSelector();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】固定比例裁剪");
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
mBgSourceUtils.getPreviewBackgroundBean(),
|
||||
mBackgroundView.getWidth(),
|
||||
mBackgroundView.getHeight(),
|
||||
false,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】自由裁剪");
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
mBgSourceUtils.getPreviewBackgroundBean(),
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】拍照");
|
||||
// 移除:旧文件删除逻辑
|
||||
try {
|
||||
boolean createSuccess = mfTakePhoto.createNewFile();
|
||||
LogUtils.d(TAG, "【拍照准备】创建新文件:" + (createSuccess ? "成功" : "失败"));
|
||||
if (!createSuccess) {
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【拍照异常】" + e.getMessage());
|
||||
ToastUtils.show("拍照文件创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
|
||||
LogUtils.d(TAG, "【拍照权限】已获取");
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
try {
|
||||
Uri photoUri = getFileProviderUri(mfTakePhoto);
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
|
||||
LogUtils.d(TAG, "【拍照启动】Uri:" + photoUri.toString());
|
||||
} catch (Exception e) {
|
||||
String errMsg = "拍照启动异常:" + e.getMessage();
|
||||
ToastUtils.show(errMsg.substring(0, 20));
|
||||
LogUtils.e(TAG, "【拍照失败】" + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "【拍照权限】已申请");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ToastUtils.show("图片接收功能暂未实现");
|
||||
LogUtils.d(TAG, "【按钮点击】图片接收");
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】像素拾取");
|
||||
String targetImagePath = mBgSourceUtils.getCurrentBackgroundBean().getBackgroundFilePath();
|
||||
File targetFile = new File(targetImagePath);
|
||||
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
|
||||
ToastUtils.show("无有效图片可拾取像素");
|
||||
LogUtils.e(TAG, "【像素拾取失败】文件无效:" + targetImagePath);
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
|
||||
intent.putExtra("imagePath", targetImagePath);
|
||||
startActivity(intent);
|
||||
LogUtils.d(TAG, "【像素拾取启动】路径:" + targetImagePath);
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
|
||||
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
|
||||
int oldColor = bean.getPixelColor();
|
||||
bean.setPixelColor(0);
|
||||
mBgSourceUtils.saveSettings();
|
||||
doubleRefreshPreview();
|
||||
ToastUtils.show("像素颜色已清空");
|
||||
LogUtils.d(TAG, "【像素清空】旧颜色:" + oldColor);
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 工具方法 ======================
|
||||
/**
|
||||
* 生成 FileProvider Uri,适配 Android 7.0+
|
||||
*/
|
||||
public Uri getFileProviderUri(File file) {
|
||||
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, "getFileProviderUri: 生成Uri失败:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Bitmap 是否有效(未被回收且不为空)
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
return bitmap != null && !bitmap.isRecycled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
* 移除:缓存清空逻辑
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
LogUtils.d(TAG, "【工具方法】doubleRefreshPreview 开始执行");
|
||||
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
|
||||
LogUtils.w(TAG, "【双重刷新】跳过:对象为空或Activity已结束");
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一重刷新
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
|
||||
LogUtils.d(TAG, "【双重刷新】第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二重刷新(延迟执行)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析裁剪临时文件为 Bitmap,带采样率优化
|
||||
*/
|
||||
private Bitmap parseCropTempFileToBitmap(File cropTempFile) {
|
||||
LogUtils.d(TAG, "【工具方法】parseCropTempFileToBitmap 解析文件:" + (cropTempFile != null ? cropTempFile.getAbsolutePath() : "null"));
|
||||
if (cropTempFile == null || !cropTempFile.exists() || !cropTempFile.isFile() || cropTempFile.length() <= 100) {
|
||||
LogUtils.e(TAG, "【Bitmap解析】文件无效");
|
||||
return null;
|
||||
}
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options);
|
||||
|
||||
int maxSize = 2048;
|
||||
int sampleSize = 1;
|
||||
while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) {
|
||||
sampleSize *= 2;
|
||||
}
|
||||
sampleSize = Math.min(sampleSize, 16);
|
||||
LogUtils.d(TAG, "【Bitmap解析】采样率:" + sampleSize);
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
options.inPurgeable = true;
|
||||
options.inInputShareable = true;
|
||||
|
||||
try {
|
||||
Bitmap cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options);
|
||||
if (!isBitmapValid(cropBitmap)) {
|
||||
LogUtils.e(TAG, "【Bitmap解析】解析失败");
|
||||
return null;
|
||||
}
|
||||
LogUtils.d(TAG, "【Bitmap解析】成功,尺寸:" + cropBitmap.getWidth() + "x" + cropBitmap.getHeight());
|
||||
return cropBitmap;
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "【Bitmap解析】OOM异常");
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Bitmap解析】异常:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整 Bitmap 比例至目标比例
|
||||
*/
|
||||
private Bitmap adjustBitmapToFinalRatio(Bitmap originalBitmap, float finalCropRatio) {
|
||||
LogUtils.d(TAG, "【工具方法】adjustBitmapToFinalRatio 调整比例,目标比例:" + finalCropRatio);
|
||||
if (!isBitmapValid(originalBitmap) || finalCropRatio <= 0) {
|
||||
LogUtils.e(TAG, "【比例调整】参数无效");
|
||||
return null;
|
||||
}
|
||||
|
||||
int originalWidth = originalBitmap.getWidth();
|
||||
int originalHeight = originalBitmap.getHeight();
|
||||
float originalRatio = (float) originalWidth / originalHeight;
|
||||
|
||||
if (Math.abs(originalRatio - finalCropRatio) < 0.001f) {
|
||||
LogUtils.d(TAG, "【比例调整】比例一致,生成副本");
|
||||
return originalBitmap.copy(originalBitmap.getConfig(), false);
|
||||
}
|
||||
|
||||
int targetWidth, targetHeight;
|
||||
targetHeight = originalHeight;
|
||||
targetWidth = Math.round(targetHeight * finalCropRatio);
|
||||
if (targetWidth > originalWidth) {
|
||||
targetWidth = originalWidth;
|
||||
targetHeight = Math.round(targetWidth / finalCropRatio);
|
||||
}
|
||||
targetWidth = Math.round(targetHeight * finalCropRatio);
|
||||
LogUtils.d(TAG, "【比例调整】调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight);
|
||||
|
||||
try {
|
||||
Bitmap adjustedBitmap = Bitmap.createBitmap(
|
||||
originalBitmap,
|
||||
(originalWidth - targetWidth) / 2,
|
||||
(originalHeight - targetHeight) / 2,
|
||||
targetWidth,
|
||||
targetHeight
|
||||
);
|
||||
return adjustedBitmap;
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "【比例调整】OOM异常");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Bitmap 到目标文件
|
||||
* 移除:原文件删除逻辑
|
||||
*/
|
||||
private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) {
|
||||
LogUtils.d(TAG, "【工具方法】saveScaledBitmapToFile 保存图片:" + targetFile.getAbsolutePath());
|
||||
if (!isBitmapValid(bitmap) || targetFile == null) {
|
||||
LogUtils.e(TAG, "【图片保存】参数无效");
|
||||
return;
|
||||
}
|
||||
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = new BufferedOutputStream(new FileOutputStream(targetFile));
|
||||
bitmap.compress(CompressFormat.JPEG, 100, outputStream);
|
||||
outputStream.flush();
|
||||
LogUtils.d(TAG, "【图片保存】成功,文件大小:" + targetFile.length() + " bytes");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【图片保存】异常:" + e.getMessage());
|
||||
} finally {
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【图片保存】关闭流异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从裁剪文件中读取比例
|
||||
*/
|
||||
private float getRatioFromSystemCropFile(File systemCropFile) {
|
||||
LogUtils.d(TAG, "【工具方法】getRatioFromSystemCropFile 读取比例:" + systemCropFile.getAbsolutePath());
|
||||
if (systemCropFile == null || !systemCropFile.exists() || !systemCropFile.isFile()) {
|
||||
LogUtils.e(TAG, "【比例读取】文件无效");
|
||||
return -1;
|
||||
}
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(systemCropFile.getAbsolutePath(), options);
|
||||
|
||||
int cropWidth = options.outWidth;
|
||||
int cropHeight = options.outHeight;
|
||||
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||
LogUtils.e(TAG, "【比例读取】尺寸无效");
|
||||
return -1;
|
||||
}
|
||||
|
||||
float systemRatio = (float) cropWidth / cropHeight;
|
||||
LogUtils.d(TAG, "【比例读取】成功,比例:" + systemRatio);
|
||||
return systemRatio;
|
||||
}
|
||||
|
||||
// ====================== 业务逻辑方法 ======================
|
||||
/**
|
||||
* 处理分享意图
|
||||
*/
|
||||
private boolean handleShareIntent() {
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
|
||||
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener(){
|
||||
@Override
|
||||
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
|
||||
ToastUtils.show(String.format("uriRecivedPicture %s", uriRecivedPicture));
|
||||
}
|
||||
});
|
||||
dlg.show();
|
||||
LogUtils.d(TAG, "【分享处理】收到分享图片意图");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片类型
|
||||
*/
|
||||
boolean isImageType(String lowerMimeType) {
|
||||
return lowerMimeType.equals("image/jpeg")
|
||||
|| lowerMimeType.equals("image/png")
|
||||
|| lowerMimeType.equals("image/tiff")
|
||||
|| lowerMimeType.equals("image/jpg")
|
||||
|| lowerMimeType.equals("image/svg+xml");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动图片选择器
|
||||
*/
|
||||
private void launchImageSelector() {
|
||||
LogUtils.d(TAG, "【业务逻辑】launchImageSelector 启动选择器");
|
||||
Intent[] intents = new Intent[3];
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Intent validIntent = null;
|
||||
for (Intent intent : intents) {
|
||||
if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
|
||||
validIntent = intent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (validIntent != null) {
|
||||
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, "【选图意图】启动图片选择");
|
||||
} else {
|
||||
LogUtils.d(TAG, "【选图意图】无相册应用");
|
||||
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) {
|
||||
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
|
||||
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
|
||||
if (marketIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(marketIntent);
|
||||
} else {
|
||||
ToastUtils.show("无法打开应用商店");
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理存储权限回调
|
||||
*/
|
||||
private void handleStoragePermissionCallback() {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
LogUtils.d(TAG, "【权限回调】已授予");
|
||||
ToastUtils.show("存储权限已获取");
|
||||
} else {
|
||||
LogUtils.d(TAG, "【权限回调】已拒绝");
|
||||
ToastUtils.show("存储权限不足");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理操作取消或失败
|
||||
*/
|
||||
private void handleOperationCancelOrFail() {
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
LogUtils.d(TAG, "【业务逻辑】操作取消或失败,恢复预览");
|
||||
ToastUtils.show("操作取消或失败");
|
||||
doubleRefreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拍照结果
|
||||
*/
|
||||
private void handleTakePhotoResult(int resultCode, Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】handleTakePhotoResult 处理拍照结果");
|
||||
if (resultCode != RESULT_OK || data == null) {
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) {
|
||||
ToastUtils.show("拍照文件无效");
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap photoBitmap = getTakePhotoBitmap(data);
|
||||
if (isBitmapValid(photoBitmap)) {
|
||||
mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap);
|
||||
} else {
|
||||
ToastUtils.show("拍照图片为空");
|
||||
return;
|
||||
}
|
||||
|
||||
mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath());
|
||||
doubleRefreshPreview();
|
||||
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
mBgSourceUtils.getPreviewBackgroundBean(),
|
||||
mBackgroundView.getWidth(),
|
||||
mBackgroundView.getHeight(),
|
||||
false,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
LogUtils.d(TAG, "【拍照完成】已启动裁剪");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析拍照返回的 Bitmap
|
||||
*/
|
||||
private Bitmap getTakePhotoBitmap(Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】getTakePhotoBitmap 解析拍照Bitmap");
|
||||
if (mfTakePhoto != null && mfTakePhoto.exists()) {
|
||||
LogUtils.d(TAG, "【拍照Bitmap解析】从文件解析");
|
||||
Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto);
|
||||
if (isBitmapValid(photoBitmap)) {
|
||||
LogUtils.d(TAG, "【拍照Bitmap解析】成功");
|
||||
return photoBitmap;
|
||||
} else {
|
||||
LogUtils.w(TAG, "【拍照Bitmap解析】文件解析失败,尝试Intent");
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, "【拍照Bitmap解析】文件无效,尝试Intent");
|
||||
}
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
Bitmap thumbnailBitmap = data.getParcelableExtra("data");
|
||||
if (isBitmapValid(thumbnailBitmap)) {
|
||||
LogUtils.d(TAG, "【拍照Bitmap解析】从Intent获取成功");
|
||||
return thumbnailBitmap;
|
||||
} else {
|
||||
LogUtils.e(TAG, "【拍照Bitmap解析】Intent解析失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【拍照Bitmap解析】Intent异常:" + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
LogUtils.e(TAG, "【拍照Bitmap解析】Intent为空");
|
||||
}
|
||||
|
||||
LogUtils.e(TAG, "【拍照Bitmap解析】失败");
|
||||
ToastUtils.show("拍照图片解析失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选图结果
|
||||
* 移除:缓存清空逻辑
|
||||
*/
|
||||
private void handleSelectPictureResult(int resultCode, Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】handleSelectPictureResult 处理选图结果");
|
||||
if (resultCode != RESULT_OK || data == null) {
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
Uri selectedImage = data.getData();
|
||||
if (selectedImage == null) {
|
||||
ToastUtils.show("图片Uri为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "【选图回调】系统返回Uri : " + selectedImage.toString());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
getContentResolver().takePersistableUriPermission(
|
||||
selectedImage,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
);
|
||||
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
|
||||
}
|
||||
|
||||
if (putUriFileToPreviewSource(selectedImage)) {
|
||||
LogUtils.d(TAG, "【选图同步】路径绑定完成");
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
mBgSourceUtils.getPreviewBackgroundBean(),
|
||||
mBackgroundView.getWidth(),
|
||||
mBackgroundView.getHeight(),
|
||||
false,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
} else {
|
||||
ToastUtils.show("图片同步失败");
|
||||
LogUtils.e(TAG, "【选图同步】文件复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Uri 文件同步到预览 Bean
|
||||
*/
|
||||
boolean putUriFileToPreviewSource(Uri srcUriFile) {
|
||||
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
|
||||
* 保留:核心修复逻辑(更新预览Bean路径)
|
||||
*/
|
||||
boolean putUriFileToPreviewSource(File srcFile) {
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource(File srcFile) srcFile %s", srcFile));
|
||||
mBgSourceUtils.loadSettings();
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
File dstFile = new File(previewBean.getBackgroundFilePath());
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource(File srcFile) dstFile %s", dstFile));
|
||||
if (FileUtils.copyFile(srcFile, dstFile)) {
|
||||
LogUtils.d(TAG, "putUriFileToPreviewSource(File srcFile) 文件拷贝成功。 ");
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "putUriFileToPreviewSource(File srcFile) 文件无法拷贝。 ");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪结果
|
||||
*/
|
||||
private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
|
||||
LogUtils.d(TAG, "【业务逻辑】handleCropImageResult 处理裁剪结果");
|
||||
File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath());
|
||||
boolean isFileExist = cropTempFile.exists();
|
||||
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
|
||||
long fileSize = isFileExist ? cropTempFile.length() : 0;
|
||||
boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100;
|
||||
|
||||
if (isCropSuccess) {
|
||||
isPreviewBackgroundChanged = true;
|
||||
LogUtils.d(TAG, "【裁剪结果】裁剪成功");
|
||||
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
previewBean.setIsUseBackgroundFile(true);
|
||||
previewBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
mBgSourceUtils.saveSettings();
|
||||
|
||||
float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
|
||||
if (systemFileRatio > 0) {
|
||||
Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
|
||||
if (isBitmapValid(cropBitmap)) {
|
||||
Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
|
||||
if (isBitmapValid(scaledCropBitmap)) {
|
||||
saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
|
||||
scaledCropBitmap.recycle();
|
||||
}
|
||||
cropBitmap.recycle();
|
||||
} else {
|
||||
LogUtils.e(TAG, "【裁剪结果】裁剪Bitmap解析无效");
|
||||
}
|
||||
}
|
||||
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isFinishing()) {
|
||||
doubleRefreshPreview();
|
||||
LogUtils.d(TAG, "【裁剪结果】触发双重刷新");
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
handleOperationCancelOrFail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,8 @@ 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;
|
||||
@@ -35,11 +32,11 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public class BatteryReportActivity extends Activity {
|
||||
public static final String TAG = "BatteryReportActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
private RecyclerView rvBatteryReport;
|
||||
private BatteryReportAdapter adapter;
|
||||
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
|
||||
@@ -53,34 +50,10 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
|
||||
private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
mPackageManager = getPackageManager();
|
||||
|
||||
// 权限检查(Java7 传统条件判断)
|
||||
|
||||
@@ -7,37 +7,26 @@ import android.view.View;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
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.beans.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
import java.util.ArrayList;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
|
||||
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public class ClearRecordActivity extends Activity {
|
||||
|
||||
public static final String TAG = "ClearRecordActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
AToolbar mAToolbar;
|
||||
TextView mtvRecordText;
|
||||
App mApplication;
|
||||
boolean mIsShowRecordWithEnter = false;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -45,18 +34,21 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
mApplication = (App) getApplication();
|
||||
|
||||
// 初始化工具栏
|
||||
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();
|
||||
}
|
||||
});
|
||||
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
|
||||
setActionBar(mAToolbar);
|
||||
//mAToolbar.setTitle(getTitle() + " - " + getString(R.string.subtitle_activity_clearrecord));
|
||||
mAToolbar.setSubtitle(R.string.subtitle_activity_clearrecord);
|
||||
//mAToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
//mAToolbar.setSubtitleTextAppearance(this, R.style.Toolbar_SubTitleText);
|
||||
//mAToolbar.setBackgroundColor(getColor(R.color.colorPrimary));
|
||||
setActionBar(mAToolbar);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置滑动清理控件
|
||||
//
|
||||
|
||||
@@ -24,10 +24,10 @@ import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.views.AToolbar;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
|
||||
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -193,10 +193,10 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
// 可以在这里添加确定后的回调逻辑
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundBean bean = utils.getCurrentBackgroundBean();
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveSettings();
|
||||
utils.saveData();
|
||||
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
|
||||
setBackgroundColor();
|
||||
}
|
||||
@@ -217,8 +217,8 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
|
||||
|
||||
void setBackgroundColor() {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundBean bean = utils.getCurrentBackgroundBean();
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
|
||||
int nPixelColor = bean.getPixelColor();
|
||||
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
|
||||
mainLayout.setBackgroundColor(nPixelColor);
|
||||
@@ -235,7 +235,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, BackgroundSettingsActivity.class);
|
||||
intent.setClass(this, BackgroundPictureActivity.class);
|
||||
startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), );
|
||||
return true;
|
||||
@@ -248,7 +248,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, BackgroundSettingsActivity.class);
|
||||
intent.setClass(this, BackgroundPictureActivity.class);
|
||||
startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
|
||||
}
|
||||
|
||||
@@ -1,67 +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.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/27 14:26
|
||||
* @Describe 应用设置窗口
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "SettingsActivity";
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
@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);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onCheckPermission(View view) {
|
||||
//ToastUtils.show("onCheckPermission");
|
||||
PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
|
||||
PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,6 @@ import android.widget.TextView;
|
||||
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.powerbell.BuildConfig;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
@@ -31,24 +28,15 @@ import cc.winboll.studio.powerbell.R;
|
||||
public abstract class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||
|
||||
protected TextView mTagView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
changeFullScreen(this);
|
||||
}
|
||||
|
||||
void setThemeStyle() {
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
@@ -91,13 +79,13 @@ public abstract class WinBoLLActivity extends AppCompatActivity implements IWinB
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
//GlobalApplication.getWinBoLLActivityManager().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
//GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
|
||||
import cc.winboll.studio.powerbell.models.BatteryData;
|
||||
import cc.winboll.studio.powerbell.beans.BatteryData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -0,0 +1,99 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 11:52:28
|
||||
* @Describe 应用背景图片数据类
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class BackgroundPictureBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "BackgroundPictureBean";
|
||||
|
||||
int backgroundWidth = 100;
|
||||
int backgroundHeight = 100;
|
||||
boolean isUseBackgroundFile = false;
|
||||
// 图片拾取像素颜色
|
||||
int pixelColor = 0;
|
||||
|
||||
public BackgroundPictureBean() {
|
||||
}
|
||||
|
||||
public BackgroundPictureBean(String recivedFileName, boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public void setPixelColor(int pixelColor) {
|
||||
this.pixelColor = pixelColor;
|
||||
}
|
||||
|
||||
public int getPixelColor() {
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
public void setBackgroundWidth(int backgroundWidth) {
|
||||
this.backgroundWidth = backgroundWidth;
|
||||
}
|
||||
|
||||
public int getBackgroundWidth() {
|
||||
return backgroundWidth;
|
||||
}
|
||||
|
||||
public void setBackgroundHeight(int backgroundHeight) {
|
||||
this.backgroundHeight = backgroundHeight;
|
||||
}
|
||||
|
||||
public int getBackgroundHeight() {
|
||||
return backgroundHeight;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public boolean isUseBackgroundFile() {
|
||||
return isUseBackgroundFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return BackgroundPictureBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
BackgroundPictureBean bean = this;
|
||||
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
|
||||
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
|
||||
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
|
||||
jsonWriter.name("pixelColor").value(bean.getPixelColor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
BackgroundPictureBean bean = new BackgroundPictureBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("backgroundWidth")) {
|
||||
bean.setBackgroundWidth(jsonReader.nextInt());
|
||||
} else if (name.equals("backgroundHeight")) {
|
||||
bean.setBackgroundHeight(jsonReader.nextInt());
|
||||
} else if (name.equals("isUseBackgroundFile")) {
|
||||
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
|
||||
} else if (name.equals("pixelColor")) {
|
||||
bean.setPixelColor(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
|
||||
// 应用消息结构
|
||||
//
|
||||
@@ -2,19 +2,22 @@ package cc.winboll.studio.powerbell.dialogs;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -26,25 +29,21 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
public static final String TAG = "BackgroundPicturePreviewDialog";
|
||||
|
||||
Context mContext;
|
||||
//BackgroundSourceUtils mBackgroundPictureUtils;
|
||||
BackgroundPictureUtils mBackgroundPictureUtils;
|
||||
Button dialogbackgroundpicturepreviewButton1;
|
||||
Button dialogbackgroundpicturepreviewButton2;
|
||||
//String mszPreReceivedFileName;
|
||||
IOnRecivedPictureListener mIOnRecivedPictureListener;
|
||||
Uri mUriRecivedPicture;
|
||||
BackgroundView mBackgroundView;
|
||||
String mszPreReceivedFileName;
|
||||
|
||||
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
|
||||
public BackgroundPicturePreviewDialog(Context context) {
|
||||
super(context);
|
||||
setContentView(R.layout.dialog_backgroundpicturepreview);
|
||||
mIOnRecivedPictureListener = iOnRecivedPictureListener;
|
||||
//initEnv();
|
||||
initEnv();
|
||||
|
||||
mContext = context;
|
||||
//mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
|
||||
mBackgroundPictureUtils = ((BackgroundPictureActivity)context).mBackgroundPictureUtils;
|
||||
|
||||
mBackgroundView = findViewById(R.id.backgroundview);
|
||||
previewRecivedPicture();
|
||||
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
|
||||
copyAndViewRecivePicture(imageView);
|
||||
|
||||
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
|
||||
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
|
||||
@@ -54,7 +53,6 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
// 跳转到主窗口
|
||||
Intent i = new Intent(mContext, MainActivity.class);
|
||||
mContext.startActivity(i);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -64,77 +62,79 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 使用分享到的图片
|
||||
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
|
||||
//
|
||||
//LogUtils.d(TAG, "mszReceivedFileName : " + mszReceivedFileName);
|
||||
((IOnRecivedPictureListener)mContext).onAcceptRecivedPicture(mszPreReceivedFileName);
|
||||
// 关闭对话框
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// void initEnv() {
|
||||
// LogUtils.d(TAG, "initEnv()");
|
||||
// mszPreReceivedFileName = "PreReceived.data";
|
||||
// }
|
||||
void initEnv() {
|
||||
LogUtils.d(TAG, "initEnv()");
|
||||
mszPreReceivedFileName = "PreReceived.data";
|
||||
}
|
||||
|
||||
void previewRecivedPicture() {
|
||||
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
|
||||
void copyAndViewRecivePicture(ImageView imageView) {
|
||||
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
|
||||
BackgroundPictureActivity activity = ((BackgroundPictureActivity)mContext);
|
||||
|
||||
//取出文件uri
|
||||
mUriRecivedPicture = activity.getIntent().getData();
|
||||
if (mUriRecivedPicture == null) {
|
||||
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
Uri uri = activity.getIntent().getData();
|
||||
if (uri == null) {
|
||||
uri = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
//获取文件真实地址
|
||||
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
|
||||
String szSrcImage = UriUtil.getFilePathFromUri(mContext, uri);
|
||||
if (TextUtils.isEmpty(szSrcImage)) {
|
||||
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
mBackgroundView.loadImage(szSrcImage);
|
||||
//
|
||||
// File fSrcImage = new File(szSrcImage);
|
||||
// //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
|
||||
// File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
|
||||
// // 复制源图片到剪裁文件
|
||||
// try {
|
||||
// FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
|
||||
// LogUtils.d(TAG, "copyFileUsingFileChannels");
|
||||
// Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
|
||||
// imageView.setBackground(drawable);
|
||||
// //LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
|
||||
// } catch (IOException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// }
|
||||
|
||||
File fSrcImage = new File(szSrcImage);
|
||||
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
|
||||
File mfPreReceivedPhoto = new File(activity.mBackgroundPictureUtils.getBackgroundDir(), mszPreReceivedFileName);
|
||||
// 复制源图片到剪裁文件
|
||||
try {
|
||||
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
|
||||
LogUtils.d(TAG, "copyFileUsingFileChannels");
|
||||
Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
|
||||
imageView.setBackground(drawable);
|
||||
//LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 创建图片背景图片目录
|
||||
//
|
||||
// boolean createBackgroundFolder2(String szBackgroundFolder) {
|
||||
// // 文件路径参数为空值或无效值时返回false.
|
||||
// if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
|
||||
// File f = new File(szBackgroundFolder);
|
||||
// if (f.exists()) {
|
||||
// if (f.isDirectory()) {
|
||||
// return true;
|
||||
// } else {
|
||||
// // 工作路径不是一个目录
|
||||
// LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
|
||||
// return false;
|
||||
// }
|
||||
// } else {
|
||||
// return f.mkdirs();
|
||||
// }
|
||||
// }
|
||||
boolean createBackgroundFolder2(String szBackgroundFolder) {
|
||||
// 文件路径参数为空值或无效值时返回false.
|
||||
if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
|
||||
File f = new File(szBackgroundFolder);
|
||||
if (f.exists()) {
|
||||
if (f.isDirectory()) {
|
||||
return true;
|
||||
} else {
|
||||
// 工作路径不是一个目录
|
||||
LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return f.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IOnRecivedPictureListener {
|
||||
void onAcceptRecivedPicture(Uri uriRecivedPicture);
|
||||
void onAcceptRecivedPicture(String szBackgroundFileName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,12 +15,11 @@ 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.PictureUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import cc.winboll.studio.powerbell.utils.ImageDownloader;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -42,17 +41,15 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
private Button btnConfirm;
|
||||
private Button btnPreview;
|
||||
private EditText etURL;
|
||||
BackgroundView mBackgroundView;
|
||||
BackgroundView bvBackgroundPreview;
|
||||
Context mContext;
|
||||
// 主线程 Handler,用于接收子线程消息并更新 UI
|
||||
private Handler mUiHandler;
|
||||
String mPreviewFilePath;
|
||||
String mPreviewFileUrl;
|
||||
String mDownloadSavedPath;
|
||||
String previewFilePath;
|
||||
|
||||
// 按钮点击回调接口(Java7 接口实现)
|
||||
public interface OnDialogClickListener {
|
||||
void onConfirm(String szConfirmFilePath, String previewFileUrl); // 确认按钮点击
|
||||
void onConfirm(); // 确认按钮点击
|
||||
void onCancel(); // 取消按钮点击
|
||||
}
|
||||
|
||||
@@ -90,12 +87,12 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
switch (msg.what) {
|
||||
case MSG_IMAGE_LOAD_SUCCESS:
|
||||
// 图片加载成功,获取文件路径并设置背景
|
||||
mDownloadSavedPath = (String) msg.obj;
|
||||
previewBackground(mDownloadSavedPath);
|
||||
String filePath = (String) msg.obj;
|
||||
setBackgroundFromPath(filePath);
|
||||
break;
|
||||
case MSG_IMAGE_LOAD_FAILED:
|
||||
// 图片加载失败,设置默认背景
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
|
||||
ToastUtils.show("图片预览失败,请检查链接");
|
||||
break;
|
||||
}
|
||||
@@ -137,9 +134,8 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
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);
|
||||
// 加载初始图片
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
bvBackgroundPreview = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
|
||||
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListeners();
|
||||
}
|
||||
@@ -153,9 +149,6 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击");
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.setCurrentSourceToPreview();
|
||||
|
||||
dismiss(); // 关闭对话框
|
||||
if (listener != null) {
|
||||
listener.onCancel();
|
||||
@@ -169,12 +162,11 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
|
||||
// 确定预览背景资源
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
bvBackgroundPreview.saveToBackgroundSources(previewFilePath);
|
||||
|
||||
dismiss(); // 关闭对话框
|
||||
if (listener != null) {
|
||||
listener.onConfirm(mPreviewFilePath, mPreviewFileUrl);
|
||||
listener.onConfirm();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -183,7 +175,14 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
btnPreview.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "确认预览点击");
|
||||
downloadImageToAlbumAndPreview();
|
||||
/*String url = etURL.getText().toString().trim();
|
||||
if (url.isEmpty()) {
|
||||
ToastUtils.show("请输入图片链接");
|
||||
return;
|
||||
}
|
||||
ImageDownloader.getInstance(mContext).downloadImage(url, mDownloadCallback);*/
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -192,25 +191,26 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
* 根据文件路径设置 BackgroundView 背景(主线程调用)
|
||||
* @param filePath 图片文件路径
|
||||
*/
|
||||
private void previewBackground(String previewFilePath) {
|
||||
private void setBackgroundFromPath(String filePath) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
File imageFile = new File(previewFilePath);
|
||||
File imageFile = new File(filePath);
|
||||
if (!imageFile.exists()) {
|
||||
ToastUtils.show("图片文件不存在:" + previewFilePath);
|
||||
LogUtils.e(TAG, "图片文件不存在:" + previewFilePath);
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
LogUtils.e(TAG, "图片文件不存在:" + filePath);
|
||||
ToastUtils.show("Test");
|
||||
//bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预览背景
|
||||
mPreviewFilePath = previewFilePath;
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
mBackgroundView.loadBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
previewFilePath = filePath;
|
||||
bvBackgroundPreview.previewBackgroundImage(previewFilePath);
|
||||
|
||||
LogUtils.d(TAG, "图片预览成功:" + filePath);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
|
||||
LogUtils.e(TAG, "图片预览失败:" + e.getMessage());
|
||||
} finally {
|
||||
// Java7 手动关闭流,避免资源泄漏
|
||||
@@ -249,20 +249,40 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/*ImageDownloader.DownloadCallback mDownloadCallback = new ImageDownloader.DownloadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String filePath) {
|
||||
ToastUtils.show("图片下载成功:" + filePath);
|
||||
LogUtils.d(TAG, filePath);
|
||||
// 发送消息到主线程,携带图片路径
|
||||
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, filePath);
|
||||
mUiHandler.sendMessage(successMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
ToastUtils.show("下载失败:" + errorMsg);
|
||||
LogUtils.e(TAG, errorMsg);
|
||||
// 发送图片加载失败消息
|
||||
mUiHandler.sendEmptyMessage(MSG_IMAGE_LOAD_FAILED);
|
||||
}
|
||||
};*/
|
||||
|
||||
void downloadImageToAlbumAndPreview() {
|
||||
//String previewFileUrl = "https://example.com/test.jpg";
|
||||
mPreviewFileUrl = etURL.getText().toString();
|
||||
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback(){
|
||||
//String imgUrl = "https://example.com/test.jpg";
|
||||
String imgUrl = etURL.getText().toString();
|
||||
PictureUtils.downloadImageToAlbum(mContext, imgUrl, new PictureUtils.DownloadCallback(){
|
||||
@Override
|
||||
public void onSuccess(String savePath) {
|
||||
ToastUtils.show("下载成功:" + savePath);
|
||||
// 发送消息到主线程,携带图片路径
|
||||
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
|
||||
mUiHandler.sendMessage(successMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
ToastUtils.show("下载失败:" + errorMsg);
|
||||
public void onFailure(Exception e) {
|
||||
ToastUtils.show("下载失败:" + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,11 +19,8 @@ import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import cc.winboll.studio.powerbell.views.BatteryDrawable;
|
||||
@@ -33,8 +30,328 @@ public class MainViewFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "MainViewFragment";
|
||||
|
||||
|
||||
|
||||
public static final int MSG_RELOAD_APPCONFIG = 0;
|
||||
public static final int MSG_CURRENTVALUEBATTERY = 1;
|
||||
|
||||
static MainViewFragment _mMainViewFragment;
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
View mView;
|
||||
Drawable mDrawableFrame;
|
||||
LinearLayout mllLeftSeekBar;
|
||||
LinearLayout mllRightSeekBar;
|
||||
CheckBox mcbIsEnableChargeReminder;
|
||||
CheckBox mcbIsEnableUsegeReminder;
|
||||
Switch mswIsEnableService;
|
||||
TextView mtvTips;
|
||||
|
||||
// 背景布局
|
||||
//LinearLayout mLinearLayoutloadBackground;
|
||||
|
||||
// 现在电量图示
|
||||
BatteryDrawable mCurrentValueBatteryDrawable;
|
||||
// 现在充电提醒电量图示
|
||||
BatteryDrawable mChargeReminderValueBatteryDrawable;
|
||||
// 现在耗电提醒电量图示
|
||||
BatteryDrawable mUsegeReminderValueBatteryDrawable;
|
||||
|
||||
ImageView mCurrentValueBatteryImageView;
|
||||
ImageView mChargeReminderValueBatteryImageView;
|
||||
ImageView mUsegeReminderValueBatteryImageView;
|
||||
|
||||
VerticalSeekBar mChargeReminderSeekBar;
|
||||
ChargeReminderSeekBarChangeListener mChargeReminderSeekBarChangeListener;
|
||||
TextView mtvChargeReminderValue;
|
||||
|
||||
|
||||
VerticalSeekBar mUsegeReminderSeekBar;
|
||||
UsegeReminderSeekBarChangeListener mUsegeReminderSeekBarChangeListener;
|
||||
TextView mtvUsegeReminderValue;
|
||||
CheckBox mcbUsegeReminderValue;
|
||||
TextView mtvCurrentValue;
|
||||
BackgroundView bvPreviewBackground;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
mView = inflater.inflate(R.layout.fragment_mainview, container, false);
|
||||
_mMainViewFragment = MainViewFragment.this;
|
||||
mAppConfigUtils = App.getAppConfigUtils(getActivity());
|
||||
|
||||
// 获取指定ID的View实例
|
||||
bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
|
||||
/*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1);
|
||||
|
||||
// 注册OnGlobalLayoutListener
|
||||
mainImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
// 获取宽度和高度
|
||||
int width = mainImageView.getMeasuredWidth();
|
||||
int height = mainImageView.getMeasuredHeight();
|
||||
|
||||
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(getActivity());
|
||||
BackgroundPictureBean bean = utils.loadBackgroundPictureBean();
|
||||
bean.setBackgroundWidth(width);
|
||||
bean.setBackgroundHeight(height);
|
||||
utils.saveData();
|
||||
// 移除监听器以避免内存泄漏
|
||||
mainImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});*/
|
||||
|
||||
mDrawableFrame = getActivity().getDrawable(R.drawable.bg_frame);
|
||||
mllLeftSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout1);
|
||||
mllRightSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout2);
|
||||
|
||||
// 初始化充电电量提醒设置控件
|
||||
mtvChargeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView2);
|
||||
mChargeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar1);
|
||||
mcbIsEnableChargeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox1);
|
||||
|
||||
// 初始化耗电电量提醒设置控件
|
||||
mtvUsegeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView3);
|
||||
mUsegeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar2);
|
||||
mcbIsEnableUsegeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox2);
|
||||
|
||||
// 初始化现在电量显示控件
|
||||
mtvCurrentValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView4);
|
||||
|
||||
// 初始化服务总开关
|
||||
mswIsEnableService = (Switch) mView.findViewById(R.id.fragmentandroidviewSwitch1);
|
||||
mtvTips = mView.findViewById(R.id.fragmentandroidviewTextView1);
|
||||
|
||||
// 设置视图显示数据
|
||||
setViewData();
|
||||
// 设置视图控件响应
|
||||
setViewListener();
|
||||
|
||||
// 注册一个广播接收
|
||||
//mMainActivityReceiver = new MainActivityReceiver(this);
|
||||
//mMainActivityReceiver.registerAction();
|
||||
|
||||
// 启动的时候检查一下服务
|
||||
if (mAppConfigUtils.getIsEnableService()
|
||||
&& ServiceUtils.isServiceAlive(getActivity(), ControlCenterService.class.getName()) == false) {
|
||||
// 如果配置了服务启动,服务没有启动
|
||||
// 就启动服务
|
||||
Intent intent = new Intent(getActivity(), ControlCenterService.class);
|
||||
getActivity().startForegroundService(intent);
|
||||
}
|
||||
|
||||
return mView;
|
||||
}
|
||||
|
||||
void setViewData() {
|
||||
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
|
||||
int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue();
|
||||
int nCurrentValue = mAppConfigUtils.getCurrentValue();
|
||||
|
||||
mllLeftSeekBar.setBackground(mDrawableFrame);
|
||||
mllRightSeekBar.setBackground(mDrawableFrame);
|
||||
|
||||
// 初始化电量图
|
||||
mCurrentValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCurrent));
|
||||
mCurrentValueBatteryDrawable.setValue(mAppConfigUtils.getCurrentValue());
|
||||
mCurrentValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView1);
|
||||
mCurrentValueBatteryImageView.setImageDrawable(mCurrentValueBatteryDrawable);
|
||||
|
||||
// 初始化充电电量提醒图
|
||||
mChargeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCharge));
|
||||
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
|
||||
mChargeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView3);
|
||||
mChargeReminderValueBatteryImageView.setImageDrawable(mChargeReminderValueBatteryDrawable);
|
||||
|
||||
// 初始化耗电电量提醒图
|
||||
mUsegeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorUsege));
|
||||
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
|
||||
mUsegeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView2);
|
||||
mUsegeReminderValueBatteryImageView.setImageDrawable(mUsegeReminderValueBatteryDrawable);
|
||||
|
||||
// 初始化充电电量提醒设置控件
|
||||
mtvChargeReminderValue.setTextColor(getActivity().getColor(R.color.colorCharge));
|
||||
//LogUtils.d(TAG, "Color.YELLOW is " + Integer.toString(mApplication.getColor(R.color.colorCharge)));
|
||||
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
|
||||
mChargeReminderSeekBar.setProgress(nChargeReminderValue);
|
||||
mcbIsEnableChargeReminder.setChecked(mAppConfigUtils.getIsEnableChargeReminder());
|
||||
|
||||
// 初始化耗电电量提醒设置控件
|
||||
mtvUsegeReminderValue.setTextColor(getActivity().getColor(R.color.colorUsege));
|
||||
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
|
||||
mUsegeReminderSeekBar.setProgress(nUsegeReminderValue);
|
||||
mcbIsEnableUsegeReminder.setChecked(mAppConfigUtils.getIsEnableUsegeReminder());
|
||||
|
||||
// 初始化现在电量显示控件
|
||||
mtvCurrentValue.setTextColor(getActivity().getColor(R.color.colorCurrent));
|
||||
mtvCurrentValue.setText(Integer.toString(nCurrentValue) + "%");
|
||||
|
||||
// 初始化服务总开关
|
||||
mswIsEnableService.setChecked(mAppConfigUtils.getIsEnableService());
|
||||
if (mAppConfigUtils.getIsEnableService()) {
|
||||
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
|
||||
ControlCenterService.startControlCenterService(getActivity());
|
||||
} else {
|
||||
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
|
||||
ControlCenterService.stopControlCenterService(getActivity());
|
||||
}
|
||||
mswIsEnableService.setText(getString(R.string.txt_aboveswitch));
|
||||
mtvTips.setText(getString(R.string.txt_aboveswitchtips));
|
||||
|
||||
}
|
||||
|
||||
void setViewListener() {
|
||||
// 初始化充电电量提醒设置控件
|
||||
mChargeReminderSeekBarChangeListener = new ChargeReminderSeekBarChangeListener();
|
||||
mChargeReminderSeekBar.setOnSeekBarChangeListener(mChargeReminderSeekBarChangeListener);
|
||||
mcbIsEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "setIsEnableChargeReminder");
|
||||
mAppConfigUtils.setIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
|
||||
//ControlCenterService.updateIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 初始化耗电电量提醒设置控件
|
||||
mUsegeReminderSeekBarChangeListener = new UsegeReminderSeekBarChangeListener();
|
||||
mUsegeReminderSeekBar.setOnSeekBarChangeListener(mUsegeReminderSeekBarChangeListener);
|
||||
mcbIsEnableUsegeReminder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "setIsEnableUsegeReminder");
|
||||
mAppConfigUtils.setIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
|
||||
//ControlCenterService.updateIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化服务总开关
|
||||
mswIsEnableService.setOnClickListener(new CompoundButton.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
mAppConfigUtils.setIsEnableService(getActivity(), mswIsEnableService.isChecked());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setCurrentValueBattery(int value) {
|
||||
//LogUtils.d(TAG, "setCurrentValueBattery");
|
||||
mtvCurrentValue.setText(Integer.toString(value) + "%");
|
||||
mCurrentValueBatteryDrawable.setValue(value);
|
||||
mCurrentValueBatteryDrawable.invalidateSelf();
|
||||
}
|
||||
|
||||
class ChargeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
//LogUtils.d(TAG, "call onProgressChanged");
|
||||
int nChargeReminderValue = progress;
|
||||
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
|
||||
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
|
||||
mChargeReminderValueBatteryDrawable.invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStartTrackingTouch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStopTrackingTouch");
|
||||
//取得当前进度条的刻度
|
||||
int nChargeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
|
||||
|
||||
mAppConfigUtils.setChargeReminderValue(nChargeReminderValue);
|
||||
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
|
||||
//ControlCenterService.updateChargeReminderValue(nChargeReminderValue);
|
||||
}
|
||||
}
|
||||
|
||||
class UsegeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
//LogUtils.d(TAG, "call onProgressChanged");
|
||||
int nUsegeReminderValue = progress;
|
||||
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
|
||||
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
|
||||
mUsegeReminderValueBatteryDrawable.invalidateSelf();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStartTrackingTouch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
//LogUtils.d(TAG, "call onStopTrackingTouch");
|
||||
//取得当前进度条的刻度
|
||||
int nUsegeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
|
||||
LogUtils.d(TAG, "nUsegeReminderValue is " + Integer.toString(nUsegeReminderValue));
|
||||
//LogUtils.d(TAG, "mPowerReminder is " + mApplication);
|
||||
mAppConfigUtils.setUsegeReminderValue(nUsegeReminderValue);
|
||||
//LogUtils.d(TAG, "opopopopopopopop");
|
||||
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
|
||||
//ControlCenterService.updateUsegeReminderValue(nUsegeReminderValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadBackground() {
|
||||
bvPreviewBackground.reloadBackgroundImage();
|
||||
// BackgroundPictureBean bean = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundPictureBean();
|
||||
// ImageView imageView = mView.findViewById(R.id.fragmentmainviewImageView1);
|
||||
// String szBackgroundFilePath = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundDir() + BackgroundPictureActivity.getBackgroundFileName();
|
||||
// File fBackgroundFilePath = new File(szBackgroundFilePath);
|
||||
// LogUtils.d(TAG, "szBackgroundFilePath : " + szBackgroundFilePath);
|
||||
// LogUtils.d(TAG, String.format("fBackgroundFilePath.exists() %s", fBackgroundFilePath.exists()));
|
||||
// if (bean.isUseBackgroundFile() && fBackgroundFilePath.exists()) {
|
||||
// Drawable drawableBackground = Drawable.createFromPath(szBackgroundFilePath);
|
||||
// //drawableBackground.setAlpha(120);
|
||||
// imageView.setImageDrawable(drawableBackground);
|
||||
// } else {
|
||||
// Drawable drawableBackground = getActivity().getDrawable(R.drawable.blank10x10);
|
||||
// //drawableBackground.setAlpha(120);
|
||||
// imageView.setImageDrawable(drawableBackground);
|
||||
// }
|
||||
}
|
||||
|
||||
Handler mHandler = new Handler(){
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_RELOAD_APPCONFIG : {
|
||||
setViewData();
|
||||
break;
|
||||
}
|
||||
case MSG_CURRENTVALUEBATTERY : {
|
||||
setCurrentValueBattery(msg.arg1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static void relaodAppConfigs() {
|
||||
if (_mMainViewFragment != null) {
|
||||
Handler handler = _mMainViewFragment.mHandler;
|
||||
handler.sendMessage(handler.obtainMessage(MSG_RELOAD_APPCONFIG));
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMsgCurrentValueBattery(int value) {
|
||||
if (_mMainViewFragment != null) {
|
||||
Handler handler = _mMainViewFragment.mHandler;
|
||||
Message msg = handler.obtainMessage(MSG_CURRENTVALUEBATTERY);
|
||||
msg.arg1 = value;
|
||||
handler.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 11:52:28
|
||||
* @Describe 应用背景图片数据类(存储正式/预览背景配置,支持JSON序列化/反序列化)
|
||||
*/
|
||||
public class BackgroundBean extends BaseBean implements Serializable {
|
||||
|
||||
public static final String TAG = "BackgroundPictureBean";
|
||||
|
||||
// 核心字段:背景图片文件名(对应应用私有目录下的图片文件,与BackgroundSettingsActivity的_mSourceCroppedFile匹配)
|
||||
private String backgroundFileName = "";
|
||||
// 核心字段:背景图片完整路径(解决仅存文件名导致的路径拼接错误,与backgroundScaledCompressFilePath对应)
|
||||
private String backgroundFilePath = "";
|
||||
// 附加字段:图片信息(如Uri、网络地址等,仅作备注,不参与路径生成)
|
||||
private String backgroundFileInfo = "";
|
||||
// 控制字段:是否启用背景图片(true-显示背景图,false-显示透明背景)
|
||||
private boolean isUseBackgroundFile = false;
|
||||
// 核心字段:压缩后背景图片文件名(对应应用私有目录下的压缩图片,与saveCropBitmap的压缩图匹配)
|
||||
private String backgroundScaledCompressFileName = "";
|
||||
// 核心字段:压缩后背景图片完整路径(解决仅存文件名导致的路径拼接错误,适配BackgroundSettingsActivity的私有目录)
|
||||
private String backgroundScaledCompressFilePath = "";
|
||||
// 重命名字段:是否启用压缩背景图(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile,语义更清晰)
|
||||
private boolean isUseBackgroundScaledCompressFile = false;
|
||||
// 裁剪比例字段:背景图宽高比(默认1:1,用于固定比例裁剪)
|
||||
private int backgroundWidth = 100;
|
||||
private int backgroundHeight = 100;
|
||||
// 像素拾取字段:拾取的像素颜色(用于纯色背景)
|
||||
private int pixelColor = 0;
|
||||
|
||||
/**
|
||||
* 无参构造器(必须,JSON反序列化时需默认构造器)
|
||||
*/
|
||||
public BackgroundBean() {
|
||||
}
|
||||
|
||||
// ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)======================================
|
||||
public String getBackgroundFileName() {
|
||||
return backgroundFileName;
|
||||
}
|
||||
|
||||
public void setBackgroundFileName(String backgroundFileName) {
|
||||
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName; // 防null,避免空指针
|
||||
}
|
||||
|
||||
public String getBackgroundFilePath() {
|
||||
return backgroundFilePath;
|
||||
}
|
||||
|
||||
public void setBackgroundFilePath(String backgroundFilePath) {
|
||||
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath; // 防null,避免路径拼接错误
|
||||
}
|
||||
|
||||
public String getBackgroundFileInfo() {
|
||||
return backgroundFileInfo;
|
||||
}
|
||||
|
||||
public void setBackgroundFileInfo(String backgroundFileInfo) {
|
||||
this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo; // 防null,避免空指针
|
||||
}
|
||||
|
||||
public boolean isUseBackgroundFile() {
|
||||
return isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
|
||||
this.isUseBackgroundFile = isUseBackgroundFile;
|
||||
}
|
||||
|
||||
public String getBackgroundScaledCompressFileName() {
|
||||
return backgroundScaledCompressFileName;
|
||||
}
|
||||
|
||||
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
|
||||
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null
|
||||
}
|
||||
|
||||
public String getBackgroundScaledCompressFilePath() {
|
||||
return backgroundScaledCompressFilePath;
|
||||
}
|
||||
|
||||
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
|
||||
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null,避免路径错误
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名:原isUseScaledCompress → 新isUseBackgroundScaledCompressFile(Getter/Setter同步修改)
|
||||
* 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆
|
||||
*/
|
||||
public boolean isUseBackgroundScaledCompressFile() {
|
||||
return isUseBackgroundScaledCompressFile;
|
||||
}
|
||||
|
||||
public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) {
|
||||
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
|
||||
}
|
||||
|
||||
public int getBackgroundWidth() {
|
||||
return backgroundWidth;
|
||||
}
|
||||
|
||||
public void setBackgroundWidth(int backgroundWidth) {
|
||||
this.backgroundWidth = backgroundWidth <= 0 ? 100 : backgroundWidth; // 防无效值,确保宽高比有效
|
||||
}
|
||||
|
||||
public int getBackgroundHeight() {
|
||||
return backgroundHeight;
|
||||
}
|
||||
|
||||
public void setBackgroundHeight(int backgroundHeight) {
|
||||
this.backgroundHeight = backgroundHeight <= 0 ? 100 : backgroundHeight; // 防无效值,确保宽高比有效
|
||||
}
|
||||
|
||||
public int getPixelColor() {
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
public void setPixelColor(int pixelColor) {
|
||||
this.pixelColor = pixelColor;
|
||||
}
|
||||
|
||||
// ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本)======================================
|
||||
@Override
|
||||
public String getName() {
|
||||
return BackgroundBean.class.getName(); // 必须重写,BaseBean序列化时需类名标识
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化:同步重命名字段(原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("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile());
|
||||
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
|
||||
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
|
||||
jsonWriter.name("pixelColor").value(bean.getPixelColor());
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化:同步处理重命名字段(兼容旧版本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());
|
||||
break;
|
||||
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
|
||||
case "isUseScaledCompress":
|
||||
tempUseScaledCompress = jsonReader.nextBoolean();
|
||||
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(); // 跳过未知字段,兼容旧版本Bean(避免崩溃)
|
||||
break;
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
// 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充
|
||||
if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) {
|
||||
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================================== 辅助方法(同步更新重命名字段)======================================
|
||||
/**
|
||||
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
|
||||
*/
|
||||
public void resetBackgroundConfig() {
|
||||
this.backgroundFileName = "";
|
||||
this.backgroundFilePath = ""; // 新增:重置背景原图完整路径
|
||||
this.backgroundScaledCompressFileName = "";
|
||||
this.backgroundScaledCompressFilePath = "";
|
||||
this.backgroundFileInfo = "";
|
||||
this.isUseBackgroundFile = false;
|
||||
this.isUseBackgroundScaledCompressFile = false; // 重命名字段:重置为false
|
||||
this.backgroundWidth = 100;
|
||||
this.backgroundHeight = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验)
|
||||
* 同步使用重命名字段判断压缩图是否启用
|
||||
* @return true-配置有效(可显示背景图),false-配置无效
|
||||
*/
|
||||
public boolean isBackgroundConfigValid() {
|
||||
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
|
||||
if (!isUseBackgroundFile) {
|
||||
return false;
|
||||
}
|
||||
// 原图校验:路径非空 或 文件名非空
|
||||
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
|
||||
// 压缩图校验:启用压缩图时,路径/文件名需非空
|
||||
boolean isCompressValid = true;
|
||||
if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图
|
||||
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
|
||||
}
|
||||
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
|
||||
return isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.beans.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
|
||||
@@ -5,9 +5,10 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationHelper;
|
||||
|
||||
public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
|
||||
@@ -47,7 +48,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
//NotificationHelper.cancelRemindNotification(context);
|
||||
|
||||
App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity);
|
||||
MainActivity.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
|
||||
MainViewFragment.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
|
||||
// 保存好新的电池状态标志
|
||||
_mIsCharging = isCharging;
|
||||
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
|
||||
|
||||
@@ -23,15 +23,15 @@ import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.beans.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.beans.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.services.AssistantService;
|
||||
import cc.winboll.studio.powerbell.threads.RemindThread;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationHelper;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class ControlCenterService extends Service {
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
AppCacheUtils mAppCacheUtils;
|
||||
// 前台服务通知工具
|
||||
NotificationManagerUtils mNotificationManagerUtils;
|
||||
NotificationHelper mNotificationHelper;
|
||||
Notification notification;
|
||||
RemindThread mRemindThread;
|
||||
ControlCenterServiceHandler mControlCenterServiceHandler;
|
||||
@@ -72,7 +72,7 @@ public class ControlCenterService extends Service {
|
||||
isServiceRunning = false;
|
||||
mAppConfigUtils = App.getAppConfigUtils(this);
|
||||
mAppCacheUtils = App.getAppCacheUtils(this);
|
||||
mNotificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
|
||||
mNotificationHelper = new NotificationHelper(ControlCenterService.this);
|
||||
|
||||
|
||||
if (mMyServiceConnection == null) {
|
||||
@@ -101,10 +101,10 @@ public class ControlCenterService extends Service {
|
||||
wakeupAndBindAssistant();
|
||||
// 显示前台通知栏
|
||||
// 在Service中
|
||||
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(this);
|
||||
//Intent intent = new Intent(this, MainActivity.class);
|
||||
notificationManagerUtils.startForegroundServiceNotify(ControlCenterService.this, new NotificationMessage(getString(R.string.app_name), "Service Running, Click to open app"));
|
||||
//startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
|
||||
NotificationHelper helper = new NotificationHelper(this);
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
notification = helper.showForegroundNotification(intent, getString(R.string.app_name), "Service Running, Click to open app");
|
||||
startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
|
||||
|
||||
// NotificationMessage notificationMessage=createNotificationMessage();
|
||||
// //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show();
|
||||
@@ -260,9 +260,9 @@ public class ControlCenterService extends Service {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
msg += szRemindMSG;
|
||||
}
|
||||
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
|
||||
NotificationHelper helper = new NotificationHelper(ControlCenterService.this);
|
||||
Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
|
||||
notificationManagerUtils.showTempAlertNotify(intent, getString(R.string.app_name), msg);
|
||||
helper.showTemporaryNotification(intent, getString(R.string.app_name), msg);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package cc.winboll.studio.powerbell.unittest;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import android.widget.Button;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/19 18:16
|
||||
* @Describe BackgroundViewTestFragment
|
||||
*/
|
||||
public class BackgroundViewTestFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "BackgroundViewTestFragment";
|
||||
|
||||
View mainView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
//super.onCreateView(inflater, container, savedInstanceState);
|
||||
|
||||
// 非调试状态就结束本线程
|
||||
if (!GlobalApplication.isDebugging()) {
|
||||
Thread.currentThread().destroy();
|
||||
}
|
||||
|
||||
mainView = inflater.inflate(R.layout.fragment_test_backgroundview, container, false);
|
||||
|
||||
((Button)mainView.findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
}
|
||||
});
|
||||
|
||||
ToastUtils.show(String.format("%s onCreate", TAG));
|
||||
return mainView;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
package cc.winboll.studio.powerbell.unittest;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.nfc.tech.TagTechnology;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -27,157 +18,22 @@ import java.io.File;
|
||||
public class MainUnitTestActivity extends AppCompatActivity {
|
||||
|
||||
public static final String TAG = "MainUnitTestActivity";
|
||||
public static final int REQUEST_CROP_IMAGE = 0;
|
||||
// 新增:权限请求码
|
||||
public static final int REQUEST_STORAGE_PERMISSION = 1001;
|
||||
View mainView;
|
||||
BackgroundSourceUtils mBgSourceUtils;
|
||||
BackgroundView mBackgroundView;
|
||||
// 测试图片路径(用Environment获取,适配低版本,避免硬编码)
|
||||
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/1764946782079.jpeg";
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
mBgSourceUtils.loadSettings();
|
||||
|
||||
// 非调试状态就退出
|
||||
if (!GlobalApplication.isDebugging()) {
|
||||
finish();
|
||||
}
|
||||
setContentView(R.layout.activity_mainunittest);
|
||||
|
||||
mBackgroundView = findViewById(R.id.backgroundview);
|
||||
|
||||
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
|
||||
}
|
||||
});
|
||||
|
||||
// 裁剪测试按钮点击事件(新增权限校验)
|
||||
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ToastUtils.show("onClick:准备启动裁剪");
|
||||
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
|
||||
|
||||
// 修复1:移除高版本API依赖,适配低版本存储权限校验
|
||||
if (checkStoragePermission()) {
|
||||
// 权限已授予,启动裁剪
|
||||
startCropTest();
|
||||
} else {
|
||||
// 权限未授予,申请权限
|
||||
requestStoragePermission();
|
||||
}
|
||||
}
|
||||
});
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG);
|
||||
fragmentTransaction.commit();
|
||||
|
||||
ToastUtils.show(String.format("%s onCreate", TAG));
|
||||
// 加载测试图片(验证图片路径是否有效)
|
||||
loadBackground();
|
||||
ToastUtils.show(String.format("%s onCreate", TAG));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
|
||||
*/
|
||||
private void startCropTest() {
|
||||
// 修复2:输出路径用Environment获取,确保目录存在(避免路径无效)
|
||||
File outputDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/");
|
||||
if (!outputDir.exists()) {
|
||||
outputDir.mkdirs(); // 创建目录(避免输出路径不存在导致裁剪失败)
|
||||
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
|
||||
}
|
||||
String dstOutputPath = outputDir.getAbsolutePath()
|
||||
+ "/1764946782079-crop.jpeg";
|
||||
|
||||
// 修复3:自由裁剪时比例传0(避免100:100过大导致机型崩溃)
|
||||
ImageCropUtils.startImageCrop(
|
||||
MainUnitTestActivity.this,
|
||||
new File(szTestSource),
|
||||
new File(dstOutputPath),
|
||||
0, // 自由裁剪传0
|
||||
0, // 自由裁剪传0
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验存储读写权限(适配Android 6.0+ 低版本SDK,移除TIRAMISU依赖)
|
||||
*/
|
||||
private boolean checkStoragePermission() {
|
||||
// 适配Android 6.0(API 23)及以上,用通用的读写权限(移除高版本API)
|
||||
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请存储读写权限(适配低版本SDK,移除READ_MEDIA_IMAGES依赖)
|
||||
*/
|
||||
private void requestStoragePermission() {
|
||||
LogUtils.d(TAG, "【裁剪测试】申请存储读写权限");
|
||||
// 用通用的读写权限(适配所有Android 6.0+ 机型,无高版本依赖)
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
REQUEST_STORAGE_PERMISSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限申请回调
|
||||
*/
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_STORAGE_PERMISSION) {
|
||||
// 校验权限是否授予
|
||||
boolean allGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allGranted) {
|
||||
ToastUtils.show("存储权限已授予,启动裁剪");
|
||||
startCropTest(); // 权限授予后启动裁剪
|
||||
} else {
|
||||
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
|
||||
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "【裁剪回调】requestCode:" + requestCode + ",resultCode:" + resultCode + ",data:" + (data == null ? "null" : data.toString()));
|
||||
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null));
|
||||
// 裁剪完成后回收权限
|
||||
if (requestCode == REQUEST_CROP_IMAGE) {
|
||||
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/1764946782079-crop.jpeg";
|
||||
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
|
||||
//ImageCropUtils.releaseCropPermission(this, outputUri);
|
||||
mBackgroundView.loadImage(dstOutputPath);
|
||||
}
|
||||
}
|
||||
|
||||
void loadBackground() {
|
||||
// 校验测试图片是否存在(避免路径错误)
|
||||
File testFile = new File(szTestSource);
|
||||
if (testFile.exists() && testFile.length() > 100) {
|
||||
mBackgroundView.loadImage(szTestSource);
|
||||
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource);
|
||||
} else {
|
||||
ToastUtils.show("测试图片不存在或无效");
|
||||
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AppCacheUtils {
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.beans.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.beans.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
@@ -79,7 +79,7 @@ public class AppConfigUtils {
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
MainActivity.relaodAppConfigs();
|
||||
MainViewFragment.relaodAppConfigs();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -183,7 +183,7 @@ public class AppConfigUtils {
|
||||
@Override
|
||||
public void onNo() {
|
||||
AppConfigUtils.getInstance(activity).loadAppConfigBean();
|
||||
MainActivity.relaodAppConfigs();
|
||||
MainViewFragment.relaodAppConfigs();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -198,6 +198,6 @@ public class AppConfigUtils {
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
// 通知活动窗口和服务配置已更新
|
||||
ControlCenterService.updateStatus(mContext, mAppConfigBean);
|
||||
MainActivity.relaodAppConfigs();
|
||||
MainViewFragment.relaodAppConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 09:14
|
||||
* @Describe Assets 目录拷贝工具类
|
||||
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
|
||||
*/
|
||||
public class AssetsCopyUtils {
|
||||
public static final String TAG = "AssetsCopyUtils";
|
||||
private static final int BUFFER_SIZE = 1024 * 8;
|
||||
|
||||
/**
|
||||
* 拷贝 assets/images/ 目录到指定目标目录
|
||||
* @param context 上下文
|
||||
* @param targetDirPath 目标目录完整路径(如 /sdcard/PowerBell/assets_images)
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
|
||||
// 拷贝 assets/images 根目录
|
||||
return copyAssetsDirToDir(context, "images", targetDirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归拷贝 assets 下指定目录到目标目录
|
||||
* @param context 上下文
|
||||
* @param assetsDir assets 下的源目录(如 "images"、"images/subdir")
|
||||
* @param targetDirPath 目标目录完整路径
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) {
|
||||
File targetDir = new File(targetDirPath);
|
||||
// 创建目标目录(含多级父目录)
|
||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||
Log.e(TAG, "创建目标目录失败:" + targetDirPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取 assets 目录下的文件/子目录列表
|
||||
String[] fileList = context.getAssets().list(assetsDir);
|
||||
if (fileList == null || fileList.length == 0) {
|
||||
Log.d(TAG, "assets 目录为空:" + assetsDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String fileName : fileList) {
|
||||
String assetsFilePath = assetsDir + File.separator + fileName;
|
||||
String targetFilePath = targetDirPath + File.separator + fileName;
|
||||
|
||||
// 判断当前项是文件还是子目录
|
||||
String[] subFileList = context.getAssets().list(assetsFilePath);
|
||||
if (subFileList != null && subFileList.length > 0) {
|
||||
// 是子目录,递归拷贝
|
||||
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 是文件,直接拷贝
|
||||
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝 assets 下单个文件到指定路径
|
||||
* @param context 上下文
|
||||
* @param assetsFilePath assets 下的文件路径(如 "images/cloud.png")
|
||||
* @param targetFilePath 目标文件完整路径
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) {
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetsFilePath);
|
||||
File targetFile = new File(targetFilePath);
|
||||
// 覆盖已存在的文件
|
||||
if (targetFile.exists() && !targetFile.delete()) {
|
||||
Log.w(TAG, "覆盖目标文件失败,跳过:" + targetFilePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
outputStream = new FileOutputStream(targetFile);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭流
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
if (outputStream != null) outputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "关闭流异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 12:07:20
|
||||
* @Describe 背景图片工具集
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
|
||||
import java.io.File;
|
||||
|
||||
public class BackgroundPictureUtils {
|
||||
|
||||
public static final String TAG = "BackgroundPictureUtils";
|
||||
|
||||
static BackgroundPictureUtils _mBackgroundPictureUtils;
|
||||
Context mContext;
|
||||
BackgroundPictureBean mBackgroundPictureBean;
|
||||
// 背景图片目录
|
||||
String mszBackgroundDir;
|
||||
|
||||
BackgroundPictureUtils(Context context) {
|
||||
mContext = context;
|
||||
String szExternalFilesDir = mContext.getExternalFilesDir(TAG) + File.separator;
|
||||
setBackgroundDir(szExternalFilesDir + "Background" + File.separator);
|
||||
loadBackgroundPictureBean();
|
||||
}
|
||||
|
||||
public static BackgroundPictureUtils getInstance(Context context) {
|
||||
if (_mBackgroundPictureUtils == null) {
|
||||
_mBackgroundPictureUtils = new BackgroundPictureUtils(context);
|
||||
}
|
||||
return _mBackgroundPictureUtils;
|
||||
}
|
||||
|
||||
//
|
||||
// 加载应用背景图片配置数据
|
||||
//
|
||||
public BackgroundPictureBean loadBackgroundPictureBean() {
|
||||
mBackgroundPictureBean = BackgroundPictureBean.loadBean(mContext, BackgroundPictureBean.class);
|
||||
if (mBackgroundPictureBean == null) {
|
||||
mBackgroundPictureBean = new BackgroundPictureBean();
|
||||
BackgroundPictureBean.saveBean(mContext, mBackgroundPictureBean);
|
||||
}
|
||||
return mBackgroundPictureBean;
|
||||
}
|
||||
|
||||
|
||||
void setBackgroundDir(String mszBackgroundDir) {
|
||||
this.mszBackgroundDir = mszBackgroundDir;
|
||||
}
|
||||
|
||||
public String getBackgroundDir() {
|
||||
return mszBackgroundDir;
|
||||
}
|
||||
|
||||
public BackgroundPictureBean getBackgroundPictureBean() {
|
||||
return mBackgroundPictureBean;
|
||||
}
|
||||
|
||||
public void saveData() {
|
||||
BackgroundPictureBean.saveBean(mContext, mBackgroundPictureBean);
|
||||
}
|
||||
}
|
||||
@@ -1,696 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.BuildConfig;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import java.io.BufferedOutputStream;
|
||||
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.util.UUID;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 12:07:20
|
||||
* @Describe 背景图片工具集(精简版:复用FileUtils,聚焦业务逻辑)
|
||||
*/
|
||||
public class BackgroundSourceUtils {
|
||||
|
||||
public static final String TAG = "BackgroundSourceUtils";
|
||||
// 裁剪相关常量(统一定义,避免硬编码)
|
||||
private static final String CROP_CACHE_DIR_NAME = "cache";
|
||||
private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg";
|
||||
private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg";
|
||||
public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider";
|
||||
// 图片操作基础目录
|
||||
private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell";
|
||||
private static final String SOURCE_DIR_NAME = "BackgroundSource";
|
||||
private static final String COMPRESS_DIR_NAME = "BackgroundCompress";
|
||||
|
||||
// 单例相关
|
||||
private static volatile BackgroundSourceUtils sInstance;
|
||||
private Context mContext;
|
||||
private File currentBackgroundBeanFile;
|
||||
private BackgroundBean currentBackgroundBean;
|
||||
private File previewBackgroundBeanFile;
|
||||
private BackgroundBean previewBackgroundBean;
|
||||
|
||||
// 目录文件相关
|
||||
private File fPictureBaseDir;
|
||||
private File fCropCacheDir;
|
||||
private File fBackgroundSourceDir;
|
||||
private File fBackgroundCompressDir;
|
||||
private File fUtilsDir;
|
||||
private File fModelDir;
|
||||
private File mCropSourceFile;
|
||||
private File mCropResultFile;
|
||||
|
||||
// 双重校验锁单例
|
||||
private BackgroundSourceUtils(Context context) {
|
||||
if (sInstance != null) {
|
||||
throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!");
|
||||
}
|
||||
this.mContext = context.getApplicationContext();
|
||||
initNecessaryDirs();
|
||||
initAllFiles();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
public static BackgroundSourceUtils getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
synchronized (BackgroundSourceUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new BackgroundSourceUtils(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一初始化所有必要目录
|
||||
*/
|
||||
private void initNecessaryDirs() {
|
||||
LogUtils.d(TAG, "【目录初始化】开始创建所有必要目录");
|
||||
initPictureDirs();
|
||||
initJsonDirs();
|
||||
LogUtils.d(TAG, "【目录初始化】所有必要目录创建完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化图片操作目录
|
||||
*/
|
||||
private void initPictureDirs() {
|
||||
fPictureBaseDir = new File(PICTURE_BASE_DIR);
|
||||
fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME);
|
||||
fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME);
|
||||
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME);
|
||||
|
||||
createDirWithPermission(fPictureBaseDir, "图片基础目录");
|
||||
createDirWithPermission(fBackgroundSourceDir, "图片存储目录");
|
||||
createDirWithPermission(fCropCacheDir, "裁剪缓存目录");
|
||||
createDirWithPermission(fBackgroundCompressDir, "压缩图存储目录");
|
||||
|
||||
validatePictureDirs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化JSON配置目录
|
||||
*/
|
||||
private void initJsonDirs() {
|
||||
fUtilsDir = mContext.getExternalFilesDir(TAG);
|
||||
if (fUtilsDir == null) {
|
||||
LogUtils.e(TAG, "应用外置存储不可用,切换到应用内部缓存目录");
|
||||
fUtilsDir = mContext.getDataDir();
|
||||
}
|
||||
fModelDir = new File(fUtilsDir, "ModelDir");
|
||||
createDirWithPermission(fModelDir, "JSON配置目录");
|
||||
|
||||
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
|
||||
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录并校验
|
||||
*/
|
||||
private void createDirWithPermission(File dir, String dirDesc) {
|
||||
if (dir == null) {
|
||||
LogUtils.e(TAG, dirDesc + "创建失败:目录对象为null");
|
||||
return;
|
||||
}
|
||||
if (!dir.exists()) {
|
||||
LogUtils.d(TAG, dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
|
||||
dir.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验图片目录是否就绪
|
||||
*/
|
||||
private void validatePictureDirs() {
|
||||
boolean allReady = fPictureBaseDir.exists() && fBackgroundSourceDir.exists()
|
||||
&& fCropCacheDir.exists() && fBackgroundCompressDir.exists();
|
||||
if (allReady) {
|
||||
LogUtils.d(TAG, "所有图片目录均已就绪");
|
||||
} else {
|
||||
LogUtils.e(TAG, "部分图片目录未就绪,可能影响后续功能");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有文件
|
||||
*/
|
||||
private void initAllFiles() {
|
||||
clearCropTempFiles();
|
||||
LogUtils.d(TAG, "文件初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将File转为ContentUri
|
||||
*/
|
||||
public Uri getFileProviderUri(File file) {
|
||||
LogUtils.d(TAG, "【getFileProviderUri调用】文件路径:" + file.getAbsolutePath());
|
||||
Uri contentUri = null;
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
contentUri = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file);
|
||||
LogUtils.d(TAG, "7.0+ 生成ContentUri:" + contentUri.toString());
|
||||
} else {
|
||||
contentUri = Uri.fromFile(file);
|
||||
LogUtils.d(TAG, "7.0以下 生成FileUri:" + contentUri.toString());
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "生成Uri失败:" + e.getMessage(), e);
|
||||
contentUri = null;
|
||||
}
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查背景是否为空并创建空白背景Bean
|
||||
*/
|
||||
boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) {
|
||||
LogUtils.d(TAG, "【checkEmptyBackgroundAndCreateBlankBackgroundBean调用】开始检查背景Bean");
|
||||
File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath());
|
||||
if (!fCheckBackgroundFile.exists()) {
|
||||
String newCropFileName = "blank10x10";
|
||||
String fileSuffix = "png";
|
||||
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
|
||||
|
||||
AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank10x10.png", mCropSourceFile.getAbsolutePath());
|
||||
try {
|
||||
mCropResultFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
previewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(false);
|
||||
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
|
||||
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
|
||||
saveSettings();
|
||||
LogUtils.d(TAG, "背景Bean为空,已创建空白背景并更新配置");
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "背景Bean文件存在,无需创建空白背景");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并更新预览剪裁环境
|
||||
*/
|
||||
public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) {
|
||||
LogUtils.d(TAG, "【createAndUpdatePreviewEnvironmentForCropping调用】开始初始化预览剪裁环境");
|
||||
InputStream is = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
clearCropTempFiles();
|
||||
if (checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) {
|
||||
LogUtils.d(TAG, "空白背景创建成功,直接返回");
|
||||
return true;
|
||||
}
|
||||
|
||||
Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
|
||||
String fileSuffix = FileUtils.getFileSuffix(mContext, uri);
|
||||
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
|
||||
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
|
||||
|
||||
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) {
|
||||
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile);
|
||||
} else {
|
||||
mCropResultFile.createNewFile();
|
||||
}
|
||||
|
||||
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) {
|
||||
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile);
|
||||
} else {
|
||||
mCropSourceFile.createNewFile();
|
||||
is = mContext.getContentResolver().openInputStream(uri);
|
||||
if (is == null) {
|
||||
LogUtils.e(TAG, "ContentResolver打开Uri失败:" + uri.toString());
|
||||
return false;
|
||||
}
|
||||
fos = new FileOutputStream(mCropSourceFile);
|
||||
byte[] buffer = new byte[1024 * 8];
|
||||
int readLen;
|
||||
while ((readLen = is.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, readLen);
|
||||
}
|
||||
fos.flush();
|
||||
try {
|
||||
fos.getFD().sync();
|
||||
} catch (IOException e) {
|
||||
LogUtils.w(TAG, "文件同步到磁盘失败,flush兜底:" + e.getMessage());
|
||||
fos.flush();
|
||||
}
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
|
||||
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
|
||||
saveSettings();
|
||||
|
||||
LogUtils.d(TAG, "预览剪裁环境初始化成功");
|
||||
LogUtils.d(TAG, "→ 原Uri:" + uri.toString());
|
||||
LogUtils.d(TAG, "→ 剪裁数据源:" + mCropSourceFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "→ 剪裁结果文件:" + mCropResultFile.getAbsolutePath());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "预览剪裁环境初始化异常:" + e.getMessage(), e);
|
||||
clearCropTempFiles();
|
||||
return false;
|
||||
} finally {
|
||||
if (is != null) {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载背景配置
|
||||
*/
|
||||
public void loadSettings() {
|
||||
currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
|
||||
if (currentBackgroundBean == null) {
|
||||
currentBackgroundBean = new BackgroundBean();
|
||||
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
|
||||
LogUtils.d(TAG, "正式背景Bean不存在,已创建新实例");
|
||||
}
|
||||
|
||||
previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
|
||||
if (previewBackgroundBean == null) {
|
||||
previewBackgroundBean = new BackgroundBean();
|
||||
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
|
||||
LogUtils.d(TAG, "预览背景Bean不存在,已创建新实例");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------ 对外提供的核心方法 ------------------------------
|
||||
public BackgroundBean getCurrentBackgroundBean() {
|
||||
return currentBackgroundBean;
|
||||
}
|
||||
|
||||
public BackgroundBean getPreviewBackgroundBean() {
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
|
||||
public String getPreviewBackgroundScaledCompressFilePath() {
|
||||
String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
|
||||
if (TextUtils.isEmpty(compressFileName)) {
|
||||
LogUtils.e(TAG, "预览压缩背景文件名为空");
|
||||
return "";
|
||||
}
|
||||
File file = new File(fBackgroundCompressDir, compressFileName);
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getCurrentBackgroundScaledCompressFilePath() {
|
||||
String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName();
|
||||
if (TextUtils.isEmpty(compressFileName)) {
|
||||
LogUtils.e(TAG, "正式压缩背景文件名为空");
|
||||
return "";
|
||||
}
|
||||
File file = new File(fBackgroundCompressDir, compressFileName);
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
*/
|
||||
public void saveSettings() {
|
||||
if (currentBackgroundBean != null && previewBackgroundBean != null) {
|
||||
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
|
||||
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
|
||||
LogUtils.d(TAG, "两份背景配置保存成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "配置保存失败:current/preview Bean存在空值");
|
||||
}
|
||||
}
|
||||
|
||||
public String getBackgroundSourceDirPath() {
|
||||
return fBackgroundSourceDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getBackgroundCompressDirPath() {
|
||||
return fBackgroundCompressDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getCropCacheDir() {
|
||||
return fCropCacheDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getFileProviderAuthority() {
|
||||
return FILE_PROVIDER_AUTHORITY;
|
||||
}
|
||||
|
||||
// ------------------------------ 核心业务方法 ------------------------------
|
||||
/**
|
||||
* 保存裁剪结果图到预览Bean
|
||||
*/
|
||||
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
|
||||
LogUtils.d(TAG, "【saveFileToPreviewBean调用】源文件路径:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
|
||||
if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "源文件无效,拒绝保存");
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
|
||||
String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath();
|
||||
if (sourceFile.getAbsolutePath().contains(originalImageDir)) {
|
||||
LogUtils.w(TAG, "禁止复制原图,跳过保存");
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
|
||||
if (!fBackgroundSourceDir.exists() && !fBackgroundSourceDir.mkdirs()) {
|
||||
LogUtils.e(TAG, "BackgroundSource目录创建失败");
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
|
||||
String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName();
|
||||
File targetFile = new File(fBackgroundSourceDir, uniqueFileName);
|
||||
if (FileUtils.copyFile(sourceFile, targetFile)) {
|
||||
LogUtils.d(TAG, "裁剪结果图保存成功:" + targetFile.getAbsolutePath());
|
||||
previewBackgroundBean.setBackgroundFileName(uniqueFileName);
|
||||
previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath());
|
||||
previewBackgroundBean.setBackgroundFileInfo(fileInfo);
|
||||
previewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
saveSettings();
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪结果图复制失败");
|
||||
}
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交预览背景到正式背景
|
||||
*/
|
||||
public void commitPreviewSourceToCurrent() {
|
||||
LogUtils.d(TAG, "【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean");
|
||||
currentBackgroundBean = new BackgroundBean();
|
||||
currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName());
|
||||
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath());
|
||||
currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo());
|
||||
currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile());
|
||||
currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName());
|
||||
currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath());
|
||||
currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile());
|
||||
currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth());
|
||||
currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight());
|
||||
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
|
||||
|
||||
String previewFileName = previewBackgroundBean.getBackgroundFileName();
|
||||
String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
|
||||
File previewFile = new File(previewBackgroundBean.getBackgroundFilePath());
|
||||
File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath());
|
||||
File currentFile = new File(fBackgroundSourceDir, previewFileName);
|
||||
File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName);
|
||||
FileUtils.copyFile(previewFile, currentFile);
|
||||
FileUtils.copyFile(previewCropFile, currentCropFile);
|
||||
currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath());
|
||||
currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath());
|
||||
|
||||
saveSettings();
|
||||
LogUtils.d(TAG, "预览背景提交到正式背景成功,两份实例完全独立");
|
||||
ToastUtils.show("背景图片应用成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将正式背景同步到预览背景
|
||||
*/
|
||||
public void setCurrentSourceToPreview() {
|
||||
LogUtils.d(TAG, "【setCurrentSourceToPreview调用】开始深拷贝正式Bean到预览Bean");
|
||||
previewBackgroundBean = new BackgroundBean();
|
||||
previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName());
|
||||
previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath());
|
||||
previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo());
|
||||
previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath());
|
||||
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile());
|
||||
previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth());
|
||||
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
|
||||
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
|
||||
|
||||
saveSettings();
|
||||
LogUtils.d(TAG, "正式背景同步到预览背景成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理单个旧文件
|
||||
*/
|
||||
private void clearOldFile(File file, String fileDesc) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
LogUtils.d(TAG, fileDesc + "已删除");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理裁剪临时文件
|
||||
*/
|
||||
void clearCropTempFiles() {
|
||||
File[] files = fCropCacheDir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
clearOldFile(file, "旧裁剪缓存文件:" + file.getAbsolutePath());
|
||||
}
|
||||
mCropSourceFile = null;
|
||||
mCropResultFile = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件
|
||||
*/
|
||||
public boolean copyFile(File source, File target) {
|
||||
LogUtils.d(TAG, "【copyFile调用】源文件:" + (source != null ? source.getAbsolutePath() : "null") + " 目标:" + (target != null ? target.getAbsolutePath() : "null"));
|
||||
if (source == null || TextUtils.isEmpty(source.getPath()) || (source.exists() && source.length() <= 0)) {
|
||||
if (target == null) {
|
||||
LogUtils.e(TAG, "目录创建失败:目标对象为null");
|
||||
return false;
|
||||
}
|
||||
File targetDir = target.isFile() ? target.getParentFile() : target;
|
||||
createDirWithPermission(targetDir, "空源文件场景-目录创建");
|
||||
LogUtils.d(TAG, "空源文件场景,目录创建完成");
|
||||
return true;
|
||||
}
|
||||
return FileUtils.copyFile(source, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录类型描述
|
||||
*/
|
||||
public String getDirTypeDesc(File dir) {
|
||||
if (dir == null) {
|
||||
return "未知目录(null)";
|
||||
}
|
||||
String dirPath = dir.getAbsolutePath();
|
||||
String publicPicturePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath();
|
||||
String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : "";
|
||||
String cachePath = mContext.getCacheDir().getAbsolutePath();
|
||||
|
||||
if (!TextUtils.isEmpty(publicPicturePath)) {
|
||||
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) {
|
||||
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCompress,压缩图统一存储目录)";
|
||||
} else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
|
||||
return "系统公共图片目录(/Pictures/PowerBell,图片存储/裁剪目录)";
|
||||
}
|
||||
} else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) {
|
||||
return "应用私有外部目录(getExternalFilesDir(),JSON配置目录)";
|
||||
} else if (dirPath.contains(cachePath)) {
|
||||
return "应用内部缓存目录(getCacheDir(),兜底目录)";
|
||||
} else {
|
||||
return "外部存储目录(非应用私有,权限受限)";
|
||||
}
|
||||
return "未知目录";
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移旧压缩图路径到新目录
|
||||
*/
|
||||
private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) {
|
||||
LogUtils.d(TAG, "【migrateCompressPathToNewDir调用】开始迁移" + (isCurrentBean ? "正式" : "预览") + "Bean压缩路径");
|
||||
String oldCompressPath = bean.getBackgroundScaledCompressFilePath();
|
||||
String beanType = isCurrentBean ? "正式Bean" : "预览Bean";
|
||||
|
||||
if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) {
|
||||
LogUtils.d(TAG, beanType + "无需迁移:旧路径为空或已在目标目录");
|
||||
return;
|
||||
}
|
||||
|
||||
File oldCompressFile = new File(oldCompressPath);
|
||||
if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) {
|
||||
LogUtils.w(TAG, beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath);
|
||||
String compressFileName = bean.getBackgroundScaledCompressFileName();
|
||||
if (!TextUtils.isEmpty(compressFileName)) {
|
||||
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
|
||||
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
|
||||
saveSettings();
|
||||
LogUtils.d(TAG, beanType + "压缩路径已重置到目标目录");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String compressFileName = bean.getBackgroundScaledCompressFileName();
|
||||
if (TextUtils.isEmpty(compressFileName)) {
|
||||
compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg";
|
||||
}
|
||||
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
|
||||
|
||||
boolean copySuccess = FileUtils.copyFile(oldCompressFile, newCompressFile);
|
||||
if (copySuccess) {
|
||||
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
|
||||
saveSettings();
|
||||
clearOldFile(oldCompressFile, beanType + "旧压缩文件(迁移后清理)");
|
||||
LogUtils.d(TAG, beanType + "压缩路径迁移成功:" + oldCompressPath + " → " + newCompressFile.getAbsolutePath());
|
||||
} else {
|
||||
LogUtils.e(TAG, beanType + "压缩文件复制失败,迁移终止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片旋转角度
|
||||
*/
|
||||
public int getImageRotateAngle(String imagePath) {
|
||||
LogUtils.d(TAG, "【getImageRotateAngle调用】图片路径:" + imagePath);
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "图片路径为空");
|
||||
return 0;
|
||||
}
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "图片文件无效:" + imagePath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(imageFile);
|
||||
ExifInterface exifInterface = new ExifInterface(inputStream);
|
||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
return 270;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.w(TAG, "读取EXIF异常:" + e.getMessage());
|
||||
return 0;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "流关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片并保存(默认路径)
|
||||
*/
|
||||
public void compressQualityToRecivedPicture(Bitmap bitmap) {
|
||||
LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】使用默认路径压缩图片");
|
||||
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
|
||||
compressQualityToRecivedPicture(bitmap, defaultCompressPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片并保存(指定路径)
|
||||
*/
|
||||
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
|
||||
LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】指定路径压缩图片,目标路径:" + targetCompressPath);
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
ToastUtils.show("压缩失败:图片为空");
|
||||
LogUtils.e(TAG, "Bitmap为空或已回收");
|
||||
return;
|
||||
}
|
||||
|
||||
OutputStream outStream = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
LogUtils.d(TAG, "Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB");
|
||||
File targetCompressFile = new File(targetCompressPath);
|
||||
if (targetCompressFile.exists()) {
|
||||
targetCompressFile.delete();
|
||||
}
|
||||
targetCompressFile.createNewFile();
|
||||
|
||||
fos = new FileOutputStream(targetCompressFile);
|
||||
outStream = new BufferedOutputStream(fos);
|
||||
boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
|
||||
outStream.flush();
|
||||
try {
|
||||
fos.getFD().sync();
|
||||
LogUtils.d(TAG, "图片已强制同步到磁盘");
|
||||
} catch (IOException e) {
|
||||
LogUtils.w(TAG, "sync失败,flush兜底:" + e.getMessage());
|
||||
outStream.flush();
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "图片压缩" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB");
|
||||
ToastUtils.show(compressSuccess ? "图片压缩成功" : "图片压缩失败");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "图片压缩IO异常:" + e.getMessage(), e);
|
||||
ToastUtils.show("图片压缩失败");
|
||||
} finally {
|
||||
if (outStream != null) {
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "BufferedOutputStream关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "FileOutputStream关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 01:57
|
||||
* @Describe 单例 Bitmap 缓存工具类(Java 7 兼容)
|
||||
* 功能:内存缓存 Bitmap,支持路径关联缓存、全局获取、缓存清空
|
||||
* 特点:1. 单例模式 2. 压缩加载避免OOM 3. 路径- Bitmap 映射 4. 线程安全
|
||||
*/
|
||||
public class BitmapCacheUtils {
|
||||
public static final String TAG = "BitmapCacheUtils";
|
||||
// 最大图片尺寸(适配1080P屏幕,可根据需求调整)
|
||||
private static final int MAX_WIDTH = 1080;
|
||||
private static final int MAX_HEIGHT = 1920;
|
||||
|
||||
// 单例实例(volatile 保证多线程可见性)
|
||||
private static volatile BitmapCacheUtils sInstance;
|
||||
// 路径-Bitmap 缓存容器(内存缓存)
|
||||
private final Map<String, Bitmap> mBitmapCacheMap;
|
||||
|
||||
// 私有构造器(单例模式)
|
||||
private BitmapCacheUtils() {
|
||||
mBitmapCacheMap = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,线程安全)
|
||||
*/
|
||||
public static BitmapCacheUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (BitmapCacheUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new BitmapCacheUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心接口:根据图片路径缓存 Bitmap 到内存
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存成功的 Bitmap / null(路径无效/文件不存在/解码失败)
|
||||
*/
|
||||
public Bitmap cacheBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片路径为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片文件无效(不存在/非文件/空文件) - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已缓存则直接返回,避免重复加载
|
||||
if (mBitmapCacheMap.containsKey(imagePath)) {
|
||||
Bitmap cachedBitmap = mBitmapCacheMap.get(imagePath);
|
||||
// 额外校验缓存的Bitmap是否有效
|
||||
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath);
|
||||
return cachedBitmap;
|
||||
} else {
|
||||
// 缓存的Bitmap已失效,移除后重新加载
|
||||
mBitmapCacheMap.remove(imagePath);
|
||||
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效,移除后重新加载 - " + imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩加载 Bitmap(避免OOM)
|
||||
Bitmap bitmap = decodeCompressedBitmap(imagePath);
|
||||
if (bitmap != null) {
|
||||
// 存入缓存容器
|
||||
mBitmapCacheMap.put(imagePath, bitmap);
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功 - " + imagePath);
|
||||
} else {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心接口:根据路径获取缓存的 Bitmap
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存的有效 Bitmap / null(未缓存/已回收)
|
||||
*/
|
||||
public Bitmap getCachedBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return null;
|
||||
}
|
||||
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
|
||||
// 校验Bitmap是否有效
|
||||
if (bitmap != null && bitmap.isRecycled()) {
|
||||
mBitmapCacheMap.remove(imagePath);
|
||||
return null;
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 Bitmap 缓存(释放内存)
|
||||
*/
|
||||
public void clearAllCache() {
|
||||
synchronized (mBitmapCacheMap) {
|
||||
for (Bitmap bitmap : mBitmapCacheMap.values()) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle(); // 主动回收 Bitmap
|
||||
}
|
||||
}
|
||||
mBitmapCacheMap.clear();
|
||||
}
|
||||
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定路径的 Bitmap 缓存
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void removeCachedBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return;
|
||||
}
|
||||
synchronized (mBitmapCacheMap) {
|
||||
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩解码 Bitmap(按最大尺寸缩放,避免OOM)
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 解码后的 Bitmap / null(文件无效/解码失败)
|
||||
*/
|
||||
private Bitmap decodeCompressedBitmap(String imagePath) {
|
||||
// 前置校验:确保文件有效
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
// 第一步:只获取图片尺寸,不加载像素
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(imagePath, options);
|
||||
|
||||
// 校验尺寸是否有效
|
||||
if (options.outWidth <= 0 || options.outHeight <= 0) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 图片尺寸无效 - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算缩放比例
|
||||
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
|
||||
|
||||
// 第二步:加载压缩后的 Bitmap
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存(比ARGB_8888少一半内存)
|
||||
options.inPurgeable = true;
|
||||
options.inInputShareable = true;
|
||||
|
||||
try {
|
||||
return BitmapFactory.decodeFile(imagePath, options);
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Bitmap 缩放比例
|
||||
*/
|
||||
private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
|
||||
int rawWidth = options.outWidth;
|
||||
int rawHeight = options.outHeight;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (rawWidth > maxWidth || rawHeight > maxHeight) {
|
||||
int halfWidth = rawWidth / 2;
|
||||
int halfHeight = rawHeight / 2;
|
||||
while ((halfWidth / inSampleSize) >= maxWidth && (halfHeight / inSampleSize) >= maxHeight) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/08 21:11
|
||||
* @Describe 把 R.drawable 中的图片保存为 File 对象的工具类
|
||||
* 适配 PowerBell 项目:支持指定保存路径、自动创建目录、处理PNG图片压缩
|
||||
*/
|
||||
public class DrawableToFileUtils {
|
||||
private static final String TAG = "DrawableToFileUtils";
|
||||
|
||||
/**
|
||||
* 核心方法:将 R.drawable 图片保存为 File 对象
|
||||
* @param context 上下文(用于获取 Resources)
|
||||
* @param drawableResId 图片资源ID(如 R.drawable.ic_test_png)
|
||||
* @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.png")
|
||||
* @return 保存成功返回 File 对象,失败返回 null
|
||||
*/
|
||||
public static File saveDrawableToFile(Context context, int drawableResId, String filePath) {
|
||||
// 1. 校验参数(避免空指针/无效参数)
|
||||
if (context == null || drawableResId == 0 || filePath == null || filePath.isEmpty()) {
|
||||
LogUtils.e(TAG, "【保存失败】参数无效(context为空/资源ID为0/文件名为空)");
|
||||
return null;
|
||||
}
|
||||
if (!filePath.endsWith(".png")) {
|
||||
filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确
|
||||
LogUtils.d(TAG, "【格式适配】自动添加.png后缀,最终文件名:" + filePath);
|
||||
}
|
||||
|
||||
// 3. 构建目标 File 对象(最终保存的文件路径)
|
||||
File targetFile = new File(filePath);
|
||||
LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath());
|
||||
|
||||
// 4. 读取 drawable 资源为 Bitmap(处理高清图/缩放问题)
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
// 读取 drawable 资源(适配不同分辨率的图片,避免变形)
|
||||
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "【读取失败】无法读取drawable资源(资源ID:" + drawableResId + ")");
|
||||
return null;
|
||||
}
|
||||
LogUtils.d(TAG, "【读取成功】drawable资源转Bitmap成功(宽:" + bitmap.getWidth() + ",高:" + bitmap.getHeight() + ")");
|
||||
|
||||
// 5. 将 Bitmap 写入 File(PNG格式,无损保存)
|
||||
FileOutputStream fos = new FileOutputStream(targetFile);
|
||||
// 压缩参数:PNG格式,质量100(无损),写入输出流
|
||||
boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.flush(); // 刷新输出流
|
||||
fos.close(); // 关闭输出流
|
||||
|
||||
// 6. 校验保存结果(文件是否存在且有效)
|
||||
if (isSaved && targetFile.exists() && targetFile.length() > 100) {
|
||||
LogUtils.d(TAG, "【保存成功】drawable图片保存为File:" + targetFile.getAbsolutePath());
|
||||
return targetFile; // 保存成功,返回File对象
|
||||
} else {
|
||||
LogUtils.e(TAG, "【保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)");
|
||||
// 保存失败,删除无效文件
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete();
|
||||
LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【保存异常】写入文件时出错:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e));
|
||||
return null;
|
||||
} finally {
|
||||
// 回收Bitmap资源(避免内存溢出)
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
LogUtils.d(TAG, "【资源回收】Bitmap资源已回收");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:自定义保存路径(灵活适配不同场景)
|
||||
* @param context 上下文
|
||||
* @param drawableResId 图片资源ID
|
||||
* @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/")
|
||||
* @param fileName 保存的文件名(带.png后缀)
|
||||
* @return 保存成功返回File对象,失败返回null
|
||||
*/
|
||||
public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) {
|
||||
File filePath = new File(saveDirPath, fileName);
|
||||
return saveDrawableToFile(context, drawableResId, filePath.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,286 +1,176 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* 文件操作工具类
|
||||
* 功能:文件读写、复制、图片转换、文件名处理等常用文件操作
|
||||
* 适配:Java 7+,支持Android全版本
|
||||
* 注意:调用文件操作前需确保已获取存储权限(Android 6.0+ 需动态申请)
|
||||
* 文件读取工具类
|
||||
*/
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
/** 日志标签 */
|
||||
public static final String TAG = "FileUtils";
|
||||
/** 读取文件默认缓冲区大小(10KB) */
|
||||
private static final int BUFFER_SIZE = 10240;
|
||||
/** 最大读取文件大小(1GB),防止OOM */
|
||||
private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024;
|
||||
|
||||
// ====================================== 文件读取相关 ======================================
|
||||
|
||||
/**
|
||||
* 读取文件内容并转为字符串
|
||||
* @param filePath 文件绝对路径(非空)
|
||||
* @return 文件内容字符串
|
||||
* @throws IOException 异常:文件不存在、文件过大、读取失败等
|
||||
*/
|
||||
//
|
||||
// 读取文件内容,作为字符串返回
|
||||
//
|
||||
public static String readFileAsString(String filePath) throws IOException {
|
||||
// 1. 校验文件合法性
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("文件不存在:" + filePath);
|
||||
}
|
||||
if (file.length() > MAX_READ_FILE_SIZE) {
|
||||
throw new IOException("文件过大(超过1GB),禁止读取:" + filePath);
|
||||
}
|
||||
throw new FileNotFoundException(filePath);
|
||||
}
|
||||
|
||||
// 2. 读取文件内容(使用StringBuilder高效拼接)
|
||||
StringBuilder sb = new StringBuilder((int) file.length());
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int readLen;
|
||||
// 循环读取缓冲区,避免一次性读取大文件导致OOM
|
||||
while ((readLen = fis.read(buffer)) > 0) {
|
||||
sb.append(new String(buffer, 0, readLen));
|
||||
}
|
||||
}
|
||||
if (file.length() > 1024 * 1024 * 1024) {
|
||||
throw new IOException("File is too large");
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder((int) (file.length()));
|
||||
// 创建字节输入流
|
||||
FileInputStream fis = new FileInputStream(filePath);
|
||||
// 创建一个长度为10240的Buffer
|
||||
byte[] bbuf = new byte[10240];
|
||||
// 用于保存实际读取的字节数
|
||||
int hasRead = 0;
|
||||
while ((hasRead = fis.read(bbuf)) > 0) {
|
||||
sb.append(new String(bbuf, 0, hasRead));
|
||||
}
|
||||
fis.close();
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件内容并转为byte数组(适用于二进制文件:图片、音频等)
|
||||
* @param filePath 文件绝对路径(非空)
|
||||
* @return 文件内容byte数组
|
||||
* @throws IOException 异常:文件不存在、读取失败等
|
||||
*/
|
||||
//
|
||||
// 根据文件路径读取byte[] 数组
|
||||
//
|
||||
public static byte[] readFileByBytes(String filePath) throws IOException {
|
||||
// 1. 校验文件合法性
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("文件不存在:" + filePath);
|
||||
}
|
||||
throw new FileNotFoundException(filePath);
|
||||
} else {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
|
||||
BufferedInputStream in = null;
|
||||
|
||||
// 2. 缓冲流读取(高效,减少IO次数)
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
|
||||
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
|
||||
try {
|
||||
in = new BufferedInputStream(new FileInputStream(file));
|
||||
short bufSize = 1024;
|
||||
byte[] buffer = new byte[bufSize];
|
||||
int len1;
|
||||
while (-1 != (len1 = in.read(buffer, 0, bufSize))) {
|
||||
bos.write(buffer, 0, len1);
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int readLen;
|
||||
while ((readLen = bis.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, readLen);
|
||||
byte[] var7 = bos.toByteArray();
|
||||
return var7;
|
||||
} finally {
|
||||
try {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
} catch (IOException var14) {
|
||||
var14.printStackTrace();
|
||||
}
|
||||
|
||||
bos.close();
|
||||
}
|
||||
bos.flush();
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 文件复制相关 ======================================
|
||||
|
||||
/**
|
||||
* 基于FileChannel复制文件(高效,适用于大文件复制)
|
||||
* @param source 源文件(非空,必须存在)
|
||||
* @param dest 目标文件(非空,父目录会自动创建)
|
||||
* @throws IOException 异常:源文件不存在、复制失败等
|
||||
*/
|
||||
//
|
||||
// 文件复制函数
|
||||
//
|
||||
public static void copyFileUsingFileChannels(File source, File dest) throws IOException {
|
||||
// 1. 校验源文件合法性
|
||||
if (!source.exists() || !source.isFile()) {
|
||||
throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 2. 创建目标文件父目录
|
||||
if (!dest.getParentFile().exists()) {
|
||||
dest.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
// 3. 通道复制(try-with-resources 自动关闭通道,无需手动关闭)
|
||||
try (FileChannel inputChannel = new FileInputStream(source).getChannel();
|
||||
FileChannel outputChannel = new FileOutputStream(dest).getChannel()) {
|
||||
// 从输入通道复制到输出通道(高效,底层优化)
|
||||
FileChannel inputChannel = null;
|
||||
FileChannel outputChannel = null;
|
||||
try {
|
||||
inputChannel = new FileInputStream(source).getChannel();
|
||||
outputChannel = new FileOutputStream(dest).getChannel();
|
||||
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
|
||||
LogUtils.d(TAG, "文件复制成功(FileChannel):" + source.getAbsolutePath() + " → " + dest.getAbsolutePath());
|
||||
} finally {
|
||||
inputChannel.close();
|
||||
outputChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化版文件复制(基于NIO Files工具类,代码简洁,适用于中小文件)
|
||||
* @param oldFile 源文件(非空,必须存在)
|
||||
* @param newFile 目标文件(非空,父目录会自动创建)
|
||||
* @return 复制结果:true-成功,false-失败
|
||||
* 将文件生成位图
|
||||
* @param path
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static boolean copyFile(File oldFile, File newFile) {
|
||||
// 1. 校验源文件合法性
|
||||
if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) {
|
||||
LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null"));
|
||||
return false;
|
||||
public static BitmapDrawable getImageDrawable(String path)
|
||||
throws IOException {
|
||||
//打开文件
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 创建目标文件父目录
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
int BUFFER_SIZE = 1000;
|
||||
byte[] bt = new byte[BUFFER_SIZE];
|
||||
|
||||
//得到文件的输入流
|
||||
InputStream in = new FileInputStream(file);
|
||||
|
||||
//将文件读出到输出流中
|
||||
int readLength = in.read(bt);
|
||||
while (readLength != -1) {
|
||||
outStream.write(bt, 0, readLength);
|
||||
readLength = in.read(bt);
|
||||
}
|
||||
|
||||
//转换成byte 后 再格式化成位图
|
||||
byte[] data = outStream.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);// 生成位图
|
||||
BitmapDrawable bd = new BitmapDrawable(bitmap);
|
||||
|
||||
return bd;
|
||||
}
|
||||
|
||||
public static boolean copyFile(File oldFile, File newFile) {
|
||||
//String oldPath = "path/to/original/file.txt";
|
||||
//String newPath = "path/to/new-location/for/file.txt";
|
||||
|
||||
//File oldFile = new java.io.File(oldPath);
|
||||
//File newFile = new java.io.File(newPath);
|
||||
if (!newFile.getParentFile().exists()) {
|
||||
newFile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
// 3. 复制文件(覆盖已有目标文件)
|
||||
try {
|
||||
Path sourcePath = Paths.get(oldFile.getPath());
|
||||
Path destPath = Paths.get(newFile.getPath());
|
||||
// 先删除已有目标文件(避免覆盖失败)
|
||||
if (newFile.exists()) {
|
||||
newFile.delete();
|
||||
}
|
||||
Files.copy(sourcePath, destPath);
|
||||
LogUtils.d(TAG, "文件复制成功(Files):" + oldFile.getAbsolutePath() + " → " + newFile.getAbsolutePath());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 图片文件相关 ======================================
|
||||
|
||||
/**
|
||||
* 从文件路径获取BitmapDrawable(适用于Android图片显示)
|
||||
* @param path 图片文件绝对路径(非空)
|
||||
* @return BitmapDrawable 图片对象(文件不存在/读取失败返回null)
|
||||
* @throws IOException 异常:文件读取IO错误
|
||||
*/
|
||||
public static BitmapDrawable getImageDrawable(String path) throws IOException {
|
||||
// 1. 校验文件合法性
|
||||
File file = new File(path);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
LogUtils.e(TAG, "图片文件不存在:" + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 读取文件并转为BitmapDrawable(缓冲流读取,减少内存占用)
|
||||
try (InputStream is = new FileInputStream(file);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int readLen;
|
||||
while ((readLen = is.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, readLen);
|
||||
}
|
||||
|
||||
// 3. 生成Bitmap并包装为BitmapDrawable
|
||||
byte[] imageBytes = bos.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||
return new BitmapDrawable(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================== 文件名处理相关 ======================================
|
||||
|
||||
/**
|
||||
* 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png")
|
||||
* @param file 目标文件(可为null)
|
||||
* @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串
|
||||
*/
|
||||
public static String getFileSuffixWithMultiDot(File file) {
|
||||
// 1. 校验文件合法性
|
||||
if (file == null || !file.isFile()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 2. 提取文件名并查找最后一个 "."
|
||||
String fileName = file.getName();
|
||||
int lastDotIndex = fileName.lastIndexOf(".");
|
||||
|
||||
// 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件)
|
||||
if (lastDotIndex == -1 // 无 "."
|
||||
|| lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore")
|
||||
|| (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5(异常文件名)
|
||||
return "";
|
||||
}
|
||||
|
||||
// 4. 返回小写后缀(统一格式,避免大小写不一致问题)
|
||||
return fileName.substring(lastDotIndex).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件名(优化版:唯一、合法、简洁)
|
||||
* 生成规则:UUID(去掉"-") + "_" + 时间戳 + 原文件后缀
|
||||
* @param refFile 参考文件(用于提取后缀名,可为null)
|
||||
* @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点)
|
||||
*/
|
||||
public static String createUniqueFileName(File refFile) {
|
||||
// 1. 获取参考文件的后缀名(自动容错null/无效文件)
|
||||
String suffix = getFileSuffixWithMultiDot(refFile);
|
||||
|
||||
// 2. 生成唯一标识(UUID确保全局唯一,时间戳进一步降低重复概率)
|
||||
String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名
|
||||
long timeStamp = System.currentTimeMillis();
|
||||
|
||||
// 3. 拼接文件名(分场景处理,避免多余点)
|
||||
if (suffix.isEmpty()) {
|
||||
// 无后缀:唯一ID + 时间戳
|
||||
return String.format("%s_%d", uniqueId, timeStamp);
|
||||
if (!oldFile.exists()) {
|
||||
//System.out.println("The original file does not exist.");
|
||||
LogUtils.d(TAG, "The original file does not exist.");
|
||||
} else {
|
||||
// 有后缀:唯一ID + 时间戳 + 后缀(无多余点)
|
||||
return String.format("%s_%d%s", uniqueId, timeStamp, suffix);
|
||||
try {
|
||||
// 源文件路径
|
||||
Path sourcePath = Paths.get(oldFile.getPath());
|
||||
// 目标文件路径
|
||||
Path destPath = Paths.get(newFile.getPath());
|
||||
if(newFile.exists()) {
|
||||
newFile.delete();
|
||||
}
|
||||
Files.copy(sourcePath, destPath);
|
||||
LogUtils.d(TAG, "File copy successfully.");
|
||||
//System.out.println("File moved successfully.");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
//System.err.println("An error occurred while moving the file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制输入流到文件(兼容Uri解析失败场景)
|
||||
*/
|
||||
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException {
|
||||
if (inputStream == null || file == null) {
|
||||
throw new IllegalArgumentException("InputStream或File不能为空");
|
||||
}
|
||||
File parentDir = file.getParentFile();
|
||||
if (!parentDir.exists() && !parentDir.mkdirs()) {
|
||||
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
|
||||
}
|
||||
try {
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
outputStream.flush();
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFileSuffix(Context context, Uri uri){
|
||||
String szType = context.getContentResolver().getType(uri);
|
||||
// 2. 截取MIME类型后缀(如从image/jpeg中提取jpeg)【核心新增逻辑】
|
||||
String fileSuffix = "";
|
||||
if (szType != null && szType.contains("/")) {
|
||||
// 分割字符串,取"/"后面的部分(如"image/jpeg" → 分割后取索引1的"jpeg")
|
||||
fileSuffix = szType.split("/")[1];
|
||||
// 调试日志:打印截取后的文件后缀
|
||||
} else {
|
||||
// 异常处理:若类型为空或格式错误,默认后缀设为jpeg(保留原逻辑兼容性)
|
||||
fileSuffix = "jpeg";
|
||||
}
|
||||
return fileSuffix;
|
||||
}
|
||||
|
||||
public static boolean isFileExists(String path) {
|
||||
File file = new File(path);
|
||||
return file.exists();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import com.yalantis.ucrop.UCrop;
|
||||
import com.yalantis.ucrop.UCropActivity;
|
||||
import java.io.File;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
/**
|
||||
* 图片裁剪工具类(集成uCrop,脱离系统依赖)
|
||||
*/
|
||||
public class ImageCropUtils {
|
||||
public static final String TAG = "ImageCropUtils";
|
||||
// FileProvider 授权(与项目一致)
|
||||
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
|
||||
|
||||
/**
|
||||
* 启动uCrop裁剪(核心方法,替代系统裁剪)
|
||||
* @param activity 上下文
|
||||
* @param inputFile 输入图片文件
|
||||
* @param outputFile 输出图片文件
|
||||
* @param isFreeCrop 是否自由裁剪(true=自由,false=固定比例)
|
||||
* @param requestCode 裁剪请求码
|
||||
*/
|
||||
public static void startImageCrop(Activity activity,
|
||||
File inputFile,
|
||||
File outputFile,
|
||||
int aspectX,
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
// 校验输入参数
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【裁剪异常】上下文Activity无效");
|
||||
return;
|
||||
}
|
||||
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入文件无效");
|
||||
showToast(activity, "无有效图片可裁剪");
|
||||
return;
|
||||
}
|
||||
if (outputFile == null) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输出文件路径为空");
|
||||
showToast(activity, "裁剪输出路径无效");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成输入/输出Uri(适配FileProvider)
|
||||
Uri inputUri = getFileProviderUri(activity, inputFile);
|
||||
Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri(兼容低版本)
|
||||
|
||||
// 配置uCrop参数
|
||||
UCrop uCrop = UCrop.of(inputUri, outputUri);
|
||||
UCrop.Options options = new UCrop.Options();
|
||||
|
||||
// 裁剪模式配置(自由裁剪/固定比例)
|
||||
if (isFreeCrop) {
|
||||
// 自由裁剪:无固定比例,可随意调整
|
||||
uCrop.withAspectRatio(0, 0);
|
||||
options.setFreeStyleCropEnabled(true); // 开启自由裁剪
|
||||
} else {
|
||||
// 固定比例(默认1:1,可根据需求修改)
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
options.setFreeStyleCropEnabled(false);
|
||||
}
|
||||
|
||||
// 裁剪配置(优化体验)
|
||||
options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
|
||||
options.setCompressionQuality(100); // 图片质量
|
||||
options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
|
||||
options.setToolbarTitle("图片裁剪"); // 工具栏标题
|
||||
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
|
||||
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
|
||||
|
||||
// 应用配置并启动裁剪
|
||||
uCrop.withOptions(options);
|
||||
// 启动uCrop裁剪Activity(替代系统裁剪)
|
||||
uCrop.start(activity, requestCode);
|
||||
|
||||
LogUtils.d(TAG, "【uCrop启动】成功,输入Uri:" + inputUri + ",输出Uri:" + outputUri + ",请求码:" + requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:适配BackgroundBean
|
||||
*/
|
||||
public static void startImageCrop(Activity activity,
|
||||
BackgroundBean cropBean,
|
||||
int aspectX,
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
File inputFile = new File(cropBean.getBackgroundFilePath());
|
||||
File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath());
|
||||
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成FileProvider Uri
|
||||
*/
|
||||
private static Uri getFileProviderUri(Activity activity, File file) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
|
||||
Uri uri = FileProvider.getUriForFile(activity, authority, file);
|
||||
LogUtils.d(TAG, "【Uri生成】FileProvider Uri:" + uri);
|
||||
return uri;
|
||||
} else {
|
||||
Uri uri = Uri.fromFile(file);
|
||||
LogUtils.d(TAG, "【Uri生成】普通Uri:" + uri);
|
||||
return uri;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri生成】失败:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理uCrop裁剪回调(在Activity的onActivityResult中调用)
|
||||
* @param requestCode 请求码
|
||||
* @param resultCode 结果码
|
||||
* @param data 回调数据
|
||||
* @return 裁剪成功返回输出文件路径,失败返回null
|
||||
*/
|
||||
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
|
||||
// 校验是否是uCrop的回调
|
||||
if (requestCode == cropRequestCode) {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
// 裁剪成功,获取输出Uri
|
||||
Uri outputUri = UCrop.getOutput(data);
|
||||
if (outputUri != null) {
|
||||
String outputPath = outputUri.getPath();
|
||||
LogUtils.d(TAG, "【uCrop回调】裁剪成功,输出路径:" + outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
} else if (resultCode == UCrop.RESULT_ERROR) {
|
||||
// 裁剪失败,获取异常信息
|
||||
Throwable error = UCrop.getError(data);
|
||||
LogUtils.e(TAG, "【uCrop回调】裁剪失败:" + (error != null ? error.getMessage() : "未知错误"));
|
||||
} else {
|
||||
LogUtils.d(TAG, "【uCrop回调】裁剪被取消");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast
|
||||
*/
|
||||
private static void showToast(Activity activity, String msg) {
|
||||
if (activity != null && !activity.isFinishing()) {
|
||||
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露getFileProviderUri方法(供外部调用)
|
||||
*/
|
||||
public static Uri getFileProviderUriPublic(Activity activity, File file) {
|
||||
return getFileProviderUri(activity, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 04:39:40
|
||||
* @Describe 通知工具类
|
||||
*/
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
import android.widget.RemoteViews;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
public class NotificationHelper {
|
||||
public static final String TAG = "NotificationHelper";
|
||||
|
||||
// 渠道ID和名称
|
||||
private static final String CHANNEL_ID_FOREGROUND = "foreground_channel";
|
||||
private static final String CHANNEL_NAME_FOREGROUND = "Foreground Service";
|
||||
private static final String CHANNEL_ID_TEMPORARY = "temporary_channel";
|
||||
private static final String CHANNEL_NAME_TEMPORARY = "Temporary Notifications";
|
||||
|
||||
// 通知ID
|
||||
public static final int FOREGROUND_NOTIFICATION_ID = 1001;
|
||||
public static final int TEMPORARY_NOTIFICATION_ID = 2001;
|
||||
|
||||
private final Context mContext;
|
||||
private final NotificationManager mNotificationManager;
|
||||
|
||||
public NotificationHelper(Context context) {
|
||||
mContext = context;
|
||||
mNotificationManager = context.getSystemService(NotificationManager.class);
|
||||
createNotificationChannels();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createForegroundChannel();
|
||||
createTemporaryChannel();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createForegroundChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_FOREGROUND,
|
||||
CHANNEL_NAME_FOREGROUND,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Persistent service notifications");
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createTemporaryChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_TEMPORARY,
|
||||
CHANNEL_NAME_TEMPORARY,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription("Temporary alert notifications");
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(true);
|
||||
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
|
||||
channel.setBypassDnd(true);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
// 显示常驻通知(通常用于前台服务)
|
||||
public Notification showForegroundNotification(Intent intent, String title, String content) {
|
||||
PendingIntent pendingIntent = createPendingIntent(intent);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_FOREGROUND)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
|
||||
//.setContentTitle(title + "\n" + content)
|
||||
.setContentTitle(content)
|
||||
//.setContentText(content)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
|
||||
mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
|
||||
return notification;
|
||||
}
|
||||
|
||||
// 显示临时通知(自动消失)
|
||||
public void showTemporaryNotification(Intent intent, String title, String content) {
|
||||
PendingIntent pendingIntent = createPendingIntent(intent);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setVibrate(new long[]{100, 200, 300, 400})
|
||||
.build();
|
||||
|
||||
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
// 创建自定义布局通知(可扩展)
|
||||
public void showCustomNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) {
|
||||
PendingIntent pendingIntent = createPendingIntent(intent);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setContent(contentView)
|
||||
.setCustomBigContentView(bigContentView)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.build();
|
||||
|
||||
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID + 1, notification);
|
||||
}
|
||||
|
||||
// 取消所有通知
|
||||
public void cancelAllNotifications() {
|
||||
mNotificationManager.cancelAll();
|
||||
}
|
||||
|
||||
// 创建PendingIntent(兼容不同API版本)
|
||||
private PendingIntent createPendingIntent(Intent intent) {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
// }
|
||||
return PendingIntent.getActivity(
|
||||
mContext,
|
||||
0,
|
||||
intent,
|
||||
flags
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.media.RingtoneManager;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/13 20:44
|
||||
* @Describe 全局通知管理工具类(整合所有通知能力,适配API29-30,兼容Java7)
|
||||
*/
|
||||
public class NotificationManagerUtils {
|
||||
// ====================== 常量定义(统一管理,避免冲突,首屏可见)======================
|
||||
public static final String TAG = "NotificationManagerUtils";
|
||||
// 通知渠道(4大渠道,场景隔离,API26+必填)
|
||||
// 1. 前台服务保活渠道(低优先级,无打扰)
|
||||
private static final String CHANNEL_ID_FOREGROUND_SERVICE = "channel_foreground_service";
|
||||
private static final String CHANNEL_NAME_FOREGROUND_SERVICE = "前台服务保活通知";
|
||||
private static final String CHANNEL_DESC_FOREGROUND_SERVICE = "后台服务运行状态,无声音无震动,不打扰用户";
|
||||
// 2. 电量提醒渠道(高优先级,闹钟铃声+震动,强提醒)
|
||||
private static final String CHANNEL_ID_BATTERY_REMIND = "channel_battery_remind";
|
||||
private static final String CHANNEL_NAME_BATTERY_REMIND = "电量异常提醒通知";
|
||||
private static final String CHANNEL_DESC_BATTERY_REMIND = "电量过高/过低提醒,强震动+闹钟铃声,突破免打扰";
|
||||
// 3. 通用临时通知渠道(高优先级,仅震动,普通告警)
|
||||
private static final String CHANNEL_ID_TEMP_ALERT = "channel_temp_alert";
|
||||
private static final String CHANNEL_NAME_TEMP_ALERT = "通用临时提醒通知";
|
||||
private static final String CHANNEL_DESC_TEMP_ALERT = "普通即时告警,仅震动提醒,自动取消";
|
||||
// 通知ID(唯一区分,避免覆盖,按场景分段)
|
||||
public static final int NOTIFY_ID_FOREGROUND_SERVICE = 1001; // 前台服务
|
||||
public static final int NOTIFY_ID_BATTERY_REMIND = 1002; // 电量提醒
|
||||
public static final int NOTIFY_ID_TEMP_ALERT = 1003; // 通用临时通知
|
||||
public static final int NOTIFY_ID_CUSTOM_LAYOUT = 1004; // 自定义布局通知
|
||||
// 通用配置
|
||||
private static final int PENDING_INTENT_FLAGS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
: PendingIntent.FLAG_UPDATE_CURRENT; // API30安全标志
|
||||
private static final long[] VIBRATE_PATTERN = new long[]{100, 200, 300, 400}; // 标准震动节奏
|
||||
private static final String DEFAULT_JUMP_PACKAGE = "cc.winboll.studio.powerbell"; // 默认跳转包名(API29+必填)
|
||||
|
||||
// ====================== 成员变量(按场景分组,私有封装,避免外部篡改)======================
|
||||
private final Context mContext;
|
||||
private final NotificationManager mNotificationManager;
|
||||
// 前台服务通知专属
|
||||
private Notification mForegroundServiceNotify;
|
||||
private RemoteViews mForegroundServiceRemoteViews;
|
||||
// 电量提醒通知专属
|
||||
private Notification mBatteryRemindNotify;
|
||||
private RemoteViews mBatteryRemindRemoteViews;
|
||||
|
||||
// ====================== 构造方法(单例思想/实例化通用,自动初始化渠道)======================
|
||||
public NotificationManagerUtils(Context context) {
|
||||
LogUtils.d(TAG, "【初始化】全局通知管理工具类 构造方法调用");
|
||||
this.mContext = context.getApplicationContext(); // 用应用上下文,避免内存泄漏
|
||||
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
createAllNotificationChannels(); // 自动创建所有渠道(API26+)
|
||||
LogUtils.d(TAG, "【初始化】全局通知管理工具类 完成,渠道创建状态:" + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "已创建4个渠道" : "无需创建"));
|
||||
}
|
||||
|
||||
// ====================== 核心基础能力(渠道创建+Intent构建,复用逻辑,减少冗余)======================
|
||||
/**
|
||||
* 创建所有通知渠道(API26+专属,低版本自动跳过,确保通知正常显示)
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void createAllNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
LogUtils.d(TAG, "【渠道管理】开始创建所有通知渠道");
|
||||
createForegroundServiceChannel();
|
||||
createBatteryRemindChannel();
|
||||
createTempAlertChannel();
|
||||
LogUtils.d(TAG, "【渠道管理】4个通知渠道创建完成(含3个核心渠道+预留扩展)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建前台服务保活渠道(IMPORTANCE_LOW,无声音无震动,不打扰用户)
|
||||
*/
|
||||
private void createForegroundServiceChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_FOREGROUND_SERVICE,
|
||||
CHANNEL_NAME_FOREGROUND_SERVICE,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription(CHANNEL_DESC_FOREGROUND_SERVICE);
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
channel.setShowBadge(false); // 不显示应用角标
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); // 锁屏隐藏
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "【渠道管理】前台服务保活渠道创建成功:" + CHANNEL_NAME_FOREGROUND_SERVICE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建电量提醒渠道(IMPORTANCE_HIGH,闹钟铃声+震动,突破免打扰)
|
||||
*/
|
||||
private void createBatteryRemindChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_BATTERY_REMIND,
|
||||
CHANNEL_NAME_BATTERY_REMIND,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription(CHANNEL_DESC_BATTERY_REMIND);
|
||||
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null); // 闹钟铃声
|
||||
channel.enableVibration(true);
|
||||
channel.setVibrationPattern(VIBRATE_PATTERN);
|
||||
channel.setBypassDnd(true); // 突破免打扰
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // 锁屏可见
|
||||
channel.setShowBadge(true);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "【渠道管理】电量提醒渠道创建成功:" + CHANNEL_NAME_BATTERY_REMIND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通用临时通知渠道(IMPORTANCE_HIGH,仅震动,普通告警)
|
||||
*/
|
||||
private void createTempAlertChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_TEMP_ALERT,
|
||||
CHANNEL_NAME_TEMP_ALERT,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription(CHANNEL_DESC_TEMP_ALERT);
|
||||
channel.setSound(null, null); // 仅震动,不发声
|
||||
channel.enableVibration(true);
|
||||
channel.setVibrationPattern(VIBRATE_PATTERN);
|
||||
channel.setBypassDnd(false); // 不突破免打扰
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
channel.setShowBadge(true);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "【渠道管理】通用临时通知渠道创建成功:" + CHANNEL_NAME_TEMP_ALERT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通用跳转PendingIntent(适配API29-30安全规范,支持自定义跳转目标)
|
||||
* @param targetIntent 自定义跳转意图(传null则默认跳MainActivity)
|
||||
* @return 安全的PendingIntent,避免跳转失败/泄露
|
||||
*/
|
||||
private PendingIntent buildPendingIntent(Intent targetIntent) {
|
||||
Intent intent = targetIntent;
|
||||
// 传null则默认跳MainActivity(兼容旧逻辑)
|
||||
if (intent == null) {
|
||||
intent = new Intent(mContext, MainActivity.class);
|
||||
LogUtils.d(TAG, "【Intent构建】未传自定义Intent,默认跳转:MainActivity");
|
||||
}
|
||||
// API29+ 强制要求:明确包名,避免跳转目标模糊
|
||||
intent.setPackage(DEFAULT_JUMP_PACKAGE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 确保跳转生效
|
||||
LogUtils.d(TAG, "【Intent构建】跳转包名:" + DEFAULT_JUMP_PACKAGE + ",目标:" + intent.getComponent().getClassName());
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
mContext,
|
||||
0,
|
||||
intent,
|
||||
PENDING_INTENT_FLAGS
|
||||
);
|
||||
LogUtils.d(TAG, "【Intent构建】PendingIntent创建成功,安全标志:" + PENDING_INTENT_FLAGS);
|
||||
return pendingIntent;
|
||||
}
|
||||
|
||||
// ====================== 场景1:前台服务保活通知(支持自定义布局+更新)======================
|
||||
/**
|
||||
* 初始化前台服务通知自定义布局(RemoteViews)
|
||||
*/
|
||||
private void initForegroundServiceRemoteViews(ControlCenterService service, NotificationMessage msg) {
|
||||
LogUtils.d(TAG, "【布局初始化】开始初始化前台服务通知布局,标题:" + msg.getTitle());
|
||||
mForegroundServiceRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
|
||||
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView1, msg.getTitle());
|
||||
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView3, msg.getContent());
|
||||
mForegroundServiceRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
|
||||
LogUtils.d(TAG, "【布局初始化】前台服务通知布局填充完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动前台服务保活通知(ControlCenterService专用,API26+强制要求,保活后台服务)
|
||||
*/
|
||||
public void startForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
|
||||
LogUtils.d(TAG, "【前台服务通知】开始构建保活通知,内容:" + msg.getContent());
|
||||
if (service == null || msg == null) {
|
||||
LogUtils.e(TAG, "【前台服务通知】构建失败:Service/NotificationMessage为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 构建跳转Intent
|
||||
PendingIntent pendingIntent = buildPendingIntent(null); // 默认跳MainActivity
|
||||
// 2. 构建基础通知(兼容API26+渠道,低版本用Builder)
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder = new Notification.Builder(service, CHANNEL_ID_FOREGROUND_SERVICE);
|
||||
} else {
|
||||
builder = new Notification.Builder(service);
|
||||
}
|
||||
mForegroundServiceNotify = builder
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
|
||||
.setContentTitle(msg.getTitle())
|
||||
.setContentText(msg.getContent())
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setColor(Color.parseColor("#F00606")) // 小图标背景色
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true) // 常驻通知,不可滑动取消(保活关键)
|
||||
.setAutoCancel(false) // 禁止点击取消
|
||||
.build();
|
||||
// 3. 设置自定义布局
|
||||
initForegroundServiceRemoteViews(service, msg);
|
||||
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
|
||||
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
|
||||
// 4. 启动前台服务(必须调用,否则Service易被回收)
|
||||
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
|
||||
LogUtils.d(TAG, "【前台服务通知】保活通知启动成功,通知ID:" + NOTIFY_ID_FOREGROUND_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前台服务保活通知内容(无需重启服务,直接刷新布局)
|
||||
*/
|
||||
public void updateForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
|
||||
LogUtils.d(TAG, "【前台服务通知】开始更新保活通知,新内容:" + msg.getContent());
|
||||
if (mForegroundServiceNotify == null || mForegroundServiceRemoteViews == null) {
|
||||
LogUtils.e(TAG, "【前台服务通知】更新失败:通知对象未初始化,先调用startForegroundServiceNotify");
|
||||
return;
|
||||
}
|
||||
// 更新自定义布局数据
|
||||
initForegroundServiceRemoteViews(service, msg);
|
||||
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
|
||||
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
|
||||
// 发送更新
|
||||
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
|
||||
LogUtils.d(TAG, "【前台服务通知】保活通知更新成功");
|
||||
}
|
||||
|
||||
// ====================== 场景2:电量提醒通知(支持自定义布局+更新+单独取消)======================
|
||||
/**
|
||||
* 初始化电量提醒通知自定义布局(RemoteViews,支持充电/耗电切换)
|
||||
*/
|
||||
private void initBatteryRemindRemoteViews(ControlCenterService service, NotificationMessage msg) {
|
||||
LogUtils.d(TAG, "【布局初始化】开始初始化电量提醒布局,提醒类型:" + msg.getRemindMSG());
|
||||
mBatteryRemindRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
|
||||
mBatteryRemindRemoteViews.setTextViewText(R.id.viewremindnotificationTextView1, msg.getTitle());
|
||||
mBatteryRemindRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
|
||||
// 切换布局(+:充电提醒,-:耗电提醒)
|
||||
String remindType = msg.getRemindMSG() != null ? msg.getRemindMSG().trim() : "";
|
||||
if ("+".equals(remindType)) {
|
||||
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.GONE);
|
||||
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
|
||||
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:充电提醒");
|
||||
} else if ("-".equals(remindType)) {
|
||||
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
|
||||
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
|
||||
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:耗电提醒");
|
||||
} else {
|
||||
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
|
||||
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
|
||||
LogUtils.w(TAG, "【布局初始化】未知电量提醒类型:" + remindType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化电量提醒通知(仅构建,不发送,配合update触发提醒)
|
||||
*/
|
||||
public void initBatteryRemindNotify(ControlCenterService service, NotificationMessage msg) {
|
||||
LogUtils.d(TAG, "【电量提醒通知】开始初始化提醒通知,标题:" + msg.getTitle());
|
||||
if (service == null || msg == null) {
|
||||
LogUtils.e(TAG, "【电量提醒通知】初始化失败:Service/NotificationMessage为空");
|
||||
return;
|
||||
}
|
||||
// 1. 构建跳转Intent
|
||||
PendingIntent pendingIntent = buildPendingIntent(null);
|
||||
// 2. 构建基础通知
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder = new Notification.Builder(service, CHANNEL_ID_BATTERY_REMIND);
|
||||
} else {
|
||||
builder = new Notification.Builder(service);
|
||||
}
|
||||
mBatteryRemindNotify = builder
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
|
||||
.setContentTitle(msg.getTitle())
|
||||
.setContentText(msg.getContent())
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setColor(Color.parseColor("#F00606"))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true) // 点击取消
|
||||
.build();
|
||||
// 3. 初始化自定义布局
|
||||
initBatteryRemindRemoteViews(service, msg);
|
||||
mBatteryRemindNotify.contentView = mBatteryRemindRemoteViews;
|
||||
mBatteryRemindNotify.bigContentView = mBatteryRemindRemoteViews;
|
||||
LogUtils.d(TAG, "【电量提醒通知】初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送/更新电量提醒通知(初始化后调用,触发强提醒)
|
||||
*/
|
||||
public void sendOrUpdateBatteryRemindNotify() {
|
||||
LogUtils.d(TAG, "【电量提醒通知】开始发送/更新提醒");
|
||||
if (mBatteryRemindNotify == null || mBatteryRemindRemoteViews == null) {
|
||||
LogUtils.e(TAG, "【电量提醒通知】发送失败:通知未初始化,先调用initBatteryRemindNotify");
|
||||
return;
|
||||
}
|
||||
mNotificationManager.notify(NOTIFY_ID_BATTERY_REMIND, mBatteryRemindNotify);
|
||||
LogUtils.d(TAG, "【电量提醒通知】发送/更新成功,通知ID:" + NOTIFY_ID_BATTERY_REMIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单独取消电量提醒通知(静态方法,外部可直接调用,无需实例化)
|
||||
*/
|
||||
public static void cancelBatteryRemindNotify(Context context) {
|
||||
LogUtils.d(TAG, "【电量提醒通知】开始取消提醒,通知ID:" + NOTIFY_ID_BATTERY_REMIND);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "【电量提醒通知】取消失败:Context为空");
|
||||
return;
|
||||
}
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
manager.cancel(NOTIFY_ID_BATTERY_REMIND);
|
||||
LogUtils.d(TAG, "【电量提醒通知】取消成功");
|
||||
}
|
||||
|
||||
// ====================== 场景3:通用临时通知(简单文本,自动取消,无需自定义布局)======================
|
||||
/**
|
||||
* 显示通用临时通知(普通告警,仅震动,自动取消,支持自定义跳转目标)
|
||||
* @param targetIntent 自定义跳转Intent(传null默认跳MainActivity)
|
||||
* @param title 通知标题
|
||||
* @param content 通知内容
|
||||
*/
|
||||
public void showTempAlertNotify(Intent targetIntent, String title, String content) {
|
||||
LogUtils.d(TAG, "【通用临时通知】开始构建,标题:" + title + ",内容:" + content);
|
||||
if (title == null || content == null) {
|
||||
LogUtils.e(TAG, "【通用临时通知】构建失败:标题/内容为空");
|
||||
return;
|
||||
}
|
||||
// 1. 构建跳转Intent
|
||||
PendingIntent pendingIntent = buildPendingIntent(targetIntent);
|
||||
// 2. 用NotificationCompat.Builder(兼容所有版本,简化逻辑)
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setVibrate(VIBRATE_PATTERN);
|
||||
// 3. 发送通知
|
||||
Notification notification = builder.build();
|
||||
mNotificationManager.notify(NOTIFY_ID_TEMP_ALERT, notification);
|
||||
LogUtils.d(TAG, "【通用临时通知】显示成功,通知ID:" + NOTIFY_ID_TEMP_ALERT);
|
||||
}
|
||||
|
||||
// ====================== 场景4:自定义布局通知(灵活扩展,支持复杂样式)======================
|
||||
/**
|
||||
* 显示自定义布局通知(支持普通布局+大布局,通用所有场景,可自定义跳转)
|
||||
* @param targetIntent 自定义跳转Intent
|
||||
* @param contentView 普通自定义布局(必填)
|
||||
* @param bigContentView 下拉大布局(可选)
|
||||
*/
|
||||
public void showCustomLayoutNotify(Intent targetIntent, RemoteViews contentView, RemoteViews bigContentView) {
|
||||
LogUtils.d(TAG, "【自定义布局通知】开始构建,布局ID:" + (contentView != null ? contentView.getLayoutId() : null));
|
||||
if (contentView == null) {
|
||||
LogUtils.e(TAG, "【自定义布局通知】构建失败:普通布局contentView为空");
|
||||
return;
|
||||
}
|
||||
// 1. 构建跳转Intent
|
||||
PendingIntent pendingIntent = buildPendingIntent(targetIntent);
|
||||
// 2. 构建自定义布局通知
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
|
||||
.setSmallIcon(R.drawable.ic_launcher) // 必传,不可省略
|
||||
.setContentIntent(pendingIntent)
|
||||
.setContent(contentView)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true);
|
||||
// 添加大布局(可选)
|
||||
if (bigContentView != null) {
|
||||
builder.setCustomBigContentView(bigContentView);
|
||||
LogUtils.d(TAG, "【自定义布局通知】已添加下拉大布局,布局ID:" + bigContentView.getLayoutId());
|
||||
}
|
||||
// 3. 发送通知
|
||||
Notification notification = builder.build();
|
||||
mNotificationManager.notify(NOTIFY_ID_CUSTOM_LAYOUT, notification);
|
||||
LogUtils.d(TAG, "【自定义布局通知】显示成功,通知ID:" + NOTIFY_ID_CUSTOM_LAYOUT);
|
||||
}
|
||||
|
||||
// ====================== 通知取消工具(支持精准取消/全取消)======================
|
||||
/**
|
||||
* 取消指定ID的通知(精准取消,灵活控制)
|
||||
*/
|
||||
public void cancelNotifyById(int notifyId) {
|
||||
LogUtils.d(TAG, "【通知管理】开始取消通知,ID:" + notifyId);
|
||||
mNotificationManager.cancel(notifyId);
|
||||
LogUtils.d(TAG, "【通知管理】通知取消成功,ID:" + notifyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有通知(谨慎使用,会清除所有场景的通知)
|
||||
*/
|
||||
public void cancelAllNotifies() {
|
||||
LogUtils.d(TAG, "【通知管理】开始取消所有通知");
|
||||
mNotificationManager.cancelAll();
|
||||
LogUtils.d(TAG, "【通知管理】所有通知取消完成");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/*
|
||||
* 参考:
|
||||
* https://blog.csdn.net/qq_35507234/article/details/90676587
|
||||
* https://blog.csdn.net/qq_16628781/article/details/51548324
|
||||
*/
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.media.RingtoneManager;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.beans.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
|
||||
public class NotificationUtils2 {
|
||||
|
||||
public static final String TAG = NotificationHelper.class.getSimpleName();
|
||||
|
||||
Context mContext;
|
||||
NotificationManager mNotificationManager;
|
||||
|
||||
Notification mForegroundNotification;
|
||||
PendingIntent mForegroundPendingIntent;
|
||||
Notification mRemindNotification;
|
||||
PendingIntent mRemindPendingIntent;
|
||||
RemoteViews mrvServiceNotificationView;
|
||||
RemoteViews mrvRemindNotificationView;
|
||||
|
||||
static enum NotificationType { MIN, MAX };
|
||||
private static int _mnServiceNotificationID = 1;
|
||||
private static int _mnRemindNotificationID = 2;
|
||||
private static String _mszChannelIDService = "1";
|
||||
private static String _mszChannelNameService = "Service";
|
||||
private static String _mszChannelIDRemind = "2";
|
||||
private static String _mszChannelNameRemind = "Remind";
|
||||
|
||||
// public NotificationUtils(Context context) {
|
||||
// mContext = context;
|
||||
// mNotificationManager = (NotificationManager) context.getSystemService(
|
||||
// Context.NOTIFICATION_SERVICE);
|
||||
// }
|
||||
|
||||
public NotificationUtils2(Context context) {
|
||||
mContext = context;
|
||||
mNotificationManager = context.getSystemService(NotificationManager.class);
|
||||
//createNotificationChannels();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public void createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createServiceChannel();
|
||||
createRemindChannel();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createServiceChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
_mszChannelIDService,
|
||||
_mszChannelNameService,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Background service updates");
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createRemindChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
_mszChannelIDRemind,
|
||||
_mszChannelNameRemind,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription("Critical reminders");
|
||||
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null);
|
||||
channel.enableVibration(true);
|
||||
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
|
||||
channel.setBypassDnd(true);
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
// 创建并发送服务通知
|
||||
//
|
||||
public void createForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
//创建Notification,传入Context和channelId
|
||||
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
|
||||
intent.setPackage(service.getPackageName());
|
||||
//LogUtils.d(TAG, "mService.getPackageName() : " + service.getPackageName());
|
||||
intent.setClass(service, MainActivity.class);
|
||||
//LogUtils.d(TAG, "MainActivity.class.getName() : " + MainActivity.class.getName());
|
||||
//这里放一个count用来区分每一个通知
|
||||
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
|
||||
|
||||
//参数1:context 上下文对象
|
||||
//参数2:发送者私有的请求码(Private request code for the sender)
|
||||
//参数3:intent 意图对象
|
||||
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
|
||||
//mForegroundPendingIntent = PendingIntent.getActivity(mService, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mForegroundPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
} else {
|
||||
mForegroundPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
mForegroundNotification = new Notification.Builder(service, _mszChannelIDService)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(notificationMessage.getTitle())
|
||||
.setContentText(notificationMessage.getContent())
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
//设置红色
|
||||
.setColor(Color.parseColor("#F00606"))
|
||||
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
|
||||
.setContentIntent(mForegroundPendingIntent)
|
||||
.build();
|
||||
|
||||
setForegroundNotificationRemoteViews(service, notificationMessage);
|
||||
service.startForeground(_mnServiceNotificationID, mForegroundNotification);
|
||||
}
|
||||
|
||||
void initmrvRemindNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
mrvRemindNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
|
||||
mrvRemindNotificationView.setTextViewText(R.id.viewremindnotificationTextView1, notificationMessage.getTitle());
|
||||
String szRemindMSG = notificationMessage.getRemindMSG();
|
||||
//LogUtils.d(TAG, "szRemindMSG : " + szRemindMSG);
|
||||
//mrvRemindNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
|
||||
if (szRemindMSG != null) {
|
||||
if (szRemindMSG.trim().equals("-")) {
|
||||
//LogUtils.d(TAG, "-");
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.GONE);
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
|
||||
} else if (szRemindMSG.trim().equals("+")) {
|
||||
//LogUtils.d(TAG, "+");
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.GONE);
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
|
||||
}
|
||||
mrvRemindNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
|
||||
//给我remoteViews上的控件tv_content添加监听事件
|
||||
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
|
||||
//return mrvServiceNotificationView;
|
||||
}
|
||||
}
|
||||
|
||||
void initmrvServiceNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
mrvServiceNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
|
||||
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView1, notificationMessage.getTitle());
|
||||
//String szRemindMSG = notificationMessage.getRemindMSG();
|
||||
//mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
|
||||
//rvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent() + Integer.toString(nTest));
|
||||
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent());
|
||||
mrvServiceNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
|
||||
//给我remoteViews上的控件tv_content添加监听事件
|
||||
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
|
||||
//return mrvServiceNotificationView;
|
||||
}
|
||||
|
||||
void setForegroundNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
initmrvServiceNotificationView(service, notificationMessage);
|
||||
mForegroundNotification.contentView = mrvServiceNotificationView;
|
||||
mForegroundNotification.bigContentView = mrvServiceNotificationView;
|
||||
}
|
||||
|
||||
void setRemindNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
initmrvRemindNotificationView(service, notificationMessage);
|
||||
mRemindNotification.contentView = mrvRemindNotificationView;
|
||||
mRemindNotification.bigContentView = mrvRemindNotificationView;
|
||||
}
|
||||
|
||||
// 更新服务通知
|
||||
//
|
||||
public void updateForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
setForegroundNotificationRemoteViews(service, notificationMessage);
|
||||
mNotificationManager.notify(_mnServiceNotificationID, mForegroundNotification);
|
||||
|
||||
}
|
||||
|
||||
// 创建并发送电量提醒通知
|
||||
//
|
||||
public void updateRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
//LogUtils.d(TAG, "updateRemindNotification : " + notificationMessage.getRemindMSG());
|
||||
setRemindNotificationRemoteViews(service, notificationMessage);
|
||||
mNotificationManager.notify(_mnRemindNotificationID, mRemindNotification);
|
||||
}
|
||||
|
||||
public void createRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
//LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
|
||||
//创建Notification,传入Context和channelId
|
||||
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
|
||||
intent.setPackage(service.getPackageName());
|
||||
intent.setClass(service, MainActivity.class);
|
||||
//这里放一个count用来区分每一个通知
|
||||
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
|
||||
|
||||
//参数1:context 上下文对象
|
||||
//参数2:发送者私有的请求码(Private request code for the sender)
|
||||
//参数3:intent 意图对象
|
||||
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
|
||||
//mRemindPendingIntent = PendingIntent.getActivity(mService, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mRemindPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
} else {
|
||||
mRemindPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
mRemindNotification = new Notification.Builder(service, _mszChannelIDRemind)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(notificationMessage.getTitle())
|
||||
.setContentText(notificationMessage.getContent())
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
//设置红色
|
||||
.setColor(Color.parseColor("#F00606"))
|
||||
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
|
||||
.setContentIntent(mRemindPendingIntent)
|
||||
.build();
|
||||
setRemindNotificationRemoteViews(service, notificationMessage);
|
||||
}
|
||||
|
||||
public static void cancelRemindNotification(Context context){
|
||||
// 获取 NotificationManager 实例
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
// 撤回指定 ID 的通知栏消息
|
||||
notificationManager.cancel(_mnRemindNotificationID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 权限申请工具类
|
||||
* 适配 Android 13+ 媒体权限 & 低版本存储权限
|
||||
* 兼容 SDK 版本低于 33 的编译环境
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
private static final String TAG = "PermissionUtils";
|
||||
// 存储权限请求码
|
||||
public static final int REQUEST_STORAGE = 1000;
|
||||
// 媒体图片权限请求码(Android 13+)
|
||||
public static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
// 手动定义 Android 13+ 媒体图片权限常量(解决 SDK 低于 33 无法识别问题)
|
||||
private static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES";
|
||||
// Android 13 对应的 SDK 版本号(替代 Build.VERSION_CODES.TIRAMISU)
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
|
||||
// 单例模式
|
||||
private static volatile PermissionUtils sInstance;
|
||||
|
||||
private PermissionUtils() {}
|
||||
|
||||
public static PermissionUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (PermissionUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PermissionUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并请求 存储权限(Android 12及以下)
|
||||
*/
|
||||
public boolean checkAndRequestStoragePermission(Activity activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11+ 无需申请 READ_EXTERNAL_STORAGE,直接返回true
|
||||
return true;
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
String[] permissions = {
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
};
|
||||
if (ContextCompat.checkSelfPermission(activity, permissions[0]) != PackageManager.PERMISSION_GRANTED
|
||||
|| ContextCompat.checkSelfPermission(activity, permissions[1]) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity, permissions, REQUEST_STORAGE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并请求 媒体图片权限(Android 13+)
|
||||
* 兼容 SDK 编译版本低于 33 的情况
|
||||
*/
|
||||
public boolean checkAndRequestMediaImagesPermission(Activity activity, int requestCode) {
|
||||
// 用数值 33 替代 Build.VERSION_CODES.TIRAMISU
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
// 用手动定义的权限常量替代 android.Manifest.permission.READ_MEDIA_IMAGES
|
||||
if (ContextCompat.checkSelfPermission(activity, READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity, new String[]{READ_MEDIA_IMAGES}, requestCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 低版本已通过存储权限覆盖,直接返回true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限请求结果处理
|
||||
*/
|
||||
public void handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_STORAGE:
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "存储权限申请成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "存储权限申请失败");
|
||||
}
|
||||
break;
|
||||
case REQUEST_READ_MEDIA_IMAGES:
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "媒体图片权限申请成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "媒体图片权限申请失败");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LogUtils.d(TAG, "未知权限请求码:" + requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有管理所有文件权限(Android 11+)
|
||||
*/
|
||||
public boolean checkManageExternalStoragePermission(Activity activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return android.os.Environment.isExternalStorageManager();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求管理所有文件权限(Android 11+)
|
||||
*/
|
||||
public void requestManageExternalStoragePermission(Activity activity, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
android.content.Intent intent = new android.content.Intent(
|
||||
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
||||
);
|
||||
intent.setData(android.net.Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "请求管理文件权限异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/21 18:55
|
||||
* @Describe
|
||||
* 图片下载工具类(指定目录保存:Pictures/PowerBell/BackgroundHistory)
|
||||
*/
|
||||
public class PictureUtils {
|
||||
private static final String TAG = "PictureUtils";
|
||||
private static final String ROOT_DIR = "PowerBell/BackgroundHistory"; // 自定义目录结构
|
||||
private static OkHttpClient sOkHttpClient;
|
||||
|
||||
static {
|
||||
sOkHttpClient = new OkHttpClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载网络图片到指定目录(外部存储/Pictures/PowerBell/BackgroundHistory)
|
||||
* @param context 上下文(用于通知相册刷新)
|
||||
* @param imgUrl 图片网络URL
|
||||
* @param callback 下载结果回调(成功/失败)
|
||||
*/
|
||||
public static void downloadImageToAlbum(final Context context, final String imgUrl, final DownloadCallback callback) {
|
||||
// 检查参数合法性
|
||||
if (context == null) {
|
||||
if (callback != null) {
|
||||
callback.onFailure(new IllegalArgumentException("Context不能为空"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (imgUrl == null || imgUrl.isEmpty()) {
|
||||
if (callback != null) {
|
||||
callback.onFailure(new IllegalArgumentException("图片URL为空"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
startDownload(context, imgUrl, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行实际的下载逻辑
|
||||
*/
|
||||
private static void startDownload(final Context context, final String imgUrl, final DownloadCallback callback) {
|
||||
Request request = new Request.Builder().url(imgUrl).build();
|
||||
sOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (!response.isSuccessful()) {
|
||||
if (callback != null) {
|
||||
callback.onFailure(new IOException("请求失败,响应码:" + response.code()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
inputStream = response.body().byteStream();
|
||||
// 1. 获取并创建指定保存目录(外部存储/Pictures/PowerBell/BackgroundHistory)
|
||||
File saveDir = getTargetSaveDir(context);
|
||||
if (!saveDir.exists()) {
|
||||
boolean isDirCreated = saveDir.mkdirs(); // 递归创建多级目录
|
||||
if (!isDirCreated) {
|
||||
if (callback != null) {
|
||||
callback.onFailure(new IOException("创建目录失败:" + saveDir.getAbsolutePath()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 解析图片后缀
|
||||
String fileSuffix = getImageSuffix(imgUrl, response);
|
||||
// 3. 生成时间戳文件名
|
||||
String fileName = generateTimeFileName() + fileSuffix;
|
||||
// 4. 创建文件
|
||||
final File saveFile = new File(saveDir, fileName);
|
||||
|
||||
// 5. 写入文件
|
||||
outputStream = new FileOutputStream(saveFile);
|
||||
byte[] buffer = new byte[1024 * 4];
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, len);
|
||||
}
|
||||
outputStream.flush();
|
||||
|
||||
// 6. 通知相册刷新(使图片显示在系统相册中)
|
||||
notifyAlbumRefresh(context, saveFile);
|
||||
|
||||
// 成功回调
|
||||
if (callback != null) {
|
||||
callback.onSuccess(saveFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "下载图片异常", e);
|
||||
if (callback != null) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
} finally {
|
||||
// 关闭资源
|
||||
if (inputStream != null) inputStream.close();
|
||||
if (outputStream != null) outputStream.close();
|
||||
if (response.body() != null) response.body().close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call call, final IOException e) {
|
||||
Log.e(TAG, "下载图片失败", e);
|
||||
if (callback != null) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标保存目录:外部存储/Pictures/PowerBell/BackgroundHistory
|
||||
*/
|
||||
private static File getTargetSaveDir(Context context) {
|
||||
// 优先使用公共Pictures目录(兼容多数设备)
|
||||
File publicPicturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
if (publicPicturesDir.exists()) {
|
||||
return new File(publicPicturesDir, ROOT_DIR);
|
||||
}
|
||||
// 备选:应用私有Pictures目录(若公共目录不可用)
|
||||
File appPicturesDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
||||
if (appPicturesDir != null) {
|
||||
return new File(appPicturesDir, ROOT_DIR);
|
||||
}
|
||||
// 极端情况:外部存储根目录
|
||||
return new File(Environment.getExternalStorageDirectory(), ROOT_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析图片后缀名
|
||||
*/
|
||||
private static String getImageSuffix(String imgUrl, Response response) {
|
||||
// 优先从URL解析
|
||||
if (imgUrl.lastIndexOf('.') != -1) {
|
||||
String suffix = imgUrl.substring(imgUrl.lastIndexOf('.'));
|
||||
if (suffix.length() <= 5 && (suffix.contains("png") || suffix.contains("jpg") || suffix.contains("jpeg") || suffix.contains("gif"))) {
|
||||
return suffix.toLowerCase(Locale.getDefault());
|
||||
}
|
||||
}
|
||||
// 从响应头解析
|
||||
String contentType = response.header("Content-Type");
|
||||
if (contentType != null) {
|
||||
if (contentType.contains("png")) return ".png";
|
||||
if (contentType.contains("jpeg") || contentType.contains("jpg")) return ".jpg";
|
||||
if (contentType.contains("gif")) return ".gif";
|
||||
}
|
||||
return ".jpg"; // 默认后缀
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成时间戳文件名
|
||||
*/
|
||||
private static String generateTimeFileName() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault());
|
||||
return sdf.format(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知相册刷新
|
||||
*/
|
||||
private static void notifyAlbumRefresh(Context context, File file) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
Uri uri = Uri.fromFile(file);
|
||||
intent.setData(uri);
|
||||
context.sendBroadcast(intent);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "通知相册刷新失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载结果回调接口
|
||||
*/
|
||||
public interface DownloadCallback {
|
||||
void onSuccess(String savePath); // 下载成功(子线程回调)
|
||||
void onFailure(Exception e); // 下载失败(子线程回调)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class StringUtils {
|
||||
|
||||
@@ -10,17 +10,15 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class UriUtils {
|
||||
public class UriUtil {
|
||||
|
||||
public static final String TAG = "UriUtil";
|
||||
|
||||
@@ -105,85 +103,15 @@ public class UriUtils {
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public static Uri getUriForFile(Context context, String filePath) {
|
||||
// 1. 打印传入的文件路径
|
||||
LogUtils.d(TAG, "getUriForFile -> 传入路径:" + filePath);
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
LogUtils.e(TAG, "getUriForFile -> 传入路径为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
// 2. 打印File对象的绝对路径和存在性
|
||||
LogUtils.d(TAG, "getUriForFile -> 文件绝对路径:" + file.getAbsolutePath());
|
||||
LogUtils.d(TAG, "getUriForFile -> 文件是否存在:" + file.exists());
|
||||
LogUtils.d(TAG, "getUriForFile -> 是否为目录:" + file.isDirectory());
|
||||
public static Uri getUriForFile(Context context, File file) {
|
||||
//Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
||||
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
|
||||
return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
||||
}
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
|
||||
// 3. 合法性校验
|
||||
if (!file.exists() || file.isDirectory()) {
|
||||
LogUtils.e(TAG, "getUriForFile -> 非法路径:文件不存在或为目录");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. 校验路径是否在配置的合法目录内
|
||||
String appFilesDir = context.getExternalFilesDir(null) != null ? context.getExternalFilesDir(null).getAbsolutePath() : "null";
|
||||
String publicPicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBell/";
|
||||
String internalFilesDir = context.getFilesDir().getAbsolutePath();
|
||||
String cacheDir = context.getCacheDir().getAbsolutePath();
|
||||
|
||||
String absolutePath = file.getAbsolutePath();
|
||||
boolean isInConfigDir = absolutePath.startsWith(appFilesDir)
|
||||
|| absolutePath.startsWith(publicPicDir)
|
||||
|| absolutePath.startsWith(internalFilesDir)
|
||||
|| absolutePath.startsWith(cacheDir);
|
||||
LogUtils.d(TAG, "getUriForFile -> 路径是否在配置目录内:" + isInConfigDir);
|
||||
if (!isInConfigDir) {
|
||||
LogUtils.w(TAG, "getUriForFile -> 路径不在FileProvider配置范围内,可能导致异常");
|
||||
// 非强制拦截,保留原有逻辑,仅警告
|
||||
}
|
||||
|
||||
return getUriForFile(context, file);
|
||||
}
|
||||
|
||||
public static Uri getUriForFile(Context context, File file) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getUriForFile -> Context为空");
|
||||
return null;
|
||||
}
|
||||
if (file == null) {
|
||||
LogUtils.e(TAG, "getUriForFile -> File对象为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 二次校验文件状态
|
||||
LogUtils.d(TAG, "getUriForFile(File) -> 文件路径:" + file.getAbsolutePath());
|
||||
if (!file.exists() || file.isDirectory()) {
|
||||
LogUtils.e(TAG, "getUriForFile(File) -> 文件不存在或为目录");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 版本判断与Uri生成
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
LogUtils.d(TAG, "getUriForFile -> Android 7.0+,使用FileProvider生成Uri");
|
||||
try {
|
||||
String authority = context.getPackageName() + ".fileprovider";
|
||||
LogUtils.d(TAG, "getUriForFile -> FileProvider authority:" + authority);
|
||||
Uri uri = FileProvider.getUriForFile(context, authority, file);
|
||||
LogUtils.d(TAG, "getUriForFile -> 生成Content Uri成功:" + uri.toString());
|
||||
return uri;
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "getUriForFile -> FileProvider生成Uri失败:路径未配置或权限不足", e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "getUriForFile -> Android 7.0以下,使用Uri.fromFile生成Uri");
|
||||
Uri uri = Uri.fromFile(file);
|
||||
LogUtils.d(TAG, "getUriForFile -> 生成File Uri成功:" + uri.toString());
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
|
||||
throws IOException {
|
||||
File targetFile = null;
|
||||
@@ -4,228 +4,327 @@ import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
/**
|
||||
* 基于Java7的BackgroundView(LinearLayout+ImageView,保持原图比例居中平铺)
|
||||
* 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/19 18:01
|
||||
* @Describe 背景图片视图控件(支持预览临时图片 + 外部刷新)
|
||||
*/
|
||||
public class BackgroundView extends RelativeLayout {
|
||||
|
||||
public static final String TAG = "BackgroundView";
|
||||
// 新增:记录当前已缓存的图片路径
|
||||
private String mCurrentCachedPath = "";
|
||||
|
||||
private Context mContext;
|
||||
private LinearLayout mLlContainer; // 主容器LinearLayout
|
||||
private ImageView mIvBackground; // 图片显示控件
|
||||
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高)
|
||||
Context mContext;
|
||||
private ImageView ivBackground;
|
||||
|
||||
private static String BACKGROUND_IMAGE_FOLDER = "Background";
|
||||
private static String BACKGROUND_IMAGE_FILENAME = "current.data";
|
||||
private static String BACKGROUND_IMAGE_PREVIEW_FILENAME = "current_preview.data";
|
||||
private static String backgroundSourceFilePath;
|
||||
private float imageAspectRatio = 1.0f; // 默认 1:1
|
||||
// 标记当前是否处于预览状态
|
||||
private boolean isPreviewMode = false;
|
||||
|
||||
// ====================================== 构造器(Java7兼容) ======================================
|
||||
public BackgroundView(Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ===");
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
public BackgroundView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ===");
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ===");
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
// ====================================== 初始化 ======================================
|
||||
private void initView() {
|
||||
LogUtils.d(TAG, "=== initView 启动 ===");
|
||||
// 1. 配置当前控件:全屏+透明
|
||||
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
|
||||
// 2. 初始化主容器LinearLayout
|
||||
initLinearLayout();
|
||||
|
||||
// 3. 初始化ImageView
|
||||
initImageView();
|
||||
|
||||
// 初始设置透明背景
|
||||
setDefaultTransparentBackground();
|
||||
|
||||
LogUtils.d(TAG, "=== initView 完成 ===");
|
||||
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
this.mContext = context;
|
||||
initView();
|
||||
}
|
||||
|
||||
private void initLinearLayout() {
|
||||
LogUtils.d(TAG, "=== initLinearLayout 启动 ===");
|
||||
mLlContainer = new LinearLayout(mContext);
|
||||
// 配置LinearLayout:全屏+垂直方向+居中
|
||||
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
mLlContainer.setLayoutParams(llParams);
|
||||
mLlContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中
|
||||
mLlContainer.setBackgroundColor(0x00000000);
|
||||
this.addView(mLlContainer);
|
||||
LogUtils.d(TAG, "=== initLinearLayout 完成 ===");
|
||||
void initView() {
|
||||
initBackgroundImageView();
|
||||
initBackgroundImagePath();
|
||||
loadAndSetImageViewBackground();
|
||||
}
|
||||
|
||||
private void initImageView() {
|
||||
LogUtils.d(TAG, "=== initImageView 启动 ===");
|
||||
mIvBackground = new ImageView(mContext);
|
||||
// 配置ImageView:wrap_content+居中+透明背景
|
||||
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
mIvBackground.setLayoutParams(ivParams);
|
||||
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺
|
||||
mIvBackground.setBackgroundColor(0x00000000);
|
||||
mLlContainer.addView(mIvBackground);
|
||||
LogUtils.d(TAG, "=== initImageView 完成 ===");
|
||||
private void initBackgroundImageView() {
|
||||
ivBackground = new ImageView(mContext);
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
|
||||
ivBackground.setLayoutParams(layoutParams);
|
||||
ivBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
this.addView(ivBackground);
|
||||
}
|
||||
|
||||
public void loadBackgroundBean(BackgroundBean bean) {
|
||||
loadBackgroundBean(bean, false);
|
||||
}
|
||||
|
||||
public void loadBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
if (!bean.isUseBackgroundFile()) {
|
||||
setDefaultTransparentBackground();
|
||||
private void initBackgroundImagePath() {
|
||||
File externalFilesDir = mContext.getExternalFilesDir(null);
|
||||
if (externalFilesDir == null) {
|
||||
LogUtils.e(TAG, "外置存储不可用,无法初始化背景图片路径");
|
||||
return;
|
||||
}
|
||||
String targetPath = bean.isUseBackgroundScaledCompressFile()
|
||||
? bean.getBackgroundScaledCompressFilePath()
|
||||
: bean.getBackgroundFilePath();
|
||||
|
||||
if (!(new File(targetPath).exists())) {
|
||||
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用带路径判断的loadImage方法
|
||||
if (isRefresh) {
|
||||
App._mBitmapCacheUtils.removeCachedBitmap(targetPath);
|
||||
App._mBitmapCacheUtils.cacheBitmap(targetPath);
|
||||
}
|
||||
loadImage(targetPath);
|
||||
File backgroundDir = new File(externalFilesDir, BACKGROUND_IMAGE_FOLDER);
|
||||
if (!backgroundDir.exists()) {
|
||||
backgroundDir.mkdirs();
|
||||
}
|
||||
backgroundSourceFilePath = new File(backgroundDir, BACKGROUND_IMAGE_FILENAME).getAbsolutePath();
|
||||
}
|
||||
|
||||
// ====================================== 对外方法 ======================================
|
||||
/**
|
||||
* 改造后:添加路径判断,路径更新时同步更新缓存;缓存Bitmap为null时提示并加载透明背景
|
||||
* @param imagePath 图片绝对路径
|
||||
* 拷贝图片文件到背景资源目录(正式背景)
|
||||
*/
|
||||
public void loadImage(String imagePath) {
|
||||
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
setDefaultTransparentBackground();
|
||||
public void saveToBackgroundSources(String srcBackgroundPath) {
|
||||
initBackgroundImagePath();
|
||||
if (backgroundSourceFilePath == null) {
|
||||
LogUtils.e(TAG, "目标路径初始化失败,无法保存背景图片");
|
||||
return;
|
||||
}
|
||||
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile()) {
|
||||
LogUtils.e(TAG, "图片文件无效");
|
||||
setDefaultTransparentBackground();
|
||||
File srcFile = new File(srcBackgroundPath);
|
||||
if (!srcFile.exists() || !srcFile.isFile()) {
|
||||
LogUtils.e(TAG, String.format("源文件不存在或不是文件:%s", srcBackgroundPath));
|
||||
return;
|
||||
}
|
||||
|
||||
mIvBackground.setVisibility(View.GONE);
|
||||
|
||||
// ======================== 新增:路径判断逻辑 ========================
|
||||
// 1. 路径未变化:直接使用缓存
|
||||
if (imagePath.equals(mCurrentCachedPath)) {
|
||||
Bitmap cachedBitmap = App._mBitmapCacheUtils.getCachedBitmap(imagePath);
|
||||
// 核心修改:判断缓存Bitmap是否为null
|
||||
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
|
||||
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
|
||||
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
|
||||
mIvBackground.setImageBitmap(cachedBitmap);
|
||||
adjustImageViewSize();
|
||||
mIvBackground.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
} else {
|
||||
// 缓存Bitmap为空或已回收,提示并加载透明背景
|
||||
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
|
||||
ToastUtils.show("全局位图缓存为空,无法加载图片");
|
||||
setDefaultTransparentBackground();
|
||||
File destFile = new File(backgroundSourceFilePath);
|
||||
File destDir = destFile.getParentFile();
|
||||
if (destDir != null && !destDir.exists()) {
|
||||
boolean isDirCreated = destDir.mkdirs();
|
||||
if (!isDirCreated) {
|
||||
LogUtils.e(TAG, "目标目录创建失败:" + destDir.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
|
||||
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
|
||||
App._mBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
|
||||
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
|
||||
FileInputStream fis = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fis = new FileInputStream(srcFile);
|
||||
fos = new FileOutputStream(destFile);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while ((len = fis.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, len);
|
||||
}
|
||||
fos.flush();
|
||||
|
||||
LogUtils.d(TAG, String.format("文件拷贝成功:%s -> %s", srcBackgroundPath, backgroundSourceFilePath));
|
||||
// 拷贝成功后,若处于预览模式则退出预览,加载正式背景
|
||||
if (isPreviewMode) {
|
||||
exitPreviewMode();
|
||||
} else {
|
||||
loadAndSetImageViewBackground();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, String.format("文件拷贝失败:%s", e.getMessage()), e);
|
||||
if (destFile.exists()) {
|
||||
destFile.delete();
|
||||
LogUtils.d(TAG, "已删除损坏的目标文件");
|
||||
}
|
||||
} finally {
|
||||
if (fis != null) {
|
||||
try {
|
||||
fis.close();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
// ======================== 路径判断逻辑结束 ========================
|
||||
|
||||
// 无缓存/路径更新:走原有逻辑加载图片
|
||||
if (!calculateImageAspectRatio(imageFile)) {
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "loadImage: 图片解码失败");
|
||||
ToastUtils.show("图片解码失败,无法加载");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 缓存新图片,并更新当前缓存路径记录
|
||||
App._mBitmapCacheUtils.cacheBitmap(imagePath);
|
||||
mCurrentCachedPath = imagePath;
|
||||
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
|
||||
|
||||
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
|
||||
adjustImageViewSize();
|
||||
mIvBackground.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "=== loadImage 完成 ===");
|
||||
}
|
||||
|
||||
// ====================================== 内部工具方法 ======================================
|
||||
/**
|
||||
* 【新增公共函数】预览临时图片(不修改正式背景文件)
|
||||
* @param previewImagePath 临时预览图片的路径
|
||||
*/
|
||||
public void previewBackgroundImage(String previewImagePath) {
|
||||
if (previewImagePath == null || previewImagePath.isEmpty()) {
|
||||
LogUtils.e(TAG, "预览图片路径为空");
|
||||
return;
|
||||
}
|
||||
|
||||
File previewFile = new File(previewImagePath);
|
||||
if (!previewFile.exists() || !previewFile.isFile()) {
|
||||
LogUtils.e(TAG, "预览图片不存在或不是文件:" + previewImagePath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算预览图片宽高比
|
||||
if (!calculateImageAspectRatio(previewFile)) {
|
||||
LogUtils.e(TAG, "预览图片尺寸无效,无法预览");
|
||||
return;
|
||||
}
|
||||
|
||||
// 压缩加载预览图片
|
||||
Bitmap previewBitmap = decodeBitmapWithCompress(previewFile, 1080, 1920);
|
||||
if (previewBitmap == null) {
|
||||
LogUtils.e(TAG, "预览图片加载失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置预览图片到 ImageView
|
||||
Drawable previewDrawable = new BitmapDrawable(mContext.getResources(), previewBitmap);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
ivBackground.setBackground(previewDrawable);
|
||||
} else {
|
||||
ivBackground.setBackgroundDrawable(previewDrawable);
|
||||
}
|
||||
|
||||
// 调整 ImageView 尺寸以匹配预览图片宽高比
|
||||
adjustImageViewSize();
|
||||
isPreviewMode = true;
|
||||
LogUtils.d(TAG, "进入预览模式,预览图片路径:" + previewImagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【新增公共函数】退出预览模式,恢复显示正式背景图片
|
||||
*/
|
||||
public void exitPreviewMode() {
|
||||
if (isPreviewMode) {
|
||||
loadAndSetImageViewBackground();
|
||||
isPreviewMode = false;
|
||||
LogUtils.d(TAG, "退出预览模式,恢复正式背景");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共函数:供外部类调用,重新加载正式背景图片(刷新显示)
|
||||
*/
|
||||
public void reloadBackgroundImage() {
|
||||
LogUtils.d(TAG, "外部调用重新加载背景图片");
|
||||
initBackgroundImagePath();
|
||||
loadAndSetImageViewBackground();
|
||||
// 若处于预览模式,退出预览
|
||||
if (isPreviewMode) {
|
||||
isPreviewMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载正式背景图片并设置到 ImageView
|
||||
*/
|
||||
private void loadAndSetImageViewBackground() {
|
||||
if (backgroundSourceFilePath == null) {
|
||||
setDefaultImageViewBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
File backgroundFile = new File(backgroundSourceFilePath);
|
||||
if (!backgroundFile.exists() || !backgroundFile.isFile()) {
|
||||
LogUtils.e(TAG, "背景图片不存在:" + backgroundSourceFilePath);
|
||||
setDefaultImageViewBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calculateImageAspectRatio(backgroundFile)) {
|
||||
setDefaultImageViewBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bitmap = decodeBitmapWithCompress(backgroundFile, 1080, 1920);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "图片加载失败,无法解析为 Bitmap");
|
||||
setDefaultImageViewBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable backgroundDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
ivBackground.setBackground(backgroundDrawable);
|
||||
} else {
|
||||
ivBackground.setBackgroundDrawable(backgroundDrawable);
|
||||
}
|
||||
|
||||
adjustImageViewSize();
|
||||
LogUtils.d(TAG, "ImageView 背景加载成功,宽高比:" + imageAspectRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算图片宽高比(宽/高)
|
||||
*/
|
||||
private boolean calculateImageAspectRatio(File file) {
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
|
||||
int width = options.outWidth;
|
||||
int height = options.outHeight;
|
||||
if (width <= 0 || height <= 0) {
|
||||
LogUtils.e(TAG, "图片尺寸无效");
|
||||
int imageWidth = options.outWidth;
|
||||
int imageHeight = options.outHeight;
|
||||
|
||||
if (imageWidth <= 0 || imageHeight <= 0) {
|
||||
LogUtils.e(TAG, "图片尺寸无效:宽=" + imageWidth + ", 高=" + imageHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
mImageAspectRatio = (float) width / height;
|
||||
LogUtils.d(TAG, "原图比例:" + mImageAspectRatio);
|
||||
imageAspectRatio = (float) imageWidth / imageHeight;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "计算比例失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, "计算图片宽高比失败:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态调整 ImageView 尺寸以匹配图片宽高比
|
||||
*/
|
||||
private void adjustImageViewSize() {
|
||||
int parentWidth = getWidth();
|
||||
int parentHeight = getHeight();
|
||||
|
||||
if (parentWidth == 0 || parentHeight == 0) {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adjustImageViewSize();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
int imageViewWidth, imageViewHeight;
|
||||
if (imageAspectRatio >= 1.0f) { // 横图
|
||||
imageViewWidth = Math.min(parentWidth, (int) (parentHeight * imageAspectRatio));
|
||||
imageViewHeight = (int) (imageViewWidth / imageAspectRatio);
|
||||
} else { // 竖图
|
||||
imageViewHeight = Math.min(parentHeight, (int) (parentWidth / imageAspectRatio));
|
||||
imageViewWidth = (int) (imageViewHeight * imageAspectRatio);
|
||||
}
|
||||
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivBackground.getLayoutParams();
|
||||
layoutParams.width = imageViewWidth;
|
||||
layoutParams.height = imageViewHeight;
|
||||
ivBackground.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带压缩的 Bitmap 解码(避免 OOM)
|
||||
*/
|
||||
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) {
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
@@ -235,59 +334,41 @@ public class BackgroundView extends RelativeLayout {
|
||||
int scaleX = options.outWidth / maxWidth;
|
||||
int scaleY = options.outHeight / maxHeight;
|
||||
int inSampleSize = Math.max(scaleX, scaleY);
|
||||
if (inSampleSize <= 0) inSampleSize = 1;
|
||||
if (inSampleSize <= 0) {
|
||||
inSampleSize = 1;
|
||||
}
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = inSampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, "图片压缩加载失败:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustImageViewSize() {
|
||||
if (mLlContainer == null || mIvBackground == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int llWidth = mLlContainer.getWidth();
|
||||
int llHeight = mLlContainer.getHeight();
|
||||
|
||||
if (llWidth != 0 && llHeight != 0) {
|
||||
int ivWidth, ivHeight;
|
||||
if (mImageAspectRatio >= 1.0f) {
|
||||
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
|
||||
ivHeight = (int) (ivWidth / mImageAspectRatio);
|
||||
} else {
|
||||
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
|
||||
ivWidth = (int) (ivHeight * mImageAspectRatio);
|
||||
}
|
||||
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
|
||||
params.width = ivWidth;
|
||||
params.height = ivHeight;
|
||||
mIvBackground.setLayoutParams(params);
|
||||
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
|
||||
mIvBackground.setVisibility(View.VISIBLE);
|
||||
}
|
||||
/**
|
||||
* 设置默认背景(图片加载失败时兜底)
|
||||
*/
|
||||
private void setDefaultImageViewBackground() {
|
||||
ivBackground.setBackgroundResource(R.drawable.default_background);
|
||||
imageAspectRatio = 1.0f;
|
||||
adjustImageViewSize();
|
||||
LogUtils.d(TAG, "已设置 ImageView 默认背景");
|
||||
}
|
||||
|
||||
private void setDefaultTransparentBackground() {
|
||||
mIvBackground.setImageBitmap(null);
|
||||
mIvBackground.setBackgroundColor(0x00000000);
|
||||
mImageAspectRatio = 1.0f;
|
||||
// 清空缓存路径记录
|
||||
mCurrentCachedPath = "";
|
||||
mIvBackground.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// ====================================== 重写方法 ======================================
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
adjustImageViewSize(); // 尺寸变化时重新调整
|
||||
adjustImageViewSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供:判断当前是否处于预览模式
|
||||
*/
|
||||
public boolean isPreviewMode() {
|
||||
return isPreviewMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
powerbell/src/main/res/layout/activity_about.xml
Normal file
31
powerbell/src/main/res/layout/activity_about.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/root_ll"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,150 +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:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF28C000">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF3243E2"
|
||||
android:id="@+id/background_view">
|
||||
|
||||
</cc.winboll.studio.powerbell.views.BackgroundView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:background="#B92FABE6">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Origin BG"
|
||||
android:id="@+id/activitybackgroundpictureAButton5"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_margin="5dp"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Received BG"
|
||||
android:id="@+id/activitybackgroundpictureAButton4"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_margin="5dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="◎"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton1"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="☑"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton2"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="♾"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton9"
|
||||
android:onClick="onNetworkBackgroundDialog"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton3"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+~]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton6"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[◐]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton7"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[○]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton8"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
140
powerbell/src/main/res/layout/activity_backgroundpicture.xml
Normal file
140
powerbell/src/main/res/layout/activity_backgroundpicture.xml
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitybackgroundpictureRelativeLayout1"/>
|
||||
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF7381FF"
|
||||
android:id="@+id/activitybackgroundpictureBackgroundView1">
|
||||
|
||||
</cc.winboll.studio.powerbell.views.BackgroundView>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/toolbar">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Origin BG"
|
||||
android:id="@+id/activitybackgroundpictureAButton5"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_margin="5dp"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Received BG"
|
||||
android:id="@+id/activitybackgroundpictureAButton4"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_margin="5dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="◎"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton1"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="☑"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton2"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="♾"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton9"
|
||||
android:onClick="onNetworkBackgroundDialog"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton3"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+~]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton6"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[◐]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton7"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[○]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton8"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -5,13 +5,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<!-- 搜索框:提示文本改为“搜索应用名称或包名” -->
|
||||
<EditText
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
<cc.winboll.studio.libaes.views.AToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -1,259 +1,40 @@
|
||||
<?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">
|
||||
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">
|
||||
|
||||
<!-- 顶部Toolbar(首屏核心,同步加载,保留原有ASupportToolbar) -->
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<!-- 主内容区(优化层级,减少冗余RelativeLayout) -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<!-- 首屏核心容器(合并原冗余RelativeLayout,减少层级) -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitymainRelativeLayout1">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitymainRelativeLayout1"
|
||||
android:background="#FFB7B7B7"/>
|
||||
|
||||
<!-- 1. 背景视图(首屏核心,同步加载,保留原有) -->
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/fragmentmainviewBackgroundView1"/>
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitymainFrameLayout1"/>
|
||||
|
||||
<!-- 2. 功能控件容器(首屏核心,同步加载,保留原有结构) -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- 服务总开关布局 -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentmainviewLinearLayout3"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/bg_frame">
|
||||
|
||||
<Switch
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewSwitch1"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0"
|
||||
android:textSize="@dimen/text_title_size"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 电量控制核心布局(SeekBar+图标) -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<!-- 耗电提醒布局 -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:id="@+id/fragmentmainviewLinearLayout1">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/usege"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:id="@+id/fragmentmainviewCheckBox2"/>
|
||||
|
||||
<cc.winboll.studio.powerbell.views.VerticalSeekBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewVerticalSeekBar2"
|
||||
android:progressTint="@color/colorUsege"
|
||||
android:progressBackgroundTint="@color/colorUsege"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_margin="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 耗电提醒数值+图标 -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="@dimen/text_title_size"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:id="@+id/fragmentandroidviewTextView3"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewImageView2"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 当前电量数值+图标 -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="@dimen/text_title_size"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center_horizontal"
|
||||
android:id="@+id/fragmentandroidviewTextView4"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewImageView1"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 充电提醒数值+图标 -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="@dimen/text_title_size"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:id="@+id/fragmentandroidviewTextView2"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewImageView3"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 充电提醒布局 -->
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:id="@+id/fragmentmainviewLinearLayout2">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/charge"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:id="@+id/fragmentmainviewCheckBox1"/>
|
||||
|
||||
<cc.winboll.studio.powerbell.views.VerticalSeekBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewVerticalSeekBar1"
|
||||
android:progressTint="@color/colorCharge"
|
||||
android:progressBackgroundTint="@color/colorCharge"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_margin="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Tips文本 -->
|
||||
<LinearLayout
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Tips"
|
||||
android:textSize="@dimen/text_content_size"
|
||||
android:id="@+id/fragmentandroidviewTextView1"
|
||||
android:background="@drawable/bg_frame"
|
||||
android:padding="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 3. 广告视图:关键优化→用ViewStub延迟加载(替代原直接加载的ADsBannerView) -->
|
||||
<!-- 首次启动仅占位(1px),不inflate真实广告视图,减少首次耗时 -->
|
||||
<ViewStub
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/stub_ads_banner"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout="@layout/view_ads_banner"/> <!-- 广告视图独立布局文件 -->
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -5,47 +5,11 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:orientation="vertical"
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF0C6BBF">
|
||||
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/backgroundview"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#AF4FDA4E">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Main"
|
||||
android:id="@+id/btn_main_activity"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TestCropImage"
|
||||
android:id="@+id/btn_test_cropimage"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</RelativeLayout>
|
||||
android:id="@+id/activitymainunittestFrameLayout1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -1,37 +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:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.libaes.views.ASupportToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/toolbar"
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="CheckPermission"
|
||||
android:padding="10dp"
|
||||
android:onClick="onCheckPermission"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsControlView
|
||||
android:id="@+id/ads_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|center_horizontal">
|
||||
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
android:orientation="vertical"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:id="@+id/backgroundview"/>
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
android:id="@+id/dialogbackgroundpicturepreviewImageView1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
218
powerbell/src/main/res/layout/fragment_mainview.xml
Normal file
218
powerbell/src/main/res/layout/fragment_mainview.xml
Normal file
@@ -0,0 +1,218 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF7381FF"
|
||||
android:id="@+id/fragmentmainviewBackgroundView1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentmainviewLinearLayout3"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/bg_frame">
|
||||
|
||||
<Switch
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewSwitch1"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0"
|
||||
android:textSize="@dimen/text_title_size"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:id="@+id/fragmentmainviewLinearLayout1">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/usege"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:id="@+id/fragmentmainviewCheckBox2"/>
|
||||
|
||||
<cc.winboll.studio.powerbell.views.VerticalSeekBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewVerticalSeekBar2"
|
||||
android:progressTint="@color/colorUsege"
|
||||
android:progressBackgroundTint="@color/colorUsege"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_margin="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="@dimen/text_title_size"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:id="@+id/fragmentandroidviewTextView3"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewImageView2"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="@dimen/text_title_size"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center_horizontal"
|
||||
android:id="@+id/fragmentandroidviewTextView4"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewImageView1"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="@dimen/text_title_size"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:id="@+id/fragmentandroidviewTextView2"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewImageView3"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:id="@+id/fragmentmainviewLinearLayout2">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/charge"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:id="@+id/fragmentmainviewCheckBox1"/>
|
||||
|
||||
<cc.winboll.studio.powerbell.views.VerticalSeekBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/fragmentandroidviewVerticalSeekBar1"
|
||||
android:progressTint="@color/colorCharge"
|
||||
android:progressBackgroundTint="@color/colorCharge"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_margin="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Tips"
|
||||
android:textSize="@dimen/text_content_size"
|
||||
android:id="@+id/fragmentandroidviewTextView1"
|
||||
android:background="@drawable/bg_frame"
|
||||
android:padding="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF7381FF">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Main"
|
||||
android:id="@+id/btn_main_activity"/>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</cc.winboll.studio.powerbell.views.BackgroundView>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 广告视图独立布局(供ViewStub延迟加载) -->
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"/>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/bg_main">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/bg_imageview"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
<item
|
||||
android:id="@+id/action_changepicture"
|
||||
android:title="@string/item_changepicture"/>
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:title="@string/item_settings"/>
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:title="@string/item_about"/>
|
||||
android:title="@string/item_aboutview"/>
|
||||
</menu>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_log"
|
||||
android:title="@string/item_logview"/>
|
||||
<item
|
||||
android:id="@+id/action_unittestactivity"
|
||||
android:title="@string/item_mainunittestactivity"/>
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
<string name="app_name_cn2">泡额呗额</string>
|
||||
<string name="app_description">一个接收手机电量信息的应用,当电量值达到设定范围时会提醒用户。</string>
|
||||
<string name="about_crashed">本应用崩溃了,作者水平有限,敬请谅解!</string>
|
||||
<string name="item_main">主窗口</string>
|
||||
<string name="item_about">关于</string>
|
||||
<string name="item_settings">应用设置</string>
|
||||
<string name="item_battery_report">应用耗电记录</string>
|
||||
<string name="item_mainunittestactivity">开发调试窗口</string>
|
||||
<string name="item_clearrecord">清理电量记录</string>
|
||||
<string name="item_changepicture">更换背景图片</string>
|
||||
<string name="item_devoloperoptionsview">开发调试窗口</string>
|
||||
<string name="item_logview">日志窗口</string>
|
||||
<string name="item_sourceview">源码窗口</string>
|
||||
<string name="item_mainview">Main View</string>
|
||||
<string name="item_aboutview">About</string>
|
||||
<string name="item_clearrecord">Clear Record</string>
|
||||
<string name="item_changepicture">Change Picture</string>
|
||||
<string name="item_devoloperoptionsview">Developer View</string>
|
||||
<string name="item_logview">Log View</string>
|
||||
<string name="item_sourceview">Source View</string>
|
||||
<string name="txt_aboveswitch">消息总开关</string>
|
||||
<string name="txt_aboveswitchtips">当电量低于左边(放电状态)或高于右边(充电状态),就会发送一个提醒铃声。</string>
|
||||
<string name="texthint_CustomSlideToCleanRecord">Slide Right To Clean Up APP Record.</string>
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
<string name="switchto_en1">PowerBell</string>
|
||||
<string name="switchto_cn1">能源钟</string>
|
||||
<string name="switchto_cn2">泡额呗额</string>
|
||||
<string name="en1_switch_disabled">PowerBell X</string>
|
||||
<string name="cn1_switch_disabled">能源钟 X</string>
|
||||
<string name="cn2_switch_disabled">泡额呗额 X</string>
|
||||
<string name="en1_switch_disabled">PowerBell</string>
|
||||
<string name="cn1_switch_disabled">能源钟</string>
|
||||
<string name="cn2_switch_disabled">泡额呗额</string>
|
||||
<string name="item_mainview">Main View</string>
|
||||
<string name="item_about">About</string>
|
||||
<string name="item_settings">Settings</string>
|
||||
<string name="item_aboutview">About</string>
|
||||
<string name="item_battery_report">Battery Report</string>
|
||||
<string name="item_clearrecord">Clear Record</string>
|
||||
<string name="item_changepicture">Change Picture</string>
|
||||
@@ -31,15 +30,4 @@
|
||||
<string name="subtitle_activity_pixelpicker">Pixel Picker</string>
|
||||
<string name="subtitle_activity_about">About The APP</string>
|
||||
<string name="msg_AOHPCTCSeekBar_ClearRecord">>>>Seek 100% Right Is Clean Record.>>></string>
|
||||
|
||||
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
|
||||
<string name="permission_title">权限申请</string>
|
||||
<string name="permission_denied_title">权限被拒绝</string>
|
||||
<string name="permission_grant_success">权限获取成功,请重新操作</string>
|
||||
<string name="permission_storage_rationale">需要存储权限才能选择/拍照/裁剪图片,请授予权限</string>
|
||||
<string name="permission_storage_setting_guide">存储权限已被拒绝且勾选“不再询问”,请前往设置页开启权限</string>
|
||||
<string name="confirm">确定</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="go_to_setting">去设置</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,66 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- ====================================== 兼容适配:其他必要目录(可选)====================================== -->
|
||||
<!-- 应用内部缓存目录(适配少数依赖缓存的场景,如图片缓存)
|
||||
路径:/data/user/0/${applicationId}/cache/
|
||||
关联代码:getCacheDir() -->
|
||||
<paths>
|
||||
<external-path
|
||||
name="external_storage_root"
|
||||
path="." />
|
||||
<files-path
|
||||
name="files_path"
|
||||
path="." />
|
||||
<cache-path
|
||||
name="cache_path"
|
||||
path="." />
|
||||
|
||||
<!-- ====================================== 核心适配:应用私有外部目录(必选)====================================== -->
|
||||
<!-- 1. 裁剪临时文件目录(对应BackgroundSettingsActivity中onCreate的CropTemp目录)
|
||||
路径:/storage/emulated/0/Android/data/${applicationId}/files/Pictures/CropTemp/
|
||||
关联代码:_mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName) -->
|
||||
<external-files-path
|
||||
name="app_private_pictures"
|
||||
path="Pictures/" /> <!-- 仅保留1次,覆盖Pictures下所有子目录(含CropTemp) -->
|
||||
|
||||
<!-- 2. 背景图片目录(对应BackgroundSourceUtils的背景存储目录)
|
||||
路径:/storage/emulated/0/Android/data/${applicationId}/files/BackgroundPictureUtils/BackgroundSource/
|
||||
关联代码:mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()) -->
|
||||
<external-files-path
|
||||
name="background_source"
|
||||
path="BackgroundPictureUtils/BackgroundSource/" />
|
||||
|
||||
<!-- 应用私有外部存储目录(适配BackgroundSourceUtils的目录) -->
|
||||
<!--/storage/emulated/0/Android/data/...-->
|
||||
<external-files-path
|
||||
name="external_file_path"
|
||||
path="BackgroundSourceUtils/" /> <!-- 对应fUtilsDir(BackgroundSourceUtils根目录) -->
|
||||
|
||||
<!-- 应用外部缓存目录(适配Android11+ 外部缓存场景,如第三方SDK依赖)
|
||||
路径:/storage/emulated/0/Android/data/${applicationId}/cache/
|
||||
关联代码:getExternalCacheDir() -->
|
||||
path="." />
|
||||
<external-files-path
|
||||
name="files_root"
|
||||
path="mimoDownload" />
|
||||
<!--代表app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的目录-->
|
||||
<external-cache-path
|
||||
name="external_cache_path"
|
||||
path="." />
|
||||
|
||||
<!-- 3. 应用临时目录(对应App.getTempDirPath(),适配拍照临时文件)
|
||||
路径:/storage/emulated/0/Android/data/${applicationId}/files/temp/
|
||||
关联代码:mfPictureDir = new File(App.getTempDirPath())、mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg") -->
|
||||
<external-files-path
|
||||
name="app_temp"
|
||||
path="temp/" />
|
||||
|
||||
<!-- 通用应用外部文件目录(适配分享/下载等通用场景,覆盖files下所有目录)
|
||||
路径:/storage/emulated/0/Android/data/${applicationId}/files/
|
||||
关联代码:getExternalFilesDir(null) -->
|
||||
<external-files-path
|
||||
name="external_file_path"
|
||||
path="." />
|
||||
|
||||
<!-- 【核心添加】4. 应用内目录(getFilesDir(),对应别名 app_internal_files) -->
|
||||
<!-- 用于映射 /data/user/0/包名/files/ 路径,解决裁剪临时文件路径匹配问题 -->
|
||||
<files-path
|
||||
name="app_internal_files"
|
||||
path="." /> <!-- path="." 表示映射整个应用内目录 -->
|
||||
<!-- 关键新增:系统公共图片目录 /Pictures/PowerBell(图片存储/裁剪目录) -->
|
||||
<external-path
|
||||
name="public_pictures_powerbell"
|
||||
path="Pictures/PowerBell/" /> <!-- 路径:/storage/emulated/0/Pictures/PowerBell/ -->
|
||||
<!-- 兜底:应用内部缓存目录 -->
|
||||
<cache-path
|
||||
name="cache_path"
|
||||
path="BackgroundSourceUtils/" />
|
||||
<!--配置root-path。这样子可以读取到sd卡和一些应用分身的目录,否则微信分身保存的图片,就会导致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg,在小米6的手机上微信分身有这个crash,华为没有
|
||||
-->
|
||||
<root-path
|
||||
name="root_path"
|
||||
path="" />
|
||||
</paths>
|
||||
|
||||
@@ -18,16 +18,14 @@ def genVersionName(def versionName){
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
// 1. compileSdkVersion:必须 ≥ targetSdkVersion,建议直接等于 targetSdkVersion(30)
|
||||
compileSdkVersion 30
|
||||
|
||||
// 2. buildToolsVersion:需匹配 compileSdkVersion,建议使用 30.x.x 最新稳定版(无需高于 compileSdkVersion)
|
||||
buildToolsVersion "30.0.3" // 这是 30 对应的最新稳定版,避免使用 beta 版
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.winboll"
|
||||
minSdkVersion 23
|
||||
minSdkVersion 23
|
||||
// 适配MIUI12
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
@@ -43,6 +41,12 @@ android {
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -63,6 +67,10 @@ dependencies {
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
// OkHttp网络请求
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||
// FastJSON解析
|
||||
implementation 'com.alibaba:fastjson:1.2.76'
|
||||
|
||||
// AndroidX 类库
|
||||
api 'androidx.appcompat:appcompat:1.1.0'
|
||||
@@ -82,12 +90,12 @@ dependencies {
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
//api 'cc.winboll.studio:libaes:15.12.0'
|
||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
||||
api 'cc.winboll.studio:libaes:15.12.13'
|
||||
api 'cc.winboll.studio:libappbase:15.14.2'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.1'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Dec 07 03:42:32 HKT 2025
|
||||
stageCount=8
|
||||
#Sat Jan 10 02:54:10 GMT 2026
|
||||
stageCount=9
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.7
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.8
|
||||
publishVersion=15.11.8
|
||||
buildCount=13
|
||||
baseBetaVersion=15.11.9
|
||||
|
||||
BIN
winboll/libs/libWeWorkSpecSDK.so
Normal file
BIN
winboll/libs/libWeWorkSpecSDK.so
Normal file
Binary file not shown.
@@ -278,6 +278,10 @@
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.activities.SettingsActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.unittest.TestWeWorkSpecSDK"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.activities.WXPayActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -1,6 +1,6 @@
|
||||
package cc.winboll.studio.winboll;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
@@ -8,20 +8,17 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
||||
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.models.DrawerMenuBean;
|
||||
import cc.winboll.studio.libaes.unittests.TestAButtonFragment;
|
||||
import cc.winboll.studio.libaes.unittests.TestViewPageFragment;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.activities.SettingsActivity;
|
||||
import cc.winboll.studio.winboll.activities.WXPayActivity;
|
||||
import cc.winboll.studio.winboll.fragments.BrowserFragment;
|
||||
import java.util.ArrayList;
|
||||
import android.content.Intent;
|
||||
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
||||
|
||||
public class MainActivity extends DrawerFragmentActivity {
|
||||
|
||||
@@ -152,6 +149,8 @@ public class MainActivity extends DrawerFragmentActivity {
|
||||
}
|
||||
} else if (nItemId == R.id.item_settings) {
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
|
||||
} else if (nItemId == R.id.item_wxpayactivity) {
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), WXPayActivity.class);
|
||||
} else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) {
|
||||
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
APPInfo appInfo = genDefaultAPPInfo();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package cc.winboll.studio.winboll;
|
||||
|
||||
/**
|
||||
* 微信支付配置类
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/07
|
||||
*/
|
||||
public class WxPayConfig {
|
||||
// ========== 核心修改点:替换为你的服务端地址 ==========
|
||||
// 服务端IP/域名 + 端口(Docker部署的服务端,需确保安卓端可访问)
|
||||
public static final String BASE_URL = "https://wxpay.winboll.cc";
|
||||
|
||||
// 统一下单接口路径(对应服务端的测试接口)
|
||||
public static final String CREATE_ORDER_URL = BASE_URL + "/pay/createOrder";
|
||||
|
||||
// 订单查询接口路径
|
||||
public static final String QUERY_ORDER_URL = BASE_URL + "/pay/queryOrder";
|
||||
|
||||
// ========== 固定支付配置 ==========
|
||||
public static final String ORDER_BODY = "定额测试支付"; // 商品描述
|
||||
public static final int TOTAL_FEE = 1; // 固定金额:1分(沙箱环境推荐)
|
||||
public static final String TRADE_TYPE = "NATIVE"; // 支付类型:二维码
|
||||
|
||||
// ========== 轮询配置 ==========
|
||||
public static final long POLL_INTERVAL = 10000; // 轮询间隔:10秒
|
||||
public static final long POLL_TIMEOUT = 45000; // 轮询超时:45秒
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package cc.winboll.studio.winboll.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.WxPayConfig;
|
||||
import cc.winboll.studio.winboll.utils.SpecUtil;
|
||||
import cc.winboll.studio.winboll.utils.WxPayApi;
|
||||
import cc.winboll.studio.winboll.utils.ZXingUtils;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 主界面:生成二维码 + 轮询查询支付结果
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/07
|
||||
*/
|
||||
public class WXPayActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
private static final String TAG = "WXPayActivity";
|
||||
|
||||
// Handler消息标识
|
||||
private static final int MSG_POLL_TIMEOUT = 1001;
|
||||
private static final int MSG_POLL_SUCCESS = 1002;
|
||||
private static final int MSG_POLL_FAILED = 1003;
|
||||
|
||||
private ImageView mIvQrCode;
|
||||
private TextView mTvOrderNo;
|
||||
private TextView mTvPayStatus;
|
||||
private Button mBtnCreateOrder;
|
||||
|
||||
private String mOutTradeNo; // 商户订单号
|
||||
private Timer mPollTimer; // 轮询定时器
|
||||
private long mPollStartTime; // 轮询开始时间
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private Handler mPollHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
switch (msg.what) {
|
||||
case MSG_POLL_TIMEOUT:
|
||||
stopPoll();
|
||||
mTvPayStatus.setText("支付状态:轮询超时");
|
||||
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.darker_gray));
|
||||
Toast.makeText(WXPayActivity.this, "轮询超时,请手动查询", Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case MSG_POLL_SUCCESS:
|
||||
boolean isPaySuccess = (boolean) msg.obj;
|
||||
String tradeState = (String) msg.getData().getString("tradeState");
|
||||
stopPoll();
|
||||
if (isPaySuccess) {
|
||||
mTvPayStatus.setText("支付状态:支付成功 ✅");
|
||||
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark));
|
||||
Toast.makeText(WXPayActivity.this, "支付成功!", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
mTvPayStatus.setText("支付状态:" + tradeState);
|
||||
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
|
||||
}
|
||||
break;
|
||||
case MSG_POLL_FAILED:
|
||||
String errorMsg = (String) msg.obj;
|
||||
mTvPayStatus.setText("查询失败:" + errorMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_wxpay);
|
||||
|
||||
initView();
|
||||
initListener();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
mIvQrCode = findViewById(R.id.iv_qrcode);
|
||||
mTvOrderNo = findViewById(R.id.tv_order_no);
|
||||
mTvPayStatus = findViewById(R.id.tv_pay_status);
|
||||
mBtnCreateOrder = findViewById(R.id.btn_create_order);
|
||||
}
|
||||
|
||||
private void initListener() {
|
||||
mBtnCreateOrder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
createOrder();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一下单,生成二维码
|
||||
*/
|
||||
private void createOrder() {
|
||||
mBtnCreateOrder.setEnabled(false);
|
||||
mTvPayStatus.setText("支付状态:生成订单中...");
|
||||
mIvQrCode.setImageBitmap(null);
|
||||
|
||||
WxPayApi.createOrder(new WxPayApi.OnCreateOrderCallback() {
|
||||
@Override
|
||||
public void onSuccess(String outTradeNo, String codeUrl) {
|
||||
mOutTradeNo = outTradeNo;
|
||||
mTvOrderNo.setText("商户订单号:" + outTradeNo);
|
||||
mTvPayStatus.setText("支付状态:未支付,请扫码");
|
||||
|
||||
// 生成二维码
|
||||
Bitmap qrCodeBitmap = ZXingUtils.createQRCodeBitmap(codeUrl, 250, 250);
|
||||
if (qrCodeBitmap != null) {
|
||||
mIvQrCode.setImageBitmap(qrCodeBitmap);
|
||||
// 开始轮询查询支付结果
|
||||
startPoll();
|
||||
} else {
|
||||
mTvPayStatus.setText("支付状态:生成二维码失败");
|
||||
mBtnCreateOrder.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
SpecUtil.WWSpecLogError(TAG, "统一下单失败:" + errorMsg);
|
||||
mTvPayStatus.setText("生成订单失败:" + errorMsg);
|
||||
mBtnCreateOrder.setEnabled(true);
|
||||
Toast.makeText(WXPayActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始轮询查询支付结果
|
||||
*/
|
||||
private void startPoll() {
|
||||
stopPoll(); // 先停止之前的轮询
|
||||
mPollStartTime = System.currentTimeMillis();
|
||||
mPollTimer = new Timer();
|
||||
mPollTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 检查是否超时
|
||||
long elapsedTime = System.currentTimeMillis() - mPollStartTime;
|
||||
if (elapsedTime >= WxPayConfig.POLL_TIMEOUT) {
|
||||
mPollHandler.sendEmptyMessage(MSG_POLL_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询订单状态
|
||||
WxPayApi.queryOrder(mOutTradeNo, new WxPayApi.OnQueryOrderCallback() {
|
||||
@Override
|
||||
public void onSuccess(boolean isPaySuccess, String tradeState) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = MSG_POLL_SUCCESS;
|
||||
msg.obj = isPaySuccess;
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("tradeState", tradeState);
|
||||
msg.setData(bundle);
|
||||
mPollHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = MSG_POLL_FAILED;
|
||||
msg.obj = errorMsg;
|
||||
mPollHandler.sendMessage(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, WxPayConfig.POLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
private void stopPoll() {
|
||||
if (mPollTimer != null) {
|
||||
mPollTimer.cancel();
|
||||
mPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopPoll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
package cc.winboll.studio.winboll.unittest;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.activities.WinBoLLActivity;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/03 10:52
|
||||
* @Describe 企业微信SDK接口测试(基础调试版)
|
||||
* 包含:SDK初始化、基础接口调用、日志输出、主线程回调处理
|
||||
*/
|
||||
public class TestWeWorkSpecSDK extends WinBoLLActivity implements IWinBoLLActivity, View.OnClickListener {
|
||||
|
||||
public static final String TAG = "TestWeWorkSpecSDK";
|
||||
|
||||
// ------------------- 企业微信SDK配置常量(需替换为实际项目参数) -------------------
|
||||
// 企业微信 CorpID(从企业微信管理后台获取)
|
||||
private static final String CORP_ID = "wwb37c73f34c722852";
|
||||
// 应用 AgentID(从企业微信应用管理后台获取)
|
||||
private static final String AGENT_ID = "your_agent_id_here";
|
||||
// 应用 Secret(从企业微信应用管理后台获取,注意保密)
|
||||
private static final String APP_SECRET = "your_app_secret_here";
|
||||
|
||||
// ------------------- Handler消息标识(主线程处理SDK回调) -------------------
|
||||
private static final int MSG_SDK_INIT_SUCCESS = 1001;
|
||||
private static final int MSG_SDK_INIT_FAILED = 1002;
|
||||
private static final int MSG_GET_CORP_INFO_SUCCESS = 1003;
|
||||
private static final int MSG_GET_CORP_INFO_FAILED = 1004;
|
||||
|
||||
// ------------------- 控件声明 -------------------
|
||||
private Button mBtnInitSDK;
|
||||
private Button mBtnGetCorpInfo;
|
||||
private Button mBtnCheckAuth;
|
||||
|
||||
// ------------------- 主线程Handler(处理SDK异步回调) -------------------
|
||||
private Handler mWeWorkHandler;
|
||||
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_test_weworkspecsdk);
|
||||
|
||||
// 初始化控件
|
||||
initViews();
|
||||
// 绑定点击事件
|
||||
initEvents();
|
||||
// 初始化Handler(主线程处理回调,更新UI)
|
||||
initHandler();
|
||||
// 初始化SDK(可选:启动时自动初始化,或点击按钮初始化)
|
||||
// initWeWorkSDK();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化控件(Java 7 显式绑定)
|
||||
*/
|
||||
private void initViews() {
|
||||
mBtnInitSDK = (Button) findViewById(R.id.btn_init_sdk);
|
||||
mBtnGetCorpInfo = (Button) findViewById(R.id.btn_get_corp_info);
|
||||
mBtnCheckAuth = (Button) findViewById(R.id.btn_check_auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定点击事件(Java 7 匿名内部类)
|
||||
*/
|
||||
private void initEvents() {
|
||||
mBtnInitSDK.setOnClickListener(this);
|
||||
mBtnGetCorpInfo.setOnClickListener(this);
|
||||
mBtnCheckAuth.setOnClickListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主线程Handler(处理SDK异步回调,安全更新UI)
|
||||
*/
|
||||
private void initHandler() {
|
||||
mWeWorkHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
switch (msg.what) {
|
||||
case MSG_SDK_INIT_SUCCESS:
|
||||
showToast("企业微信SDK初始化成功");
|
||||
LogUtils.d(TAG, "SDK初始化成功");
|
||||
break;
|
||||
case MSG_SDK_INIT_FAILED:
|
||||
String initError = (String) msg.obj;
|
||||
showToast("SDK初始化失败:" + initError);
|
||||
LogUtils.e(TAG, "SDK初始化失败:" + initError);
|
||||
break;
|
||||
case MSG_GET_CORP_INFO_SUCCESS:
|
||||
String corpInfo = (String) msg.obj;
|
||||
showToast("获取企业信息成功");
|
||||
LogUtils.d(TAG, "企业信息:" + corpInfo);
|
||||
break;
|
||||
case MSG_GET_CORP_INFO_FAILED:
|
||||
String corpError = (String) msg.obj;
|
||||
showToast("获取企业信息失败:" + corpError);
|
||||
LogUtils.e(TAG, "获取企业信息失败:" + corpError);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------- 企业微信SDK核心接口调用 -------------------
|
||||
|
||||
/**
|
||||
* 初始化企业微信SDK(异步操作,通过Handler回调结果)
|
||||
*/
|
||||
private void initWeWorkSDK() {
|
||||
showToast("开始初始化企业微信SDK...");
|
||||
// 模拟SDK异步初始化(实际项目中替换为企业微信SDK的真实初始化接口)
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 真实SDK初始化逻辑示例:
|
||||
// WeWorkSDK.init(TestWeWorkSpecSDK.this, CORP_ID, AGENT_ID, new WeWorkSDKCallback() {
|
||||
// @Override
|
||||
// public void onSuccess() {
|
||||
// mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(String errorMsg) {
|
||||
// Message msg = Message.obtain();
|
||||
// msg.what = MSG_SDK_INIT_FAILED;
|
||||
// msg.obj = errorMsg;
|
||||
// mWeWorkHandler.sendMessage(msg);
|
||||
// }
|
||||
// });
|
||||
|
||||
// 调试模拟:休眠1秒,模拟异步初始化
|
||||
Thread.sleep(1000);
|
||||
// 模拟初始化成功(如需测试失败,替换为发送MSG_SDK_INIT_FAILED)
|
||||
mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
|
||||
// 模拟初始化失败
|
||||
// Message msg = Message.obtain();
|
||||
// msg.what = MSG_SDK_INIT_FAILED;
|
||||
// msg.obj = "CorpID或AgentID错误";
|
||||
// mWeWorkHandler.sendMessage(msg);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
Message msg = Message.obtain();
|
||||
msg.what = MSG_SDK_INIT_FAILED;
|
||||
msg.obj = "线程中断:" + e.getMessage();
|
||||
mWeWorkHandler.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业基本信息(异步操作,需先初始化SDK)
|
||||
*/
|
||||
private void getCorpInfo() {
|
||||
if (!isSDKInitialized()) {
|
||||
showToast("请先初始化SDK");
|
||||
return;
|
||||
}
|
||||
showToast("开始获取企业信息...");
|
||||
// 模拟SDK异步获取企业信息(实际项目中替换为真实接口)
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 真实SDK接口示例:
|
||||
// WeWorkSDK.getCorpInfo(APP_SECRET, new CorpInfoCallback() {
|
||||
// @Override
|
||||
// public void onSuccess(CorpInfo info) {
|
||||
// Message msg = Message.obtain();
|
||||
// msg.what = MSG_GET_CORP_INFO_SUCCESS;
|
||||
// msg.obj = "企业名称:" + info.getCorpName() + ",企业ID:" + info.getCorpId();
|
||||
// mWeWorkHandler.sendMessage(msg);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(String errorMsg) {
|
||||
// Message msg = Message.obtain();
|
||||
// msg.what = MSG_GET_CORP_INFO_FAILED;
|
||||
// msg.obj = errorMsg;
|
||||
// mWeWorkHandler.sendMessage(msg);
|
||||
// }
|
||||
// });
|
||||
|
||||
// 调试模拟:休眠1秒,模拟异步获取
|
||||
Thread.sleep(1000);
|
||||
// 模拟获取成功
|
||||
Message successMsg = Message.obtain();
|
||||
successMsg.what = MSG_GET_CORP_INFO_SUCCESS;
|
||||
successMsg.obj = "企业名称:WinBoLL Studio,企业ID:" + CORP_ID;
|
||||
mWeWorkHandler.sendMessage(successMsg);
|
||||
// 模拟获取失败
|
||||
// Message failMsg = Message.obtain();
|
||||
// failMsg.what = MSG_GET_CORP_INFO_FAILED;
|
||||
// failMsg.obj = "AppSecret错误或权限不足";
|
||||
// mWeWorkHandler.sendMessage(failMsg);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
Message msg = Message.obtain();
|
||||
msg.what = MSG_GET_CORP_INFO_FAILED;
|
||||
msg.obj = "线程中断:" + e.getMessage();
|
||||
mWeWorkHandler.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否已授权(同步操作,示例)
|
||||
*/
|
||||
private void checkAuthStatus() {
|
||||
if (!isSDKInitialized()) {
|
||||
showToast("请先初始化SDK");
|
||||
return;
|
||||
}
|
||||
// 真实SDK接口示例:
|
||||
// boolean isAuthorized = WeWorkSDK.isAuthorized();
|
||||
// 调试模拟:默认返回true
|
||||
boolean isAuthorized = true;
|
||||
|
||||
if (isAuthorized) {
|
||||
showToast("用户已授权");
|
||||
LogUtils.d(TAG, "当前用户已授权企业微信应用");
|
||||
} else {
|
||||
showToast("用户未授权,请先授权");
|
||||
LogUtils.d(TAG, "当前用户未授权企业微信应用");
|
||||
// 真实项目中可调用授权接口:
|
||||
// WeWorkSDK.requestAuth(TestWeWorkSpecSDK.this, new AuthCallback() {
|
||||
// @Override
|
||||
// public void onSuccess(String code) {
|
||||
// showToast("授权成功,code:" + code);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(String errorMsg) {
|
||||
// showToast("授权失败:" + errorMsg);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- 工具方法 -------------------
|
||||
|
||||
/**
|
||||
* 检查SDK是否已初始化(模拟方法,实际项目中替换为SDK的真实状态检查)
|
||||
*/
|
||||
private boolean isSDKInitialized() {
|
||||
// 真实SDK可通过静态方法检查状态:
|
||||
// return WeWorkSDK.isInitialized();
|
||||
// 调试模拟:假设Handler不为空即表示已初始化
|
||||
return mWeWorkHandler != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast提示(Java 7 简化封装)
|
||||
*/
|
||||
private void showToast(String msg) {
|
||||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// ------------------- 点击事件处理 -------------------
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
if (id == R.id.btn_init_sdk) {
|
||||
initWeWorkSDK();
|
||||
} else if (id == R.id.btn_get_corp_info) {
|
||||
getCorpInfo();
|
||||
} else if (id == R.id.btn_check_auth) {
|
||||
checkAuthStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- 生命周期管理 -------------------
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 释放Handler资源,避免内存泄漏
|
||||
if (mWeWorkHandler != null) {
|
||||
mWeWorkHandler.removeCallbacksAndMessages(null);
|
||||
mWeWorkHandler = null;
|
||||
}
|
||||
// 真实SDK需调用销毁方法:
|
||||
// WeWorkSDK.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package cc.winboll.studio.winboll.utils;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* OkHttp网络请求工具类
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/07
|
||||
*/
|
||||
public class OkHttpUtil {
|
||||
|
||||
private static OkHttpClient sOkHttpClient;
|
||||
private static Handler sMainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
static {
|
||||
sOkHttpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
* @param url 请求地址
|
||||
* @param callback 回调
|
||||
*/
|
||||
public static void get(String url, final OnResultCallback callback) {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build();
|
||||
|
||||
sOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, final IOException e) {
|
||||
sMainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (callback != null) {
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
final String result = response.body().string();
|
||||
sMainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (callback != null) {
|
||||
if (response.isSuccessful()) {
|
||||
callback.onSuccess(result);
|
||||
} else {
|
||||
callback.onFailure("请求失败:" + response.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调接口
|
||||
*/
|
||||
public interface OnResultCallback {
|
||||
void onSuccess(String result);
|
||||
void onFailure(String errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package cc.winboll.studio.winboll.utils;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* 日志工具类(适配项目规范)
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/07
|
||||
*/
|
||||
public class SpecUtil {
|
||||
|
||||
private static final boolean isDebug = true;
|
||||
|
||||
public static void WWSpecLogInfo(String tag, String msg) {
|
||||
if (isDebug) {
|
||||
LogUtils.i(tag, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WWSpecLogError(String tag, String msg) {
|
||||
if (isDebug) {
|
||||
LogUtils.e(tag, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package cc.winboll.studio.winboll.utils;
|
||||
|
||||
import cc.winboll.studio.winboll.WxPayConfig;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
/**
|
||||
* 微信支付服务端接口封装
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/07
|
||||
*/
|
||||
public class WxPayApi {
|
||||
|
||||
/**
|
||||
* 统一下单(生成二维码)
|
||||
* @param callback 回调
|
||||
*/
|
||||
public static void createOrder(final OnCreateOrderCallback callback) {
|
||||
// 拼接请求参数(服务端测试接口需支持GET传参,若为POST需修改为表单/JSON)
|
||||
String url = WxPayConfig.CREATE_ORDER_URL +
|
||||
"?body=" + WxPayConfig.ORDER_BODY +
|
||||
"&totalFee=" + WxPayConfig.TOTAL_FEE +
|
||||
"&tradeType=" + WxPayConfig.TRADE_TYPE;
|
||||
|
||||
OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
try {
|
||||
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||
String outTradeNo = jsonObject.getString("out_trade_no");
|
||||
String codeUrl = jsonObject.getString("code_url");
|
||||
if (callback != null) {
|
||||
callback.onSuccess(outTradeNo, codeUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (callback != null) {
|
||||
callback.onFailure("解析统一下单结果失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
if (callback != null) {
|
||||
callback.onFailure("统一下单请求失败:" + errorMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单查询
|
||||
* @param outTradeNo 商户订单号
|
||||
* @param callback 回调
|
||||
*/
|
||||
public static void queryOrder(String outTradeNo, final OnQueryOrderCallback callback) {
|
||||
String url = WxPayConfig.QUERY_ORDER_URL + "?outTradeNo=" + outTradeNo;
|
||||
|
||||
OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
try {
|
||||
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||
String tradeState = jsonObject.getString("trade_state");
|
||||
boolean isSuccess = "SUCCESS".equals(tradeState);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(isSuccess, tradeState);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (callback != null) {
|
||||
callback.onFailure("解析订单查询结果失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String errorMsg) {
|
||||
if (callback != null) {
|
||||
callback.onFailure("订单查询请求失败:" + errorMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一下单回调接口
|
||||
*/
|
||||
public interface OnCreateOrderCallback {
|
||||
void onSuccess(String outTradeNo, String codeUrl);
|
||||
void onFailure(String errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单查询回调接口
|
||||
*/
|
||||
public interface OnQueryOrderCallback {
|
||||
void onSuccess(boolean isPaySuccess, String tradeState);
|
||||
void onFailure(String errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package cc.winboll.studio.winboll.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
import com.journeyapps.barcodescanner.BarcodeEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ZXing二维码生成工具类
|
||||
* 依赖:com.google.zxing:core:3.4.1 + com.journeyapps:zxing-android-embedded:3.6.0
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/07
|
||||
*/
|
||||
public class ZXingUtils {
|
||||
|
||||
/**
|
||||
* 生成二维码Bitmap(核心方法,使用journeyapps工具类)
|
||||
* @param content 内容(如微信支付的code_url)
|
||||
* @param width 二维码宽度(px)
|
||||
* @param height 二维码高度(px)
|
||||
* @return 二维码Bitmap,失败返回null
|
||||
*/
|
||||
public static Bitmap createQRCodeBitmap(String content, int width, int height) {
|
||||
// 1. 入参合法性校验
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 配置二维码参数
|
||||
Map<EncodeHintType, Object> hints = new HashMap<>();
|
||||
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 字符编码
|
||||
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 高容错级别(H级可容忍30%遮挡)
|
||||
hints.put(EncodeHintType.MARGIN, 1); // 边距(值越小,二维码越紧凑,建议1-4)
|
||||
|
||||
try {
|
||||
// 3. 生成BitMatrix(二维码矩阵)
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = qrCodeWriter.encode(
|
||||
content,
|
||||
BarcodeFormat.QR_CODE,
|
||||
width,
|
||||
height,
|
||||
hints
|
||||
);
|
||||
|
||||
// 4. 转换BitMatrix为Bitmap(关键:使用journeyapps的BarcodeEncoder)
|
||||
BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
|
||||
return barcodeEncoder.createBitmap(bitMatrix);
|
||||
|
||||
} catch (WriterException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:生成正方形二维码(宽度=高度)
|
||||
* @param content 内容
|
||||
* @param size 二维码边长(px)
|
||||
* @return 二维码Bitmap
|
||||
*/
|
||||
public static Bitmap createQRCodeBitmap(String content, int size) {
|
||||
return createQRCodeBitmap(content, size, size);
|
||||
}
|
||||
}
|
||||
|
||||
210
winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java
Normal file
210
winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java
Normal file
@@ -0,0 +1,210 @@
|
||||
package com.tencent.wework;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;;
|
||||
|
||||
/**
|
||||
* @warning: 1. 不要修改成员变量名,native方法内有反射调用
|
||||
* 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内
|
||||
* 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件
|
||||
*/
|
||||
public final class SpecCallbackSDK {
|
||||
|
||||
/**
|
||||
* @description 调用本地方法后实例化的对象指针
|
||||
*/
|
||||
private long specCallbackSDKptr = 0;
|
||||
|
||||
public long GetPtr() { return specCallbackSDKptr; }
|
||||
|
||||
/**
|
||||
* @description: 回包的headers
|
||||
*/
|
||||
private Map<String, String> responseHeaders;
|
||||
|
||||
public Map<String, String> GetResponseHeaders() { return responseHeaders; }
|
||||
|
||||
/**
|
||||
* @description: 回包的加密后的body
|
||||
*/
|
||||
private String responseBody;
|
||||
|
||||
public String GetResponseBody() { return responseBody; }
|
||||
|
||||
/**
|
||||
* @description: 每个请求构造一个SpecCallbackSDK示例,
|
||||
* SpecCallbackSDK仅持有headers和body的引用,
|
||||
* 因此需保证headers和body的生存期比SpecCallbackSDK长
|
||||
* @param method: 请求方法GET/POST
|
||||
* @param headers: 请求header
|
||||
* @param body: 请求body
|
||||
* @example:
|
||||
* SpecCallbackSDK sdk = new SpecCallbackSDK(method, headers, body);
|
||||
* if (sdk.IsOk()) {
|
||||
* String corpid = sdk.GetCorpId();
|
||||
* String agentid = sdk.GetAgentId();
|
||||
* String call_type = sdk.GetCallType();
|
||||
* String data = sdk.GetData();
|
||||
* //do something...
|
||||
* }
|
||||
* String response = ...;
|
||||
* sdk.BuildResponseHeaderBody(response);
|
||||
* Map<String, String> responseHeaders = sdk.GetResponseHeaders();
|
||||
* String body = sdk.GetResponseBody();
|
||||
* //do response
|
||||
*
|
||||
* @return errorcode 示例如下:
|
||||
* -920001: 未设置请求方法
|
||||
* -920002: 未设置请求header
|
||||
* -920003: 未设置请求body
|
||||
* */
|
||||
public SpecCallbackSDK(String method, Map<String, String> headers, String body) {
|
||||
try {
|
||||
specCallbackSDKptr = NewCallbackSDK(method, headers, body);
|
||||
} catch (Exception e) {
|
||||
SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private native long NewCallbackSDK(String method, Map<String, String> headers, String body);
|
||||
|
||||
/**
|
||||
* @usage 在Java对象的内存回收前析构C++对象
|
||||
*/
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
DeleteCPPInstance(specCallbackSDKptr);
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
private native void DeleteCPPInstance(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 判断构造函数中传入的请求是否解析成功
|
||||
* @return: 成功与否
|
||||
* */
|
||||
public boolean IsOk() {
|
||||
return IsOk(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native boolean IsOk(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求的企业
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: corpid
|
||||
* */
|
||||
public String GetCorpId() {
|
||||
return GetCorpId(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native String GetCorpId(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求的应用
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: agentid
|
||||
* */
|
||||
public long GetAgentId() {
|
||||
return GetAgentId(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native long GetAgentId(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求的类型
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: 1 - 来自[应用调用专区]的请求
|
||||
* 2 - 来自企业微信的回调事件
|
||||
* */
|
||||
public long GetCallType() {
|
||||
return GetCallType(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native long GetCallType(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求数据
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: 请求数据,根据call_type可能是:
|
||||
* - 企业微信回调事件
|
||||
* - [应用调用专区]接口中的request_data
|
||||
* */
|
||||
public String GetData() {
|
||||
return GetData(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native String GetData(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 是否异步请求
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: 是否异步请求
|
||||
* */
|
||||
public boolean GetIsAsync() {
|
||||
return GetIsAsync(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native boolean GetIsAsync(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求的job_info,
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: job_info,无需理解内容,
|
||||
* 在同一个请求上下文中使用SpecSDK的时候传入
|
||||
* */
|
||||
public String GetJobInfo() {
|
||||
return GetJobInfo(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native String GetJobInfo(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求的ability_id,[应用调用专区]接口时指定
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: ability_id
|
||||
* */
|
||||
public String GetAbilityId() {
|
||||
return GetAbilityId(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native String GetAbilityId(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 获取请求的notify_id,用于[应用同步调用专区程序]接口
|
||||
* @require: 仅当IsOk() == true可调用
|
||||
* @return: notify_id
|
||||
* */
|
||||
public String GetNotifyId() {
|
||||
return GetNotifyId(specCallbackSDKptr);
|
||||
}
|
||||
|
||||
private native String GetNotifyId(long specCallbackSDKptr);
|
||||
|
||||
/**
|
||||
* @description: 对返回包计算签名&加密
|
||||
* @param response: 待加密的回包明文.如果IsOk()==false,传入空串即可
|
||||
* @note 本接口的执行问题可查看日志
|
||||
* */
|
||||
public void BuildResponseHeaderBody(String response) {
|
||||
try {
|
||||
responseHeaders = new HashMap<String, String>();
|
||||
responseBody = "";
|
||||
BuildResponseHeaderBody(specCallbackSDKptr, response);
|
||||
} catch (Exception e) {
|
||||
SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private native void BuildResponseHeaderBody(long specCallbackSDKptr, String response);
|
||||
|
||||
// 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询
|
||||
static {
|
||||
try {
|
||||
Class.forName("com.tencent.wework.SpecUtil");
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
163
winboll/src/main/java/com/tencent/wework/SpecSDK.java
Normal file
163
winboll/src/main/java/com/tencent/wework/SpecSDK.java
Normal file
@@ -0,0 +1,163 @@
|
||||
package com.tencent.wework;
|
||||
|
||||
/**
|
||||
* @warning: 1. 不要修改成员变量名,native方法内有反射调用
|
||||
* 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内
|
||||
* 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件
|
||||
*/
|
||||
public final class SpecSDK {
|
||||
|
||||
/**
|
||||
* @description 调用本地方法后实例化的对象指针
|
||||
*/
|
||||
private long specSDKptr = 0;
|
||||
|
||||
/**
|
||||
* @usage invoke的请求
|
||||
* @example "{\"limit\":1}
|
||||
*/
|
||||
private String request;
|
||||
|
||||
public void SetRequest(String request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @usage 访问上一次invoke的结果
|
||||
*/
|
||||
private String response;
|
||||
|
||||
public String GetResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param corpid: 企业corpid,必选参数
|
||||
* @param agentid: 应用id,必选参数
|
||||
* @param ability_id: 能力ID,可选参数
|
||||
* @param job_info: job_info,可选参数
|
||||
* */
|
||||
public SpecSDK(String corpId, long agentId) {
|
||||
specSDKptr = NewSDK1(corpId, agentId);
|
||||
}
|
||||
|
||||
private native long NewSDK1(String corpId, long agentId);
|
||||
|
||||
public SpecSDK(String corpId, long agentId, String abilityId) {
|
||||
specSDKptr = NewSDK2(corpId, agentId, abilityId);
|
||||
}
|
||||
|
||||
private native long NewSDK2(String corpId, long agentId, String abilityId);
|
||||
|
||||
public SpecSDK(String corpId, long agentId, String abilityId, String jobInfo) {
|
||||
specSDKptr = NewSDK3(corpId, agentId, abilityId, jobInfo);
|
||||
}
|
||||
|
||||
private native long NewSDK3(String corpId, long agentId, String abilityId, String jobInfo);
|
||||
|
||||
/**
|
||||
* @description 使用callback的请求来初始化
|
||||
* @param callback_sdk: 要求IsOk()==true
|
||||
* @return C++内部指针,创建失败时指针仍为0,并输出错误日志
|
||||
* */
|
||||
public SpecSDK(SpecCallbackSDK callbackSDK) {
|
||||
specSDKptr = NewSDK4(callbackSDK.GetPtr());
|
||||
}
|
||||
|
||||
private native long NewSDK4(long callbackSDK);
|
||||
|
||||
/**
|
||||
* @usage 在Java对象的内存回收前析构C++对象
|
||||
*/
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
DeleteCPPInstance(specSDKptr);
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
private native void DeleteCPPInstance(long specSDKptr);
|
||||
|
||||
/**
|
||||
* @description 用于在专区内调用企业微信接口
|
||||
* @param api_name 接口名
|
||||
* @param request json格式的请求数据
|
||||
* @param response json格式的返回数据
|
||||
* @return errorcode 参考如下:
|
||||
* 0: 成功
|
||||
* -910001: SDK没有初始化
|
||||
* -910002: 没有设置请求体
|
||||
* -910003: 没有设置请求的API
|
||||
* -910004: 在SDK成员内找不到成员"response",注意lib内有反射机制,不要修改成员变量名
|
||||
* -910005: 使用未初始化的callback初始化SDK
|
||||
* -910006: invoke调用失败,应检查日志查看具体原因
|
||||
* -910007: 响应体为空
|
||||
* @note 当返回0时,表示没有网络或请求协议层面或调用方法的失败,
|
||||
* 调用方需继续检查response中的errcode字段确保业务层面的成功
|
||||
*
|
||||
* @usage 当前版本sdk支持的接口列表,每个接口的具体协议请查看企业微信文档:
|
||||
* https://developer.work.weixin.qq.com/document/path/91201
|
||||
*
|
||||
* +--------------------------------+--------------------------------+
|
||||
* |接口名 |描述 |
|
||||
* |--------------------------------|--------------------------------|
|
||||
* |program_async_job_call_back |上报异步任务结果 |
|
||||
* |sync_msg |获取会话记录 |
|
||||
* |get_group_chat |获取内部群信息 |
|
||||
* |get_agree_status_single |获取单聊会话同意情况 |
|
||||
* |get_agree_status_room |获取群聊会话同意情况 |
|
||||
* |set_hide_sensitiveinfo_config |设置成员会话组件敏感信息隐藏配置|
|
||||
* |get_hide_sensitiveinfo_config |获取成员会话组件敏感信息隐藏配置|
|
||||
* |search_chat |会话名称搜索 |
|
||||
* |search_msg |会话消息搜索 |
|
||||
* |create_rule |新增关键词规则 |
|
||||
* |get_rule_list |获取关键词列表 |
|
||||
* |get_rule_detail |获取关键词规则详情 |
|
||||
* |update_rule |修改关键词规则 |
|
||||
* |delete_rule |删除关键词规则 |
|
||||
* |get_hit_msg_list |获取命中关键词规则的会话记录 |
|
||||
* |create_sentiment_task |创建情感分析任务 |
|
||||
* |get_sentiment_result |获取情感分析结果 |
|
||||
* |create_summary_task |创建摘要提取任务 |
|
||||
* |get_summary_result |获取摘要提取结果 |
|
||||
* |create_customer_tag_task |创建标签匹配任务 |
|
||||
* |get_customer_tag_result |获取标签任务结果 |
|
||||
* |create_recommend_dialog_task |创建话术推荐任务 |
|
||||
* |get_recommend_dialog_result |获取话术推荐结果 |
|
||||
* |create_private_task |创建自定义模型任务 |
|
||||
* |get_private_task_result |获取自定义模型结果 |
|
||||
* |(废弃)document_list |获取知识集列表 |
|
||||
* |create_spam_task |会话反垃圾创建分析任务 |
|
||||
* |get_spam_result |会话反垃圾获取任务结果 |
|
||||
* |create_chatdata_export_job |创建会话内容导出任务 |
|
||||
* |get_chatdata_export_job_status |获取会话内容导出任务结果 |
|
||||
* |spec_notify_app |专区通知应用 |
|
||||
* |create_program_task |创建自定义程序任务 |
|
||||
* |get_program_task_result |获取自定义程序结果 |
|
||||
* |knowledge_base_list |获取企业授权给应用的知识集列表 |
|
||||
* |knowledge_base_create |创建知识集 |
|
||||
* |knowledge_base_detail |获取知识集详情 |
|
||||
* |knowledge_base_add_doc |添加知识集內容 |
|
||||
* |knowledge_base_remove_doc |删除知识集內容 |
|
||||
* |knowledge_base_modify_name |修改知识集名称 |
|
||||
* |knowledge_base_delete |删除知识集 |
|
||||
* |search_contact_or_customer |员工或者客户名称搜索 |
|
||||
* |create_ww_model_task |创建企微通用模型任务 |
|
||||
* |get_ww_model_result |获取企微通用模型结果 |
|
||||
* |get_msg_list_by_page_id |page_id获取消息列表 |
|
||||
* +-----------------------------------------------------------------+
|
||||
* */
|
||||
public int Invoke(String apiName) {
|
||||
return Invoke(specSDKptr, apiName, request);
|
||||
}
|
||||
|
||||
private native int Invoke(long sdk, String apiName, String request);
|
||||
|
||||
// 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询
|
||||
static {
|
||||
try {
|
||||
Class.forName("com.tencent.wework.SpecUtil");
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
171
winboll/src/main/java/com/tencent/wework/SpecUtil.java
Normal file
171
winboll/src/main/java/com/tencent/wework/SpecUtil.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.tencent.wework;
|
||||
|
||||
//import java.lang.management.ManagementFactory;
|
||||
//import java.lang.management.RuntimeMXBean;
|
||||
|
||||
/**
|
||||
* @warning: 1. 不要修改成员变量名,native方法内有反射调用
|
||||
* 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内
|
||||
* 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件
|
||||
* 4. 使用其他工具打印的日志将无法被查询,如需使用SLF4j风格的日志或性能更好的日志框架,
|
||||
* 请自行封装SpecUtil.SpecLog或SpecUtil.SpecLogNative方法
|
||||
*
|
||||
* @usage: 1. 获取SDK的版本号
|
||||
* 2. 打印三个级别的日志
|
||||
* 3. 开启调试模式
|
||||
*/
|
||||
public final class SpecUtil {
|
||||
|
||||
/**
|
||||
* @description SDK版本号
|
||||
* @usage 可用于校对不同SDK版本,或后续针对不同的SDK版本添加业务逻辑
|
||||
*/
|
||||
private static final String SDK_VERSION = "1.4.0";
|
||||
|
||||
public static String GetSDKVersion() {
|
||||
return SDK_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 正确的包名,SDK必须存放在"com.tencent.wework"下,否则会影响本地方法的调用
|
||||
*/
|
||||
private static final String EXPECTED_PACKAGE_NAME = "com.tencent.wework";
|
||||
|
||||
public static String GetExpectedPackageName() {
|
||||
return EXPECTED_PACKAGE_NAME;
|
||||
}
|
||||
|
||||
private static final String LINE_SEPERATOR = System.getProperty("line.separator");
|
||||
|
||||
public static void WWSpecLogInfo(String... args) {
|
||||
SpecLog('I', args);
|
||||
}
|
||||
|
||||
public static void WWSpecLogError(String... args) {
|
||||
SpecLog('E', args);
|
||||
}
|
||||
|
||||
public static void WWSpecLogDebug(String... args) {
|
||||
SpecLog('D', args);
|
||||
}
|
||||
|
||||
public static void WWSpecLogInfoWithReqId(String reqId, String... args) {
|
||||
SpecLogWithReqId(reqId, 'I', args);
|
||||
}
|
||||
|
||||
public static void WWSpecLogErrorWithReqId(String reqId, String... args) {
|
||||
SpecLogWithReqId(reqId, 'E', args);
|
||||
}
|
||||
|
||||
public static void WWSpecLogDebugWithReqId(String reqId, String... args) {
|
||||
SpecLogWithReqId(reqId, 'D', args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @usage 打印标准日志
|
||||
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||
* @param args 自定义参数
|
||||
*/
|
||||
public static void SpecLog(char logLevel, String... args) {
|
||||
StackTraceElement element = Thread.currentThread().getStackTrace()[3];
|
||||
SpecLogNative(
|
||||
logLevel,
|
||||
element.getFileName(),
|
||||
element.getLineNumber(),
|
||||
String.join(",", args).replace(LINE_SEPERATOR, " ")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @usage 打印标准日志
|
||||
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||
* @param reqid 请求id
|
||||
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||
* @param args 自定义参数
|
||||
*/
|
||||
public static void SpecLogWithReqId(String reqId, char logLevel, String... args) {
|
||||
StackTraceElement element = Thread.currentThread().getStackTrace()[3];
|
||||
SpecLogNativeWithReqId(
|
||||
reqId,
|
||||
logLevel,
|
||||
element.getFileName(),
|
||||
element.getLineNumber(),
|
||||
String.join(",", args).replace(LINE_SEPERATOR, " ")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @usage 打印标准日志
|
||||
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||
* 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数
|
||||
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||
* @param fileName 文件名(类名)
|
||||
* @param lineNumber 行号
|
||||
* @param argsString 自定义参数
|
||||
*/
|
||||
public static native void SpecLogNative(char logLevel, String fileName, int lineNumber, String argsString);
|
||||
|
||||
/**
|
||||
* @usage 打印标准日志
|
||||
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||
* 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数
|
||||
* @param reqid 请求id
|
||||
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||
* @param fileName 文件名(类名)
|
||||
* @param lineNumber 行号
|
||||
* @param argsString 自定义参数
|
||||
*/
|
||||
public static native void SpecLogNativeWithReqId(String reqId, char logLevel, String fileName, int lineNumber, String argsString);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @usage 开启调试模式,进程级别开关
|
||||
* @param debugToken 调试凭证,在管理端获取
|
||||
* @param accessToken 应用access token
|
||||
* @return 是否开启成功
|
||||
*/
|
||||
public static boolean SpecOpenDebugMode(String debugToken, String accessToken) {
|
||||
return SpecOpenDebugModeNative(debugToken, accessToken);
|
||||
}
|
||||
|
||||
private static native boolean SpecOpenDebugModeNative(String debugToken, String accessToken);
|
||||
|
||||
/**
|
||||
* @usage 生成notify id。用户可调用本接口生成notify id,也可完全自定义生成
|
||||
* @return 新的notify id,支持纳秒级隔离,内部异常时会输出日志并返回空串
|
||||
* @note 1. 用户可先生成notify id,将其与回调数据关联存储后,再使用该notify id通知应用,
|
||||
* 从而保证回调数据被请求时已存储完毕
|
||||
*/
|
||||
public static String GenerateNotifyId() {
|
||||
return GenerateNotifyIdNative();
|
||||
}
|
||||
|
||||
private static native String GenerateNotifyIdNative();
|
||||
|
||||
static {
|
||||
// 检查包名
|
||||
String packageName = SpecUtil.class.getPackage().getName();
|
||||
if (!EXPECTED_PACKAGE_NAME.equals(packageName)) {
|
||||
// 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询
|
||||
System.out.println("SpecUtil class must be in package com.tencent.wework");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// 加载so库
|
||||
try {
|
||||
System.loadLibrary("WeWorkSpecSDK");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
System.out.println("libWeWorkSpecSDK.so not found in java.library.path");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
} catch (Exception e) {
|
||||
System.out.println("unexpected exception: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
SpecUtil.WWSpecLogInfo("SDK init done", "packageName=" + packageName, "SDK_VERSION=" + SDK_VERSION);
|
||||
}
|
||||
}
|
||||
30
winboll/src/main/res/layout/activity_test_weworkspecsdk.xml
Normal file
30
winboll/src/main/res/layout/activity_test_weworkspecsdk.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_init_sdk"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="初始化企业微信SDK"
|
||||
android:layout_marginBottom="10dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_get_corp_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="获取企业基本信息"
|
||||
android:layout_marginBottom="10dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_check_auth"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="检查用户授权状态"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
43
winboll/src/main/res/layout/activity_wxpay.xml
Normal file
43
winboll/src/main/res/layout/activity_wxpay.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="定额支付测试(0.01元)"
|
||||
android:textSize="20sp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_qrcode"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="250dp"
|
||||
android:scaleType="fitXY"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_order_no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="商户订单号:"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_pay_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="支付状态:未支付"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@android:color/black"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_create_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="生成支付二维码"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -7,4 +7,7 @@
|
||||
<item
|
||||
android:id="@+id/item_settings"
|
||||
android:title="Settings"/>
|
||||
<item
|
||||
android:id="@+id/item_wxpayactivity"
|
||||
android:title="WXPayActivity"/>
|
||||
</menu>
|
||||
|
||||
Reference in New Issue
Block a user