Compare commits
129 Commits
powerbell-
...
powerbell-
| Author | SHA1 | Date | |
|---|---|---|---|
| 8609c2f784 | |||
| 863b743330 | |||
| 61b7afa4b5 | |||
| 8e4c7a6832 | |||
| d29068b029 | |||
| 51963f8e0f | |||
| 18a5762c15 | |||
| 60de16ab45 | |||
| 6d60d71991 | |||
| 0cfbc43acb | |||
| 20227e29ef | |||
| 2b7007f478 | |||
| 212b8185c8 | |||
| 2ef09e020e | |||
| 6de9b7379b | |||
| 7fba2c8812 | |||
| 20e3a5f974 | |||
| c8333e1e81 | |||
| 6beb56efae | |||
| eb51b2c8f4 | |||
| 36bdd059b0 | |||
| bf051dcc9f | |||
| b38a8df462 | |||
| a8dbe43d4b | |||
| 05a8dc5205 | |||
| 280fc1abd6 | |||
| fe2084f5ff | |||
| 2a75aa140e | |||
| 04b597cbe7 | |||
| 35d210c378 | |||
| 79ae6de6fc | |||
| 328f559d2e | |||
| 28d340a772 | |||
| 65acbfcd04 | |||
| 6af2096b30 | |||
| e539922478 | |||
| 1d9a03554c | |||
| 23beabe99b | |||
| 9c1e9dc75b | |||
| 76c1dee625 | |||
| e967ce5511 | |||
| bea77409a5 | |||
| e584e824c0 | |||
| bc6a82af41 | |||
| 9bc71bb3f6 | |||
| b0dee5e98e | |||
| 7d796b5c3f | |||
| d43ba4bff2 | |||
| 796d826331 | |||
| b3f4571b57 | |||
| 5e6de91430 | |||
| d61d1da5d1 | |||
| 5cf47172f4 | |||
| 683dc6791e | |||
| 1e9a6adc88 | |||
| dbcb5259d9 | |||
| 740ab932a4 | |||
| 29b9f3c82b | |||
| 47601ef542 | |||
| 7b17fae798 | |||
| 405b914f02 | |||
| 1c7ceebb78 | |||
| 493b7e433c | |||
| d2ddfedc96 | |||
| bba48a4458 | |||
| b7ae6ce190 | |||
| ba5470ebcb | |||
| 78c7763212 | |||
| d051b1f737 | |||
| d6323bc1ed | |||
| dffcc0f8a0 | |||
| 9426618b59 | |||
| 68d98d4be3 | |||
| 4db458dda8 | |||
| 83a8f5dada | |||
| 8e1d6ba197 | |||
| 70a004d9e3 | |||
| c7f8aea1ce | |||
| 6d4381d78a | |||
| ddcd9a450e | |||
| ca2323f534 | |||
| 851800e39a | |||
| f17624048c | |||
| 724fce895f | |||
| 5ece532dd4 | |||
| 8b20bc84c8 | |||
| 634c71dfd4 | |||
| 947df2e9b4 | |||
| 08a33365b3 | |||
| 7cffe5c0a5 | |||
| 5a0c429131 | |||
| cff26b3d11 | |||
| e59034e48d | |||
| 3d3301064c | |||
| 2d12397f5e | |||
| f09bb17cbc | |||
| 28d8a5679f | |||
| b4d9bdf3b3 | |||
| 111cf01f9a | |||
| e51d46186a | |||
| 8fc6855066 | |||
| 4ceaf1e46a | |||
| e669bbb04b | |||
| 6bf3ebe2fd | |||
| a44f7fe6d4 | |||
| 35a34b5b53 | |||
| d35d0d0291 | |||
| 03212c0554 | |||
| 0c963213df | |||
| 10ddca4f73 | |||
| f240d9c057 | |||
| 2c77bf775b | |||
| 1db7c9bf80 | |||
| fd556fd06f | |||
| 220aa9dbfb | |||
| ecafd2026f | |||
| 6ed9bc0d8e | |||
| bcb5db0a17 | |||
| 6b69e04706 | |||
| a2a61bbf0b | |||
| 9f4211c83e | |||
| 447a786632 | |||
| ff0f239ffc | |||
| 7c59a982fc | |||
| 895cc4630d | |||
| 851a539364 | |||
| d79f2937ba | |||
| c524a21429 | |||
| a148de2ab8 |
@@ -29,11 +29,11 @@ android {
|
||||
applicationId "cc.winboll.studio.powerbell"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 6
|
||||
versionCode 7
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.12"
|
||||
versionName "15.14"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
@@ -88,7 +88,7 @@ dependencies {
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
//api fileTree(dir: 'libs', include: ['*.aar'])
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed Dec 10 18:32:41 HKT 2025
|
||||
stageCount=9
|
||||
#Mon Dec 22 11:02:54 HKT 2025
|
||||
stageCount=22
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.8
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.21
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.9
|
||||
baseBetaVersion=15.14.22
|
||||
|
||||
279
powerbell/build_copyright_pdf.sh
Normal file
279
powerbell/build_copyright_pdf.sh
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/bin/bash
|
||||
# PowerBell软著版本号快速修改+生成脚本
|
||||
# 无需手动改主脚本,输入版本号直接运行
|
||||
|
||||
# 颜色输出函数
|
||||
red_echo() { echo -e "\033[31m$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m$1\033[0m"; }
|
||||
|
||||
# 1. 提示用户输入新版本号
|
||||
blue_echo "==== 请输入软著版本号(格式示例:V15、V15.0.1) ===="
|
||||
read -p "输入版本号:" NEW_VERSION
|
||||
|
||||
# 校验版本号格式(避免特殊符号)
|
||||
if [[ ! $NEW_VERSION =~ ^V[0-9]+(\.[0-9]+)*$ ]]; then
|
||||
red_echo "错误:版本号格式无效!请遵循「V+数字」格式(如V15、V15.0.1),不含特殊符号"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 定义固定配置(仅需修改这里的著作权人,其他无需动)
|
||||
SOFTWARE_NAME="PowerBell"
|
||||
COPYRIGHT_OWNER="张绍建陆丰东海镇云宝软件开发工作室"
|
||||
LINES_PER_PAGE=55
|
||||
|
||||
# 3. 生成主脚本(自动替换新版本号)
|
||||
blue_echo -e "\n==== 生成${NEW_VERSION}版本主脚本 ===="
|
||||
cat > build_copyright_pdf_temp.sh << EOF
|
||||
#!/bin/bash
|
||||
# PowerBell软著PDF生成脚本(版本:$NEW_VERSION)
|
||||
red_echo() { echo -e "\033[31m\$1\033[0m"; }
|
||||
green_echo() { echo -e "\033[32m\$1\033[0m"; }
|
||||
blue_echo() { echo -e "\033[34m\$1\033[0m"; }
|
||||
|
||||
# 配置项(已自动替换为${NEW_VERSION})
|
||||
SOFTWARE_NAME="$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION="$NEW_VERSION"
|
||||
COPYRIGHT_OWNER="$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE=$LINES_PER_PAGE
|
||||
|
||||
# 步骤1:检查依赖
|
||||
blue_echo "==== 1/7 检查并安装依赖 ===="
|
||||
sudo apt update > /dev/null 2>&1
|
||||
REQUIRED_PKGS=("python3" "wkhtmltopdf" "fonts-wqy-microhei" "pdftk" "poppler-utils")
|
||||
for pkg in "\${REQUIRED_PKGS[@]}"; do
|
||||
if ! dpkg -s "\$pkg" > /dev/null 2>&1; then
|
||||
green_echo "安装依赖:\$pkg"
|
||||
sudo apt install -y "\$pkg" > /dev/null 2>&1
|
||||
fi
|
||||
done
|
||||
|
||||
# 步骤2:生成纯文本源码
|
||||
blue_echo -e "\n==== 2/7 生成纯文本核心源码 ===="
|
||||
cat > generate_source.py << GEN_EOF
|
||||
import os
|
||||
PROJECT_PATH = "./"
|
||||
OUTPUT_TXT = "PowerBell_Core_Source.txt"
|
||||
INCLUDE_EXT = [".java", ".kt"]
|
||||
EXCLUDE_DIRS = ["build", "libs", "test", "androidTest", ".git", ".idea", "gradle", "unittest"]
|
||||
MIN_LINE_COUNT = 3
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
|
||||
def clean_text(text):
|
||||
return ''.join(c for c in text if c.isprintable() or c in "\\n\\r\\t")
|
||||
|
||||
def generate_source_txt():
|
||||
valid_files = []
|
||||
main_dir = os.path.join(PROJECT_PATH, "src", "main")
|
||||
if not os.path.exists(main_dir):
|
||||
print("Error: src/main directory not found!")
|
||||
return
|
||||
for root, dirs, files in os.walk(main_dir):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] in INCLUDE_EXT:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = f.readlines()
|
||||
code_lines = [l for l in lines if l.strip() and not l.strip().startswith("//")]
|
||||
if len(code_lines) >= MIN_LINE_COUNT:
|
||||
valid_files.append(file_path)
|
||||
except:
|
||||
continue
|
||||
valid_files.sort(key=lambda x: os.path.getsize(x), reverse=True)
|
||||
with open(OUTPUT_TXT, "w", encoding="utf-8-sig") as f:
|
||||
f.write(f"\{SOFTWARE_NAME} \{SOFTWARE_VERSION} 核心源码 - 著作权人:\{COPYRIGHT_OWNER}\\n\\n")
|
||||
for idx, file_path in enumerate(valid_files, 1):
|
||||
f.write(f"\\n{'='*60}\\n")
|
||||
f.write(f"文件 \{idx}:\{file_path.replace(PROJECT_PATH, '')}\\n")
|
||||
f.write(f"{'='*60}\\n\\n")
|
||||
try:
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
except UnicodeDecodeError:
|
||||
with open(file_path, "r", encoding="gbk") as src_f:
|
||||
content = clean_text(src_f.read())
|
||||
f.write(content)
|
||||
f.write("\\n\\n")
|
||||
except Exception as e:
|
||||
f.write(f"文件读取失败:\{str(e)}\\n\\n")
|
||||
continue
|
||||
print(f"有效源码文件数:\{len(valid_files)}")
|
||||
print(f"纯文本文件路径:\{os.path.abspath(OUTPUT_TXT)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_source_txt()
|
||||
GEN_EOF
|
||||
|
||||
python3 generate_source.py
|
||||
if [ ! -f "PowerBell_Core_Source.txt" ]; then
|
||||
red_echo "纯文本源码生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤3:生成带版本号页眉的HTML
|
||||
blue_echo -e "\n==== 3/7 生成带${NEW_VERSION}页眉的HTML ===="
|
||||
cat > txt2html.py << TXT_EOF
|
||||
import os
|
||||
TXT_FILE = "PowerBell_Core_Source.txt"
|
||||
HTML_FILE = "PowerBell_Source.html"
|
||||
SOFTWARE_NAME = "$SOFTWARE_NAME"
|
||||
SOFTWARE_VERSION = "$NEW_VERSION"
|
||||
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
|
||||
LINES_PER_PAGE = $LINES_PER_PAGE
|
||||
|
||||
CSS_STYLE = """
|
||||
<style>
|
||||
@page {{
|
||||
size: A4;
|
||||
margin: 10mm 5mm;
|
||||
@top-center {{
|
||||
content: "{} {} - 源代码(著作权人:{})";
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
}}
|
||||
@bottom-center {{
|
||||
content: "页码 " counter(page) " / " counter(pages);
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
}}
|
||||
}}
|
||||
body {{
|
||||
font-family: 'WenQuanYi Micro Hei', monospace;
|
||||
font-size: 9pt;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
padding: 5mm 0 0 0;
|
||||
counter-reset: code-line;
|
||||
}}
|
||||
.file-header {{
|
||||
background: #f0f0f0;
|
||||
padding: 3px;
|
||||
margin: 6px 0;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}}
|
||||
.code-block {{
|
||||
white-space: pre;
|
||||
margin-left: 8px;
|
||||
line-height: 1.1;
|
||||
counter-increment: code-line;
|
||||
}}
|
||||
.code-block:before {{
|
||||
content: counter(code-line) " ";
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
}}
|
||||
.page-break {{ page-break-after: always; counter-reset: code-line; }}
|
||||
</style>
|
||||
""".format(SOFTWARE_NAME, SOFTWARE_VERSION, COPYRIGHT_OWNER)
|
||||
|
||||
def txt_to_html():
|
||||
with open(TXT_FILE, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
html_content = "<!DOCTYPE html><html><head><meta charset='utf-8'>" + CSS_STYLE + "</head><body>"
|
||||
content_lines = content.split("\\n")[2:]
|
||||
content_clean = "\\n".join(content_lines)
|
||||
blocks = content_clean.split("====")
|
||||
|
||||
line_count = 0
|
||||
for block in blocks:
|
||||
if not block.strip():
|
||||
continue
|
||||
if "文件 " in block and ":" in block:
|
||||
file_header = block.split("\\n")[0].strip() if "\\n" in block else block.strip()
|
||||
html_content += f"<div class='file-header'>\{file_header}</div>"
|
||||
code_part = block.split("\\n")[1:] if "\\n" in block else []
|
||||
block = "\\n".join(code_part)
|
||||
code_lines = block.split("\\n")
|
||||
for line in code_lines:
|
||||
if line.strip() or line_count > 0:
|
||||
line_count += 1
|
||||
html_content += f"<div class='code-block'>\{line}</div>"
|
||||
if line_count >= LINES_PER_PAGE:
|
||||
html_content += "<div class='page-break'></div>"
|
||||
line_count = 0
|
||||
html_content += "</body></html>"
|
||||
with open(HTML_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
print(f"HTML文件路径:\{os.path.abspath(HTML_FILE)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
txt_to_html()
|
||||
TXT_EOF
|
||||
|
||||
python3 txt2html.py
|
||||
if [ ! -f "PowerBell_Source.html" ]; then
|
||||
red_echo "HTML文件生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤4:生成完整PDF
|
||||
blue_echo -e "\n==== 4/7 生成完整PDF(版本:${NEW_VERSION}) ===="
|
||||
wkhtmltopdf --page-size A4 \
|
||||
--margin-top 15mm --margin-bottom 15mm --margin-left 5mm --margin-right 5mm \
|
||||
--encoding utf-8 \
|
||||
--no-images --disable-javascript \
|
||||
--enable-local-file-access \
|
||||
--no-stop-slow-scripts \
|
||||
PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
|
||||
if [ ! -f "PowerBell_soft_full.pdf" ]; then
|
||||
red_echo "完整PDF生成失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤5:截取60页
|
||||
blue_echo -e "\n==== 5/7 截取前30+后30页 ===="
|
||||
TOTAL_PAGES=\$(pdfinfo PowerBell_soft_full.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "源码完整PDF总页数:\$TOTAL_PAGES 页"
|
||||
|
||||
if [ "\$TOTAL_PAGES" -le 60 ]; then
|
||||
cp PowerBell_soft_full.pdf PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
green_echo "源码不足60页,直接使用完整PDF"
|
||||
else
|
||||
pdftk PowerBell_soft_full.pdf cat 1-30 output PowerBell_前30页.pdf
|
||||
START_PAGE=\$((TOTAL_PAGES - 29))
|
||||
pdftk PowerBell_soft_full.pdf cat \$START_PAGE-\$TOTAL_PAGES output PowerBell_后30页.pdf
|
||||
pdftk PowerBell_前30页.pdf PowerBell_后30页.pdf cat output PowerBell_软著源码_${NEW_VERSION}_60页.pdf
|
||||
rm -f PowerBell_前30页.pdf PowerBell_后30页.pdf
|
||||
green_echo "源码超过60页,已截取前30页+后30页合并为60页"
|
||||
fi
|
||||
|
||||
# 步骤6:验证规范
|
||||
blue_echo -e "\n==== 6/7 验证${NEW_VERSION}版本PDF规范 ===="
|
||||
FINAL_PAGES=\$(pdfinfo PowerBell_软著源码_${NEW_VERSION}_60页.pdf | grep "Pages" | awk '{print \$2}')
|
||||
green_echo "最终PDF页数:\$FINAL_PAGES 页"
|
||||
green_echo "每页代码行数:\$LINES_PER_PAGE 行(≥50行)"
|
||||
green_echo "页眉信息:$SOFTWARE_NAME $NEW_VERSION - 源代码(著作权人:$COPYRIGHT_OWNER)"
|
||||
|
||||
# 步骤7:清理临时文件
|
||||
blue_echo -e "\n==== 7/7 清理临时文件 ===="
|
||||
rm -f generate_source.py txt2html.py PowerBell_Core_Source.txt PowerBell_Source.html PowerBell_soft_full.pdf
|
||||
green_echo "临时文件清理完成!"
|
||||
|
||||
# 输出结果
|
||||
green_echo -e "\n====================================="
|
||||
green_echo "✅ $SOFTWARE_NAME $NEW_VERSION 软著PDF生成成功!🎉"
|
||||
green_echo "📄 最终文件:\$(pwd)/PowerBell_软著源码_${NEW_VERSION}_60页.pdf"
|
||||
green_echo "💡 可直接提交软著登记,无需手动修改!"
|
||||
green_echo "====================================="
|
||||
EOF
|
||||
|
||||
# 4. 赋予执行权限并运行
|
||||
chmod +x build_copyright_pdf_temp.sh
|
||||
blue_echo -e "\n==== 开始生成${NEW_VERSION}版本PDF ===="
|
||||
./build_copyright_pdf_temp.sh
|
||||
|
||||
# 5. 删除临时主脚本(可选,保留则注释此行)
|
||||
rm -f build_copyright_pdf_temp.sh
|
||||
|
||||
green_echo -e "\n==== 操作完成!${NEW_VERSION}版本PDF已生成 ===="
|
||||
@@ -4,30 +4,15 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.powerbell">
|
||||
|
||||
<!-- 只能在前台获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<!-- 只有在前台运行时才能获取大致位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<!-- 拍摄照片和视频 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<!-- 运行前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<!-- 运行“specialUse”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
|
||||
<!-- 开机启动 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<!-- MANAGE_EXTERNAL_STORAGE -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 显示通知 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
@@ -40,9 +25,34 @@
|
||||
<!-- 计算应用存储空间 -->
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- MANAGE_EXTERNAL_STORAGE -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 请求忽略电池优化 -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- 拍摄照片和视频 -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
@@ -50,9 +60,19 @@
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<queries>
|
||||
|
||||
<package android:name="com.miui.securitycenter"/>
|
||||
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -64,7 +84,8 @@
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -74,7 +95,9 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.CrashActivity"/>
|
||||
<activity
|
||||
android:name=".activities.CrashActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityEN1"
|
||||
@@ -143,14 +166,13 @@
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity"
|
||||
android:name=".activities.ClearRecordActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
</activity>
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name="cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity"
|
||||
android:name=".activities.BackgroundSettingsActivity"
|
||||
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
@@ -178,41 +200,74 @@
|
||||
<receiver
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
|
||||
<intent-filter>
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
<action android:name="android.intent.action.POWER_CONNECTED"/>
|
||||
|
||||
<action android:name="android.intent.action.USER_PRESENT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name="cc.winboll.studio.powerbell.services.ControlCenterService"
|
||||
android:name=".services.ControlCenterService"
|
||||
android:priority="1000"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".controlcenterservice"/>
|
||||
android:process=".controlcenterservice"
|
||||
android:foregroundServiceType="dataSync">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="后台核心功能运行、持续保活"/>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="cc.winboll.studio.powerbell.services.AssistantService"
|
||||
android:name=".services.AssistantService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=".assistantservice"/>
|
||||
android:process=".assistantservice">
|
||||
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
|
||||
android:value="辅助核心功能运行"/>
|
||||
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/>
|
||||
<activity
|
||||
android:name=".activities.BatteryReporterActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
|
||||
<activity
|
||||
android:name=".activities.PixelPickerActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
|
||||
<activity
|
||||
android:name=".activities.BatteryReportActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/>
|
||||
<activity
|
||||
android:name=".unittest.MainUnitTestActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ShortcutActionActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -226,17 +281,15 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/>
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true">
|
||||
|
||||
<activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/>
|
||||
</activity>
|
||||
|
||||
<!-- 1. 注册 UCropActivity(关键:解决崩溃) -->
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:exported="true"> <!-- 必须添加:Android 12+ 要求显式声明 exported -->
|
||||
</activity>
|
||||
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
BIN
powerbell/src/main/assets/images/blank100x100.png
Normal file
BIN
powerbell/src/main/assets/images/blank100x100.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 B |
BIN
powerbell/src/main/assets/unittest/unittest-miku.png
Normal file
BIN
powerbell/src/main/assets/unittest/unittest-miku.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -4,92 +4,247 @@ import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 应用全局入口类(适配Android API 30,基于Java 7编写)
|
||||
* 核心策略:无论内存是否紧张,强制保持位图缓存与视图控件缓存
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
// ===================== 常量定义区 =====================
|
||||
public static final String TAG = "App";
|
||||
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
// 组件跳转常量
|
||||
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
|
||||
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
|
||||
public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2";
|
||||
|
||||
// 数据配置存储工具
|
||||
static AppConfigUtils _mAppConfigUtils;
|
||||
static AppCacheUtils _mAppCacheUtils;
|
||||
GlobalApplicationReceiver mReceiver;
|
||||
static String szTempDir = "";
|
||||
// 动作跳转常量
|
||||
public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1";
|
||||
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
|
||||
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
|
||||
|
||||
// ===================== 静态属性区 =====================
|
||||
// 数据配置工具
|
||||
private static AppConfigUtils sAppConfigUtils;
|
||||
private static AppCacheUtils sAppCacheUtils;
|
||||
// 全局Bitmap缓存工具(强制保持,不随内存紧张清理)
|
||||
public static BitmapCacheUtils sBitmapCacheUtils;
|
||||
// 全局视图控件缓存工具(强制保持,不随内存紧张清理)
|
||||
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
|
||||
// 临时文件夹路径
|
||||
private static String sTempDirPath = "";
|
||||
|
||||
// ===================== 成员属性区 =====================
|
||||
// 全局广播接收器
|
||||
private GlobalApplicationReceiver mGlobalReceiver;
|
||||
// 通知管理工具
|
||||
private NotificationManagerUtils mNotificationManager;
|
||||
|
||||
// ===================== 公共方法区 =====================
|
||||
/**
|
||||
* 获取临时文件夹路径
|
||||
*/
|
||||
public static String getTempDirPath() {
|
||||
return szTempDir;
|
||||
return sTempDirPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用配置工具实例
|
||||
*/
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
LogUtils.d(TAG, "getAppConfigUtils() 调用,传入Context:" + context.getClass().getSimpleName());
|
||||
if (sAppConfigUtils == null) {
|
||||
sAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "getAppConfigUtils():AppConfigUtils实例已初始化");
|
||||
}
|
||||
return sAppConfigUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用缓存工具实例
|
||||
*/
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
LogUtils.d(TAG, "getAppCacheUtils() 调用,传入Context:" + context.getClass().getSimpleName());
|
||||
if (sAppCacheUtils == null) {
|
||||
sAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
LogUtils.d(TAG, "getAppCacheUtils():AppCacheUtils实例已初始化");
|
||||
}
|
||||
return sAppCacheUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除电池历史数据
|
||||
*/
|
||||
public void clearBatteryHistory() {
|
||||
LogUtils.d(TAG, "clearBatteryHistory() 调用");
|
||||
sAppCacheUtils.clearBatteryHistory();
|
||||
}
|
||||
|
||||
// ===================== 生命周期方法区 =====================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
//setIsDebugging(false);
|
||||
LogUtils.d(TAG, "onCreate() 应用启动,开始初始化");
|
||||
|
||||
// 初始化活动窗口管理
|
||||
WinBoLLActivityManager.init(this);
|
||||
// 初始化 Toast 框架
|
||||
// 初始化调试模式
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
LogUtils.d(TAG, "onCreate() 调试模式:" + BuildConfig.DEBUG);
|
||||
|
||||
// 初始化基础工具
|
||||
initBaseTools();
|
||||
// 初始化临时文件夹
|
||||
initTempDir();
|
||||
// 初始化工具类实例(含强制缓存工具)
|
||||
initUtils();
|
||||
// 初始化广播接收器
|
||||
initReceiver();
|
||||
|
||||
LogUtils.d(TAG, "onCreate() 应用初始化完成,强制缓存策略已启用");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放资源");
|
||||
|
||||
// 释放Toast工具
|
||||
ToastUtils.release();
|
||||
// 释放通知工具
|
||||
releaseNotificationManager();
|
||||
// 释放广播接收器
|
||||
releaseReceiver();
|
||||
|
||||
LogUtils.d(TAG, "onTerminate() 应用资源释放完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
// 核心修改:移除所有缓存清理逻辑,强制保持位图和视图控件缓存
|
||||
LogUtils.w(TAG, "onTrimMemory() 调用,内存等级level:" + level + ",强制保持所有缓存(不清理)");
|
||||
// 仅记录缓存状态,不执行任何清理操作
|
||||
logCacheStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
// 核心修改:低内存时也不清理缓存,仅记录日志
|
||||
LogUtils.w(TAG, "onLowMemory() 调用,强制保持所有缓存(不清理)");
|
||||
// 仅记录缓存状态,不执行任何清理操作
|
||||
logCacheStatus();
|
||||
}
|
||||
|
||||
// ===================== 私有初始化方法区 =====================
|
||||
/**
|
||||
* 初始化基础工具(Activity管理、Toast)
|
||||
*/
|
||||
private void initBaseTools() {
|
||||
LogUtils.d(TAG, "initBaseTools() 开始初始化基础工具");
|
||||
WinBoLLActivityManager.init(this);
|
||||
ToastUtils.init(this);
|
||||
|
||||
// 临时文件夹方案1
|
||||
// 获取Pictures文件夹路径(Android 10及以上推荐使用MediaStore,此处为传统方式)
|
||||
LogUtils.d(TAG, "initBaseTools() 基础工具初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化临时文件夹(适配API 30外部存储访问)
|
||||
*/
|
||||
private void initTempDir() {
|
||||
LogUtils.d(TAG, "initTempDir() 开始初始化临时文件夹");
|
||||
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();
|
||||
boolean isMkSuccess = powerBellDir.mkdirs();
|
||||
LogUtils.d(TAG, "initTempDir() 文件夹创建结果:" + isMkSuccess);
|
||||
}
|
||||
szTempDir = powerBellDir.getAbsolutePath();
|
||||
|
||||
// 设置数据配置存储工具
|
||||
_mAppConfigUtils = getAppConfigUtils(this);
|
||||
_mAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
mReceiver = new GlobalApplicationReceiver(this);
|
||||
mReceiver.registerAction();
|
||||
sTempDirPath = powerBellDir.getAbsolutePath();
|
||||
LogUtils.d(TAG, "initTempDir() 临时文件夹路径:" + sTempDirPath);
|
||||
}
|
||||
|
||||
public static AppConfigUtils getAppConfigUtils(Context context) {
|
||||
if (_mAppConfigUtils == null) {
|
||||
_mAppConfigUtils = AppConfigUtils.getInstance(context);
|
||||
/**
|
||||
* 初始化工具类实例(核心:强制初始化缓存工具,不随内存紧张重建)
|
||||
*/
|
||||
private void initUtils() {
|
||||
LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用强制缓存策略");
|
||||
sAppConfigUtils = getAppConfigUtils(this);
|
||||
sAppCacheUtils = getAppCacheUtils(this);
|
||||
|
||||
// 强制初始化Bitmap缓存工具(单例唯一,不重复创建)
|
||||
if (sBitmapCacheUtils == null) {
|
||||
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
|
||||
LogUtils.d(TAG, "initUtils() Bitmap缓存工具已初始化(强制保持)");
|
||||
}
|
||||
return _mAppConfigUtils;
|
||||
}
|
||||
|
||||
public static AppCacheUtils getAppCacheUtils(Context context) {
|
||||
if (_mAppCacheUtils == null) {
|
||||
_mAppCacheUtils = AppCacheUtils.getInstance(context);
|
||||
// 强制初始化视图控件缓存工具(单例唯一,不重复创建)
|
||||
if (sMemoryCachedBackgroundView == null) {
|
||||
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
|
||||
LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(强制保持)");
|
||||
}
|
||||
return _mAppCacheUtils;
|
||||
|
||||
mNotificationManager = new NotificationManagerUtils(this);
|
||||
LogUtils.d(TAG, "initUtils() 工具类初始化完成,强制缓存策略已生效");
|
||||
}
|
||||
|
||||
public void clearBatteryHistory() {
|
||||
_mAppCacheUtils.clearBatteryHistory();
|
||||
/**
|
||||
* 初始化广播接收器
|
||||
*/
|
||||
private void initReceiver() {
|
||||
LogUtils.d(TAG, "initReceiver() 开始初始化广播接收器");
|
||||
mGlobalReceiver = new GlobalApplicationReceiver(this);
|
||||
mGlobalReceiver.registerAction();
|
||||
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
}
|
||||
// ===================== 私有工具方法区 =====================
|
||||
/**
|
||||
* 释放广播接收器资源
|
||||
*/
|
||||
private void releaseReceiver() {
|
||||
LogUtils.d(TAG, "releaseReceiver() 开始释放广播接收器");
|
||||
if (mGlobalReceiver != null) {
|
||||
mGlobalReceiver.unregisterAction();
|
||||
mGlobalReceiver = null;
|
||||
LogUtils.d(TAG, "releaseReceiver() 广播接收器资源已释放");
|
||||
} else {
|
||||
LogUtils.d(TAG, "releaseReceiver() 广播接收器未初始化,无需释放");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放通知管理工具资源
|
||||
*/
|
||||
private void releaseNotificationManager() {
|
||||
LogUtils.d(TAG, "releaseNotificationManager() 开始释放通知工具");
|
||||
if (mNotificationManager != null) {
|
||||
mNotificationManager.release();
|
||||
mNotificationManager = null;
|
||||
LogUtils.d(TAG, "releaseNotificationManager() 通知工具资源已释放");
|
||||
} else {
|
||||
LogUtils.d(TAG, "releaseNotificationManager() 通知工具未初始化,无需释放");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存状态(用于调试,不影响缓存数据)
|
||||
*/
|
||||
private void logCacheStatus() {
|
||||
LogUtils.d(TAG, "logCacheStatus() 开始记录缓存状态");
|
||||
if (sBitmapCacheUtils != null) {
|
||||
LogUtils.d(TAG, "logCacheStatus() Bitmap缓存工具实例有效(强制保持)");
|
||||
}
|
||||
if (sMemoryCachedBackgroundView != null) {
|
||||
LogUtils.d(TAG, "logCacheStatus() 视图控件缓存工具实例有效(强制保持)");
|
||||
}
|
||||
LogUtils.d(TAG, "logCacheStatus() 缓存状态记录完成,所有缓存均强制保持");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
@@ -70,7 +70,7 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
mApplication.clearBatteryHistory();
|
||||
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_SERVICENOTIFICATION));
|
||||
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
|
||||
initRecordText();
|
||||
String szMSG = "The APP battery record is cleaned.";
|
||||
LogUtils.d(TAG, szMSG);
|
||||
|
||||
@@ -26,7 +26,7 @@ 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.PixelPickerActivity;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -194,7 +194,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
dialog.dismiss();
|
||||
// 可以在这里添加确定后的回调逻辑
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundBean bean = utils.getCurrentBackgroundBean();
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
bean.setPixelColor(pixelColor);
|
||||
utils.saveSettings();
|
||||
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
|
||||
@@ -218,7 +218,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
|
||||
void setBackgroundColor() {
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
|
||||
BackgroundBean bean = utils.getCurrentBackgroundBean();
|
||||
BackgroundBean bean = utils.getPreviewBackgroundBean();
|
||||
int nPixelColor = bean.getPixelColor();
|
||||
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
|
||||
mainLayout.setBackgroundColor(nPixelColor);
|
||||
@@ -247,9 +247,11 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, BackgroundSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
// Intent intent = new Intent();
|
||||
// intent.setClass(this, BackgroundSettingsActivity.class);
|
||||
// startActivity(intent);
|
||||
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ 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>
|
||||
@@ -18,6 +16,7 @@ import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||
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;
|
||||
|
||||
@@ -49,11 +48,4 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onCheckPermission(View view) {
|
||||
//ToastUtils.show("onCheckPermission");
|
||||
if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
|
||||
ToastUtils.show("【权限检查】存储权限已全部获取");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.model.BatteryData;
|
||||
import cc.winboll.studio.powerbell.models.BatteryData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -2,22 +2,19 @@ package cc.winboll.studio.powerbell.dialogs;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -29,21 +26,25 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
public static final String TAG = "BackgroundPicturePreviewDialog";
|
||||
|
||||
Context mContext;
|
||||
BackgroundSourceUtils mBackgroundPictureUtils;
|
||||
//BackgroundSourceUtils mBackgroundPictureUtils;
|
||||
Button dialogbackgroundpicturepreviewButton1;
|
||||
Button dialogbackgroundpicturepreviewButton2;
|
||||
String mszPreReceivedFileName;
|
||||
//String mszPreReceivedFileName;
|
||||
IOnRecivedPictureListener mIOnRecivedPictureListener;
|
||||
Uri mUriRecivedPicture;
|
||||
BackgroundView mBackgroundView;
|
||||
|
||||
public BackgroundPicturePreviewDialog(Context context) {
|
||||
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
|
||||
super(context);
|
||||
setContentView(R.layout.dialog_backgroundpicturepreview);
|
||||
initEnv();
|
||||
mIOnRecivedPictureListener = iOnRecivedPictureListener;
|
||||
//initEnv();
|
||||
|
||||
mContext = context;
|
||||
mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
|
||||
//mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
|
||||
|
||||
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
|
||||
copyAndViewRecivePicture(imageView);
|
||||
mBackgroundView = findViewById(R.id.backgroundview);
|
||||
previewRecivedPicture();
|
||||
|
||||
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
|
||||
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
|
||||
@@ -53,6 +54,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
// 跳转到主窗口
|
||||
Intent i = new Intent(mContext, MainActivity.class);
|
||||
mContext.startActivity(i);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,79 +64,77 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 使用分享到的图片
|
||||
//
|
||||
//LogUtils.d(TAG, "mszReceivedFileName : " + mszReceivedFileName);
|
||||
((IOnRecivedPictureListener)mContext).onAcceptRecivedPicture(mszPreReceivedFileName);
|
||||
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
|
||||
// 关闭对话框
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void initEnv() {
|
||||
LogUtils.d(TAG, "initEnv()");
|
||||
mszPreReceivedFileName = "PreReceived.data";
|
||||
}
|
||||
// void initEnv() {
|
||||
// LogUtils.d(TAG, "initEnv()");
|
||||
// mszPreReceivedFileName = "PreReceived.data";
|
||||
// }
|
||||
|
||||
void copyAndViewRecivePicture(ImageView imageView) {
|
||||
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
|
||||
void previewRecivedPicture() {
|
||||
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
|
||||
|
||||
//取出文件uri
|
||||
Uri uri = activity.getIntent().getData();
|
||||
if (uri == null) {
|
||||
uri = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
mUriRecivedPicture = activity.getIntent().getData();
|
||||
if (mUriRecivedPicture == null) {
|
||||
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
//获取文件真实地址
|
||||
String szSrcImage = UriUtil.getFilePathFromUri(mContext, uri);
|
||||
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
|
||||
if (TextUtils.isEmpty(szSrcImage)) {
|
||||
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
File fSrcImage = new File(szSrcImage);
|
||||
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
|
||||
File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
|
||||
// 复制源图片到剪裁文件
|
||||
try {
|
||||
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
|
||||
LogUtils.d(TAG, "copyFileUsingFileChannels");
|
||||
Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
|
||||
imageView.setBackground(drawable);
|
||||
//LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
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());
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 创建图片背景图片目录
|
||||
//
|
||||
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(String szBackgroundFileName);
|
||||
void onAcceptRecivedPicture(Uri uriRecivedPicture);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,747 @@
|
||||
package cc.winboll.studio.powerbell.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import com.a4455jkjh.colorpicker.ColorPickerDialog;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/12/16 11:47
|
||||
* @Describe 调色板对话框(支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
|
||||
*/
|
||||
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
|
||||
// ====================== 常量定义(首屏可见,统一管理) ======================
|
||||
public static final String TAG = "ColorPaletteDialog";
|
||||
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值(0-255)
|
||||
private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比(100%,无调节)
|
||||
private static final int BRIGHTNESS_STEP = 5; // 亮度调节步长(每次±5%,精准流畅)
|
||||
private static final int MIN_BRIGHTNESS = 10; // 亮度最小值(10%,避免全黑看不见)
|
||||
private static final int MAX_BRIGHTNESS = 200; // 亮度最大值(200%,避免过曝失真)
|
||||
private static final int MAX_ALPHA_PERCENT = 100; // 透明度最大值(100%=不透明)
|
||||
private static final int MIN_ALPHA_PERCENT = 0; // 透明度最小值(0%=完全透明)
|
||||
|
||||
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
|
||||
public interface OnColorSelectedListener {
|
||||
void onColorSelected(int color); // 返回0xAARRGGBB格式颜色(含透明度)
|
||||
}
|
||||
|
||||
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
|
||||
// 核心数据:原始基准值(用户输入/选择颜色时更新)+ 实时调节值(亮度/透明度变化时更新)
|
||||
private OnColorSelectedListener mListener; // 颜色选择回调(非空校验)
|
||||
private int mInitialColor; // 初始颜色(传入的默认颜色)
|
||||
private int mCurrentColor; // 当前最终颜色(含亮度+透明度调节)
|
||||
private int mCurrentBrightnessPercent; // 当前亮度百分比(10%-200%)
|
||||
// 透明度:百分比(0-100%,用户直观操作)+ 原始/实时值(0-255,颜色计算用)
|
||||
private int mOriginalAlphaPercent; // 原始透明度百分比(基准值,用户输入/选色时更新)
|
||||
private int mCurrentAlphaPercent; // 实时透明度百分比(调节进度条时更新)
|
||||
private int mOriginalAlpha; // 原始透明度(0-255,基准值)
|
||||
private int mCurrentAlpha; // 实时透明度(0-255,计算用)
|
||||
// RGB:原始基准值+实时调节值
|
||||
private int mOriginalR; // 原始R分量(基准值,用户输入/选色时更新)
|
||||
private int mOriginalG; // 原始G分量(基准值,用户输入/选色时更新)
|
||||
private int mOriginalB; // 原始B分量(基准值,用户输入/选色时更新)
|
||||
private int mCurrentR; // 实时R分量(亮度调节后,同步输入框显示)
|
||||
private int mCurrentG; // 实时G分量(亮度调节后,同步输入框显示)
|
||||
private int mCurrentB; // 实时B分量(亮度调节后,同步输入框显示)
|
||||
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
|
||||
private static volatile boolean isAppSelfUpdatingColor = false;
|
||||
|
||||
// 控件引用(新增透明度进度条+文本)
|
||||
private ImageView ivColorPicker; // 颜色预览拾取框
|
||||
private ImageView ivColorScaler; // 颜色渐变拾取框
|
||||
private EditText etR; // R分量输入框(显示实时调节值)
|
||||
private EditText etG; // G分量输入框(显示实时调节值)
|
||||
private EditText etB; // B分量输入框(显示实时调节值)
|
||||
private EditText etColorValue; // 颜色值输入框(#AARRGGBB,显示最终值)
|
||||
private SeekBar sbAlpha; // 透明度调节进度条(0-100%)
|
||||
private TextView tvAlphaValue; // 透明度数值显示(X%)
|
||||
private TextView tvBrightnessMinus;// 亮度减少按钮(-)
|
||||
private TextView tvBrightnessValue;// 亮度数值显示(X%,直观易懂)
|
||||
private TextView tvBrightnessPlus; // 亮度增加按钮(+)
|
||||
private TextView tvConfirm; // 确认按钮
|
||||
private TextView tvCancel; // 取消按钮
|
||||
|
||||
// ====================== 构造方法(初始化核心数据,严格校验) ======================
|
||||
public ColorPaletteDialog(Context context, int initialColor, OnColorSelectedListener listener) {
|
||||
super(context, R.style.CustomDialogStyle);
|
||||
this.mInitialColor = initialColor;
|
||||
this.mListener = listener;
|
||||
|
||||
// 1. 强制回调非空,避免后续空指针(容错)
|
||||
if (mListener == null) {
|
||||
throw new IllegalArgumentException("OnColorSelectedListener can not be null!");
|
||||
}
|
||||
|
||||
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
|
||||
// 透明度:初始颜色的alpha(0-255)转百分比(0-100%)
|
||||
this.mOriginalAlpha = Color.alpha(initialColor);
|
||||
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
this.mCurrentAlpha = mOriginalAlpha;
|
||||
this.mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
// RGB:初始颜色的RGB分量
|
||||
this.mOriginalR = Color.red(initialColor);
|
||||
this.mOriginalG = Color.green(initialColor);
|
||||
this.mOriginalB = Color.blue(initialColor);
|
||||
this.mCurrentR = mOriginalR;
|
||||
this.mCurrentG = mOriginalG;
|
||||
this.mCurrentB = mOriginalB;
|
||||
|
||||
// 3. 初始化当前状态(默认亮度100%,当前颜色=初始颜色)
|
||||
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
this.mCurrentColor = initialColor;
|
||||
|
||||
LogUtils.d(TAG, "init dialog success | 初始颜色:" + String.format("#%08X", initialColor)
|
||||
+ " | 原始RGB:" + mOriginalR + "," + mOriginalG + "," + mOriginalB
|
||||
+ " | 原始透明度:" + mOriginalAlphaPercent + "%"
|
||||
+ " | 初始亮度:" + mCurrentBrightnessPercent + "%");
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE); // 隐藏标题栏
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_color_palette, null);
|
||||
setContentView(view);
|
||||
|
||||
// 初始化流程:控件绑定→数据赋值→监听设置→尺寸适配(小米机型优先适配)
|
||||
initViewBind(view);
|
||||
initData();
|
||||
initListener();
|
||||
adjustDialogSize();
|
||||
LogUtils.d(TAG, "dialog create complete | 适配小米API29-30机型");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
// 释放资源,避免内存泄漏(回调引用置空)
|
||||
mListener = null;
|
||||
LogUtils.d(TAG, "dialog dismiss | 释放资源完成");
|
||||
}
|
||||
|
||||
// ====================== 初始化核心方法(职责单一,便于维护) ======================
|
||||
/**
|
||||
* 控件绑定(新增透明度进度条+文本绑定)
|
||||
*/
|
||||
private void initViewBind(View view) {
|
||||
ivColorPicker = view.findViewById(R.id.iv_color_picker);
|
||||
ivColorScaler = view.findViewById(R.id.iv_color_scaler);
|
||||
etR = view.findViewById(R.id.et_r);
|
||||
etG = view.findViewById(R.id.et_g);
|
||||
etB = view.findViewById(R.id.et_b);
|
||||
etColorValue = view.findViewById(R.id.et_color_value);
|
||||
sbAlpha = view.findViewById(R.id.sb_alpha);
|
||||
tvAlphaValue = view.findViewById(R.id.tv_alpha_value);
|
||||
tvBrightnessMinus = view.findViewById(R.id.tv_brightness_minus);
|
||||
tvBrightnessValue = view.findViewById(R.id.tv_brightness_value);
|
||||
tvBrightnessPlus = view.findViewById(R.id.tv_brightness_plus);
|
||||
tvConfirm = view.findViewById(R.id.tv_confirm);
|
||||
tvCancel = view.findViewById(R.id.tv_cancel);
|
||||
|
||||
// 控件非空校验(小米低版本容错,绑定失败直接关闭对话框)
|
||||
if (ivColorPicker == null || ivColorScaler == null || etR == null || etG == null || etB == null || etColorValue == null
|
||||
|| sbAlpha == null || tvAlphaValue == null
|
||||
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|
||||
|| tvConfirm == null || tvCancel == null) {
|
||||
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确!");
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "view bind complete | 所有控件绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据初始化(无监听状态下赋值,避免循环回调)
|
||||
*/
|
||||
private void initData() {
|
||||
// 1. 颜色预览(显示当前最终颜色,初始=原始颜色)
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. RGB输入框(显示「实时分量」,初始=原始值)
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB,大写更规范)
|
||||
etColorValue.setText(String.format("#%08X", mCurrentColor));
|
||||
|
||||
// 4. 透明度控件(进度条+文本,初始=原始透明度)
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
|
||||
|
||||
// 5. 亮度控件(显示默认100%,初始化按钮状态)
|
||||
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
|
||||
updateBrightnessBtnStatus(); // 禁用边界值按钮(初始100%,都可用)
|
||||
|
||||
LogUtils.d(TAG, "init data complete | 原始透明度:" + mOriginalAlphaPercent + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听初始化(新增透明度进度条监听)
|
||||
*/
|
||||
private void initListener() {
|
||||
// 点击监听(按钮+颜色拾取框)
|
||||
ivColorPicker.setOnClickListener(this);
|
||||
ivColorScaler.setOnClickListener(this);
|
||||
tvConfirm.setOnClickListener(this);
|
||||
tvCancel.setOnClickListener(this);
|
||||
tvBrightnessMinus.setOnClickListener(this);
|
||||
tvBrightnessPlus.setOnClickListener(this);
|
||||
// 透明度进度条监听
|
||||
sbAlpha.setOnSeekBarChangeListener(this);
|
||||
// 输入框监听(RGB+颜色值,避免循环同步)
|
||||
initTextWatcherListener();
|
||||
LogUtils.d(TAG, "all listener init complete | 监听绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框尺寸适配(小米全面屏+软键盘优化,避免输入框被遮挡)
|
||||
*/
|
||||
private void adjustDialogSize() {
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams lp = window.getAttributes();
|
||||
// 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸)
|
||||
lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8);
|
||||
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
// 软键盘适配:小米虚拟导航栏兼容,避免输入框被遮挡
|
||||
window.setAttributes(lp);
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
|
||||
| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
|
||||
LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 监听子方法(细分类型,逻辑清晰) ======================
|
||||
/**
|
||||
* 输入框文本监听(RGB+颜色值,传入触发ID避免循环同步)
|
||||
*/
|
||||
private void initTextWatcherListener() {
|
||||
// RGB输入框监听(复用方法,减少冗余)
|
||||
setEditTextWatcher(etR, R.id.et_r);
|
||||
setEditTextWatcher(etG, R.id.et_g);
|
||||
setEditTextWatcher(etB, R.id.et_b);
|
||||
|
||||
// 颜色值输入框监听(支持#RRGGBB/#AARRGGBB格式)
|
||||
etColorValue.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
parseColorFromStr(s.toString().trim(), R.id.et_color_value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 透明度进度条监听实现(核心新增) ======================
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
// 仅处理用户手动拖动进度条(避免应用自身更新时触发)
|
||||
if (fromUser && !isAppSelfUpdatingColor) {
|
||||
updateAlphaBySeekBar(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
/**
|
||||
* 拖动透明度进度条更新颜色(核心新增逻辑)
|
||||
*/
|
||||
private synchronized void updateAlphaBySeekBar(int alphaPercent) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
try {
|
||||
// 更新实时透明度(百分比+0-255值)
|
||||
mCurrentAlphaPercent = alphaPercent;
|
||||
mCurrentAlpha = percent2Alpha(alphaPercent);
|
||||
// 重新计算最终颜色(基于当前亮度+新透明度)
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "update alpha by seekbar | 透明度:" + mCurrentAlphaPercent + "%");
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 颜色核心逻辑(新增透明度参数,全功能兼容) ======================
|
||||
/**
|
||||
* 核心计算:基于原始RGB+当前亮度+当前透明度,计算实时RGB+最终颜色
|
||||
* 逻辑:亮度百分比→调节系数→原始RGB×系数→限制0-255→拼接透明度→最终颜色
|
||||
*/
|
||||
private void calculateBrightnessAndUpdate() {
|
||||
// 亮度百分比转调节系数(10%→0.1,100%→1.0,200%→2.0)
|
||||
float brightnessFactor = mCurrentBrightnessPercent / 100.0f;
|
||||
|
||||
// RGB三个分量同时调节(基于原始基准值,避免叠加失真),限制0-255
|
||||
mCurrentR = Math.min(Math.max(Math.round(mOriginalR * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
mCurrentG = Math.min(Math.max(Math.round(mOriginalG * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
mCurrentB = Math.min(Math.max(Math.round(mOriginalB * brightnessFactor), 0), MAX_RGB_VALUE);
|
||||
|
||||
// 拼接「实时透明度」+「实时RGB」,得到最终颜色(0xAARRGGBB)
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度减少(每次减5%,最低10%,防止过暗)
|
||||
*/
|
||||
private void decreaseBrightness() {
|
||||
changeBrightness(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度增加(每次加5%,最高200%,防止过曝)
|
||||
*/
|
||||
private void increaseBrightness() {
|
||||
changeBrightness(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 亮度调节核心方法(统一逻辑,加并发控制,同步所有控件)
|
||||
*/
|
||||
private synchronized void changeBrightness(boolean isIncrease) {
|
||||
// 关键:判断非应用自身更新,才执行调节(避免重复触发/循环)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
try {
|
||||
if (isIncrease) {
|
||||
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return; // 达到最大值,不处理
|
||||
mCurrentBrightnessPercent += BRIGHTNESS_STEP; // 增加步长
|
||||
} else {
|
||||
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return; // 达到最小值,不处理
|
||||
mCurrentBrightnessPercent -= BRIGHTNESS_STEP; // 减少步长
|
||||
}
|
||||
// 计算亮度调节后的实时RGB+最终颜色(含当前透明度)
|
||||
calculateBrightnessAndUpdate();
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, (isIncrease ? "increase" : "decrease") + " brightness | "
|
||||
+ "亮度:" + mCurrentBrightnessPercent + "% | 实时RGB:" + mCurrentR + "," + mCurrentG + "," + mCurrentB);
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB,容错处理,更新原始基准值+实时值)
|
||||
* 新增:解析颜色的透明度,同步更新透明度进度条
|
||||
*/
|
||||
private void parseColorFromStr(String colorStr, int triggerViewId) {
|
||||
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
try {
|
||||
if (TextUtils.isEmpty(colorStr)) return;
|
||||
|
||||
// 补全#前缀(兼容用户输入习惯,如直接输AARRGGBB)
|
||||
if (!colorStr.startsWith("#")) {
|
||||
colorStr = "#" + colorStr;
|
||||
}
|
||||
|
||||
// 格式校验(仅支持6位RRGGBB/8位AARRGGBB,避免非法格式)
|
||||
if (colorStr.length() != 7 && colorStr.length() != 9) {
|
||||
LogUtils.e(TAG, "parse color failed | 格式错误(需#RRGGBB/#AARRGGBB),输入:" + colorStr);
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析颜色(系统API,安全可靠)
|
||||
int parsedColor = Color.parseColor(colorStr);
|
||||
|
||||
// 更新原始基准值(用户输入颜色,重置基准)
|
||||
// 透明度:解析颜色的alpha(0-255)转百分比(0-100%)
|
||||
mOriginalAlpha = Color.alpha(parsedColor);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
// RGB:解析颜色的RGB分量
|
||||
mOriginalR = Color.red(parsedColor);
|
||||
mOriginalG = Color.green(parsedColor);
|
||||
mOriginalB = Color.blue(parsedColor);
|
||||
// 更新实时值(原始值=实时值,无调节)
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
// 重置亮度为100%
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = parsedColor;
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "parse color success | 解析颜色:" + String.format("#%08X", parsedColor)
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "parse color failed | 非法颜色格式,输入:" + colorStr, e);
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过RGB输入框更新颜色(用户输入后,更新原始基准值+实时值,重置亮度为100%)
|
||||
* 新增:透明度基准值保持不变,仅更新RGB
|
||||
*/
|
||||
private synchronized void updateColorByRGB(int triggerViewId) {
|
||||
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true; // 标记为应用自身更新
|
||||
try {
|
||||
// 解析用户输入的RGB值(限制0-255,非法输入设为0)
|
||||
int inputR = parseInputValue(etR.getText().toString());
|
||||
int inputG = parseInputValue(etG.getText().toString());
|
||||
int inputB = parseInputValue(etB.getText().toString());
|
||||
|
||||
// 更新原始基准值(用户手动输入,作为新的调节基准)
|
||||
mOriginalR = inputR;
|
||||
mOriginalG = inputG;
|
||||
mOriginalB = inputB;
|
||||
// 更新实时值(输入值=实时值,无亮度调节)
|
||||
mCurrentR = inputR;
|
||||
mCurrentG = inputG;
|
||||
mCurrentB = inputB;
|
||||
// 重置亮度为100%(透明度保持当前值不变)
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
// 计算最终颜色(无亮度调节,拼接当前透明度)
|
||||
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
|
||||
|
||||
// 同步所有控件
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "update color by RGB | 新原始RGB:" + mOriginalR + "," + mOriginalG + "," + mOriginalB
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "update color by RGB failed", e);
|
||||
} finally {
|
||||
// 直接释放标记,避免卡顿
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心同步:更新所有控件显示(新增透明度控件同步,统一方法)
|
||||
*/
|
||||
private void updateAllViews() {
|
||||
// 1. 同步颜色预览(显示最终颜色,含透明度+亮度)
|
||||
ivColorPicker.setBackgroundColor(mCurrentColor);
|
||||
|
||||
// 2. 同步RGB输入框(显示实时调节值)
|
||||
etR.setText(String.valueOf(mCurrentR));
|
||||
etG.setText(String.valueOf(mCurrentG));
|
||||
etB.setText(String.valueOf(mCurrentB));
|
||||
|
||||
// 3. 同步颜色值输入框(显示最终颜色,含透明度,格式#AARRGGBB)
|
||||
etColorValue.setText(String.format("#%08X", mCurrentColor));
|
||||
|
||||
// 4. 同步透明度控件(进度条+文本,显示实时透明度)
|
||||
sbAlpha.setProgress(mCurrentAlphaPercent);
|
||||
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
|
||||
|
||||
// 5. 同步亮度控件(数值+按钮状态)
|
||||
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
|
||||
updateBrightnessBtnStatus();
|
||||
|
||||
LogUtils.d(TAG, "sync all views complete | 最终颜色:" + String.format("#%08X", mCurrentColor)
|
||||
+ " | 实时RGB:" + mCurrentR + "," + mCurrentG + "," + mCurrentB
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "% | 亮度:" + mCurrentBrightnessPercent + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新亮度按钮状态(边界值禁用,提升交互体验)
|
||||
*/
|
||||
private void updateBrightnessBtnStatus() {
|
||||
// 亮度≤10%:禁用减号(文字变浅灰);≥200%:禁用加号(文字变浅灰)
|
||||
boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS;
|
||||
boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS;
|
||||
|
||||
tvBrightnessMinus.setEnabled(canMinus);
|
||||
tvBrightnessPlus.setEnabled(canPlus);
|
||||
tvBrightnessMinus.setTextColor(canMinus ? Color.BLACK : Color.parseColor("#CCCCCC"));
|
||||
tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC"));
|
||||
}
|
||||
|
||||
// ====================== 工具方法(新增透明度转换工具,通用复用) ======================
|
||||
/**
|
||||
* 透明度:0-255 → 0-100%(颜色计算值转用户直观百分比)
|
||||
*/
|
||||
private int alpha2Percent(int alpha) {
|
||||
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 透明度:0-100% → 0-255(用户操作百分比转颜色计算值)
|
||||
*/
|
||||
private int percent2Alpha(int percent) {
|
||||
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析输入值(限制0-255,非法输入返回0,容错处理)
|
||||
*/
|
||||
private int parseInputValue(String input) {
|
||||
if (TextUtils.isEmpty(input)) return 0;
|
||||
try {
|
||||
int value = Integer.parseInt(input);
|
||||
return Math.min(Math.max(value, 0), MAX_RGB_VALUE); // 限制范围,避免溢出
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "parse input failed | 非法数字,输入:" + input, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB输入框监听复用(减少冗余代码,统一逻辑)
|
||||
*/
|
||||
private void setEditTextWatcher(EditText editText, final int viewId) {
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
updateColorByRGB(viewId); // 输入变化后更新颜色
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* dp转px(适配小米不同分辨率,避免尺寸错乱,通用工具)
|
||||
*/
|
||||
private int dp2px(float dp) {
|
||||
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示系统颜色选择器(兼容API29-30,无高版本依赖,小米机型适配)
|
||||
* 核心调整:新增「水平滚动容器+颜色排列容器」二级结构,内置圆形按钮,无额外drawable依赖
|
||||
*/
|
||||
private void showSystemColorPicker() {
|
||||
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30,支持横向滚动");
|
||||
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
|
||||
builder.setTitle("选择基础颜色");
|
||||
|
||||
// 50种常用颜色:按「红→橙→黄→绿→青→蓝→紫→粉→棕→灰→黑白」彩虹光谱顺序排列
|
||||
final int[] systemColors = {
|
||||
// 红色系(6种:深红→大红→浅红→玫红→暗红→橘红)
|
||||
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
|
||||
// 橙色系(5种:深橙→橙→浅橙→橙黄→橘橙)
|
||||
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
|
||||
// 黄色系(5种:深黄→黄→浅黄→鹅黄→金黄)
|
||||
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
|
||||
// 绿色系(7种:深绿→绿→浅绿→草绿→薄荷绿→翠绿→墨绿)
|
||||
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
|
||||
// 青色系(5种:深青→青→浅青→蓝绿→青绿)
|
||||
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
|
||||
// 蓝色系(8种:深蓝→藏蓝→蓝→浅蓝→天蓝→宝蓝→湖蓝→靛蓝)
|
||||
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
|
||||
// 紫色系(6种:深紫→紫→浅紫→紫罗兰→紫红→蓝紫)
|
||||
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
|
||||
// 粉色系(5种:深粉→粉→浅粉→嫩粉→桃粉)
|
||||
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
|
||||
// 棕色系(4种:深棕→棕→浅棕→棕黄)
|
||||
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
|
||||
// 灰色系(6种:深灰→灰→浅灰→银灰→淡灰→浅银灰)
|
||||
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
|
||||
// 黑白系(3种:黑→白→米白)
|
||||
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
|
||||
};
|
||||
|
||||
// 1. 第一级:水平滚动容器
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
|
||||
horizontalScrollView.setHorizontalScrollBarEnabled(true);
|
||||
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
|
||||
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
|
||||
|
||||
// 2. 第二级:颜色排列容器(横向)
|
||||
LinearLayout colorLayout = new LinearLayout(getContext());
|
||||
colorLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
colorLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
|
||||
|
||||
// 3. 循环添加颜色按钮(内置圆形效果,无额外依赖)
|
||||
for (int i = 0; i < systemColors.length; i++) {
|
||||
final int color = systemColors[i];
|
||||
ImageView colorBtn = new ImageView(getContext());
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
|
||||
if (i != systemColors.length - 1) {
|
||||
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
|
||||
}
|
||||
colorBtn.setLayoutParams(lp);
|
||||
|
||||
// 核心:内置圆形背景(白色边框+圆形形状,无需drawable文件)
|
||||
GradientDrawable circleBg = new GradientDrawable();
|
||||
circleBg.setShape(GradientDrawable.OVAL); // 圆形
|
||||
circleBg.setColor(color); // 按钮颜色
|
||||
circleBg.setStroke(dp2px(2), Color.WHITE); // 白色边框(2dp宽,区分颜色)
|
||||
colorBtn.setBackground(circleBg); // 设置圆形背景
|
||||
|
||||
colorBtn.setClickable(true);
|
||||
colorBtn.setFocusable(true);
|
||||
|
||||
// 点击事件(逻辑不变)
|
||||
colorBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
builder.create().dismiss();
|
||||
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "%");
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
colorLayout.addView(colorBtn);
|
||||
}
|
||||
|
||||
// 层级嵌套(滚动容器→颜色容器)
|
||||
horizontalScrollView.addView(colorLayout);
|
||||
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
|
||||
}
|
||||
|
||||
// ====================== 点击事件实现(统一处理,逻辑清晰) ======================
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//ToastUtils.show("onClick");
|
||||
int id = v.getId();
|
||||
// 关键:所有点击事件均加判断(避免并发冲突/重复触发)
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
if (id == R.id.iv_color_picker) {
|
||||
showSystemColorPicker(); // 打开系统颜色选择器
|
||||
} if (id == R.id.iv_color_scaler) {
|
||||
//ToastUtils.show("iv_color_scale");
|
||||
openColorScalerDialog(mCurrentColor); // 打开系统颜色选择器
|
||||
} else if (id == R.id.tv_confirm) {
|
||||
mListener.onColorSelected(mCurrentColor); // 确认选择,回调颜色
|
||||
LogUtils.d(TAG, "confirm color | 回调颜色:" + String.format("#%08X", mCurrentColor));
|
||||
dismiss();
|
||||
} else if (id == R.id.tv_cancel) {
|
||||
dismiss(); // 取消,关闭对话框
|
||||
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
|
||||
} else if (id == R.id.tv_brightness_minus) {
|
||||
decreaseBrightness(); // 减少亮度
|
||||
} else if (id == R.id.tv_brightness_plus) {
|
||||
increaseBrightness(); // 增加亮度
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void openColorScalerDialog(int nColor) {
|
||||
//ToastUtils.show("openColorPickerDialog");
|
||||
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
|
||||
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
|
||||
|
||||
@Override
|
||||
public void beforeColorChanged() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorChanged(int color) {
|
||||
dlg.currentColorScalerDialogColor = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterColorChanged() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
dlg.show();
|
||||
}
|
||||
|
||||
class ColorScalerDialog extends ColorPickerDialog {
|
||||
public int currentColorScalerDialogColor = 0;
|
||||
public ColorScalerDialog(Context context, int p) {
|
||||
super(context, p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
int color = currentColorScalerDialogColor;
|
||||
ToastUtils.show(String.format("dismiss color %d", color));
|
||||
if (!isAppSelfUpdatingColor) {
|
||||
isAppSelfUpdatingColor = true;
|
||||
try {
|
||||
mOriginalAlpha = Color.alpha(color);
|
||||
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
|
||||
mOriginalR = Color.red(color);
|
||||
mOriginalG = Color.green(color);
|
||||
mOriginalB = Color.blue(color);
|
||||
mCurrentAlpha = mOriginalAlpha;
|
||||
mCurrentAlphaPercent = mOriginalAlphaPercent;
|
||||
mCurrentR = mOriginalR;
|
||||
mCurrentG = mOriginalG;
|
||||
mCurrentB = mOriginalB;
|
||||
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
|
||||
mCurrentColor = color;
|
||||
updateAllViews();
|
||||
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
|
||||
+ " | 透明度:" + mCurrentAlphaPercent + "%");
|
||||
} finally {
|
||||
isAppSelfUpdatingColor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import cc.winboll.studio.powerbell.utils.ImageDownloader;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -52,7 +53,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
|
||||
// 按钮点击回调接口(Java7 接口实现)
|
||||
public interface OnDialogClickListener {
|
||||
void onConfirm(String szConfirmFilePath, String previewFileUrl); // 确认按钮点击
|
||||
void onConfirm(String szConfirmFilePath); // 确认按钮点击
|
||||
void onCancel(); // 取消按钮点击
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
case MSG_IMAGE_LOAD_SUCCESS:
|
||||
// 图片加载成功,获取文件路径并设置背景
|
||||
mDownloadSavedPath = (String) msg.obj;
|
||||
previewBackground(mDownloadSavedPath);
|
||||
mBackgroundView.loadImage(mDownloadSavedPath);
|
||||
break;
|
||||
case MSG_IMAGE_LOAD_FAILED:
|
||||
// 图片加载失败,设置默认背景
|
||||
@@ -139,7 +140,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
etURL = (EditText) dialogView.findViewById(R.id.et_url);
|
||||
mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
|
||||
// 加载初始图片
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
mBackgroundView.setBackgroundResource(R.drawable.blank100x100);
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListeners();
|
||||
}
|
||||
@@ -168,13 +169,13 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
|
||||
// 确定预览背景资源
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
|
||||
dismiss(); // 关闭对话框
|
||||
if(TextUtils.isEmpty(mDownloadSavedPath)) {
|
||||
ToastUtils.show("未下载图片。");
|
||||
return;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onConfirm(mPreviewFilePath, mPreviewFileUrl);
|
||||
listener.onConfirm(mDownloadSavedPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -207,7 +208,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
mPreviewFilePath = previewFilePath;
|
||||
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
|
||||
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
|
||||
mBackgroundView.loadBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
mBackgroundView.loadByBackgroundBean(utils.getPreviewBackgroundBean());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
|
||||
@@ -258,6 +259,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
|
||||
// 发送消息到主线程,携带图片路径
|
||||
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
|
||||
mUiHandler.sendMessage(successMsg);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import cc.winboll.studio.powerbell.views.BatteryDrawable;
|
||||
import cc.winboll.studio.powerbell.views.VerticalSeekBar;
|
||||
|
||||
public class MainViewFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "MainViewFragment";
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,35 +2,119 @@ package cc.winboll.studio.powerbell.handlers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* 服务通信Handler
|
||||
* 功能:处理电量提醒消息,构建并发送标准化通知
|
||||
* 特性:弱引用防泄漏、参数严格校验、通知格式统一
|
||||
* 适配:Java7 | API30 | 小米手机
|
||||
*/
|
||||
public class ControlCenterServiceHandler extends Handler {
|
||||
public static final String TAG = ControlCenterServiceHandler.class.getSimpleName();
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "ControlCenterServiceHandler";
|
||||
public static final int MSG_REMIND_TEXT = 1001; // 电量提醒消息标识
|
||||
|
||||
public static final int MSG_REMIND_TEXT = 0;
|
||||
// 提醒类型常量
|
||||
private static final String REMIND_TYPE_CHARGE = "+";
|
||||
private static final String REMIND_TYPE_USAGE = "-";
|
||||
|
||||
WeakReference<ControlCenterService> serviceWeakReference;
|
||||
// 电量范围常量
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// 通知文案常量(抽离魔法值,便于统一修改)
|
||||
private static final String CHARGE_REMIND_TITLE = "充电提醒";
|
||||
private static final String USAGE_REMIND_TITLE = "耗电提醒";
|
||||
private static final String CHARGE_REMIND_CONTENT_FORMAT = "(+)电量已达额定值。当前电量%d%%,%s。";
|
||||
private static final String USAGE_REMIND_CONTENT_FORMAT = "(-)电量低于指定值。当前电量%d%%,%s。";
|
||||
private static final String CHARGE_STATE_CHARGING = "充电中";
|
||||
private static final String CHARGE_STATE_NOT_CHARGING = "未充电";
|
||||
|
||||
// ================================== 成员变量区(弱引用防泄漏,final保证不可变)=================================
|
||||
private final WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
|
||||
// ================================== 构造方法(强制传入服务,初始化弱引用)=================================
|
||||
public ControlCenterServiceHandler(ControlCenterService service) {
|
||||
serviceWeakReference = new WeakReference<ControlCenterService>(service);
|
||||
LogUtils.d(TAG, "构造方法执行 | service=" + (service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ================================== 核心消息处理(重写handleMessage,解析多参数消息)=================================
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
// 解析消息参数:obj=提醒类型(+/-),arg1=当前电量,arg2=充电状态(1=充电/0=未充电)
|
||||
String remindType = (msg.obj != null) ? (String) msg.obj : "";
|
||||
int currentBattery = msg.arg1;
|
||||
boolean isCharging = msg.arg2 == 1;
|
||||
|
||||
LogUtils.d(TAG, "handleMessage: 接收消息 | what=" + msg.what + " | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
|
||||
|
||||
// 弱引用获取服务,避免内存泄漏
|
||||
ControlCenterService service = mwrControlCenterService.get();
|
||||
if (service == null) {
|
||||
LogUtils.e(TAG, "handleMessage: 服务实例已被GC回收,终止消息处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按消息类型分发处理
|
||||
switch (msg.what) {
|
||||
case MSG_REMIND_TEXT: // 处理下载完成消息,更新UI
|
||||
{
|
||||
// 显示提醒消息
|
||||
//
|
||||
//LogUtils.d(TAG, "显示提醒消息");
|
||||
ControlCenterService controlCenterService = serviceWeakReference.get();
|
||||
if (controlCenterService != null) {
|
||||
//LogUtils.d(TAG, ((NotificationMessage)msg.obj).getTitle());
|
||||
//LogUtils.d(TAG, ((NotificationMessage)msg.obj).getContent());
|
||||
controlCenterService.appenRemindMSG((String)msg.obj);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_REMIND_TEXT:
|
||||
handleRemindMessage(service, remindType, currentBattery, isCharging);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "handleMessage: 未知消息类型,忽略处理 | what=" + msg.what);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 业务辅助方法(构建通知并发送,全链路参数校验)=================================
|
||||
/**
|
||||
* 处理电量提醒消息,构建带电量+充电状态的通知并发送
|
||||
* @param service 控制中心服务实例(已校验非空)
|
||||
* @param remindType 提醒类型(+充电/-耗电)
|
||||
* @param currentBattery 当前电量(0-100)
|
||||
* @param isCharging 充电状态
|
||||
*/
|
||||
private void handleRemindMessage(ControlCenterService service, String remindType, int currentBattery, boolean isCharging) {
|
||||
LogUtils.d(TAG, "handleRemindMessage: 开始处理提醒消息 | type=" + remindType + " | battery=" + currentBattery + " | isCharging=" + isCharging);
|
||||
|
||||
// 1. 前置校验:通知工具类+参数有效性
|
||||
if (service.getNotificationManager() == null) {
|
||||
LogUtils.e(TAG, "handleRemindMessage: 通知管理工具类未初始化,无法发送提醒");
|
||||
return;
|
||||
}
|
||||
if (!REMIND_TYPE_CHARGE.equals(remindType) && !REMIND_TYPE_USAGE.equals(remindType)) {
|
||||
LogUtils.w(TAG, "handleRemindMessage: 提醒类型无效,忽略 | type=" + remindType + " | 允许值:" + REMIND_TYPE_CHARGE + "/" + REMIND_TYPE_USAGE);
|
||||
return;
|
||||
}
|
||||
if (currentBattery < BATTERY_LEVEL_MIN || currentBattery > BATTERY_LEVEL_MAX) {
|
||||
LogUtils.w(TAG, "handleRemindMessage: 电量值超出范围,忽略 | battery=" + currentBattery + " | 允许范围:" + BATTERY_LEVEL_MIN + "-" + BATTERY_LEVEL_MAX);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 构建通知模型,使用统一格式
|
||||
NotificationMessage remindMsg = new NotificationMessage();
|
||||
String chargeStateDesc = isCharging ? CHARGE_STATE_CHARGING : CHARGE_STATE_NOT_CHARGING;
|
||||
if (REMIND_TYPE_CHARGE.equals(remindType)) {
|
||||
remindMsg.setTitle(CHARGE_REMIND_TITLE);
|
||||
remindMsg.setContent(String.format(CHARGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc));
|
||||
remindMsg.setRemindMSG("charge_remind");
|
||||
} else {
|
||||
remindMsg.setTitle(USAGE_REMIND_TITLE);
|
||||
remindMsg.setContent(String.format(USAGE_REMIND_CONTENT_FORMAT, currentBattery, chargeStateDesc));
|
||||
remindMsg.setRemindMSG("usage_remind");
|
||||
}
|
||||
LogUtils.d(TAG, "handleRemindMessage: 通知模型构建完成 | title=" + remindMsg.getTitle() + " | content=" + remindMsg.getContent());
|
||||
|
||||
// 3. 调用通知工具类发送提醒
|
||||
LogUtils.d(TAG, "handleRemindMessage: 调用通知工具类发送提醒 | remindMSG=" + remindMsg.getRemindMSG());
|
||||
service.getNotificationManager().showRemindNotification(service, remindMsg);
|
||||
LogUtils.d(TAG, "handleRemindMessage: 提醒通知发送流程执行完毕");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/29 17:24:53
|
||||
* @Describe 应用运行参数类
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class AppConfigBean extends BaseBean implements Serializable {
|
||||
|
||||
transient public static final String TAG = "AppConfigBean";
|
||||
|
||||
boolean isEnableUsegeReminder = false;
|
||||
int usegeReminderValue = 45;
|
||||
boolean isEnableChargeReminder = false;
|
||||
int chargeReminderValue = 100;
|
||||
// 铃声提醒间隔时间。.
|
||||
int reminderIntervalTime = 5000;
|
||||
// 电池是否正在充电。
|
||||
boolean isCharging = false;
|
||||
// 电池当前电量。.
|
||||
int currentValue = -1;
|
||||
|
||||
public AppConfigBean() {
|
||||
setChargeReminderValue(100);
|
||||
setIsEnableChargeReminder(false);
|
||||
setUsegeReminderValue(10);
|
||||
setIsEnableUsegeReminder(false);
|
||||
setReminderIntervalTime(5000);
|
||||
}
|
||||
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
this.reminderIntervalTime = reminderIntervalTime;
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return reminderIntervalTime;
|
||||
}
|
||||
|
||||
public void setIsCharging(boolean isCharging) {
|
||||
this.isCharging = isCharging;
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
public void setCurrentValue(int currentValue) {
|
||||
this.currentValue = currentValue;
|
||||
}
|
||||
|
||||
public int getCurrentValue() {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
|
||||
this.isEnableUsegeReminder = isEnableUsegeReminder;
|
||||
}
|
||||
|
||||
public boolean isEnableUsegeReminder() {
|
||||
return isEnableUsegeReminder;
|
||||
}
|
||||
|
||||
public void setUsegeReminderValue(int usegeReminderValue) {
|
||||
this.usegeReminderValue = usegeReminderValue;
|
||||
}
|
||||
|
||||
public int getUsegeReminderValue() {
|
||||
return usegeReminderValue;
|
||||
}
|
||||
|
||||
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
this.isEnableChargeReminder = isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public void setChargeReminderValue(int chargeReminderValue) {
|
||||
this.chargeReminderValue = chargeReminderValue;
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
AppConfigBean bean = this;
|
||||
jsonWriter.name("isEnableUsegeReminder").value(bean.isEnableUsegeReminder());
|
||||
jsonWriter.name("usegeReminderValue").value(bean.getUsegeReminderValue());
|
||||
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
|
||||
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("isEnableUsegeReminder")) {
|
||||
bean.setIsEnableUsegeReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("usegeReminderValue")) {
|
||||
bean.setUsegeReminderValue(jsonReader.nextInt());
|
||||
} else if (name.equals("isEnableChargeReminder")) {
|
||||
bean.setIsEnableChargeReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("chargeReminderValue")) {
|
||||
bean.setChargeReminderValue(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 07:06:07
|
||||
* @Describe 服务控制参数
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ControlCenterServiceBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "ControlCenterServiceBean";
|
||||
|
||||
boolean isEnableService = false;
|
||||
|
||||
public ControlCenterServiceBean() {
|
||||
this.isEnableService = false;
|
||||
}
|
||||
|
||||
public ControlCenterServiceBean(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
public void setIsEnableService(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
public boolean isEnableService() {
|
||||
return isEnableService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return ControlCenterServiceBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
ControlCenterServiceBean bean = this;
|
||||
jsonWriter.name("isEnableService").value(bean.isEnableService());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (name.equals("isEnableService")) {
|
||||
bean.setIsEnableService(jsonReader.nextBoolean());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
// 结束 JSON 对象
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
// 应用消息结构
|
||||
//
|
||||
public class NotificationMessage {
|
||||
|
||||
String Title;
|
||||
String Content;
|
||||
String RemindMSG;
|
||||
|
||||
public NotificationMessage(String title, String content) {
|
||||
Title = title;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public void setRemindMSG(String remindMSG) {
|
||||
RemindMSG = remindMSG;
|
||||
}
|
||||
|
||||
public String getRemindMSG() {
|
||||
return RemindMSG;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
Title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return Title;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return Content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/04/29 17:24:53
|
||||
* @Describe 应用运行参数类
|
||||
*/
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
// 核心修正:新增 Parcelable 接口实现(API30 持久化/Intent 传递必备)
|
||||
public class AppConfigBean extends BaseBean implements Serializable, Parcelable {
|
||||
|
||||
// 序列化版本号(Serializable 必备,避免反序列化失败)
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
transient public static final String TAG = "AppConfigBean";
|
||||
|
||||
// 核心配置字段(保留原有字段,统一状态字段命名)
|
||||
boolean isEnableUsageReminder = false; // 耗电提醒开关
|
||||
int usageReminderValue = 45; // 耗电提醒阈值(0-100)
|
||||
boolean isEnableChargeReminder = false;// 充电提醒开关
|
||||
int chargeReminderValue = 100; // 充电提醒阈值(0-100)
|
||||
int reminderIntervalTime = 5000; // 铃声提醒间隔(ms,原有)
|
||||
boolean isCharging = false; // 是否充电(状态字段,原有)
|
||||
int currentBatteryValue = -1; // 修正:统一命名为「currentBatteryValue」(原 currentValue)
|
||||
int batteryDetectInterval = 2000; // 新增:电量检测间隔(ms,适配 RemindThread)
|
||||
|
||||
// 构造方法:初始化默认配置(同步修正字段名,统一默认值)
|
||||
public AppConfigBean() {
|
||||
setChargeReminderValue(100);
|
||||
setEnableChargeReminder(false);
|
||||
setUsageReminderValue(10);
|
||||
setEnableUsageReminder(false);
|
||||
setReminderIntervalTime(5000);
|
||||
setBatteryDetectInterval(1000); // 新增:默认检测间隔1秒
|
||||
setCurrentBatteryValue(-1); // 修正:初始化当前电量字段
|
||||
}
|
||||
|
||||
// ====================== 核心修复:补全缺失方法(适配 RemindThread/Receiver 调用) ======================
|
||||
/**
|
||||
* 设置当前电池电量(Receiver 监听电池变化时调用,与 RemindThread 字段对齐)
|
||||
*/
|
||||
public void setCurrentBatteryValue(int currentBatteryValue) {
|
||||
// 强化校验:电量范围限制在 0-100,异常值置为 -1(标识无效)
|
||||
this.currentBatteryValue = (currentBatteryValue >= 0 && currentBatteryValue <= 100)
|
||||
? currentBatteryValue : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前电池电量(RemindThread 同步配置时调用,与 set 方法对应)
|
||||
*/
|
||||
public int getCurrentBatteryValue() {
|
||||
return currentBatteryValue;
|
||||
}
|
||||
|
||||
// ====================== 原有字段 Setter/Getter(修正命名,强化校验) ======================
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
// 校验:提醒间隔不小于 1000ms,避免频繁提醒
|
||||
this.reminderIntervalTime = Math.max(reminderIntervalTime, 1000);
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return reminderIntervalTime;
|
||||
}
|
||||
|
||||
public void setIsCharging(boolean isCharging) { // 修正:方法名与字段名统一(原 setCharging)
|
||||
this.isCharging = isCharging;
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
public void setEnableUsageReminder(boolean isEnableUsageReminder) {
|
||||
this.isEnableUsageReminder = isEnableUsageReminder;
|
||||
}
|
||||
|
||||
public boolean isEnableUsageReminder() {
|
||||
return isEnableUsageReminder;
|
||||
}
|
||||
|
||||
public void setUsageReminderValue(int usageReminderValue) {
|
||||
// 校验:阈值范围 0-100
|
||||
this.usageReminderValue = Math.min(Math.max(usageReminderValue, 0), 100);
|
||||
}
|
||||
|
||||
public int getUsageReminderValue() {
|
||||
return usageReminderValue;
|
||||
}
|
||||
|
||||
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
this.isEnableChargeReminder = isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public void setChargeReminderValue(int chargeReminderValue) {
|
||||
// 校验:阈值范围 0-100
|
||||
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, 0), 100);
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
// ====================== 电量检测间隔 Setter/Getter(适配 RemindThread) ======================
|
||||
public int getBatteryDetectInterval() {
|
||||
return batteryDetectInterval;
|
||||
}
|
||||
|
||||
// 强化校验:检测间隔不小于500ms(避免 CPU 高占用,与 RemindThread 最小休眠一致)
|
||||
public void setBatteryDetectInterval(int batteryDetectInterval) {
|
||||
this.batteryDetectInterval = Math.max(batteryDetectInterval, 500);
|
||||
}
|
||||
|
||||
// ====================== JSON 序列化/反序列化(兼容旧配置,同步修正字段) ======================
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
AppConfigBean bean = this;
|
||||
// 原有字段序列化(保留拼写兼容,同步修正字段名)
|
||||
jsonWriter.name("isEnableUsageReminder").value(bean.isEnableUsageReminder());
|
||||
jsonWriter.name("usageReminderValue").value(bean.getUsageReminderValue());
|
||||
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
|
||||
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
|
||||
jsonWriter.name("reminderIntervalTime").value(bean.getReminderIntervalTime());
|
||||
jsonWriter.name("isCharging").value(bean.isCharging());
|
||||
// 修正:序列化新字段名 currentBatteryValue,兼容旧字段 currentValue
|
||||
jsonWriter.name("currentBatteryValue").value(bean.getCurrentBatteryValue());
|
||||
jsonWriter.name("currentValue").value(bean.getCurrentBatteryValue()); // 兼容旧配置,避免数据丢失
|
||||
// 新增字段序列化:电量检测间隔
|
||||
jsonWriter.name("batteryDetectInterval").value(bean.getBatteryDetectInterval());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
// 原有字段反序列化(兼容旧 Key 拼写,同步修正字段)
|
||||
if (name.equals("isEnableUsageReminder") || name.equals("isEnableUsegeReminder")) {
|
||||
bean.setEnableUsageReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("usageReminderValue") || name.equals("usegeReminderValue")) {
|
||||
bean.setUsageReminderValue(jsonReader.nextInt());
|
||||
} else if (name.equals("isEnableChargeReminder")) {
|
||||
bean.setEnableChargeReminder(jsonReader.nextBoolean());
|
||||
} else if (name.equals("chargeReminderValue")) {
|
||||
bean.setChargeReminderValue(jsonReader.nextInt());
|
||||
} else if (name.equals("reminderIntervalTime")) {
|
||||
bean.setReminderIntervalTime(jsonReader.nextInt());
|
||||
} else if (name.equals("isCharging")) {
|
||||
bean.setIsCharging(jsonReader.nextBoolean()); // 修正:调用新方法名
|
||||
}
|
||||
// 核心兼容:优先读取旧字段 currentValue,再读取新字段 currentBatteryValue(新字段覆盖旧字段)
|
||||
else if (name.equals("currentValue")) {
|
||||
bean.setCurrentBatteryValue(jsonReader.nextInt());
|
||||
} else if (name.equals("currentBatteryValue")) {
|
||||
bean.setCurrentBatteryValue(jsonReader.nextInt());
|
||||
}
|
||||
// 新增字段反序列化(兼容无此字段的旧配置,用默认值1000ms)
|
||||
else if (name.equals("batteryDetectInterval")) {
|
||||
bean.setBatteryDetectInterval(jsonReader.nextInt());
|
||||
} else {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ====================== Parcelable 接口实现(同步修正字段,确保 Intent 传递正常) ======================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0; // 无特殊内容描述,固定返回0
|
||||
}
|
||||
|
||||
// 序列化:将所有字段写入 Parcel(同步修正字段名,Java7 适配)
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeByte((byte) (isEnableUsageReminder ? 1 : 0)); // boolean → byte
|
||||
dest.writeInt(usageReminderValue);
|
||||
dest.writeByte((byte) (isEnableChargeReminder ? 1 : 0)); // boolean → byte
|
||||
dest.writeInt(chargeReminderValue);
|
||||
dest.writeInt(reminderIntervalTime);
|
||||
dest.writeByte((byte) (isCharging ? 1 : 0)); // boolean → byte
|
||||
dest.writeInt(currentBatteryValue); // 修正:序列化新字段名
|
||||
dest.writeInt(batteryDetectInterval);
|
||||
}
|
||||
|
||||
// 反序列化:从 Parcel 读取字段,创建对象(必须 public static final 修饰)
|
||||
public static final Parcelable.Creator<AppConfigBean> CREATOR = new Parcelable.Creator<AppConfigBean>() {
|
||||
@Override
|
||||
public AppConfigBean createFromParcel(Parcel source) {
|
||||
AppConfigBean bean = new AppConfigBean();
|
||||
// 按 writeToParcel 顺序读取,同步修正字段
|
||||
bean.isEnableUsageReminder = source.readByte() != 0;
|
||||
bean.usageReminderValue = source.readInt();
|
||||
bean.isEnableChargeReminder = source.readByte() != 0;
|
||||
bean.chargeReminderValue = source.readInt();
|
||||
bean.reminderIntervalTime = source.readInt();
|
||||
bean.isCharging = source.readByte() != 0;
|
||||
bean.currentBatteryValue = source.readInt(); // 修正:读取新字段名
|
||||
bean.batteryDetectInterval = source.readInt();
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigBean[] newArray(int size) {
|
||||
return new AppConfigBean[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import 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 {
|
||||
public class BackgroundBean extends BaseBean implements Serializable {
|
||||
|
||||
public static final String TAG = "BackgroundPictureBean";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
@@ -0,0 +1,142 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 15:55
|
||||
* @Describe 服务控制参数模型:管理服务启用状态,支持序列化、Parcel传递、JSON解析
|
||||
*/
|
||||
public class ControlCenterServiceBean extends BaseBean implements Parcelable, Serializable {
|
||||
// ================================== 静态常量(置顶统一管理,避免魔法值)=================================
|
||||
private static final long serialVersionUID = 1L; // Serializable 必备,保障反序列化兼容
|
||||
private static final String TAG = "ControlCenterServiceBean";
|
||||
// JSON 字段常量,避免硬编码,减少拼写错误
|
||||
private static final String JSON_FIELD_IS_ENABLE_SERVICE = "isEnableService";
|
||||
|
||||
// ================================== 核心成员变量(私有封装,规范命名)=================================
|
||||
private boolean isEnableService = false; // 服务启用状态:true=启用,false=禁用
|
||||
|
||||
// ================================== Parcelable 静态创建器(必须 public static final,适配 API30 传递)=================================
|
||||
public static final Parcelable.Creator<ControlCenterServiceBean> CREATOR = new Parcelable.Creator<ControlCenterServiceBean>() {
|
||||
@Override
|
||||
public ControlCenterServiceBean createFromParcel(Parcel source) {
|
||||
LogUtils.d(TAG, "Parcelable createFromParcel: 从Parcel反序列化对象");
|
||||
// Java7 + API30 适配:Parcel 无直接 writeBoolean,用 byte 存储/读取
|
||||
boolean isEnable = source.readByte() != 0;
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnable);
|
||||
LogUtils.d(TAG, "Parcelable createFromParcel: 反序列化完成,isEnableService=" + isEnable);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlCenterServiceBean[] newArray(int size) {
|
||||
LogUtils.d(TAG, "Parcelable newArray: 创建数组,长度=" + size);
|
||||
return new ControlCenterServiceBean[size];
|
||||
}
|
||||
};
|
||||
|
||||
// ================================== 构造方法(无参+有参,满足不同初始化场景)=================================
|
||||
/**
|
||||
* 无参构造(JSON解析、反射创建必备)
|
||||
*/
|
||||
public ControlCenterServiceBean() {
|
||||
this.isEnableService = false;
|
||||
LogUtils.d(TAG, "无参构造:初始化服务状态为禁用(false)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 有参构造(指定服务启用状态)
|
||||
* @param isEnableService 服务启用状态
|
||||
*/
|
||||
public ControlCenterServiceBean(boolean isEnableService) {
|
||||
this.isEnableService = isEnableService;
|
||||
LogUtils.d(TAG, "有参构造:初始化服务状态,isEnableService=" + isEnableService);
|
||||
}
|
||||
|
||||
// ================================== Getter/Setter 方法(封装成员变量,控制访问)=================================
|
||||
public boolean isEnableService() {
|
||||
LogUtils.d(TAG, "get isEnableService: 当前状态=" + isEnableService);
|
||||
return isEnableService;
|
||||
}
|
||||
|
||||
public void setIsEnableService(boolean isEnableService) {
|
||||
LogUtils.d(TAG, "set isEnableService: 旧状态=" + this.isEnableService + ",新状态=" + isEnableService);
|
||||
this.isEnableService = isEnableService;
|
||||
}
|
||||
|
||||
// ================================== 父类 BaseBean 方法重写(核心业务逻辑)=================================
|
||||
@Override
|
||||
public String getName() {
|
||||
String className = ControlCenterServiceBean.class.getName();
|
||||
LogUtils.d(TAG, "getName: 返回类名=" + className);
|
||||
return className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 JSON(适配数据持久化/网络传输)
|
||||
*/
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将对象序列化到JSON");
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
// 写入服务启用状态字段
|
||||
jsonWriter.name(JSON_FIELD_IS_ENABLE_SERVICE).value(this.isEnableService);
|
||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成,字段=" + JSON_FIELD_IS_ENABLE_SERVICE + ",值=" + this.isEnableService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 反序列化创建对象(适配数据恢复)
|
||||
*/
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON反序列化对象");
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean();
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String fieldName = jsonReader.nextName();
|
||||
if (JSON_FIELD_IS_ENABLE_SERVICE.equals(fieldName)) {
|
||||
// 读取并设置服务启用状态
|
||||
boolean isEnable = jsonReader.nextBoolean();
|
||||
bean.setIsEnableService(isEnable);
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: 读取JSON字段," + fieldName + "=" + isEnable);
|
||||
} else {
|
||||
// 跳过未知字段,避免解析异常
|
||||
jsonReader.skipValue();
|
||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段=" + fieldName);
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成");
|
||||
return bean;
|
||||
}
|
||||
|
||||
// ================================== Parcelable 接口方法实现(适配 Intent 组件间传递)=================================
|
||||
@Override
|
||||
public int describeContents() {
|
||||
// 无特殊内容(如文件描述符),返回0即可(API30 标准实现)
|
||||
LogUtils.d(TAG, "describeContents: 返回内容描述符=0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象到 Parcel(Intent 传递必备,Java7 适配)
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
LogUtils.d(TAG, "writeToParcel: 开始将对象序列化到Parcel,flags=" + flags);
|
||||
// Java7 + API30 适配:Parcel 无 writeBoolean 方法,用 byte 存储(1=true,0=false)
|
||||
dest.writeByte((byte) (this.isEnableService ? 1 : 0));
|
||||
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成,isEnableService=" + this.isEnableService + "(存储为byte=" + (this.isEnableService ? 1 : 0) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package cc.winboll.studio.powerbell.models;
|
||||
|
||||
/**
|
||||
* 通知数据模型:统一存储通知标题、内容等信息,适配各组件数据传递
|
||||
*/
|
||||
public class NotificationMessage {
|
||||
private String title; // 通知标题
|
||||
private String content; // 通知内容
|
||||
private String remindMSG; // 通知标识(区分服务运行/充电/耗电)
|
||||
|
||||
// ====================== Setter/Getter 方法 ======================
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getRemindMSG() {
|
||||
return remindMSG;
|
||||
}
|
||||
|
||||
public void setRemindMSG(String remindMSG) {
|
||||
this.remindMSG = remindMSG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,83 +5,251 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.model.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* 控制中心广播接收器
|
||||
* 功能:监听电池状态变化、前台通知更新、配置变更指令
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:23
|
||||
*/
|
||||
public class ControlCenterServiceReceiver extends BroadcastReceiver {
|
||||
public static final String TAG = ControlCenterServiceReceiver.class.getSimpleName();
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "ControlCenterServiceReceiver";
|
||||
|
||||
public static final String ACTION_UPDATE_SERVICENOTIFICATION = ControlCenterServiceReceiver.class.getName() + ".ACTION_UPDATE_NOTIFICATION";
|
||||
public static final String ACTION_START_REMINDTHREAD = ControlCenterServiceReceiver.class.getName() + ".ACTION_UPDATE_REMINDTHREAD";
|
||||
// 广播Action常量(带包名前缀防冲突)
|
||||
public static final String ACTION_UPDATE_FOREGROUND_NOTIFICATION = "cc.winboll.studio.powerbell.action.ACTION_UPDATE_FOREGROUND_NOTIFICATION";
|
||||
public static final String ACTION_APPCONFIG_CHANGED = "cc.winboll.studio.powerbell.action.ACTION_APPCONFIG_CHANGED";
|
||||
public static final String EXTRA_APP_CONFIG_BEAN = "extra_app_config_bean";
|
||||
|
||||
WeakReference<ControlCenterService> mwrService;
|
||||
// 存储电量指示值,
|
||||
// 用于校验电量消息时的电量变化
|
||||
static volatile int _mnTheQuantityOfElectricityOld = -1;
|
||||
static volatile boolean _mIsCharging = false;
|
||||
// 广播优先级与电量范围常量
|
||||
private static final int BROADCAST_PRIORITY = IntentFilter.SYSTEM_HIGH_PRIORITY - 10;
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// ================================== 静态状态标记(volatile保证多线程可见性)=================================
|
||||
private static volatile int sLastBatteryLevel = -1; // 上次电量(多线程可见)
|
||||
private static volatile boolean sIsCharging = false; // 上次充电状态(多线程可见)
|
||||
|
||||
// ================================== 成员变量区(弱引用防泄漏,按功能分层)=================================
|
||||
private WeakReference<ControlCenterService> mwrControlCenterService;
|
||||
private boolean isRegistered = false; // 新增:标记广播注册状态,避免冗余操作
|
||||
|
||||
// ================================== 构造方法(初始化弱引用,避免服务强引用泄漏)=================================
|
||||
public ControlCenterServiceReceiver(ControlCenterService service) {
|
||||
mwrService = new WeakReference<ControlCenterService>(service);
|
||||
LogUtils.d(TAG, "构造接收器 | service=" + (service != null ? service.getClass().getSimpleName() : "null"));
|
||||
this.mwrControlCenterService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
// ================================== 广播核心接收逻辑(入口方法,分Action分发处理)=================================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(ACTION_UPDATE_SERVICENOTIFICATION)) {
|
||||
mwrService.get().updateServiceNotification();
|
||||
} else if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
|
||||
boolean isCharging = BatteryUtils.isCharging(intent);
|
||||
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
|
||||
if (mwrService.get().getRemindThread() != null) {
|
||||
// 先设置提醒进程电池状态标志
|
||||
if (_mIsCharging != isCharging) {
|
||||
mwrService.get().getRemindThread().setIsCharging(isCharging);
|
||||
}
|
||||
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
mwrService.get().getRemindThread().setQuantityOfElectricity(nTheQuantityOfElectricity);
|
||||
}
|
||||
LogUtils.d(TAG, "onReceive: 接收广播 | action=" + (intent != null ? intent.getAction() : "null"));
|
||||
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null || intent.getAction() == null) {
|
||||
LogUtils.e(TAG, "onReceive: 参数无效(context=" + context + " | intent=" + intent + "),终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 弱引用获取服务,双重校验服务有效性
|
||||
ControlCenterService service = mwrControlCenterService != null ? mwrControlCenterService.get() : null;
|
||||
if (service == null || service.isDestroyed()) {
|
||||
LogUtils.e(TAG, "onReceive: 服务已销毁或为空(service=" + service + "),注销广播");
|
||||
unregisterAction(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 分Action处理业务逻辑
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case Intent.ACTION_BATTERY_CHANGED:
|
||||
handleBatteryStateChanged(service, intent);
|
||||
break;
|
||||
case ACTION_UPDATE_FOREGROUND_NOTIFICATION:
|
||||
handleUpdateForegroundNotification(service);
|
||||
break;
|
||||
case ACTION_APPCONFIG_CHANGED:
|
||||
LogUtils.d(TAG, "onReceive: 开始处理配置更新广播"); // 新增:标记配置广播处理起点
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onReceive: 未知Action=" + action);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ================================== 业务处理方法(按功能拆分,强化容错与日志)=================================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param service 控制中心服务实例
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(ControlCenterService service, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态 | service=" + service + " | intent=" + intent);
|
||||
try {
|
||||
// 1. 解析并校验当前电池状态
|
||||
boolean currentCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 当前状态 | 充电=" + currentCharging + " | 电量=" + currentBatteryLevel + "%");
|
||||
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentCharging == sIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
// 新电池状态标志某一个有变化就更新显示信息
|
||||
if (_mIsCharging != isCharging || _mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
mwrService.get().updateServiceNotification();
|
||||
AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(context);
|
||||
appConfigUtils.loadAppConfigBean();
|
||||
AppConfigBean appConfigBean = appConfigUtils.mAppConfigBean;
|
||||
appConfigBean.setCurrentValue(nTheQuantityOfElectricity);
|
||||
appConfigBean.setIsCharging(isCharging);
|
||||
mwrService.get().startRemindThread(appConfigBean);
|
||||
|
||||
// 保存电池报告
|
||||
// 示例数据更新逻辑
|
||||
// List<BatteryData> newData = new ArrayList<>(adapter.getDataList());
|
||||
// newData.add(0, new BatteryData(percentage, "00:00:00", "00:00:00"));
|
||||
// adapter.updateData(newData);
|
||||
|
||||
// 保存好新的电池状态标志
|
||||
_mIsCharging = isCharging;
|
||||
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
|
||||
}
|
||||
} else if (intent.getAction().equals(ACTION_START_REMINDTHREAD)) {
|
||||
LogUtils.d(TAG, "ACTION_START_REMINDTHREAD");
|
||||
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance(context);
|
||||
//appConfigUtils.loadAppConfigBean();
|
||||
AppConfigBean appConfigBean = (AppConfigBean)intent.getSerializableExtra("appConfigBean");
|
||||
mwrService.get().startRemindThread(appConfigBean);
|
||||
|
||||
// 4. 更新静态缓存状态,保证多线程可见
|
||||
sIsCharging = currentCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
|
||||
handleNotifyAppConfigUpdate(service);
|
||||
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 电池状态处理成功 | 缓存电量=" + sLastBatteryLevel + "% | 缓存充电状态=" + sIsCharging);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Receiver
|
||||
//
|
||||
/**
|
||||
* 处理配置变更通知,同步缓存状态到配置
|
||||
* @param service 控制中心服务实例
|
||||
*/
|
||||
private void handleNotifyAppConfigUpdate(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 同步缓存状态到配置 | service=" + service);
|
||||
try {
|
||||
// 加载最新配置
|
||||
AppConfigBean latestConfig = AppConfigUtils.getInstance(service).loadAppConfig();
|
||||
if (latestConfig == null) { // 新增:配置空指针防护
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 最新配置为空,终止处理");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 加载最新配置 | 充电阈值=" + latestConfig.getChargeReminderValue() + " | 耗电阈值=" + latestConfig.getUsageReminderValue());
|
||||
|
||||
// 同步缓存的电池状态到配置
|
||||
latestConfig.setCurrentBatteryValue(sLastBatteryLevel);
|
||||
latestConfig.setIsCharging(sIsCharging);
|
||||
service.notifyAppConfigUpdate(latestConfig);
|
||||
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置同步成功 | 缓存电量=" + sLastBatteryLevel + "% | 充电状态=" + sIsCharging);
|
||||
LogUtils.d(TAG, "handleNotifyAppConfigUpdate: 配置更新广播处理完成"); // 新增:标记配置广播处理终点
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleNotifyAppConfigUpdate: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前台服务通知更新
|
||||
* @param service 控制中心服务实例
|
||||
*/
|
||||
private void handleUpdateForegroundNotification(ControlCenterService service) {
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification: 更新前台通知 | service=" + service);
|
||||
try {
|
||||
NotificationManagerUtils notifyUtils = service.getNotificationManager();
|
||||
NotificationMessage notifyMsg = service.getForegroundNotifyMsg();
|
||||
|
||||
// 非空校验,避免空指针
|
||||
if (notifyUtils == null || notifyMsg == null) {
|
||||
LogUtils.e(TAG, "handleUpdateForegroundNotification: 通知工具类或消息为空(notifyUtils=" + notifyUtils + " | notifyMsg=" + notifyMsg + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
notifyUtils.updateForegroundServiceNotify(notifyMsg);
|
||||
LogUtils.d(TAG, "handleUpdateForegroundNotification: 前台通知更新成功 | 通知标题=" + notifyMsg.getTitle());
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "handleUpdateForegroundNotification: 处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 广播注册/注销(强化容错,避免重复操作)=================================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
* @param context 上下文
|
||||
*/
|
||||
public void registerAction(Context context) {
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(ACTION_UPDATE_SERVICENOTIFICATION);
|
||||
filter.addAction(ACTION_START_REMINDTHREAD);
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
context.registerReceiver(this, filter);
|
||||
LogUtils.d(TAG, "registerAction: 注册广播接收器 | context=" + context);
|
||||
if (context == null || isRegistered) { // 新增:已注册则跳过
|
||||
LogUtils.e(TAG, "registerAction: 上下文为空或已注册,注册失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(ACTION_UPDATE_FOREGROUND_NOTIFICATION);
|
||||
filter.addAction(ACTION_APPCONFIG_CHANGED);
|
||||
filter.setPriority(BROADCAST_PRIORITY);
|
||||
|
||||
context.registerReceiver(this, filter);
|
||||
isRegistered = true; // 标记为已注册
|
||||
LogUtils.d(TAG, "registerAction: 广播注册成功 | 优先级=" + BROADCAST_PRIORITY);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器
|
||||
* @param context 上下文
|
||||
*/
|
||||
public void unregisterAction(Context context) {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播接收器 | context=" + context);
|
||||
if (context == null || !isRegistered) { // 新增:未注册则跳过
|
||||
LogUtils.e(TAG, "unregisterAction: 上下文为空或未注册,注销失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
isRegistered = false; // 标记为未注册
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 资源释放与Getter方法(按需开放,防泄漏)=================================
|
||||
/**
|
||||
* 主动释放资源,避免内存泄漏
|
||||
*/
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release: 释放广播接收器资源");
|
||||
// 清空弱引用,帮助GC回收
|
||||
if (mwrControlCenterService != null) {
|
||||
mwrControlCenterService.clear();
|
||||
mwrControlCenterService = null;
|
||||
LogUtils.d(TAG, "release: 弱引用已清空");
|
||||
}
|
||||
// 重置静态状态缓存
|
||||
sLastBatteryLevel = -1;
|
||||
sIsCharging = false;
|
||||
LogUtils.d(TAG, "release: 静态状态缓存已重置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上次记录的电池电量
|
||||
* @return 电量值(0-100),未初始化返回-1
|
||||
*/
|
||||
public static int getLastBatteryLevel() {
|
||||
return sLastBatteryLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上次记录的充电状态
|
||||
* @return true=充电中,false=未充电
|
||||
*/
|
||||
public static boolean isLastCharging() {
|
||||
return sIsCharging;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,62 +4,175 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.BatteryUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/19 20:13
|
||||
* @Describe 全局应用广播接收器
|
||||
* 功能:监听系统电池状态变化,同步状态到配置工具类,通知页面更新
|
||||
* 适配:Java7 | API30 | 内存泄漏防护
|
||||
*/
|
||||
public class GlobalApplicationReceiver extends BroadcastReceiver {
|
||||
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "GlobalApplicationReceiver";
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
App mGlobalApplication;
|
||||
// 存储电量指示值,
|
||||
// 用于校验电量消息时的电量变化
|
||||
static volatile int _mnTheQuantityOfElectricityOld = -1;
|
||||
static volatile boolean _mIsCharging = false;
|
||||
// 保存当前实例,
|
||||
// 便利封装 registerAction() 函数
|
||||
GlobalApplicationReceiver mReceiver;
|
||||
// ================================== 静态成员变量(线程安全,volatile保证多线程可见性)=================================
|
||||
private static volatile int sLastBatteryLevel = -1; // 历史电量(0-100)
|
||||
private static volatile boolean sLastIsCharging = false; // 历史充电状态
|
||||
|
||||
// ================================== 成员变量区(按功能分层)=================================
|
||||
private App mGlobalApplication;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private GlobalApplicationReceiver mCurrentReceiver;
|
||||
|
||||
// ================================== 构造方法(强化参数校验,初始化核心依赖)=================================
|
||||
public GlobalApplicationReceiver(App globalApplication) {
|
||||
mReceiver = this;
|
||||
mGlobalApplication = globalApplication;
|
||||
mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
|
||||
LogUtils.d(TAG, "构造接收器 | App=" + globalApplication);
|
||||
if (globalApplication == null) {
|
||||
LogUtils.e(TAG, "构造失败:App实例为空");
|
||||
throw new IllegalArgumentException("App cannot be null");
|
||||
}
|
||||
this.mCurrentReceiver = this;
|
||||
this.mGlobalApplication = globalApplication;
|
||||
this.mAppConfigUtils = App.getAppConfigUtils(mGlobalApplication);
|
||||
LogUtils.d(TAG, "构造完成:AppConfigUtils=" + mAppConfigUtils);
|
||||
}
|
||||
|
||||
// ================================== 广播核心接收逻辑(入口方法,过滤电池状态广播)=================================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
|
||||
// 先设置好新电池状态标志
|
||||
boolean isCharging = BatteryUtils.isCharging(intent);
|
||||
if (_mIsCharging != isCharging) {
|
||||
mAppConfigUtils.setIsCharging(isCharging);
|
||||
LogUtils.d(TAG, "onReceive: 接收广播 | context=" + context + " | intent=" + intent + " | action=" + (intent != null ? intent.getAction() : "null"));
|
||||
|
||||
// 基础参数校验
|
||||
if (context == null || intent == null || intent.getAction() == null) {
|
||||
LogUtils.e(TAG, "onReceive: 参数无效,终止处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅处理电池状态变化广播
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
|
||||
handleBatteryStateChanged(context, intent);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onReceive: 广播处理完成");
|
||||
}
|
||||
|
||||
// ================================== 业务逻辑方法(处理电池状态变化,同步配置+通知页面)=================================
|
||||
/**
|
||||
* 处理电池状态变化广播
|
||||
* @param context 上下文
|
||||
* @param intent 电池状态广播意图
|
||||
*/
|
||||
private void handleBatteryStateChanged(Context context, Intent intent) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 解析电池状态 | intent=" + intent);
|
||||
// 1. 解析当前电池状态(复用工具类,二次校验电量范围)
|
||||
boolean currentIsCharging = BatteryUtils.isCharging(intent);
|
||||
int currentBatteryLevel = BatteryUtils.getCurrentBatteryLevel(intent);
|
||||
currentBatteryLevel = Math.min(Math.max(currentBatteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 当前状态 | 充电=" + currentIsCharging + " | 电量=" + currentBatteryLevel + "%");
|
||||
|
||||
// 2. 状态无变化则跳过,减少无效运算
|
||||
if (currentIsCharging == sLastIsCharging && currentBatteryLevel == sLastBatteryLevel) {
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 状态无变化,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 同步最新状态到配置工具类
|
||||
if (mAppConfigUtils != null) {
|
||||
if (currentIsCharging != sLastIsCharging) {
|
||||
mAppConfigUtils.setCharging(currentIsCharging);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 同步充电状态 | " + currentIsCharging);
|
||||
}
|
||||
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
|
||||
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
mAppConfigUtils.setCurrentValue(nTheQuantityOfElectricity);
|
||||
}
|
||||
// 新电池状态标志某一个有变化就更新显示信息
|
||||
if (_mIsCharging != isCharging || _mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {
|
||||
// 电池状态改变先取消旧的提醒消息
|
||||
//NotificationHelper.cancelRemindNotification(context);
|
||||
|
||||
App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity);
|
||||
MainActivity.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
|
||||
// 保存好新的电池状态标志
|
||||
_mIsCharging = isCharging;
|
||||
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;
|
||||
if (currentBatteryLevel != sLastBatteryLevel) {
|
||||
mAppConfigUtils.setCurrentBatteryValue(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 同步电量 | " + currentBatteryLevel + "%");
|
||||
}
|
||||
} else {
|
||||
LogUtils.e(TAG, "handleBatteryStateChanged: AppConfigUtils为空,同步失败");
|
||||
}
|
||||
|
||||
// 4. 执行状态变化后的业务逻辑
|
||||
// 记录电量变化时间
|
||||
if (App.getAppCacheUtils(context) != null) {
|
||||
App.getAppCacheUtils(context).addChangingTime(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 记录电量变化时间");
|
||||
}
|
||||
// 通知MainActivity更新电量
|
||||
MainActivity.sendCurrentBatteryValueMessage(currentBatteryLevel);
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 发送电量更新消息到MainActivity");
|
||||
|
||||
// 5. 更新历史状态缓存
|
||||
sLastIsCharging = currentIsCharging;
|
||||
sLastBatteryLevel = currentBatteryLevel;
|
||||
LogUtils.d(TAG, "handleBatteryStateChanged: 更新历史状态完成");
|
||||
}
|
||||
|
||||
// ================================== 广播注册/注销(强化容错,避免重复操作)=================================
|
||||
/**
|
||||
* 注册广播接收器
|
||||
*/
|
||||
public void registerAction() {
|
||||
LogUtils.d(TAG, "registerAction: 注册广播");
|
||||
if (mGlobalApplication == null || mCurrentReceiver == null) {
|
||||
LogUtils.e(TAG, "注册失败:App或Receiver实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先注销再注册,避免重复注册异常
|
||||
unregisterAction();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
mGlobalApplication.registerReceiver(mCurrentReceiver, filter);
|
||||
LogUtils.d(TAG, "registerAction: 广播注册成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "registerAction: 注册失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Receiver
|
||||
//
|
||||
public void registerAction() {
|
||||
IntentFilter filter=new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
mGlobalApplication.registerReceiver(mReceiver, filter);
|
||||
/**
|
||||
* 注销广播接收器
|
||||
*/
|
||||
public void unregisterAction() {
|
||||
LogUtils.d(TAG, "unregisterAction: 注销广播");
|
||||
if (mGlobalApplication == null || mCurrentReceiver == null) {
|
||||
LogUtils.e(TAG, "注销失败:App或Receiver实例为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mGlobalApplication.unregisterReceiver(mCurrentReceiver);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播未注册,跳过注销");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "unregisterAction: 注销失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 资源释放方法(主动释放,彻底避免内存泄漏)=================================
|
||||
/**
|
||||
* 释放接收器资源,供App销毁时调用
|
||||
*/
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release: 释放接收器资源");
|
||||
// 注销广播
|
||||
unregisterAction();
|
||||
// 置空引用,帮助GC回收
|
||||
mGlobalApplication = null;
|
||||
mAppConfigUtils = null;
|
||||
mCurrentReceiver = null;
|
||||
// 重置静态状态缓存
|
||||
sLastBatteryLevel = -1;
|
||||
sLastIsCharging = false;
|
||||
LogUtils.d(TAG, "release: 资源释放完成");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class MainReceiver extends BroadcastReceiver {
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String szAction = intent.getAction();
|
||||
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
|
||||
boolean isEnableService = App.getAppConfigUtils(context).getIsEnableService();
|
||||
boolean isEnableService = App.getAppConfigUtils(context).isServiceEnabled();
|
||||
if (isEnableService) {
|
||||
if (ServiceUtils.isServiceAlive(context.getApplicationContext(), ControlCenterService.class.getName()) == false) {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain() Wakeup... ControlCenterService");
|
||||
|
||||
@@ -5,101 +5,177 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
|
||||
/**
|
||||
* 电池提醒核心服务进程守护类
|
||||
* 功能:监听主服务 {@link ControlCenterService} 存活状态,异常断开时自动重启并绑定
|
||||
* 适配:Java7 | API30 | 前台服务启动规则 | 服务绑定稳定性保障
|
||||
*/
|
||||
public class AssistantService extends Service {
|
||||
private final static String TAG = "AssistantService";
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
private static final String TAG = "AssistantService";
|
||||
// 服务返回策略常量(统一定义,避免魔法值)
|
||||
private static final int SERVICE_RETURN_STICKY = START_STICKY;
|
||||
// 服务绑定标记常量
|
||||
private static final int BIND_FLAG = Context.BIND_IMPORTANT;
|
||||
|
||||
//MyBinder mMyBinder;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
volatile boolean mIsThreadAlive;
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
// ================================== 成员变量区(按功能分层,volatile保证多线程可见性)=================================
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private volatile boolean mIsThreadAlive;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
//return mMyBinder;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
//LogUtils.d(TAG, "onCreate");
|
||||
super.onCreate();
|
||||
mAppConfigUtils = App.getAppConfigUtils(this);
|
||||
|
||||
//mMyBinder = new MyBinder();
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
// 设置运行参数
|
||||
mIsThreadAlive = false;
|
||||
run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
//LogUtils.d(TAG, "call onStartCommand(...)");
|
||||
run();
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/*class MyBinder extends IMyAidlInterface.Stub {
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return AssistantService.class.getSimpleName();
|
||||
}
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
mIsThreadAlive = false;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void run() {
|
||||
//LogUtils.d(TAG, "run");
|
||||
if (mAppConfigUtils.getIsEnableService()) {
|
||||
if (mIsThreadAlive == false) {
|
||||
// 设置运行状态
|
||||
mIsThreadAlive = true;
|
||||
// 唤醒和绑定主进程
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
if (ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName()) == false) {
|
||||
//LogUtils.d(TAG, "wakeupAndBindMain() Wakeup... ControlCenterService");
|
||||
startForegroundService(new Intent(AssistantService.this, ControlCenterService.class));
|
||||
}
|
||||
//LogUtils.d(TAG, "wakeupAndBindMain() Bind... ControlCenterService");
|
||||
bindService(new Intent(AssistantService.this, ControlCenterService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
class MyServiceConnection implements ServiceConnection {
|
||||
// ================================== 内部类(服务连接状态监听,前置定义便于引用)=================================
|
||||
/**
|
||||
* 服务连接状态监听器
|
||||
* 主服务连接成功时记录状态,断开时自动重连
|
||||
*/
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
//LogUtils.d(TAG, "call onServiceConnected(...)");
|
||||
LogUtils.d(TAG, "onServiceConnected: 主服务连接成功 | 组件名=" + name.getClassName() + " | Binder=" + service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
//LogUtils.d(TAG, "call onServiceDisconnected(...)");
|
||||
if (mAppConfigUtils.getIsEnableService()) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected: 主服务连接断开 | 组件名=" + name.getClassName());
|
||||
// 主服务断开且配置启用时,重新唤醒绑定
|
||||
if (mAppConfigUtils != null && mAppConfigUtils.isServiceEnabled()) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected: 配置启用,尝试重新唤醒并绑定主服务");
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 服务生命周期方法(按执行顺序排列:onCreate→onStartCommand→onBind→onDestroy)=================================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate: 守护服务启动 | 进程ID=" + android.os.Process.myPid());
|
||||
|
||||
// 初始化配置工具类,添加空指针防护
|
||||
mAppConfigUtils = App.getAppConfigUtils(this);
|
||||
if (mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "onCreate: AppConfigUtils初始化失败,守护服务无法工作");
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化服务连接对象
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
LogUtils.d(TAG, "onCreate: ServiceConnection初始化完成");
|
||||
}
|
||||
|
||||
// 初始化运行状态,执行核心守护逻辑
|
||||
mIsThreadAlive = false;
|
||||
run();
|
||||
LogUtils.d(TAG, "onCreate: 守护服务初始化完成 | 服务启用状态=" + mAppConfigUtils.isServiceEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 守护服务触发重启 | flags=" + flags + " | startId=" + startId);
|
||||
// 配置工具类为空时,直接返回非粘性策略
|
||||
if (mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "onStartCommand: AppConfigUtils未初始化,终止服务");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
run();
|
||||
int returnFlag = mAppConfigUtils.isServiceEnabled() ? SERVICE_RETURN_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand: 处理完成 | 返回策略=" + (returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT"));
|
||||
return returnFlag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务绑定请求 | intent=" + intent);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁流程启动");
|
||||
|
||||
// 重置运行状态,终止守护逻辑
|
||||
mIsThreadAlive = false;
|
||||
|
||||
// 解绑主服务,添加异常捕获防止重复解绑崩溃
|
||||
unbindMainService();
|
||||
|
||||
// 置空工具类引用,帮助GC回收
|
||||
mAppConfigUtils = null;
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁完成");
|
||||
}
|
||||
|
||||
// ================================== 核心业务逻辑(守护主服务存活)=================================
|
||||
/**
|
||||
* 执行守护逻辑:检查主服务状态,按需唤醒并绑定
|
||||
* 前置条件:mAppConfigUtils 必须初始化完成
|
||||
*/
|
||||
private void run() {
|
||||
LogUtils.d(TAG, "run: 执行守护逻辑 | 配置启用=" + mAppConfigUtils.isServiceEnabled() + " | 线程存活=" + mIsThreadAlive);
|
||||
if (mAppConfigUtils.isServiceEnabled()) {
|
||||
if (!mIsThreadAlive) {
|
||||
mIsThreadAlive = true;
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "run: 服务未启用,跳过守护逻辑");
|
||||
// 服务未启用时,重置线程状态
|
||||
mIsThreadAlive = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒主服务并建立绑定,确保主服务持续运行
|
||||
* 适配 API26+ 前台服务启动规则,避免系统限制导致启动失败
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
// 检查主服务存活状态
|
||||
boolean isMainServiceAlive = ServiceUtils.isServiceAlive(getApplicationContext(), ControlCenterService.class.getName());
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 主服务存活状态=" + isMainServiceAlive);
|
||||
|
||||
// 主服务未存活时,按需启动(区分API版本)
|
||||
if (!isMainServiceAlive) {
|
||||
Intent mainServiceIntent = new Intent(AssistantService.this, ControlCenterService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(mainServiceIntent);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: API26+ 以前台服务方式启动主服务");
|
||||
} else {
|
||||
startService(mainServiceIntent);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 以普通服务方式启动主服务");
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定主服务,监听连接状态,添加结果日志
|
||||
Intent bindIntent = new Intent(AssistantService.this, ControlCenterService.class);
|
||||
boolean bindResult = bindService(bindIntent, mMyServiceConnection, BIND_FLAG);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 绑定主服务结果=" + bindResult + " | 绑定标记=BIND_IMPORTANT");
|
||||
}
|
||||
|
||||
// ================================== 辅助工具方法(拆分独立逻辑,提高可维护性)=================================
|
||||
/**
|
||||
* 解绑主服务,包含异常捕获与状态日志
|
||||
*/
|
||||
private void unbindMainService() {
|
||||
if (mMyServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mMyServiceConnection);
|
||||
LogUtils.d(TAG, "unbindMainService: 已成功解绑ControlCenterService");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unbindMainService: 解绑服务失败,服务未绑定 | " + e.getMessage());
|
||||
}
|
||||
mMyServiceConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,314 +1,495 @@
|
||||
package cc.winboll.studio.powerbell.services;
|
||||
|
||||
/*
|
||||
* PowerBy : ZhanGSKen(ZhangShaojian2018@163.com)
|
||||
* 参考:
|
||||
* 进程保活-双进程守护的正确姿势
|
||||
* https://blog.csdn.net/sinat_35159441/article/details/75267380
|
||||
* Android Service之onStartCommand方法研究
|
||||
* https://blog.csdn.net/cyp331203/article/details/38920491
|
||||
*/
|
||||
import android.app.Notification;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Handler;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.widget.RemoteViews;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.model.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.model.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.services.AssistantService;
|
||||
import cc.winboll.studio.powerbell.threads.RemindThread;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationHelper;
|
||||
import cc.winboll.studio.powerbell.utils.ServiceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 电池提醒核心服务
|
||||
* 功能:管理前台服务生命周期、控制提醒线程启停、处理配置更新
|
||||
* 适配:Java7 | API30 | 前台服务超时防护 | 电池优化忽略引导
|
||||
*/
|
||||
public class ControlCenterService extends Service {
|
||||
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "ControlCenterService";
|
||||
private static final long THREAD_STOP_TIMEOUT = 1000L;
|
||||
private static final int SERVICE_RETURN_STICKY = START_STICKY;
|
||||
private static final int DEFAULT_CHARGE_REMINDER_VALUE = 80;
|
||||
private static final int DEFAULT_USAGE_REMINDER_VALUE = 20;
|
||||
private static final int DEFAULT_BATTERY_DETECT_INTERVAL = 1000;
|
||||
private static final int RUNNING_SERVICE_LIST_LIMIT = 100;
|
||||
|
||||
public static final int MSG_UPDATE_STATUS = 0;
|
||||
// ================================== 静态状态标记(volatile保证多线程可见性)=================================
|
||||
private static volatile boolean isServiceRunning = false;
|
||||
private static volatile boolean mIsDestroyed = true;
|
||||
|
||||
static ControlCenterService _mControlCenterService;
|
||||
|
||||
volatile boolean isServiceRunning;
|
||||
|
||||
AppConfigUtils mAppConfigUtils;
|
||||
AppCacheUtils mAppCacheUtils;
|
||||
// 前台服务通知工具
|
||||
NotificationHelper mNotificationHelper;
|
||||
Notification notification;
|
||||
RemindThread mRemindThread;
|
||||
ControlCenterServiceHandler mControlCenterServiceHandler;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
ControlCenterServiceReceiver mControlCenterServiceReceiver;
|
||||
ControlCenterServiceReceiver mControlCenterServiceReceiverLocalBroadcast;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public RemindThread getRemindThread() {
|
||||
return mRemindThread;
|
||||
}
|
||||
// ================================== 成员变量区(按功能分层:配置→核心组件→通知相关)=================================
|
||||
// 服务控制配置
|
||||
private ControlCenterServiceBean mServiceControlBean;
|
||||
private AppConfigBean mCurrentConfigBean;
|
||||
// 业务核心组件
|
||||
private ControlCenterServiceHandler mServiceHandler;
|
||||
private ControlCenterServiceReceiver mControlCenterServiceReceiver;
|
||||
// 通知相关
|
||||
private NotificationManagerUtils mNotificationManager;
|
||||
private NotificationMessage mForegroundNotifyMsg;
|
||||
|
||||
// ================================== 服务生命周期方法(按执行顺序:onCreate→onStartCommand→onBind→onDestroy)=================================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_mControlCenterService = ControlCenterService.this;
|
||||
isServiceRunning = false;
|
||||
mAppConfigUtils = App.getAppConfigUtils(this);
|
||||
mAppCacheUtils = App.getAppCacheUtils(this);
|
||||
mNotificationHelper = new NotificationHelper(ControlCenterService.this);
|
||||
|
||||
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
mControlCenterServiceHandler = new ControlCenterServiceHandler(this);
|
||||
|
||||
// 运行服务内容
|
||||
run();
|
||||
LogUtils.d(TAG, "onCreate执行 | 线程=" + Thread.currentThread().getName() + " | 进程ID=" + android.os.Process.myPid());
|
||||
runCoreServiceLogic();
|
||||
LogUtils.d(TAG, "onCreate完成 | 前台状态=" + isServiceRunning + " | 服务启用=" + (mServiceControlBean != null && mServiceControlBean.isEnableService()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// 运行服务内容
|
||||
run();
|
||||
return (mAppConfigUtils.getIsEnableService()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand执行 | startId=" + startId + " | action=" + (intent != null ? intent.getAction() : "null"));
|
||||
loadLatestServiceControlConfig();
|
||||
runCoreServiceLogic();
|
||||
|
||||
int returnFlag = (mServiceControlBean != null && mServiceControlBean.isEnableService())
|
||||
? SERVICE_RETURN_STICKY
|
||||
: super.onStartCommand(intent, flags, startId);
|
||||
LogUtils.d(TAG, "onStartCommand完成 | 返回策略=" + (returnFlag == SERVICE_RETURN_STICKY ? "START_STICKY" : "DEFAULT"));
|
||||
return returnFlag;
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void run() {
|
||||
if (mAppConfigUtils.getIsEnableService() && isServiceRunning == false) {
|
||||
LogUtils.d(TAG, "run");
|
||||
isServiceRunning = true;
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
// 显示前台通知栏
|
||||
// 在Service中
|
||||
NotificationHelper helper = new NotificationHelper(this);
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
notification = helper.showForegroundNotification(intent, getString(R.string.app_name), "Service Running, Click to open app");
|
||||
startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
|
||||
|
||||
// NotificationMessage notificationMessage=createNotificationMessage();
|
||||
// //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show();
|
||||
// mNotificationUtils.createForegroundNotification(this, notificationMessage);
|
||||
// mNotificationUtils.createRemindNotification(this, notificationMessage);
|
||||
|
||||
if (mControlCenterServiceReceiver == null) {
|
||||
// 注册广播接收器
|
||||
mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this);
|
||||
mControlCenterServiceReceiver.registerAction(this);
|
||||
}
|
||||
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable(){
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
startRemindThread(mAppConfigUtils.mAppConfigBean);
|
||||
ToastUtils.show("Service Is Start.");
|
||||
LogUtils.i(TAG, "Service Is Start.");
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
String getValuesString() {
|
||||
String szReturn = "Usege: ";
|
||||
szReturn += mAppConfigUtils.getIsEnableUsegeReminder() ? Integer.toString(mAppConfigUtils.getUsegeReminderValue()) : "?";
|
||||
szReturn += "% Charge: ";
|
||||
szReturn += mAppConfigUtils.getIsEnableChargeReminder() ? Integer.toString(mAppConfigUtils.getChargeReminderValue()) : "?";
|
||||
szReturn += "%\nCurrent: " + Integer.toString(mAppConfigUtils.getCurrentValue()) + "%";
|
||||
return szReturn;
|
||||
}
|
||||
|
||||
NotificationMessage createNotificationMessage() {
|
||||
String szTitle = ((App)getApplication()).getString(R.string.app_name);
|
||||
String szContent = getValuesString() + " {?} " + StringUtils.formatPCMListString(mAppCacheUtils.getArrayListBatteryInfo());
|
||||
return new NotificationMessage(szTitle, szContent);
|
||||
}
|
||||
|
||||
// 更新前台通知
|
||||
//
|
||||
public void updateServiceNotification() {
|
||||
//mNotificationUtils.updateForegroundNotification(ControlCenterService.this, createNotificationMessage());
|
||||
}
|
||||
|
||||
// 更新前台通知
|
||||
//
|
||||
public void updateServiceNotification(NotificationMessage notificationMessage) {
|
||||
//mNotificationUtils.updateForegroundNotification(ControlCenterService.this, notificationMessage);
|
||||
}
|
||||
|
||||
// 更新前台通知
|
||||
//
|
||||
public void updateRemindNotification(NotificationMessage notificationMessage) {
|
||||
//mNotificationUtils.updateRemindNotification(ControlCenterService.this, notificationMessage);
|
||||
}
|
||||
|
||||
// 唤醒和绑定守护进程
|
||||
//
|
||||
void wakeupAndBindAssistant() {
|
||||
if (ServiceUtils.isServiceAlive(getApplicationContext(), AssistantService.class.getName()) == false) {
|
||||
startService(new Intent(ControlCenterService.this, AssistantService.class));
|
||||
//LogUtils.d(TAG, "call wakeupAndBindAssistant() : Binding... AssistantService");
|
||||
bindService(new Intent(ControlCenterService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
}
|
||||
|
||||
// 开启提醒铃声线程
|
||||
//
|
||||
public void startRemindThread(AppConfigBean appConfigBean) {
|
||||
//LogUtils.d(TAG, "startRemindThread");
|
||||
if (mRemindThread == null) {
|
||||
mRemindThread = new RemindThread(this, mControlCenterServiceHandler);
|
||||
} else {
|
||||
if (mRemindThread.isExist() == true) {
|
||||
mRemindThread = new RemindThread(this, mControlCenterServiceHandler);
|
||||
} else {
|
||||
// 提醒进程正在进行中就更新状态后退出
|
||||
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
|
||||
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue());
|
||||
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
|
||||
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder());
|
||||
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
|
||||
mRemindThread.setIsCharging(appConfigBean.isCharging());
|
||||
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
|
||||
//LogUtils.d(TAG, "mRemindThread update.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
|
||||
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue());
|
||||
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
|
||||
mRemindThread.setIsCharging(appConfigBean.isCharging());
|
||||
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
|
||||
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
|
||||
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder());
|
||||
mRemindThread.start();
|
||||
//LogUtils.d(TAG, "mRemindThread.start()");
|
||||
}
|
||||
|
||||
public void stopRemindThread() {
|
||||
if (mRemindThread != null) {
|
||||
mRemindThread.setIsExist(true);
|
||||
mRemindThread = null;
|
||||
}
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind执行 | intent=" + intent);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
mAppConfigUtils.loadAppConfigBean();
|
||||
if (mAppConfigUtils.getIsEnableService() == false) {
|
||||
// 设置运行状态
|
||||
isServiceRunning = false;
|
||||
// 停止守护进程
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
stopService(intent);
|
||||
// 停止Receiver
|
||||
if (mControlCenterServiceReceiver != null) {
|
||||
unregisterReceiver(mControlCenterServiceReceiver);
|
||||
mControlCenterServiceReceiver = null;
|
||||
LogUtils.d(TAG, "onDestroy执行:服务销毁流程启动");
|
||||
super.onDestroy();
|
||||
|
||||
// 资源释放顺序:前台服务 → 线程 → 广播接收器 → Handler → 通知 → 引用(避免内存泄漏)
|
||||
stopForegroundService();
|
||||
RemindThread.stopRemindThread();
|
||||
releaseBroadcastReceiver();
|
||||
destroyHandler();
|
||||
releaseNotificationResource();
|
||||
clearAllReferences();
|
||||
|
||||
// 状态重置
|
||||
mCurrentConfigBean = null;
|
||||
mForegroundNotifyMsg = null;
|
||||
mServiceHandler = null;
|
||||
isServiceRunning = false;
|
||||
mIsDestroyed = true;
|
||||
|
||||
LogUtils.d(TAG, "onDestroy完成:服务销毁完成");
|
||||
}
|
||||
|
||||
// ================================== 核心业务逻辑(独立抽取,统一调用)=================================
|
||||
/**
|
||||
* 服务核心运行逻辑,在onCreate/onStartCommand复用
|
||||
* 避免重复初始化,保证前台服务优先启动
|
||||
*/
|
||||
private synchronized void runCoreServiceLogic() {
|
||||
LogUtils.d(TAG, "runCoreServiceLogic执行");
|
||||
loadLatestServiceControlConfig();
|
||||
|
||||
boolean serviceEnabled = mServiceControlBean != null && mServiceControlBean.isEnableService();
|
||||
LogUtils.d(TAG, "runCoreServiceLogic:服务启用=" + serviceEnabled + " | 已运行=" + isServiceRunning + " | 已销毁=" + mIsDestroyed);
|
||||
|
||||
if (serviceEnabled && !isServiceRunning) {
|
||||
isServiceRunning = true;
|
||||
mIsDestroyed = false;
|
||||
|
||||
if (initForegroundNotificationImmediately()) {
|
||||
loadDefaultConfig();
|
||||
initServiceBusinessLogic();
|
||||
LogUtils.d(TAG, "runCoreServiceLogic:核心组件初始化成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "runCoreServiceLogic:前台通知初始化失败,终止业务");
|
||||
stopForegroundService();
|
||||
isServiceRunning = false;
|
||||
}
|
||||
// 停止前台通知栏
|
||||
} else {
|
||||
LogUtils.d(TAG, "runCoreServiceLogic:无需执行核心逻辑");
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 前台通知管理(优先执行,防止API26+前台服务5秒超时)=================================
|
||||
/**
|
||||
* 立即初始化前台通知,防止API26+前台服务超时异常
|
||||
* @return true=成功 false=失败
|
||||
*/
|
||||
private boolean initForegroundNotificationImmediately() {
|
||||
LogUtils.d(TAG, "initForegroundNotificationImmediately执行");
|
||||
try {
|
||||
if (mNotificationManager == null) {
|
||||
mNotificationManager = new NotificationManagerUtils(this);
|
||||
LogUtils.d(TAG, "initForegroundNotificationImmediately:通知工具类初始化完成");
|
||||
}
|
||||
|
||||
if (mForegroundNotifyMsg == null) {
|
||||
mForegroundNotifyMsg = new NotificationMessage();
|
||||
mForegroundNotifyMsg.setTitle("电池监测服务");
|
||||
mForegroundNotifyMsg.setContent("后台运行中");
|
||||
mForegroundNotifyMsg.setRemindMSG("service_running");
|
||||
LogUtils.d(TAG, "initForegroundNotificationImmediately:通知消息构建完成");
|
||||
}
|
||||
|
||||
mNotificationManager.startForegroundServiceNotify(this, mForegroundNotifyMsg);
|
||||
ToastUtils.show("电池监测服务已启动");
|
||||
LogUtils.d(TAG, "initForegroundNotificationImmediately:前台通知发送成功 | ID=" + NotificationManagerUtils.NOTIFY_ID_FOREGROUND_SERVICE);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "initForegroundNotificationImmediately:通知初始化异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止前台服务并取消通知
|
||||
*/
|
||||
private void stopForegroundService() {
|
||||
LogUtils.d(TAG, "stopForegroundService执行");
|
||||
try {
|
||||
stopForeground(true);
|
||||
// 停止消息提醒进程
|
||||
stopRemindThread();
|
||||
super.onDestroy();
|
||||
//LogUtils.d(TAG, "onDestroy done");
|
||||
LogUtils.d(TAG, "stopForegroundService:前台服务已停止,通知已取消");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "stopForegroundService:停止异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
//LogUtils.d(TAG, "call onServiceConnected(...)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
//LogUtils.d(TAG, "call onServiceConnected(...)");
|
||||
if (mAppConfigUtils.getIsEnableService()) {
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
}
|
||||
// ================================== 配置管理(本地持久化+内存同步)=================================
|
||||
/**
|
||||
* 加载本地最新服务控制配置
|
||||
*/
|
||||
private void loadLatestServiceControlConfig() {
|
||||
LogUtils.d(TAG, "loadLatestServiceControlConfig执行");
|
||||
ControlCenterServiceBean latestBean = ControlCenterServiceBean.loadBean(this, ControlCenterServiceBean.class);
|
||||
if (latestBean != null) {
|
||||
mServiceControlBean = latestBean;
|
||||
LogUtils.d(TAG, "loadLatestServiceControlConfig:配置读取成功 | 启用=" + mServiceControlBean.isEnableService());
|
||||
} else {
|
||||
LogUtils.w(TAG, "loadLatestServiceControlConfig:本地无配置,沿用内存配置");
|
||||
}
|
||||
}
|
||||
|
||||
public void appenRemindMSG(String szRemindMSG) {
|
||||
String msg = "";
|
||||
for (int i = 0; i < 20; i++) {
|
||||
msg += szRemindMSG;
|
||||
/**
|
||||
* 加载默认业务配置(首次启动兜底)
|
||||
*/
|
||||
private void loadDefaultConfig() {
|
||||
LogUtils.d(TAG, "loadDefaultConfig执行");
|
||||
if (mCurrentConfigBean == null) {
|
||||
mCurrentConfigBean = new AppConfigBean();
|
||||
mCurrentConfigBean.setEnableChargeReminder(true);
|
||||
mCurrentConfigBean.setChargeReminderValue(DEFAULT_CHARGE_REMINDER_VALUE);
|
||||
mCurrentConfigBean.setEnableUsageReminder(true);
|
||||
mCurrentConfigBean.setUsageReminderValue(DEFAULT_USAGE_REMINDER_VALUE);
|
||||
mCurrentConfigBean.setBatteryDetectInterval(DEFAULT_BATTERY_DETECT_INTERVAL);
|
||||
LogUtils.d(TAG, "loadDefaultConfig:默认配置加载完成 | 充电阈值=" + DEFAULT_CHARGE_REMINDER_VALUE + " | 耗电阈值=" + DEFAULT_USAGE_REMINDER_VALUE + " | 检测间隔=" + DEFAULT_BATTERY_DETECT_INTERVAL + "ms");
|
||||
} else {
|
||||
LogUtils.d(TAG, "loadDefaultConfig:内存已有配置,无需加载");
|
||||
}
|
||||
NotificationHelper helper = new NotificationHelper(ControlCenterService.this);
|
||||
Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
|
||||
helper.showTemporaryNotification(intent, getString(R.string.app_name), msg);
|
||||
|
||||
|
||||
|
||||
// NotificationMessage notificationMessage = createNotificationMessage();
|
||||
// notificationMessage.setRemindMSG(szRemindMSG);
|
||||
// //LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
|
||||
// updateRemindNotification(notificationMessage);
|
||||
}
|
||||
|
||||
// 设置颜色背景
|
||||
public static RemoteViews setLinearLayoutColor(RemoteViews remoteViews, int viewId, int color) {
|
||||
remoteViews.setInt(viewId, "setBackgroundColor", color);
|
||||
return remoteViews;
|
||||
// ================================== 业务组件初始化与销毁(Handler/广播/线程等)=================================
|
||||
/**
|
||||
* 初始化Handler等核心业务组件
|
||||
*/
|
||||
private void initServiceBusinessLogic() {
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic执行");
|
||||
// 初始化Handler
|
||||
if (mServiceHandler == null) {
|
||||
mServiceHandler = new ControlCenterServiceHandler(this);
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:Handler初始化完成");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:Handler已存在");
|
||||
}
|
||||
// 初始化广播接收器
|
||||
if (mControlCenterServiceReceiver == null) {
|
||||
mControlCenterServiceReceiver = new ControlCenterServiceReceiver(this);
|
||||
mControlCenterServiceReceiver.registerAction(this);
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:广播接收器初始化并注册完成 | 接收器=" + mControlCenterServiceReceiver);
|
||||
} else {
|
||||
LogUtils.d(TAG, "initServiceBusinessLogic:广播接收器已存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置Drawable背景
|
||||
public static RemoteViews setLinearLayoutDrawable(RemoteViews remoteViews, int viewId, int drawableRes) {
|
||||
remoteViews.setInt(viewId, "setBackgroundResource", drawableRes);
|
||||
return remoteViews;
|
||||
/**
|
||||
* 释放广播接收器资源
|
||||
*/
|
||||
private void releaseBroadcastReceiver() {
|
||||
LogUtils.d(TAG, "releaseBroadcastReceiver执行");
|
||||
if (mControlCenterServiceReceiver != null) {
|
||||
mControlCenterServiceReceiver.release();
|
||||
mControlCenterServiceReceiver = null;
|
||||
LogUtils.d(TAG, "releaseBroadcastReceiver:广播接收器已释放");
|
||||
} else {
|
||||
LogUtils.w(TAG, "releaseBroadcastReceiver:广播接收器实例为空");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 启动服务
|
||||
//
|
||||
/**
|
||||
* 销毁Handler,移除所有消息和回调,防止内存泄漏
|
||||
*/
|
||||
private void destroyHandler() {
|
||||
LogUtils.d(TAG, "destroyHandler执行");
|
||||
if (mServiceHandler != null) {
|
||||
mServiceHandler.removeCallbacksAndMessages(null);
|
||||
mServiceHandler = null;
|
||||
LogUtils.d(TAG, "destroyHandler:Handler已销毁");
|
||||
} else {
|
||||
LogUtils.w(TAG, "destroyHandler:Handler实例为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放通知工具类资源
|
||||
*/
|
||||
private void releaseNotificationResource() {
|
||||
LogUtils.d(TAG, "releaseNotificationResource执行");
|
||||
if (mNotificationManager != null) {
|
||||
mNotificationManager.release();
|
||||
mNotificationManager = null;
|
||||
LogUtils.d(TAG, "releaseNotificationResource:通知资源已释放");
|
||||
} else {
|
||||
LogUtils.w(TAG, "releaseNotificationResource:通知工具类实例为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 置空所有引用,防止内存泄漏
|
||||
*/
|
||||
private void clearAllReferences() {
|
||||
LogUtils.d(TAG, "clearAllReferences执行");
|
||||
mForegroundNotifyMsg = null;
|
||||
mServiceControlBean = null;
|
||||
LogUtils.d(TAG, "clearAllReferences:引用清理完成");
|
||||
}
|
||||
|
||||
// ================================== 外部调用接口(静态方法,提供服务启停/配置更新入口)=================================
|
||||
/**
|
||||
* 外部启动服务的统一入口
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void startControlCenterService(Context context) {
|
||||
LogUtils.d(TAG, "startControlCenterService执行 | context=" + context);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startControlCenterService:Context为空,启动失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存启用配置
|
||||
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(true);
|
||||
ControlCenterServiceBean.saveBean(context, controlBean);
|
||||
LogUtils.d(TAG, "startControlCenterService:服务启用配置已保存 | 配置=" + controlBean);
|
||||
|
||||
// 启动服务(区分API版本)
|
||||
Intent intent = new Intent(context, ControlCenterService.class);
|
||||
context.startForegroundService(intent);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent);
|
||||
LogUtils.d(TAG, "startControlCenterService:以前台服务方式启动(API26+)");
|
||||
} else {
|
||||
context.startService(intent);
|
||||
LogUtils.d(TAG, "startControlCenterService:以普通服务方式启动(API26-)");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 停止服务
|
||||
//
|
||||
/**
|
||||
* 外部停止服务的统一入口
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void stopControlCenterService(Context context) {
|
||||
LogUtils.d(TAG, "stopControlCenterService执行 | context=" + context);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopControlCenterService:Context为空,停止失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存停用配置
|
||||
ControlCenterServiceBean controlBean = new ControlCenterServiceBean(false);
|
||||
ControlCenterServiceBean.saveBean(context, controlBean);
|
||||
LogUtils.d(TAG, "stopControlCenterService:服务停用配置已保存 | 配置=" + controlBean);
|
||||
|
||||
// 停止服务
|
||||
Intent intent = new Intent(context, ControlCenterService.class);
|
||||
context.stopService(intent);
|
||||
LogUtils.d(TAG, "stopControlCenterService:停止指令已发送");
|
||||
}
|
||||
|
||||
public static void updateStatus(Context context, AppConfigBean appConfigBean) {
|
||||
//LogUtils.d(TAG, "updateStatus");
|
||||
// 创建一个Intent实例,定义广播的内容
|
||||
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_START_REMINDTHREAD);
|
||||
// 设置可选的Action数据,如额外信息
|
||||
intent.putExtra("appConfigBean", appConfigBean);
|
||||
// 发送广播
|
||||
context.sendBroadcast(intent);
|
||||
/**
|
||||
* 外部更新配置并触发线程重启
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void sendAppConfigStatusUpdateMessage(Context context) {
|
||||
LogUtils.d(TAG, "sendAppConfigStatusUpdateMessage执行 | context=" + context);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "sendAppConfigStatusUpdateMessage:参数为空,更新失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
|
||||
intent.setPackage(context.getPackageName());
|
||||
// 新增:发送广播并记录结果
|
||||
context.sendBroadcast(intent);
|
||||
LogUtils.d(TAG, "sendAppConfigStatusUpdateMessage:配置更新广播发送 :action=" + ControlCenterServiceReceiver.ACTION_APPCONFIG_CHANGED);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查并引导用户开启忽略电池优化(API23+)
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void checkIgnoreBatteryOptimization(Context context) {
|
||||
LogUtils.d(TAG, "checkIgnoreBatteryOptimization执行 | context=" + context);
|
||||
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.w(TAG, "checkIgnoreBatteryOptimization:无需检查(Context为空或API<23)");
|
||||
return;
|
||||
}
|
||||
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
if (powerManager == null) {
|
||||
LogUtils.e(TAG, "checkIgnoreBatteryOptimization:PowerManager获取失败");
|
||||
return;
|
||||
}
|
||||
|
||||
String packageName = context.getPackageName();
|
||||
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(packageName);
|
||||
LogUtils.d(TAG, "checkIgnoreBatteryOptimization:已忽略电池优化=" + isIgnored);
|
||||
|
||||
if (!isIgnored) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + packageName));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
context.startActivity(intent);
|
||||
LogUtils.d(TAG, "checkIgnoreBatteryOptimization:已跳转至系统设置页 | package=" + packageName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务是否运行(适配API30+)
|
||||
* @param context 上下文
|
||||
* @param serviceClass 服务类
|
||||
* @return true=运行中 false=未运行
|
||||
*/
|
||||
private static boolean isServiceRunning(Context context, Class<?> serviceClass) {
|
||||
LogUtils.d(TAG, "isServiceRunning执行 | context=" + context + " | service=" + (serviceClass != null ? serviceClass.getName() : "null"));
|
||||
if (context == null || serviceClass == null) {
|
||||
LogUtils.e(TAG, "isServiceRunning:参数为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (am == null) {
|
||||
LogUtils.e(TAG, "isServiceRunning:ActivityManager获取失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isRunning = false;
|
||||
String packageName = context.getPackageName();
|
||||
String serviceClassName = serviceClass.getName();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// API30+ 禁止获取其他应用服务,通过进程状态判断
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
|
||||
if (processes != null) {
|
||||
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
||||
if (packageName.equals(process.processName) &&
|
||||
(process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE ||
|
||||
process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND)) {
|
||||
isRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning:API30+ 判断结果=" + isRunning);
|
||||
} else {
|
||||
// API30- 通过服务列表判断
|
||||
List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(RUNNING_SERVICE_LIST_LIMIT);
|
||||
if (services != null) {
|
||||
for (ActivityManager.RunningServiceInfo info : services) {
|
||||
if (serviceClassName.equals(info.service.getClassName())) {
|
||||
isRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "isServiceRunning:API30- 判断结果=" + isRunning);
|
||||
}
|
||||
|
||||
// 兜底判断:配置启用状态
|
||||
if (!isRunning) {
|
||||
isRunning = isServiceStarted(context, serviceClass);
|
||||
LogUtils.d(TAG, "isServiceRunning:兜底判断结果=" + isRunning);
|
||||
}
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底判断服务是否已启动(通过配置文件)
|
||||
*/
|
||||
private static boolean isServiceStarted(Context context, Class<?> serviceClass) {
|
||||
LogUtils.d(TAG, "isServiceStarted执行");
|
||||
try {
|
||||
ControlCenterServiceBean controlBean = ControlCenterServiceBean.loadBean(context, ControlCenterServiceBean.class);
|
||||
return controlBean != null && controlBean.isEnableService();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "isServiceStarted:兜底判断异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 业务方法(配置更新/电池状态回调)=================================
|
||||
/**
|
||||
* 接收外部配置更新,同步到提醒线程
|
||||
* @param latestConfig 最新配置
|
||||
*/
|
||||
public void notifyAppConfigUpdate(AppConfigBean latestConfig) {
|
||||
LogUtils.d(TAG, "notifyAppConfigUpdate执行 | 充电阈值=" + (latestConfig != null ? latestConfig.getChargeReminderValue() : null) + " | 耗电阈值=" + (latestConfig != null ? latestConfig.getUsageReminderValue() : null));
|
||||
if (latestConfig != null && mServiceHandler != null) {
|
||||
mCurrentConfigBean = latestConfig;
|
||||
RemindThread.startRemindThreadWithAppConfig(this, mServiceHandler, latestConfig);
|
||||
LogUtils.d(TAG, "notifyAppConfigUpdate:配置已同步到提醒线程");
|
||||
} else {
|
||||
LogUtils.e(TAG, "notifyAppConfigUpdate:参数为空,同步失败 | latestConfig=" + latestConfig + " | mServiceHandler=" + mServiceHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== Getter 方法(按需开放,避免冗余Setter)=================================
|
||||
public ControlCenterServiceBean getServiceControlBean() {
|
||||
return mServiceControlBean;
|
||||
}
|
||||
|
||||
public NotificationManagerUtils getNotificationManager() {
|
||||
return mNotificationManager;
|
||||
}
|
||||
|
||||
public NotificationMessage getForegroundNotifyMsg() {
|
||||
return mForegroundNotifyMsg;
|
||||
}
|
||||
|
||||
public AppConfigBean getCurrentConfigBean() {
|
||||
return mCurrentConfigBean;
|
||||
}
|
||||
|
||||
public boolean isDestroyed() {
|
||||
return mIsDestroyed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,36 +4,368 @@ import android.content.Context;
|
||||
import android.os.Message;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 提醒线程(多实例列表管理)
|
||||
* 功能:管理充电/耗电提醒逻辑,触发条件时向Handler发送提醒消息
|
||||
* 适配:Java7 | API30 | 内存泄漏防护 | 多线程状态同步
|
||||
* 对外接口:{@link #startRemindThreadWithAppConfig(Context, ControlCenterServiceHandler, AppConfigBean)}、{
|
||||
* @link #startRemindThreadWithBatteryInfo(Context, ControlCenterServiceHandler, boolean, int)}、{@link #stopRemindThread()}
|
||||
*/
|
||||
public class RemindThread extends Thread {
|
||||
|
||||
public static final String TAG = RemindThread.class.getSimpleName();
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "RemindThread";
|
||||
|
||||
Context mContext;
|
||||
|
||||
// 控制线程是否退出的标志
|
||||
volatile boolean isExist = false;
|
||||
// 消息提醒开关
|
||||
static volatile boolean isReminding = false;
|
||||
// 充电提醒开关
|
||||
static volatile boolean isEnableUsegeReminder = false;
|
||||
// 耗电提醒开关
|
||||
static volatile boolean isEnableChargeReminder = false;
|
||||
// 电量比较停顿时间
|
||||
static volatile int sleepTime = 1000;
|
||||
// 充电提醒电量
|
||||
static volatile int chargeReminderValue = -1;
|
||||
// 耗电提醒电量
|
||||
static volatile int usegeReminderValue = -1;
|
||||
// 当前电量
|
||||
static volatile int quantityOfElectricity = -1;
|
||||
// 是否正在充电
|
||||
static volatile boolean isCharging = false;
|
||||
// 服务Handler, 用于线程发送消息使用
|
||||
WeakReference<ControlCenterServiceHandler> mwrControlCenterServiceHandler;
|
||||
// 时间常量 (ms)
|
||||
private static final int MIN_SLEEP_TIME = 2000;
|
||||
private static final long THREAD_JOIN_TIMEOUT = 1000L;
|
||||
|
||||
// 状态常量
|
||||
private static final int INVALID_BATTERY_VALUE = -1;
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// 提醒类型常量
|
||||
private static final String REMIND_TYPE_CHARGE = "+";
|
||||
private static final String REMIND_TYPE_USAGE = "-";
|
||||
|
||||
// ================================== 静态成员(多实例列表管理)=================================
|
||||
private static volatile ArrayList<RemindThread> sRemindThreadList;
|
||||
|
||||
// ================================== 成员变量区(按功能分层,volatile保证多线程可见性)=================================
|
||||
// 并发安全锁(保护线程状态变更)
|
||||
private final Object mRemindLock = new Object();
|
||||
|
||||
// 弱引用依赖(防内存泄漏,ApplicationContext 避免 Activity 引用)
|
||||
private Context mContext;
|
||||
private WeakReference<ControlCenterServiceHandler> mwrControlCenterServiceHandler;
|
||||
|
||||
// 线程状态标记(volatile 确保多线程可见)
|
||||
private volatile boolean isReminding;
|
||||
public volatile boolean isExist;
|
||||
|
||||
// 业务配置参数(volatile 确保配置变更实时生效)
|
||||
private volatile boolean isEnableChargeReminder;
|
||||
private volatile boolean isEnableUsageReminder;
|
||||
private volatile long sleepTime;
|
||||
private volatile int chargeReminderValue;
|
||||
private volatile int usageReminderValue;
|
||||
private volatile int quantityOfElectricity;
|
||||
private volatile boolean isCharging;
|
||||
|
||||
// ================================== 私有构造器(禁止外部实例化)=================================
|
||||
private RemindThread(Context context, ControlCenterServiceHandler handler) {
|
||||
LogUtils.d(TAG, "构造器调用 | context=" + context + " | handler=" + handler);
|
||||
this.mContext = context.getApplicationContext();
|
||||
this.mwrControlCenterServiceHandler = new WeakReference<>(handler);
|
||||
resetThreadStateInternal();
|
||||
LogUtils.d(TAG, "构造完成 | threadId=" + getId() + " | 初始状态重置成功");
|
||||
}
|
||||
|
||||
// ================================== 对外公开静态接口(多实例列表管理)=================================
|
||||
/**
|
||||
* 启动提醒线程,同步最新配置
|
||||
* 逻辑:停止所有旧线程 → 创建新线程 → 加入列表管理
|
||||
* @param context 上下文(非空)
|
||||
* @param handler 服务处理器(非空)
|
||||
* @param config 应用配置Bean(非空)
|
||||
* @return true: 启动成功;false: 入参非法
|
||||
*/
|
||||
public static boolean startRemindThreadWithAppConfig(Context context, ControlCenterServiceHandler handler, AppConfigBean config) {
|
||||
LogUtils.d(TAG, "startRemindThreadWithAppConfig调用 | context=" + context + " | handler=" + handler + " | config=" + config);
|
||||
|
||||
// 入参严格校验
|
||||
if (context == null || handler == null || config == null) {
|
||||
LogUtils.e(TAG, "启动失败:入参为空 | context=" + context + " | handler=" + handler + " | config=" + config);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化线程列表
|
||||
if (sRemindThreadList == null) {
|
||||
synchronized (RemindThread.class) {
|
||||
if (sRemindThreadList == null) {
|
||||
sRemindThreadList = new ArrayList<RemindThread>();
|
||||
LogUtils.d(TAG, "线程列表初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止所有旧线程
|
||||
stopAllOldThreadsInternal();
|
||||
|
||||
// 创建并启动新线程
|
||||
RemindThread newRemindThread = new RemindThread(context, handler);
|
||||
newRemindThread.setAppConfigBean(config);
|
||||
newRemindThread.isExist = false;
|
||||
newRemindThread.start();
|
||||
sRemindThreadList.add(newRemindThread);
|
||||
LogUtils.d(TAG, "新线程启动成功 | threadId=" + newRemindThread.getId() + " | 列表大小=" + sRemindThreadList.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动提醒线程,同步电池状态信息
|
||||
* 逻辑:停止所有旧线程 → 创建新线程 → 同步电池状态 → 加入列表管理
|
||||
* @param context 上下文(非空)
|
||||
* @param handler 服务处理器(非空)
|
||||
* @param isCharging 充电状态
|
||||
* @param batteryLevel 当前电量
|
||||
* @return true: 启动成功;false: 入参非法
|
||||
*/
|
||||
public static boolean startRemindThreadWithBatteryInfo(Context context, ControlCenterServiceHandler handler, boolean isCharging, int batteryLevel) {
|
||||
LogUtils.d(TAG, "startRemindThreadWithBatteryInfo调用 | context=" + context + " | handler=" + handler + " | isCharging=" + isCharging + " | batteryLevel=" + batteryLevel);
|
||||
|
||||
// 入参严格校验
|
||||
if (context == null || handler == null) {
|
||||
LogUtils.e(TAG, "启动失败:入参为空 | context=" + context + " | handler=" + handler);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化线程列表
|
||||
if (sRemindThreadList == null) {
|
||||
synchronized (RemindThread.class) {
|
||||
if (sRemindThreadList == null) {
|
||||
sRemindThreadList = new ArrayList<RemindThread>();
|
||||
LogUtils.d(TAG, "线程列表初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止所有旧线程
|
||||
stopAllOldThreadsInternal();
|
||||
|
||||
// 创建并启动新线程
|
||||
RemindThread newRemindThread = new RemindThread(context, handler);
|
||||
// 同步电池状态
|
||||
newRemindThread.isCharging = isCharging;
|
||||
newRemindThread.quantityOfElectricity = Math.min(Math.max(batteryLevel, BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
newRemindThread.isExist = false;
|
||||
newRemindThread.start();
|
||||
sRemindThreadList.add(newRemindThread);
|
||||
LogUtils.d(TAG, "新线程启动成功 | threadId=" + newRemindThread.getId() + " | 电池状态同步完成");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全停止所有线程,清空列表
|
||||
*/
|
||||
public static void stopRemindThread() {
|
||||
LogUtils.d(TAG, "stopRemindThread调用 | 列表存在=" + (sRemindThreadList != null) + " | 列表大小=" + (sRemindThreadList != null ? sRemindThreadList.size() : 0));
|
||||
if (sRemindThreadList == null || sRemindThreadList.isEmpty()) {
|
||||
LogUtils.w(TAG, "停止失败:线程列表为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记所有线程退出
|
||||
for (RemindThread remindThread : sRemindThreadList) {
|
||||
remindThread.isExist = true;
|
||||
LogUtils.d(TAG, "标记线程退出 | threadId=" + remindThread.getId());
|
||||
}
|
||||
// 清空列表
|
||||
sRemindThreadList.clear();
|
||||
LogUtils.d(TAG, "所有线程已标记退出,列表已清空");
|
||||
}
|
||||
|
||||
// ================================== 私有静态辅助方法(多实例管理)=================================
|
||||
/**
|
||||
* 停止所有旧线程并清空列表
|
||||
*/
|
||||
private static void stopAllOldThreadsInternal() {
|
||||
if (sRemindThreadList == null || sRemindThreadList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记所有旧线程退出
|
||||
for (RemindThread remindThread : sRemindThreadList) {
|
||||
remindThread.isExist = true;
|
||||
LogUtils.d(TAG, "标记旧线程退出 | threadId=" + remindThread.getId());
|
||||
}
|
||||
// 清空旧线程列表
|
||||
sRemindThreadList.clear();
|
||||
LogUtils.d(TAG, "旧线程已全部标记退出,列表已清空");
|
||||
}
|
||||
|
||||
// ================================== 线程核心运行逻辑=================================
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "run执行 | threadId=" + getId() + " | 状态=" + getState());
|
||||
|
||||
// 初始化提醒状态(加锁保护,避免多线程竞争)
|
||||
synchronized (mRemindLock) {
|
||||
if (isReminding) {
|
||||
LogUtils.w(TAG, "线程已在提醒状态,退出运行 | threadId=" + getId());
|
||||
return;
|
||||
}
|
||||
isReminding = true;
|
||||
}
|
||||
|
||||
// 核心电量检测循环
|
||||
LogUtils.d(TAG, "进入电量检测循环 | 休眠时间=" + sleepTime + "ms | threadId=" + getId());
|
||||
while (!isExist) {
|
||||
try {
|
||||
// 快速退出判断
|
||||
if (isExist) break;
|
||||
|
||||
// 电量有效性校验(非0-100视为无效),退出电量提醒线程
|
||||
if (quantityOfElectricity < BATTERY_LEVEL_MIN || quantityOfElectricity > BATTERY_LEVEL_MAX) {
|
||||
LogUtils.w(TAG, "电量无效,退出电量提醒线程 | 当前电量=" + quantityOfElectricity + " | threadId=" + getId());
|
||||
break;
|
||||
}
|
||||
|
||||
// 充电/耗电提醒触发逻辑
|
||||
if (isCharging && isEnableChargeReminder && quantityOfElectricity >= chargeReminderValue) {
|
||||
LogUtils.d(TAG, "触发充电提醒 | 当前电量=" + quantityOfElectricity + " ≥ 阈值=" + chargeReminderValue + " | threadId=" + getId());
|
||||
sendNotificationMessageInternal(REMIND_TYPE_CHARGE, quantityOfElectricity, isCharging);
|
||||
} else if (!isCharging && isEnableUsageReminder && quantityOfElectricity <= usageReminderValue) {
|
||||
LogUtils.d(TAG, "触发耗电提醒 | 当前电量=" + quantityOfElectricity + " ≤ 阈值=" + usageReminderValue + " | threadId=" + getId());
|
||||
sendNotificationMessageInternal(REMIND_TYPE_USAGE, quantityOfElectricity, isCharging);
|
||||
} else {
|
||||
// 未有合适类型提醒,退出提醒线程
|
||||
LogUtils.d(TAG, "未有合适类型提醒,退出提醒线程");
|
||||
break;
|
||||
}
|
||||
// 安全休眠,保留中断标记
|
||||
safeSleepInternal(sleepTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "循环运行异常,退出电量提醒线程 | 当前电量=" + quantityOfElectricity + " | threadId=" + getId(), e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 循环退出,清理状态
|
||||
cleanThreadStateInternal();
|
||||
LogUtils.d(TAG, "run结束 | threadId=" + getId());
|
||||
}
|
||||
|
||||
// ================================== 内部业务辅助方法=================================
|
||||
/**
|
||||
* 发送提醒消息到Handler(弱引用避免内存泄漏)
|
||||
* @param type 提醒类型:+充电/-耗电
|
||||
* @param battery 当前电量
|
||||
* @param isCharging 充电状态
|
||||
*/
|
||||
private void sendNotificationMessageInternal(String type, int battery, boolean isCharging) {
|
||||
LogUtils.d(TAG, "sendNotificationMessageInternal调用 | 类型=" + type + " | 电量=" + battery + " | isCharging=" + isCharging + " | threadId=" + getId());
|
||||
// 前置状态校验
|
||||
if (isExist || !isReminding) {
|
||||
LogUtils.d(TAG, "消息发送跳过:线程已退出或提醒关闭 | threadId=" + getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取弱引用的Handler
|
||||
ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get();
|
||||
if (handler == null) {
|
||||
LogUtils.w(TAG, "消息发送失败:Handler已被回收 | threadId=" + getId());
|
||||
return;
|
||||
}
|
||||
|
||||
Message message = Message.obtain(handler, ControlCenterServiceHandler.MSG_REMIND_TEXT);
|
||||
message.obj = type;
|
||||
message.arg1 = battery;
|
||||
message.arg2 = isCharging ? 1 : 0;
|
||||
|
||||
try {
|
||||
handler.sendMessage(message);
|
||||
LogUtils.d(TAG, "提醒消息发送成功 | 类型=" + type + " | 电量=" + battery + " | threadId=" + getId());
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "消息发送异常 | threadId=" + getId(), e);
|
||||
// 异常时回收Message,避免内存泄漏
|
||||
if (message != null) {
|
||||
message.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全休眠,响应线程中断
|
||||
* @param millis 休眠时长(ms)
|
||||
*/
|
||||
private void safeSleepInternal(long millis) {
|
||||
LogUtils.d(TAG, "safeSleepInternal调用 | 休眠时长=" + millis + "ms | threadId=" + getId());
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LogUtils.w(TAG, "休眠被中断,线程准备退出 | threadId=" + getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置线程初始状态(构造器专用)
|
||||
*/
|
||||
private void resetThreadStateInternal() {
|
||||
LogUtils.d(TAG, "resetThreadStateInternal调用 | threadId=" + getId());
|
||||
// 状态标记初始化
|
||||
isExist = false;
|
||||
isReminding = false;
|
||||
// 配置参数初始化
|
||||
isEnableChargeReminder = false;
|
||||
isEnableUsageReminder = false;
|
||||
sleepTime = MIN_SLEEP_TIME;
|
||||
chargeReminderValue = -1;
|
||||
usageReminderValue = -1;
|
||||
quantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
isCharging = false;
|
||||
LogUtils.d(TAG, "线程初始状态重置完成 | threadId=" + getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理线程运行状态(循环退出时调用)
|
||||
*/
|
||||
private void cleanThreadStateInternal() {
|
||||
LogUtils.d(TAG, "cleanThreadStateInternal调用 | threadId=" + getId());
|
||||
isReminding = false;
|
||||
isExist = true;
|
||||
quantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
// 中断当前线程(如果存活)
|
||||
if (isAlive()) {
|
||||
interrupt();
|
||||
}
|
||||
LogUtils.d(TAG, "线程运行状态清理完成 | threadId=" + getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步应用配置,校验参数有效性
|
||||
* @param config 应用配置Bean
|
||||
*/
|
||||
public void setAppConfigBean(AppConfigBean config) {
|
||||
LogUtils.d(TAG, "setAppConfigBean调用 | config=" + config + " | threadId=" + getId());
|
||||
if (config == null) {
|
||||
LogUtils.e(TAG, "配置同步失败:配置Bean为空 | threadId=" + getId());
|
||||
quantityOfElectricity = INVALID_BATTERY_VALUE;
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置参数同步 + 范围校验(确保参数合法)
|
||||
isEnableChargeReminder = config.isEnableChargeReminder();
|
||||
isEnableUsageReminder = config.isEnableUsageReminder();
|
||||
chargeReminderValue = Math.min(Math.max(config.getChargeReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
usageReminderValue = Math.min(Math.max(config.getUsageReminderValue(), BATTERY_LEVEL_MIN), BATTERY_LEVEL_MAX);
|
||||
sleepTime = Math.max(config.getBatteryDetectInterval(), MIN_SLEEP_TIME);
|
||||
quantityOfElectricity = (config.getCurrentBatteryValue() >= BATTERY_LEVEL_MIN && config.getCurrentBatteryValue() <= BATTERY_LEVEL_MAX)
|
||||
? config.getCurrentBatteryValue() : INVALID_BATTERY_VALUE;
|
||||
isCharging = config.isCharging();
|
||||
|
||||
LogUtils.d(TAG, "配置同步完成 | 休眠时间=" + sleepTime + "ms | 提醒开启=" + isReminding + " | 当前电量=" + quantityOfElectricity + " | 充电阈值=" + chargeReminderValue + " | 耗电阈值=" + usageReminderValue + " | threadId=" + getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断线程是否处于运行状态
|
||||
* @return true: 运行中;false: 已停止
|
||||
*/
|
||||
private boolean isRunning() {
|
||||
boolean running = !isExist && isAlive();
|
||||
LogUtils.d(TAG, "isRunning调用 | 运行中=" + running + " | 退出标记=" + isExist + " | 存活=" + isAlive() + " | threadId=" + getId());
|
||||
return running;
|
||||
}
|
||||
|
||||
// ================================== Getter/Setter(按需开放)=================================
|
||||
public void setIsExist(boolean isExist) {
|
||||
LogUtils.d(TAG, "setIsExist调用 | isExist=" + isExist + " | threadId=" + getId());
|
||||
this.isExist = isExist;
|
||||
}
|
||||
|
||||
@@ -41,157 +373,20 @@ public class RemindThread extends Thread {
|
||||
return isExist;
|
||||
}
|
||||
|
||||
public static void setIsReminding(boolean isReminding) {
|
||||
RemindThread.isReminding = isReminding;
|
||||
}
|
||||
|
||||
public static boolean isReminding() {
|
||||
return isReminding;
|
||||
}
|
||||
|
||||
public static void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
|
||||
RemindThread.isEnableUsegeReminder = isEnableUsegeReminder;
|
||||
}
|
||||
|
||||
public static boolean isEnableUsegeReminder() {
|
||||
return isEnableUsegeReminder;
|
||||
}
|
||||
|
||||
public static void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
RemindThread.isEnableChargeReminder = isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public static boolean isEnableChargeReminder() {
|
||||
return isEnableChargeReminder;
|
||||
}
|
||||
|
||||
public static void setSleepTime(int sleepTime) {
|
||||
RemindThread.sleepTime = sleepTime;
|
||||
}
|
||||
|
||||
public static int getSleepTime() {
|
||||
return sleepTime;
|
||||
}
|
||||
|
||||
public static void setChargeReminderValue(int chargeReminderValue) {
|
||||
RemindThread.chargeReminderValue = chargeReminderValue;
|
||||
}
|
||||
|
||||
public static int getChargeReminderValue() {
|
||||
return chargeReminderValue;
|
||||
}
|
||||
|
||||
public static void setUsegeReminderValue(int usegeReminderValue) {
|
||||
RemindThread.usegeReminderValue = usegeReminderValue;
|
||||
}
|
||||
|
||||
public static int getUsegeReminderValue() {
|
||||
return usegeReminderValue;
|
||||
}
|
||||
|
||||
public static void setQuantityOfElectricity(int quantityOfElectricity) {
|
||||
RemindThread.quantityOfElectricity = quantityOfElectricity;
|
||||
}
|
||||
|
||||
public static int getQuantityOfElectricity() {
|
||||
return quantityOfElectricity;
|
||||
}
|
||||
|
||||
public static void setIsCharging(boolean isCharging) {
|
||||
RemindThread.isCharging = isCharging;
|
||||
}
|
||||
|
||||
public static boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
// 发送消息给用户
|
||||
//
|
||||
void sendNotificationMessage(String sz) {
|
||||
//LogUtils.d(TAG, "sz is " + sz);
|
||||
Message message = Message.obtain();
|
||||
message.what = ControlCenterServiceHandler.MSG_REMIND_TEXT;
|
||||
//message.obj = new NotificationMessage(mContext.getString(R.string.app_name), sz);
|
||||
message.obj = sz;
|
||||
ControlCenterServiceHandler handler = mwrControlCenterServiceHandler.get();
|
||||
if (isReminding && handler != null) {
|
||||
handler.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public RemindThread(Context context, ControlCenterServiceHandler handler) {
|
||||
mContext = context;
|
||||
mwrControlCenterServiceHandler = new WeakReference<ControlCenterServiceHandler>(handler);
|
||||
}
|
||||
|
||||
// ================================== 调试辅助方法=================================
|
||||
@Override
|
||||
public void run() {
|
||||
//LogUtils.d(TAG, "call run()");
|
||||
if (isReminding == false) {
|
||||
isReminding = true;
|
||||
|
||||
// 等待些许时间,等所有数据初始化完成再执行下面的程序
|
||||
// 解决窗口移除后自动重启后会发送一个错误消息的问题
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {}
|
||||
|
||||
// 发送提醒线程开始的参数设置
|
||||
//sendMessageToUser(Integer.toString(_mnTheQuantityOfElectricity) + ">>>" + Integer.toString(_mnTargetNumber));
|
||||
//ToastUtils.show("Service Is Start.");
|
||||
//LogUtils.i(TAG, "Service Is Start.");
|
||||
while (!isExist()) {
|
||||
|
||||
/*
|
||||
LogUtils.d(TAG, "isCharging is " + Boolean.toString(isCharging));
|
||||
LogUtils.d(TAG, "usegeReminderValue is " + Integer.toString(usegeReminderValue));
|
||||
LogUtils.d(TAG, "quantityOfElectricity is " + Integer.toString(quantityOfElectricity));
|
||||
LogUtils.d(TAG, "chargeReminderValue is " + Integer.toString(chargeReminderValue));
|
||||
LogUtils.d(TAG, "isEnableChargeReminder is " + Boolean.toString(isEnableChargeReminder));
|
||||
LogUtils.d(TAG, "isEnableUsegeReminder is " + Boolean.toString(isEnableUsegeReminder));
|
||||
*/
|
||||
|
||||
try {
|
||||
if (isCharging) {
|
||||
if ((quantityOfElectricity >= chargeReminderValue)
|
||||
&& (isEnableChargeReminder)) {
|
||||
// 正在充电时电量大于指定电量发送提醒
|
||||
sendNotificationMessage("+");
|
||||
// 应用需要继续提醒,设置退出标志为否
|
||||
setIsExist(false);
|
||||
//sendNotificationMessage("I am ready! +");
|
||||
} else {
|
||||
// 设置退出标志,如果后续不需要继续提醒就退出当前进程,用于应用节能。
|
||||
setIsExist(true);
|
||||
isReminding = false;
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
if ((quantityOfElectricity <= usegeReminderValue)
|
||||
&& (isEnableUsegeReminder)) {
|
||||
// 正在放电时电量小于指定电量发送提醒
|
||||
sendNotificationMessage("-");
|
||||
// 应用需要继续提醒,设置退出标志为否
|
||||
setIsExist(false);
|
||||
//sendNotificationMessage("I am ready! -");
|
||||
} else {
|
||||
// 设置退出标志,如果后续不需要继续提醒就退出当前进程,用于应用节能。
|
||||
setIsExist(true);
|
||||
isReminding = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
//ToastUtils.show("Service Is Stop.");
|
||||
//LogUtils.i(TAG, "Service Is Stop.");
|
||||
isReminding = false;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "RemindThread{" +
|
||||
"threadId=" + getId() +
|
||||
", threadName='" + getName() + '\'' +
|
||||
", isRunning=" + isRunning() +
|
||||
", isReminding=" + isReminding +
|
||||
", chargeThreshold=" + chargeReminderValue +
|
||||
", usageThreshold=" + usageReminderValue +
|
||||
", currentBattery=" + quantityOfElectricity +
|
||||
", isCharging=" + isCharging +
|
||||
", sleepTime=" + sleepTime + "ms" +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
package cc.winboll.studio.powerbell.unittest;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/22 08:31
|
||||
* @Describe MainUnitTest2Activity
|
||||
*/
|
||||
public class MainUnitTest2Activity extends AppCompatActivity {
|
||||
// ====================== 常量定义 ======================
|
||||
public static final String TAG = "MainUnitTest2Activity";
|
||||
public static final int REQUEST_CROP_IMAGE = 0;
|
||||
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
|
||||
|
||||
// ====================== 成员变量(移除所有Uri相关) ======================
|
||||
private MemoryCachedBackgroundView mMemoryCachedBackgroundView;
|
||||
private String mAppPrivateDirPath;
|
||||
private File mPrivateTestImageFile; // 仅用File,不用Uri
|
||||
private File mPrivateCropImageFile;
|
||||
BackgroundBean mPreviewBackgroundBean;
|
||||
LinearLayout mllBackgroundView;
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
|
||||
|
||||
initBaseParams();
|
||||
initViewAndEvent();
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
//loadBackgroundByFile(); // 直接用File加载
|
||||
mPreviewBackgroundBean = new BackgroundBean();
|
||||
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
doubleRefreshPreview();
|
||||
|
||||
ToastUtils.show("单元测试页面启动完成");
|
||||
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "=== onActivityResult 回调 ===");
|
||||
if (requestCode == REQUEST_CROP_IMAGE) {
|
||||
handleCropResult(resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 初始化相关方法 ======================
|
||||
private void initBaseParams() {
|
||||
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File");
|
||||
|
||||
// 私有目录(无需权限,无UID冲突)
|
||||
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
|
||||
File privateDir = new File(mAppPrivateDirPath);
|
||||
if (!privateDir.exists()) {
|
||||
privateDir.mkdirs();
|
||||
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
|
||||
}
|
||||
|
||||
// 初始化File(无Uri)
|
||||
File refFile = new File(ASSETS_TEST_IMAGE_PATH);
|
||||
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
|
||||
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
|
||||
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
|
||||
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
|
||||
|
||||
LogUtils.d(TAG, "测试图File路径:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private void initViewAndEvent() {
|
||||
LogUtils.d(TAG, "初始化布局与控件事件");
|
||||
setContentView(R.layout.activity_mainunittest2);
|
||||
mllBackgroundView = (LinearLayout) findViewById(R.id.ll_backgroundview);
|
||||
mMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, "", false);
|
||||
mllBackgroundView.addView(mMemoryCachedBackgroundView);
|
||||
|
||||
//mMemoryCachedBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
|
||||
|
||||
// 跳转主页面按钮
|
||||
Button btnMain = (Button) findViewById(R.id.btn_main_activity);
|
||||
btnMain.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:跳转主页面");
|
||||
startActivity(new Intent(MainUnitTest2Activity.this, MainActivity.class));
|
||||
}
|
||||
});
|
||||
|
||||
// 裁剪按钮(直接用File路径启动,无Uri)
|
||||
Button btnCrop = (Button) findViewById(R.id.btn_test_cropimage);
|
||||
btnCrop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:启动裁剪(File路径版)");
|
||||
ToastUtils.show("准备启动图片裁剪");
|
||||
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
startCropTestByFile(); // 直接传File
|
||||
} else {
|
||||
ToastUtils.show("测试图片未准备好,重新拷贝");
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 从assets拷贝图片(不变,确保File存在)
|
||||
private void copyAssetsTestImageToPrivateDir() {
|
||||
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
LogUtils.d(TAG, "图片已存在,无需拷贝");
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
|
||||
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
|
||||
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), e);
|
||||
ToastUtils.show("图片准备失败");
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "关闭流失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法(全改为File路径) ======================
|
||||
/** 直接用File路径加载背景图(无Uri,无冲突) */
|
||||
// private void loadBackgroundByFile() {
|
||||
// LogUtils.d(TAG, "开始加载背景图(File路径版)");
|
||||
// if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
// mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
|
||||
// LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
// ToastUtils.show("背景图加载成功");
|
||||
// } else {
|
||||
// LogUtils.e(TAG, "背景图加载失败:文件无效");
|
||||
// ToastUtils.show("背景图加载失败");
|
||||
// }
|
||||
// }
|
||||
|
||||
/** 直接用File启动裁剪(关键:调用ImageCropUtils的File重载方法) */
|
||||
private void startCropTestByFile() {
|
||||
LogUtils.d(TAG, "启动裁剪(File路径版),原图:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
|
||||
// 确保输出目录存在
|
||||
File cropParent = mPrivateCropImageFile.getParentFile();
|
||||
if (!cropParent.exists()) {
|
||||
cropParent.mkdirs();
|
||||
}
|
||||
|
||||
// 调用ImageCropUtils的File参数方法(核心:绕开Uri)
|
||||
ImageCropUtils.startImageCrop(
|
||||
this,
|
||||
mPrivateTestImageFile, // 原图File
|
||||
mPrivateCropImageFile, // 输出File
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
|
||||
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
ToastUtils.show("已启动图片裁剪");
|
||||
}
|
||||
|
||||
/** 处理裁剪结果(直接校验输出File) */
|
||||
private void handleCropResult(int resultCode) {
|
||||
LogUtils.d(TAG, "裁剪回调处理:resultCode=" + resultCode);
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
|
||||
mMemoryCachedBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
ToastUtils.show("裁剪成功");
|
||||
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
doubleRefreshPreview();
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪成功但输出文件无效");
|
||||
ToastUtils.show("裁剪失败:输出文件无效");
|
||||
}
|
||||
} else if (resultCode == RESULT_CANCELED) {
|
||||
LogUtils.d(TAG, "裁剪取消");
|
||||
ToastUtils.show("裁剪已取消");
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪失败:resultCode异常");
|
||||
ToastUtils.show("裁剪失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
* 移除:缓存清空逻辑
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
|
||||
// 第一重刷新
|
||||
try {
|
||||
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第一重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二重刷新(延迟执行)
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMemoryCachedBackgroundView != null && !isFinishing()) {
|
||||
try {
|
||||
mMemoryCachedBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mMemoryCachedBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +1,249 @@
|
||||
package cc.winboll.studio.powerbell.unittest;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import cc.winboll.studio.powerbell.utils.FileUtils;
|
||||
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/19 18:04
|
||||
* @Describe 单元测试启动主页窗口
|
||||
* 终极修复版:放弃FileProvider,直接用私有目录File路径,彻底解决UID冲突
|
||||
*/
|
||||
public class MainUnitTestActivity extends AppCompatActivity {
|
||||
|
||||
// ====================== 常量定义 ======================
|
||||
public static final String TAG = "MainUnitTestActivity";
|
||||
public static final int REQUEST_CROP_IMAGE = 0;
|
||||
// 新增:权限请求码
|
||||
public static final int REQUEST_STORAGE_PERMISSION = 1001;
|
||||
View mainView;
|
||||
BackgroundSourceUtils mBgSourceUtils;
|
||||
BackgroundView mBackgroundView;
|
||||
// 测试图片路径(用Environment获取,适配低版本,避免硬编码)
|
||||
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
|
||||
|
||||
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
|
||||
|
||||
// ====================== 成员变量(移除所有Uri相关) ======================
|
||||
private BackgroundView mBackgroundView;
|
||||
private String mAppPrivateDirPath;
|
||||
private File mPrivateTestImageFile; // 仅用File,不用Uri
|
||||
private File mPrivateCropImageFile;
|
||||
BackgroundBean mPreviewBackgroundBean;
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
|
||||
mBgSourceUtils.loadSettings();
|
||||
|
||||
setContentView(R.layout.activity_mainunittest);
|
||||
|
||||
mBackgroundView = findViewById(R.id.backgroundview);
|
||||
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
|
||||
|
||||
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
|
||||
}
|
||||
});
|
||||
|
||||
// 裁剪测试按钮点击事件(新增权限校验)
|
||||
((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();
|
||||
}
|
||||
}
|
||||
});
|
||||
initBaseParams();
|
||||
initViewAndEvent();
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
//loadBackgroundByFile(); // 直接用File加载
|
||||
mPreviewBackgroundBean = new BackgroundBean();
|
||||
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
|
||||
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
|
||||
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
doubleRefreshPreview();
|
||||
|
||||
ToastUtils.show(String.format("%s onCreate", TAG));
|
||||
// 加载测试图片(验证图片路径是否有效)
|
||||
loadBackground();
|
||||
ToastUtils.show("单元测试页面启动完成");
|
||||
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
|
||||
*/
|
||||
private void startCropTest() {
|
||||
// 修复2:输出路径用Environment获取,确保目录存在(避免路径无效)
|
||||
File outputDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/");
|
||||
if (!outputDir.exists()) {
|
||||
outputDir.mkdirs(); // 创建目录(避免输出路径不存在导致裁剪失败)
|
||||
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
|
||||
}
|
||||
String dstOutputPath = outputDir.getAbsolutePath()
|
||||
+ "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
|
||||
|
||||
// 修复3:自由裁剪时比例传0(避免100:100过大导致机型崩溃)
|
||||
ImageCropUtils.startImageCrop(
|
||||
MainUnitTestActivity.this,
|
||||
new File(szTestSource),
|
||||
new File(dstOutputPath),
|
||||
0, // 自由裁剪传0
|
||||
0, // 自由裁剪传0
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验存储读写权限(适配Android 6.0+ 低版本SDK,移除TIRAMISU依赖)
|
||||
*/
|
||||
private boolean checkStoragePermission() {
|
||||
// 适配Android 6.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;
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "=== onActivityResult 回调 ===");
|
||||
if (requestCode == REQUEST_CROP_IMAGE) {
|
||||
handleCropResult(resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 初始化相关方法 ======================
|
||||
private void initBaseParams() {
|
||||
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File");
|
||||
|
||||
// 私有目录(无需权限,无UID冲突)
|
||||
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
|
||||
File privateDir = new File(mAppPrivateDirPath);
|
||||
if (!privateDir.exists()) {
|
||||
privateDir.mkdirs();
|
||||
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
|
||||
}
|
||||
|
||||
// 初始化File(无Uri)
|
||||
File refFile = new File(ASSETS_TEST_IMAGE_PATH);
|
||||
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
|
||||
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
|
||||
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
|
||||
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
|
||||
|
||||
LogUtils.d(TAG, "测试图File路径:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private void initViewAndEvent() {
|
||||
LogUtils.d(TAG, "初始化布局与控件事件");
|
||||
setContentView(R.layout.activity_mainunittest);
|
||||
mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
|
||||
|
||||
// 跳转主页面按钮
|
||||
Button btnMain = (Button) findViewById(R.id.btn_main_activity);
|
||||
btnMain.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:跳转主页面");
|
||||
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
|
||||
}
|
||||
});
|
||||
|
||||
// 裁剪按钮(直接用File路径启动,无Uri)
|
||||
Button btnCrop = (Button) findViewById(R.id.btn_test_cropimage);
|
||||
btnCrop.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击按钮:启动裁剪(File路径版)");
|
||||
ToastUtils.show("准备启动图片裁剪");
|
||||
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
startCropTestByFile(); // 直接传File
|
||||
} else {
|
||||
ToastUtils.show("测试图片未准备好,重新拷贝");
|
||||
copyAssetsTestImageToPrivateDir();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 从assets拷贝图片(不变,确保File存在)
|
||||
private void copyAssetsTestImageToPrivateDir() {
|
||||
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
|
||||
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
LogUtils.d(TAG, "图片已存在,无需拷贝");
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
|
||||
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
|
||||
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), e);
|
||||
ToastUtils.show("图片准备失败");
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "关闭流失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (allGranted) {
|
||||
ToastUtils.show("存储权限已授予,启动裁剪");
|
||||
startCropTest(); // 权限授予后启动裁剪
|
||||
} else {
|
||||
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
|
||||
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
LogUtils.d(TAG, "【裁剪回调】requestCode:" + requestCode + ",resultCode:" + resultCode + ",data:" + (data == null ? "null" : data.toString()));
|
||||
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null));
|
||||
// 裁剪完成后回收权限
|
||||
if (requestCode == REQUEST_CROP_IMAGE) {
|
||||
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
|
||||
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
|
||||
//ImageCropUtils.releaseCropPermission(this, outputUri);
|
||||
mBackgroundView.loadImage(dstOutputPath);
|
||||
// ====================== 核心业务方法(全改为File路径) ======================
|
||||
/** 直接用File路径加载背景图(无Uri,无冲突) */
|
||||
// private void loadBackgroundByFile() {
|
||||
// LogUtils.d(TAG, "开始加载背景图(File路径版)");
|
||||
// if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
|
||||
// mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
|
||||
// LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
// ToastUtils.show("背景图加载成功");
|
||||
// } else {
|
||||
// LogUtils.e(TAG, "背景图加载失败:文件无效");
|
||||
// ToastUtils.show("背景图加载失败");
|
||||
// }
|
||||
// }
|
||||
|
||||
/** 直接用File启动裁剪(关键:调用ImageCropUtils的File重载方法) */
|
||||
private void startCropTestByFile() {
|
||||
LogUtils.d(TAG, "启动裁剪(File路径版),原图:" + mPrivateTestImageFile.getAbsolutePath());
|
||||
|
||||
// 确保输出目录存在
|
||||
File cropParent = mPrivateCropImageFile.getParentFile();
|
||||
if (!cropParent.exists()) {
|
||||
cropParent.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
void loadBackground() {
|
||||
// 校验测试图片是否存在(避免路径错误)
|
||||
File testFile = new File(szTestSource);
|
||||
if (testFile.exists() && testFile.length() > 100) {
|
||||
mBackgroundView.loadImage(szTestSource);
|
||||
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource);
|
||||
|
||||
// 调用ImageCropUtils的File参数方法(核心:绕开Uri)
|
||||
ImageCropUtils.startImageCrop(
|
||||
this,
|
||||
mPrivateTestImageFile, // 原图File
|
||||
mPrivateCropImageFile, // 输出File
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
|
||||
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
ToastUtils.show("已启动图片裁剪");
|
||||
}
|
||||
|
||||
/** 处理裁剪结果(直接校验输出File) */
|
||||
private void handleCropResult(int resultCode) {
|
||||
LogUtils.d(TAG, "裁剪回调处理:resultCode=" + resultCode);
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
|
||||
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
|
||||
ToastUtils.show("裁剪成功");
|
||||
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
|
||||
doubleRefreshPreview();
|
||||
} else {
|
||||
LogUtils.e(TAG, "裁剪成功但输出文件无效");
|
||||
ToastUtils.show("裁剪失败:输出文件无效");
|
||||
}
|
||||
} else if (resultCode == RESULT_CANCELED) {
|
||||
LogUtils.d(TAG, "裁剪取消");
|
||||
ToastUtils.show("裁剪已取消");
|
||||
} else {
|
||||
ToastUtils.show("测试图片不存在或无效");
|
||||
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource);
|
||||
LogUtils.e(TAG, "裁剪失败:resultCode异常");
|
||||
ToastUtils.show("裁剪失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 双重刷新预览,确保背景加载最新数据
|
||||
* 移除:缓存清空逻辑
|
||||
*/
|
||||
private void doubleRefreshPreview() {
|
||||
|
||||
// 第一重刷新
|
||||
try {
|
||||
mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
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()) {
|
||||
try {
|
||||
mBackgroundView.loadByBackgroundBean(mPreviewBackgroundBean, true);
|
||||
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.model.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AppCacheUtils {
|
||||
|
||||
@@ -5,199 +5,311 @@ import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.model.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.model.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
|
||||
import cc.winboll.studio.powerbell.models.AppConfigBean;
|
||||
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import java.io.File;
|
||||
|
||||
// 应用配置工具类
|
||||
//
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 13:59
|
||||
* @Describe 应用配置工具类:管理应用核心配置(服务开关、电池提醒阈值、背景设置等)
|
||||
* 适配:Java7 | API30 | 小米手机,单例模式,线程安全,配置持久化
|
||||
*/
|
||||
public class AppConfigUtils {
|
||||
// ======================== 静态常量(顶部统一管理,抽离魔法值)========================
|
||||
public static final String TAG = "AppConfigUtils";
|
||||
public static final String BACKGROUND_DIR = "Background"; // 背景图片存储目录
|
||||
private static final int MIN_REMINDER_VALUE = 0; // 提醒阈值最小值
|
||||
private static final int MAX_REMINDER_VALUE = 100; // 提醒阈值最大值
|
||||
private static final int MIN_INTERVAL_TIME = 1000; // 最小提醒间隔(ms)
|
||||
private static final int MIN_DETECT_INTERVAL = 500; // 最小电量检测间隔(ms)
|
||||
|
||||
public static final String BACKGROUND_DIR = "Background";
|
||||
// ======================== 静态成员(单例实例,严格控制初始化)========================
|
||||
private static AppConfigUtils sInstance; // 单例实例(私有,禁止外部直接创建)
|
||||
|
||||
// 保存唯一配置实例
|
||||
static AppConfigUtils _mAppConfigUtils;
|
||||
// 应用环境上下文
|
||||
Context mContext;
|
||||
// ======================== 核心依赖属性(优先排列,final保障安全)========================
|
||||
private final Context mContext; // 应用上下文(避免内存泄漏)
|
||||
private App mApplication; // 应用Application实例
|
||||
|
||||
// 是否启动铃声提醒服务
|
||||
volatile boolean mIsEnableService = false;
|
||||
// ======================== 配置Bean属性(持久化核心,volatile保障线程安全)========================
|
||||
public volatile AppConfigBean mAppConfigBean; // 应用配置Bean
|
||||
|
||||
public volatile AppConfigBean mAppConfigBean;
|
||||
|
||||
// 电池充电提醒值。
|
||||
// Battery charge reminder value.
|
||||
volatile int mnChargeReminderValue = -1;
|
||||
volatile boolean mIsEnableChargeReminder = false;
|
||||
// 电池耗电量提醒值。
|
||||
// Battery power usege reminder value.
|
||||
volatile int mnUsegeReminderValue = -1;
|
||||
volatile boolean mIsEnableUsegeReminder = false;
|
||||
// ======================== 缓存状态属性(减少Bean读取次数,提升性能)========================
|
||||
private volatile boolean mIsServiceEnabled = false; // 服务开关缓存状态
|
||||
|
||||
volatile boolean mIsUseBackgroundFile = false;
|
||||
volatile String mszBackgroundFileName = "";
|
||||
|
||||
// 保存应用实例
|
||||
App mApplication;
|
||||
|
||||
AppConfigUtils(Context context) {
|
||||
mContext = context;
|
||||
String szExternalFilesDir = mContext.getExternalFilesDir(TAG) + File.separator;
|
||||
//mlistAppConfigBean = new ArrayList<AppConfigBean>();
|
||||
// ======================== 单例构造方法(私有,禁止外部实例化)========================
|
||||
private AppConfigUtils(Context context) {
|
||||
LogUtils.d(TAG, "初始化配置工具类");
|
||||
this.mContext = context.getApplicationContext(); // 强制取应用上下文,杜绝内存泄漏
|
||||
this.mApplication = (App) context.getApplicationContext();
|
||||
// 初始化配置Bean
|
||||
mAppConfigBean = new AppConfigBean();
|
||||
loadAppConfigBean();
|
||||
// 加载持久化配置
|
||||
loadAppConfig();
|
||||
LogUtils.d(TAG, "配置工具类初始化完成");
|
||||
}
|
||||
|
||||
// 返回唯一实例
|
||||
//
|
||||
// ======================== 单例获取方法(双重校验锁,线程安全,适配多线程)========================
|
||||
public static AppConfigUtils getInstance(Context context) {
|
||||
if (_mAppConfigUtils == null) {
|
||||
_mAppConfigUtils = new AppConfigUtils(context);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: Context不能为空,获取实例失败");
|
||||
throw new IllegalArgumentException("Context cannot be null");
|
||||
}
|
||||
return _mAppConfigUtils;
|
||||
}
|
||||
|
||||
public void setIsEnableService(Activity activity, final boolean isEnableService) {
|
||||
YesNoAlertDialog.show(activity, "应用设置信息", "是否保存应用配置?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
@Override
|
||||
public void onYes() {
|
||||
mIsEnableService = isEnableService;
|
||||
ControlCenterServiceBean bean = new ControlCenterServiceBean(isEnableService);
|
||||
ControlCenterServiceBean.saveBean(mContext, bean);
|
||||
if (mIsEnableService) {
|
||||
LogUtils.d(TAG, "startControlCenterService");
|
||||
ControlCenterService.startControlCenterService(mContext);
|
||||
} else {
|
||||
LogUtils.d(TAG, "stopControlCenterService");
|
||||
ControlCenterService.stopControlCenterService(mContext);
|
||||
}
|
||||
if (sInstance == null) {
|
||||
synchronized (AppConfigUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new AppConfigUtils(context);
|
||||
LogUtils.d(TAG, "getInstance: 单例实例创建成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
MainActivity.relaodAppConfigs();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public boolean getIsEnableService() {
|
||||
ControlCenterServiceBean bean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
|
||||
if (bean == null) {
|
||||
|
||||
// ======================== 核心配置加载/保存方法(内部核心逻辑,优先排列)========================
|
||||
/**
|
||||
* 加载所有配置(应用配置+服务配置,统一入口,初始化/重载通用)
|
||||
*/
|
||||
public AppConfigBean loadAppConfig() {
|
||||
LogUtils.d(TAG, "loadAllConfig: 开始加载所有配置");
|
||||
// 加载应用配置
|
||||
AppConfigBean savedAppBean = (AppConfigBean) AppConfigBean.loadBean(mContext, AppConfigBean.class);
|
||||
if (savedAppBean != null) {
|
||||
mAppConfigBean = savedAppBean;
|
||||
LogUtils.d(TAG, "loadAllConfig: 应用配置加载成功");
|
||||
} else {
|
||||
mAppConfigBean = new AppConfigBean();
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
LogUtils.d(TAG, "loadAllConfig: 无已保存应用配置,使用默认值并持久化");
|
||||
}
|
||||
return mAppConfigBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存应用配置(内部核心方法,直接持久化,同步通知服务+Activity)
|
||||
*/
|
||||
private void saveAppConfig() {
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
LogUtils.d(TAG, "saveAppConfig: 应用配置保存成功,已同步服务和Activity");
|
||||
}
|
||||
|
||||
// ======================== 充电提醒配置方法(单独归类,逻辑聚焦)========================
|
||||
/**
|
||||
* 设置充电提醒开关状态(直接生效,无弹窗)
|
||||
* @param isEnabled 目标状态(true=开启,false=关闭)
|
||||
*/
|
||||
public void setChargeReminderEnabled(final boolean isEnabled) {
|
||||
if (isEnabled == mAppConfigBean.isEnableChargeReminder()) {
|
||||
LogUtils.d(TAG, "setChargeReminderEnabled: 充电提醒状态无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setEnableChargeReminder(isEnabled);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setChargeReminderEnabled: 充电提醒状态更新为=" + (isEnabled ? "开启" : "关闭"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充电提醒开关状态
|
||||
* @return 充电提醒状态(true=开启,false=关闭)
|
||||
*/
|
||||
public boolean isChargeReminderEnabled() {
|
||||
boolean isEnabled = mAppConfigBean.isEnableChargeReminder();
|
||||
LogUtils.d(TAG, "isChargeReminderEnabled: 获取充电提醒状态=" + (isEnabled ? "开启" : "关闭"));
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置充电提醒阈值(直接生效,无弹窗,自动校准范围,适配API30数据安全)
|
||||
* @param value 目标阈值(自动校准0-100)
|
||||
*/
|
||||
public void setChargeReminderValue(final int value) {
|
||||
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
|
||||
if (calibratedValue == mAppConfigBean.getChargeReminderValue()) {
|
||||
LogUtils.d(TAG, "setChargeReminderValue: 充电提醒阈值无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setChargeReminderValue(calibratedValue);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setChargeReminderValue: 充电提醒阈值更新为=" + calibratedValue + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充电提醒阈值
|
||||
* @return 充电提醒阈值(0-100)
|
||||
*/
|
||||
public int getChargeReminderValue() {
|
||||
int value = mAppConfigBean.getChargeReminderValue();
|
||||
LogUtils.d(TAG, "getChargeReminderValue: 获取充电提醒阈值=" + value + "%");
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// ======================== 耗电提醒配置方法(单独归类,逻辑聚焦)========================
|
||||
/**
|
||||
* 设置耗电提醒开关状态(直接生效,无弹窗)
|
||||
* @param isEnabled 目标状态(true=开启,false=关闭)
|
||||
*/
|
||||
public void setUsageReminderEnabled(final boolean isEnabled) {
|
||||
if (isEnabled == mAppConfigBean.isEnableUsageReminder()) {
|
||||
LogUtils.d(TAG, "setUsageReminderEnabled: 耗电提醒状态无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setEnableUsageReminder(isEnabled);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setUsageReminderEnabled: 耗电提醒状态更新为=" + (isEnabled ? "开启" : "关闭"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取耗电提醒开关状态
|
||||
* @return 耗电提醒状态(true=开启,false=关闭)
|
||||
*/
|
||||
public boolean isUsageReminderEnabled() {
|
||||
boolean isEnabled = mAppConfigBean.isEnableUsageReminder();
|
||||
LogUtils.d(TAG, "isUsageReminderEnabled: 获取耗电提醒状态=" + (isEnabled ? "开启" : "关闭"));
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置耗电提醒阈值(直接生效,无弹窗,自动校准范围,适配小米手机电量跳变)
|
||||
* @param value 目标阈值(自动校准0-100)
|
||||
*/
|
||||
public void setUsageReminderValue(final int value) {
|
||||
final int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
|
||||
if (calibratedValue == mAppConfigBean.getUsageReminderValue()) {
|
||||
LogUtils.d(TAG, "setUsageReminderValue: 耗电提醒阈值无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setUsageReminderValue(calibratedValue);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setUsageReminderValue: 耗电提醒阈值更新为=" + calibratedValue + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取耗电提醒阈值
|
||||
* @return 耗电提醒阈值(0-100)
|
||||
*/
|
||||
public int getUsageReminderValue() {
|
||||
int value = mAppConfigBean.getUsageReminderValue();
|
||||
LogUtils.d(TAG, "getUsageReminderValue: 获取耗电提醒阈值=" + value + "%");
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// ======================== 实时电池状态配置方法(临时缓存,不持久化,无需弹窗)========================
|
||||
/**
|
||||
* 设置当前充电状态(仅内存缓存,不持久化)
|
||||
* @param isCharging 充电状态(true=充电中,false=未充电)
|
||||
*/
|
||||
public void setCharging(boolean isCharging) {
|
||||
if (isCharging == mAppConfigBean.isCharging()) {
|
||||
LogUtils.d(TAG, "setCharging: 充电状态无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setIsCharging(isCharging);
|
||||
LogUtils.d(TAG, "setCharging: 充电状态更新为=" + (isCharging ? "充电中" : "未充电"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前充电状态
|
||||
* @return 充电状态(true=充电中,false=未充电)
|
||||
*/
|
||||
public boolean isCharging() {
|
||||
boolean isCharging = mAppConfigBean.isCharging();
|
||||
LogUtils.d(TAG, "isCharging: 获取充电状态=" + (isCharging ? "充电中" : "未充电"));
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前电池电量(仅内存缓存,不持久化,自动校准范围)
|
||||
* @param value 当前电量(自动校准0-100)
|
||||
*/
|
||||
public void setCurrentBatteryValue(int value) {
|
||||
int calibratedValue = Math.min(Math.max(value, MIN_REMINDER_VALUE), MAX_REMINDER_VALUE);
|
||||
if (calibratedValue == mAppConfigBean.getCurrentBatteryValue()) {
|
||||
LogUtils.d(TAG, "setCurrentBatteryValue: 电池电量无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setCurrentBatteryValue(calibratedValue);
|
||||
LogUtils.d(TAG, "setCurrentBatteryValue: 电池电量更新为=" + calibratedValue + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前电池电量
|
||||
* @return 当前电池电量(0-100)
|
||||
*/
|
||||
public int getCurrentBatteryValue() {
|
||||
int value = mAppConfigBean.getCurrentBatteryValue();
|
||||
LogUtils.d(TAG, "getCurrentBatteryValue: 获取电池电量=" + value + "%");
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// ======================== 间隔配置方法(持久化存储,直接生效,无弹窗)========================
|
||||
/**
|
||||
* 设置提醒间隔时间(直接生效,无弹窗,自动校准最小1000ms)
|
||||
* @param interval 目标间隔(单位:ms)
|
||||
*/
|
||||
public void setReminderIntervalTime(final int interval) {
|
||||
final int calibratedInterval = Math.max(interval, MIN_INTERVAL_TIME);
|
||||
if (calibratedInterval == mAppConfigBean.getReminderIntervalTime()) {
|
||||
LogUtils.d(TAG, "setReminderIntervalTime: 提醒间隔无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setReminderIntervalTime(calibratedInterval);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setReminderIntervalTime: 提醒间隔更新为=" + calibratedInterval + "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提醒间隔时间
|
||||
* @return 提醒间隔(单位:ms)
|
||||
*/
|
||||
public int getReminderIntervalTime() {
|
||||
int interval = mAppConfigBean.getReminderIntervalTime();
|
||||
LogUtils.d(TAG, "getReminderIntervalTime: 获取提醒间隔=" + interval + "ms");
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置电量检测间隔(直接生效,无弹窗,自动校准最小500ms,与RemindThread同步)
|
||||
* @param interval 目标间隔(单位:ms)
|
||||
*/
|
||||
public void setBatteryDetectInterval(final int interval) {
|
||||
final int calibratedInterval = Math.max(interval, MIN_DETECT_INTERVAL);
|
||||
if (calibratedInterval == mAppConfigBean.getBatteryDetectInterval()) {
|
||||
LogUtils.d(TAG, "setBatteryDetectInterval: 检测间隔无变化,无需操作");
|
||||
return;
|
||||
}
|
||||
mAppConfigBean.setBatteryDetectInterval(calibratedInterval);
|
||||
saveAppConfig();
|
||||
LogUtils.d(TAG, "setBatteryDetectInterval: 电量检测间隔更新为=" + calibratedInterval + "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电量检测间隔
|
||||
* @return 电量检测间隔(单位:ms)
|
||||
*/
|
||||
public int getBatteryDetectInterval() {
|
||||
int interval = mAppConfigBean.getBatteryDetectInterval();
|
||||
LogUtils.d(TAG, "getBatteryDetectInterval: 获取电量检测间隔=" + interval + "ms");
|
||||
return interval;
|
||||
}
|
||||
|
||||
public boolean isServiceEnabled() {
|
||||
// 加载服务配置
|
||||
ControlCenterServiceBean savedServiceBean = (ControlCenterServiceBean) ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
|
||||
if (savedServiceBean != null) {
|
||||
return savedServiceBean.isEnableService();
|
||||
} else {
|
||||
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(false));
|
||||
return false;
|
||||
}
|
||||
return bean.isEnableService();
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
|
||||
mAppConfigBean.setIsEnableChargeReminder(isEnableChargeReminder);
|
||||
saveConfigData(MainActivity._mMainActivity);
|
||||
}
|
||||
|
||||
public boolean getIsEnableChargeReminder() {
|
||||
return mAppConfigBean.isEnableChargeReminder();
|
||||
}
|
||||
|
||||
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
|
||||
mAppConfigBean.setIsEnableUsegeReminder(isEnableUsegeReminder);
|
||||
saveConfigData(MainActivity._mMainActivity);
|
||||
}
|
||||
|
||||
public boolean getIsEnableUsegeReminder() {
|
||||
return mAppConfigBean.isEnableUsegeReminder();
|
||||
}
|
||||
|
||||
public void setChargeReminderValue(int value) {
|
||||
mAppConfigBean.setChargeReminderValue(value);
|
||||
saveConfigData(MainActivity._mMainActivity);
|
||||
}
|
||||
|
||||
public int getChargeReminderValue() {
|
||||
return mAppConfigBean.getChargeReminderValue();
|
||||
}
|
||||
|
||||
public void setUsegeReminderValue(int value) {
|
||||
mAppConfigBean.setUsegeReminderValue(value);
|
||||
saveConfigData(MainActivity._mMainActivity);
|
||||
}
|
||||
|
||||
public int getUsegeReminderValue() {
|
||||
return mAppConfigBean.getUsegeReminderValue();
|
||||
}
|
||||
|
||||
public void setIsCharging(boolean isCharging) {
|
||||
mAppConfigBean.setIsCharging(isCharging);
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return mAppConfigBean.isCharging();
|
||||
}
|
||||
|
||||
public void setCurrentValue(int nCurrentValue) {
|
||||
mAppConfigBean.setCurrentValue(nCurrentValue);
|
||||
}
|
||||
|
||||
public int getCurrentValue() {
|
||||
return mAppConfigBean.getCurrentValue();
|
||||
}
|
||||
|
||||
public void setReminderIntervalTime(int reminderIntervalTime) {
|
||||
mAppConfigBean.setReminderIntervalTime(reminderIntervalTime);
|
||||
}
|
||||
|
||||
public int getReminderIntervalTime() {
|
||||
return mAppConfigBean.getReminderIntervalTime();
|
||||
}
|
||||
|
||||
//
|
||||
// 加载电池提醒配置数据
|
||||
//
|
||||
public void loadAppConfigBean() {
|
||||
AppConfigBean bean = AppConfigBean.loadBean(mContext, AppConfigBean.class);
|
||||
if (bean == null) {
|
||||
bean = new AppConfigBean();
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
}
|
||||
mAppConfigBean.setIsEnableUsegeReminder(bean.isEnableUsegeReminder());
|
||||
mAppConfigBean.setUsegeReminderValue(bean.getUsegeReminderValue());
|
||||
mAppConfigBean.setIsEnableChargeReminder(bean.isEnableChargeReminder());
|
||||
mAppConfigBean.setChargeReminderValue(bean.getChargeReminderValue());
|
||||
}
|
||||
|
||||
public void saveConfigData(final MainActivity activity) {
|
||||
if (MainActivity._mMainActivity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
YesNoAlertDialog.show(activity, "应用设置信息", "是否保存应用配置?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
saveConfigData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
AppConfigUtils.getInstance(activity).loadAppConfigBean();
|
||||
MainActivity.relaodAppConfigs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// 保存应用配置数据
|
||||
//
|
||||
void saveConfigData() {
|
||||
// 更新配置先取消一下旧的的提醒消息
|
||||
//NotificationHelper.cancelRemindNotification(mContext);
|
||||
|
||||
AppConfigBean.saveBean(mContext, mAppConfigBean);
|
||||
// 通知活动窗口和服务配置已更新
|
||||
ControlCenterService.updateStatus(mContext, mAppConfigBean);
|
||||
MainActivity.relaodAppConfigs();
|
||||
}
|
||||
public void setIsServiceEnabled(boolean isServiceEnabled) {
|
||||
ControlCenterServiceBean.saveBean(mContext, new ControlCenterServiceBean(isServiceEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,75 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 04:32:46
|
||||
* @Describe 电池工具类
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.BatteryManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 04:32:46
|
||||
* @Describe 电池状态工具类
|
||||
* 功能:解析电池广播Intent,获取充电状态、当前电量
|
||||
* 适配:Java7 | API30 | 小米手机
|
||||
*/
|
||||
public class BatteryUtils {
|
||||
|
||||
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
|
||||
public static final String TAG = "BatteryUtils";
|
||||
|
||||
// 电池电量计算常量
|
||||
private static final int BATTERY_SCALE_DEFAULT = 100;
|
||||
private static final int BATTERY_LEVEL_MIN = 0;
|
||||
private static final int BATTERY_LEVEL_MAX = 100;
|
||||
|
||||
// ================================== 工具方法(静态方法,无状态设计)=================================
|
||||
/**
|
||||
* 判断当前是否处于充电状态
|
||||
* @param intent 电池状态广播Intent(非空)
|
||||
* @return true=充电中/已充满,false=未充电
|
||||
*/
|
||||
public static boolean isCharging(Intent intent) {
|
||||
LogUtils.d(TAG, "isCharging: 调用 | intent=" + intent);
|
||||
// 入参非空校验
|
||||
if (intent == null) {
|
||||
LogUtils.e(TAG, "isCharging: intent为空,返回false");
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
LogUtils.d(TAG, "isCharging: 解析完成 | status=" + status + " | result=" + isCharging);
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
public static int getTheQuantityOfElectricity(Intent intent) {
|
||||
int intLevel = intent.getIntExtra("level", 0);
|
||||
int intScale = intent.getIntExtra("scale", 100);
|
||||
return intLevel * 100 / intScale;
|
||||
/**
|
||||
* 获取当前电池电量百分比(0-100)
|
||||
* @param intent 电池状态广播Intent(非空)
|
||||
* @return 电量百分比,异常返回0
|
||||
*/
|
||||
public static int getCurrentBatteryLevel(Intent intent) {
|
||||
LogUtils.d(TAG, "getCurrentBatteryLevel: 调用 | intent=" + intent);
|
||||
// 入参非空校验
|
||||
if (intent == null) {
|
||||
LogUtils.e(TAG, "getCurrentBatteryLevel: intent为空,返回0");
|
||||
return BATTERY_LEVEL_MIN;
|
||||
}
|
||||
|
||||
// 解析电量原始值与刻度值
|
||||
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN);
|
||||
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE_DEFAULT);
|
||||
LogUtils.d(TAG, "getCurrentBatteryLevel: 原始值 | level=" + level + " | scale=" + scale);
|
||||
|
||||
// 计算并校验电量百分比,避免除以0或数值越界
|
||||
int batteryLevel;
|
||||
if (scale <= 0) {
|
||||
batteryLevel = level;
|
||||
LogUtils.w(TAG, "getCurrentBatteryLevel: scale无效,直接使用level值");
|
||||
} else {
|
||||
batteryLevel = level * BATTERY_SCALE_DEFAULT / scale;
|
||||
}
|
||||
// 确保电量值在0-100范围内
|
||||
batteryLevel = Math.max(BATTERY_LEVEL_MIN, Math.min(batteryLevel, BATTERY_LEVEL_MAX));
|
||||
LogUtils.d(TAG, "getCurrentBatteryLevel: 计算完成 | batteryLevel=" + batteryLevel + "%");
|
||||
return batteryLevel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 01:57
|
||||
* @Describe 单例 Bitmap 缓存工具类(Java 7 兼容)- 强制缓存版
|
||||
* 功能:内存缓存 Bitmap,支持路径关联缓存、全局获取、缓存清空、SP 持久化最后缓存路径、构造时预加载
|
||||
* 特点:1. 单例模式 2. 硬引用唯一缓存(强制保持,内存紧张不回收) 3. 路径-Bitmap 映射 4. 线程安全
|
||||
* 5. SP 持久化最后缓存路径 6. 构造时预加载 7. 引用计数防误回收 8. 高压缩比减少OOM风险
|
||||
* 核心策略:无论内存如何紧张,强制保持已缓存的Bitmap,通过高压缩比降低单张Bitmap内存占用
|
||||
*/
|
||||
public class BitmapCacheUtils {
|
||||
public static final String TAG = "BitmapCacheUtils";
|
||||
// 最大图片尺寸(降低至720P,进一步减少内存占用,强制缓存核心策略)
|
||||
private static final int MAX_WIDTH = 720;
|
||||
private static final int MAX_HEIGHT = 1280;
|
||||
|
||||
// SP 相关常量
|
||||
private static final String SP_NAME = "BitmapCacheSP";
|
||||
private static final String SP_KEY_LAST_CACHE_PATH = "last_cache_image_path";
|
||||
|
||||
// 单例实例(volatile 保证多线程可见性)
|
||||
private static volatile BitmapCacheUtils sInstance;
|
||||
// 路径-Bitmap 硬引用缓存(唯一缓存,强制保持,内存紧张不回收)
|
||||
private final Map<String, Bitmap> mHardCacheMap;
|
||||
// 路径-引用计数 映射(解决多实例共享问题)
|
||||
private final Map<String, Integer> mRefCountMap;
|
||||
// SP 实例(用于持久化最后缓存路径)
|
||||
private final SharedPreferences mSp;
|
||||
|
||||
// 私有构造器(单例模式)
|
||||
private BitmapCacheUtils() {
|
||||
// 使用ConcurrentHashMap保证线程安全,避免手动同步
|
||||
mHardCacheMap = new ConcurrentHashMap<>();
|
||||
mRefCountMap = new ConcurrentHashMap<>();
|
||||
// 初始化 SP(使用 App 全局上下文,避免内存泄漏)
|
||||
mSp = App.getInstance().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
// 构造时自动预加载 SP 中保存的最后一次缓存路径的图片
|
||||
preloadLastCachedBitmap();
|
||||
// 注册内存状态监听(仅记录日志,不清理缓存)
|
||||
registerMemoryStatusListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,线程安全)
|
||||
*/
|
||||
public static BitmapCacheUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (BitmapCacheUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new BitmapCacheUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 补充接口:直接缓存已解码的Bitmap(适配BackgroundView改进需求)
|
||||
* @param imagePath 图片绝对路径
|
||||
* @param bitmap 已解码的有效Bitmap
|
||||
* @return 缓存后的Bitmap / null(参数无效)
|
||||
*/
|
||||
public Bitmap cacheBitmap(String imagePath, Bitmap bitmap) {
|
||||
if (TextUtils.isEmpty(imagePath) || !isBitmapValid(bitmap)) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 路径或Bitmap无效");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 强制存入硬引用缓存,不转软引用
|
||||
mHardCacheMap.put(imagePath, bitmap);
|
||||
// 初始化引用计数为1
|
||||
mRefCountMap.put(imagePath, 1);
|
||||
// 持久化当前路径到 SP
|
||||
saveLastCachePathToSp(imagePath);
|
||||
LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功(强制保持) - " + imagePath);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心接口:根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存成功的 Bitmap / null(路径无效/文件不存在/解码失败)
|
||||
*/
|
||||
public Bitmap cacheBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片路径为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片文件无效(不存在/非文件/空文件) - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已缓存则直接返回,避免重复加载
|
||||
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
|
||||
if (isBitmapValid(hardCacheBitmap)) {
|
||||
LogUtils.d(TAG, "cacheBitmap: 硬引用缓存命中,引用计数+1 - " + imagePath);
|
||||
// 引用计数+1
|
||||
increaseRefCount(imagePath);
|
||||
// 持久化当前路径到 SP
|
||||
saveLastCachePathToSp(imagePath);
|
||||
return hardCacheBitmap;
|
||||
}
|
||||
|
||||
// 高压缩比加载 Bitmap(强制缓存核心:通过降低分辨率减少单张Bitmap内存占用)
|
||||
Bitmap bitmap = decodeCompressedBitmap(imagePath);
|
||||
if (bitmap != null) {
|
||||
// 强制存入硬引用缓存,不转软引用
|
||||
mHardCacheMap.put(imagePath, bitmap);
|
||||
// 初始化引用计数为1
|
||||
mRefCountMap.put(imagePath, 1);
|
||||
// 持久化当前路径到 SP
|
||||
saveLastCachePathToSp(imagePath);
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径(强制保持) - " + imagePath);
|
||||
} else {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心接口:根据路径获取缓存的 Bitmap
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存的有效 Bitmap / null(未缓存/已回收)
|
||||
*/
|
||||
public Bitmap getCachedBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 仅从硬引用缓存获取,无软引用 fallback
|
||||
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
|
||||
if (isBitmapValid(hardCacheBitmap)) {
|
||||
return hardCacheBitmap;
|
||||
}
|
||||
|
||||
// 缓存未命中或Bitmap已失效
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增接口:增加指定路径Bitmap的引用计数
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void increaseRefCount(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return;
|
||||
}
|
||||
synchronized (mRefCountMap) {
|
||||
Integer count = mRefCountMap.get(imagePath);
|
||||
if (count == null) {
|
||||
mRefCountMap.put(imagePath, 1);
|
||||
} else {
|
||||
mRefCountMap.put(imagePath, count + 1);
|
||||
}
|
||||
LogUtils.d(TAG, "increaseRefCount: " + imagePath + " 引用计数变为 " + mRefCountMap.get(imagePath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增接口:减少指定路径Bitmap的引用计数,计数为0时仅标记不回收(强制缓存策略)
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void decreaseRefCount(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return;
|
||||
}
|
||||
synchronized (mRefCountMap) {
|
||||
Integer count = mRefCountMap.get(imagePath);
|
||||
if (count == null || count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int newCount = count - 1;
|
||||
if (newCount <= 0) {
|
||||
// 强制缓存策略:引用计数为0时仅移除计数,不回收Bitmap
|
||||
mRefCountMap.remove(imagePath);
|
||||
LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数为0,保留Bitmap(强制缓存)");
|
||||
} else {
|
||||
mRefCountMap.put(imagePath, newCount);
|
||||
LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数变为 " + newCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 Bitmap 缓存(仅手动调用时执行,内存紧张时不自动执行)
|
||||
*/
|
||||
public void clearAllCache() {
|
||||
LogUtils.d(TAG, "clearAllCache: 手动清空所有缓存(强制缓存策略:仅手动触发)");
|
||||
|
||||
// 清空硬引用缓存并回收Bitmap
|
||||
for (Bitmap bitmap : mHardCacheMap.values()) {
|
||||
if (isBitmapValid(bitmap)) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
mHardCacheMap.clear();
|
||||
|
||||
// 清空引用计数
|
||||
mRefCountMap.clear();
|
||||
|
||||
// 清空 SP 中保存的最后缓存路径
|
||||
clearLastCachePathInSp();
|
||||
|
||||
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定路径的 Bitmap 缓存(仅手动调用时执行,内存紧张时不自动执行)
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void removeCachedBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (mRefCountMap) {
|
||||
// 手动移除时才回收Bitmap
|
||||
Bitmap hardBitmap = mHardCacheMap.remove(imagePath);
|
||||
if (isBitmapValid(hardBitmap)) {
|
||||
hardBitmap.recycle();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 手动回收硬引用缓存 - " + imagePath);
|
||||
}
|
||||
mRefCountMap.remove(imagePath);
|
||||
|
||||
// 若移除的是最后缓存的路径,清空 SP
|
||||
String lastPath = getLastCachePathFromSp();
|
||||
if (imagePath.equals(lastPath)) {
|
||||
clearLastCachePathInSp();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 移除的是最后缓存路径,已清空 SP");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高压缩比解码 Bitmap(强制缓存核心:通过降低分辨率+RGB_565减少单张Bitmap内存占用)
|
||||
* @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 = calculateHighCompressSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
|
||||
|
||||
// 第二步:加载高压缩比的 Bitmap
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565; // 强制使用RGB_565,比ARGB_8888少一半内存
|
||||
options.inPurgeable = false; // 关闭可清除标志,强制保持内存
|
||||
options.inInputShareable = false;
|
||||
|
||||
try {
|
||||
return BitmapFactory.decodeFile(imagePath, options);
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常(已启用高压缩比) - " + imagePath);
|
||||
// 强制缓存策略:OOM时仅记录日志,不清理已缓存的Bitmap
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算高压缩比缩放比例(强制缓存核心:优先保证不超过最大尺寸,尽可能压缩)
|
||||
*/
|
||||
private int calculateHighCompressSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
|
||||
int rawWidth = options.outWidth;
|
||||
int rawHeight = options.outHeight;
|
||||
int inSampleSize = 1;
|
||||
|
||||
// 高压缩比逻辑:只要超过最大尺寸,就持续放大采样率
|
||||
while (rawWidth / inSampleSize > maxWidth || rawHeight / inSampleSize > maxHeight) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "calculateHighCompressSampleSize: 高压缩比缩放比例为 " + inSampleSize);
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:判断Bitmap是否有效(非空且未被回收)
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
return bitmap != null && !bitmap.isRecycled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 SP 中获取最后一次缓存的图片路径
|
||||
* @return 最后缓存的路径 / null(未保存)
|
||||
*/
|
||||
private String getLastCachePathFromSp() {
|
||||
return mSp.getString(SP_KEY_LAST_CACHE_PATH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前缓存路径持久化到 SP
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
private void saveLastCachePathToSp(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return;
|
||||
}
|
||||
mSp.edit().putString(SP_KEY_LAST_CACHE_PATH, imagePath).commit(); // Java 7 兼容,使用 commit 而非 apply
|
||||
LogUtils.d(TAG, "saveLastCachePathToSp: 持久化最后缓存路径 - " + imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空 SP 中保存的最后缓存路径
|
||||
*/
|
||||
private void clearLastCachePathInSp() {
|
||||
mSp.edit().remove(SP_KEY_LAST_CACHE_PATH).commit();
|
||||
LogUtils.d(TAG, "clearLastCachePathInSp: SP 中最后缓存路径已清空");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造时预加载 SP 中保存的最后一次缓存路径的图片
|
||||
*/
|
||||
private void preloadLastCachedBitmap() {
|
||||
String lastPath = getLastCachePathFromSp();
|
||||
if (TextUtils.isEmpty(lastPath)) {
|
||||
LogUtils.d(TAG, "preloadLastCachedBitmap: SP 中无保存的缓存路径,跳过预加载");
|
||||
return;
|
||||
}
|
||||
// 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断)
|
||||
Bitmap bitmap = cacheBitmap(lastPath);
|
||||
if (bitmap != null) {
|
||||
LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功(强制保持) - " + lastPath);
|
||||
} else {
|
||||
LogUtils.w(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径失败,清空无效路径 - " + lastPath);
|
||||
// 预加载失败,清空 SP 中无效路径
|
||||
clearLastCachePathInSp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册内存状态监听(仅记录日志,不清理缓存,强制缓存策略)
|
||||
*/
|
||||
private void registerMemoryStatusListener() {
|
||||
if (Build.VERSION.SDK_INT >= 14) {
|
||||
App.getInstance().registerComponentCallbacks(new MemoryStatusCallback());
|
||||
LogUtils.d(TAG, "registerMemoryStatusListener: 内存状态监听已注册(仅记录日志,不清理缓存)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存状态回调(仅记录日志,不清理缓存,强制缓存策略)
|
||||
*/
|
||||
private class MemoryStatusCallback implements android.content.ComponentCallbacks2 {
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
// 强制缓存策略:内存紧张时仅记录日志,不清理任何缓存
|
||||
LogUtils.w(TAG, "onTrimMemory: 内存紧张级别 - " + level + ",强制保持所有Bitmap缓存");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
// 强制缓存策略:低内存时仅记录日志,不清理任何缓存
|
||||
LogUtils.w(TAG, "onLowMemory: 系统低内存,强制保持所有Bitmap缓存(已启用高压缩比)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
|
||||
// 配置变化时无需处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,24 +263,28 @@ public class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFileSuffix(Context context, Uri uri){
|
||||
String szType = context.getContentResolver().getType(uri);
|
||||
// 2. 截取MIME类型后缀(如从image/jpeg中提取jpeg)【核心新增逻辑】
|
||||
String fileSuffix = "";
|
||||
if (szType != null && szType.contains("/")) {
|
||||
// 分割字符串,取"/"后面的部分(如"image/jpeg" → 分割后取索引1的"jpeg")
|
||||
fileSuffix = szType.split("/")[1];
|
||||
// 调试日志:打印截取后的文件后缀
|
||||
} else {
|
||||
// 异常处理:若类型为空或格式错误,默认后缀设为jpeg(保留原逻辑兼容性)
|
||||
fileSuffix = "jpeg";
|
||||
}
|
||||
return fileSuffix;
|
||||
}
|
||||
|
||||
public static boolean isFileExists(String path) {
|
||||
File file = new File(path);
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
|
||||
* @param file 目标文件
|
||||
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
|
||||
*/
|
||||
public static String getFileSuffix(File file) {
|
||||
if (file == null || file.getName().isEmpty()) {
|
||||
return ""; // 空文件/空文件名,返回空
|
||||
}
|
||||
String fileName = file.getName();
|
||||
int lastDotIndex = fileName.lastIndexOf(".");
|
||||
// 无后缀(没有点,或点在开头/结尾)
|
||||
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
|
||||
return fileName.substring(lastDotIndex + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,42 +6,101 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import com.yalantis.ucrop.UCrop;
|
||||
import com.yalantis.ucrop.UCropActivity;
|
||||
import java.io.File;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import com.yalantis.ucrop.UCrop;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 图片裁剪工具类(集成uCrop,脱离系统依赖)
|
||||
* 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File 双传参)
|
||||
*/
|
||||
public class ImageCropUtils {
|
||||
public static final String TAG = "ImageCropUtils";
|
||||
// FileProvider 授权(与项目一致)
|
||||
// FileProvider 授权(与 AndroidManifest 配置一致)
|
||||
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
|
||||
// 强制输出格式:固定为 PNG(保留透明通道)
|
||||
private static final String FORCE_OUTPUT_SUFFIX = "png";
|
||||
private static final android.graphics.Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = android.graphics.Bitmap.CompressFormat.PNG;
|
||||
|
||||
// ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)======================
|
||||
/**
|
||||
* 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG,保留透明通道
|
||||
* @param activity 上下文
|
||||
* @param inputUri 输入图片 Uri(本应用 FileProvider Uri,非空)
|
||||
* @param outputUri 输出图片 Uri(本应用 FileProvider Uri,非空)
|
||||
* @param aspectX 固定比例 X(自由裁剪传 0)
|
||||
* @param aspectY 固定比例 Y(自由裁剪传 0)
|
||||
* @param isFreeCrop 是否自由裁剪
|
||||
* @param requestCode 裁剪请求码
|
||||
*/
|
||||
public static void startImageCrop(Activity activity,
|
||||
Uri inputUri,
|
||||
Uri outputUri,
|
||||
int aspectX,
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
// 1. 输入参数校验
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
|
||||
return;
|
||||
}
|
||||
if (inputUri == null || outputUri == null) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入/输出 Uri 为空");
|
||||
showToast(activity, "图片 Uri 无效,无法裁剪");
|
||||
return;
|
||||
}
|
||||
if (!isValidUri(activity, inputUri)) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入 Uri 无效:" + inputUri);
|
||||
showToast(activity, "原图 Uri 无效,无法裁剪");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 核心:强制修正输出为 PNG(忽略原图格式,统一转 PNG)
|
||||
File outputFile = uriToFile(activity, outputUri);
|
||||
if (outputFile == null) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输出 Uri 转 File 失败:" + outputUri);
|
||||
showToast(activity, "裁剪输出路径无效");
|
||||
return;
|
||||
}
|
||||
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
|
||||
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
|
||||
|
||||
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
|
||||
UCrop uCrop = UCrop.of(inputUri, outputUri);
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
|
||||
|
||||
// 4. 启动裁剪
|
||||
uCrop.withOptions(options);
|
||||
uCrop.start(activity, requestCode);
|
||||
LogUtils.d(TAG, "【裁剪启动成功(Uri 版)】强制输出 PNG(透明保留),输出路径:" + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动uCrop裁剪(核心方法,替代系统裁剪)
|
||||
* 【File 传参版】启动 uCrop 裁剪 - 强制输出 PNG,保留透明通道
|
||||
* @param activity 上下文
|
||||
* @param inputFile 输入图片文件
|
||||
* @param outputFile 输出图片文件
|
||||
* @param isFreeCrop 是否自由裁剪(true=自由,false=固定比例)
|
||||
* @param inputFile 输入图片文件(任意格式)
|
||||
* @param outputFile 输出图片文件(最终强制转为 PNG)
|
||||
* @param aspectX 固定比例 X(自由裁剪传 0)
|
||||
* @param aspectY 固定比例 Y(自由裁剪传 0)
|
||||
* @param isFreeCrop 是否自由裁剪
|
||||
* @param requestCode 裁剪请求码
|
||||
*/
|
||||
public static void startImageCrop(Activity activity,
|
||||
File inputFile,
|
||||
File outputFile,
|
||||
int aspectX,
|
||||
int aspectX,
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
// 校验输入参数
|
||||
// 1. 输入参数校验
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【裁剪异常】上下文Activity无效");
|
||||
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
|
||||
return;
|
||||
}
|
||||
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
|
||||
LogUtils.e(TAG, "【裁剪异常】输入文件无效");
|
||||
LogUtils.e(TAG, "【裁剪异常】输入图片文件无效");
|
||||
showToast(activity, "无有效图片可裁剪");
|
||||
return;
|
||||
}
|
||||
@@ -51,47 +110,28 @@ public class ImageCropUtils {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成输入/输出Uri(适配FileProvider)
|
||||
// 2. 核心:强制修正输出为 PNG(忽略原图格式)
|
||||
Uri inputUri = getFileProviderUri(activity, inputFile);
|
||||
Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri(兼容低版本)
|
||||
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
|
||||
Uri outputUri = getFileProviderUri(activity, outputFile);
|
||||
|
||||
// 配置uCrop参数
|
||||
// 3. 初始化 uCrop + 强制 PNG 配置
|
||||
UCrop uCrop = UCrop.of(inputUri, outputUri);
|
||||
UCrop.Options options = new UCrop.Options();
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
|
||||
|
||||
// 裁剪模式配置(自由裁剪/固定比例)
|
||||
if (isFreeCrop) {
|
||||
// 自由裁剪:无固定比例,可随意调整
|
||||
uCrop.withAspectRatio(0, 0);
|
||||
options.setFreeStyleCropEnabled(true); // 开启自由裁剪
|
||||
} else {
|
||||
// 固定比例(默认1:1,可根据需求修改)
|
||||
uCrop.withAspectRatio(aspectX, aspectY);
|
||||
options.setFreeStyleCropEnabled(false);
|
||||
}
|
||||
|
||||
// 裁剪配置(优化体验)
|
||||
options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
|
||||
options.setCompressionQuality(100); // 图片质量
|
||||
options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
|
||||
options.setToolbarTitle("图片裁剪"); // 工具栏标题
|
||||
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
|
||||
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
|
||||
|
||||
// 应用配置并启动裁剪
|
||||
// 4. 启动裁剪
|
||||
uCrop.withOptions(options);
|
||||
// 启动uCrop裁剪Activity(替代系统裁剪)
|
||||
uCrop.start(activity, requestCode);
|
||||
|
||||
LogUtils.d(TAG, "【uCrop启动】成功,输入Uri:" + inputUri + ",输出Uri:" + outputUri + ",请求码:" + requestCode);
|
||||
LogUtils.d(TAG, "【裁剪启动成功(File 版)】强制输出 PNG(透明保留),输出路径:" + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:适配BackgroundBean
|
||||
* 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG,保留透明通道
|
||||
*/
|
||||
public static void startImageCrop(Activity activity,
|
||||
BackgroundBean cropBean,
|
||||
int aspectX,
|
||||
int aspectX,
|
||||
int aspectY,
|
||||
boolean isFreeCrop,
|
||||
int requestCode) {
|
||||
@@ -100,70 +140,163 @@ public class ImageCropUtils {
|
||||
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;
|
||||
// ====================== 裁剪结果处理(保持兼容,优化日志)======================
|
||||
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
|
||||
if (requestCode != cropRequestCode) return null;
|
||||
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
Uri outputUri = UCrop.getOutput(data);
|
||||
if (outputUri != null) {
|
||||
String outputPath = uriToPath(outputUri);
|
||||
LogUtils.d(TAG, "【裁剪成功】强制输出 PNG(透明保留),输出路径:" + outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
} else if (resultCode == UCrop.RESULT_ERROR) {
|
||||
Throwable error = UCrop.getError(data);
|
||||
LogUtils.e(TAG, "【裁剪失败】原因:" + (error != null ? error.getMessage() : "未知错误"));
|
||||
} else {
|
||||
LogUtils.d(TAG, "【裁剪取消】用户手动取消");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ====================== 辅助方法(优化适配强制 PNG 逻辑)======================
|
||||
/** 校验 Uri 有效性(确保是图片类型) */
|
||||
private static boolean isValidUri(Activity activity, Uri uri) {
|
||||
try {
|
||||
String type = activity.getContentResolver().getType(uri);
|
||||
return type != null && type.startsWith("image/");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri生成】失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, "【Uri 校验失败】原因:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Uri 转 File(适配 FileProvider Uri 和普通 Uri) */
|
||||
private static File uriToFile(Activity activity, Uri uri) {
|
||||
if (uri == null) return null;
|
||||
try {
|
||||
if (uri.getScheme().equals("file")) {
|
||||
return new File(uri.getPath());
|
||||
}
|
||||
String filePath = uri.getPath();
|
||||
if (filePath == null) return null;
|
||||
if (filePath.contains("/external_files/")) {
|
||||
filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/");
|
||||
} else if (filePath.contains("/cache/")) {
|
||||
filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/");
|
||||
}
|
||||
return new File(filePath);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri 转 File 失败】uri=" + uri + ",原因:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Uri 提取文件路径 */
|
||||
private static String uriToPath(Uri uri) {
|
||||
if (uri == null) return null;
|
||||
try {
|
||||
if (uri.getScheme().equals("file")) {
|
||||
return uri.getPath();
|
||||
}
|
||||
String path = uri.getPath();
|
||||
if (path == null) return null;
|
||||
String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"};
|
||||
for (String prefix : prefixes) {
|
||||
if (path.contains(prefix)) {
|
||||
path = path.substring(path.indexOf(prefix) + prefix.length());
|
||||
String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
return externalRoot + "/" + path;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理uCrop裁剪回调(在Activity的onActivityResult中调用)
|
||||
* @param requestCode 请求码
|
||||
* @param resultCode 结果码
|
||||
* @param data 回调数据
|
||||
* @return 裁剪成功返回输出文件路径,失败返回null
|
||||
* 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心)
|
||||
* 移除 isPng 参数,全程用 PNG 配置
|
||||
*/
|
||||
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;
|
||||
private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
|
||||
|
||||
UCrop.Options options = new UCrop.Options();
|
||||
|
||||
// 裁剪模式配置(自由裁剪/固定比例)
|
||||
options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
|
||||
|
||||
// 裁剪配置(优化体验)
|
||||
//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)); // 状态栏颜色
|
||||
|
||||
|
||||
// 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
|
||||
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
|
||||
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
|
||||
options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键)
|
||||
options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色
|
||||
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
|
||||
|
||||
// 3. 通用 UI 配置(保持原有风格)
|
||||
options.setHideBottomControls(true); // 隐藏底部控制栏
|
||||
options.setToolbarTitle("图片裁剪");
|
||||
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
|
||||
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));
|
||||
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark));
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast
|
||||
* 修正文件后缀(强制转为 .png,覆盖原有任何图片后缀)
|
||||
*/
|
||||
private static File correctFileSuffix(File originFile, String targetSuffix) {
|
||||
String originName = originFile.getName();
|
||||
// 强制替换所有图片后缀为 targetSuffix(避免漏改)
|
||||
originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix;
|
||||
return new File(originFile.getParent(), originName);
|
||||
}
|
||||
|
||||
/** 生成 FileProvider Uri(适配 Android 7.0+) */
|
||||
private static Uri getFileProviderUri(Activity activity, File file) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
|
||||
return FileProvider.getUriForFile(activity, authority, file);
|
||||
} else {
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage());
|
||||
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);
|
||||
}
|
||||
|
||||
public static File getFileFromUriPublic(Activity activity, Uri uri) {
|
||||
return uriToFile(activity, uri);
|
||||
}
|
||||
|
||||
public static String getPathFromUriPublic(Uri uri) {
|
||||
return uriToPath(uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/22 04:39:40
|
||||
* @Describe 通知工具类
|
||||
*/
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Build;
|
||||
import android.widget.RemoteViews;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
|
||||
public class NotificationHelper {
|
||||
public static final String TAG = "NotificationHelper";
|
||||
|
||||
// 渠道ID和名称
|
||||
private static final String CHANNEL_ID_FOREGROUND = "foreground_channel";
|
||||
private static final String CHANNEL_NAME_FOREGROUND = "Foreground Service";
|
||||
private static final String CHANNEL_ID_TEMPORARY = "temporary_channel";
|
||||
private static final String CHANNEL_NAME_TEMPORARY = "Temporary Notifications";
|
||||
|
||||
// 通知ID
|
||||
public static final int FOREGROUND_NOTIFICATION_ID = 1001;
|
||||
public static final int TEMPORARY_NOTIFICATION_ID = 2001;
|
||||
|
||||
private final Context mContext;
|
||||
private final NotificationManager mNotificationManager;
|
||||
|
||||
public NotificationHelper(Context context) {
|
||||
mContext = context;
|
||||
mNotificationManager = context.getSystemService(NotificationManager.class);
|
||||
createNotificationChannels();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createForegroundChannel();
|
||||
createTemporaryChannel();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createForegroundChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_FOREGROUND,
|
||||
CHANNEL_NAME_FOREGROUND,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Persistent service notifications");
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createTemporaryChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID_TEMPORARY,
|
||||
CHANNEL_NAME_TEMPORARY,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription("Temporary alert notifications");
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(true);
|
||||
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
|
||||
channel.setBypassDnd(true);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
// 显示常驻通知(通常用于前台服务)
|
||||
public Notification showForegroundNotification(Intent intent, String title, String content) {
|
||||
PendingIntent pendingIntent = createPendingIntent(intent);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_FOREGROUND)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
|
||||
//.setContentTitle(title + "\n" + content)
|
||||
.setContentTitle(content)
|
||||
//.setContentText(content)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
|
||||
mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
|
||||
return notification;
|
||||
}
|
||||
|
||||
// 显示临时通知(自动消失)
|
||||
public void showTemporaryNotification(Intent intent, String title, String content) {
|
||||
PendingIntent pendingIntent = createPendingIntent(intent);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setVibrate(new long[]{100, 200, 300, 400})
|
||||
.build();
|
||||
|
||||
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
// 创建自定义布局通知(可扩展)
|
||||
public void showCustomNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) {
|
||||
PendingIntent pendingIntent = createPendingIntent(intent);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setContent(contentView)
|
||||
.setCustomBigContentView(bigContentView)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.build();
|
||||
|
||||
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID + 1, notification);
|
||||
}
|
||||
|
||||
// 取消所有通知
|
||||
public void cancelAllNotifications() {
|
||||
mNotificationManager.cancelAll();
|
||||
}
|
||||
|
||||
// 创建PendingIntent(兼容不同API版本)
|
||||
private PendingIntent createPendingIntent(Intent intent) {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
// }
|
||||
return PendingIntent.getActivity(
|
||||
mContext,
|
||||
0,
|
||||
intent,
|
||||
flags
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.RingtoneManager;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.NotificationMessage;
|
||||
|
||||
/**
|
||||
* 通知工具类:统一管理前台服务/电池提醒/应用配置信息通知
|
||||
* 适配:API19-30 | Java7 | 小米手机
|
||||
* 特性:前台服务无铃声、提醒通知系统默认铃声、配置通知低优先级无打扰、API分级适配、内存泄漏防护
|
||||
*/
|
||||
public class NotificationManagerUtils {
|
||||
// ================================== 静态常量(置顶统一管理,杜绝魔法值)=================================
|
||||
public static final String TAG = "NotificationManagerUtils";
|
||||
// 通知渠道ID(API26+ 必需,区分通知类型)
|
||||
public static final String CHANNEL_ID_FOREGROUND = "cc.winboll.studio.powerbell.channel.foreground";
|
||||
public static final String CHANNEL_ID_REMIND = "cc.winboll.studio.powerbell.channel.remind";
|
||||
public static final String CHANNEL_ID_CONFIG = "cc.winboll.studio.powerbell.channel.config"; // 新增:应用配置信息渠道
|
||||
// 通知ID(唯一标识,避免重复)
|
||||
public static final int NOTIFY_ID_FOREGROUND_SERVICE = 1001;
|
||||
public static final int NOTIFY_ID_REMIND = 1002;
|
||||
public static final int NOTIFY_ID_CONFIG = 1003; // 新增:应用配置信息通知ID
|
||||
// 低版本兼容:默认通知图标(API<21 避免显示异常)
|
||||
private static final int NOTIFICATION_DEFAULT_ICON = R.drawable.ic_launcher;
|
||||
// 通知内容兜底常量
|
||||
private static final String FOREGROUND_NOTIFY_TITLE_DEFAULT = "电池服务运行中";
|
||||
private static final String FOREGROUND_NOTIFY_CONTENT_DEFAULT = "后台监测电池状态";
|
||||
private static final String REMIND_NOTIFY_TITLE_DEFAULT = "电池状态提醒";
|
||||
private static final String REMIND_NOTIFY_CONTENT_DEFAULT = "电池状态异常,请及时处理";
|
||||
private static final String CONFIG_NOTIFY_TITLE_DEFAULT = "应用配置更新"; // 新增:配置通知默认标题
|
||||
private static final String CONFIG_NOTIFY_CONTENT_DEFAULT = "配置信息已更新,生效中"; // 新增:配置通知默认内容
|
||||
// PendingIntent请求码
|
||||
private static final int PENDING_INTENT_REQUEST_CODE_FOREGROUND = 0;
|
||||
private static final int PENDING_INTENT_REQUEST_CODE_REMIND = 1;
|
||||
private static final int PENDING_INTENT_REQUEST_CODE_CONFIG = 2; // 新增:配置通知请求码
|
||||
|
||||
// ================================== 成员变量(私有封装,按依赖优先级排序)=================================
|
||||
// 核心上下文(应用级,避免内存泄漏)
|
||||
private Context mContext;
|
||||
// 系统通知服务(核心依赖)
|
||||
private NotificationManager mNotificationManager;
|
||||
// 前台服务通知实例(单独持有,便于更新/取消)
|
||||
private Notification mForegroundServiceNotify;
|
||||
|
||||
// ================================== 构造方法(初始化核心资源,前置校验)=================================
|
||||
public NotificationManagerUtils(Context context) {
|
||||
LogUtils.d(TAG, "NotificationManagerUtils: 构造方法执行 | context=" + context);
|
||||
// 前置校验:Context非空
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "NotificationManagerUtils: 构造失败:context is null");
|
||||
return;
|
||||
}
|
||||
// 初始化核心资源
|
||||
this.mContext = context.getApplicationContext();
|
||||
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
LogUtils.d(TAG, "NotificationManagerUtils: 核心资源初始化完成 | mContext=" + mContext + " | mNotificationManager=" + mNotificationManager);
|
||||
// 初始化通知渠道(API26+ 必需)
|
||||
initNotificationChannels();
|
||||
LogUtils.d(TAG, "NotificationManagerUtils: 构造完成");
|
||||
}
|
||||
|
||||
// ================================== 核心初始化方法(通知渠道,API分级适配)=================================
|
||||
/**
|
||||
* 初始化通知渠道:前台服务渠道(无铃声+无振动)、提醒渠道(系统默认铃声+无振动)、配置信息渠道(低优先级无打扰)
|
||||
*/
|
||||
private void initNotificationChannels() {
|
||||
LogUtils.d(TAG, "initNotificationChannels: 执行通知渠道初始化");
|
||||
// API<26 无渠道机制,直接返回
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
LogUtils.d(TAG, "initNotificationChannels: API<26,无需创建渠道");
|
||||
return;
|
||||
}
|
||||
// 通知服务为空,避免空指针
|
||||
if (mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "initNotificationChannels: 失败:NotificationManager is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 前台服务渠道(低优先级,后台保活无打扰)
|
||||
NotificationChannel foregroundChannel = new NotificationChannel(
|
||||
CHANNEL_ID_FOREGROUND,
|
||||
"电池服务保活",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
foregroundChannel.setDescription("电池监测服务后台运行,无声音、无振动");
|
||||
foregroundChannel.enableLights(false);
|
||||
foregroundChannel.enableVibration(false);
|
||||
foregroundChannel.setSound(null, null); // 强制无铃声
|
||||
foregroundChannel.setShowBadge(false);
|
||||
foregroundChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
|
||||
LogUtils.d(TAG, "initNotificationChannels: 前台服务渠道配置完成");
|
||||
|
||||
// 2. 电池提醒渠道(中优先级,系统默认铃声,无振动)
|
||||
NotificationChannel remindChannel = new NotificationChannel(
|
||||
CHANNEL_ID_REMIND,
|
||||
"电池状态提醒",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
remindChannel.setDescription("电池满电/低电量提醒,系统默认铃声,无振动");
|
||||
remindChannel.enableLights(true);
|
||||
remindChannel.enableVibration(false);
|
||||
remindChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT);
|
||||
remindChannel.setShowBadge(false);
|
||||
remindChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
LogUtils.d(TAG, "initNotificationChannels: 电池提醒渠道配置完成");
|
||||
|
||||
// 3. 应用配置信息渠道(新增:最低优先级,无铃声无振动,仅提示不打扰)
|
||||
NotificationChannel configChannel = new NotificationChannel(
|
||||
CHANNEL_ID_CONFIG,
|
||||
"应用配置信息",
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
);
|
||||
configChannel.setDescription("应用配置更新、参数变更等提示,无声音、无振动");
|
||||
configChannel.enableLights(false);
|
||||
configChannel.enableVibration(false);
|
||||
configChannel.setSound(null, null);
|
||||
configChannel.setShowBadge(false);
|
||||
configChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
||||
LogUtils.d(TAG, "initNotificationChannels: 应用配置信息渠道配置完成");
|
||||
|
||||
// 注册渠道到系统
|
||||
mNotificationManager.createNotificationChannel(foregroundChannel);
|
||||
mNotificationManager.createNotificationChannel(remindChannel);
|
||||
mNotificationManager.createNotificationChannel(configChannel); // 注册新增渠道
|
||||
LogUtils.d(TAG, "initNotificationChannels: 成功:创建前台服务+电池提醒+应用配置信息渠道");
|
||||
}
|
||||
|
||||
// ================================== 对外核心方法(前台服务通知:启动/更新/取消)=================================
|
||||
/**
|
||||
* 启动前台服务通知(API30适配,无铃声)
|
||||
*/
|
||||
public void startForegroundServiceNotify(Service service, NotificationMessage message) {
|
||||
LogUtils.d(TAG, "startForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | service=" + service + " | message=" + message);
|
||||
// 前置校验:参数非空
|
||||
if (service == null || message == null || mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "startForegroundServiceNotify: 失败:param is null | service=" + service + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建前台通知
|
||||
mForegroundServiceNotify = buildForegroundNotification(message);
|
||||
if (mForegroundServiceNotify == null) {
|
||||
LogUtils.e(TAG, "startForegroundServiceNotify: 失败:构建通知为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动前台服务(API30无FOREGROUND_SERVICE_TYPE限制,全版本通用)
|
||||
try {
|
||||
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
|
||||
LogUtils.d(TAG, "startForegroundServiceNotify: 成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "startForegroundServiceNotify: 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前台服务通知内容(复用通知ID,保持无铃声)
|
||||
*/
|
||||
public void updateForegroundServiceNotify(NotificationMessage message) {
|
||||
LogUtils.d(TAG, "updateForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE + " | message=" + message);
|
||||
if (message == null || mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "updateForegroundServiceNotify: 失败:param is null | message=" + message + " | mNotificationManager=" + mNotificationManager);
|
||||
return;
|
||||
}
|
||||
|
||||
mForegroundServiceNotify = buildForegroundNotification(message);
|
||||
if (mForegroundServiceNotify == null) {
|
||||
LogUtils.e(TAG, "updateForegroundServiceNotify: 失败:构建通知为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
|
||||
LogUtils.d(TAG, "updateForegroundServiceNotify: 成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "updateForegroundServiceNotify: 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消前台服务通知(Service销毁时调用)
|
||||
*/
|
||||
public void cancelForegroundServiceNotify() {
|
||||
LogUtils.d(TAG, "cancelForegroundServiceNotify: 执行 | notifyId=" + NOTIFY_ID_FOREGROUND_SERVICE);
|
||||
cancelNotification(NOTIFY_ID_FOREGROUND_SERVICE);
|
||||
mForegroundServiceNotify = null; // 置空释放
|
||||
LogUtils.d(TAG, "cancelForegroundServiceNotify: 成功");
|
||||
}
|
||||
|
||||
// ================================== 对外核心方法(电池提醒通知:发送)=================================
|
||||
/**
|
||||
* 发送电池提醒通知(系统默认铃声,无振动)
|
||||
*/
|
||||
public void showRemindNotification(Context context, NotificationMessage message) {
|
||||
LogUtils.d(TAG, "showRemindNotification: 执行 | notifyId=" + NOTIFY_ID_REMIND + " | context=" + context + " | message=" + message);
|
||||
if (context == null || message == null || mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "showRemindNotification: 失败:param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
|
||||
return;
|
||||
}
|
||||
|
||||
Notification remindNotify = buildRemindNotification(context, message);
|
||||
if (remindNotify == null) {
|
||||
LogUtils.e(TAG, "showRemindNotification: 失败:构建通知为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mNotificationManager.notify(NOTIFY_ID_REMIND, remindNotify);
|
||||
LogUtils.d(TAG, "showRemindNotification: 成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "showRemindNotification: 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 对外核心方法(应用配置信息通知:发送)=================================
|
||||
/**
|
||||
* 发送应用配置信息通知(新增:低优先级无铃声,仅提示不打扰)
|
||||
*/
|
||||
public void showConfigNotification(Context context, NotificationMessage message) {
|
||||
LogUtils.d(TAG, "showConfigNotification: 执行 | notifyId=" + NOTIFY_ID_CONFIG + " | context=" + context + " | message=" + message);
|
||||
if (context == null || message == null || mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "showConfigNotification: 失败:param is null | context=" + context + " | message=" + message + " | mNotificationManager=" + mNotificationManager);
|
||||
return;
|
||||
}
|
||||
|
||||
Notification configNotify = buildConfigNotification(context, message);
|
||||
if (configNotify == null) {
|
||||
LogUtils.e(TAG, "showConfigNotification: 失败:构建通知为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mNotificationManager.notify(NOTIFY_ID_CONFIG, configNotify);
|
||||
LogUtils.d(TAG, "showConfigNotification: 成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "showConfigNotification: 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 对外工具方法(通知取消:单个/全部)=================================
|
||||
/**
|
||||
* 取消指定ID的通知
|
||||
*/
|
||||
public void cancelNotification(int notifyId) {
|
||||
LogUtils.d(TAG, "cancelNotification: 执行 | notifyId=" + notifyId);
|
||||
if (mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "cancelNotification: 失败:NotificationManager is null");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mNotificationManager.cancel(notifyId);
|
||||
LogUtils.d(TAG, "cancelNotification: 成功 | notifyId=" + notifyId);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "cancelNotification: 异常 | notifyId=" + notifyId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有通知(兜底场景使用)
|
||||
*/
|
||||
public void cancelAllNotifications() {
|
||||
LogUtils.d(TAG, "cancelAllNotifications: 执行");
|
||||
if (mNotificationManager == null) {
|
||||
LogUtils.e(TAG, "cancelAllNotifications: 失败:NotificationManager is null");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mNotificationManager.cancelAll();
|
||||
LogUtils.d(TAG, "cancelAllNotifications: 成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "cancelAllNotifications: 异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 内部辅助方法(通知构建:前台服务通知)=================================
|
||||
/**
|
||||
* 构建前台服务通知(全版本无铃声+无振动)
|
||||
*/
|
||||
private Notification buildForegroundNotification(NotificationMessage message) {
|
||||
LogUtils.d(TAG, "buildForegroundNotification: 执行 | message=" + message);
|
||||
if (message == null || mContext == null) {
|
||||
LogUtils.e(TAG, "buildForegroundNotification: 失败:param is null | message=" + message + " | mContext=" + mContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 内容兜底
|
||||
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : FOREGROUND_NOTIFY_TITLE_DEFAULT;
|
||||
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : FOREGROUND_NOTIFY_CONTENT_DEFAULT;
|
||||
LogUtils.d(TAG, "buildForegroundNotification: 内容兜底完成 | title=" + title + " | content=" + content);
|
||||
|
||||
Notification.Builder builder;
|
||||
// API分级构建
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// API26+:绑定前台渠道(渠道已配置无铃声)
|
||||
builder = new Notification.Builder(mContext, CHANNEL_ID_FOREGROUND);
|
||||
LogUtils.d(TAG, "buildForegroundNotification: 使用API26+渠道构建");
|
||||
} else {
|
||||
// API<26:直接构建,手动禁用铃声振动
|
||||
builder = new Notification.Builder(mContext);
|
||||
builder.setSound(null);
|
||||
builder.setVibrate(new long[]{0});
|
||||
builder.setDefaults(0);
|
||||
LogUtils.d(TAG, "buildForegroundNotification: 使用API<26手动配置");
|
||||
}
|
||||
|
||||
// 通用配置
|
||||
builder.setSmallIcon(NOTIFICATION_DEFAULT_ICON)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(true) // 不可手动关闭
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentIntent(createJumpPendingIntent(mContext, PENDING_INTENT_REQUEST_CODE_FOREGROUND));
|
||||
|
||||
// API21+ 新增大图标+主题色
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setLargeIcon(getAppIcon(mContext))
|
||||
.setColor(mContext.getResources().getColor(R.color.colorPrimary))
|
||||
.setPriority(Notification.PRIORITY_LOW);
|
||||
LogUtils.d(TAG, "buildForegroundNotification: 补充API21+配置");
|
||||
}
|
||||
|
||||
Notification notification = builder.build();
|
||||
LogUtils.d(TAG, "buildForegroundNotification: 成功构建前台通知");
|
||||
return notification;
|
||||
}
|
||||
|
||||
// ================================== 内部辅助方法(通知构建:电池提醒通知)=================================
|
||||
/**
|
||||
* 构建电池提醒通知(全版本系统默认铃声+无振动)
|
||||
*/
|
||||
private Notification buildRemindNotification(Context context, NotificationMessage message) {
|
||||
LogUtils.d(TAG, "buildRemindNotification: 执行 | context=" + context + " | message=" + message);
|
||||
if (context == null || message == null) {
|
||||
LogUtils.e(TAG, "buildRemindNotification: 失败:param is null | context=" + context + " | message=" + message);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 内容兜底
|
||||
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : REMIND_NOTIFY_TITLE_DEFAULT;
|
||||
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : REMIND_NOTIFY_CONTENT_DEFAULT;
|
||||
LogUtils.d(TAG, "buildRemindNotification: 内容兜底完成 | title=" + title + " | content=" + content);
|
||||
|
||||
Notification.Builder builder;
|
||||
// API分级构建
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// API26+:绑定提醒渠道(渠道已配置默认铃声)
|
||||
builder = new Notification.Builder(context, CHANNEL_ID_REMIND);
|
||||
LogUtils.d(TAG, "buildRemindNotification: 使用API26+渠道构建");
|
||||
} else {
|
||||
// API<26:手动配置默认铃声,关闭振动
|
||||
builder = new Notification.Builder(context);
|
||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI) // 显式默认铃声
|
||||
.setVibrate(new long[]{0})
|
||||
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND);
|
||||
LogUtils.d(TAG, "buildRemindNotification: 使用API<26手动配置");
|
||||
}
|
||||
|
||||
// 通用配置
|
||||
builder.setSmallIcon(NOTIFICATION_DEFAULT_ICON)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), Notification.AUDIO_ATTRIBUTES_DEFAULT)
|
||||
.setAutoCancel(true) // 点击关闭
|
||||
.setOngoing(false)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentIntent(createJumpPendingIntent(context, PENDING_INTENT_REQUEST_CODE_REMIND));
|
||||
|
||||
// API21+ 新增大图标+主题色
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setLargeIcon(getAppIcon(context))
|
||||
.setColor(context.getResources().getColor(R.color.colorPrimary))
|
||||
.setPriority(Notification.PRIORITY_DEFAULT);
|
||||
LogUtils.d(TAG, "buildRemindNotification: 补充API21+配置");
|
||||
}
|
||||
|
||||
Notification notification = builder.build();
|
||||
LogUtils.d(TAG, "buildRemindNotification: 成功构建提醒通知");
|
||||
return notification;
|
||||
}
|
||||
|
||||
// ================================== 内部辅助方法(通知构建:应用配置信息通知)=================================
|
||||
/**
|
||||
* 构建应用配置信息通知(新增:全版本无铃声+无振动,低优先级)
|
||||
*/
|
||||
private Notification buildConfigNotification(Context context, NotificationMessage message) {
|
||||
LogUtils.d(TAG, "buildConfigNotification: 执行 | context=" + context + " | message=" + message);
|
||||
if (context == null || message == null) {
|
||||
LogUtils.e(TAG, "buildConfigNotification: 失败:param is null | context=" + context + " | message=" + message);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 内容兜底
|
||||
String title = message.getTitle() != null && !message.getTitle().isEmpty() ? message.getTitle() : CONFIG_NOTIFY_TITLE_DEFAULT;
|
||||
String content = message.getContent() != null && !message.getContent().isEmpty() ? message.getContent() : CONFIG_NOTIFY_CONTENT_DEFAULT;
|
||||
LogUtils.d(TAG, "buildConfigNotification: 内容兜底完成 | title=" + title + " | content=" + content);
|
||||
|
||||
Notification.Builder builder;
|
||||
// API分级构建
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// API26+:绑定配置渠道(渠道已配置无铃声)
|
||||
builder = new Notification.Builder(context, CHANNEL_ID_CONFIG);
|
||||
LogUtils.d(TAG, "buildConfigNotification: 使用API26+渠道构建");
|
||||
} else {
|
||||
// API<26:直接构建,手动禁用铃声振动
|
||||
builder = new Notification.Builder(context);
|
||||
builder.setSound(null);
|
||||
builder.setVibrate(new long[]{0});
|
||||
builder.setDefaults(0);
|
||||
LogUtils.d(TAG, "buildConfigNotification: 使用API<26手动配置");
|
||||
}
|
||||
|
||||
// 通用配置
|
||||
builder.setSmallIcon(NOTIFICATION_DEFAULT_ICON)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setAutoCancel(true) // 点击关闭
|
||||
.setOngoing(false)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentIntent(createJumpPendingIntent(context, PENDING_INTENT_REQUEST_CODE_CONFIG));
|
||||
|
||||
// API21+ 新增大图标+主题色
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setLargeIcon(getAppIcon(context))
|
||||
.setColor(context.getResources().getColor(R.color.colorPrimary))
|
||||
.setPriority(Notification.PRIORITY_MIN); // 最低优先级
|
||||
LogUtils.d(TAG, "buildConfigNotification: 补充API21+配置");
|
||||
}
|
||||
|
||||
Notification notification = builder.build();
|
||||
LogUtils.d(TAG, "buildConfigNotification: 成功构建配置信息通知");
|
||||
return notification;
|
||||
}
|
||||
|
||||
// ================================== 内部辅助方法(创建跳转PendingIntent,API30安全适配)=================================
|
||||
/**
|
||||
* 创建跳转MainActivity的PendingIntent,API23+ 添加IMMUTABLE标记(避免安全异常)
|
||||
*/
|
||||
private PendingIntent createJumpPendingIntent(Context context, int requestCode) {
|
||||
LogUtils.d(TAG, "createJumpPendingIntent: 执行 | requestCode=" + requestCode + " | context=" + context);
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
LogUtils.d(TAG, "createJumpPendingIntent: 跳转Intent配置完成");
|
||||
|
||||
// API23+ 必需添加IMMUTABLE,适配API30安全规范
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
LogUtils.d(TAG, "createJumpPendingIntent: 添加FLAG_IMMUTABLE标记(API23+)");
|
||||
}
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, flags);
|
||||
LogUtils.d(TAG, "createJumpPendingIntent: 成功 | requestCode=" + requestCode);
|
||||
return pendingIntent;
|
||||
}
|
||||
|
||||
// ================================== 内部辅助方法(获取APP图标,异常兜底)=================================
|
||||
/**
|
||||
* 获取APP图标,失败返回默认图标
|
||||
*/
|
||||
private Bitmap getAppIcon(Context context) {
|
||||
LogUtils.d(TAG, "getAppIcon: 执行 | context=" + context);
|
||||
try {
|
||||
PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
Bitmap appIcon = BitmapFactory.decodeResource(context.getResources(), pkgInfo.applicationInfo.icon);
|
||||
LogUtils.d(TAG, "getAppIcon: 成功:获取应用图标");
|
||||
return appIcon;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.e(TAG, "getAppIcon: 异常:获取应用图标失败,使用默认图标", e);
|
||||
return BitmapFactory.decodeResource(context.getResources(), NOTIFICATION_DEFAULT_ICON);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================== 资源释放方法(避免内存泄漏)=================================
|
||||
/**
|
||||
* 释放资源,销毁时调用
|
||||
*/
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release: 执行资源释放");
|
||||
cancelForegroundServiceNotify();
|
||||
mNotificationManager = null;
|
||||
mContext = null;
|
||||
LogUtils.d(TAG, "release: 成功:所有资源已释放");
|
||||
}
|
||||
|
||||
// ================================== 对外 getter 方法(仅前台通知实例,只读)=================================
|
||||
public Notification getForegroundServiceNotify() {
|
||||
return mForegroundServiceNotify;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/*
|
||||
* 参考:
|
||||
* https://blog.csdn.net/qq_35507234/article/details/90676587
|
||||
* https://blog.csdn.net/qq_16628781/article/details/51548324
|
||||
*/
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.media.RingtoneManager;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.model.NotificationMessage;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
|
||||
public class NotificationUtils2 {
|
||||
|
||||
public static final String TAG = NotificationHelper.class.getSimpleName();
|
||||
|
||||
Context mContext;
|
||||
NotificationManager mNotificationManager;
|
||||
|
||||
Notification mForegroundNotification;
|
||||
PendingIntent mForegroundPendingIntent;
|
||||
Notification mRemindNotification;
|
||||
PendingIntent mRemindPendingIntent;
|
||||
RemoteViews mrvServiceNotificationView;
|
||||
RemoteViews mrvRemindNotificationView;
|
||||
|
||||
static enum NotificationType { MIN, MAX };
|
||||
private static int _mnServiceNotificationID = 1;
|
||||
private static int _mnRemindNotificationID = 2;
|
||||
private static String _mszChannelIDService = "1";
|
||||
private static String _mszChannelNameService = "Service";
|
||||
private static String _mszChannelIDRemind = "2";
|
||||
private static String _mszChannelNameRemind = "Remind";
|
||||
|
||||
// public NotificationUtils(Context context) {
|
||||
// mContext = context;
|
||||
// mNotificationManager = (NotificationManager) context.getSystemService(
|
||||
// Context.NOTIFICATION_SERVICE);
|
||||
// }
|
||||
|
||||
public NotificationUtils2(Context context) {
|
||||
mContext = context;
|
||||
mNotificationManager = context.getSystemService(NotificationManager.class);
|
||||
//createNotificationChannels();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public void createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createServiceChannel();
|
||||
createRemindChannel();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createServiceChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
_mszChannelIDService,
|
||||
_mszChannelNameService,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Background service updates");
|
||||
channel.setSound(null, null);
|
||||
channel.enableVibration(false);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void createRemindChannel() {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
_mszChannelIDRemind,
|
||||
_mszChannelNameRemind,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription("Critical reminders");
|
||||
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null);
|
||||
channel.enableVibration(true);
|
||||
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
|
||||
channel.setBypassDnd(true);
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
// 创建并发送服务通知
|
||||
//
|
||||
public void createForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
//创建Notification,传入Context和channelId
|
||||
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
|
||||
intent.setPackage(service.getPackageName());
|
||||
//LogUtils.d(TAG, "mService.getPackageName() : " + service.getPackageName());
|
||||
intent.setClass(service, MainActivity.class);
|
||||
//LogUtils.d(TAG, "MainActivity.class.getName() : " + MainActivity.class.getName());
|
||||
//这里放一个count用来区分每一个通知
|
||||
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
|
||||
|
||||
//参数1:context 上下文对象
|
||||
//参数2:发送者私有的请求码(Private request code for the sender)
|
||||
//参数3:intent 意图对象
|
||||
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
|
||||
//mForegroundPendingIntent = PendingIntent.getActivity(mService, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mForegroundPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
} else {
|
||||
mForegroundPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
mForegroundNotification = new Notification.Builder(service, _mszChannelIDService)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(notificationMessage.getTitle())
|
||||
.setContentText(notificationMessage.getContent())
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
//设置红色
|
||||
.setColor(Color.parseColor("#F00606"))
|
||||
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
|
||||
.setContentIntent(mForegroundPendingIntent)
|
||||
.build();
|
||||
|
||||
setForegroundNotificationRemoteViews(service, notificationMessage);
|
||||
service.startForeground(_mnServiceNotificationID, mForegroundNotification);
|
||||
}
|
||||
|
||||
void initmrvRemindNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
mrvRemindNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
|
||||
mrvRemindNotificationView.setTextViewText(R.id.viewremindnotificationTextView1, notificationMessage.getTitle());
|
||||
String szRemindMSG = notificationMessage.getRemindMSG();
|
||||
//LogUtils.d(TAG, "szRemindMSG : " + szRemindMSG);
|
||||
//mrvRemindNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
|
||||
if (szRemindMSG != null) {
|
||||
if (szRemindMSG.trim().equals("-")) {
|
||||
//LogUtils.d(TAG, "-");
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.GONE);
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
|
||||
} else if (szRemindMSG.trim().equals("+")) {
|
||||
//LogUtils.d(TAG, "+");
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.GONE);
|
||||
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
|
||||
}
|
||||
mrvRemindNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
|
||||
//给我remoteViews上的控件tv_content添加监听事件
|
||||
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
|
||||
//return mrvServiceNotificationView;
|
||||
}
|
||||
}
|
||||
|
||||
void initmrvServiceNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
mrvServiceNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
|
||||
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView1, notificationMessage.getTitle());
|
||||
//String szRemindMSG = notificationMessage.getRemindMSG();
|
||||
//mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
|
||||
//rvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent() + Integer.toString(nTest));
|
||||
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent());
|
||||
mrvServiceNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
|
||||
//给我remoteViews上的控件tv_content添加监听事件
|
||||
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
|
||||
//return mrvServiceNotificationView;
|
||||
}
|
||||
|
||||
void setForegroundNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
initmrvServiceNotificationView(service, notificationMessage);
|
||||
mForegroundNotification.contentView = mrvServiceNotificationView;
|
||||
mForegroundNotification.bigContentView = mrvServiceNotificationView;
|
||||
}
|
||||
|
||||
void setRemindNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
initmrvRemindNotificationView(service, notificationMessage);
|
||||
mRemindNotification.contentView = mrvRemindNotificationView;
|
||||
mRemindNotification.bigContentView = mrvRemindNotificationView;
|
||||
}
|
||||
|
||||
// 更新服务通知
|
||||
//
|
||||
public void updateForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
setForegroundNotificationRemoteViews(service, notificationMessage);
|
||||
mNotificationManager.notify(_mnServiceNotificationID, mForegroundNotification);
|
||||
|
||||
}
|
||||
|
||||
// 创建并发送电量提醒通知
|
||||
//
|
||||
public void updateRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
//LogUtils.d(TAG, "updateRemindNotification : " + notificationMessage.getRemindMSG());
|
||||
setRemindNotificationRemoteViews(service, notificationMessage);
|
||||
mNotificationManager.notify(_mnRemindNotificationID, mRemindNotification);
|
||||
}
|
||||
|
||||
public void createRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
|
||||
//LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
|
||||
//创建Notification,传入Context和channelId
|
||||
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
|
||||
intent.setPackage(service.getPackageName());
|
||||
intent.setClass(service, MainActivity.class);
|
||||
//这里放一个count用来区分每一个通知
|
||||
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
|
||||
|
||||
//参数1:context 上下文对象
|
||||
//参数2:发送者私有的请求码(Private request code for the sender)
|
||||
//参数3:intent 意图对象
|
||||
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
|
||||
//mRemindPendingIntent = PendingIntent.getActivity(mService, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mRemindPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
} else {
|
||||
mRemindPendingIntent = PendingIntent.getActivity(service,
|
||||
1, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
mRemindNotification = new Notification.Builder(service, _mszChannelIDRemind)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(notificationMessage.getTitle())
|
||||
.setContentText(notificationMessage.getContent())
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
//设置红色
|
||||
.setColor(Color.parseColor("#F00606"))
|
||||
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
|
||||
.setContentIntent(mRemindPendingIntent)
|
||||
.build();
|
||||
setRemindNotificationRemoteViews(service, notificationMessage);
|
||||
}
|
||||
|
||||
public static void cancelRemindNotification(Context context){
|
||||
// 获取 NotificationManager 实例
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
// 撤回指定 ID 的通知栏消息
|
||||
notificationManager.cancel(_mnRemindNotificationID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,213 +2,349 @@ package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ComponentName;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.MainActivity;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/01 16:05
|
||||
* @Describe 权限申请工具类(单例)
|
||||
* 核心特性:
|
||||
* 1. 适配全Android版本(6.0+ 动态权限 / 13+ 兼容)
|
||||
* 2. 支持多包名场景(无硬编码包名)
|
||||
* 3. 统一权限校验、申请、回调处理
|
||||
* 4. 自带用户引导(拒绝权限+不再询问场景)
|
||||
* @Date 2025/12/14 03:05
|
||||
* @Describe 权限申请工具类(Java7兼容版)
|
||||
* 适配 小米手机+API29-30,整合自启动、电池优化、全文件管理权限,专注后台保活核心权限
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
public static final String TAG = "PermissionUtils";
|
||||
// 存储权限请求码(与Activity保持一致,避免冲突)
|
||||
public static final int STORAGE_PERMISSION_REQUEST2 = 100;
|
||||
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
|
||||
// ====================== 常量定义(首屏可见,统一管理,避免冲突)======================
|
||||
// 日志标签
|
||||
public static final String TAG = "PermissionUtils";
|
||||
// 权限请求码(按场景分段,避免重复)
|
||||
public static final int REQUEST_IGNORE_BATTERY_OPTIMIZATION = 1000; // 电池优化权限
|
||||
public static final int REQUEST_AUTO_START = 1001; // 自启动权限(小米专属)
|
||||
public static final int REQUEST_ALL_FILE_MANAGE = 1002; // 全文件管理权限(API30+)
|
||||
// SDK版本常量(适配API29-30,替代系统枚举,Java7兼容)
|
||||
private static final int SDK_VERSION_Q = 29; // Android 10(API29)
|
||||
private static final int SDK_VERSION_R = 30; // Android 11(API30)
|
||||
// 小米自启动权限页面配置(专属跳转路径,精准适配)
|
||||
private static final String XIAOMI_AUTO_START_PACKAGE = "com.miui.securitycenter";
|
||||
private static final String XIAOMI_AUTO_START_CLASS = "com.miui.permcenter.autostart.AutoStartManagementActivity";
|
||||
|
||||
// 单例实例(双重校验锁,线程安全)
|
||||
private static volatile PermissionUtils sInstance;
|
||||
// ====================== 单例模式(Java7标准双重校验锁,线程安全+懒加载)======================
|
||||
private static volatile PermissionUtils sInstance;
|
||||
|
||||
// 私有构造(禁止外部实例化)
|
||||
private PermissionUtils() {}
|
||||
private PermissionUtils() {}
|
||||
|
||||
/**
|
||||
* 获取单例实例(适配多包名,无硬编码)
|
||||
*/
|
||||
public static PermissionUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (PermissionUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PermissionUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
public static PermissionUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (PermissionUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PermissionUtils();
|
||||
LogUtils.d(TAG, "初始化:PermissionUtils 单例创建成功");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// ======================================== 存储权限核心方法 ========================================
|
||||
/**
|
||||
* 检查并申请存储权限(统一入口,适配全Android版本)
|
||||
* @param activity 上下文(用于权限申请和弹窗)
|
||||
* @return true:权限已全部获取;false:需要申请权限
|
||||
*/
|
||||
public boolean checkAndRequestStoragePermission(Activity activity) {
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁,权限检查失败");
|
||||
return false;
|
||||
}
|
||||
LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT);
|
||||
// ====================== 核心权限1:全文件管理权限(API29-30适配,通用所有机型)======================
|
||||
/**
|
||||
* 检查全文件管理权限(适配API30+ MANAGE_EXTERNAL_STORAGE,兼容API29-旧权限)
|
||||
* @param activity 上下文Activity(不可为null)
|
||||
* @return true=权限已授予,false=权限未授予
|
||||
*/
|
||||
public boolean checkAllFileManagePermission(Activity activity) {
|
||||
LogUtils.d(TAG, "全文件权限-检查:开始校验,系统版本=" + Build.VERSION.SDK_INT);
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "全文件权限-检查:失败,Activity为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号)
|
||||
String[] requiredPermissions = {
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
};
|
||||
// API30+:校验 MANAGE_EXTERNAL_STORAGE 特殊权限
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
|
||||
boolean hasManagePerm = Environment.isExternalStorageManager();
|
||||
LogUtils.d(TAG, "全文件权限-检查:API30+,MANAGE_EXTERNAL_STORAGE权限=" + (hasManagePerm ? "已授予" : "未授予"));
|
||||
return hasManagePerm;
|
||||
} else if (Build.VERSION.SDK_INT == SDK_VERSION_Q) {
|
||||
LogUtils.d(TAG, "全文件权限-检查:API29,无需申请,默认支持文件管理");
|
||||
return true;
|
||||
} else {
|
||||
boolean hasWritePerm = ContextCompat.checkSelfPermission(activity,
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||
LogUtils.d(TAG, "全文件权限-检查:API29以下,WRITE_EXTERNAL_STORAGE权限=" + (hasWritePerm ? "已授予" : "未授予"));
|
||||
return hasWritePerm;
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选未授予的权限
|
||||
List<String> needPermissions = new ArrayList<>();
|
||||
for (String permission : requiredPermissions) {
|
||||
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
needPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 申请全文件管理权限(适配API30+特殊权限流程,兼容API29-旧权限申请)
|
||||
* @param activity 申请权限的Activity(不可为null)
|
||||
*/
|
||||
public void requestAllFileManagePermission(Activity activity) {
|
||||
LogUtils.d(TAG, "全文件权限-申请:开始处理,系统版本=" + Build.VERSION.SDK_INT);
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "全文件权限-申请:失败,Activity无效/已销毁");
|
||||
return;
|
||||
}
|
||||
|
||||
// 无权限需要申请:触发动态申请
|
||||
if (!needPermissions.isEmpty()) {
|
||||
String[] permissionsArr = needPermissions.toArray(new String[0]);
|
||||
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2);
|
||||
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr));
|
||||
return false;
|
||||
}
|
||||
// 先检查权限,已授予直接返回
|
||||
if (checkAllFileManagePermission(activity)) {
|
||||
LogUtils.d(TAG, "全文件权限-申请:已拥有权限,无需发起");
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有权限已授予
|
||||
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
|
||||
return true;
|
||||
}
|
||||
// API30+:跳转系统特殊权限申请页(用户手动授权)
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
intent.setData(Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
|
||||
LogUtils.d(TAG, "全文件权限-申请:API30+,跳转特殊权限申请页");
|
||||
} catch (Exception e) {
|
||||
// 备用跳转:系统设置首页,引导手动操作
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
|
||||
LogUtils.w(TAG, "全文件权限-申请:跳转失败,引导手动开启");
|
||||
showAllFileManageTipsDialog(activity);
|
||||
}
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
REQUEST_ALL_FILE_MANAGE);
|
||||
LogUtils.d(TAG, "全文件权限-申请:API29以下,发起WRITE_EXTERNAL_STORAGE权限申请");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理存储权限申请回调(统一逻辑,无需在Activity中重复编写)
|
||||
* @param activity 上下文
|
||||
* @param requestCode 请求码(匹配STORAGE_PERMISSION_REQUEST)
|
||||
* @param permissions 申请的权限数组
|
||||
* @param grantResults 权限授予结果数组
|
||||
* @return true:回调已处理;false:非当前工具类的权限回调
|
||||
*/
|
||||
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
|
||||
// 过滤非存储权限回调
|
||||
if (requestCode != STORAGE_PERMISSION_REQUEST2) {
|
||||
return false;
|
||||
}
|
||||
LogUtils.d(TAG, "【权限回调】处理存储权限回调,requestCode:" + requestCode);
|
||||
// ====================== 核心权限2:自启动权限(小米专属,API29-30适配)======================
|
||||
/**
|
||||
* 检查自启动权限(仅小米机型需要,非小米直接返回无需申请)
|
||||
* @param activity 上下文Activity(不可为null)
|
||||
* @return true=小米机型(需手动开启);false=非小米机型(无需申请)
|
||||
*/
|
||||
// public boolean checkAutoStartPermission(Activity activity) {
|
||||
// LogUtils.d(TAG, "自启动权限-检查:开始,设备品牌=" + Build.BRAND);
|
||||
// if (activity == null) {
|
||||
// LogUtils.e(TAG, "自启动权限-检查:失败,Activity为空");
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// boolean isXiaomi = Build.BRAND.toLowerCase().contains("xiaomi");
|
||||
// LogUtils.d(TAG, "自启动权限-检查:结果=" + (isXiaomi ? "小米机型(需开启)" : "非小米机型(无需申请)"));
|
||||
// return isXiaomi;
|
||||
// }
|
||||
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【权限回调】Activity为空或已销毁,回调处理终止");
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 请求自启动权限(小米专属,多方案跳转,适配API29-30机型差异)
|
||||
* @param activity 申请权限的Activity(不可为null)
|
||||
*/
|
||||
public void requestAutoStartPermission(Activity activity) {
|
||||
LogUtils.d(TAG, "自启动权限-申请:开始处理");
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "自启动权限-申请:失败,Activity无效/已销毁");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验所有权限是否授予
|
||||
boolean allGranted = true;
|
||||
for (int grantResult : grantResults) {
|
||||
if (grantResult != PackageManager.PERMISSION_GRANTED) {
|
||||
allGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 非小米机型,直接返回
|
||||
// if (!checkAutoStartPermission(activity)) {
|
||||
// LogUtils.d(TAG, "自启动权限-申请:非小米机型,无需处理");
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (allGranted) {
|
||||
// 全部授予:提示用户重新操作
|
||||
ToastUtils.show(activity.getString(R.string.permission_grant_success));
|
||||
LogUtils.d(TAG, "【权限回调】所有存储权限已授予");
|
||||
} else {
|
||||
// 部分/全部拒绝:判断是否勾选“不再询问”
|
||||
boolean shouldShowRationale = false;
|
||||
for (String permission : permissions) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
|
||||
shouldShowRationale = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// API30+ 小米:优先精准跳转自启动管理页
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
|
||||
try {
|
||||
// 方案1:组件名精准跳转(成功率最高)
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(new ComponentName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS));
|
||||
activity.startActivityForResult(intent, REQUEST_AUTO_START);
|
||||
LogUtils.d(TAG, "自启动权限-申请:API30+,组件名跳转自启动管理页");
|
||||
} catch (Exception e1) {
|
||||
try {
|
||||
// 方案2:Action备用跳转(兼容机型差异)
|
||||
Intent intent = new Intent("miui.intent.action.OP_AUTO_START");
|
||||
intent.setClassName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS);
|
||||
activity.startActivityForResult(intent, REQUEST_AUTO_START);
|
||||
LogUtils.d(TAG, "自启动权限-申请:API30+,Action跳转自启动管理页");
|
||||
} catch (Exception e2) {
|
||||
// 方案3:终极备用,跳转系统设置+提示
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivityForResult(intent, REQUEST_AUTO_START);
|
||||
LogUtils.w(TAG, "自启动权限-申请:跳转失败,引导手动操作");
|
||||
showAutoStartTipsDialog(activity);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldShowRationale) {
|
||||
// 未勾选“不再询问”:弹窗引导重新申请
|
||||
showPermissionRationaleDialog(activity);
|
||||
} else {
|
||||
// 已勾选“不再询问”:引导用户去设置页开启
|
||||
showPermissionSettingDialog(activity);
|
||||
}
|
||||
LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// API29 小米:低版本兼容跳转
|
||||
try {
|
||||
Intent intent = new Intent(XIAOMI_AUTO_START_CLASS);
|
||||
intent.setPackage(XIAOMI_AUTO_START_PACKAGE);
|
||||
activity.startActivityForResult(intent, REQUEST_AUTO_START);
|
||||
LogUtils.d(TAG, "自启动权限-申请:API29,低版本跳转自启动管理页");
|
||||
} catch (Exception e) {
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivityForResult(intent, REQUEST_AUTO_START);
|
||||
showAutoStartTipsDialog(activity);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================== 辅助方法(私有,封装细节) ========================================
|
||||
/**
|
||||
* 弹窗:未勾选“不再询问”时,提示用户授予权限
|
||||
*/
|
||||
private void showPermissionRationaleDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.permission_title))
|
||||
.setMessage(activity.getString(R.string.permission_storage_rationale))
|
||||
.setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() {
|
||||
// ====================== 核心权限3:电池优化权限(通用所有机型,API29-30适配)======================
|
||||
/**
|
||||
* 检查忽略电池优化权限(精准判断,API23+有效,低版本视为已拥有)
|
||||
* @param activity 上下文Activity(不可为null)
|
||||
* @return true=已忽略优化;false=未忽略(需申请)
|
||||
*/
|
||||
public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) {
|
||||
LogUtils.d(TAG, "电池优化权限-检查:开始,系统版本=" + Build.VERSION.SDK_INT);
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "电池优化权限-检查:失败,Activity为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// API23以下无此权限,视为已拥有
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.d(TAG, "电池优化权限-检查:API23以下,无需校验,视为已拥有");
|
||||
return true;
|
||||
}
|
||||
|
||||
// API23+ 精准校验权限状态
|
||||
PowerManager powerManager = (PowerManager) activity.getSystemService(Activity.POWER_SERVICE);
|
||||
if (powerManager == null) {
|
||||
LogUtils.e(TAG, "电池优化权限-检查:获取PowerManager失败,校验异常");
|
||||
return false;
|
||||
}
|
||||
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());
|
||||
LogUtils.d(TAG, "电池优化权限-检查:结果=" + (isIgnored ? "已忽略优化" : "未忽略(需申请)"));
|
||||
return isIgnored;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求忽略电池优化权限(多方案跳转,适配API29-30,自动判断是否需要申请)
|
||||
* @param activity 申请权限的Activity(不可为null)
|
||||
*/
|
||||
public void requestIgnoreBatteryOptimizationPermission(Activity activity) {
|
||||
LogUtils.d(TAG, "电池优化权限-申请:开始处理");
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "电池优化权限-申请:失败,Activity无效/已销毁");
|
||||
return;
|
||||
}
|
||||
|
||||
// 已拥有权限,直接返回
|
||||
if (checkIgnoreBatteryOptimizationPermission(activity)) {
|
||||
LogUtils.d(TAG, "电池优化权限-申请:已拥有权限,无需发起");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 方案1:直接跳转一键授权页(优先使用,用户操作简单)
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
|
||||
LogUtils.d(TAG, "电池优化权限-申请:跳转一键授权页");
|
||||
} catch (Exception e) {
|
||||
// 方案2:备用跳转优化管理页+提示
|
||||
Intent intent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
|
||||
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
|
||||
LogUtils.w(TAG, "电池优化权限-申请:跳转失败,引导手动操作");
|
||||
showBatteryOptTipsDialog(activity);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 辅助方法:手动开启提示弹窗(适配跳转失败场景)======================
|
||||
/**
|
||||
* 全文件管理权限手动开启提示弹窗
|
||||
*/
|
||||
private void showAllFileManageTipsDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("全文件管理权限申请提示")
|
||||
.setMessage("请手动开启全文件管理权限,否则文件操作功能异常:\n1. 进入设置 → 应用 → 本应用 → 权限\n2. 找到「文件管理」/「存储」权限,开启「允许管理所有文件」")
|
||||
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// 重新申请权限
|
||||
checkAndRequestStoragePermission(activity);
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(activity.getString(R.string.cancel), null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
LogUtils.d(TAG, "全文件权限:显示手动开启提示弹窗");
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限
|
||||
*/
|
||||
private void showPermissionSettingDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.permission_denied_title))
|
||||
.setMessage(activity.getString(R.string.permission_storage_setting_guide))
|
||||
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() {
|
||||
/**
|
||||
* 自启动权限手动开启提示弹窗(小米专属)
|
||||
*/
|
||||
private void showAutoStartTipsDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("自启动权限申请提示")
|
||||
.setMessage("请手动开启自启动权限,否则应用后台保活异常:\n1. 进入小米安全中心 → 应用管理 → 自启动管理\n2. 找到本应用,开启「允许自启动」开关")
|
||||
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// 跳转应用设置页(适配多包名,动态获取当前包名)
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(activity.getString(R.string.cancel), null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
LogUtils.d(TAG, "自启动权限:显示手动开启提示弹窗");
|
||||
}
|
||||
|
||||
// ======================================== 扩展:其他权限方法(可选) ========================================
|
||||
/**
|
||||
* 检查单个权限是否已授予(通用方法,可复用)
|
||||
*/
|
||||
public boolean isPermissionGranted(Activity activity, String permission) {
|
||||
if (activity == null || TextUtils.isEmpty(permission)) {
|
||||
return false;
|
||||
}
|
||||
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
/**
|
||||
* 电池优化权限手动开启提示弹窗
|
||||
*/
|
||||
private void showBatteryOptTipsDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("电池优化权限申请提示")
|
||||
.setMessage("请手动忽略电池优化,否则应用后台运行被限制:\n1. 进入设置 → 电池 → 电池优化\n2. 找到本应用,选择「不优化」选项")
|
||||
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
LogUtils.d(TAG, "电池优化权限:显示手动开启提示弹窗");
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请单个权限(通用方法,可复用)
|
||||
*/
|
||||
public void requestSinglePermission(Activity activity, String permission, int requestCode) {
|
||||
if (activity == null || TextUtils.isEmpty(permission)) {
|
||||
return;
|
||||
public void startPermissionRequest(final Activity activity) {
|
||||
// 电池优化权限(通用所有机型)
|
||||
if (!checkIgnoreBatteryOptimizationPermission(activity)) {
|
||||
YesNoAlertDialog.show(activity, activity.getString(R.string.app_name) + "权限申请提示:", "本应用要正常使用,需要申请电池优化与自启动权限。是否进入权限设置步骤?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
@Override
|
||||
public void onNo() {
|
||||
ToastUtils.show(activity.getString(R.string.app_name) + "应用可能无法正常使用。");
|
||||
}
|
||||
@Override
|
||||
public void onYes() {
|
||||
requestIgnoreBatteryOptimizationPermission(activity);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!isPermissionGranted(activity, permission)) {
|
||||
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
|
||||
}
|
||||
|
||||
public void handlePermissionRequest(final Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == PermissionUtils.REQUEST_IGNORE_BATTERY_OPTIMIZATION) {
|
||||
// 自启动权限(小米专属)
|
||||
// 小米机型,发起自启动权限申请
|
||||
requestAutoStartPermission(activity);
|
||||
} else if (requestCode == PermissionUtils.REQUEST_AUTO_START) {
|
||||
// 自启动权限(小米专属)
|
||||
if (App.isDebugging() && !checkAllFileManagePermission(activity)) {
|
||||
// 小米机型,发起自启动权限申请
|
||||
requestAllFileManagePermission(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
|
||||
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class StringUtils {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/28 04:23:04
|
||||
* @Describe UriUtil
|
||||
*/
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.core.content.FileProvider;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class UriUtil {
|
||||
|
||||
public static final String TAG = "UriUtil";
|
||||
|
||||
/**
|
||||
* 获取真实路径
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public static String getFilePathFromUri(Context context, Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
switch (uri.getScheme()) {
|
||||
case ContentResolver.SCHEME_CONTENT:
|
||||
//Android7.0之后的uri content:// URI
|
||||
return getFilePathFromContentUri(context, uri);
|
||||
case ContentResolver.SCHEME_FILE:
|
||||
default:
|
||||
//Android7.0之前的uri file://
|
||||
return new File(uri.getPath()).getAbsolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从uri获取path
|
||||
*
|
||||
* @param uri content://media/external/file/109009
|
||||
* <p>
|
||||
* FileProvider适配
|
||||
* content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/
|
||||
* content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/
|
||||
*/
|
||||
private static String getFilePathFromContentUri(Context context, Uri uri) {
|
||||
if (null == uri) return null;
|
||||
String data = null;
|
||||
|
||||
String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
|
||||
Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null);
|
||||
if (null != cursor) {
|
||||
if (cursor.moveToFirst()) {
|
||||
int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
|
||||
if (index > -1) {
|
||||
data = cursor.getString(index);
|
||||
} else {
|
||||
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
|
||||
String fileName = cursor.getString(nameIndex);
|
||||
data = getPathFromInputStreamUri(context, uri, fileName);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用流拷贝文件一份到自己APP私有目录下
|
||||
*
|
||||
* @param context
|
||||
* @param uri
|
||||
* @param fileName
|
||||
*/
|
||||
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
|
||||
InputStream inputStream = null;
|
||||
String filePath = null;
|
||||
|
||||
if (uri.getAuthority() != null) {
|
||||
try {
|
||||
inputStream = context.getContentResolver().openInputStream(uri);
|
||||
File file = createTemporalFileFrom(context, inputStream, fileName);
|
||||
filePath = file.getPath();
|
||||
|
||||
} catch (Exception e) {
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public static Uri getUriForFile(Context context, String filePath) {
|
||||
File file = new File(filePath);
|
||||
return getUriForFile(context, file);
|
||||
}
|
||||
|
||||
public static Uri getUriForFile(Context context, File file) {
|
||||
//Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
||||
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
|
||||
return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
||||
}
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
|
||||
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
|
||||
throws IOException {
|
||||
File targetFile = null;
|
||||
|
||||
if (inputStream != null) {
|
||||
int read;
|
||||
byte[] buffer = new byte[8 * 1024];
|
||||
//自己定义拷贝文件路径
|
||||
targetFile = new File(context.getExternalCacheDir(), fileName);
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete();
|
||||
}
|
||||
OutputStream outputStream = new FileOutputStream(targetFile);
|
||||
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, read);
|
||||
}
|
||||
outputStream.flush();
|
||||
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.core.content.FileProvider;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Uri 工具类(Java7兼容,适配API29-30+小米机型,FileProvider安全适配)
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/06/28
|
||||
*/
|
||||
public class UriUtils {
|
||||
// ====================== 常量定义(顶部统一管理)======================
|
||||
public static final String TAG = "UriUtils";
|
||||
// FileProvider 授权后缀(与Manifest配置保持一致)
|
||||
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
|
||||
// 应用公共图片目录(API29+ 适配,替代废弃API)
|
||||
private static final String APP_PUBLIC_PIC_DIR = "PowerBell/";
|
||||
// MIME类型与文件后缀映射表(覆盖常见格式,小米机型精准匹配)
|
||||
private static final Map<String, String> MIME_SUFFIX_MAP = new HashMap<String, String>() {{
|
||||
// 图片格式(重点,含透明格式)
|
||||
put("image/png", "png");
|
||||
put("image/jpeg", "jpg");
|
||||
put("image/jpg", "jpg");
|
||||
put("image/gif", "gif");
|
||||
put("image/bmp", "bmp");
|
||||
put("image/webp", "webp");
|
||||
// 音视频格式
|
||||
put("video/mp4", "mp4");
|
||||
put("video/avi", "avi");
|
||||
put("video/mkv", "mkv");
|
||||
put("audio/mp3", "mp3");
|
||||
put("audio/wav", "wav");
|
||||
// 文档格式
|
||||
put("application/pdf", "pdf");
|
||||
put("application/msword", "doc");
|
||||
put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
|
||||
put("application/vnd.ms-excel", "xls");
|
||||
put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
|
||||
}};
|
||||
|
||||
// ====================== 新增核心方法:Uri 转文件后缀 ======================
|
||||
/**
|
||||
* 【静态公共方法】根据 Uri 获取文件真实后缀(优先MIME类型匹配,适配所有Uri场景+小米机型)
|
||||
* @param context 上下文(非空,用于获取ContentResolver)
|
||||
* @param uri 待解析 Uri(支持 content:// / file:// 双Scheme)
|
||||
* @return 小写文件后缀(如 png/jpg/mp4,无匹配返回空字符串)
|
||||
*/
|
||||
public static String getSuffixFromUri(Context context, Uri uri) {
|
||||
LogUtils.d(TAG, "=== getSuffixFromUri 调用 start,Uri:" + (uri != null ? uri.toString() : "null") + " ===");
|
||||
// 1. 基础参数校验
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getSuffixFromUri:Context 为空,获取失败");
|
||||
return "";
|
||||
}
|
||||
if (uri == null) {
|
||||
LogUtils.e(TAG, "getSuffixFromUri:Uri 为空,获取失败");
|
||||
return "";
|
||||
}
|
||||
|
||||
String suffix = "";
|
||||
String scheme = uri.getScheme();
|
||||
// 2. 按 Uri Scheme 分类处理(优先精准匹配,再降级截取)
|
||||
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
|
||||
// 场景1:content:// Uri(优先通过MIME类型获取,最精准)
|
||||
suffix = getSuffixFromContentUri(context, uri);
|
||||
LogUtils.d(TAG, "getSuffixFromUri:content:// Uri,MIME匹配后缀:" + suffix);
|
||||
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
|
||||
// 场景2:file:// Uri(直接解析文件名截取后缀)
|
||||
String filePath = new File(uri.getPath()).getAbsolutePath();
|
||||
suffix = getSuffixFromFilePath(filePath);
|
||||
LogUtils.d(TAG, "getSuffixFromUri:file:// Uri,路径截取后缀:" + suffix);
|
||||
} else {
|
||||
// 场景3:未知Scheme(尝试解析Uri路径截取,兜底)
|
||||
String uriPath = uri.getPath();
|
||||
suffix = uriPath != null ? getSuffixFromFilePath(uriPath) : "";
|
||||
LogUtils.w(TAG, "getSuffixFromUri:未知Scheme=" + scheme + ",兜底截取后缀:" + suffix);
|
||||
}
|
||||
|
||||
// 3. 最终结果处理(统一小写,去空)
|
||||
suffix = suffix != null ? suffix.trim().toLowerCase() : "";
|
||||
LogUtils.d(TAG, "=== getSuffixFromUri 调用 end,最终后缀:" + suffix + " ===");
|
||||
return suffix;
|
||||
}
|
||||
|
||||
// ====================== 公有核心方法(对外提供能力,按功能排序)======================
|
||||
/**
|
||||
* Uri 转真实文件路径(核心方法,适配 content:// / file:// 双 Scheme)
|
||||
* @param context 上下文(非空)
|
||||
* @param uri 待转换 Uri(非空)
|
||||
* @return 真实文件绝对路径(转换失败返回 null)
|
||||
*/
|
||||
public static String getFilePathFromUri(Context context, Uri uri) {
|
||||
LogUtils.d(TAG, "=== getFilePathFromUri 调用 start ===");
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getFilePathFromUri:Context 为空,转换失败");
|
||||
return null;
|
||||
}
|
||||
if (uri == null) {
|
||||
LogUtils.e(TAG, "getFilePathFromUri:Uri 为空,转换失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
String filePath = null;
|
||||
// 按 Uri Scheme 分类处理
|
||||
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
|
||||
LogUtils.d(TAG, "getFilePathFromUri:Scheme=content,执行ContentUri转换");
|
||||
filePath = getFilePathFromContentUri(context, uri);
|
||||
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
|
||||
LogUtils.d(TAG, "getFilePathFromUri:Scheme=file,直接转换路径");
|
||||
filePath = new File(uri.getPath()).getAbsolutePath();
|
||||
} else {
|
||||
LogUtils.w(TAG, "getFilePathFromUri:未知Scheme=" + scheme + ",转换失败");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "=== getFilePathFromUri 调用 end,结果:" + filePath + " ===");
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件路径转 Uri(核心方法,适配 Android7.0+ FileProvider,API29-30兼容)
|
||||
* @param context 上下文(非空)
|
||||
* @param filePath 真实文件路径(非空)
|
||||
* @return 安全 Uri(转换失败返回 null)
|
||||
*/
|
||||
public static Uri getUriForFile(Context context, String filePath) {
|
||||
LogUtils.d(TAG, "=== getUriForFile(路径版)调用 start ===");
|
||||
// 1. 基础参数校验
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getUriForFile:Context 为空,转换失败");
|
||||
return null;
|
||||
}
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
LogUtils.e(TAG, "getUriForFile:文件路径为空,转换失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. File 对象初始化与校验
|
||||
File file = new File(filePath);
|
||||
LogUtils.d(TAG, "getUriForFile:文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
|
||||
if (!file.exists() || file.isDirectory()) {
|
||||
LogUtils.e(TAG, "getUriForFile:文件不存在或为目录,转换失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 合法路径校验(适配小米机型,避免FileProvider配置外路径)
|
||||
if (!isPathInValidDir(context, file)) {
|
||||
LogUtils.w(TAG, "getUriForFile:路径不在安全配置目录内,小米机型可能出现权限异常");
|
||||
}
|
||||
|
||||
// 4. 调用重载方法生成 Uri
|
||||
Uri uri = getUriForFile(context, file);
|
||||
LogUtils.d(TAG, "=== getUriForFile(路径版)调用 end,结果:" + (uri != null ? uri.toString() : "null") + " ===");
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* File 对象转 Uri(重载方法,直接接收File,内部安全适配)
|
||||
* @param context 上下文(非空)
|
||||
* @param file 待转换 File 对象(非空)
|
||||
* @return 安全 Uri(转换失败返回 null)
|
||||
*/
|
||||
public static Uri getUriForFile(Context context, File file) {
|
||||
LogUtils.d(TAG, "=== getUriForFile(File版)调用 start ===");
|
||||
// 1. 基础参数校验
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getUriForFile:Context 为空,转换失败");
|
||||
return null;
|
||||
}
|
||||
if (file == null) {
|
||||
LogUtils.e(TAG, "getUriForFile:File 对象为空,转换失败");
|
||||
return null;
|
||||
}
|
||||
LogUtils.d(TAG, "getUriForFile:文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
|
||||
if (!file.exists() || file.isDirectory()) {
|
||||
LogUtils.e(TAG, "getUriForFile:文件不存在或为目录,转换失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 按系统版本生成 Uri(API24+ 强制 FileProvider,适配小米机型)
|
||||
Uri uri = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LogUtils.d(TAG, "getUriForFile:Android7.0+,使用FileProvider生成Uri");
|
||||
String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX;
|
||||
LogUtils.d(TAG, "getUriForFile:FileProvider Authority=" + authority);
|
||||
try {
|
||||
uri = FileProvider.getUriForFile(context, authority, file);
|
||||
LogUtils.d(TAG, "getUriForFile:Content Uri生成成功=" + uri.toString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "getUriForFile:FileProvider生成失败(小米机型常见原因:路径未配置/Authority不匹配)", e);
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, "getUriForFile:Android7.0以下,使用Uri.fromFile生成");
|
||||
uri = Uri.fromFile(file);
|
||||
LogUtils.d(TAG, "getUriForFile:File Uri生成成功=" + uri.toString());
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "=== getUriForFile(File版)调用 end ===");
|
||||
return uri;
|
||||
}
|
||||
|
||||
// ====================== 私有辅助方法(内部逻辑封装,不对外暴露)======================
|
||||
/**
|
||||
* ContentUri 转真实路径(适配 content:// 格式,处理小米机型特殊Uri)
|
||||
* @param context 上下文
|
||||
* @param uri ContentUri(如:content://media/external/file/xxx)
|
||||
* @return 真实文件路径(失败返回 null)
|
||||
*/
|
||||
private static String getFilePathFromContentUri(Context context, Uri uri) {
|
||||
LogUtils.d(TAG, "getFilePathFromContentUri:Uri=" + uri.toString());
|
||||
String filePath = null;
|
||||
Cursor cursor = null;
|
||||
// Java7 语法:try-catch-finally 手动关闭Cursor,避免内存泄漏
|
||||
try {
|
||||
// 查询字段:优先 DATA 字段,失败则通过文件名+流拷贝获取
|
||||
String[] queryColumns = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
|
||||
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
// 优先读取 DATA 字段(直接获取路径)
|
||||
int dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
|
||||
if (dataIndex != -1) {
|
||||
filePath = cursor.getString(dataIndex);
|
||||
LogUtils.d(TAG, "getFilePathFromContentUri:从DATA字段获取路径=" + filePath);
|
||||
} else {
|
||||
// DATA 字段为空,通过流拷贝到私有目录获取路径(小米机型特殊场景适配)
|
||||
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
|
||||
String fileName = cursor.getString(nameIndex);
|
||||
LogUtils.d(TAG, "getFilePathFromContentUri:DATA字段为空,通过流拷贝获取,文件名=" + fileName);
|
||||
filePath = getPathFromInputStreamUri(context, uri, fileName);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getFilePathFromContentUri:查询失败", e);
|
||||
} finally {
|
||||
// 强制关闭Cursor,避免资源泄漏(Java7 必须手动处理)
|
||||
if (cursor != null) {
|
||||
try {
|
||||
cursor.close();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getFilePathFromContentUri:关闭Cursor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流拷贝获取路径(适配无 DATA 字段的 ContentUri,小米机型特殊Uri兼容)
|
||||
* 将目标文件拷贝到应用私有缓存目录,返回拷贝后的路径
|
||||
* @param context 上下文
|
||||
* @param uri ContentUri
|
||||
* @param fileName 文件名
|
||||
* @return 拷贝后的文件路径(失败返回 null)
|
||||
*/
|
||||
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
|
||||
LogUtils.d(TAG, "getPathFromInputStreamUri:开始流拷贝,文件名=" + fileName);
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
File targetFile = null;
|
||||
try {
|
||||
// 1. 打开输入流(读取Uri对应文件)
|
||||
inputStream = context.getContentResolver().openInputStream(uri);
|
||||
if (inputStream == null) {
|
||||
LogUtils.e(TAG, "getPathFromInputStreamUri:输入流打开失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 创建目标文件(应用私有缓存目录,无权限限制)
|
||||
targetFile = new File(context.getExternalCacheDir(), fileName);
|
||||
// 若文件已存在,先删除(避免覆盖导致格式异常)
|
||||
if (targetFile.exists()) {
|
||||
boolean deleteSuccess = targetFile.delete();
|
||||
LogUtils.d(TAG, "getPathFromInputStreamUri:删除已存在文件,结果=" + deleteSuccess);
|
||||
}
|
||||
|
||||
// 3. 流拷贝(Java7 手动处理流,避免 try-with-resources)
|
||||
outputStream = new FileOutputStream(targetFile);
|
||||
byte[] buffer = new byte[8 * 1024]; // 8KB 缓冲区,平衡效率与内存
|
||||
int readLength;
|
||||
while ((readLength = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, readLength);
|
||||
}
|
||||
outputStream.flush();
|
||||
LogUtils.d(TAG, "getPathFromInputStreamUri:流拷贝成功,路径=" + targetFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getPathFromInputStreamUri:流拷贝失败", e);
|
||||
// 拷贝失败,删除临时文件
|
||||
if (targetFile != null && targetFile.exists()) {
|
||||
targetFile.delete();
|
||||
}
|
||||
targetFile = null;
|
||||
} finally {
|
||||
// 强制关闭流,避免资源泄漏(Java7 必须手动关闭)
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "getPathFromInputStreamUri:关闭输出流失败", e);
|
||||
}
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "getPathFromInputStreamUri:关闭输入流失败", e);
|
||||
}
|
||||
}
|
||||
return targetFile != null ? targetFile.getAbsolutePath() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验路径是否在安全目录内(适配API29-30+小米机型,避免FileProvider权限异常)
|
||||
* 仅允许:应用私有目录、缓存目录、应用专属公共目录
|
||||
* @param context 上下文
|
||||
* @param file 待校验文件
|
||||
* @return true=安全路径,false=非安全路径
|
||||
*/
|
||||
private static boolean isPathInValidDir(Context context, File file) {
|
||||
String absolutePath = file.getAbsolutePath();
|
||||
// 1. 应用外部私有目录(API29+ 推荐,无权限限制)
|
||||
String externalPrivateDir = context.getExternalFilesDir(null) != null
|
||||
? context.getExternalFilesDir(null).getAbsolutePath()
|
||||
: "";
|
||||
// 2. 应用内部私有目录(无权限限制)
|
||||
String internalPrivateDir = context.getFilesDir().getAbsolutePath();
|
||||
// 3. 应用缓存目录(无权限限制)
|
||||
String cacheDir = context.getCacheDir().getAbsolutePath();
|
||||
// 4. 应用专属公共目录(API29+ 适配,替代废弃的 getExternalStoragePublicDirectory)
|
||||
String appPublicDir = Environment.getExternalStorageDirectory().getAbsolutePath()
|
||||
+ File.separator + Environment.DIRECTORY_PICTURES
|
||||
+ File.separator + APP_PUBLIC_PIC_DIR;
|
||||
|
||||
// 校验路径是否在安全目录内(小米机型必须严格校验,否则FileProvider会抛异常)
|
||||
boolean isInValidDir = absolutePath.startsWith(externalPrivateDir)
|
||||
|| absolutePath.startsWith(internalPrivateDir)
|
||||
|| absolutePath.startsWith(cacheDir)
|
||||
|| absolutePath.startsWith(appPublicDir);
|
||||
|
||||
LogUtils.d(TAG, "isPathInValidDir:外部私有目录=" + externalPrivateDir
|
||||
+ ",公共目录=" + appPublicDir
|
||||
+ ",校验结果=" + isInValidDir);
|
||||
return isInValidDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流拷贝创建临时文件(内部辅助,封装拷贝逻辑)
|
||||
* @param context 上下文
|
||||
* @param inputStream 输入流
|
||||
* @param fileName 文件名
|
||||
* @return 临时文件(失败返回 null)
|
||||
* @throws IOException 流操作异常
|
||||
*/
|
||||
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) throws IOException {
|
||||
File targetFile = null;
|
||||
if (inputStream != null) {
|
||||
byte[] buffer = new byte[8 * 1024];
|
||||
int readLength;
|
||||
targetFile = new File(context.getExternalCacheDir(), fileName);
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete();
|
||||
}
|
||||
OutputStream outputStream = new FileOutputStream(targetFile);
|
||||
while ((readLength = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, readLength);
|
||||
}
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
}
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助:ContentUri 通过 MIME 类型获取后缀(精准匹配,不受文件名伪造影响)
|
||||
* @param context 上下文
|
||||
* @param uri ContentUri
|
||||
* @return 匹配的后缀(无匹配返回空字符串)
|
||||
*/
|
||||
private static String getSuffixFromContentUri(Context context, Uri uri) {
|
||||
String mime = null;
|
||||
try {
|
||||
// 通过 ContentResolver 获取 Uri 对应的 MIME 类型(系统级匹配,最精准)
|
||||
mime = context.getContentResolver().getType(uri);
|
||||
LogUtils.d(TAG, "getSuffixFromContentUri:获取MIME类型=" + mime);
|
||||
if (mime == null || mime.isEmpty()) {
|
||||
// MIME 为空,尝试解析文件名兜底
|
||||
String fileName = getFileNameFromContentUri(context, uri);
|
||||
return getSuffixFromFilePath(fileName);
|
||||
}
|
||||
// MIME 类型匹配后缀(优先完全匹配,再模糊匹配)
|
||||
if (MIME_SUFFIX_MAP.containsKey(mime)) {
|
||||
return MIME_SUFFIX_MAP.get(mime);
|
||||
}
|
||||
// 模糊匹配(如 image/* 匹配通用图片后缀,默认png)
|
||||
if (mime.startsWith("image/")) {
|
||||
return "png";
|
||||
} else if (mime.startsWith("video/")) {
|
||||
return "mp4";
|
||||
} else if (mime.startsWith("audio/")) {
|
||||
return "mp3";
|
||||
} else if (mime.startsWith("application/")) {
|
||||
return "pdf";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getSuffixFromContentUri:MIME解析失败,mime=" + mime, e);
|
||||
}
|
||||
// 所有方式失败,解析Uri路径兜底
|
||||
return getSuffixFromFilePath(uri.getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助:从 ContentUri 获取文件名(MIME 解析失败时兜底)
|
||||
* @param context 上下文
|
||||
* @param uri ContentUri
|
||||
* @return 文件名(失败返回空字符串)
|
||||
*/
|
||||
private static String getFileNameFromContentUri(Context context, Uri uri) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
String[] queryColumns = {MediaStore.MediaColumns.DISPLAY_NAME};
|
||||
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
|
||||
return cursor.getString(nameIndex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getFileNameFromContentUri:查询失败", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
try {
|
||||
cursor.close();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getFileNameFromContentUri:关闭Cursor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助:从文件路径/文件名截取后缀(兜底方案,处理各种路径格式)
|
||||
* @param path 文件路径/文件名
|
||||
* @return 截取的后缀(无后缀返回空字符串)
|
||||
*/
|
||||
private static String getSuffixFromFilePath(String path) {
|
||||
if (path == null || path.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
// 处理路径中的分隔符(兼容 Windows/Android 路径格式)
|
||||
path = path.replace("\\", "/");
|
||||
// 取最后一个 "/" 后的文件名(避免路径包含 "." 导致误判)
|
||||
int lastSepIndex = path.lastIndexOf("/");
|
||||
if (lastSepIndex != -1 && lastSepIndex < path.length() - 1) {
|
||||
path = path.substring(lastSepIndex + 1);
|
||||
}
|
||||
// 截取最后一个 "." 后的后缀(过滤无后缀/点开头/点结尾场景)
|
||||
int lastDotIndex = path.lastIndexOf(".");
|
||||
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == path.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
// 过滤后缀中的非法字符(仅保留字母/数字,避免特殊字符干扰)
|
||||
String suffix = path.substring(lastDotIndex + 1).replaceAll("[^a-zA-Z0-9]", "");
|
||||
// 限制后缀长度(1-5位,避免超长伪造后缀)
|
||||
return suffix.length() >= 1 && suffix.length() <= 5 ? suffix : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package cc.winboll.studio.powerbell.views;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -12,16 +14,21 @@ import android.widget.ImageView.ScaleType;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 基于Java7的BackgroundView(LinearLayout+ImageView,保持原图比例居中平铺)
|
||||
* 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪
|
||||
* 改进:强化Bitmap生命周期管理,防止已回收Bitmap绘制崩溃
|
||||
*/
|
||||
public class BackgroundView extends RelativeLayout {
|
||||
|
||||
public static final String TAG = "BackgroundView";
|
||||
// 记录当前已缓存的图片路径
|
||||
private String mCurrentCachedPath = "";
|
||||
|
||||
private Context mContext;
|
||||
private LinearLayout mLlContainer; // 主容器LinearLayout
|
||||
@@ -55,8 +62,6 @@ public class BackgroundView extends RelativeLayout {
|
||||
LogUtils.d(TAG, "=== initView 启动 ===");
|
||||
// 1. 配置当前控件:全屏+透明
|
||||
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
//setBackgroundColor(0x00000000);
|
||||
//setBackground(new ColorDrawable(0x00000000));
|
||||
|
||||
// 2. 初始化主容器LinearLayout
|
||||
initLinearLayout();
|
||||
@@ -64,7 +69,7 @@ public class BackgroundView extends RelativeLayout {
|
||||
// 3. 初始化ImageView
|
||||
initImageView();
|
||||
|
||||
// 初始设置透明背景
|
||||
// 初始设置透明背景
|
||||
setDefaultTransparentBackground();
|
||||
|
||||
LogUtils.d(TAG, "=== initView 完成 ===");
|
||||
@@ -101,22 +106,39 @@ public class BackgroundView extends RelativeLayout {
|
||||
LogUtils.d(TAG, "=== initImageView 完成 ===");
|
||||
}
|
||||
|
||||
public void loadBackgroundBean(BackgroundBean bean) {
|
||||
if (!bean.isUseBackgroundFile()) {
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
if (bean.isUseBackgroundScaledCompressFile()) {
|
||||
loadImage(bean.getBackgroundScaledCompressFilePath());
|
||||
} else {
|
||||
public void loadByBackgroundBean(BackgroundBean bean) {
|
||||
loadByBackgroundBean(bean, false);
|
||||
}
|
||||
|
||||
loadImage(bean.getBackgroundFilePath());
|
||||
}
|
||||
}
|
||||
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
if (!bean.isUseBackgroundFile()) {
|
||||
setDefaultTransparentBackground();
|
||||
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.sBitmapCacheUtils.removeCachedBitmap(targetPath);
|
||||
// 刷新时直接解码,避免缓存旧数据
|
||||
Bitmap newBitmap = decodeBitmapWithCompress(new File(targetPath), 1080, 1920);
|
||||
if (newBitmap != null) {
|
||||
App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap);
|
||||
}
|
||||
}
|
||||
loadImage(targetPath);
|
||||
}
|
||||
|
||||
// ====================================== 对外方法 ======================================
|
||||
/**
|
||||
* 加载图片(保持原图比例,在LinearLayout中居中平铺)
|
||||
* 改进版:强化Bitmap有效性校验,增加缓存重加载机制,防止已回收Bitmap崩溃
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void loadImage(String imagePath) {
|
||||
@@ -132,29 +154,66 @@ public class BackgroundView extends RelativeLayout {
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
mIvBackground.setVisibility(View.GONE);
|
||||
|
||||
// 计算原图比例
|
||||
mIvBackground.setVisibility(View.GONE);
|
||||
|
||||
// ======================== 路径判断逻辑(改进版) ========================
|
||||
// 1. 路径未变化:校验缓存有效性,无效则重加载
|
||||
if (imagePath.equals(mCurrentCachedPath)) {
|
||||
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
|
||||
if (isBitmapValid(cachedBitmap)) {
|
||||
LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存 Bitmap");
|
||||
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
|
||||
mIvBackground.setImageBitmap(cachedBitmap);
|
||||
adjustImageViewSize();
|
||||
return;
|
||||
} else {
|
||||
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效,尝试重加载 - " + imagePath);
|
||||
// 缓存无效,移除旧缓存并强制重加载
|
||||
App.sBitmapCacheUtils.removeCachedBitmap(imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 路径已更新:移除旧缓存
|
||||
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
|
||||
App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
|
||||
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
|
||||
}
|
||||
// ======================== 路径判断逻辑结束 ========================
|
||||
|
||||
// 无缓存/缓存无效/路径更新:重新加载图片
|
||||
if (!calculateImageAspectRatio(imageFile)) {
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 压缩加载Bitmap
|
||||
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "loadImage: 图片解码失败");
|
||||
ToastUtils.show("图片解码失败,无法加载");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置图片
|
||||
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
|
||||
adjustImageViewSize(); // 调整尺寸
|
||||
// 缓存新图片,并更新当前缓存路径记录
|
||||
App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap);
|
||||
mCurrentCachedPath = imagePath;
|
||||
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
|
||||
|
||||
// 改进:直接使用setImageBitmap,避免BitmapDrawable包装的引用风险
|
||||
mIvBackground.setImageBitmap(bitmap);
|
||||
adjustImageViewSize();
|
||||
LogUtils.d(TAG, "=== loadImage 完成 ===");
|
||||
}
|
||||
|
||||
// ====================================== 内部工具方法 ======================================
|
||||
/**
|
||||
* 工具方法:判断Bitmap是否有效(非空且未被回收)
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
return bitmap != null && !bitmap.isRecycled();
|
||||
}
|
||||
|
||||
private boolean calculateImageAspectRatio(File file) {
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
@@ -183,14 +242,17 @@ public class BackgroundView extends RelativeLayout {
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
|
||||
int scaleX = options.outWidth / maxWidth;
|
||||
int scaleY = options.outHeight / maxHeight;
|
||||
// 改进:更精准的采样率计算(避免过度压缩)
|
||||
int scaleX = (int) Math.ceil((float) options.outWidth / maxWidth);
|
||||
int scaleY = (int) Math.ceil((float) options.outHeight / maxHeight);
|
||||
int inSampleSize = Math.max(scaleX, scaleY);
|
||||
if (inSampleSize <= 0) inSampleSize = 1;
|
||||
inSampleSize = Math.max(1, inSampleSize); // 确保采样率≥1
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = inSampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存
|
||||
options.inPurgeable = true; // 允许系统在内存紧张时回收
|
||||
options.inInputShareable = true;
|
||||
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
|
||||
@@ -198,30 +260,26 @@ public class BackgroundView extends RelativeLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整ImageView尺寸(保持原图比例,在LinearLayout中居中平铺)
|
||||
*/
|
||||
private void adjustImageViewSize() {
|
||||
//LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
|
||||
if (mLlContainer == null || mIvBackground == null) {
|
||||
//LogUtils.e(TAG, "控件为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取LinearLayout尺寸
|
||||
int llWidth = mLlContainer.getWidth();
|
||||
int llHeight = mLlContainer.getHeight();
|
||||
|
||||
if (llWidth == 0 || llHeight == 0) {
|
||||
postDelayed(new Runnable() {
|
||||
LogUtils.w(TAG, "adjustImageViewSize: 容器尺寸未初始化,延迟调整");
|
||||
// 延迟调整(容器尺寸未就绪时)
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adjustImageViewSize();
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算ImageView尺寸(保持比例,不超出LinearLayout)
|
||||
int ivWidth, ivHeight;
|
||||
if (mImageAspectRatio >= 1.0f) {
|
||||
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
|
||||
@@ -231,29 +289,65 @@ public class BackgroundView extends RelativeLayout {
|
||||
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);
|
||||
|
||||
|
||||
//LogUtils.d(TAG, "ImageView尺寸:" + ivWidth + "x" + ivHeight);
|
||||
//LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
|
||||
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
|
||||
mIvBackground.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void setDefaultTransparentBackground() {
|
||||
mIvBackground.setImageBitmap(null);
|
||||
// 改进:先清空Drawable,避免残留已回收Bitmap
|
||||
mIvBackground.setImageDrawable(null);
|
||||
mIvBackground.setBackgroundColor(0x00000000);
|
||||
mImageAspectRatio = 1.0f;
|
||||
mCurrentCachedPath = "";
|
||||
}
|
||||
|
||||
// ====================================== 重写方法 ======================================
|
||||
// ====================================== 重写方法(核心改进) ======================================
|
||||
/**
|
||||
* 重写:绘制前强制校验Bitmap有效性,防止已回收Bitmap崩溃
|
||||
*/
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
Drawable drawable = mIvBackground.getDrawable();
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
|
||||
Bitmap bitmap = bitmapDrawable.getBitmap();
|
||||
if (!isBitmapValid(bitmap)) {
|
||||
LogUtils.e(TAG, "onDraw: 检测到已回收Bitmap,清空绘制");
|
||||
mIvBackground.setImageDrawable(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写:View从窗口移除时主动释放资源,避免内存泄漏和无效引用
|
||||
*/
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
LogUtils.d(TAG, "onDetachedFromWindow: 释放Bitmap资源");
|
||||
// 清空ImageView的Drawable,释放Bitmap引用
|
||||
mIvBackground.setImageDrawable(null);
|
||||
// 清空当前缓存路径,避免后续错误引用
|
||||
mCurrentCachedPath = "";
|
||||
// 可选:如果当前View是唯一使用者,移除全局缓存
|
||||
// if (!TextUtils.isEmpty(mCurrentCachedPath)) {
|
||||
// App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写:恢复尺寸调整逻辑,确保View尺寸变化时正确显示
|
||||
*/
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
adjustImageViewSize(); // 尺寸变化时重新调整
|
||||
adjustImageViewSize(); // 恢复尺寸调整
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +1,179 @@
|
||||
package cc.winboll.studio.powerbell.views;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 12:55
|
||||
* @Describe 电池电量Drawable:适配API30,兼容小米机型,支持能量/条纹两种绘制风格切换
|
||||
*/
|
||||
public class BatteryDrawable extends Drawable {
|
||||
public static final String TAG = BatteryDrawable.class.getSimpleName();
|
||||
// ====================== 静态常量(置顶,按重要性排序) ======================
|
||||
public static final String TAG = "BatteryDrawable";
|
||||
// 小米机型绘制偏移校准(适配MIUI渲染特性,避免绘制错位)
|
||||
private static final int MIUI_DRAW_OFFSET = 1;
|
||||
// 默认电量透明度(兼顾显示效果与API30渲染性能)
|
||||
private static final int DEFAULT_BATTERY_ALPHA = 210;
|
||||
|
||||
// 电量颜色画笔
|
||||
final Paint mPaint;
|
||||
// 电量值
|
||||
int mnValue = 1;
|
||||
// ====================== 核心成员变量(按功能归类,final优先) ======================
|
||||
// 绘制画笔(final修饰,避免重复创建,提升性能)
|
||||
private final Paint mBatteryPaint;
|
||||
// 业务控制变量
|
||||
private int mBatteryValue = -1; // 当前电量(0-100,-1=未初始化)
|
||||
private boolean mIsEnergyStyle = true; // 绘制风格(true=能量,false=条纹)
|
||||
|
||||
// @int color : 电量颜色
|
||||
//
|
||||
public BatteryDrawable(int color) {
|
||||
mPaint = new Paint();
|
||||
mPaint.setColor(color);
|
||||
mPaint.setAlpha(210);
|
||||
// ====================== 构造方法(重载适配,优先暴露常用构造) ======================
|
||||
/**
|
||||
* 构造方法(默认能量风格,常用场景)
|
||||
* @param batteryColor 电量显示颜色
|
||||
*/
|
||||
public BatteryDrawable(int batteryColor) {
|
||||
LogUtils.d(TAG, "constructor: 初始化(能量风格),颜色=" + Integer.toHexString(batteryColor));
|
||||
mBatteryPaint = new Paint();
|
||||
initPaintConfig(batteryColor);
|
||||
}
|
||||
|
||||
// 设置电量值
|
||||
//
|
||||
public void setValue(int value) {
|
||||
mnValue = value;
|
||||
/**
|
||||
* 构造方法(支持指定绘制风格,扩展场景)
|
||||
* @param batteryColor 电量显示颜色
|
||||
* @param isEnergyStyle 是否启用能量风格
|
||||
*/
|
||||
public BatteryDrawable(int batteryColor, boolean isEnergyStyle) {
|
||||
LogUtils.d(TAG, "constructor: 初始化,颜色=" + Integer.toHexString(batteryColor) + ",风格=" + (isEnergyStyle ? "能量" : "条纹"));
|
||||
mBatteryPaint = new Paint();
|
||||
mIsEnergyStyle = isEnergyStyle;
|
||||
initPaintConfig(batteryColor);
|
||||
}
|
||||
|
||||
// ====================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================
|
||||
/**
|
||||
* 初始化画笔配置(适配API30渲染特性,优化小米机型兼容性)
|
||||
*/
|
||||
private void initPaintConfig(int color) {
|
||||
mBatteryPaint.setColor(color);
|
||||
mBatteryPaint.setAlpha(DEFAULT_BATTERY_ALPHA);
|
||||
mBatteryPaint.setAntiAlias(true); // 抗锯齿,解决小米低分辨率锯齿问题
|
||||
mBatteryPaint.setStyle(Paint.Style.FILL); // 固定填充模式,避免混乱
|
||||
mBatteryPaint.setDither(false); // 禁用抖动,提升API30颜色显示一致性
|
||||
LogUtils.d(TAG, "initPaintConfig: 画笔配置完成");
|
||||
}
|
||||
|
||||
// ====================== 核心绘制方法(Drawable抽象方法,优先级最高) ======================
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
int nWidth = getBounds().width();
|
||||
int nHeight = getBounds().height();
|
||||
int mnDx = nHeight / 203;
|
||||
// 未初始化/异常电量,直接跳过,避免无效绘制
|
||||
if (mBatteryValue < 0) {
|
||||
LogUtils.w(TAG, "draw: 电量未初始化,跳过绘制");
|
||||
return;
|
||||
}
|
||||
// 强制校准电量范围(0-100),防止异常值导致绘制错误
|
||||
int validBattery = Math.max(0, Math.min(mBatteryValue, 100));
|
||||
Rect drawBounds = getBounds();
|
||||
int drawHeight = drawBounds.height();
|
||||
|
||||
// 绘制耗电电量提醒值电量
|
||||
// 能量绘图风格
|
||||
int nTop;
|
||||
int nLeft = 0;
|
||||
int nBottom;
|
||||
int nRight = nWidth;
|
||||
// 小米机型绘制偏移校准(解决MIUI系统渲染偏移问题)
|
||||
int offset = MIUI_DRAW_OFFSET;
|
||||
int left = drawBounds.left + offset;
|
||||
int right = drawBounds.right - offset;
|
||||
|
||||
//for (int i = 0; i < mnValue; i ++) {
|
||||
nBottom = nHeight;
|
||||
nTop = nHeight - (nHeight * mnValue / 100);
|
||||
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mPaint);
|
||||
|
||||
// 绘制耗电电量提醒值电量
|
||||
// 意兴阑珊绘图风格
|
||||
/*int nTop;
|
||||
int nLeft = 0;
|
||||
int nBottom;
|
||||
int nRight = nWidth;
|
||||
// 按风格执行绘制(精简日志,仅保留核心绘制参数)
|
||||
LogUtils.d(TAG, "draw: 开始绘制,电量=" + validBattery + ",风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
|
||||
if (mIsEnergyStyle) {
|
||||
drawEnergyStyle(canvas, validBattery, left, right, drawHeight);
|
||||
} else {
|
||||
drawStripeStyle(canvas, validBattery, left, right, drawHeight);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < mnValue; i ++) {
|
||||
nBottom = (nHeight * (100-i)/100) - mnDx;
|
||||
nTop = nBottom + mnDx;
|
||||
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mPaint);
|
||||
}*/
|
||||
// ====================== 绘制风格实现(私有封装,按风格拆分) ======================
|
||||
/**
|
||||
* 能量风格绘制(整块填充,高效简洁,默认风格)
|
||||
*/
|
||||
private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) {
|
||||
int top = height - (height * battery / 100); // 计算电量对应顶部坐标
|
||||
canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
|
||||
LogUtils.d(TAG, "drawEnergyStyle: 绘制完成,顶部坐标=" + top);
|
||||
}
|
||||
|
||||
/**
|
||||
* 条纹风格绘制(分段条纹,扩展风格)
|
||||
*/
|
||||
private void drawStripeStyle(Canvas canvas, int battery, int left, int right, int height) {
|
||||
int stripeHeight = height / 100; // 单条条纹高度(均匀拆分)
|
||||
// 从底部向上绘制对应电量条纹
|
||||
for (int i = 0; i < battery; i++) {
|
||||
int bottom = height - (stripeHeight * i);
|
||||
int top = bottom - stripeHeight;
|
||||
canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
|
||||
}
|
||||
LogUtils.d(TAG, "drawStripeStyle: 绘制完成,条纹数量=" + battery);
|
||||
}
|
||||
|
||||
// ====================== 对外暴露方法(业务控制入口,按功能排序) ======================
|
||||
/**
|
||||
* 设置当前电量(外部核心调用入口)
|
||||
* @param value 电量值(0-100)
|
||||
*/
|
||||
public void setBatteryValue(int value) {
|
||||
LogUtils.d(TAG, "setBatteryValue: 电量更新,旧值=" + mBatteryValue + ",新值=" + value);
|
||||
mBatteryValue = value;
|
||||
invalidateSelf(); // 触发重绘,确保UI实时更新
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换绘制风格
|
||||
* @param isEnergyStyle true=能量风格,false=条纹风格
|
||||
*/
|
||||
public void switchDrawStyle(boolean isEnergyStyle) {
|
||||
LogUtils.d(TAG, "switchDrawStyle: 风格切换,旧=" + (mIsEnergyStyle ? "能量" : "条纹") + ",新=" + (isEnergyStyle ? "能量" : "条纹"));
|
||||
mIsEnergyStyle = isEnergyStyle;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电量显示颜色
|
||||
* @param color 新颜色值
|
||||
*/
|
||||
public void updateBatteryColor(int color) {
|
||||
LogUtils.d(TAG, "updateBatteryColor: 颜色更新,旧=" + Integer.toHexString(mBatteryPaint.getColor()) + ",新=" + Integer.toHexString(color));
|
||||
mBatteryPaint.setColor(color);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
// ====================== Getter方法(按需暴露,简洁无冗余) ======================
|
||||
public int getBatteryValue() {
|
||||
return mBatteryValue;
|
||||
}
|
||||
|
||||
public boolean isEnergyStyle() {
|
||||
return mIsEnergyStyle;
|
||||
}
|
||||
|
||||
// ====================== Drawable抽象方法(必须实现,精简逻辑) ======================
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
LogUtils.d(TAG, "setAlpha: 透明度更新,旧=" + mBatteryPaint.getAlpha() + ",新=" + alpha);
|
||||
mBatteryPaint.setAlpha(alpha);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
// This method is required
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int p1) {
|
||||
|
||||
LogUtils.d(TAG, "setColorFilter: 设置颜色过滤,filter=" + colorFilter);
|
||||
mBatteryPaint.setColorFilter(colorFilter);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.UNKNOWN;
|
||||
// 固定返回半透明,适配API30透明度渲染机制,兼容小米机型
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,844 @@
|
||||
package cc.winboll.studio.powerbell.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
|
||||
import cc.winboll.studio.powerbell.services.ControlCenterService;
|
||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 13:14
|
||||
* @Describe 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
|
||||
* 适配:Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
|
||||
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
|
||||
*/
|
||||
public class MainContentView {
|
||||
// ======================== 静态常量(置顶,唯一标识)========================
|
||||
public static final String TAG = "MainContentView";
|
||||
|
||||
// 变更类型常量(区分不同控件,精准处理逻辑)
|
||||
private static final int CHANGE_TYPE_CHARGE_SWITCH = 1;
|
||||
private static final int CHANGE_TYPE_USAGE_SWITCH = 2;
|
||||
private static final int CHANGE_TYPE_SERVICE_SWITCH = 3;
|
||||
private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4;
|
||||
private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5;
|
||||
|
||||
// ======================== 内部静态类(临时数据载体,避免外部依赖)========================
|
||||
/**
|
||||
* 临时配置数据实体(缓存变更信息,取消时恢复)
|
||||
*/
|
||||
private static class TempConfigData {
|
||||
int changeType;
|
||||
boolean originalBooleanValue;
|
||||
int originalIntValue;
|
||||
boolean newBooleanValue;
|
||||
int newIntValue;
|
||||
|
||||
// 构造方法(开关类型)
|
||||
TempConfigData(int changeType, boolean originalValue, boolean newValue) {
|
||||
this.changeType = changeType;
|
||||
this.originalBooleanValue = originalValue;
|
||||
this.newBooleanValue = newValue;
|
||||
}
|
||||
|
||||
// 构造方法(进度条类型)
|
||||
TempConfigData(int changeType, int originalValue, int newValue) {
|
||||
this.changeType = changeType;
|
||||
this.originalIntValue = originalValue;
|
||||
this.newIntValue = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 事件回调接口(解耦视图与业务,提升扩展性)========================
|
||||
public interface OnViewActionListener {
|
||||
void onChargeReminderSwitchChanged(boolean isChecked);
|
||||
void onUsageReminderSwitchChanged(boolean isChecked);
|
||||
void onServiceSwitchChanged(boolean isChecked);
|
||||
void onChargeReminderProgressChanged(int progress);
|
||||
void onUsageReminderProgressChanged(int progress);
|
||||
}
|
||||
|
||||
// ======================== 成员变量(按功能分类,避免混乱)========================
|
||||
// 外部依赖实例(生命周期关联,优先声明)
|
||||
private Context mContext;
|
||||
private AppConfigUtils mAppConfigUtils;
|
||||
private OnViewActionListener mActionListener;
|
||||
|
||||
// 视图控件(按「布局→开关→文本→进度条→图标」功能归类)
|
||||
// 基础布局控件
|
||||
public RelativeLayout mainLayout;
|
||||
public MemoryCachedBackgroundView backgroundView;
|
||||
LinearLayout mllBackgroundView;
|
||||
// 容器布局控件
|
||||
public LinearLayout llLeftSeekBar;
|
||||
public LinearLayout llRightSeekBar;
|
||||
// 开关控件
|
||||
public CheckBox cbEnableChargeReminder;
|
||||
public CheckBox cbEnableUsageReminder;
|
||||
public Switch swEnableService;
|
||||
// 文本显示控件
|
||||
public TextView tvTips;
|
||||
public TextView tvChargeReminderValue;
|
||||
public TextView tvUsageReminderValue;
|
||||
public TextView tvCurrentBatteryValue;
|
||||
// 进度条控件(使用自定义 VerticalSeekBar)
|
||||
public VerticalSeekBar sbChargeReminder;
|
||||
public VerticalSeekBar sbUsageReminder;
|
||||
// 图标显示控件
|
||||
public ImageView ivCurrentBattery;
|
||||
public ImageView ivChargeReminderBattery;
|
||||
public ImageView ivUsageReminderBattery;
|
||||
|
||||
// 进度缓存(用于实时计算比值,避免频繁调用 getProgress())
|
||||
private int mCurrentChargeProgress;
|
||||
private int mCurrentUsageProgress;
|
||||
|
||||
// 内部复用资源(避免重复创建,优化性能)
|
||||
private BatteryDrawable mCurrentBatteryDrawable;
|
||||
private BatteryDrawable mChargeReminderBatteryDrawable;
|
||||
private BatteryDrawable mUsageReminderBatteryDrawable;
|
||||
|
||||
// 配置变更确认对话框(单例复用,避免重复创建)
|
||||
private AlertDialog mConfigConfirmDialog;
|
||||
private AlertDialog.Builder mDialogBuilder;
|
||||
// 临时存储变更数据(对话框确认前缓存,取消时恢复)
|
||||
private TempConfigData mTempConfigData;
|
||||
// 对话框状态锁(避免快速点击重复弹窗)
|
||||
private boolean isDialogShowing = false;
|
||||
|
||||
// ======================== 构造方法(初始化入口,逻辑闭环)========================
|
||||
public MainContentView(Context context, View rootView, OnViewActionListener actionListener) {
|
||||
LogUtils.d(TAG, "MainContentView() | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener);
|
||||
// 初始化外部依赖
|
||||
this.mContext = context;
|
||||
this.mActionListener = actionListener;
|
||||
this.mAppConfigUtils = AppConfigUtils.getInstance(context.getApplicationContext());
|
||||
|
||||
// 执行核心初始化流程(按顺序执行,避免依赖空指针)
|
||||
bindViews(rootView);
|
||||
initBatteryDrawables();
|
||||
initConfirmDialog();
|
||||
bindViewListeners();
|
||||
|
||||
LogUtils.d(TAG, "MainContentView 初始化完成");
|
||||
}
|
||||
|
||||
// ======================== 私有初始化方法(封装内部逻辑,仅暴露入口)========================
|
||||
/**
|
||||
* 绑定视图控件(显式强转适配 Java7,适配 API30 视图加载机制)
|
||||
*/
|
||||
private void bindViews(View rootView) {
|
||||
LogUtils.d(TAG, "bindViews() | rootView=" + rootView);
|
||||
// 基础布局绑定
|
||||
mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1);
|
||||
//backgroundView = (BackgroundView) rootView.findViewById(R.id.fragmentmainviewBackgroundView1);
|
||||
mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview);
|
||||
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
|
||||
if (backgroundView.getParent() != null) {
|
||||
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
|
||||
}
|
||||
mllBackgroundView.addView(backgroundView);
|
||||
// 容器布局绑定
|
||||
llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1);
|
||||
llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2);
|
||||
// 开关控件绑定
|
||||
cbEnableChargeReminder = (CheckBox) rootView.findViewById(R.id.fragmentmainviewCheckBox1);
|
||||
cbEnableUsageReminder = (CheckBox) rootView.findViewById(R.id.fragmentmainviewCheckBox2);
|
||||
swEnableService = (Switch) rootView.findViewById(R.id.fragmentandroidviewSwitch1);
|
||||
// 文本控件绑定
|
||||
tvTips = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView1);
|
||||
tvChargeReminderValue = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView2);
|
||||
tvUsageReminderValue = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView3);
|
||||
tvCurrentBatteryValue = (TextView) rootView.findViewById(R.id.fragmentandroidviewTextView4);
|
||||
// 进度条控件绑定(自定义 VerticalSeekBar)
|
||||
sbChargeReminder = (VerticalSeekBar) rootView.findViewById(R.id.fragmentandroidviewVerticalSeekBar1);
|
||||
sbUsageReminder = (VerticalSeekBar) rootView.findViewById(R.id.fragmentandroidviewVerticalSeekBar2);
|
||||
// 图标控件绑定
|
||||
ivCurrentBattery = (ImageView) rootView.findViewById(R.id.fragmentandroidviewImageView1);
|
||||
ivChargeReminderBattery = (ImageView) rootView.findViewById(R.id.fragmentandroidviewImageView3);
|
||||
ivUsageReminderBattery = (ImageView) rootView.findViewById(R.id.fragmentandroidviewImageView2);
|
||||
|
||||
// 初始化进度缓存(从配置读取初始值)
|
||||
mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue();
|
||||
mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue();
|
||||
|
||||
// 关键视图绑定校验(仅保留核心控件错误日志,精简冗余)
|
||||
if (mainLayout == null) LogUtils.e(TAG, "mainLayout 绑定失败");
|
||||
if (backgroundView == null) LogUtils.e(TAG, "backgroundView 绑定失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化电池 Drawable(集成 BatteryDrawable,默认能量风格,适配小米机型渲染)
|
||||
*/
|
||||
private void initBatteryDrawables() {
|
||||
LogUtils.d(TAG, "initBatteryDrawables()");
|
||||
// 当前电量 Drawable(颜色从资源读取,适配 API30 主题)
|
||||
int colorCurrent = getResourceColor(R.color.colorCurrent);
|
||||
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
|
||||
// 充电提醒 Drawable
|
||||
int colorCharge = getResourceColor(R.color.colorCharge);
|
||||
mChargeReminderBatteryDrawable = new BatteryDrawable(colorCharge);
|
||||
// 耗电提醒 Drawable
|
||||
int colorUsage = getResourceColor(R.color.colorUsege);
|
||||
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
|
||||
*/
|
||||
private void initConfirmDialog() {
|
||||
LogUtils.d(TAG, "initConfirmDialog()");
|
||||
if (mContext == null) {
|
||||
LogUtils.e(TAG, "Context 为空,初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 初始化 Builder(核心:后续通过 Builder 更新消息)
|
||||
mDialogBuilder = new AlertDialog.Builder(mContext);
|
||||
mDialogBuilder.setTitle("配置变更确认");
|
||||
mDialogBuilder.setMessage("是否确认修改当前配置?");
|
||||
|
||||
// 确定按钮:保存配置+回调+更新视图
|
||||
mDialogBuilder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
confirmConfigChange();
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
// 取消按钮:恢复原始配置(补充物理取消按钮,提升用户体验)
|
||||
mDialogBuilder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
cancelConfigChange();
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
// 对话框外部点击监听:关闭对话框+恢复原始配置
|
||||
mDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
cancelConfigChange();
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 初始化对话框实例(设置可取消,支持外部点击关闭)
|
||||
mConfigConfirmDialog = mDialogBuilder.create();
|
||||
mConfigConfirmDialog.setCancelable(true);
|
||||
mConfigConfirmDialog.setCanceledOnTouchOutside(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定视图事件监听(Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效)
|
||||
*/
|
||||
private void bindViewListeners() {
|
||||
LogUtils.d(TAG, "bindViewListeners()");
|
||||
// 依赖校验,避免空指针
|
||||
if (mAppConfigUtils == null || mActionListener == null || mConfigConfirmDialog == null) {
|
||||
LogUtils.e(TAG, "依赖实例为空,跳过监听绑定");
|
||||
return;
|
||||
}
|
||||
|
||||
// 充电提醒进度条监听(使用 VerticalSeekBar 专属接口,确保弹窗100%触发)
|
||||
if (sbChargeReminder != null) {
|
||||
// 原有:触摸抬起/取消监听(用于配置确认)
|
||||
sbChargeReminder.setOnVerticalSeekBarTouchListener(new VerticalSeekBar.OnVerticalSeekBarTouchListener() {
|
||||
@Override
|
||||
public void onTouchUp(VerticalSeekBar seekBar, int progress) {
|
||||
int originalValue = mAppConfigUtils.getChargeReminderValue();
|
||||
// 进度无变化,不处理
|
||||
if (originalValue == progress) {
|
||||
LogUtils.d(TAG, "ChargeReminderSeekBar: 进度无变化,跳过");
|
||||
return;
|
||||
}
|
||||
// 缓存变更数据,显示确认对话框
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchCancel(VerticalSeekBar seekBar, int progress) {
|
||||
// 触摸取消,回滚视图进度(UI 与配置保持一致)
|
||||
int originalValue = mAppConfigUtils.getChargeReminderValue();
|
||||
if (tvChargeReminderValue != null && mChargeReminderBatteryDrawable != null && ivChargeReminderBattery != null) {
|
||||
mChargeReminderBatteryDrawable.setBatteryValue(originalValue);
|
||||
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
|
||||
tvChargeReminderValue.setText(originalValue + "%");
|
||||
}
|
||||
seekBar.setProgress(originalValue);
|
||||
// 恢复进度缓存
|
||||
mCurrentChargeProgress = originalValue;
|
||||
LogUtils.d(TAG, "ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
|
||||
}
|
||||
});
|
||||
|
||||
// 新增:实时进度变化监听(用于比值预览)
|
||||
sbChargeReminder.setOnVerticalSeekBarChangeListener(new VerticalSeekBar.OnVerticalSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(VerticalSeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
mCurrentChargeProgress = progress;
|
||||
// 同步更新进度文本和电池图标(保持UI一致性)
|
||||
if (tvChargeReminderValue != null && mChargeReminderBatteryDrawable != null && ivChargeReminderBattery != null) {
|
||||
mChargeReminderBatteryDrawable.setBatteryValue(progress);
|
||||
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
|
||||
tvChargeReminderValue.setText(progress + "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(VerticalSeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
|
||||
});
|
||||
LogUtils.d(TAG, "充电提醒进度条专属监听绑定完成");
|
||||
}
|
||||
|
||||
// 充电提醒开关监听
|
||||
if (cbEnableChargeReminder != null) {
|
||||
cbEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean originalValue = mAppConfigUtils.isChargeReminderEnabled();
|
||||
boolean newValue = cbEnableChargeReminder.isChecked();
|
||||
// 状态无变化,不处理
|
||||
if (originalValue == newValue) return;
|
||||
// 缓存变更数据,显示确认对话框
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "充电提醒开关监听绑定完成");
|
||||
}
|
||||
|
||||
// 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口,确保弹窗100%触发)
|
||||
if (sbUsageReminder != null) {
|
||||
// 原有:触摸抬起/取消监听(用于配置确认)
|
||||
sbUsageReminder.setOnVerticalSeekBarTouchListener(new VerticalSeekBar.OnVerticalSeekBarTouchListener() {
|
||||
@Override
|
||||
public void onTouchUp(VerticalSeekBar seekBar, int progress) {
|
||||
int originalValue = mAppConfigUtils.getUsageReminderValue();
|
||||
// 进度无变化,不处理
|
||||
if (originalValue == progress) {
|
||||
LogUtils.d(TAG, "UsageReminderSeekBar: 进度无变化,跳过");
|
||||
return;
|
||||
}
|
||||
// 缓存变更数据,显示确认对话框
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchCancel(VerticalSeekBar seekBar, int progress) {
|
||||
// 触摸取消,回滚视图进度(UI 与配置保持一致)
|
||||
int originalValue = mAppConfigUtils.getUsageReminderValue();
|
||||
if (tvUsageReminderValue != null && mUsageReminderBatteryDrawable != null && ivUsageReminderBattery != null) {
|
||||
mUsageReminderBatteryDrawable.setBatteryValue(originalValue);
|
||||
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
|
||||
tvUsageReminderValue.setText(originalValue + "%");
|
||||
}
|
||||
seekBar.setProgress(originalValue);
|
||||
// 恢复进度缓存
|
||||
mCurrentUsageProgress = originalValue;
|
||||
LogUtils.d(TAG, "UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
|
||||
}
|
||||
});
|
||||
|
||||
// 新增:实时进度变化监听(用于比值预览)
|
||||
sbUsageReminder.setOnVerticalSeekBarChangeListener(new VerticalSeekBar.OnVerticalSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(VerticalSeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
mCurrentUsageProgress = progress;
|
||||
// 同步更新进度文本和电池图标(保持UI一致性)
|
||||
if (tvUsageReminderValue != null && mUsageReminderBatteryDrawable != null && ivUsageReminderBattery != null) {
|
||||
mUsageReminderBatteryDrawable.setBatteryValue(progress);
|
||||
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
|
||||
tvUsageReminderValue.setText(progress + "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(VerticalSeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
|
||||
});
|
||||
LogUtils.d(TAG, "耗电提醒进度条专属监听绑定完成");
|
||||
}
|
||||
|
||||
// 耗电提醒开关监听
|
||||
if (cbEnableUsageReminder != null) {
|
||||
cbEnableUsageReminder.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean originalValue = mAppConfigUtils.isUsageReminderEnabled();
|
||||
boolean newValue = cbEnableUsageReminder.isChecked();
|
||||
// 状态无变化,不处理
|
||||
if (originalValue == newValue) return;
|
||||
// 缓存变更数据,显示确认对话框
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue);
|
||||
updateDialogMessageByChangeType();
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "耗电提醒开关监听绑定完成");
|
||||
}
|
||||
|
||||
// 服务总开关监听(核心优化:逻辑与其他控件完全对齐)
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 从服务控制Bean读取原始状态,确保与实际一致
|
||||
boolean originalValue = getServiceEnableState();
|
||||
boolean newValue = ((Switch) v).isChecked();
|
||||
// 状态无变化,不处理
|
||||
if (originalValue == newValue) return;
|
||||
// 缓存变更数据
|
||||
mTempConfigData = new TempConfigData(CHANGE_TYPE_SERVICE_SWITCH, originalValue, newValue);
|
||||
// 更新差异化提示语
|
||||
updateDialogMessageByChangeType();
|
||||
// 显示确认对话框
|
||||
showConfigConfirmDialog();
|
||||
LogUtils.d(TAG, "swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "服务总开关监听绑定完成");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "所有事件监听绑定完成");
|
||||
}
|
||||
|
||||
// ======================== 对外暴露核心方法(业务入口,精简参数,明确职责)========================
|
||||
/**
|
||||
* 更新所有视图数据(从配置读取数据,统一刷新 UI,适配 API30 视图更新规范)
|
||||
* @param frameDrawable 进度条背景 Drawable(外部传入,适配主题切换)
|
||||
*/
|
||||
public void updateViewData(Drawable frameDrawable) {
|
||||
LogUtils.d(TAG, "updateViewData() | frameDrawable=" + frameDrawable);
|
||||
if (mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "AppConfigUtils 为空,跳过更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 一次读取所有配置参数,减少工具类调用,提升性能
|
||||
int chargeVal = mAppConfigUtils.getChargeReminderValue();
|
||||
int usageVal = mAppConfigUtils.getUsageReminderValue();
|
||||
int currentVal = mAppConfigUtils.getCurrentBatteryValue();
|
||||
boolean chargeEnable = mAppConfigUtils.isChargeReminderEnabled();
|
||||
boolean usageEnable = mAppConfigUtils.isUsageReminderEnabled();
|
||||
// 从服务控制Bean读取状态,确保UI与实际一致
|
||||
boolean serviceEnable = getServiceEnableState();
|
||||
// 更新进度缓存
|
||||
mCurrentChargeProgress = chargeVal;
|
||||
mCurrentUsageProgress = usageVal;
|
||||
LogUtils.d(TAG, "配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable);
|
||||
|
||||
// 进度条背景更新
|
||||
if (frameDrawable != null) {
|
||||
if (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable);
|
||||
if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable);
|
||||
}
|
||||
|
||||
// 当前电量更新(联动 BatteryDrawable,实时刷新图标)
|
||||
if (ivCurrentBattery != null && mCurrentBatteryDrawable != null) {
|
||||
mCurrentBatteryDrawable.setBatteryValue(currentVal);
|
||||
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
|
||||
}
|
||||
if (tvCurrentBatteryValue != null) {
|
||||
tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent));
|
||||
tvCurrentBatteryValue.setText(currentVal + "%");
|
||||
}
|
||||
|
||||
// 充电提醒视图更新
|
||||
if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) {
|
||||
mChargeReminderBatteryDrawable.setBatteryValue(chargeVal);
|
||||
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
|
||||
}
|
||||
if (tvChargeReminderValue != null) {
|
||||
tvChargeReminderValue.setTextColor(getResourceColor(R.color.colorCharge));
|
||||
tvChargeReminderValue.setText(chargeVal + "%");
|
||||
}
|
||||
if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal);
|
||||
if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable);
|
||||
|
||||
// 耗电提醒视图更新
|
||||
if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) {
|
||||
mUsageReminderBatteryDrawable.setBatteryValue(usageVal);
|
||||
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
|
||||
}
|
||||
if (tvUsageReminderValue != null) {
|
||||
tvUsageReminderValue.setTextColor(getResourceColor(R.color.colorUsege));
|
||||
tvUsageReminderValue.setText(usageVal + "%");
|
||||
}
|
||||
if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal);
|
||||
if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable);
|
||||
|
||||
// 服务开关+提示文本更新(确保状态准确)
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setChecked(serviceEnable);
|
||||
swEnableService.setText(mContext.getString(R.string.txt_aboveswitch));
|
||||
}
|
||||
if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips));
|
||||
|
||||
LogUtils.d(TAG, "所有视图数据更新完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时更新当前电量(单独抽离,适配电池实时监控场景,优化 API30 UI 响应速度)
|
||||
* @param value 电量值(自动校准 0-100,避免异常值)
|
||||
*/
|
||||
public void updateCurrentBattery(int value) {
|
||||
LogUtils.d(TAG, "updateCurrentBattery() | 原始值=" + value);
|
||||
// 核心依赖校验
|
||||
if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) {
|
||||
LogUtils.e(TAG, "视图/Drawable 为空,跳过更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校准电量范围(强制 0-100,防止 API30 视图显示异常)
|
||||
int validValue = Math.max(0, Math.min(value, 100));
|
||||
// 联动 BatteryDrawable 更新图标,同步文本显示
|
||||
mCurrentBatteryDrawable.setBatteryValue(validValue);
|
||||
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
|
||||
tvCurrentBatteryValue.setText(validValue + "%");
|
||||
|
||||
LogUtils.d(TAG, "更新完成 | 校准后值=" + validValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用)
|
||||
*/
|
||||
public void releaseResources() {
|
||||
LogUtils.d(TAG, "releaseResources()");
|
||||
// 释放对话框资源(安全销毁,避免内存泄漏)
|
||||
if (mConfigConfirmDialog != null) {
|
||||
if (mConfigConfirmDialog.isShowing()) {
|
||||
mConfigConfirmDialog.dismiss();
|
||||
}
|
||||
mConfigConfirmDialog.setOnDismissListener(null);
|
||||
mConfigConfirmDialog.setOnCancelListener(null);
|
||||
mConfigConfirmDialog = null;
|
||||
}
|
||||
// 释放 Builder
|
||||
mDialogBuilder = null;
|
||||
// 释放临时数据
|
||||
mTempConfigData = null;
|
||||
|
||||
// 释放 BatteryDrawable 资源(重点回收绘制资源,避免 OOM)
|
||||
mCurrentBatteryDrawable = null;
|
||||
mChargeReminderBatteryDrawable = null;
|
||||
mUsageReminderBatteryDrawable = null;
|
||||
|
||||
// 置空视图实例(断开视图引用,辅助 GC 回收)
|
||||
mainLayout = null;
|
||||
backgroundView = null;
|
||||
llLeftSeekBar = null;
|
||||
llRightSeekBar = null;
|
||||
cbEnableChargeReminder = null;
|
||||
cbEnableUsageReminder = null;
|
||||
swEnableService = null;
|
||||
tvTips = null;
|
||||
tvChargeReminderValue = null;
|
||||
tvUsageReminderValue = null;
|
||||
tvCurrentBatteryValue = null;
|
||||
sbChargeReminder = null;
|
||||
sbUsageReminder = null;
|
||||
ivCurrentBattery = null;
|
||||
ivChargeReminderBattery = null;
|
||||
ivUsageReminderBattery = null;
|
||||
|
||||
// 置空外部依赖(断开生命周期关联,杜绝内存泄漏)
|
||||
mContext = null;
|
||||
mAppConfigUtils = null;
|
||||
mActionListener = null;
|
||||
|
||||
LogUtils.d(TAG, "所有资源释放完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务开关启用状态(外部调用,同步 UI 与服务状态,适配 Activity 视图刷新)
|
||||
* @param enabled 服务启用状态
|
||||
*/
|
||||
public void setServiceSwitchChecked(boolean enabled) {
|
||||
LogUtils.d(TAG, "setServiceSwitchChecked() | enabled=" + enabled);
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setChecked(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务开关点击状态(外部调用,避免更新 UI 时触发重复回调)
|
||||
* @param enabled 是否允许点击
|
||||
*/
|
||||
public void setServiceSwitchEnabled(boolean enabled) {
|
||||
LogUtils.d(TAG, "setServiceSwitchEnabled() | enabled=" + enabled);
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑)========================
|
||||
/**
|
||||
* 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗)
|
||||
*/
|
||||
private void showConfigConfirmDialog() {
|
||||
LogUtils.d(TAG, "showConfigConfirmDialog() | isDialogShowing=" + isDialogShowing);
|
||||
// 对话框状态锁:正在显示则跳过,避免重复触发
|
||||
if (isDialogShowing) {
|
||||
LogUtils.d(TAG, "对话框已显示,跳过重复调用");
|
||||
return;
|
||||
}
|
||||
// 基础校验:对话框/上下文/Builder 为空
|
||||
if (mDialogBuilder == null || mContext == null) {
|
||||
LogUtils.e(TAG, "对话框Builder/上下文异常,无法显示");
|
||||
if (mTempConfigData != null) cancelConfigChange();
|
||||
return;
|
||||
}
|
||||
// Activity 状态校验:避免销毁后弹窗崩溃(适配 API30)
|
||||
Activity activity = (Activity) mContext;
|
||||
if (activity.isFinishing() || activity.isDestroyed()) {
|
||||
LogUtils.e(TAG, "Activity 已销毁,无法显示对话框");
|
||||
if (mTempConfigData != null) cancelConfigChange();
|
||||
return;
|
||||
}
|
||||
// 重新构建对话框(核心:确保最新消息生效)
|
||||
mConfigConfirmDialog = mDialogBuilder.create();
|
||||
// 显示对话框,设置状态锁+关闭监听
|
||||
mConfigConfirmDialog.show();
|
||||
isDialogShowing = true;
|
||||
// 对话框关闭时解锁
|
||||
mConfigConfirmDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
isDialogShowing = false;
|
||||
mConfigConfirmDialog.setOnDismissListener(null);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "确认对话框显示成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认配置变更(保存数据+回调监听+更新视图)
|
||||
*/
|
||||
private void confirmConfigChange() {
|
||||
LogUtils.d(TAG, "confirmConfigChange() | mTempConfigData=" + mTempConfigData);
|
||||
if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) {
|
||||
LogUtils.e(TAG, "依赖数据为空,确认失败");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mTempConfigData.changeType) {
|
||||
// 充电提醒开关
|
||||
case CHANGE_TYPE_CHARGE_SWITCH:
|
||||
mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue);
|
||||
mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
|
||||
break;
|
||||
// 耗电提醒开关
|
||||
case CHANGE_TYPE_USAGE_SWITCH:
|
||||
mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue);
|
||||
mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
|
||||
break;
|
||||
// 服务总开关(核心:持久化配置+触发 Activity 回调)
|
||||
case CHANGE_TYPE_SERVICE_SWITCH:
|
||||
// 1. 设置服务启停
|
||||
if (mTempConfigData.newBooleanValue) {
|
||||
ControlCenterService.startControlCenterService(mContext);
|
||||
} else {
|
||||
ControlCenterService.stopControlCenterService(mContext);
|
||||
}
|
||||
// 2. 强制触发 Activity 回调,执行服务启停逻辑
|
||||
mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue);
|
||||
LogUtils.d(TAG, "服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
|
||||
break;
|
||||
// 充电提醒进度条
|
||||
case CHANGE_TYPE_CHARGE_SEEKBAR:
|
||||
mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue);
|
||||
mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue);
|
||||
LogUtils.d(TAG, "充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
|
||||
break;
|
||||
// 耗电提醒进度条
|
||||
case CHANGE_TYPE_USAGE_SEEKBAR:
|
||||
mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue);
|
||||
mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue);
|
||||
LogUtils.d(TAG, "耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "未知变更类型,跳过");
|
||||
break;
|
||||
}
|
||||
|
||||
// 确认完成,清空临时数据
|
||||
mTempConfigData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致)
|
||||
*/
|
||||
private void cancelConfigChange() {
|
||||
LogUtils.d(TAG, "cancelConfigChange() | mTempConfigData=" + mTempConfigData);
|
||||
if (mTempConfigData == null || mAppConfigUtils == null) {
|
||||
LogUtils.e(TAG, "依赖数据为空,取消失败");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mTempConfigData.changeType) {
|
||||
case CHANGE_TYPE_CHARGE_SWITCH:
|
||||
if (cbEnableChargeReminder != null) {
|
||||
cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue);
|
||||
}
|
||||
LogUtils.d(TAG, "充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
break;
|
||||
case CHANGE_TYPE_USAGE_SWITCH:
|
||||
if (cbEnableUsageReminder != null) {
|
||||
cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue);
|
||||
}
|
||||
LogUtils.d(TAG, "耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
break;
|
||||
case CHANGE_TYPE_SERVICE_SWITCH:
|
||||
if (swEnableService != null) {
|
||||
swEnableService.setChecked(mTempConfigData.originalBooleanValue);
|
||||
}
|
||||
LogUtils.d(TAG, "服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
|
||||
break;
|
||||
case CHANGE_TYPE_CHARGE_SEEKBAR:
|
||||
if (sbChargeReminder != null) {
|
||||
sbChargeReminder.setProgress(mTempConfigData.originalIntValue);
|
||||
}
|
||||
if (tvChargeReminderValue != null && mChargeReminderBatteryDrawable != null && ivChargeReminderBattery != null) {
|
||||
mChargeReminderBatteryDrawable.setBatteryValue(mTempConfigData.originalIntValue);
|
||||
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
|
||||
tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
|
||||
break;
|
||||
case CHANGE_TYPE_USAGE_SEEKBAR:
|
||||
if (sbUsageReminder != null) {
|
||||
sbUsageReminder.setProgress(mTempConfigData.originalIntValue);
|
||||
}
|
||||
if (tvUsageReminderValue != null && mUsageReminderBatteryDrawable != null && ivUsageReminderBattery != null) {
|
||||
mUsageReminderBatteryDrawable.setBatteryValue(mTempConfigData.originalIntValue);
|
||||
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
|
||||
tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%");
|
||||
}
|
||||
LogUtils.d(TAG, "耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "未知变更类型,跳过");
|
||||
break;
|
||||
}
|
||||
|
||||
// 取消完成,清空临时数据
|
||||
mTempConfigData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效)
|
||||
*/
|
||||
private void updateDialogMessageByChangeType() {
|
||||
LogUtils.d(TAG, "updateDialogMessageByChangeType() | mTempConfigData=" + mTempConfigData);
|
||||
if (mDialogBuilder == null || mTempConfigData == null) return;
|
||||
String message;
|
||||
if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) {
|
||||
// 服务开关差异化提示语
|
||||
message = mTempConfigData.newBooleanValue ?
|
||||
"启用服务后,将后台持续监控电池状态,是否确认?" :
|
||||
"禁用服务后,电池监控功能将停止,是否确认?";
|
||||
} else {
|
||||
// 普通配置默认提示语
|
||||
message = "是否确认修改当前配置?";
|
||||
}
|
||||
// 通过 Builder 设置消息,确保弹窗显示最新内容
|
||||
mDialogBuilder.setMessage(message);
|
||||
}
|
||||
|
||||
// ======================== 内部工具方法(封装重复逻辑,提升复用性)========================
|
||||
/**
|
||||
* 实时计算并更新比值预览(sbUsageReminder / sbChargeReminder)
|
||||
* 处理除数为0的情况,避免崩溃
|
||||
*/
|
||||
// private void updateRatioPreview() {
|
||||
// if (mTvRatioPreview == null) return;
|
||||
// float ratio;
|
||||
// // 处理除数为0:充电进度为0时显示0(可根据需求改为“--”)
|
||||
// if (mCurrentChargeProgress == 0) {
|
||||
// ratio = 0.0f;
|
||||
// } else {
|
||||
// ratio = (float) mCurrentUsageProgress / mCurrentChargeProgress;
|
||||
// }
|
||||
// // 格式化比值:保留1位小数,适配本地化(解决小米手机小数分隔符问题)
|
||||
// String ratioText = String.format(Locale.getDefault(), "比值:%.1f", ratio);
|
||||
// mTvRatioPreview.setText(ratioText);
|
||||
// // 触发比值变化回调
|
||||
// if (mActionListener != null) {
|
||||
// mActionListener.onRatioChanged(ratio);
|
||||
// }
|
||||
// LogUtils.d(TAG, "比值预览更新 | usage=" + mCurrentUsageProgress + " | charge=" + mCurrentChargeProgress + " | ratio=" + ratio);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取资源颜色(适配 API30 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针)
|
||||
* @param colorResId 颜色资源 ID
|
||||
* @return 校准后的颜色值
|
||||
*/
|
||||
private int getResourceColor(int colorResId) {
|
||||
LogUtils.d(TAG, "getResourceColor() | colorResId=" + colorResId);
|
||||
// 空指针防御:Context 为空返回默认黑色
|
||||
if (mContext == null) {
|
||||
LogUtils.e(TAG, "Context 为空,返回默认黑色");
|
||||
return 0xFF000000;
|
||||
}
|
||||
// 适配 API30 主题颜色读取
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return mContext.getResources().getColor(colorResId, mContext.getTheme());
|
||||
} else {
|
||||
return mContext.getResources().getColor(colorResId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务启用状态(统一从服务控制Bean读取,确保全链路状态一致)
|
||||
* @return 服务启用状态(true=启用,false=禁用)
|
||||
*/
|
||||
private boolean getServiceEnableState() {
|
||||
LogUtils.d(TAG, "getServiceEnableState()");
|
||||
ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
|
||||
// 本地无配置时,默认禁用服务(与服务初始化逻辑对齐)
|
||||
boolean state = serviceBean != null && serviceBean.isEnableService();
|
||||
LogUtils.d(TAG, "服务启用状态获取完成 | state=" + state);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package cc.winboll.studio.powerbell.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.models.BackgroundBean;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/21 20:43
|
||||
* @Describe 单实例缓存版背景视图控件(基于Java7)
|
||||
* 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片
|
||||
* 新增:SP持久化最后加载路径、获取最后加载实例功能
|
||||
*/
|
||||
public class MemoryCachedBackgroundView extends BackgroundView {
|
||||
public static final String TAG = "MemoryCachedBackgroundView";
|
||||
// 静态属性:保存当前缓存的路径和实例(替代原Map,仅维护单实例)
|
||||
private static String sCachedImagePath;
|
||||
private static MemoryCachedBackgroundView sCachedView;
|
||||
// SP相关常量
|
||||
private static final String SP_NAME = "MemoryCachedBackgroundView_SP";
|
||||
private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path";
|
||||
|
||||
// ====================================== 构造器(继承并兼容父类) ======================================
|
||||
private MemoryCachedBackgroundView(Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "构造器1:创建MemoryCachedBackgroundView实例");
|
||||
}
|
||||
|
||||
private MemoryCachedBackgroundView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "构造器2:创建MemoryCachedBackgroundView实例");
|
||||
}
|
||||
|
||||
private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "构造器3:创建MemoryCachedBackgroundView实例");
|
||||
}
|
||||
|
||||
// ====================================== 核心静态方法:获取/创建缓存实例 ======================================
|
||||
/**
|
||||
* 从缓存获取或创建MemoryCachedBackgroundView实例
|
||||
* @param context 上下文
|
||||
* @param imagePath 图片绝对路径(作为缓存标识)
|
||||
* @param isReload 是否强制重新加载图片(路径匹配时仍刷新)
|
||||
* @return 缓存/新创建的MemoryCachedBackgroundView实例
|
||||
*/
|
||||
public static MemoryCachedBackgroundView getInstance(Context context, String imagePath, boolean isReload) {
|
||||
LogUtils.d(TAG, "getInstance() 调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload);
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "getInstance():图片路径为空,创建空实例");
|
||||
return new MemoryCachedBackgroundView(context);
|
||||
}
|
||||
|
||||
// 1. 路径匹配缓存 → 判断是否强制重载
|
||||
if (imagePath.equals(sCachedImagePath) && sCachedView != null) {
|
||||
LogUtils.d(TAG, "getInstance():路径已缓存,当前缓存实例有效");
|
||||
if (isReload) {
|
||||
LogUtils.d(TAG, "getInstance():强制重载图片 | " + imagePath);
|
||||
sCachedView.loadImage(imagePath);
|
||||
} else {
|
||||
LogUtils.d(TAG, "getInstance():使用缓存实例,无需重载 | " + imagePath);
|
||||
}
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存
|
||||
LogUtils.d(TAG, "getInstance():路径未缓存,新建实例 | " + imagePath);
|
||||
sCachedView = new MemoryCachedBackgroundView(context);
|
||||
sCachedImagePath = imagePath;
|
||||
sCachedView.loadImage(imagePath);
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// ====================================== 新增功能:获取最后加载的实例 ======================================
|
||||
/**
|
||||
* 获取最后一次loadImage的路径对应的实例
|
||||
* 无实例则创建并加载图片,同时更新静态缓存
|
||||
* @param context 上下文
|
||||
* @return 最后加载路径对应的实例
|
||||
*/
|
||||
public static MemoryCachedBackgroundView getLastInstance(Context context) {
|
||||
LogUtils.d(TAG, "getLastInstance() 调用");
|
||||
// 1. 从SP获取最后加载的路径
|
||||
String lastPath = getLastLoadImagePath(context);
|
||||
if (TextUtils.isEmpty(lastPath)) {
|
||||
LogUtils.e(TAG, "getLastInstance():无最后加载路径,创建空实例");
|
||||
return new MemoryCachedBackgroundView(context);
|
||||
}
|
||||
|
||||
// 2. 路径匹配当前缓存 → 直接返回
|
||||
if (lastPath.equals(sCachedImagePath) && sCachedView != null) {
|
||||
LogUtils.d(TAG, "getLastInstance():使用最后路径缓存实例 | " + lastPath);
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// 3. 路径不匹配 → 新建实例并更新缓存
|
||||
LogUtils.d(TAG, "getLastInstance():最后路径未缓存,新建实例并加载 | " + lastPath);
|
||||
sCachedView = new MemoryCachedBackgroundView(context);
|
||||
sCachedImagePath = lastPath;
|
||||
sCachedView.loadImage(lastPath);
|
||||
return sCachedView;
|
||||
}
|
||||
|
||||
// ====================================== 工具方法:SP持久化最后加载路径 ======================================
|
||||
/**
|
||||
* 保存最后一次loadImage的路径到SP
|
||||
* @param context 上下文
|
||||
* @param imagePath 图片路径
|
||||
*/
|
||||
private static void saveLastLoadImagePath(Context context, String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath) || context == null) {
|
||||
return;
|
||||
}
|
||||
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
sp.edit().putString(KEY_LAST_LOAD_IMAGE_PATH, imagePath).apply();
|
||||
LogUtils.d(TAG, "saveLastLoadImagePath():已保存最后路径 | " + imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SP获取最后一次loadImage的路径
|
||||
* @param context 上下文
|
||||
* @return 最后加载的图片路径,空则返回null
|
||||
*/
|
||||
public static String getLastLoadImagePath(Context context) {
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
String lastPath = sp.getString(KEY_LAST_LOAD_IMAGE_PATH, null);
|
||||
LogUtils.d(TAG, "getLastLoadImagePath():获取最后路径 | " + lastPath);
|
||||
return lastPath;
|
||||
}
|
||||
|
||||
// ====================================== 工具方法:缓存管理 ======================================
|
||||
/**
|
||||
* 清除当前缓存实例和路径
|
||||
*/
|
||||
public static void clearCache() {
|
||||
LogUtils.d(TAG, "clearCache() 调用 | 当前缓存路径:" + sCachedImagePath);
|
||||
sCachedView = null;
|
||||
sCachedImagePath = null;
|
||||
LogUtils.d(TAG, "clearCache():已清除当前缓存实例");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定路径的缓存(仅当路径匹配当前缓存时生效)
|
||||
* @param imagePath 图片路径
|
||||
*/
|
||||
public static void removeCache(String imagePath) {
|
||||
LogUtils.d(TAG, "removeCache() 调用 | 图片路径:" + imagePath);
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
LogUtils.e(TAG, "removeCache():图片路径为空,清除失败");
|
||||
return;
|
||||
}
|
||||
if (imagePath.equals(sCachedImagePath)) {
|
||||
clearCache();
|
||||
// 同步删除SP中最后路径记录
|
||||
clearLastLoadImagePath(getContextFromCache());
|
||||
LogUtils.d(TAG, "removeCache():已清除匹配路径的缓存 | " + imagePath);
|
||||
} else {
|
||||
LogUtils.d(TAG, "removeCache():路径不匹配当前缓存,无需清除 | " + imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存(同clearCache,保持方法兼容性)
|
||||
*/
|
||||
public static void clearAllCache() {
|
||||
LogUtils.d(TAG, "clearAllCache() 调用");
|
||||
clearCache();
|
||||
clearLastLoadImagePath(getContextFromCache());
|
||||
LogUtils.d(TAG, "clearAllCache():已清除所有缓存及最后路径记录");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在缓存实例
|
||||
* @return 存在返回true,否则返回false
|
||||
*/
|
||||
public static boolean hasCache() {
|
||||
return sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除SP中最后加载的路径记录
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void clearLastLoadImagePath(Context context) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
sp.edit().remove(KEY_LAST_LOAD_IMAGE_PATH).apply();
|
||||
LogUtils.d(TAG, "clearLastLoadImagePath():已清除最后路径记录");
|
||||
}
|
||||
|
||||
// ====================================== 辅助方法:从缓存获取上下文 ======================================
|
||||
/**
|
||||
* 从缓存实例中获取上下文(用于无外部上下文时的SP操作)
|
||||
* @return 上下文实例,无则返回null
|
||||
*/
|
||||
private static Context getContextFromCache() {
|
||||
return sCachedView != null ? sCachedView.getContext() : null;
|
||||
}
|
||||
|
||||
// ====================================== 重写父类方法:增强日志+SP持久化 ======================================
|
||||
@Override
|
||||
public void loadImage(String imagePath) {
|
||||
LogUtils.d(TAG, "loadImage() 重载方法调用 | 图片路径:" + imagePath);
|
||||
super.loadImage(imagePath);
|
||||
// 保存最后加载路径到SP
|
||||
saveLastLoadImagePath(getContext(), imagePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadByBackgroundBean(BackgroundBean bean) {
|
||||
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()));
|
||||
super.loadByBackgroundBean(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
|
||||
super.loadByBackgroundBean(bean, isRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,92 +5,227 @@ import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.SeekBar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/17 14:11
|
||||
* @Describe 垂直进度条控件,适配 API30,支持逆时针旋转(0在下,100在上),修复滑块同步+弹窗触发bug
|
||||
* 新增:实时进度变化监听接口,支持拖动时实时回调进度
|
||||
*/
|
||||
public class VerticalSeekBar extends SeekBar {
|
||||
public static final String TAG = VerticalSeekBar.class.getSimpleName();
|
||||
// ======================== 静态常量 =========================
|
||||
private static final String TAG = VerticalSeekBar.class.getSimpleName();
|
||||
|
||||
public volatile int _mnProgress = -1;
|
||||
|
||||
public VerticalSeekBar(Context context) {
|
||||
super(context);
|
||||
// ======================== 接口定义(前置,便于外部调用)========================
|
||||
/**
|
||||
* 垂直进度条触摸事件回调接口,解决原生 OnSeekBarChangeListener 回调失效问题
|
||||
* 直接在触摸抬起时回调,确保配置变更对话框100%触发
|
||||
*/
|
||||
public interface OnVerticalSeekBarTouchListener {
|
||||
/**
|
||||
* 触摸抬起时回调(滑块停止滑动,触发弹窗的核心时机)
|
||||
* @param seekBar 当前垂直进度条实例
|
||||
* @param progress 最终滑动进度(0~100)
|
||||
*/
|
||||
void onTouchUp(VerticalSeekBar seekBar, int progress);
|
||||
|
||||
/**
|
||||
* 触摸取消时回调(可选,用于异常场景进度回滚)
|
||||
* @param seekBar 当前垂直进度条实例
|
||||
* @param progress 取消时的进度
|
||||
*/
|
||||
void onTouchCancel(VerticalSeekBar seekBar, int progress);
|
||||
}
|
||||
|
||||
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
/**
|
||||
* 垂直进度条实时进度变化监听接口
|
||||
* 支持拖动过程中实时回调进度,用于比值预览等实时UI更新场景
|
||||
*/
|
||||
public interface OnVerticalSeekBarChangeListener {
|
||||
/**
|
||||
* 进度变化时回调
|
||||
* @param seekBar 当前垂直进度条实例
|
||||
* @param progress 当前进度(0~100)
|
||||
* @param fromUser 是否是用户触摸导致的进度变化
|
||||
*/
|
||||
void onProgressChanged(VerticalSeekBar seekBar, int progress, boolean fromUser);
|
||||
|
||||
/**
|
||||
* 开始触摸进度条时回调
|
||||
* @param seekBar 当前垂直进度条实例
|
||||
*/
|
||||
void onStartTrackingTouch(VerticalSeekBar seekBar);
|
||||
|
||||
/**
|
||||
* 停止触摸进度条时回调
|
||||
* @param seekBar 当前垂直进度条实例
|
||||
*/
|
||||
void onStopTrackingTouch(VerticalSeekBar seekBar);
|
||||
}
|
||||
|
||||
// ======================== 成员变量 =========================
|
||||
// 核心状态:当前进度缓存,修复滑块同步问题(volatile 保证多线程可见性)
|
||||
private volatile int mProgress = -1;
|
||||
// 监听接口:触摸事件回调(原有,用于弹窗触发)
|
||||
private OnVerticalSeekBarTouchListener mTouchListener;
|
||||
// 监听接口:实时进度变化回调(新增,用于比值计算)
|
||||
private OnVerticalSeekBarChangeListener mProgressChangeListener;
|
||||
|
||||
// ======================== 构造方法 =========================
|
||||
public VerticalSeekBar(Context context) {
|
||||
super(context);
|
||||
initView();
|
||||
LogUtils.d(TAG, "VerticalSeekBar(Context) 初始化");
|
||||
}
|
||||
|
||||
public VerticalSeekBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
// 去除冗余的水平阴影
|
||||
initView();
|
||||
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet) 初始化");
|
||||
}
|
||||
|
||||
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initView();
|
||||
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet, int) 初始化");
|
||||
}
|
||||
|
||||
// ======================== 初始化方法 =========================
|
||||
private void initView() {
|
||||
// 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销
|
||||
setBackgroundDrawable(null);
|
||||
|
||||
LogUtils.d(TAG, "initView: 移除默认背景阴影,完成视图初始化");
|
||||
}
|
||||
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(h, w, oldh, oldw);
|
||||
// ======================== 对外设置方法(监听接口绑定)========================
|
||||
/**
|
||||
* 设置触摸事件监听器(给外部调用,如 MainContentView 绑定)
|
||||
* @param listener 触摸事件回调实例
|
||||
*/
|
||||
public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener listener) {
|
||||
this.mTouchListener = listener;
|
||||
LogUtils.d(TAG, "setOnVerticalSeekBarTouchListener: 触摸监听器绑定完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实时进度变化监听器(给外部调用,如 MainContentView 绑定)
|
||||
* @param listener 实时进度变化回调实例
|
||||
*/
|
||||
public void setOnVerticalSeekBarChangeListener(OnVerticalSeekBarChangeListener listener) {
|
||||
this.mProgressChangeListener = listener;
|
||||
LogUtils.d(TAG, "setOnVerticalSeekBarChangeListener: 实时进度监听器绑定完成");
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ======================== 重写系统方法(测量/布局/绘制)========================
|
||||
@Override
|
||||
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
|
||||
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
|
||||
LogUtils.v(TAG, "onMeasure: 垂直测量完成,宽=" + getMeasuredHeight() + ", 高=" + getMeasuredWidth());
|
||||
}
|
||||
|
||||
protected void onDraw(Canvas c) {
|
||||
// 0--------100,顺时针旋转,小在上
|
||||
// c.rotate(+90);
|
||||
// c.translate(0, -getWidth());
|
||||
|
||||
// 0--------100,逆时针旋转,小在下
|
||||
c.rotate(-90);
|
||||
c.translate(-getHeight(), 0);
|
||||
|
||||
super.onDraw(c);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// 调用基类的处理函数
|
||||
// 该方法可以使得
|
||||
// SeekBar.OnSeekBarChangeListener
|
||||
// 的 onStopTrackingTouch 和 onStartTrackingTouch 等函数有效。
|
||||
boolean handled = super.onTouchEvent(event);
|
||||
|
||||
if (handled) {
|
||||
int action = event.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// 0--------100,顺时针旋转,小在上
|
||||
//_mnProgress = (int)(getMax() * event.getY() / getHeight());
|
||||
// // 0--------100,逆时针旋转,小在下
|
||||
_mnProgress = getMax() - (int) (getMax() * event.getY() / getHeight());
|
||||
_mnProgress = _mnProgress > 100 ? 100 : _mnProgress ;
|
||||
//LogUtils.d(TAG, "_mnProgress is " + Integer.toString(_mnProgress));
|
||||
setProgress(_mnProgress);
|
||||
//onSizeChanged(getWidth(), getHeight(), 0, 0);
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
break;
|
||||
default :
|
||||
//LogUtils.d(TAG, "event.getAction() is " + event.getAction());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(h, w, oldh, oldw);
|
||||
LogUtils.v(TAG, "onSizeChanged: 尺寸变化,新宽=" + h + ", 新高=" + w);
|
||||
}
|
||||
|
||||
// 解决调用setProgress()方法时滑块不跟随的bug
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
// 逆时针旋转90度,平移画布避免绘制偏移(0在下,100在上)
|
||||
canvas.rotate(-90);
|
||||
canvas.translate(-getHeight(), 0);
|
||||
super.onDraw(canvas);
|
||||
LogUtils.v(TAG, "onDraw: 完成垂直绘制,旋转角度=-90°");
|
||||
}
|
||||
|
||||
// ======================== 重写进度设置方法(修复滑块同步+新增实时回调)========================
|
||||
/**
|
||||
* 重写进度设置,调用尺寸变化方法强制刷新,解决 setProgress 滑块不跟随问题
|
||||
* 新增:支持外部调用 setProgress 时触发实时进度回调
|
||||
*/
|
||||
@Override
|
||||
public synchronized void setProgress(int progress) {
|
||||
super.setProgress(progress);
|
||||
// 强制触发尺寸变化,同步刷新滑块位置(核心bug修复逻辑)
|
||||
onSizeChanged(getWidth(), getHeight(), 0, 0);
|
||||
mProgress = progress;
|
||||
LogUtils.d(TAG, "setProgress: 进度设置为" + progress + ",滑块同步刷新");
|
||||
// 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false)
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onProgressChanged(this, progress, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 重写触摸事件(优化事件透传+实时进度回调)========================
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// 先调用父类方法,保留原生监听器兼容性,同时强制透传事件
|
||||
super.onTouchEvent(event);
|
||||
boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失
|
||||
boolean fromUser = true; // 标记是否是用户触摸导致的进度变化
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
LogUtils.d(TAG, "onTouchEvent: 触摸按下,Y坐标=" + event.getY());
|
||||
// 触发实时进度监听:开始触摸
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onStartTrackingTouch(this);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
calculateProgress(event.getY());
|
||||
setProgress(mProgress);
|
||||
LogUtils.v(TAG, "onTouchEvent: 触摸滑动,进度更新为" + mProgress);
|
||||
// 触发实时进度监听:进度变化
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
calculateProgress(event.getY());
|
||||
setProgress(mProgress);
|
||||
LogUtils.d(TAG, "onTouchEvent: 触摸抬起,进度=" + mProgress + ",触发弹窗回调");
|
||||
// 触发实时进度监听:停止触摸
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
|
||||
mProgressChangeListener.onStopTrackingTouch(this);
|
||||
}
|
||||
// 核心:调用原有触摸接口,通知外部触发配置变更对话框
|
||||
if (mTouchListener != null) {
|
||||
mTouchListener.onTouchUp(this, mProgress);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
LogUtils.d(TAG, "onTouchEvent: 触摸取消,当前进度=" + getProgress());
|
||||
// 触发实时进度监听:停止触摸
|
||||
if (mProgressChangeListener != null) {
|
||||
mProgressChangeListener.onStopTrackingTouch(this);
|
||||
}
|
||||
// 可选:触摸取消时回调,外部可做进度回滚处理
|
||||
if (mTouchListener != null) {
|
||||
mTouchListener.onTouchCancel(this, getProgress());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
// ======================== 内部工具方法 =========================
|
||||
/**
|
||||
* 计算垂直进度,校准范围 0~100,避免异常值
|
||||
* @param touchY 触摸点Y坐标
|
||||
*/
|
||||
private void calculateProgress(float touchY) {
|
||||
// 核心进度计算公式(逆时针旋转适配)
|
||||
mProgress = getMax() - (int) (getMax() * touchY / getHeight());
|
||||
// 校准进度范围,防止超出 0~100(兼容 API30 进度边界校验)
|
||||
mProgress = Math.max(0, Math.min(mProgress, getMax()));
|
||||
LogUtils.v(TAG, "calculateProgress: 触摸Y=" + touchY + ",计算进度=" + mProgress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
powerbell/src/main/res/drawable/blank100x100.png
Normal file
BIN
powerbell/src/main/res/drawable/blank100x100.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 B |
Binary file not shown.
|
Before Width: | Height: | Size: 100 B |
7
powerbell/src/main/res/drawable/btn_bg_gray.xml
Normal file
7
powerbell/src/main/res/drawable/btn_bg_gray.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/darker_gray" />
|
||||
<corners android:radius="6dp" />
|
||||
</shape>
|
||||
|
||||
7
powerbell/src/main/res/drawable/btn_bg_primary.xml
Normal file
7
powerbell/src/main/res/drawable/btn_bg_primary.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/holo_blue_light" />
|
||||
<corners android:radius="6dp" />
|
||||
</shape>
|
||||
|
||||
20
powerbell/src/main/res/drawable/btn_brightness_bg.xml
Normal file
20
powerbell/src/main/res/drawable/btn_brightness_bg.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按压状态:浅灰色背景 -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#E0E0E0" /> <!-- 按压深色 -->
|
||||
<corners android:radius="8dp" /> <!-- 圆角适配小米UI风格 -->
|
||||
<stroke android:width="1dp" android:color="#CCCCCC" /> <!-- 边框 -->
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 正常状态:白色背景 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" /> <!-- 正常白色 -->
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="1dp" android:color="#CCCCCC" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
18
powerbell/src/main/res/drawable/btn_cancel_bg.xml
Normal file
18
powerbell/src/main/res/drawable/btn_cancel_bg.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按压状态:深灰色背景 -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#D0D0D0" />
|
||||
<corners android:radius="8dp" /> <!-- 与亮度按钮圆角一致,统一风格 -->
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 正常状态:浅灰色背景 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#F0F0F0" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
20
powerbell/src/main/res/drawable/btn_common.xml
Normal file
20
powerbell/src/main/res/drawable/btn_common.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按压状态:深灰色 -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#CCCCCC" /> <!-- 按压时颜色 -->
|
||||
<corners android:radius="8dp" /> <!-- 圆角(可按需调整) -->
|
||||
<stroke android:width="1dp" android:color="#EEEEEE" /> <!-- 边框(可选,不加可删除) -->
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 正常状态:浅灰色 -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#F5F5F5" /> <!-- 正常时颜色 -->
|
||||
<corners android:radius="8dp" /> <!-- 圆角(和按压状态一致) -->
|
||||
<stroke android:width="1dp" android:color="#EEEEEE" /> <!-- 边框(可选) -->
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
18
powerbell/src/main/res/drawable/btn_confirm_bg.xml
Normal file
18
powerbell/src/main/res/drawable/btn_confirm_bg.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按压状态:加深深色背景 -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#2D7CFF" /> <!-- 按压深蓝 -->
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 正常状态:主色背景(可改为项目主题色) -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#4096FF" /> <!-- 正常浅蓝,适配小米系统UI -->
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
BIN
powerbell/src/main/res/drawable/color_scale_logo.png
Normal file
BIN
powerbell/src/main/res/drawable/color_scale_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
17
powerbell/src/main/res/drawable/dialog_bg_radius.xml
Normal file
17
powerbell/src/main/res/drawable/dialog_bg_radius.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<!-- 背景色:白色 -->
|
||||
<solid android:color="@android:color/white" />
|
||||
<!-- 圆角:12dp(适配小米机型圆角无锯齿) -->
|
||||
<corners android:radius="12dp" />
|
||||
<!-- 边框:浅灰色细边框(避免弹窗边缘模糊) -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@android:color/darker_gray" />
|
||||
<!-- 内边距:轻微留白,避免内容贴边 -->
|
||||
<padding
|
||||
android:bottom="5dp"
|
||||
android:top="5dp" />
|
||||
</shape>
|
||||
|
||||
10
powerbell/src/main/res/drawable/edittext_bg.xml
Normal file
10
powerbell/src/main/res/drawable/edittext_bg.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners android:radius="6dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@android:color/darker_gray" />
|
||||
</shape>
|
||||
|
||||
20
powerbell/src/main/res/drawable/seekbar_progress.xml
Normal file
20
powerbell/src/main/res/drawable/seekbar_progress.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 进度条未完成部分:浅灰色 -->
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/darker_gray" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 进度条已完成部分:系统蓝色(无需额外定义颜色) -->
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/holo_blue_light" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
15
powerbell/src/main/res/drawable/seekbar_thumb.xml
Normal file
15
powerbell/src/main/res/drawable/seekbar_thumb.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<!-- 滑块颜色:系统蓝色 -->
|
||||
<solid android:color="@android:color/holo_blue_light" />
|
||||
<!-- 滑块大小:20dp(适配小米机型触摸区域) -->
|
||||
<size
|
||||
android:width="20dp"
|
||||
android:height="20dp" />
|
||||
<!-- 白色边框:区分滑块与进度条 -->
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@android:color/white" />
|
||||
</shape>
|
||||
|
||||
@@ -13,138 +13,131 @@
|
||||
android:gravity="center_vertical"
|
||||
style="@style/DefaultAToolbar"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<RelativeLayout
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF28C000">
|
||||
android:id="@+id/background_view">
|
||||
|
||||
</cc.winboll.studio.powerbell.views.BackgroundView>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Origin BG"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton1"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Received BG"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton2"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF3243E2"
|
||||
android:id="@+id/background_view">
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="◎"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton3"/>
|
||||
|
||||
</cc.winboll.studio.powerbell.views.BackgroundView>
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="☑"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton4"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="♾"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton5"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:background="#B92FABE6">
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton6"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Origin BG"
|
||||
android:id="@+id/activitybackgroundpictureAButton5"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_margin="5dp"/>
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+~]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton7"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="Received BG"
|
||||
android:id="@+id/activitybackgroundpictureAButton4"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_margin="5dp"/>
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[◐]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton8"/>
|
||||
|
||||
</RelativeLayout>
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[©]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:onClick="onColorPaletteDialog"
|
||||
android:id="@+id/activitybackgroundsettingsAButton9"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="◎"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton1"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="☑"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton2"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="♾"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton9"
|
||||
android:onClick="onNetworkBackgroundDialog"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton3"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[+~]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton6"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[◐]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton7"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[○]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundpictureAButton8"/>
|
||||
|
||||
</LinearLayout>
|
||||
<cc.winboll.studio.libaes.views.AButton
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:text="[○]"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/activitybackgroundsettingsAButton10"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -21,20 +21,15 @@
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitymainRelativeLayout1"/>
|
||||
android:id="@+id/activitymainRelativeLayout1">
|
||||
|
||||
<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"
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/fragmentmainviewBackgroundView1"/>
|
||||
android:id="@+id/ll_backgroundview">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
@@ -237,13 +232,14 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
<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"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"
|
||||
android:layout_alignParentBottom="true"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
50
powerbell/src/main/res/layout/activity_mainunittest2.xml
Normal file
50
powerbell/src/main/res/layout/activity_mainunittest2.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF0C6BBF">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/ll_backgroundview">
|
||||
</LinearLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#AF4FDA4E">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Main"
|
||||
android:id="@+id/btn_main_activity"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TestCropImage"
|
||||
android:id="@+id/btn_test_cropimage"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
android:id="@+id/dialogbackgroundpicturepreviewImageView1"/>
|
||||
<cc.winboll.studio.powerbell.views.BackgroundView
|
||||
android:orientation="vertical"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:id="@+id/backgroundview"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
198
powerbell/src/main/res/layout/dialog_color_palette.xml
Normal file
198
powerbell/src/main/res/layout/dialog_color_palette.xml
Normal file
@@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp"
|
||||
android:background="#FFFFFFFF">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_color_picker"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:background="#FF0000"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_color_scaler"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/color_scale_logo"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="15dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="RGB:"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_r"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:hint="R"
|
||||
android:inputType="number"
|
||||
android:gravity="center"
|
||||
android:maxLength="3"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_g"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:hint="G"
|
||||
android:inputType="number"
|
||||
android:gravity="center"
|
||||
android:maxLength="3"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_b"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:hint="B"
|
||||
android:inputType="number"
|
||||
android:gravity="center"
|
||||
android:maxLength="3"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_color_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:hint="#AARRGGBB"
|
||||
android:inputType="text"
|
||||
android:gravity="center"
|
||||
android:maxLength="9"
|
||||
android:layout_marginBottom="15dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="15dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="透明度:"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_alpha_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="100%"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginLeft="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/sb_alpha"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="100"
|
||||
android:progress="100"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginBottom="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_brightness_minus"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="-"
|
||||
android:textSize="20sp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/btn_common"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_brightness_value"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="100%"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_brightness_plus"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="+"
|
||||
android:textSize="20sp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/btn_common"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cancel"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="45dp"
|
||||
android:text="取消"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/btn_common"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_marginRight="20dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_confirm"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="45dp"
|
||||
android:text="确认"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/btn_common"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -13,11 +13,20 @@
|
||||
android:id="@+id/tv_dialog_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="网络后台提示"
|
||||
android:text="网络图片资源下载"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_dialog_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="请输入网络图片地址:"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@android:color/darker_gray"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
@@ -54,15 +63,6 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_dialog_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="应用正在后台使用网络,是否继续允许?"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@android:color/darker_gray"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -74,7 +74,7 @@
|
||||
android:id="@+id/btn_cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="取消"
|
||||
android:text="关闭返回"
|
||||
android:textSize="14sp"
|
||||
android:background="@android:drawable/btn_default_small"
|
||||
android:layout_marginRight="8dp"/>
|
||||
@@ -83,7 +83,7 @@
|
||||
android:id="@+id/btn_confirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="允许"
|
||||
android:text="使用图片"
|
||||
android:textSize="14sp"
|
||||
android:background="@android:drawable/btn_default_small"/>
|
||||
|
||||
|
||||
9
powerbell/src/main/res/layout/layout_color_grid.xml
Normal file
9
powerbell/src/main/res/layout/layout_color_grid.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:padding="10dp">
|
||||
</LinearLayout>
|
||||
|
||||
7
powerbell/src/main/res/layout/view_ads_banner.xml
Normal file
7
powerbell/src/main/res/layout/view_ads_banner.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 广告视图独立布局(供ViewStub延迟加载) -->
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/adsbanner"/>
|
||||
@@ -2,11 +2,12 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_log"
|
||||
android:title="@string/item_logview"/>
|
||||
<item
|
||||
android:id="@+id/action_unittestactivity"
|
||||
android:title="@string/item_mainunittestactivity"/>
|
||||
|
||||
android:title="MainUnitTestActivity"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_unittest2activity"
|
||||
android:title="MainUnitTest2Activity"/>
|
||||
|
||||
</menu>
|
||||
|
||||
@@ -58,9 +58,73 @@
|
||||
<color name="colorUsege">@color/colorRed</color>
|
||||
<color name="colorCurrent">@color/colorBlue</color>
|
||||
<color name="colorCharge">@color/colorYellow</color>
|
||||
|
||||
|
||||
<!--CustomSlideToUnlockView控件配置-->
|
||||
<color name="colorCustomSlideToUnlockViewWhite">#FFFFFFFF</color>
|
||||
|
||||
<!---->
|
||||
|
||||
<!-- ============== 基础黑白(必含,适配文字/背景) ============== -->
|
||||
<color name="white">#FFFFFF</color> <!-- 纯白色(文字/背景) -->
|
||||
<color name="black">#000000</color> <!-- 近黑色(重要文字) -->
|
||||
|
||||
<!-- ============== 基础色系(按钮/强调色常用) ============== -->
|
||||
<!-- 蓝色系(常用:确认/链接/主题色) -->
|
||||
<color name="blue_light">#4A90E2</color> <!-- 浅蓝(次要按钮) -->
|
||||
<color name="blue_normal">#2196F3</color> <!-- 标准蓝(主题/确认按钮) -->
|
||||
<color name="blue_dark">#1976D2</color> <!-- 深蓝(按压态/重要强调) -->
|
||||
<!-- 绿色系(常用:成功/完成/安全提示) -->
|
||||
<color name="green_light">#66BB6A</color> <!-- 浅绿(次要成功态) -->
|
||||
<color name="green_normal">#4CAF50</color> <!-- 标准绿(成功按钮/提示) -->
|
||||
<color name="green_dark">#388E3C</color> <!-- 深绿(按压态/重要成功) -->
|
||||
<!-- 红色系(常用:错误/警告/删除按钮) -->
|
||||
<color name="red_light">#EF5350</color> <!-- 浅红(次要错误提示) -->
|
||||
<color name="red_normal">#F44336</color> <!-- 标准红(删除/错误按钮) -->
|
||||
<color name="red_dark">#D32F2F</color> <!-- 深红(按压态/重要错误) -->
|
||||
<!-- 黄色系(常用:警告/提醒/高亮) -->
|
||||
<color name="yellow_light">#FFF59D</color> <!-- 浅黄(次要提醒) -->
|
||||
<color name="yellow_normal">#FFC107</color> <!-- 标准黄(警告提示/高亮) -->
|
||||
<color name="yellow_dark">#FFA000</color> <!-- 深黄(重要警告) -->
|
||||
<!-- 橙色系(常用:提醒/进度/活力色) -->
|
||||
<color name="orange_normal">#FF9800</color> <!-- 标准橙(提醒按钮/进度) -->
|
||||
<!-- 紫色系(常用:特殊强调/个性按钮) -->
|
||||
<color name="purple_normal">#9C27B0</color> <!-- 标准紫(特殊功能按钮) -->
|
||||
|
||||
<!-- ============== 透明色(遮罩/背景叠加) ============== -->
|
||||
<color name="transparent">#00000000</color> <!-- 全透明 -->
|
||||
<color name="black_transparent_50">#80000000</color> <!-- 50%透明黑(遮罩) -->
|
||||
|
||||
|
||||
|
||||
<!-- 1. 不透明灰色(常用深浅梯度,直接用) -->
|
||||
<color name="gray_100">#F5F5F5</color> <!-- 极浅灰(接近白色,背景用) -->
|
||||
<color name="gray_200">#EEEEEE</color> <!-- 浅灰(卡片/分割线背景) -->
|
||||
<color name="gray_300">#E0E0E0</color> <!-- 中浅灰(边框/次要背景) -->
|
||||
<color name="gray_400">#BDBDBD</color> <!-- 中灰(次要文字/图标) -->
|
||||
<color name="gray_500">#9E9E9E</color> <!-- 标准中灰(常用辅助文字) -->
|
||||
<color name="gray_600">#757575</color> <!-- 中深灰(常规辅助文字) -->
|
||||
<color name="gray_700">#616161</color> <!-- 深灰(重要辅助文字) -->
|
||||
<color name="gray_800">#424242</color> <!-- 极深灰(接近黑色,标题副文本) -->
|
||||
<color name="gray_900">#212121</color> <!-- 近黑色(特殊场景用) -->
|
||||
|
||||
<!-- 2. 半透明灰色(带透明度,遮罩/蒙层用) -->
|
||||
<color name="gray_transparent_30">#4D9E9E9E</color> <!-- 30%透明中灰(A=4D) -->
|
||||
<color name="gray_transparent_50">#809E9E9E</color> <!-- 50%透明中灰(A=80) -->
|
||||
<color name="gray_transparent_70">#B39E9E9E</color> <!-- 70%透明中灰(A=B3) -->
|
||||
|
||||
<color name="gray_light">#EEE</color> <!-- 等价 #EEEEEE(浅灰) -->
|
||||
<color name="gray_mid">#999</color> <!-- 等价 #999999(中灰) -->
|
||||
<color name="gray_dark">#666</color> <!-- 等价 #666666(深灰) -->
|
||||
<color name="gray_black">#333</color> <!-- 等价 #333333(极深灰) -->
|
||||
|
||||
<!-- 50% 透明中灰(弹窗遮罩常用) -->
|
||||
<color name="mask_gray">#809E9E9E</color>
|
||||
<!-- 30% 透明深灰(背景叠加) -->
|
||||
<color name="bg_overlay_gray">#4D424242</color>
|
||||
|
||||
<!-- 1. 常规灰色(按钮默认态,常用中灰) -->
|
||||
<color name="btn_gray_normal">#9E9E9E</color>
|
||||
<!-- 2. 按压深色(按钮点击态,加深一级,提升交互感) -->
|
||||
<color name="btn_gray_pressed">#757575</color>
|
||||
<!-- 3. 禁用灰色(按钮不可点击态,浅灰) -->
|
||||
<color name="btn_gray_disabled">#E0E0E0</color>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -26,4 +26,18 @@
|
||||
<item name="android:textSize">@dimen/text_subtitle_size</item>
|
||||
</style>
|
||||
|
||||
<!-- 自定义调色板对话框样式 -->
|
||||
<style name="CustomDialogStyle" parent="@android:style/Theme.Dialog">
|
||||
<!-- 去除标题栏 -->
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<!-- 背景透明(避免小米机型弹窗周围黑边) -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<!-- 禁止弹窗全屏 -->
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<!-- 小米机型适配:弹窗大小自适应 -->
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<!-- 禁止触摸外部关闭(可选,避免误触) -->
|
||||
<item name="android:windowCloseOnTouchOutside">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user