Compare commits

..

78 Commits

Author SHA1 Message Date
ZhanGSKen 627fa5359e <aes>APK 15.20.20 release Publish. 2026-07-17 19:18:48 +08:00
BigPickle 6e01f45174 <libaes>feat: 新增SMTP邮件发送启用开关,禁用时sendMail跳过操作 2026-07-17 19:15:50 +08:00
ZhanGSKen 07f74c9f8f <libaes>Library Release 15.20.19 2026-07-17 15:03:22 +08:00
ZhanGSKen 2e0b51b9d2 <aes>APK 15.20.19 release Publish. 2026-07-17 15:03:05 +08:00
BigPickle f91b5b1b6a <libaes><aes>feat: 新增SMTP邮件发送功能(QQ邮箱兼容)
libaes模块:
- 新增 APPMSGMailUtils 邮件发送工具类,基于JavaMail SMTP协议
- 新增 SMTPConfigView 邮件配置视图控件,支持服务器/端口/发件人/授权码/收件人配置
- 新增 view_smtp_config.xml 布局文件
- 添加 android-mail:1.6.7 / android-activation:1.6.7 依赖(替代javax.mail解决Android AWT兼容问题)
- 添加 INTERNET / ACCESS_NETWORK_STATE 权限

aes模块:
- 新增 SMTPSettingsActivity 邮件设置页面
- 新增 activity_smtp_settings.xml 布局文件
- MainActivity toolbar菜单添加"SMTP设置"入口
- AndroidManifest.xml 注册 SMTPSettingsActivity
- build.gradle 添加 packagingOptions 解决META-INF重复文件冲突
2026-07-17 10:39:59 +08:00
ZhanGSKen 6657b39823 <libaes>Library Release 15.20.18 2026-07-11 20:07:17 +08:00
ZhanGSKen ae5fba531e <aes>APK 15.20.18 release Publish. 2026-07-11 20:07:00 +08:00
MiMo b59c274032 feat: 状态栏颜色跟随主题 colorPrimaryDark 属性动态变化
- 修改 AESTheme 主题 android:statusBarColor 从固定颜色改为引用 ?attr/colorPrimaryDark
- 同步修改 MyDebugActivityTheme 主题状态栏颜色配置
- 覆盖日间模式和夜间模式 (values/ values-night/)
- 主题切换时状态栏颜色自动跟随各主题定义的 colorPrimaryDark 值

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

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

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#42A5F5</color>
<color name="colorPrimaryDark">#1565C0</color>
<color name="colorAccent">#FFD740</color>
<color name="mainWindowBackgroundColor">#121212</color>
<color name="mainWindowTextColor">#E0E0E0</color>
<color name="toolbarTextColor">#FFFFFF</color>
<color name="toolbarBackgroundColor">#1E3A5F</color>
<color name="debugTextColor">#00E676</color>
<color name="colorTextBackgound">#121212</color>
</resources>
@@ -1,29 +0,0 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowDarkTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowDarkTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowDarkTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#1565C0</color>
<color name="colorPrimaryDark">#0D47A1</color>
<color name="colorAccent">#FF6F00</color>
<color name="mainWindowBackgroundColor">#FAFAFA</color>
<color name="mainWindowTextColor">#212121</color>
<color name="toolbarTextColor">#FFFFFF</color>
<color name="toolbarBackgroundColor">#1565C0</color>
<color name="debugTextColor">#616161</color>
<color name="colorTextBackgound">#FAFAFA</color>
</resources>
@@ -1,4 +0,0 @@
<resources>
<string name="app_name">MyWinBoLL</string>
</resources>
-37
View File
@@ -1,37 +0,0 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowDarkTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
<item name="colorTittle">?attr/mainWindowTextColor</item>
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>
-1
View File
@@ -1 +0,0 @@
/build
-35
View File
@@ -1,35 +0,0 @@
# Positions
#### 介绍
安卓位置应用,有关于地理位置的相关应用。
PS:使用感言~~~『記低用唔到』。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh positions
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档
-1
View File
@@ -1 +0,0 @@
-92
View File
@@ -1,92 +0,0 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 关键:改为你已安装的 SDK 32(≥ targetSdkVersion 30,兼容已安装环境)
compileSdkVersion 32
// 直接使用已安装的构建工具 33.0.3(无需修改)
buildToolsVersion "33.0.3"
defaultConfig {
applicationId "cc.winboll.studio.positions"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.12"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意:以下5个库必须要引入
//api 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// https://mvnrepository.com/artifact/com.jzxiang.pickerview/TimePickerDialog
api 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
// 谷歌定位服务核心依赖(FusedLocationProviderClient所在库)
api 'com.google.android.gms:play-services-location:21.0.1'
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// AndroidX 类库
api 'androidx.appcompat:appcompat:1.1.0'
api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.15.9'
api 'cc.winboll.studio:libappbase:15.15.21'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
api fileTree(dir: 'libs', include: ['*.jar'])
}
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jul 19 20:29:14 HKT 2026
stageCount=23
libraryProject=
baseVersion=15.12
publishVersion=15.12.22
buildCount=0
baseBetaVersion=15.12.23
-143
View File
@@ -1,143 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类(避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件(避免资源ID混淆)
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法(避免JNI调用失败)
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# ============================== 必要补充规则 ==============================
# OkHttp 4.4.1 补充规则(Java 7 兼容)
-keep class okhttp3.internal.concurrent.** { *; }
-keep class okhttp3.internal.connection.** { *; }
-dontwarn okhttp3.internal.concurrent.TaskRunner
-dontwarn okhttp3.internal.connection.RealCall
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
-keep class com.miui.zeus.** { *; }
-keep interface com.miui.zeus.** { *; }
# 保留米盟日志字段(便于广告加载失败排查)
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
public static final java.lang.String TAG;
}
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application
tools:replace="android:icon"
android:icon="@drawable/ic_launcher_beta">
<!-- Put flavor specific code here -->
</application>
</manifest>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">悟空笔记#</string>
<string name="appplus_name">时空任务#</string>
</resources>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Positions</string>
<string name="appplus_name">PositionsPlus+</string>
</resources>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="BaseBean"
path="BaseBean/" />
</paths>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<shortcut
android:shortcutId="open_appplus"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutShortLabel="@string/open_appplus"
android:shortcutLongLabel="@string/open_appplus"
android:shortcutDisabledMessage="@string/appplus_open_disabled">
<intent
android:action="cc.winboll.studio.positions.App.ACTION_OPEN_APPPLUS"
android:targetPackage="cc.winboll.studio.positions.beta"
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
android:data="open_appplus" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<shortcut
android:shortcutId="close_appplus"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutShortLabel="@string/close_appplus"
android:shortcutLongLabel="@string/close_appplus"
android:shortcutDisabledMessage="@string/appplus_close_disabled">
<intent
android:action="cc.winboll.studio.positions.App.ACTION_CLOSE_APPPLUS"
android:targetPackage="cc.winboll.studio.positions.beta"
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
android:data="close_appplus" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>
-164
View File
@@ -1,164 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.positions">
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 在后台使用位置信息 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 运行“location”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 安装快捷方式 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature
android:name="android.hardware.location.gps"
android:required="false"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.EDIT"/>
<data android:mimeType="application/json"/>
<data android:mimeType="text/x-json"/>
</intent-filter>
</activity>
<activity-alias
android:name=".MainActivityWukong"
android:targetActivity=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmain"/>
</activity-alias>
<activity-alias
android:name=".MainActivityLaojun"
android:targetActivity=".MainActivity"
android:exported="true"
android:label="@string/appplus_name"
android:icon="@drawable/ic_positions_plus"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsplus"/>
</activity-alias>
<activity android:name="cc.winboll.studio.positions.activities.LocationActivity"/>
<activity android:name="cc.winboll.studio.positions.activities.ShortcutActionActivity"/>
<service
android:name=".services.MainService"
android:exported="false"/>
<service
android:name=".services.AssistantService"
android:exported="false"/>
<service
android:name=".services.DistanceRefreshService"
android:exported="false"/>
<receiver android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver">
<intent-filter>
<action android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver"/>
</intent-filter>
</receiver>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<activity android:name="cc.winboll.studio.positions.activities.SettingsActivity"/>
<activity android:name="cc.winboll.studio.positions.activities.AboutActivity"/>
</application>
</manifest>
@@ -1,496 +0,0 @@
package cc.winboll.studio.positions;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.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.positions.services.MainService;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 全局Application应用入口类
* 功能概括:
* 1. 全局初始化管理、工具类框架初始化
* 2. 应用全局空转状态统一管理
* 3. IO读写通用工具封装
* 4. 全局崩溃捕获、崩溃日志本地保存、崩溃弹窗页面
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026/05/03 12:23:00
* @EditTime 2026/05/03 15:02:47
*/
public class App extends GlobalApplication {
//===================== 全局静态常量与变量 =====================
public static final String TAG = "App";
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
private static App sInstance;
private static final String SP_NAME = "app_idle_config";
private static final String SP_KEY_IDLE_RUNNING = "is_idle_running";
// 应用全局空转状态标记
private static boolean isAppIdleRunning = false;
//===================== 空转状态对外方法 =====================
/**
* 获取当前应用空转运行状态
* @return 是否处于空转状态
*/
public static boolean isAppIdleRunning() {
if (isAppIdleRunning) {
LogUtils.d(TAG, "isAppIdleRunning -> 当前应用处于空转运行状态");
}
return isAppIdleRunning;
}
/**
* 设置应用空转运行状态
* @param idleRunning 空转开关状态
*/
public static void setAppIdleRunning(boolean idleRunning) {
LogUtils.d(TAG, "setAppIdleRunning -> 传入参数 idleRunning = " + idleRunning);
if (isDebugging()) {
isAppIdleRunning = idleRunning;
saveIdleRunningToSp(idleRunning);
LogUtils.i(TAG, "setAppIdleRunning -> 调试模式,空转状态设置生效并持久化");
// 重启MainService服务以同步新的空转状态(stopService对未运行服务无效,故无需判断状态)
if (sInstance != null) {
LogUtils.i(TAG, "setAppIdleRunning -> 重启MainService服务以同步空转状态");
Intent serviceIntent = new Intent(sInstance, MainService.class);
sInstance.stopService(serviceIntent);
sInstance.startService(serviceIntent);
}
} else {
LogUtils.i(TAG, "setAppIdleRunning -> 非调试模式,空转设置无效");
LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless.");
}
}
/**
* 从SharedPreferences加载空转状态
*/
private static void loadIdleRunningFromSp() {
try {
if (sInstance != null) {
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
isAppIdleRunning = sp.getBoolean(SP_KEY_IDLE_RUNNING, false);
LogUtils.i(TAG, "loadIdleRunningFromSp -> 从SP加载空转状态:" + isAppIdleRunning);
}
} catch (Exception e) {
LogUtils.e(TAG, "loadIdleRunningFromSp -> 加载空转状态失败:" + e.getMessage());
isAppIdleRunning = false;
}
}
/**
* 保存空转状态到SharedPreferences
* @param idleRunning 空转状态
*/
private static void saveIdleRunningToSp(boolean idleRunning) {
try {
if (sInstance != null) {
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putBoolean(SP_KEY_IDLE_RUNNING, idleRunning).apply();
LogUtils.i(TAG, "saveIdleRunningToSp -> 空转状态已保存:" + idleRunning);
}
} catch (Exception e) {
LogUtils.e(TAG, "saveIdleRunningToSp -> 保存空转状态失败:" + e.getMessage());
}
}
//===================== 应用生命周期 =====================
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
LogUtils.i(TAG, "onCreate -> 全局Application初始化开始");
setIsDebugging(BuildConfig.DEBUG);
WinBoLLActivityManager.init(this);
ToastUtils.init(this);
loadIdleRunningFromSp();
LogUtils.i(TAG, "onCreate -> 全局组件初始化全部完成");
}
//===================== IO通用工具方法 =====================
/**
* 流式读写输入输出流
* @param input 输入流
* @param output 输出流
* @throws IOException IO读写异常
*/
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
/**
* 将字节数组写入指定文件
* @param file 目标文件
* @param data 字节数据
* @throws IOException IO读写异常
*/
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
/**
* 输入流转为UTF-8字符串
* @param input 输入流
* @return 转换后的文本
* @throws IOException IO读写异常
*/
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
/**
* 批量关闭IO流
* @param closeables 可关闭对象
*/
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException ignored) {
}
}
}
//===================== 全局崩溃捕获管理类 =====================
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
/**
* 注册全局崩溃捕获
*/
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
/**
* 注册全局崩溃并自定义崩溃日志保存目录
* @param crashDir 崩溃日志路径
*/
public void registerGlobal(Context context, String crashDir) {
LogUtils.d(TAG, "CrashHandler -> 注册全局异常捕获器");
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
/**
* 解除全局崩溃捕获
*/
public void unregister() {
LogUtils.d(TAG, "CrashHandler -> 解除全局异常捕获");
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
/**
* 注册局部前台异常捕获
*/
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
LogUtils.d(TAG, "CrashHandler -> 局部异常监听已注册");
}
/**
* 解除局部异常捕获
*/
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
LogUtils.d(TAG, "CrashHandler -> 局部异常监听已销毁");
}
}
/**
* 局部前台Looper异常捕获
*/
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
/**
* 全局未捕获异常处理实现
*/
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
}
/**
* 组装崩溃详细日志信息
*/
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {
}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS) : "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) {
builder.append("\n");
}
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(throwable.toString());
return builder.toString();
}
/**
* 写入崩溃日志到本地文件
*/
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
LogUtils.d(TAG, "崩溃日志已保存至本地文件");
} catch (Throwable e) {
LogUtils.e(TAG, "崩溃日志写入失败");
}
}
/**
* 获取系统内核版本信息
*/
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
//===================== 崩溃展示页面 =====================
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.i(TAG, "CrashActivity -> 崩溃展示页面已启动");
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
/**
* 重启当前应用
*/
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* dp转px通用方法
*/
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.copy) {
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
LogUtils.d(TAG, "CrashActivity -> 崩溃日志已复制到剪贴板");
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}
@@ -1,403 +0,0 @@
package cc.winboll.studio.positions;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.positions.activities.AboutActivity;
import cc.winboll.studio.positions.activities.LocationActivity;
import cc.winboll.studio.positions.activities.SettingsActivity;
import cc.winboll.studio.positions.activities.WinBoLLActivity;
import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
import cc.winboll.studio.positions.utils.ServiceUtil;
import cc.winboll.studio.positions.services.IdleGpsService;
import cc.winboll.studio.positions.services.MainService;
import cc.winboll.studio.positions.R;
import android.os.Handler;
import android.os.Looper;
import android.os.Handler;
import android.os.Handler;
import android.os.Looper;
import android.content.Intent;
import android.os.Handler;
/**
* 主页面控制器
* 功能简述:
* 1. 位置服务启停开关控制
* 2. 页面菜单跳转与主题管理
* 3. 应用空转状态接收与日志实时输出展示
* 4. 全局权限申请与权限结果回调处理
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026/05/03 12:23:00
* @EditTime 2026/05/03 15:42:15
*/
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ===================== 常量定义 =====================
public static final String TAG = "MainActivity";
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
// ===================== UI控件声明 =====================
private Toolbar mToolbar;
private Switch mServiceSwitch;
private Button mManagePositionsButton;
private ADsBannerView mADsBannerView;
private TextView mTvIdleLog;
private ScrollView mScrollIdleLog;
// ===================== 业务标记与回调 =====================
private boolean isServiceBound = false;
private OnAppIdleRunningListener mIdleRunningListener;
/**
* 应用空转状态回调内部接口
*/
public interface OnAppIdleRunningListener {
void onIdleStatusChange(boolean isRunning);
void onIdleLogReceive(String log);
}
// ===================== 回调绑定方法 =====================
public void setOnAppIdleRunningListener(OnAppIdleRunningListener listener) {
this.mIdleRunningListener = listener;
LogUtils.i(TAG, "setOnAppIdleRunningListener -> 空转监听绑定完成");
}
public OnAppIdleRunningListener getOnAppIdleRunningListener() {
return mIdleRunningListener;
}
// ===================== 生命周期重写 =====================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.i(TAG, "onCreate -> MainActivity页面创建完成");
initToolbar();
initViews();
setLLMainBackgroundColor();
if (!checkLocationPermissions()) {
LogUtils.d(TAG, "onCreate -> 定位权限未授予,发起权限申请");
requestLocationPermissions();
}
mADsBannerView = findViewById(R.id.adsbanner);
initAppIdleHandler();
refreshIdleLogLayout();
// 根据调试模式控制日志区域的显示
if (App.isDebugging()) {
mScrollIdleLog.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "onCreate -> 调试模式,显示日志区域");
} else {
mScrollIdleLog.setVisibility(View.GONE);
LogUtils.d(TAG, "onCreate -> 非调试模式,隐藏日志区域");
}
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume -> 页面恢复可见");
if (App.isAppIdleRunning()) {
IdleGpsService.getInstance().start();
}
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
}
// 重新加载菜单以根据当前调试状态刷新
invalidateOptionsMenu();
LogUtils.d(TAG, "onResume -> 重新加载菜单完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.i(TAG, "onDestroy -> MainActivity页面销毁释放资源");
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
}
mIdleRunningListener = null;
}
// ===================== 初始化相关方法 =====================
private void initToolbar() {
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(getString(R.string.app_name));
}
LogUtils.d(TAG, "initToolbar -> 顶部工具栏初始化完毕");
}
private void initViews() {
mTvIdleLog = (TextView) findViewById(R.id.tv_idle_log);
mScrollIdleLog = (ScrollView) findViewById(R.id.scroll_idle_log);
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control);
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
boolean serviceEnable = AppConfigsUtil.getInstance(this).isEnableMainService(true);
mServiceSwitch.setChecked(serviceEnable);
mManagePositionsButton.setEnabled(serviceEnable);
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
//切换开关时先校验权限,无权限直接强制关闭开关
if(isChecked && !checkLocationPermissions()){
mServiceSwitch.setChecked(false);
Toast.makeText(MainActivity.this,"未获取定位权限,无法开启GPS服务",Toast.LENGTH_SHORT).show();
return;
}
LogUtils.d(TAG, "onCheckedChanged -> 服务开关状态变更,isChecked = " + isChecked);
if (isChecked) {
ServiceUtil.startAutoService(MainActivity.this);
} else {
ServiceUtil.stopAutoService(MainActivity.this);
}
mManagePositionsButton.setEnabled(isChecked);
refreshManageButtonState();
}
});
LogUtils.i(TAG, "initViews -> 全部UI控件初始化绑定完成");
}
private void initAppIdleHandler() {
AppIdleRunningModeHandler.init(MainActivity.this);
setOnAppIdleRunningListener(new OnAppIdleRunningListener() {
@Override
public void onIdleStatusChange(boolean isRunning) {
refreshIdleLogLayout();
appendIdleLog("IdleRunning Status : " + isRunning);
}
@Override
public void onIdleLogReceive(String log) {
appendIdleLog(log);
}
});
LogUtils.i(TAG, "initAppIdleHandler -> 空转处理器初始化与监听绑定完成");
}
private void setLLMainBackgroundColor() {
TypedArray typedArray = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
int colorAccent = typedArray.getColor(0, Color.GRAY);
typedArray.recycle();
LinearLayout llmain = findViewById(R.id.llmain);
llmain.setBackgroundColor(colorAccent);
}
// ===================== 空转日志输出工具 =====================
private void appendIdleLog(final String logText) {
runOnUiThread(new Runnable() {
@Override
public void run() {
String allLog = mTvIdleLog.getText().toString();
mTvIdleLog.setText(allLog + logText + "\n");
mScrollIdleLog.post(new Runnable() {
@Override
public void run() {
mScrollIdleLog.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}
});
}
private void refreshIdleLogLayout() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (App.isAppIdleRunning()) {
mScrollIdleLog.setBackgroundResource(R.drawable.shape_log_border);
} else {
//关闭空转时:检测权限,无权限强制关闭GPS开关
if(!checkLocationPermissions()){
mServiceSwitch.setChecked(false);
ServiceUtil.stopAutoService(MainActivity.this);
}
mTvIdleLog.setText("");
mScrollIdleLog.setBackgroundColor(Color.TRANSPARENT);
LogUtils.d(TAG, "refreshIdleLogLayout -> 非空转状态:日志清空、边框已移除");
}
refreshToolbarSubTitle();
refreshManageButtonState();
}
});
}
private void refreshToolbarSubTitle() {
if(getSupportActionBar() == null){
return;
}
if(App.isAppIdleRunning()){
getSupportActionBar().setSubtitle("当前处于空转运行状态");
}else{
getSupportActionBar().setSubtitle("");
}
}
/**
* 空转状态专属:按钮强制可点击 + 文字追加提示
*/
private void refreshManageButtonState() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (App.isAppIdleRunning()) {
mManagePositionsButton.setText("位置与任务管理【当前空转中】");
mManagePositionsButton.setEnabled(true);
} else {
mManagePositionsButton.setText("位置与任务管理");
boolean serviceEnable = AppConfigsUtil.getInstance(MainActivity.this).isEnableMainService(true);
mManagePositionsButton.setEnabled(serviceEnable);
}
}
});
}
// ===================== 权限处理相关 =====================
private boolean checkLocationPermissions() {
int foregroundPerm = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
boolean hasForegroundPerm = (foregroundPerm == PackageManager.PERMISSION_GRANTED);
boolean hasBackgroundPerm = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
int backgroundPerm = checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
hasBackgroundPerm = (backgroundPerm == PackageManager.PERMISSION_GRANTED);
}
return hasForegroundPerm && hasBackgroundPerm;
}
private void requestLocationPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
REQUEST_BACKGROUND_LOCATION_PERMISSION);
}
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, "onRequestPermissionsResult -> 权限回调 requestCode = " + requestCode);
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
requestLocationPermissions();
} else {
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
//权限被拒绝,强制关闭开关
mServiceSwitch.setChecked(false);
ServiceUtil.stopAutoService(MainActivity.this);
mManagePositionsButton.setEnabled(false);
}
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
}
}
}
// ===================== 菜单与页面跳转 =====================
@Override
public boolean onCreateOptionsMenu(Menu menu) {
AESThemeUtil.inflateMenu(this, menu);
if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
getMenuInflater().inflate(R.menu.toolbar_main_idle, menu);
}
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int menuItemId = item.getItemId();
LogUtils.d(TAG, "onOptionsItemSelected -> 点击菜单ID = " + menuItemId);
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
recreate();
} else if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, "onOptionsItemSelected -> 进入开发工具菜单");
} else if (menuItemId == R.id.item_idle_switch) {
boolean idleNow = App.isAppIdleRunning();
boolean idleNew = !idleNow;
App.setAppIdleRunning(idleNew);
AppIdleRunningModeHandler.sendIdleSwitch(idleNew);
AppIdleRunningModeHandler.sendIdleLog("菜单手动切换空转状态:" + idleNew);
LogUtils.d(TAG, "onOptionsItemSelected -> 空转状态已切换,当前:" + idleNew);
refreshIdleLogLayout();
} else if (menuItemId == R.id.item_settings) {
Intent intent = new Intent();
intent.setClass(this, SettingsActivity.class);
startActivity(intent);
} else if (menuItemId == R.id.item_about) {
Intent intent = new Intent();
intent.setClass(this, AboutActivity.class);
startActivity(intent);
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
public void onPositions(View view) {
LogUtils.d(TAG, "onPositions -> 跳转位置任务管理页面");
startActivity(new Intent(MainActivity.this, LocationActivity.class));
}
// ===================== 接口实现 =====================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
}
@@ -1,81 +0,0 @@
package cc.winboll.studio.positions.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
import cc.winboll.studio.positions.R;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/13 11:25
* @Describe 应用介绍窗口
*/
public class AboutActivity extends WinBoLLActivity {
public static final String TAG = "AboutActivity";
private Toolbar mToolbar;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
// 设置工具栏
initToolbar();
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
finish();
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "positions";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("Positions");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Positions");
appInfo.setAppAPKName("Positions");
appInfo.setAppAPKFolderName("Positions");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}
@@ -1,487 +0,0 @@
package cc.winboll.studio.positions.activities;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.MainActivity;
import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.adapters.PositionAdapter;
import cc.winboll.studio.positions.models.PositionModel;
import cc.winboll.studio.positions.services.IdleGpsService;
import cc.winboll.studio.positions.services.MainService;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2025/09/29 18:22:00
* @EditTime 2026/05/03 16:36:54
* @Describe 位置列表页面,适配MainService GPS接口、规范服务交互、完善生命周期资源释放,新增应用空转状态副标题提示
*/
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
// 常量
public static final String TAG = "LocationActivity";
// 成员属性有序排版
private Toolbar mToolbar;
private RecyclerView mRvPosition;
private PositionAdapter mPositionAdapter;
private MainService mMainService;
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
private final AtomicBoolean isAdapterInited = new AtomicBoolean(false);
private MainService.GpsUpdateListener mGpsUpdateListener;
private PositionModel mCurrentGpsPos;
private final ArrayList<PositionModel> mLocalPosCache = new ArrayList<PositionModel>();
// 服务连接回调
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName name, final IBinder service) {
LogUtils.d(TAG, "onServiceConnected invoke");
if (!(service instanceof MainService.LocalBinder)) {
LogUtils.e(TAG, "服务绑定失败:Binder类型不匹配");
isServiceBound.set(false);
return;
}
try {
final MainService.LocalBinder binder = (MainService.LocalBinder) service;
mMainService = binder.getService();
isServiceBound.set(true);
LogUtils.d(TAG, "MainService bind success");
syncDataFromMainService();
registerGpsListener();
initPositionAdapter();
} catch (final Exception e) {
LogUtils.e(TAG, "服务绑定异常:" + e.getMessage());
isServiceBound.set(false);
mMainService = null;
showToast("服务初始化失败,无法加载数据");
}
}
@Override
public void onServiceDisconnected(final ComponentName name) {
LogUtils.w(TAG, "MainService 服务断开");
mMainService = null;
isServiceBound.set(false);
isAdapterInited.set(false);
}
};
// 接口方法实现
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
/**
* 刷新空转状态副标题
*/
private void refreshIdleStatusTitle() {
LogUtils.d(TAG, "refreshIdleStatusTitle invoke");
if (mToolbar == null) {
LogUtils.w(TAG, "Toolbar为空,跳过刷新");
return;
}
final boolean idleStatus = App.isAppIdleRunning();
if (idleStatus) {
mToolbar.setSubtitle("当前状态:应用正在空转运行");
LogUtils.d(TAG, "检测应用空转状态为 : " + idleStatus);
} else {
mToolbar.setSubtitle(getTag());
}
}
/**
* 初始化页面控件
*/
private void initView() {
LogUtils.d(TAG, "initView invoke");
mRvPosition = findViewById(R.id.rv_position_list);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRvPosition.setLayoutManager(layoutManager);
mLocalPosCache.clear();
}
/**
* 绑定后台服务
*/
private void bindMainService() {
LogUtils.d(TAG, "bindMainService invoke");
if (isServiceBound.get()) {
LogUtils.w(TAG, "服务已绑定,无需重复执行");
return;
}
final Intent serviceIntent = new Intent(this, MainService.class);
final boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
if (!bindSuccess) {
LogUtils.e(TAG, "发起服务绑定请求失败");
showToast("服务绑定失败,无法加载位置数据");
}
}
/**
* 同步服务数据至本地缓存
*/
private void syncDataFromMainService() {
LogUtils.d(TAG, "syncDataFromMainService invoke");
if (!isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "服务未就绪,使用本地缓存");
return;
}
try {
final ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
synchronized (mLocalPosCache) {
mLocalPosCache.clear();
if (servicePosList != null && !servicePosList.isEmpty()) {
mLocalPosCache.addAll(servicePosList);
}
}
LogUtils.d(TAG, "同步完成,缓存数量 : " + mLocalPosCache.size());
} catch (final Exception e) {
LogUtils.e(TAG, "数据同步异常 : " + e.getMessage());
}
}
/**
* 初始化列表适配器
*/
private void initPositionAdapter() {
LogUtils.d(TAG, "initPositionAdapter invoke");
if (isAdapterInited.get() || !isServiceBound.get()
|| mMainService == null || mRvPosition == null) {
LogUtils.w(TAG, "适配器初始化条件不满足,跳过");
return;
}
try {
mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
@Override
public void onDeleteClick(final int position) {
LogUtils.d(TAG, "onDeleteClick position = " + position);
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示",
"是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
public void onNo() {
}
@Override
public void onYes() {
if (position < 0 || position >= mLocalPosCache.size()) {
LogUtils.w(TAG, "删除索引参数非法");
return;
}
final PositionModel deletePos = mLocalPosCache.get(position);
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
mMainService.removePosition(deletePos.getPositionId());
synchronized (mLocalPosCache) {
mLocalPosCache.remove(position);
}
mPositionAdapter.notifyItemRemoved(position);
showToast("删除位置成功:" + deletePos.getMemo());
}
}
});
}
});
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
@Override
public void onSavePositionClick(final int position, final PositionModel updatedPos) {
LogUtils.d(TAG, "onSavePositionClick position = " + position);
if (position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
LogUtils.w(TAG, "保存参数非法");
showToast("服务未就绪,保存失败");
return;
}
mMainService.updatePosition(updatedPos);
synchronized (mLocalPosCache) {
mLocalPosCache.set(position, updatedPos);
}
mPositionAdapter.notifyItemChanged(position);
showToast("保存位置成功:" + updatedPos.getMemo());
}
});
mRvPosition.setAdapter(mPositionAdapter);
isAdapterInited.set(true);
LogUtils.d(TAG, "适配器初始化完成");
} catch (final Exception e) {
LogUtils.e(TAG, "适配器初始化异常 : " + e.getMessage());
isAdapterInited.set(false);
mPositionAdapter = null;
showToast("位置列表初始化失败,请重试");
}
}
/**
* 通用Toast弹窗
*/
private void showToast(final String content) {
if (isFinishing() || isDestroyed()) {
LogUtils.w(TAG, "页面已销毁,取消Toast");
return;
}
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
}
/**
* 新增位置按钮事件
*/
public void addNewPosition(final View view) {
LogUtils.d(TAG, "addNewPosition invoke");
final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null && getCurrentFocus() != null) {
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
if (!isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "服务未绑定,无法新增");
showToast("服务未就绪,无法新增位置");
return;
}
final PositionModel newPos = new PositionModel();
newPos.setPositionId(PositionModel.genPositionId());
if (mCurrentGpsPos != null) {
newPos.setLongitude(mCurrentGpsPos.getLongitude());
newPos.setLatitude(mCurrentGpsPos.getLatitude());
newPos.setMemo("当前GPS位置(可编辑)");
} else {
newPos.setLongitude(116.404267);
newPos.setLatitude(39.915119);
newPos.setMemo("默认位置(可编辑备注)");
}
newPos.setIsSimpleView(true);
newPos.setIsEnableRealPositionDistance(true);
mMainService.addPosition(newPos);
synchronized (mLocalPosCache) {
mLocalPosCache.add(newPos);
}
LogUtils.d(TAG, "新增位置成功,ID : " + newPos.getPositionId());
if (isAdapterInited.get() && mPositionAdapter != null) {
mPositionAdapter.notifyItemInserted(mLocalPosCache.size() - 1);
}
showToast("新增位置成功(已启用GPS距离计算)");
}
/**
* 初始化GPS监听实例
*/
private void initGpsUpdateListener() {
LogUtils.d(TAG, "initGpsUpdateListener invoke");
mGpsUpdateListener = new MainService.GpsUpdateListener() {
@Override
public void onGpsPositionUpdated(final PositionModel currentGpsPos) {
if (currentGpsPos == null || isFinishing() || isDestroyed()) {
return;
}
mCurrentGpsPos = currentGpsPos;
LogUtils.d(TAG, "GPS更新 -> 纬度:" + currentGpsPos.getLatitude() + " 经度:" + currentGpsPos.getLongitude());
final TextView tvLat = (TextView) findViewById(R.id.tv_latitude);
final TextView tvLon = (TextView) findViewById(R.id.tv_longitude);
final TextView tvTime = (TextView) findViewById(R.id.tv_timenow);
tvLat.setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
tvLon.setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
final long currentTime = System.currentTimeMillis();
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
final String timeStr = sdf.format(new Date(currentTime));
tvTime.setText("现在时间:" + timeStr);
}
@Override
public void onGpsStatusChanged(final String status) {
if (status == null || isFinishing() || isDestroyed()) {
return;
}
LogUtils.d(TAG, "GPS状态变更 : " + status);
if (status.contains("未开启") || status.contains("权限") || status.contains("失败")) {
ToastUtils.show("GPS提示:" + status);
}
}
};
}
/**
* 刷新GPS监听源(根据空转状态自动切换)
*/
private void refreshGpsListener() {
LogUtils.d(TAG, "refreshGpsListener invoke");
// 统一注销旧监听(防止重复注册)
IdleGpsService.getInstance().unregisterGpsUpdateListener(mGpsUpdateListener);
if (mMainService != null) {
try { mMainService.unregisterGpsUpdateListener(mGpsUpdateListener); } catch (Exception e) {}
}
// 重新注册正确的监听
registerGpsListener();
}
/**
* 注册GPS监听
*/
private void registerGpsListener() {
LogUtils.d(TAG, "registerGpsListener invoke");
if (isFinishing() || isDestroyed() || mGpsUpdateListener == null) {
return;
}
try {
if (App.isAppIdleRunning()) {
IdleGpsService.getInstance().registerGpsUpdateListener(mGpsUpdateListener);
LogUtils.d(TAG, "空转GPS监听注册成功");
} else {
if (mMainService != null) {
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
LogUtils.d(TAG, "系统GPS监听注册成功");
}
}
} catch (final Exception e) {
LogUtils.e(TAG, "GPS注册异常 : " + e.getMessage());
}
}
/**
* 反注册GPS监听
*/
private void unregisterGpsListener() {
LogUtils.d(TAG, "unregisterGpsListener invoke");
if (mGpsUpdateListener == null) {
return;
}
try {
// 尝试从空转服务注销
IdleGpsService.getInstance().unregisterGpsUpdateListener(mGpsUpdateListener);
} catch (Exception e) {
// ignore
}
try {
// 尝试从主服务注销
if (mMainService != null) {
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
}
} catch (final Exception e) {
LogUtils.e(TAG, "GPS反注册异常 : " + e.getMessage());
}
}
// 生命周期方法
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate invoke");
setContentView(R.layout.activity_location);
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
LogUtils.d(TAG, "点击返回导航按钮");
final Intent intent = new Intent(LocationActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
});
refreshIdleStatusTitle();
initView();
initGpsUpdateListener();
bindMainService();
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume invoke");
refreshIdleStatusTitle();
refreshGpsListener(); // 根据当前空转状态刷新GPS源
if (isServiceBound.get() && mMainService != null && !isAdapterInited.get()) {
syncDataFromMainService();
initPositionAdapter();
} else if (isServiceBound.get() && mMainService != null && mPositionAdapter != null) {
syncDataFromMainService();
mPositionAdapter.notifyDataSetChanged();
}
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause invoke");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy invoke,开始释放资源");
unregisterGpsListener();
if (mPositionAdapter != null) {
mPositionAdapter.release();
mPositionAdapter = null;
LogUtils.d(TAG, "Adapter资源释放完成");
}
if (isServiceBound.get()) {
try {
unbindService(mServiceConnection);
LogUtils.d(TAG, "服务解绑完成");
} catch (final IllegalArgumentException e) {
LogUtils.e(TAG, "解绑异常:服务已提前解绑");
}
isServiceBound.set(false);
mMainService = null;
}
synchronized (mLocalPosCache) {
mLocalPosCache.clear();
}
mCurrentGpsPos = null;
mGpsUpdateListener = null;
isAdapterInited.set(false);
LogUtils.d(TAG, "全部资源释放完毕");
}
}
@@ -1,51 +0,0 @@
package cc.winboll.studio.positions.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.R;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/07 23:29
* @Describe 应用设置活动窗口
*/
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "SettingsActivity";
private Toolbar mToolbar;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
}
}
@@ -1,80 +0,0 @@
package cc.winboll.studio.positions.activities;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/29 00:11
* @Describe WinBoLL 窗口基础类
*/
import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeType = getThemeType();
setThemeStyle();
super.onCreate(savedInstanceState);
}
AESThemeBean.ThemeType getThemeType() {
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
void setThemeStyle() {
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, String.format("onResume %s", getTag()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
/*if (item.getItemId() == R.id.item_log) {
WinBoLLActivityManager.getInstance().startLogActivity(this);
return true;
} else if (item.getItemId() == R.id.item_home) {
startActivity(new Intent(this, MainActivity.class));
return true;
}*/
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
return super.onOptionsItemSelected(item);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
WinBoLLActivityManager.getInstance().registeRemove(this);
}
}
@@ -1,639 +0,0 @@
package cc.winboll.studio.positions.adapters;
/**
* @Describe 位置数据适配器(修复视图复用资源加载,支持滚动后重新绑定数据,Java 7语法适配)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2025-09-29 20:25:00
* @EditTime 2026-03-31 23:14:55
*/
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.models.PositionModel;
import cc.winboll.studio.positions.models.PositionTaskModel;
import cc.winboll.studio.positions.services.MainService;
import cc.winboll.studio.positions.views.PositionTaskListView;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
public class PositionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements MainService.TaskUpdateListener {
public static final String TAG = "PositionAdapter";
// 视图类型常量
private static final int VIEW_TYPE_SIMPLE = 0;
private static final int VIEW_TYPE_EDIT = 1;
// 默认配置常量
private static final String DEFAULT_MEMO = "无备注";
private static final String DEFAULT_TASK_DESC = "新任务";
private static final int DEFAULT_TASK_DISTANCE = 50;
private static final String DISTANCE_FORMAT = "实时距离:%.1f 米";
private static final String DISTANCE_DISABLED = "实时距离:未启用";
private static final String DISTANCE_ERROR = "实时距离:计算失败";
// 核心成员变量
private final Context mContext;
private final ArrayList<PositionModel> mCachedPositionList;
private final WeakReference<MainService> mMainServiceRef;
private final ConcurrentHashMap<String, PositionTaskListView> mSimpleTaskViewMap;
private final ConcurrentHashMap<String, PositionTaskListView> mEditTaskViewMap;
private final ConcurrentHashMap<String, TextView> mPosDistanceViewMap;
// 回调接口
public interface OnDeleteClickListener {
void onDeleteClick(int position);
}
public interface OnSavePositionClickListener {
void onSavePositionClick(int position, PositionModel updatedPos);
}
private OnDeleteClickListener mOnDeleteListener;
private OnSavePositionClickListener mOnSavePosListener;
// =========================================================================
// 构造函数
// =========================================================================
public PositionAdapter(Context context, ArrayList<PositionModel> cachedPositionList, MainService mainService) {
LogUtils.d(TAG, "PositionAdapter 构造函数开始,context=" + context
+ "cachedPositionList=" + (cachedPositionList != null ? cachedPositionList.size() : 0)
+ "mainService=" + mainService);
this.mContext = context;
this.mCachedPositionList = (cachedPositionList != null) ? cachedPositionList : new ArrayList<PositionModel>();
this.mMainServiceRef = new WeakReference<MainService>(mainService);
this.mSimpleTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
this.mEditTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
this.mPosDistanceViewMap = new ConcurrentHashMap<String, TextView>();
if (mainService != null) {
mainService.registerTaskUpdateListener(this);
LogUtils.d(TAG, "已注册 MainService 任务监听");
} else {
LogUtils.w(TAG, "构造函数:MainService 为空,无法初始化任务视图");
}
LogUtils.d(TAG, "PositionAdapter 初始化完成,位置数量=" + mCachedPositionList.size());
}
// =========================================================================
// RecyclerView 核心方法
// =========================================================================
@Override
public int getItemViewType(int position) {
PositionModel posModel = getPositionByIndex(position);
return (posModel != null && posModel.isSimpleView()) ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LogUtils.d(TAG, "onCreateViewHolder viewType=" + viewType);
LayoutInflater inflater = LayoutInflater.from(mContext);
if (viewType == VIEW_TYPE_SIMPLE) {
View simpleView = inflater.inflate(R.layout.item_position_simple, parent, false);
return new SimpleViewHolder(simpleView);
} else {
View editView = inflater.inflate(R.layout.item_position_edit, parent, false);
return new EditViewHolder(editView);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
LogUtils.d(TAG, "onBindViewHolder position=" + position);
PositionModel posModel = getPositionByIndex(position);
if (posModel == null) {
LogUtils.w(TAG, "onBindViewHolder:位置模型为空,索引=" + position);
return;
}
final String posId = posModel.getPositionId();
MainService mainService = mMainServiceRef.get();
if (holder instanceof SimpleViewHolder) {
LogUtils.d(TAG, "绑定 SimpleViewHolderposId=" + posId);
SimpleViewHolder simpleHolder = (SimpleViewHolder) holder;
bindSimplePositionData(simpleHolder, posModel);
initAndBindSimpleTaskView(simpleHolder.ptlvSimpleTasks, posId, mainService);
mSimpleTaskViewMap.put(posId, simpleHolder.ptlvSimpleTasks);
mPosDistanceViewMap.put(posId, simpleHolder.tvSimpleDistance);
} else if (holder instanceof EditViewHolder) {
LogUtils.d(TAG, "绑定 EditViewHolderposId=" + posId);
EditViewHolder editHolder = (EditViewHolder) holder;
bindEditPositionData(editHolder, posModel, position);
initAndBindEditTaskView(editHolder.ptlvEditTasks, posId, mainService, editHolder.btnAddTask);
mEditTaskViewMap.put(posId, editHolder.ptlvEditTasks);
mPosDistanceViewMap.put(posId, editHolder.tvEditDistance);
}
}
@Override
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
PositionModel posModel = getPositionByIndex(holder.getAdapterPosition());
if (posModel == null || TextUtils.isEmpty(posModel.getPositionId())) {
return;
}
String posId = posModel.getPositionId();
if (mPosDistanceViewMap.containsKey(posId)) {
TextView distanceView = mPosDistanceViewMap.get(posId);
if (distanceView == null || !distanceView.isAttachedToWindow()) {
mPosDistanceViewMap.remove(posId);
LogUtils.d(TAG, "视图脱离:移除无效距离控件缓存 posId=" + posId);
}
}
if (holder instanceof SimpleViewHolder && mSimpleTaskViewMap.containsKey(posId)) {
PositionTaskListView taskView = mSimpleTaskViewMap.get(posId);
if (taskView == null || !taskView.isAttachedToWindow()) {
mSimpleTaskViewMap.remove(posId);
LogUtils.d(TAG, "视图脱离:移除无效简单任务视图缓存 posId=" + posId);
}
}
if (holder instanceof EditViewHolder && mEditTaskViewMap.containsKey(posId)) {
PositionTaskListView taskView = mEditTaskViewMap.get(posId);
if (taskView == null || !taskView.isAttachedToWindow()) {
mEditTaskViewMap.remove(posId);
LogUtils.d(TAG, "视图脱离:移除无效编辑任务视图缓存 posId=" + posId);
}
}
}
@Override
public int getItemCount() {
return mCachedPositionList.size();
}
// =========================================================================
// 位置数据绑定
// =========================================================================
private void bindSimplePositionData(SimpleViewHolder holder, final PositionModel posModel) {
LogUtils.d(TAG, "bindSimplePositionData posId=" + posModel.getPositionId());
holder.tvSimpleLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
holder.tvSimpleLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
String memo = posModel.getMemo();
holder.tvSimpleMemo.setText("备注:" + (TextUtils.isEmpty(memo) ? DEFAULT_MEMO : memo));
updateDistanceDisplay(holder.tvSimpleDistance, posModel);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
posModel.setIsSimpleView(false);
notifyItemChanged(getPositionIndexById(posModel.getPositionId()));
LogUtils.d(TAG, "简单视图点击:切换编辑模式 posId=" + posModel.getPositionId());
}
});
}
private void bindEditPositionData(final EditViewHolder holder, final PositionModel posModel, final int position) {
LogUtils.d(TAG, "bindEditPositionData posId=" + posModel.getPositionId() + " position=" + position);
final String posId = posModel.getPositionId();
holder.tvEditLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
holder.tvEditLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
String memo = posModel.getMemo();
if (!TextUtils.isEmpty(memo)) {
holder.etEditMemo.setText(memo);
holder.etEditMemo.setSelection(memo.length());
} else {
holder.etEditMemo.setText("");
}
updateDistanceDisplay(holder.tvEditDistance, posModel);
holder.rgDistanceSwitch.check(posModel.isEnableRealPositionDistance()
? R.id.rb_distance_enable
: R.id.rb_distance_disable);
holder.btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
posModel.setIsSimpleView(true);
notifyItemChanged(position);
hideSoftKeyboard(v);
LogUtils.d(TAG, "取消编辑:切换简单视图 posId=" + posId);
}
});
holder.btnDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnDeleteListener != null) {
mOnDeleteListener.onDeleteClick(position);
}
hideSoftKeyboard(v);
LogUtils.d(TAG, "触发删除位置 posId=" + posId);
}
});
holder.btnSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String newMemo = holder.etEditMemo.getText().toString().trim();
boolean isDistanceEnable = (holder.rgDistanceSwitch.getCheckedRadioButtonId() == R.id.rb_distance_enable);
PositionModel updatedPos = new PositionModel();
updatedPos.setPositionId(posId);
updatedPos.setLongitude(posModel.getLongitude());
updatedPos.setLatitude(posModel.getLatitude());
updatedPos.setMemo(newMemo);
updatedPos.setIsEnableRealPositionDistance(isDistanceEnable);
updatedPos.setIsSimpleView(true);
if (mOnSavePosListener != null) {
mOnSavePosListener.onSavePositionClick(position, updatedPos);
}
posModel.setMemo(newMemo);
posModel.setIsEnableRealPositionDistance(isDistanceEnable);
posModel.setIsSimpleView(true);
notifyItemChanged(position);
hideSoftKeyboard(v);
LogUtils.d(TAG, "保存位置 posId=" + posId + " 新备注=" + newMemo + " 距离启用=" + isDistanceEnable);
}
});
}
// =========================================================================
// PositionTaskListView 集成
// =========================================================================
private void initAndBindSimpleTaskView(PositionTaskListView taskView, String posId, MainService mainService) {
LogUtils.d(TAG, "initAndBindSimpleTaskView posId=" + posId);
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null) {
LogUtils.w(TAG, "初始化简单任务视图失败:参数无效");
return;
}
taskView.init(mainService, posId);
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_SIMPLE);
taskView.syncTasksFromMainService();
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
@Override
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
LogUtils.d(TAG, "简单模式任务更新 posId=" + positionId + " 任务数=" + updatedTasks.size());
}
});
}
private void initAndBindEditTaskView(final PositionTaskListView taskView, final String posId,
MainService mainService, Button btnAddTask) {
LogUtils.d(TAG, "initAndBindEditTaskView posId=" + posId);
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null || btnAddTask == null) {
LogUtils.w(TAG, "初始化编辑任务视图失败:参数无效");
return;
}
taskView.init(mainService, posId);
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_EDIT);
taskView.syncTasksFromMainService();
btnAddTask.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PositionTaskModel newTask = new PositionTaskModel();
newTask.setTaskId(PositionTaskModel.genTaskId());
newTask.setPositionId(posId);
newTask.setTaskDescription(DEFAULT_TASK_DESC);
newTask.setDiscussDistance(DEFAULT_TASK_DISTANCE);
newTask.setIsEnable(true);
newTask.setIsBingo(false);
taskView.addNewTask(newTask);
hideSoftKeyboard(v);
LogUtils.d(TAG, "新增任务 posId=" + posId + " taskId=" + newTask.getTaskId());
}
});
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
@Override
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
LogUtils.d(TAG, "编辑模式任务更新 posId=" + positionId + " 任务数=" + updatedTasks.size());
}
});
}
// =========================================================================
// 工具方法
// =========================================================================
private void updateDistanceDisplay(TextView distanceView, PositionModel posModel) {
if (distanceView == null || posModel == null) {
LogUtils.w(TAG, "updateDistanceDisplay:参数为空");
return;
}
if (!posModel.isEnableRealPositionDistance()) {
distanceView.setText(DISTANCE_DISABLED);
distanceView.setTextColor(mContext.getResources().getColor(R.color.gray));
return;
}
double distance = posModel.getRealPositionDistance();
if (distance < 0) {
distanceView.setText(DISTANCE_ERROR);
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
return;
}
distanceView.setText(String.format(DISTANCE_FORMAT, distance));
if (distance <= 100) {
distanceView.setTextColor(mContext.getResources().getColor(R.color.green));
} else if (distance <= 500) {
distanceView.setTextColor(mContext.getResources().getColor(R.color.yellow));
} else {
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
}
}
private PositionModel getPositionByIndex(int index) {
if (mCachedPositionList == null || index < 0 || index >= mCachedPositionList.size()) {
LogUtils.w(TAG, "getPositionByIndex:无效索引 index=" + index);
return null;
}
return mCachedPositionList.get(index);
}
private int getPositionIndexById(String positionId) {
if (TextUtils.isEmpty(positionId) || mCachedPositionList == null || mCachedPositionList.isEmpty()) {
LogUtils.w(TAG, "getPositionIndexById:参数无效");
return -1;
}
for (int i = 0; i < mCachedPositionList.size(); i++) {
PositionModel pos = mCachedPositionList.get(i);
if (positionId.equals(pos.getPositionId())) {
return i;
}
}
LogUtils.w(TAG, "未找到位置ID=" + positionId);
return -1;
}
public void updateSinglePositionDistance(String positionId) {
LogUtils.d(TAG, "updateSinglePositionDistance posId=" + positionId);
if (TextUtils.isEmpty(positionId) || mPosDistanceViewMap.isEmpty()) {
return;
}
MainService mainService = getMainServiceWithRetry(2);
if (mainService == null) {
LogUtils.e(TAG, "无法获取 MainService");
return;
}
PositionModel latestPos = null;
try {
ArrayList servicePosList = mainService.getPositionList();
if (servicePosList != null && !servicePosList.isEmpty()) {
Iterator iter = servicePosList.iterator();
while (iter.hasNext()) {
PositionModel pos = (PositionModel) iter.next();
if (positionId.equals(pos.getPositionId())) {
latestPos = pos;
break;
}
}
}
} catch (Exception e) {
LogUtils.e(TAG, "获取位置数据异常", e);
return;
}
if (latestPos == null) {
LogUtils.w(TAG, "未找到位置 posId=" + positionId);
return;
}
final TextView distanceView = mPosDistanceViewMap.get(positionId);
if (distanceView != null && distanceView.isAttachedToWindow()) {
final PositionModel finalLatestPos = latestPos;
distanceView.post(new Runnable() {
@Override
public void run() {
updateDistanceDisplay(distanceView, finalLatestPos);
}
});
} else {
mPosDistanceViewMap.remove(positionId);
}
}
public void updateAllPositionData(ArrayList<PositionModel> newPosList) {
LogUtils.d(TAG, "updateAllPositionData 新数据数量=" + (newPosList != null ? newPosList.size() : 0));
if (newPosList == null) {
return;
}
ArrayList<PositionModel> validPosList = new ArrayList<PositionModel>();
for (PositionModel pos : newPosList) {
if (TextUtils.isEmpty(pos.getPositionId())
|| pos.getLongitude() < -180 || pos.getLongitude() > 180
|| pos.getLatitude() < -90 || pos.getLatitude() > 90) {
continue;
}
validPosList.add(pos);
}
ConcurrentHashMap<String, PositionModel> uniquePosMap = new ConcurrentHashMap<String, PositionModel>();
for (PositionModel pos : validPosList) {
uniquePosMap.put(pos.getPositionId(), pos);
}
ArrayList uniquePosList = new ArrayList(uniquePosMap.values());
mCachedPositionList.clear();
mCachedPositionList.addAll(uniquePosList);
mPosDistanceViewMap.clear();
mSimpleTaskViewMap.clear();
mEditTaskViewMap.clear();
notifyDataSetChanged();
LogUtils.d(TAG, "全量更新完成,有效数量=" + uniquePosList.size());
}
private void hideSoftKeyboard(View view) {
if (mContext == null || view == null) {
return;
}
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
private MainService getMainServiceWithRetry(int retryCount) {
MainService mainService = mMainServiceRef.get();
if (mainService != null) {
return mainService;
}
for (int i = 0; i < retryCount; i++) {
try {
Thread.sleep(100);
mainService = mMainServiceRef.get();
if (mainService != null) {
LogUtils.d(TAG, "重试获取 MainService 成功,第" + (i + 1) + "");
return mainService;
}
} catch (InterruptedException e) {
LogUtils.e(TAG, "重试被中断", e);
Thread.currentThread().interrupt();
break;
}
}
LogUtils.e(TAG, "重试" + retryCount + "次仍未获取到 MainService");
return null;
}
// =========================================================================
// 任务更新监听
// =========================================================================
@Override
public void onTaskUpdated() {
LogUtils.d(TAG, "onTaskUpdated:收到服务任务更新");
if (!mSimpleTaskViewMap.isEmpty()) {
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
while (iter.hasNext()) {
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
PositionTaskListView taskView = entry.getValue();
if (taskView != null && taskView.isAttachedToWindow()) {
taskView.syncTasksFromMainService();
} else {
iter.remove();
}
}
}
if (!mEditTaskViewMap.isEmpty()) {
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
while (iter.hasNext()) {
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
PositionTaskListView taskView = entry.getValue();
if (taskView != null && taskView.isAttachedToWindow()) {
taskView.syncTasksFromMainService();
} else {
iter.remove();
}
}
}
}
// =========================================================================
// 外部回调设置
// =========================================================================
public void setOnDeleteClickListener(OnDeleteClickListener listener) {
LogUtils.d(TAG, "setOnDeleteClickListener listener=" + listener);
this.mOnDeleteListener = listener;
}
public void setOnSavePositionClickListener(OnSavePositionClickListener listener) {
LogUtils.d(TAG, "setOnSavePositionClickListener listener=" + listener);
this.mOnSavePosListener = listener;
}
// =========================================================================
// 资源释放
// =========================================================================
public void release() {
LogUtils.d(TAG, "release:开始释放 Adapter 资源");
MainService mainService = mMainServiceRef.get();
if (mainService != null) {
mainService.unregisterTaskUpdateListener(this);
LogUtils.d(TAG, "已反注册任务监听");
}
if (!mSimpleTaskViewMap.isEmpty()) {
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
while (iter.hasNext()) {
PositionTaskListView taskView = iter.next().getValue();
if (taskView != null) {
taskView.clearData();
taskView.setOnTaskUpdatedListener(null);
}
iter.remove();
}
}
if (!mEditTaskViewMap.isEmpty()) {
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
while (iter.hasNext()) {
PositionTaskListView taskView = iter.next().getValue();
if (taskView != null) {
taskView.clearData();
taskView.setOnTaskUpdatedListener(null);
}
iter.remove();
}
}
mPosDistanceViewMap.clear();
if (mCachedPositionList != null) {
mCachedPositionList.clear();
}
mOnDeleteListener = null;
mOnSavePosListener = null;
LogUtils.d(TAG, "release:资源释放完成");
}
// =========================================================================
// ViewHolder
// =========================================================================
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
TextView tvSimpleLon;
TextView tvSimpleLat;
TextView tvSimpleMemo;
TextView tvSimpleDistance;
PositionTaskListView ptlvSimpleTasks;
public SimpleViewHolder(View itemView) {
super(itemView);
tvSimpleLon = (TextView) itemView.findViewById(R.id.tv_simple_longitude);
tvSimpleLat = (TextView) itemView.findViewById(R.id.tv_simple_latitude);
tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo);
tvSimpleDistance = (TextView) itemView.findViewById(R.id.tv_simple_distance);
ptlvSimpleTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_simple_tasks);
}
}
public static class EditViewHolder extends RecyclerView.ViewHolder {
TextView tvEditLon;
TextView tvEditLat;
EditText etEditMemo;
TextView tvEditDistance;
RadioGroup rgDistanceSwitch;
Button btnCancel;
Button btnDelete;
Button btnSave;
Button btnAddTask;
TextView tvTaskCount;
PositionTaskListView ptlvEditTasks;
public EditViewHolder(View itemView) {
super(itemView);
tvEditLon = (TextView) itemView.findViewById(R.id.tv_edit_longitude);
tvEditLat = (TextView) itemView.findViewById(R.id.tv_edit_latitude);
etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo);
tvEditDistance = (TextView) itemView.findViewById(R.id.tv_edit_distance);
rgDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_distance_switch);
btnCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel);
btnDelete = (Button) itemView.findViewById(R.id.btn_edit_delete);
btnSave = (Button) itemView.findViewById(R.id.btn_edit_save);
btnAddTask = (Button) itemView.findViewById(R.id.btn_add_task);
tvTaskCount = (TextView) itemView.findViewById(R.id.tv_task_count);
ptlvEditTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_edit_tasks);
}
}
// =========================================================================
// 方法说明类(保留不动)
// =========================================================================
public static class PositionTaskListViewRequiredMethods {
}
}
@@ -1,143 +0,0 @@
package cc.winboll.studio.positions.handlers;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.MainActivity;
/**
* 应用空转事务处理器
* 作用:接收空转开关消息、空转日志消息,回调MainActivity内部接口实现UI联动
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026/05/03 12:23:00
* @EditTime 2026/05/03 14:42:18
*/
public class AppIdleRunningModeHandler extends Handler {
//===================== 常量标识 =====================
public static final String TAG = "AppIdleRunningModeHandler";
public static final int MSG_IDLE_MODE_SWITCH = 1001;
public static final int MSG_IDLE_LOG_PRINT = 1002;
//===================== 成员变量 =====================
private static AppIdleRunningModeHandler mHandler;
private MainActivity mMainActivity;
//===================== 静态初始化 =====================
static {
mHandler = new AppIdleRunningModeHandler();
LogUtils.d(TAG, "静态代码块:默认主线程Looper完成初始化");
}
//===================== 构造方法 =====================
/**
* 私有无参构造,禁止外部直接实例化
*/
private AppIdleRunningModeHandler() {
super(Looper.getMainLooper());
}
/**
* 带MainActivity绑定构造
* @param activity 主页面实例
*/
public AppIdleRunningModeHandler(MainActivity activity) {
super(Looper.getMainLooper());
this.mMainActivity = activity;
LogUtils.d(TAG, "构造方法:完成MainActivity绑定初始化");
}
//===================== 对外初始化与获取 =====================
/**
* 全局静态初始化、重新绑定MainActivity
* @param activity 主页面实例
*/
public static void init(MainActivity activity) {
if (mHandler == null) {
mHandler = new AppIdleRunningModeHandler(activity);
} else {
mHandler.mMainActivity = activity;
}
LogUtils.i(TAG, "init -> AppIdleRunningModeHandler初始化绑定成功");
}
/**
* 获取当前绑定的MainActivity实例
* @return 已绑定的Activity
*/
public MainActivity getBindMainActivity() {
return mMainActivity;
}
//===================== 对外静态发送方法 =====================
/**
* 发送空转开关控制消息
* @param isOpen 是否开启空转状态
*/
public static void sendIdleSwitch(boolean isOpen) {
if (!App.isAppIdleRunning()) {
LogUtils.d(TAG, "sendIdleSwitch -> 当前非空转状态,函数执行无效");
return;
}
LogUtils.d(TAG, "sendIdleSwitch -> 发送空转开关信号,参数isOpen = " + isOpen);
Message message = Message.obtain();
message.what = MSG_IDLE_MODE_SWITCH;
message.obj = isOpen;
mHandler.sendMessage(message);
}
/**
* 发送空转日志打印消息
* @param logText 待输出的日志内容
*/
public static void sendIdleLog(String logText) {
if (!App.isAppIdleRunning()) {
LogUtils.d(TAG, "sendIdleLog -> 当前非空转状态,函数执行无效");
return;
}
LogUtils.d(TAG, "sendIdleLog -> 发送空转日志消息");
Message message = Message.obtain();
message.what = MSG_IDLE_LOG_PRINT;
message.obj = logText;
mHandler.sendMessage(message);
}
//===================== 消息接收处理 =====================
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 全局状态校验
if (!App.isAppIdleRunning()) {
return;
}
// 空指针安全防护
if (mMainActivity == null || mMainActivity.getOnAppIdleRunningListener() == null) {
LogUtils.d(TAG, "handleMessage -> Activity或监听接口为空,终止回调");
return;
}
switch (msg.what) {
case MSG_IDLE_MODE_SWITCH:
boolean idleState = (boolean) msg.obj;
App.setAppIdleRunning(idleState);
LogUtils.i(TAG, "handleMessage -> 空转状态已变更:" + idleState);
// 回调主页面接口
mMainActivity.getOnAppIdleRunningListener().onIdleStatusChange(idleState);
break;
case MSG_IDLE_LOG_PRINT:
String logContent = (String) msg.obj;
LogUtils.i(TAG, logContent);
// 回调主页面日志接收接口
mMainActivity.getOnAppIdleRunningListener().onIdleLogReceive(logContent);
break;
}
}
}
@@ -1,75 +0,0 @@
package cc.winboll.studio.positions.models;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/01 04:50
* @Describe AppConfigsModel
*/
import cc.winboll.studio.libappbase.BaseBean;
import android.util.JsonWriter;
import android.util.JsonReader;
import java.io.IOException;
public class AppConfigsModel extends BaseBean {
public static final String TAG = "AppConfigsModel";
boolean isEnableMainService;
public AppConfigsModel(boolean isEnableMainService) {
this.isEnableMainService = isEnableMainService;
}
public AppConfigsModel() {
this.isEnableMainService = false;
}
public void setIsEnableMainService(boolean isEnableMainService) {
this.isEnableMainService = isEnableMainService;
}
public boolean isEnableMainService() {
return isEnableMainService;
}
@Override
public String getName() {
return AppConfigsModel.class.getName();
}
// JSON序列化(保存位置数据)
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name("isEnableDistanceRefreshService").value(isEnableMainService());
}
// JSON反序列化(加载位置数据,校验字段)
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
if (super.initObjectsFromJsonReader(jsonReader, name)) {
return true;
} else {
if (name.equals("isEnableDistanceRefreshService")) {
setIsEnableMainService(jsonReader.nextBoolean());
} else {
return false;
}
}
return true;
}
// 从JSON读取位置数据
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, name)) {
jsonReader.skipValue(); // 跳过未知字段
}
}
jsonReader.endObject();
return this;
}
}
@@ -1,219 +0,0 @@
package cc.winboll.studio.positions.models;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/29 18:57
* @Describe 位置数据模型
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.util.UUID;
public class PositionModel extends BaseBean {
public static final String TAG = "PositionModel";
// 位置唯一标识符(与任务的positionId绑定)
String positionId;
// 经度(范围:-180~180
double longitude;
// 纬度(范围:-90~90
double latitude;
// 位置备注(空值时显示“无备注”)
String memo;
// 定位点与指定点实时距离长度
double realPositionDistance;
// 是否启用实时距离计算
boolean isEnableRealPositionDistance;
// 是否显示简单视图(true=简单视图,false=编辑视图)
boolean isSimpleView = true;
// 带参构造(强制初始化位置ID和经纬度)
public PositionModel(String positionId, double longitude, double latitude, String memo, boolean isEnableRealPositionDistance) {
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
this.longitude = Math.max(-180, Math.min(180, longitude)); // 经度范围限制
this.latitude = Math.max(-90, Math.min(90, latitude)); // 纬度范围限制
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
}
// 无参构造(默认值初始化,避免空指针)
public PositionModel() {
this.positionId = genPositionId();
this.longitude = 0.0;
this.latitude = 0.0;
this.memo = "无备注";
this.isEnableRealPositionDistance = false;
}
public void setRealPositionDistance(double realPositionDistance) {
this.realPositionDistance = realPositionDistance;
}
public double getRealPositionDistance() {
return realPositionDistance;
}
// ---------------------- Getter/Setter(确保字段有效性) ----------------------
public void setPositionId(String positionId) {
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
}
public String getPositionId() {
return positionId;
}
public void setIsEnableRealPositionDistance(boolean isEnableRealPositionDistance) {
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
}
public boolean isEnableRealPositionDistance() {
return isEnableRealPositionDistance;
}
public void setIsSimpleView(boolean isSimpleView) {
this.isSimpleView = isSimpleView;
}
public boolean isSimpleView() {
return isSimpleView;
}
public void setMemo(String memo) {
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
}
public String getMemo() {
return memo;
}
public void setLongitude(double longitude) {
this.longitude = Math.max(-180, Math.min(180, longitude)); // 限制经度范围
}
public double getLongitude() {
return longitude;
}
public void setLatitude(double latitude) {
this.latitude = Math.max(-90, Math.min(90, latitude)); // 限制纬度范围
}
public double getLatitude() {
return latitude;
}
// ---------------------- 父类方法重写 ----------------------
@Override
public String getName() {
return PositionModel.class.getName();
}
// 生成唯一位置ID(与任务ID格式一致,确保关联匹配)
public static String genPositionId() {
UUID uniqueUuid = UUID.randomUUID();
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保与任务ID格式统一)
}
// JSON序列化(保存位置数据)
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name("positionId").value(getPositionId());
jsonWriter.name("longitude").value(getLongitude());
jsonWriter.name("latitude").value(getLatitude());
jsonWriter.name("memo").value(getMemo());
jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance());
}
// JSON反序列化(加载位置数据,校验字段)
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
if (super.initObjectsFromJsonReader(jsonReader, name)) {
return true;
} else {
if (name.equals("positionId")) {
setPositionId(jsonReader.nextString());
} else if (name.equals("longitude")) {
setLongitude(jsonReader.nextDouble());
} else if (name.equals("latitude")) {
setLatitude(jsonReader.nextDouble());
} else if (name.equals("memo")) {
setMemo(jsonReader.nextString());
} else if (name.equals("isEnableRealPositionDistance")) {
setIsEnableRealPositionDistance(jsonReader.nextBoolean());
} else {
return false;
}
}
return true;
}
// 从JSON读取位置数据
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, name)) {
jsonReader.skipValue(); // 跳过未知字段
}
}
jsonReader.endObject();
return this;
}
// ---------------------- 核心工具方法:计算两点距离(Haversine公式,确保精度) ----------------------
/**
* 计算两个位置之间的直线距离(地球表面最短距离)
* @param position1 第一个位置(非null
* @param position2 第二个位置(非null
* @param isKilometer 是否返回千米单位:true→千米,false→米
* @return 距离(保留1位小数,符合显示需求)
* @throws IllegalArgumentException 位置为null或经纬度无效时抛出
*/
public static double calculatePositionDistance(PositionModel position1, PositionModel position2, boolean isKilometer) {
// 1. 校验参数有效性(避免计算异常)
if (position1 == null || position2 == null) {
throw new IllegalArgumentException("位置对象不能为null");
}
double lon1 = position1.getLongitude();
double lat1 = position1.getLatitude();
double lon2 = position2.getLongitude();
double lat2 = position2.getLatitude();
// 经纬度范围二次校验(确保有效)
if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90
|| lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) {
throw new IllegalArgumentException("经纬度值无效(纬度:-90~90,经度:-180~180");
}
// 2. Haversine公式计算(地球半径取6371km,行业标准)
final double EARTH_RADIUS_KM = 6371;
double radLat1 = Math.toRadians(lat1); // 角度转弧度
double radLat2 = Math.toRadians(lat2);
double radLon1 = Math.toRadians(lon1);
double radLon2 = Math.toRadians(lon2);
double deltaLat = radLat2 - radLat1; // 纬度差
double deltaLon = radLon2 - radLon1; // 经度差
// 核心公式
double a = Math.pow(Math.sin(deltaLat / 2), 2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(deltaLon / 2), 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double distanceKm = EARTH_RADIUS_KM * c; // 距离(千米)
// 3. 单位转换+精度处理(保留1位小数,符合显示需求)
double distance;
if (isKilometer) {
distance = Math.round(distanceKm * 10.0) / 10.0; // 千米→1位小数
} else {
distance = Math.round(distanceKm * 1000 * 10.0) / 10.0; // 米→1位小数
}
return distance;
}
}
@@ -1,207 +0,0 @@
package cc.winboll.studio.positions.models;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/30 02:48
* @Describe 位置任务数据模型
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.util.UUID;
public class PositionTaskModel extends BaseBean {
public static final String TAG = "PositionTaskModel";
// 任务标识符(唯一)
String taskId;
// 绑定的位置标识符(与PositionModel的positionId一一对应)
String positionId;
// 任务描述
String taskDescription;
// 任务距离条件:是否大于设定距离
boolean isGreaterThan;
// 任务距离条件:是否小于设定距离(与isGreaterThan互斥)
boolean isLessThan;
// 任务条件距离(单位:米)
int discussDistance;
// 任务开始启用时间
long startTime;
// 任务是否已触发
boolean isBingo = false;
// 是否启用任务
boolean isEnable;
// 带参构造(强制传入positionId,确保任务与位置绑定)
public PositionTaskModel(String taskId, String positionId, String taskDescription, boolean isGreaterThan, int discussDistance, long startTime, boolean isEnable) {
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; // 空ID自动生成
this.positionId = positionId; // 强制绑定位置ID
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
this.isGreaterThan = isGreaterThan;
this.isLessThan = !isGreaterThan; // 确保互斥
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免无效值
this.startTime = startTime;
this.isEnable = isEnable;
}
// 无参构造(初始化默认值,positionId需后续设置)
public PositionTaskModel() {
this.taskId = genTaskId();
this.positionId = "";
this.taskDescription = "新任务";
this.isGreaterThan = true;
this.isLessThan = false; // 初始互斥
this.discussDistance = 100; // 默认100米
this.startTime = System.currentTimeMillis();
this.isEnable = true;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public long getStartTime() {
return startTime;
}
public void setIsBingo(boolean isBingo) {
this.isBingo = isBingo;
}
public boolean isBingo() {
return isBingo;
}
// ---------------------- Getter/Setter(确保positionId不为空,距离有效) ----------------------
public void setTaskId(String taskId) {
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId;
}
public String getTaskId() {
return taskId;
}
public void setPositionId(String positionId) {
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? "" : positionId; // 空值防护
}
public String getPositionId() {
return positionId;
}
public void setTaskDescription(String taskDescription) {
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
}
public String getTaskDescription() {
return taskDescription;
}
// 修复:确保isGreaterThan和isLessThan互斥
public void setIsGreaterThan(boolean isGreaterThan) {
this.isGreaterThan = isGreaterThan;
this.isLessThan = !isGreaterThan; // 关键:小于 = 非大于
}
public boolean isGreaterThan() {
return isGreaterThan;
}
// 修复:确保isLessThan和isGreaterThan互斥
public void setIsLessThan(boolean isLessThan) {
this.isLessThan = isLessThan;
this.isGreaterThan = !isLessThan; // 关键:大于 = 非小于
}
public boolean isLessThan() {
return isLessThan;
}
public void setDiscussDistance(int discussDistance) {
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免0或负数
}
public int getDiscussDistance() {
return discussDistance;
}
public void setIsEnable(boolean isEnable) {
this.isEnable = isEnable;
}
public boolean isEnable() {
return isEnable;
}
// ---------------------- 父类方法重写 ----------------------
@Override
public String getName() {
return PositionTaskModel.class.getName();
}
// 生成唯一任务ID(与PositionModel保持一致格式)
public static String genTaskId() {
UUID uniqueUuid = UUID.randomUUID();
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保唯一)
}
// JSON序列化(保存任务数据,包含所有字段)
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name("taskId").value(getTaskId());
jsonWriter.name("positionId").value(getPositionId());
jsonWriter.name("taskDescription").value(getTaskDescription());
jsonWriter.name("isGreaterThan").value(isGreaterThan());
jsonWriter.name("isLessThan").value(isLessThan());
jsonWriter.name("discussDistance").value(getDiscussDistance());
jsonWriter.name("startTime").value(getStartTime());
jsonWriter.name("isEnable").value(isEnable());
}
// JSON反序列化(加载任务数据,校验字段有效性)
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
if (super.initObjectsFromJsonReader(jsonReader, name)) {
return true;
} else {
if (name.equals("taskId")) {
setTaskId(jsonReader.nextString());
} else if (name.equals("positionId")) {
setPositionId(jsonReader.nextString());
} else if (name.equals("taskDescription")) {
setTaskDescription(jsonReader.nextString());
} else if (name.equals("isGreaterThan")) {
setIsGreaterThan(jsonReader.nextBoolean());
} else if (name.equals("isLessThan")) {
setIsLessThan(jsonReader.nextBoolean());
} else if (name.equals("discussDistance")) {
setDiscussDistance(jsonReader.nextInt());
} else if (name.equals("startTime")) {
setStartTime(jsonReader.nextLong());
} else if (name.equals("isEnable")) {
setIsEnable(jsonReader.nextBoolean());
} else {
return false;
}
}
return true;
}
// 从JSON读取任务数据(确保反序列化完整)
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, name)) {
jsonReader.skipValue(); // 跳过未知字段,避免崩溃
}
}
jsonReader.endObject();
return this;
}
}
@@ -1,101 +0,0 @@
package cc.winboll.studio.positions.services;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/19 14:30:57
* @Describe 应用主要服务组件类守护进程服务组件类
*/
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import cc.winboll.studio.positions.services.MainService;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
import cc.winboll.studio.positions.utils.ServiceUtil;
public class AssistantService extends Service {
public final static String TAG = "AssistantService";
public static final String EXTRA_IS_SETTING_TO_ENABLE = "EXTRA_IS_SETTING_TO_ENABLE";
MyServiceConnection mMyServiceConnection;
volatile boolean mIsServiceRunning;
AppConfigsUtil mAppConfigsUtil;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mAppConfigsUtil = AppConfigsUtil.getInstance(this);
if (mMyServiceConnection == null) {
mMyServiceConnection = new MyServiceConnection();
}
// 设置运行参数
mIsServiceRunning = false;
if (mAppConfigsUtil.isEnableMainService(true)) {
run();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (mAppConfigsUtil.isEnableMainService(true)) {
run();
}
return mAppConfigsUtil.isEnableMainService(true) ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
mIsServiceRunning = false;
super.onDestroy();
}
//
// 运行服务内容
//
void run() {
if (mAppConfigsUtil.isEnableMainService(true)) {
if (mIsServiceRunning == false) {
// 设置运行状态
mIsServiceRunning = true;
// 唤醒和绑定主进程
wakeupAndBindMain();
}
}
}
//
// 唤醒和绑定主进程
//
void wakeupAndBindMain() {
if (ServiceUtil.isServiceAlive(getApplicationContext(), MainService.class.getName()) == false) {
startForegroundService(new Intent(AssistantService.this, MainService.class));
}
bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
}
//
// 主进程与守护进程连接时需要用到此类
//
class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mAppConfigsUtil.isEnableMainService(true)) {
wakeupAndBindMain();
}
}
}
}
@@ -1,433 +0,0 @@
//package cc.winboll.studio.positions.services;
//
///**
// * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
// * @Date 2025/09/30 19:53
// * @Describe 位置距离服务:管理数据+定时计算距离+适配Adapter(Java 7 兼容)+ GPS信号加载
// */
//import android.app.Service;
//import android.content.Context;
//import android.content.Intent;
//import android.content.pm.PackageManager;
//import android.location.Location;
//import android.location.LocationListener;
//import android.location.LocationManager;
//import android.os.Binder;
//import android.os.Build;
//import android.os.Bundle;
//import android.os.IBinder;
//import android.os.Looper;
//import android.widget.Toast;
//import cc.winboll.studio.libappbase.LogUtils;
//import cc.winboll.studio.positions.adapters.PositionAdapter;
//import cc.winboll.studio.positions.models.AppConfigsModel;
//import cc.winboll.studio.positions.models.PositionModel;
//import cc.winboll.studio.positions.models.PositionTaskModel;
//import cc.winboll.studio.positions.utils.NotificationUtil;
//import java.util.ArrayList;
//import java.util.HashSet;
//import java.util.Iterator;
//import java.util.Set;
//import java.util.concurrent.Executors;
//import java.util.concurrent.ScheduledExecutorService;
//import java.util.concurrent.TimeUnit;
//
///**
// * 核心职责:
// * 1. 实现 PositionAdapter.DistanceServiceInterface 接口,解耦Adapter与服务
// * 2. 单例式管理位置/任务数据,提供安全增删改查接口
// * 3. 后台单线程定时计算可见位置距离,主线程回调更新UI
// * 4. 内置GPS信号加载(通过LocationManager实时获取位置,解决“等待GPS信号”问题)
// * 5. 服务启动时启动前台通知(保活后台GPS功能,符合系统规范)
// * 6. 严格Java 7语法:无Lambda/Stream,显式迭代器/匿名内部类
// */
//public class DistanceRefreshService extends Service {
// public static final String TAG = "DistanceRefreshService";
//
//
// // 服务状态与配置
// private boolean isServiceRunning = false;
//
// private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
// // 前台通知相关:记录是否已启动前台服务(避免重复调用startForeground
// private boolean isForegroundServiceStarted = false;
//
//
//
// // 服务绑定与UI回调
// private final IBinder mBinder = new DistanceBinder();
//
//
//
//
// /**
// * 在主线程显示Toast(避免子线程无法显示Toast的问题)
// */
// private void showToastOnMainThread(final String message) {
// if (Looper.myLooper() == Looper.getMainLooper()) {
// Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
// } else {
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
// @Override
// public void run() {
// Toast.makeText(DistanceRefreshService.this, message, Toast.LENGTH_SHORT).show();
// }
// });
// }
// }
//
// // ---------------------- Binder 内部类(供外部绑定服务) ----------------------
// public class DistanceBinder extends Binder {
// /**
// * 外部绑定后获取服务实例(安全暴露服务引用)
// */
// public DistanceRefreshService getService() {
// return DistanceRefreshService.this;
// }
// }
//
//
//
// @Override
// public void onCreate() {
// super.onCreate();
//
// // 初始化GPS管理器(提前获取系统服务,避免启动时延迟)
// mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
// LogUtils.d(TAG, "服务 onCreate:初始化完成,等待启动命令");
// run();
// }
//
// @Override
// public int onStartCommand(Intent intent, int flags, int startId) {
// run();
// AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
// boolean isEnableService = (bean == null) ? false : bean.isEnableMainService();
// // 服务启用时返回START_STICKY(被杀死后尝试重启),禁用时返回默认值
// return isEnableService ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
// }
//
// public void run() {
// // 仅服务未运行时启动(避免重复启动)
// if (!isServiceRunning) {
// isServiceRunning = true;
//
//
//
// startDistanceRefreshTask(); // 启动定时距离计算
// startForegroundNotification(); // 启动前台通知
//
// LogUtils.d(TAG, "服务 onStartCommand:启动成功,刷新间隔=" + REFRESH_INTERVAL + "秒,前台通知+GPS已启动");
// } else {
// LogUtils.w(TAG, "服务 onStartCommand:已在运行,无需重复启动(前台通知:" + (isForegroundServiceStarted ? "已启动" : "未启动") + " | GPS" + (isGpsEnabled ? "已开启" : "未开启") + "");
// // 异常场景恢复:补全未启动的组件
// if (!isForegroundServiceStarted) {
// startForegroundNotification();
// LogUtils.d(TAG, "服务 run:前台通知未启动,已恢复");
// }
// if (isServiceRunning && !isGpsEnabled) {
// startGpsLocation();
// LogUtils.d(TAG, "服务 runGPS未启动,已恢复");
// }
// }
// }
//
// @Override
// public IBinder onBind(Intent intent) {
// return null; // 按你的业务逻辑返回,无绑定需求则保留null
// //LogUtils.d(TAG, "服务 onBind:外部绑定成功(运行状态:" + (isServiceRunning ? "是" : "否") + " | GPS状态:" + (isGpsEnabled ? "可用" : "不可用") + "");
// //return mBinder; // 返回Binder实例,供外部获取服务
// }
//
// /*@Override
// public boolean onUnbind(Intent intent) {
// LogUtils.d(TAG, "服务 onUnbind:外部解绑,清理回调与可见位置");
// // 解绑后清理资源,避免内存泄漏
// mDistanceReceiver = null;
// mVisiblePositionIds.clear();
// // 解绑时不停止GPS(服务仍在后台运行,需持续获取位置)
// return super.onUnbind(intent);
// }*/
//
// @Override
// public void onDestroy() {
// super.onDestroy();
//
// LogUtils.d(TAG, "服务 onDestroy:销毁完成,资源已释放(GPS+前台通知+线程池)");
// }
//
// // ---------------------- 前台服务通知管理(与GPS状态联动优化) ----------------------
// /**
// * 启动前台服务通知(调用NotificationUtils创建通知,确保仅启动一次)
// */
// private void startForegroundNotification() {
// // 1. 校验:避免重复调用startForeground(系统不允许重复启动)
// if (isForegroundServiceStarted) {
// LogUtils.w(TAG, "startForegroundNotification:前台通知已启动,无需重复执行");
// return;
// }
//
// try {
//// 2. 初始化通知状态文本(根据GPS初始状态动态显示,避免固定“等待”)
// String initialStatus;
// if (isGpsPermissionGranted && isGpsEnabled) {
// initialStatus = "GPS已就绪,正在获取位置(刷新间隔" + REFRESH_INTERVAL + "秒)";
// } else if (!isGpsPermissionGranted) {
// initialStatus = "缺少定位权限,无法获取GPS位置";
// } else {
// initialStatus = "GPS未开启,请在设置中打开";
// }
//
//
//// 5. 标记前台服务已启动
// isForegroundServiceStarted = true;
// LogUtils.d(TAG, "startForegroundNotification:前台服务通知启动成功,初始状态:" + initialStatus);
//
// } catch (Exception e) {
//// 捕获异常(如上下文失效、通知渠道未创建)
// isForegroundServiceStarted = false;
// LogUtils.d(TAG, "startForegroundNotification:前台通知启动失败" + e);
// }
// }
//
//
//
// /**
//
// - 主线程回调Adapter更新UI(避免跨线程操作UI异常)
// */
// /*private void notifyDistanceUpdateToUI(final String positionId) {
// if (Looper.myLooper() == Looper.getMainLooper()) {
// if (mDistanceReceiver != null) {
// mDistanceReceiver.onDistanceUpdate(positionId);
// }
// } else {
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
// @Override
// public void run() {
// if (mDistanceReceiver != null) {
// mDistanceReceiver.onDistanceUpdate(positionId);
// }
// }
// });
// }
// }*/
//
//
//
//
//
//// ---------------------- 实现 PositionAdapter.DistanceServiceInterface 接口 ----------------------
//
// public ArrayList getPositionList() {
// if (!isServiceRunning) {
// LogUtils.w(TAG, "getPositionList:服务未运行,返回空列表");
// return new ArrayList();
// }
// return new ArrayList(mPositionList);
// }
//
//
// public ArrayList getPositionTasksList() {
// if (!isServiceRunning) {
// LogUtils.w(TAG, "getPositionTasksList:服务未运行,返回空列表");
// return new ArrayList();
// }
// return new ArrayList(mTaskList);
// }
//
//
//
//
// /*public void setOnDistanceUpdateReceiver(PositionAdapter.OnDistanceUpdateReceiver receiver) {
// this.mDistanceReceiver = receiver;
// LogUtils.d(TAG, "setOnDistanceUpdateReceiver:回调接收器已设置(" + (receiver != null ? "有效" : "无效") + "");
// }*/
//
// public void addVisibleDistanceView(String positionId) {
// if (!isServiceRunning || positionId == null) {
// LogUtils.w(TAG, "addVisibleDistanceView:服务未运行/位置ID无效,添加失败");
// return;
// }
// if (mVisiblePositionIds.add(positionId)) {
// LogUtils.d(TAG, "addVisibleDistanceView:添加成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + "");
//// 新增:添加可见位置后,立即更新通知(显示最新可见数量)
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
// syncGpsStatusToNotification();
// }
// }
// }
//
// public void removeVisibleDistanceView(String positionId) {
// if (positionId == null) {
// LogUtils.w(TAG, "removeVisibleDistanceView:位置ID为空,移除失败");
// return;
// }
// if (mVisiblePositionIds.remove(positionId)) {
// int remainingCount = mVisiblePositionIds.size();
// LogUtils.d(TAG, "removeVisibleDistanceView:移除成功(位置ID=" + positionId + ",当前可见数=" + remainingCount + "");
//// 新增:移除可见位置后,更新通知(同步数量变化)
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
// syncGpsStatusToNotification();
// }
// }
// }
//
// public void clearVisibleDistanceViews() {
// mVisiblePositionIds.clear();
// LogUtils.d(TAG, "clearVisibleDistanceViews:所有可见位置已清空");
//// 新增:清空可见位置后,更新通知(提示计算暂停)
// if (isForegroundServiceStarted) {
// updateNotificationGpsStatus("无可见位置,距离计算暂停");
// }
// }
//
//// ---------------------- 数据管理接口(修复原有语法错误+优化逻辑) ----------------------
// /**
//
// - 获取服务运行状态
// */
// public boolean isServiceRunning() {
// return isServiceRunning;
// }
//
// /**
//
// - 添加位置(修复迭代器泛型缺失问题)
// */
// public void addPosition(PositionModel position) {
// if (!isServiceRunning || position == null || position.getPositionId() == null) {
// LogUtils.w(TAG, "addPosition:服务未运行/数据无效,添加失败");
// return;
// }// 修复:显式声明PositionModel泛型,避免类型转换警告
// boolean isDuplicate = false;
// Iterator posIter = mPositionList.iterator();
// while (posIter.hasNext()) {
// PositionModel existingPos = (PositionModel)posIter.next();
// if (position.getPositionId().equals(existingPos.getPositionId())) {
// isDuplicate = true;
// break;
// }
// }if (!isDuplicate) {
// mPositionList.add(position);
// LogUtils.d(TAG, "addPosition:添加成功(位置ID=" + position.getPositionId() + ",总数=" + mPositionList.size() + "");
// } else {
// LogUtils.w(TAG, "addPosition:位置ID=" + position.getPositionId() + "已存在,添加失败");
// }
// }
//
// /**
//
// - 删除位置(修复任务删除时的类型转换错误)
// */
// public void removePosition(String positionId) {
// if (!isServiceRunning || positionId == null) {
// LogUtils.w(TAG, "removePosition:服务未运行/位置ID无效,删除失败");
// return;
// }// 1. 删除位置
// boolean isRemoved = false;
// Iterator posIter = mPositionList.iterator();
// while (posIter.hasNext()) {
// PositionModel pos = (PositionModel)posIter.next();
// if (positionId.equals(pos.getPositionId())) {
// posIter.remove();
// isRemoved = true;
// break;
// }
// }if (isRemoved) {
//// 修复:任务列表迭代时用PositionTaskModel泛型(原错误用PositionModel导致转换失败)
// Iterator taskIter = mTaskList.iterator();
// while (taskIter.hasNext()) {
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
// if (positionId.equals(task.getPositionId())) {
// taskIter.remove();
// }
// }// 3. 移除可见位置
// mVisiblePositionIds.remove(positionId);
// LogUtils.d(TAG, "removePosition:删除成功(位置ID=" + positionId + ",剩余位置数=" + mPositionList.size() + ",剩余任务数=" + mTaskList.size() + "");
// } else {
// LogUtils.w(TAG, "removePosition:位置ID=" + positionId + "不存在,删除失败");
// }
// }
//
// /**
//
// - 更新位置信息(修复代码格式+迭代器泛型)
// */
// public void updatePosition(PositionModel updatedPosition) {
// if (!isServiceRunning || updatedPosition == null || updatedPosition.getPositionId() == null) {
// LogUtils.w(TAG, "updatePosition:服务未运行/数据无效,更新失败");
// return;
// }boolean isUpdated = false;
// Iterator posIter = mPositionList.iterator();
// while (posIter.hasNext()) {
// PositionModel pos = (PositionModel)posIter.next();
// if (updatedPosition.getPositionId().equals(pos.getPositionId())) {
// pos.setMemo(updatedPosition.getMemo());
// pos.setIsEnableRealPositionDistance(updatedPosition.isEnableRealPositionDistance());
// if (!updatedPosition.isEnableRealPositionDistance()) {
// pos.setRealPositionDistance(-1);
// //notifyDistanceUpdateToUI(pos.getPositionId());
// }
// isUpdated = true;
// break;
// }
// }if (isUpdated) {
// LogUtils.d(TAG, "updatePosition:更新成功(位置ID=" + updatedPosition.getPositionId() + "");
// } else {
// LogUtils.w(TAG, "updatePosition:位置ID=" + updatedPosition.getPositionId() + "不存在,更新失败");
// }
// }
//
// /**
//
// - 同步任务列表(修复泛型缺失+代码格式)
// */
// public void syncAllPositionTasks(ArrayList tasks) {
// if (!isServiceRunning || tasks == null) {
// LogUtils.w(TAG, "syncAllPositionTasks:服务未运行/任务列表为空,同步失败");
// return;
// }// 1. 清空旧任务
// mTaskList.clear();
//// 2. 添加新任务(修复泛型+去重逻辑)
// Set taskIdSet = new HashSet();
// Iterator taskIter = tasks.iterator();
// while (taskIter.hasNext()) {
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
// if (task != null && task.getTaskId() != null && !taskIdSet.contains(task.getTaskId())) {
// taskIdSet.add(task.getTaskId());
// mTaskList.add(task);
// }
// }LogUtils.d(TAG, "syncAllPositionTasks:同步成功(接收任务数=" + tasks.size() + ",去重后=" + mTaskList.size() + "");
// }
//
//
//
//
//// ---------------------- 补充:修复LocationProvider引用缺失问题(避免编译错误) ----------------------
//// 注:原代码中onStatusChanged使用LocationProvider枚举,需补充静态导入或显式声明
//// 此处通过内部静态类定义,解决系统API引用问题(兼容Java 7语法)
// private static class LocationProvider {
// public static final int AVAILABLE = 2;
// public static final int OUT_OF_SERVICE = 0;
// public static final int TEMPORARILY_UNAVAILABLE = 1;
// }
//
//// ---------------------- 补充:Context引用工具(避免服务销毁后Context失效) ----------------------
// /*private Context getSafeContext() {
// // 服务未销毁时返回自身Context,已销毁时返回应用Context(避免内存泄漏)
// if (isDestroyed()) {
// return getApplicationContext();
// }
// return this;
// }*/
//
//// 注:isDestroyed()为API 17+方法,若需兼容更低版本,可添加版本判断
// /*private boolean isDestroyed() {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// return super.isDestroyed();
// }
// // 低版本通过状态标记间接判断(服务销毁时会置为false)
// return !isServiceRunning && !isForegroundServiceStarted;
// }*/
//}
@@ -1,182 +0,0 @@
package cc.winboll.studio.positions.services;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler;
import cc.winboll.studio.positions.models.PositionModel;
import java.util.ArrayList;
import java.util.List;
/**
* 空转GPS模拟服务
* 在应用空转模式下,模拟系统GPS服务向客户端发送坐标数据
*/
public class IdleGpsService {
private static final long MOCK_INTERVAL_MS = 5000; // 模拟坐标更新间隔(5秒)
private static final long BEARING_INTERVAL_MS = 1000; // 角动量递增间隔(1秒)
private static final double EARTH_RADIUS_M = 6371000; // 地球平均半径(米)
private static final double ANCHOR_LAT = 39.9042; // 固定锚点纬度(北京)
private static final double ANCHOR_LON = 116.4074; // 固定锚点经度(北京)
private static final double CIRCLE_RADIUS_M = 50; // 移动轨迹半径(米)
private static IdleGpsService instance;
private final List<MainService.GpsUpdateListener> listeners = new ArrayList<>();
private final Handler handler;
private final Runnable updateRunnable;
private final Runnable bearingRunnable;
private final PositionModel mockPosition;
private boolean isRunning;
private int currentBearing = 1; // 当前角动量度数(1-360
private IdleGpsService() {
handler = new Handler(Looper.getMainLooper());
mockPosition = new PositionModel();
mockPosition.setPositionId("mock_idle_pos");
mockPosition.setMemo("空转模拟坐标");
updateRunnable = new Runnable() {
@Override
public void run() {
if (isRunning) {
calculatePosition();
notifyListeners(mockPosition);
AppIdleRunningModeHandler.sendIdleLog("模拟GPS数据更新 -> 纬度:" + mockPosition.getLatitude() + ", 经度:" + mockPosition.getLongitude() + ", 角度:" + currentBearing + "°");
handler.postDelayed(this, MOCK_INTERVAL_MS);
}
}
};
bearingRunnable = new Runnable() {
@Override
public void run() {
if (isRunning) {
currentBearing++;
if (currentBearing > 360) {
currentBearing = 1;
}
handler.postDelayed(this, BEARING_INTERVAL_MS);
}
}
};
}
private void calculatePosition() {
double bearingRad = Math.toRadians(currentBearing);
double angularDistance = CIRCLE_RADIUS_M / EARTH_RADIUS_M;
double anchorLatRad = Math.toRadians(ANCHOR_LAT);
double anchorLonRad = Math.toRadians(ANCHOR_LON);
double newLatRad = Math.asin(
Math.sin(anchorLatRad) * Math.cos(angularDistance) +
Math.cos(anchorLatRad) * Math.sin(angularDistance) * Math.cos(bearingRad)
);
double newLonRad = anchorLonRad + Math.atan2(
Math.sin(bearingRad) * Math.sin(angularDistance) * Math.cos(anchorLatRad),
Math.cos(angularDistance) - Math.sin(anchorLatRad) * Math.sin(newLatRad)
);
mockPosition.setLatitude(Math.toDegrees(newLatRad));
mockPosition.setLongitude(Math.toDegrees(newLonRad));
}
public static IdleGpsService getInstance() {
if (instance == null) {
instance = new IdleGpsService();
}
return instance;
}
/**
* 启动空转模拟服务
*/
public void start() {
if (isRunning) return;
isRunning = true;
currentBearing = 1;
AppIdleRunningModeHandler.sendIdleLog("空转GPS服务已启动");
notifyStatusChange("空转GPS服务已启动");
ToastUtils.show("空转GPS服务已启动");
handler.post(updateRunnable);
handler.post(bearingRunnable);
}
/**
* 停止空转模拟服务
*/
public void stop() {
if (!isRunning) return;
isRunning = false;
handler.removeCallbacks(updateRunnable);
handler.removeCallbacks(bearingRunnable);
notifyStatusChange("空转GPS服务已停止");
AppIdleRunningModeHandler.sendIdleLog("空转GPS服务已停止");
}
/**
* 注册GPS监听
*/
public void registerGpsUpdateListener(MainService.GpsUpdateListener listener) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
if (!isRunning) {
start();
}
}
/**
* 注销GPS监听
*/
public void unregisterGpsUpdateListener(MainService.GpsUpdateListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
if (listeners.isEmpty() && isRunning) {
stopMockUpdate();
}
}
private void startMockUpdate() {
if (isRunning) return;
isRunning = true;
currentBearing = 1;
notifyStatusChange("空转GPS服务已启动");
handler.post(updateRunnable);
handler.post(bearingRunnable);
}
private void stopMockUpdate() {
isRunning = false;
handler.removeCallbacks(updateRunnable);
handler.removeCallbacks(bearingRunnable);
notifyStatusChange("空转GPS服务已停止");
}
private void notifyListeners(PositionModel pos) {
synchronized (listeners) {
for (MainService.GpsUpdateListener listener : listeners) {
listener.onGpsPositionUpdated(pos);
}
}
}
private void notifyStatusChange(String status) {
synchronized (listeners) {
for (MainService.GpsUpdateListener listener : listeners) {
listener.onGpsStatusChanged(status);
}
}
}
public boolean isRunning() {
return isRunning;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,68 +0,0 @@
package cc.winboll.studio.positions.utils;
import android.content.Context;
import cc.winboll.studio.positions.models.AppConfigsModel;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/01 12:15
* @Describe AppConfigsUtils
*/
public class AppConfigsUtil {
public static final String TAG = "AppConfigsUtils";
// 1. 私有静态成员变量(volatile 防止指令重排,确保实例初始化完全)
private static volatile AppConfigsUtil sInstance;
Context mContext;
AppConfigsModel mAppConfigsModel;
// 2. 私有构造方法(禁止外部 new 实例)
private AppConfigsUtil(Context context) {
// 可选:防止通过反射创建实例(增强单例安全性)
if (sInstance != null) {
throw new RuntimeException("禁止通过反射创建单例实例");
}
mContext = context;
loadConfigs();
}
// 3. 公开静态方法(双重校验锁,获取唯一实例)
public static AppConfigsUtil getInstance(Context context) {
// 第一次校验:无锁,快速判断实例是否存在(提升性能)
if (sInstance == null) {
// 加锁:确保同一时间只有一个线程进入初始化逻辑
synchronized (AppConfigsUtil.class) {
// 第二次校验:防止多线程并发时重复创建实例(线程安全)
if (sInstance == null) {
sInstance = new AppConfigsUtil(context);
}
}
}
return sInstance;
}
public void loadConfigs() {
mAppConfigsModel = AppConfigsModel.loadBean(mContext, AppConfigsModel.class);
}
public void saveConfigs() {
AppConfigsModel.saveBean(mContext, mAppConfigsModel);
}
public boolean isEnableMainService(boolean isReloadConfigs) {
if (isReloadConfigs) {
loadConfigs();
}
return (mAppConfigsModel == null) ?false: mAppConfigsModel.isEnableMainService();
}
public void setIsEnableMainService(boolean isEnableMainService) {
if(mAppConfigsModel == null) {
mAppConfigsModel = new AppConfigsModel();
}
mAppConfigsModel.setIsEnableMainService(isEnableMainService);
saveConfigs();
}
}
@@ -1,45 +0,0 @@
package cc.winboll.studio.positions.utils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/22 01:16
* @Describe DensityUtils
*/
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
/**
* 屏幕密度工具类(dp/sp 转 px、获取屏幕尺寸)
*/
public class DensityUtils {
public static final String TAG = "DensityUtils";
/**
* dp 转 px(根据屏幕密度)
*/
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* sp 转 px(根据文字缩放比例)
*/
public static int sp2px(Context context, float spValue) {
final float scale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * scale + 0.5f);
}
/**
* 获取屏幕宽度(像素)
*/
public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
return dm.widthPixels;
}
}
@@ -1,246 +0,0 @@
package cc.winboll.studio.positions.utils;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.models.PositionModel;
import cc.winboll.studio.positions.models.PositionTaskModel;
import cc.winboll.studio.positions.services.MainService;
import java.util.ArrayList;
import java.util.Iterator;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/27 18:40
* @Describe 距离计算工具集(单例模式)
*/
public class DistanceCalculatorUtil {
public static final String TAG = "DistanceCalculatorUtil";
// 1. 私有静态 volatile 实例:保证多线程下实例可见性,避免指令重排序导致的空指针
private static volatile DistanceCalculatorUtil sInstance;
Context mContext;
ArrayList<PositionModel> mPositionList; // 位置数据列表
ArrayList<PositionTaskModel> mAllTasks;// 任务数据列表
PositionModel mGpsPositionCalculated;
long mLastCalculatedTime = 0;
long mMinCalculatedTimeBettween = 30000; // GPS数据更新时,两次计算之间的最小时间间隔
double mMinjumpDistance = 10.0f; // GPS数据更新时,能跳跃距离最小有效值,达到有效值时,两次计算之间的最小时间间隔阀值将被忽略。
// 2. 私有构造器:禁止外部通过 new 关键字创建实例,确保单例唯一性
private DistanceCalculatorUtil(Context context) {
// 可选:初始化工具类依赖的资源(如配置参数、缓存等)
mContext = context;
LogUtils.d(TAG, "DistanceCalculatorUtil 单例实例初始化");
}
// 3. 公开静态方法:双重校验锁获取单例,兼顾线程安全与性能
public static DistanceCalculatorUtil getInstance(Context context) {
// 第一重校验:避免已创建实例时的频繁加锁,提升性能
if (sInstance == null) {
// 加锁:确保多线程下仅一个线程进入实例创建逻辑
synchronized (DistanceCalculatorUtil.class) {
// 第二重校验:防止多线程并发时重复创建实例
if (sInstance == null) {
sInstance = new DistanceCalculatorUtil(context);
}
}
}
return sInstance;
}
// ---------------------- 以下可补充距离计算相关工具方法 ----------------------
/**
* 示例:Haversine 公式计算两点间距离(单位:米)
* @param lat1 第一点纬度
* @param lon1 第一点经度
* @param lat2 第二点纬度
* @param lon2 第二点经度
* @return 两点间直线距离(米),计算失败返回 -1
*/
public static double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) {
try {
final double EARTH_RADIUS = 6371000; // 地球半径(米)
// 角度转弧度
double latDiff = Math.toRadians(lat2 - lat1);
double lonDiff = Math.toRadians(lon2 - lon1);
// Haversine 核心公式
double a = Math.sin(latDiff / 2) * Math.sin(latDiff / 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS * c; // 返回距离(米)
} catch (Exception e) {
LogUtils.d(TAG, "Haversine 距离计算失败:" + e.getMessage());
return -1; // 标记计算失败
}
}
/**
* 计算两点间距离(Haversine公式,纯Java 7 基础API,无数学工具类依赖)
* @param gpsLat GPS纬度
* @param gpsLon GPS经度
* @param posLat 目标位置纬度
* @param posLon 目标位置经度
* @return 两点间距离(单位:米)
*/
/*private double calculateHaversineDistance(double gpsLat, double gpsLon, double posLat, double posLon) {
final double EARTH_RADIUS = 6371000; // 地球半径(米)
double latDiff = Math.toRadians(posLat - gpsLat);
double lonDiff = Math.toRadians(posLon - gpsLon);
// Haversine公式核心计算(Java 7 基础数学方法)
double a = Math.sin(latDiff / 2) * Math.sin(latDiff / 2)
+ Math.cos(Math.toRadians(gpsLat)) * Math.cos(Math.toRadians(posLat))
* Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS * c;
}*/
/**
* 校验所有任务触发条件(距离达标则触发任务通知)
*/
public void checkAllTaskTriggerCondition(PositionModel currentGpsPosition) {
if (currentGpsPosition == null) {
LogUtils.d(TAG, "传入坐标参数为空,退出函数。");
return;
}
// 计算频率控制模块
//
// 计算与最近一次GPS计算的时间间隔
long nCalculatedTimeBettween = System.currentTimeMillis() - mLastCalculatedTime;
// 计算跳跃距离
double gpsPositionCalculatedLatitude = mGpsPositionCalculated == null ?0.0f: mGpsPositionCalculated.getLatitude();
double gpsPositionCalculatedLongitude = mGpsPositionCalculated == null ?0.0f: mGpsPositionCalculated.getLongitude();
double jumpDistance = calculateHaversineDistance(gpsPositionCalculatedLatitude, gpsPositionCalculatedLongitude, currentGpsPosition.getLatitude(), currentGpsPosition.getLongitude());
if (jumpDistance < mMinjumpDistance) {
LogUtils.d(TAG, String.format("checkAllTaskTriggerCondition:跳跃距离%f,小于50米。", jumpDistance));
// 跳跃距离小于最小有效跳跃值
if (nCalculatedTimeBettween < mMinCalculatedTimeBettween) {
//间隔小于最小时间间隔设定
LogUtils.d(TAG, String.format("checkAllTaskTriggerCondition:与最近一次计算间隔时间%d,坐标变化忽略。", nCalculatedTimeBettween));
return;
}
}
if (mGpsPositionCalculated == null) {
mGpsPositionCalculated = currentGpsPosition;
LogUtils.d(TAG, "最后计算位置记录为空,现在使用新坐标为初始化。");
}
LogUtils.d(TAG, String.format("checkAllTaskTriggerCondition:跳跃距离%f,与上次计算间隔%d,现在启动任务数据计算。", jumpDistance, nCalculatedTimeBettween));
// 获取位置任务基础数据
MainService mainService = MainService.getInstance(mContext);
mPositionList = mainService.getPositionList();
mAllTasks = mainService.getAllTasks();
// 位置数据为空,跳过校验。
if (mPositionList.isEmpty()) {
LogUtils.d(TAG, "checkAllTaskTriggerCondition:位置数据为空,跳过距离计算。");
return;
}
// 更新所有位置点的位置距离数据
refreshRealPositionDistance(currentGpsPosition);
// 任务数据为空,跳过校验。
if (mAllTasks.isEmpty()) {
LogUtils.d(TAG, "checkAllTaskTriggerCondition:任务数据为空,跳过任务提醒检查计算。");
return;
}
// 迭代器遍历任务(Java 7 安全遍历,避免并发修改异常)
Iterator<PositionTaskModel> taskIter = mAllTasks.iterator();
while (taskIter.hasNext()) {
PositionTaskModel task = taskIter.next();
// 仅校验“已启用”且“绑定有效位置”的任务
if (!task.isEnable() || TextUtils.isEmpty(task.getPositionId())) {
continue;
}
// 查找任务绑定的位置(Java 7 迭代器遍历位置列表)
PositionModel bindPos = null;
Iterator<PositionModel> posIter = mPositionList.iterator();
while (posIter.hasNext()) {
PositionModel pos = posIter.next();
if (task.getPositionId().equals(pos.getPositionId())) {
bindPos = pos;
break;
}
}
if (bindPos == null) {
LogUtils.w(TAG, "任务ID=" + task.getTaskId() + ":绑定位置不存在,跳过");
task.setIsBingo(false);
continue;
}
// 校验任务开始时间
if (task.getStartTime() > System.currentTimeMillis()) {
continue;
}
// 校验距离条件(判断是否满足任务触发阈值)
double currentDistance = bindPos.getRealPositionDistance();
if (currentDistance < 0) {
LogUtils.w(TAG, "任务ID=" + task.getTaskId() + ":距离计算失败,跳过");
task.setIsBingo(false);
continue;
}
boolean isTriggered = false;
int taskDistance = task.getDiscussDistance();
// 任务触发条件:大于/小于指定距离(Java 7 基础判断,无三元运算符嵌套)
if (task.isGreaterThan()) {
isTriggered = currentDistance > taskDistance;
} else if (task.isLessThan()) {
isTriggered = currentDistance < taskDistance;
}
// 更新任务触发状态+发送通知(状态变化时才处理)
if (task.isBingo() != isTriggered) {
task.setIsBingo(isTriggered);
if (isTriggered) {
MainService.getInstance(mContext).sendTaskTriggerNotification(task, bindPos, currentDistance);
}
}
}
MainService.getInstance(mContext).saveAllTasks(); // 持久化更新后的任务状态
// 记录最后坐标更新点
mGpsPositionCalculated = currentGpsPosition;
// 记录数据计算时间
mLastCalculatedTime = System.currentTimeMillis();
}
/**
* 强制刷新所有位置距离(GPS更新后调用,计算距离+校验任务触发条件)
*/
public void refreshRealPositionDistance(PositionModel currentGpsPosition) {
// 遍历所有位置计算距离(Java 7 增强for循环,无Stream
for (PositionModel pos : mPositionList) {
if (pos.isEnableRealPositionDistance()) {
double distance = DistanceCalculatorUtil.calculateHaversineDistance(
currentGpsPosition.getLatitude(),
currentGpsPosition.getLongitude(),
pos.getLatitude(),
pos.getLongitude()
);
pos.setRealPositionDistance(distance);
} else {
pos.setRealPositionDistance(-1); // 未启用距离计算,标记为无效
}
}
// 距离刷新后通知GPS监听者
MainService.getInstance(mContext).notifyAllGpsListeners(currentGpsPosition);
}
}

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