Compare commits

...

53 Commits

Author SHA1 Message Date
ZhanGSKen bc08da41e5 <bytheway>APK 15.20.3 release Publish. 2026-07-10 13:02:55 +08:00
ZhanGSKen da1c07da56 调整GPS数据接收时间间隔为5秒。 2026-07-10 12:59:34 +08:00
ZhanGSKen 91d97efe4e <bytheway>APK 15.20.2 release Publish. 2026-07-09 20:17:02 +08:00
MiMo 1d4b232491 Merge remote-tracking branch 'origin/bytheway' into merge 2026-07-09 20:11:22 +08:00
MiMo 33f74f8226 fix: 修复GpsReceiveService GPS计数值不更新问题
将GPS接收方式从BroadcastReceiver替换为LocationManager+LocationListener,
解决因无广播发送方导致计数值永远为0的问题。

- 使用LocationManager.requestLocationUpdates主动获取GPS定位
- 新增currentGpsBean内存缓存当前最新GPS数据
- 通知栏显示GPS数据信息(经纬度/精度)和接收次数
- 新增getCurrentGpsBean()供外部获取当前GPS数据
2026-07-09 20:07:38 +08:00
ZhanGSKen a74ab6aa06 更新说明书里的中文应用名说法为之前改过的名称。 2026-07-07 00:47:15 +08:00
MiMo 8b3c6c3481 主窗口菜单添加应用介绍入口,点击打开AboutActivity
- strings.xml新增action_about字符串资源
- activity_main.xml菜单新增action_about菜单项
- MainActivity onOptionsItemSelected添加action_about点击处理
2026-07-07 00:42:53 +08:00
ZhanGSKen 8a50eaf24c 添加应用内公共抽象窗口类,添加应用介绍窗口资源。 2026-07-07 00:34:39 +08:00
MiMo d4fddaecf9 GPS接收服务增加数据接收次数计数并在通知栏实时显示
- onCreate初始化gpsReceiveCount计数器归零
- parseAndSaveGpsData每次接收数据后递增计数并刷新通知栏
- 通知栏文本更新为"已接收GPS数据 N 次"
2026-07-07 00:13:26 +08:00
MiMo 7c8800f873 GpsUtil转型为GPS数据处理模块,封装数据库操作
- GpsUtil新增insertGps、queryAllGps、queryByTime、clearAllGps静态方法
- GpsDbHelper内嵌为GpsUtil私有内部类,外部不可直接访问
- 删除原GpsDbHelper.java和GpsDbManager.java独立文件
- GpsReceiveService和GpsListActivity改为通过GpsUtil访问数据库
2026-07-06 20:23:58 +08:00
MiMo 32afb3ad74 添加GPS数据处理工具类,整合分散的GPS处理方法
- 新增 GpsUtil 工具类,提供 parseFromIntent、parseFromCursor、
  formatTime、formatLatLng、formatOther、speedToKmh、formatSpeed 方法
- GpsReceiveService 调用 GpsUtil.parseFromIntent() 替代内联解析
- GpsAdapter 调用 GpsUtil 格式化方法替代内联格式化,移除 SimpleDateFormat 字段
- GpsDbManager 调用 GpsUtil.parseFromCursor() 消除两处重复的游标映射代码
2026-07-06 20:02:13 +08:00
ZhanGSKen afd57c8d70 更正服务类的属性和方法命名,明确服务分类。 2026-07-06 14:19:58 +08:00
MiMo 8b4624982c feat: GPS定位服务增加运行状态查询接口
- GpsReceiveService 新增公共静态函数 isServiceRunning()
- SettingsActivity 窗口初始化时查询服务状态并同步开关状态
2026-07-06 13:58:47 +08:00
ZhanGSKen 5283ebadc9 GPS信息接收服务总开关UI接口优化。 2026-07-05 15:05:56 +08:00
ZhanGSKen e7fdf3ce6a <bytheway>APK 15.20.1 release Publish. 2026-07-03 13:02:58 +08:00
ZhanGSKen 7407ba582e 更新应用中文名称为更贴切的应用场景名称 2026-07-03 13:00:59 +08:00
ZhanGSKen 6a9112fd20 <bytheway>APK 15.20.0 release Publish. 2026-07-03 12:40:12 +08:00
MiMo 7d960fd445 MainActivity: 主窗口菜单添加GPS列表入口,点击打开GpsListActivity 2026-07-02 16:57:20 +08:00
BigPickle e443e54548 组合框架应用操作流程响应 2026-07-02 16:38:40 +08:00
ZhanGSKen ecb7f9b881 添加GPS数据处理框架 2026-07-02 15:39:23 +08:00
ZhanGSKen 248ffcc5f1 创建ByTheWay项目主体框架 2026-07-02 14:10:15 +08:00
BigPickle 49414a3938 App/MainActivity: 修复主题切换 crash 及工具栏主题不刷新问题
- App.onCreate 添加 AESThemeUtil.init(null) 初始化 themeStyleIDList,
  修复点击主题菜单时 IndexOutOfBoundsException 崩溃
- MainActivity.onCreate 添加 AESThemeUtil.applyAppCompatTheme(this)
  置于 super.onCreate 之前,使 recreate() 后应用已保存的主题样式
2026-07-02 01:34:58 +08:00
BigPickle 08a47768a1 MainActivity: 接入 AESThemeUtil/DevelopUtils 工具类,替换 Toolbar 为 ASupportToolbar
- MainActivity 新增 onCreateOptionsMenu/onOptionsItemSelected,
  使用 AESThemeUtil.inflateCompatThemeMenu 加载主题菜单、
  DevelopUtils.inflateMenu 加载调试菜单
- 布局文件 Toolbar 替换为 cc.winboll.studio.libaes.views.ASupportToolbar,
  移除 AppBarLayout 包裹层
- MyAppTheme 日间/夜间主题增加 aSupportToolbar 属性引用 AESASupportToolbar 样式
2026-07-02 01:26:50 +08:00
ZhanGSKen 79f161b559 fix: 修复 MyAppTheme 主题继承链,适配 Android 16(SDK 36)
- 修复 view_log.xml 自定义属性解析失败崩溃(需要 APPBaseTheme 的属性集)
- 修复 AppCompatActivity 需要 Theme.AppCompat 子主题的 IllegalStateException
- 父主题改为 Theme.AppCompat.Light.NoActionBar / Theme.AppCompat.NoActionBar
- 新增 values-night/ 日夜模式自动切换支持
- 主色调从 Teal/Orange 更换为 Blue/Amber 专业配色方案
2026-07-01 17:06:20 +08:00
LaizyBoy b14538db47 20260701_163734_738 2026-07-01 16:37:41 +08:00
ZhanGSKen c9e16a9a1e 编译配置完成 2026-07-01 15:53:23 +08:00
ZhanGSKen 42dbfb4238 加入WinBoLL框架文件 2026-07-01 15:47:18 +08:00
ZhanGSKen b274503187 20260701_154438_390 2026-07-01 15:45:11 +08:00
ZhanGSKen b50e4e02ea 修复git管理脚本 2026-07-01 04:49:24 +08:00
ZhanGSKen 84f1c820cb 修复git管理脚本 2026-07-01 04:48:30 +08:00
ZhanGSKen 0e60d75ac6 修复git管理脚本 2026-07-01 04:44:47 +08:00
ZhanGSKen 773cc9f6c1 修复git管理脚本 2026-07-01 04:42:45 +08:00
ZhanGSKen ca33dab4da 修复git管理脚本 2026-07-01 04:33:36 +08:00
ZhanGSKen eee8809485 修复git管理脚本 2026-07-01 04:11:36 +08:00
ZhanGSKen 69f79549e9 合并模块WinBoLL 同步最新时间标签winboll-v15.20.8 2026-07-01 04:10:40 +08:00
ZhanGSKen f204cd2608 修复git管理脚本 2026-07-01 04:08:42 +08:00
ZhanGSKen 2d3592690a 更新新版winboll架构 2026-07-01 03:49:09 +08:00
ZhanGSKen 576f8bb5e5 更新新版winboll架构 2026-07-01 03:48:29 +08:00
qinglong e787697d6b 合并模块AES 同步最新时间标签aes-v15.20.17 2026-06-30 19:00:05 +08:00
qinglong 3db3edcf2c 合并模块APPBase 同步最新时间标签appbase-v15.20.34 2026-06-30 19:00:05 +08:00
qinglong 643d08bf22 合并模块AES 同步最新时间标签aes-v15.20.16 2026-06-26 02:00:28 +08:00
qinglong beefb16c32 合并模块APPBase 同步最新时间标签appbase-v15.20.33 2026-06-26 02:00:28 +08:00
qinglong 878009ec9f 合并模块WinBoLL 同步最新时间标签winboll-v15.20.8 2026-06-26 02:00:28 +08:00
qinglong 2d8b5d515b 合并模块AES 同步最新时间标签aes-v15.20.15 2026-06-03 20:30:22 +08:00
qinglong 1453287667 合并模块APPBase 同步最新时间标签appbase-v15.20.26 2026-06-03 20:30:22 +08:00
qinglong 5fc59f3305 合并模块AES 同步最新时间标签aes-v15.20.14 2026-06-03 08:00:01 +08:00
qinglong b6ba73126f 合并模块WinBoLL 同步最新时间标签winboll-v15.20.7 2026-06-03 08:00:01 +08:00
qinglong bdd8d1e2b6 合并模块APPBase 同步最新时间标签appbase-v15.20.25 2026-06-03 07:00:01 +08:00
qinglong 138ca80e35 合并模块APPBase 同步最新时间标签appbase-v15.20.23 2026-06-03 06:06:41 +08:00
qinglong 7a3b54b78b 合并模块WinBoLL 同步最新时间标签winboll-v15.20.6 2026-06-02 21:00:01 +08:00
qinglong db804d1897 合并模块AES 同步最新时间标签aes-v15.20.12 2026-06-02 08:55:05 +08:00
qinglong 039c8fcd98 合并模块WinBoLL 同步最新时间标签winboll-v15.20.5 2026-06-02 04:00:01 +08:00
qinglong ae63d1ec0a 合并模块AES 同步最新时间标签aes-v15.20.11 2026-06-02 03:00:01 +08:00
826 changed files with 3069 additions and 67337 deletions
+3 -3
View File
@@ -94,8 +94,8 @@ lint-results.html
## 忽略 AndroidIDE 临时文件夹
.androidide
## WinBoLL 基础应用(避免上传敏感配置)
## MyWinBoLL 基础应用(避免上传敏感配置)
/winboll.properties
/local.properties
/settings.gradle
/gradle.properties
#/settings.gradle
#/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,7 +72,6 @@ libaes
libappbase
libdebugtemp
libgpsrelaysentinel
libwinboll
local.properties-demo
mymessagemanager
positions
@@ -82,13 +81,14 @@ winboll
winboll.properties-demo
)
# ====================== 5. 获取当前目录真实文件列表 ======================
# ====================== 5. 获取当前目录真实文件列表(兼容过滤 . .. ======================
REAL_ITEMS=()
# 使用固定排序ls,自动过滤 . 和 ..,不会进入比对数组
while IFS= read -r line; do
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(ls -a)
done < <(LC_COLLATE=C ls -a1 --color=none)
# ====================== 6. 差异比对函数 ======================
check_diff() {
@@ -158,7 +158,7 @@ echo -e "## 对象列表结束
## 合并 APP 项目
MERGE_APP_PROJECT_LIST=(
DemoAPP
WinBoLL
)
echo -e "#@@@ 开始合并应用型模块源码 @@@#
## 目标合并对象列表:"
@@ -166,14 +166,13 @@ 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
)
@@ -183,10 +182,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,7 +65,6 @@ libaes
libappbase
libdebugtemp
libgpsrelaysentinel
libwinboll
local.properties-demo
mymessagemanager
positions
@@ -76,9 +75,13 @@ winboll.properties-demo
)
REAL_ITEMS=()
# 标准排序ls输出,循环强制过滤 . 和 ..
while IFS= read -r line; do
[[ $line != "." && $line != ".." ]] && REAL_ITEMS+=("$line")
done < <(ls -a)
# 跳过虚拟目录 . 和 ..
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(LC_COLLATE=C ls -a1 --color=none)
check_diff(){
local miss=() extra=()
@@ -89,7 +92,17 @@ 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
}
@@ -98,7 +111,7 @@ check_diff
echo -e "#@@@ 按时间获取最新标签合并模块源码 @@@#"
# 应用型模块
MERGE_APP_PROJECT_LIST=(DemoAPP)
MERGE_APP_PROJECT_LIST=(WinBoLL)
echo -e "---------- 应用型模块 ----------"
for name in "${MERGE_APP_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
@@ -120,7 +133,7 @@ for name in "${MERGE_APP_PROJECT_LIST[@]}";do
done
# 类库模块
MERGE_LIB_PROJECT_LIST=(WinBoLL APPBase AES)
MERGE_LIB_PROJECT_LIST=(APPBase AES)
echo -e "---------- 类库模块 ----------"
for name in "${MERGE_LIB_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
+228
View File
@@ -0,0 +1,228 @@
#!/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
@@ -0,0 +1,20 @@
#!/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
+56 -27
View File
@@ -122,7 +122,6 @@ 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()) {
@@ -130,6 +129,7 @@ android {
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
}
if(outBuildBckDir.exists()) {
def targetApkFile = new File(outBuildBckDir, outputFileName)
copy{
from file.outputFile
into outBuildBckDir
@@ -138,6 +138,14 @@ 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)
@@ -160,8 +168,7 @@ android {
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);
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
println "\n\n>>> Library Project build.properties saved.\n\n";
}
@@ -172,16 +179,12 @@ 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()) {
@@ -192,12 +195,10 @@ 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
@@ -206,7 +207,16 @@ 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
@@ -215,6 +225,14 @@ 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)
@@ -239,14 +257,11 @@ 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);
}
@@ -263,17 +278,12 @@ 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()) {
@@ -284,13 +294,11 @@ 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
@@ -299,7 +307,16 @@ 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
@@ -308,8 +325,13 @@ 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}"
}
}
}
@@ -328,6 +350,13 @@ 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}"
}
}
}
-201
View File
@@ -1,201 +0,0 @@
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
@@ -0,0 +1,97 @@
# 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 生成)
-4
View File
@@ -39,10 +39,6 @@ android {
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed May 27 20:22:45 HKT 2026
stageCount=11
#Mon Jun 29 11:28:11 HKT 2026
stageCount=18
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.10
publishVersion=15.20.17
buildCount=0
baseBetaVersion=15.20.11
baseBetaVersion=15.20.18
@@ -65,7 +65,7 @@ public class AboutActivity extends BaseWinBoLLActivity {
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("AES");
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
@@ -5,10 +5,14 @@ package cc.winboll.studio.aes;
* @Date 2024/06/13 19:03:58
* @Describe AES应用类
*/
import android.view.Gravity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
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;
public class App extends GlobalApplication {
@@ -17,11 +21,25 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
WinBoLLActivityManager.init(this);
try {
super.onCreate();
ToastUtils.init(this);
WinBoLLActivityManager.init(this);
AESThemeUtil.init(null);
} 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
);
}
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override
@@ -1,11 +1,7 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import cc.winboll.studio.libaes.views.ADsControlView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -20,7 +16,7 @@ public class SettingsActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
//ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override
@@ -6,12 +6,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed May 27 14:51:29 HKT 2026
stageCount=23
#Sun Jun 28 20:55:33 HKT 2026
stageCount=35
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.22
publishVersion=15.20.34
buildCount=0
baseBetaVersion=15.20.23
baseBetaVersion=15.20.35
@@ -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("APPBase");
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
@@ -1,8 +1,11 @@
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.BuildConfig;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,10 +24,24 @@ public class App extends GlobalApplication {
*/
@Override
public void onCreate() {
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
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
);
}
}
/**
-34
View File
@@ -1,34 +0,0 @@
# AutoNFC
#### 介绍
NFC 卡应用,主要管理 NFC 卡接触手机的动作响应,NFC 接触状态用于作为其他应用激活活动动作的启动令牌。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh autonfc
#### 使用说明
#### 参与贡献
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/)
#### 参考文档
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Mar 16 18:30:19 GMT 2026
stageCount=0
libraryProject=
baseVersion=15.11
publishVersion=15.0.0
buildCount=54
baseBetaVersion=15.0.1
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-51
View File
@@ -1,51 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.autonfc">
<uses-permission android:name="android.permission.NFC"/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"/>
<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>
<activity
android:name=".nfc.NFCInterfaceActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<!-- NFC 绑定服务 -->
<service
android:name=".nfc.AutoNFCService"
android:exported="false"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
</application>
</manifest>
@@ -1,180 +0,0 @@
package cc.winboll.studio.autonfc;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.IBinder;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.autonfc.nfc.ActionDialog;
import cc.winboll.studio.autonfc.nfc.AutoNFCService;
import cc.winboll.studio.autonfc.nfc.NFCInterfaceActivity;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent;
private AutoNFCService mService;
private boolean mBound = false;
// 服务连接
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
AutoNFCService.LocalBinder binder = (AutoNFCService.LocalBinder) service;
mService = binder.getService();
mBound = true;
LogUtils.d(TAG, "onServiceConnected: 服务已绑定");
// 关键:把 Activity 传给 Service,用于回调
mService.attachActivity(MainActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
mService = null;
LogUtils.d(TAG, "onServiceDisconnected: 服务已断开");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// 初始化 NFC
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, 0);
LogUtils.d(TAG, "onCreate() -> NFC 监听已绑定到 MainActivity");
}
@Override
protected void onStart() {
super.onStart();
// 绑定服务
Intent intent = new Intent(this, AutoNFCService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
LogUtils.d(TAG, "onStart: 绑定服务");
}
@Override
protected void onStop() {
super.onStop();
// 解绑服务
if (mBound) {
unbindService(mConnection);
mBound = false;
LogUtils.d(TAG, "onStop: 解绑服务");
}
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume() -> 开启 NFC 前台分发");
if (mNfcAdapter != null) {
mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
}
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause() -> 关闭 NFC 前台分发");
if (mNfcAdapter != null) {
mNfcAdapter.disableForegroundDispatch(this);
}
}
// NFC 卡片靠近唯一入口
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
LogUtils.d(TAG, "onNewIntent() -> 检测到 NFC 卡片");
// 把 NFC 事件交给 Service 处理
if (mBound && mService != null) {
mService.handleNfcIntent(intent);
} else {
LogUtils.e(TAG, "服务未绑定,无法处理 NFC");
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_log) {
LogActivity.startLogActivity(this);
return true;
}
return super.onOptionsItemSelected(item);
}
public void onNFCInterfaceActivity(View view) {
startActivity(new Intent(this, NFCInterfaceActivity.class));
}
// ========================= 【新增】关键方法:由 Service 回调来弹出对话框 =========================
/**
* Service 解析完 NFC 数据后,回调此方法在 Activity 中弹出对话框
*/
public void showNfcActionDialog(final String nfcData) {
LogUtils.d(TAG, "showNfcActionDialog() -> Activity 存活,安全弹出对话框");
// Activity 正在运行,直接弹框,绝对不会报 BadTokenException
final ActionDialog dialog = new ActionDialog(this);
dialog.setNfcData(nfcData);
dialog.setButtonClickListener(new ActionDialog.OnButtonClickListener() {
@Override
public void onBuildClick() {
LogUtils.d(TAG, "点击 Build");
if (mService != null) {
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD, nfcData);
}
dialog.dismiss();
}
@Override
public void onViewClick() {
LogUtils.d(TAG, "点击 View");
if (mService != null) {
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD_VIEW, nfcData);
}
dialog.dismiss();
}
@Override
public void onCancelClick() {
dialog.dismiss();
}
});
dialog.show();
}
}
@@ -1,66 +0,0 @@
package cc.winboll.studio.autonfc.models;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/03/16 09:38
*/
public class NfcTermuxCmd {
private String script; // 要执行的预制脚本名(如 auth.sh)
private String[] args; // 脚本参数
private String workDir; // 工作目录
private boolean background; // 是否后台执行
private String resultDir; // 结果输出目录(可为 null
public NfcTermuxCmd() {
}
public NfcTermuxCmd(String script, String[] args, String workDir, boolean background, String resultDir) {
this.script = script;
this.args = args;
this.workDir = workDir;
this.background = background;
this.resultDir = resultDir;
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public String[] getArgs() {
return args;
}
public void setArgs(String[] args) {
this.args = args;
}
public String getWorkDir() {
return workDir;
}
public void setWorkDir(String workDir) {
this.workDir = workDir;
}
public boolean isBackground() {
return background;
}
public void setBackground(boolean background) {
this.background = background;
}
public String getResultDir() {
return resultDir;
}
public void setResultDir(String resultDir) {
this.resultDir = resultDir;
}
}
@@ -1,123 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Dialog;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import cc.winboll.studio.autonfc.R;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 自定义对话框类,用于与用户交互,展示 NFC 相关操作选项
* 兼容 Java 7 语法
*
* @author 豆包&ZhanGSKen
* @create 2025-08-15
* @lastModify 2026-03-17
*/
public class ActionDialog extends Dialog {
private static final String TAG = "ActionDialog";
private String mNfcData;
private OnButtonClickListener mClickListener;
/**
* 构造函数
*/
public ActionDialog(Context context) {
super(context);
initDialog();
}
/**
* 设置 NFC 数据
*/
public void setNfcData(String nfcData) {
this.mNfcData = nfcData;
LogUtils.d(TAG, "setNfcData() -> " + nfcData);
}
/**
* 设置点击监听
*/
public void setButtonClickListener(OnButtonClickListener listener) {
this.mClickListener = listener;
}
/**
* 初始化布局
*/
private void initDialog() {
setTitle("请选择操作");
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(20, 20, 20, 20);
addButtons(layout);
setContentView(layout);
}
/**
* 添加按钮
*/
private void addButtons(LinearLayout layout) {
// Build 按钮
Button btnBuild = createButton("Build", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 Build");
if (mClickListener != null) {
mClickListener.onBuildClick();
}
}
});
layout.addView(btnBuild);
// View 按钮
Button btnView = createButton("View", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 View");
if (mClickListener != null) {
mClickListener.onViewClick();
}
}
});
layout.addView(btnView);
// 取消按钮
Button btnCancel = createButton("Cancel", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 Cancel");
dismiss();
}
});
layout.addView(btnCancel);
}
/**
* 创建按钮
*/
private Button createButton(String text, View.OnClickListener listener) {
Button button = new Button(getContext());
button.setText(text);
button.setPadding(10, 10, 10, 10);
button.setOnClickListener(listener);
return button;
}
/**
* 回调接口
*/
public interface OnButtonClickListener {
void onBuildClick();
void onViewClick();
void onCancelClick();
}
}
@@ -1,202 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.Binder;
import android.os.IBinder;
import cc.winboll.studio.autonfc.MainActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.nio.charset.Charset;
import java.util.Arrays;
public class AutoNFCService extends Service {
public static final String TAG = "AutoNFCService";
// ================= 已修改:更新为 Beta 包名 =================
public static final String ACTION_BUILD = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD";
public static final String ACTION_BUILD_VIEW = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD_VIEW";
private final IBinder mBinder = new LocalBinder();
private String mNfcData;
private MainActivity mActivity; // 持有 Activity 引用,用于回调
// ========================= 生命周期 =========================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate() -> 服务创建");
// 移除:startForeground(NOTIFICATION_ID, buildNotification());
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy() -> 服务已停止");
mActivity = null; // 释放引用
}
// ========================= 服务绑定 =========================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind() -> 服务被绑定");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
LogUtils.d(TAG, "onUnbind() -> 服务解绑");
// 移除:stopForeground(true);
stopSelf();
return super.onUnbind(intent);
}
// ========================= 对外暴露方法 =========================
/**
* 绑定 Activity,用于回调显示对话框
*/
public void attachActivity(MainActivity activity) {
this.mActivity = activity;
}
/**
* 处理 NFC 意图
*/
public void handleNfcIntent(Intent intent) {
LogUtils.d(TAG, "handleNfcIntent() -> 开始处理");
if (intent == null) {
LogUtils.e(TAG, "handleNfcIntent() -> 参数 intent 为空");
return;
}
String action = intent.getAction();
LogUtils.d(TAG, "handleNfcIntent() -> Action = " + action);
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
LogUtils.d(TAG, "handleNfcIntent() -> 匹配 NFC 动作");
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag == null) {
LogUtils.e(TAG, "handleNfcIntent() -> Tag 为空");
return;
}
LogUtils.d(TAG, "handleNfcIntent() -> Tag ID = " + bytesToHexString(tag.getId()));
LogUtils.d(TAG, "handleNfcIntent() -> Tech List = " + Arrays.toString(tag.getTechList()));
parseNdefData(tag);
}
}
// ========================= 内部业务 =========================
private void parseNdefData(Tag tag) {
LogUtils.d(TAG, "parseNdefData() -> 开始解析");
if (tag == null) return;
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
LogUtils.e(TAG, "parseNdefData() -> 不支持 NDEF 格式");
return;
}
try {
ndef.connect();
NdefMessage msg = ndef.getNdefMessage();
if (msg == null || msg.getRecords() == null || msg.getRecords().length == 0) {
LogUtils.w(TAG, "parseNdefData() -> 卡片无数据");
return;
}
NdefRecord record = msg.getRecords()[0];
byte[] payload = record.getPayload();
int langLen = payload[0] & 0x3F;
int start = 1 + langLen;
if (start < payload.length) {
mNfcData = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
LogUtils.d(TAG, "parseNdefData() -> 读卡成功: " + mNfcData);
// 关键:回调给 Activity 弹框,此时 Activity 一定是存活状态
if (mActivity != null) {
mActivity.showNfcActionDialog(mNfcData);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "parseNdefData() -> 读取失败", e);
} finally {
try {
ndef.close();
} catch (Exception e) {
// 忽略关闭异常
}
}
}
/**
* 执行 Termux 命令
*/
public void executeTermuxCommand(String action, String nfcData) {
LogUtils.d(TAG, "executeTermuxCommand() -> 开始执行");
if (nfcData == null || nfcData.isEmpty()) {
ToastUtils.show("数据错误");
return;
}
try {
LogUtils.d(TAG, "executeTermuxCommand() -> 发送指令: " + nfcData);
Intent bridgeIntent = new Intent(action);
// ================= 已修改:使用 Beta 包名 =================
bridgeIntent.setClassName(
"cc.winboll.studio.winboll.beta",
"cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity"
);
bridgeIntent.putExtra(Intent.EXTRA_TEXT, nfcData);
bridgeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(bridgeIntent);
ToastUtils.show("指令已发送");
} catch (Exception e) {
LogUtils.e(TAG, "executeTermuxCommand() -> 发送失败", e);
ToastUtils.show("发送失败");
}
}
// ========================= 工具方法 =========================
private String bytesToHexString(byte[] bytes) {
if (bytes == null || bytes.length == 0) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
// ========================= Binder =========================
public class LocalBinder extends Binder {
public AutoNFCService getService() {
return AutoNFCService.this;
}
}
}
@@ -1,230 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.autonfc.R;
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class NFCInterfaceActivity extends Activity {
public static final String TAG = "NFCInterfaceActivity";
private EditText et_script;
private EditText et_args;
private EditText et_workDir;
private EditText et_background;
private EditText et_resultDir;
private TextView tvResult;
private TextView tvStatus;
private NfcAdapter mNfcAdapter;
private PendingIntent mNfcPendingIntent;
private Tag mCurrentTag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nfc_interface);
initView();
initNfc();
}
private void initView() {
et_script = findViewById(R.id.et_script);
et_args = findViewById(R.id.et_args);
et_workDir = findViewById(R.id.et_workDir);
et_background = findViewById(R.id.et_background);
et_resultDir = findViewById(R.id.et_resultDir);
tvResult = findViewById(R.id.tv_result);
tvStatus = findViewById(R.id.tv_status);
}
private void initNfc() {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter == null) {
tvStatus.setText("设备不支持NFC");
return;
}
if (!mNfcAdapter.isEnabled()) {
tvStatus.setText("请开启NFC");
return;
}
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mNfcPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, PendingIntent.FLAG_UPDATE_CURRENT);
tvStatus.setText("NFC已启动,等待卡片靠近");
}
@Override
protected void onResume() {
super.onResume();
if (mNfcAdapter != null && mNfcAdapter.isEnabled()) {
mNfcAdapter.enableForegroundDispatch(this, mNfcPendingIntent, null, null);
}
}
@Override
protected void onPause() {
super.onPause();
if (mNfcAdapter != null) {
mNfcAdapter.disableForegroundDispatch(this);
}
mCurrentTag = null;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
mCurrentTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (mCurrentTag == null) return;
tvStatus.setText("卡片已连接,解析中...");
readNfc();
}
// -------------------------------------------------------------------------
// 读取 NFC(完全委托给工具类)
// -------------------------------------------------------------------------
private void readNfc() {
try {
NfcTermuxCmd cmd = NfcUtils.readTag(mCurrentTag);
if (cmd == null) {
tvStatus.setText("读取成功:标签为空");
tvResult.setText("");
// 清空窗体
clearUiFields();
return;
}
// 核心改动:读取成功后,同时更新详情显示 和 窗体输入框
updateUiWithCmd(cmd);
} catch (Exception e) {
LogUtils.e(TAG, "readNfc 失败", e);
tvStatus.setText("读取失败:" + e.getMessage());
// 出错时清空窗体
clearUiFields();
}
}
// -------------------------------------------------------------------------
// 新增:根据读取到的 Cmd 填充 UI(详情 + 窗体)
// -------------------------------------------------------------------------
private void updateUiWithCmd(NfcTermuxCmd cmd) {
if (cmd == null) return;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date());
String show = "【读取时间】 " + time + "\n\n"
+ "【解析结果】\n"
+ "script: " + cmd.getScript() + "\n"
+ "args: " + (cmd.getArgs() != null ? String.join(", ", cmd.getArgs()) : "[]") + "\n"
+ "workDir: " + cmd.getWorkDir() + "\n"
+ "background: " + cmd.isBackground() + "\n"
+ "resultDir: " + cmd.getResultDir();
tvResult.setText(show);
tvStatus.setText("读取成功!");
// 👇 关键逻辑:自动填入窗体(每次读取后都会覆盖输入框)
et_script.setText(cmd.getScript() != null ? cmd.getScript() : "");
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
et_background.setText(String.valueOf(cmd.isBackground()));
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
}
// -------------------------------------------------------------------------
// 辅助:清空所有输入框
// -------------------------------------------------------------------------
private void clearUiFields() {
et_script.setText("");
et_args.setText("");
et_workDir.setText("");
et_background.setText("");
et_resultDir.setText("");
}
// -------------------------------------------------------------------------
// 写入按钮(委托给工具类)
// -------------------------------------------------------------------------
public void onWriteClick(View view) {
if (mCurrentTag == null) {
showToast("请先靠近卡片");
return;
}
try {
NfcTermuxCmd cmd = buildCmdFromUI();
NfcUtils.writeTag(mCurrentTag, cmd);
tvStatus.setText("写入成功!");
showToast("写入成功");
readNfc(); // 写入后重读,此时会自动填入窗体
} catch (Exception e) {
LogUtils.e(TAG, "写入失败", e);
tvStatus.setText("写入失败:" + e.getMessage());
showToast("写入失败");
}
}
// -------------------------------------------------------------------------
// 填充调试数据
// -------------------------------------------------------------------------
public void onFillTestDataClick(View view) {
String testJson = "{\"script\":\"BuildWinBoLLProject.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}";
try {
NfcTermuxCmd cmd = NfcUtils.jsonToCmd(testJson);
et_script.setText(cmd.getScript());
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
et_background.setText(String.valueOf(cmd.isBackground()));
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
showToast("调试数据已填入");
} catch (Exception e) {
showToast("解析失败");
}
}
// -------------------------------------------------------------------------
// 从 UI 构建 NfcTermuxCmd
// -------------------------------------------------------------------------
private NfcTermuxCmd buildCmdFromUI() {
String script = et_script.getText().toString().trim();
String argsStr = et_args.getText().toString().trim();
String workDir = et_workDir.getText().toString().trim();
String bgStr = et_background.getText().toString().trim();
String resultDir = et_resultDir.getText().toString().trim();
NfcTermuxCmd cmd = new NfcTermuxCmd();
cmd.setScript(script);
cmd.setArgs(argsStr.isEmpty() ? new String[0] : argsStr.split(","));
cmd.setWorkDir(workDir.isEmpty() ? null : workDir);
cmd.setBackground("true".equalsIgnoreCase(bgStr));
cmd.setResultDir(resultDir.isEmpty() ? null : resultDir);
return cmd;
}
private void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
}
@@ -1,78 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import java.util.HashMap;
import java.util.Map;
public class NfcStateMonitor {
private static Map<String, OnNfcStateListener> sListenerMap = new HashMap<>();
private static boolean sIsRunning = false;
public static void startMonitor() {
if (sIsRunning) return;
sListenerMap = new HashMap<>();
sIsRunning = true;
}
public static void stopMonitor() {
if (!sIsRunning) return;
sIsRunning = false;
if (sListenerMap != null) {
sListenerMap.clear();
sListenerMap = null;
}
}
// 你原来的方法名:registerListener
public static void registerListener(String key, OnNfcStateListener listener) {
if (!sIsRunning || listener == null) return;
sListenerMap.put(key, listener);
}
public static void unregisterListener(String key) {
if (!sIsRunning || key == null) return;
sListenerMap.remove(key);
}
public static void notifyNfcConnected() {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcConnected();
}
}
public static void notifyNfcDisconnected() {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcDisconnected();
}
}
public static void notifyReadSuccess(String data) {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcReadSuccess(data);
}
}
public static void notifyReadFail(String error) {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcReadFail(error);
}
}
public static void notifyWriteSuccess() {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcWriteSuccess();
}
}
public static void notifyWriteFail(String error) {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcWriteFail(error);
}
}
}
@@ -1,136 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/03/16 14:26
*/
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
import cc.winboll.studio.libappbase.LogUtils;
import java.nio.charset.Charset;
import java.util.Locale;
public class NfcUtils {
public static final String TAG = "NfcUtils";
private static Gson sGson = new Gson();
// -------------------------------------------------------------------------
// 读取 NFC 标签并解析为 NfcTermuxCmd
// -------------------------------------------------------------------------
public static NfcTermuxCmd readTag(Tag tag) throws Exception {
if (tag == null) {
LogUtils.e(TAG, "readTag: tag is null");
return null;
}
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
LogUtils.e(TAG, "readTag: 不支持 NDEF");
return null;
}
try {
ndef.connect();
NdefMessage msg = ndef.getNdefMessage();
if (msg == null) return null;
NdefRecord[] records = msg.getRecords();
if (records == null || records.length == 0) return null;
byte[] payload = records[0].getPayload();
int status = payload[0] & 0xFF;
int langLen = status & 0x3F;
int start = 1 + langLen;
if (start >= payload.length) return null;
String json = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
LogUtils.d(TAG, "readTag: 提取 JSON -> " + json);
return sGson.fromJson(json, NfcTermuxCmd.class);
} finally {
if (ndef != null && ndef.isConnected()) {
ndef.close();
}
}
}
// -------------------------------------------------------------------------
// 写入 NfcTermuxCmd 到 NFC 标签
// -------------------------------------------------------------------------
public static void writeTag(Tag tag, NfcTermuxCmd cmd) throws Exception {
if (tag == null) throw new Exception("tag is null");
String json = sGson.toJson(cmd);
writeJson(tag, json);
}
// -------------------------------------------------------------------------
// 写入原始 JSON 字符串到 NFC
// -------------------------------------------------------------------------
public static void writeJson(Tag tag, String json) throws Exception {
if (tag == null) throw new Exception("tag is null");
Ndef ndef = Ndef.get(tag);
if (ndef == null) throw new Exception("标签不支持 NDEF");
try {
ndef.connect();
int maxSize = ndef.getMaxSize();
int realSize = json.getBytes(Charset.forName("UTF-8")).length;
if (realSize > maxSize) {
throw new Exception("数据过大 (" + realSize + ") > 容量 (" + maxSize + ")");
}
NdefRecord record = createTextRecord(json, true);
NdefMessage msg = new NdefMessage(new NdefRecord[]{record});
ndef.writeNdefMessage(msg);
LogUtils.d(TAG, "writeJson: 写入成功");
} finally {
if (ndef != null && ndef.isConnected()) {
ndef.close();
}
}
}
// -------------------------------------------------------------------------
// 创建 NFC 文本记录
// -------------------------------------------------------------------------
public static NdefRecord createTextRecord(String text, boolean isUtf8) {
byte[] langBytes = "en".getBytes(Charset.forName("US-ASCII"));
byte[] textBytes = text.getBytes(Charset.forName(isUtf8 ? "UTF-8" : "UTF-16"));
int status = isUtf8 ? 0 : 0x80;
status |= langBytes.length & 0x3F;
byte[] data = new byte[1 + langBytes.length + textBytes.length];
data[0] = (byte) status;
System.arraycopy(langBytes, 0, data, 1, langBytes.length);
System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);
return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
}
// -------------------------------------------------------------------------
// 辅助:JSON -> NfcTermuxCmd
// -------------------------------------------------------------------------
public static NfcTermuxCmd jsonToCmd(String json) throws JsonSyntaxException {
return sGson.fromJson(json, NfcTermuxCmd.class);
}
// -------------------------------------------------------------------------
// 辅助:NfcTermuxCmd -> JSON
// -------------------------------------------------------------------------
public static String cmdToJson(NfcTermuxCmd cmd) {
return sGson.toJson(cmd);
}
}
@@ -1,11 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
public interface OnNfcStateListener {
void onNfcConnected(); // 无参数!
void onNfcDisconnected();
void onNfcReadSuccess(String data);
void onNfcReadFail(String error);
void onNfcWriteSuccess();
void onNfcWriteFail(String error);
}
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="脚本名称 script:"/>
<EditText
android:id="@+id/et_script"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="如 auth.sh"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="参数 args (逗号分隔):"/>
<EditText
android:id="@+id/et_args"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="user1,pass123"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="工作目录 workDir:"/>
<EditText
android:id="@+id/et_workDir"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="/data/data/com.termux/files/home"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="后台执行 background (true/false):"/>
<EditText
android:id="@+id/et_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="结果目录 resultDir:"/>
<EditText
android:id="@+id/et_resultDir"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="/data/data/com.termux/files/log"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:onClick="onFillTestDataClick"
android:text="填入调试数据 (BuildWinBoLLProject.sh)"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:onClick="onWriteClick"
android:text="写入 NFC (NfcTermuxCmd JSON)"/>
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="状态"/>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:scrollbars="vertical"
android:textSize="14sp"/>
</LinearLayout>
</ScrollView>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_log"
android:title="启动日志"/>
</menu>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#FF9800</color>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<resources>
<string name="app_name">AutoNFC</string>
</resources>
-11
View File
@@ -1,11 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
+9 -10
View File
@@ -1,9 +1,11 @@
# Contacts
源码参考自:
https://github.com/aJIEw/PhoneCallApp.git
# [ByTheWay]
#【顺道说】
#### 介绍
这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。
The [ByTheWay] project is designed to quickly schedule task reminders related to time and location.
Its main feature is the ability to quickly create task-related data.
【顺道说】项目应用应用于快速安排一个以时间和位置有关的任务提醒事项。
其主要的特点是能快速建立事项任务数据。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
@@ -12,19 +14,16 @@ https://github.com/aJIEw/PhoneCallApp.git
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 gradle assembleStageRelease
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh bytheway
#### 使用说明
在安卓系统中需要设置两个权限允许。
1.自启动权限允许。
2.省电策略-无限制权限允许。
在安卓操作系统里使用。
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request
+17 -26
View File
@@ -23,34 +23,25 @@ android {
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.autonfc"
minSdkVersion 23
// MIUI12
applicationId "cc.winboll.studio.bytheway"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
//1. Android
//-  versionCode  int
//- Java int 2147483647
versionCode 1520000
// versionName
// .winboll/winbollBuildProps.properties stageCount=0
// build.gradle stageCount=0
// Gradle编译环境下合起来的 versionName "${versionName}.0"
versionName "15.11"
versionName "15.20"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // SO库放在libs目录下
}
}
}
dependencies {
api 'com.google.code.gson:gson:2.10.1'
//
@@ -67,8 +58,6 @@ dependencies {
api 'io.github.medyo:android-about-page:2.0.0'
//
api 'com.squareup.okhttp3:okhttp:4.4.1'
// OkHttp网络请求
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
@@ -82,11 +71,10 @@ dependencies {
//
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//使sdk
//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.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'
@@ -106,14 +94,17 @@ dependencies {
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:libaes:15.15.2'
api 'cc.winboll.studio:libappbase:15.15.11'
api 'cc.winboll.studio:libappbase:15.20.34'
api 'cc.winboll.studio:libaes:15.20.17'
// WinBoLL备用库 jitpack.io
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
// 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
@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Jul 10 13:02:55 HKT 2026
stageCount=4
libraryProject=
baseVersion=15.20
publishVersion=15.20.3
buildCount=0
baseBetaVersion=15.20.4
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DebugTemp☆</string>
<string name="app_name">顺道说☯</string>string>
</resources>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AutoNFC✌</string>
<string name="app_name">ByTheWay +</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.gpsrelaysentinel">
package="cc.winboll.studio.bytheway">
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -42,20 +42,18 @@
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
<activity
android:name=".activities.GpsListActivity"
android:exported="true"/>
<service
android:name=".MainService"
android:enabled="true"
android:name=".services.GpsReceiveService"
android:launchMode="singleInstance"
android:exported="false"/>
<service android:name=".GpsReceiverChildService1"/>
<activity android:name="cc.winboll.studio.bytheway.activities.SettingsActivity"/>
<service android:name=".GpsReceiverChildService2"/>
<service android:name=".GpsReceiverChildService3"/>
<activity android:name=".AboutActivity"/>
<activity android:name="cc.winboll.studio.bytheway.activities.AboutActivity"/>
</application>
@@ -1,4 +1,4 @@
package cc.winboll.studio.autonfc;
package cc.winboll.studio.bytheway;
import android.app.Activity;
import android.content.ClipData;
@@ -14,7 +14,6 @@ import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
@@ -22,8 +21,11 @@ import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
@@ -33,6 +35,8 @@ 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;
@@ -47,17 +51,28 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
// 初始化 Toast 框架
// ToastUtils.init(this);
// // 设置 Toast 布局样式
// //ToastUtils.setView(R.layout.view_toast);
// ToastUtils.setStyle(new WhiteToastStyle());
// ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
//
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
try {
super.onCreate();
WinBoLLActivityManager.init(this);
// 初始化主题样式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 {
@@ -0,0 +1,79 @@
package cc.winboll.studio.bytheway;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.bytheway.activities.AboutActivity;
import cc.winboll.studio.bytheway.activities.BaseWinBoLLActivity;
import cc.winboll.studio.bytheway.activities.GpsListActivity;
import cc.winboll.studio.bytheway.activities.SettingsActivity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
public class MainActivity extends BaseWinBoLLActivity {
public static final String TAG = "MainActivity";
@Override
public String getTag() {
return TAG;
}
@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);
ToastUtils.show("onCreate");
}
@Override
protected void onResume() {
super.onResume();
}
@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);
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) {
recreate();
}
if (item.getItemId() == R.id.action_gps_list) {
startActivity(new Intent(this, GpsListActivity.class));
return true;
}
if (item.getItemId() == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
if (item.getItemId() == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
}
if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,33 +1,31 @@
package cc.winboll.studio.positions.activities;
package cc.winboll.studio.bytheway.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.bytheway.MainActivity;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
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
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/07 00:17
* @Describe 应用介绍窗口
*/
public class AboutActivity extends WinBoLLActivity {
public class AboutActivity extends BaseWinBoLLActivity {
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);
@@ -39,7 +37,7 @@ public class AboutActivity extends WinBoLLActivity {
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
@@ -49,13 +47,13 @@ public class AboutActivity extends WinBoLLActivity {
}
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();
WinBoLLActivityManager.getInstance().resumeActivity(MainActivity.class);
WinBoLLActivityManager.getInstance().finish(AboutActivity.this);
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
@@ -63,18 +61,18 @@ public class AboutActivity extends WinBoLLActivity {
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "positions";
String branchName = "bytheway";
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.setAppGitName("MyWinBoLL");
appInfo.setAppGitOwner("ZhanGSKen");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Positions");
appInfo.setAppAPKName("Positions");
appInfo.setAppAPKFolderName("Positions");
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=ByTheWay");
appInfo.setAppAPKName("ByTheWay");
appInfo.setAppAPKFolderName("ByTheWay");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
@@ -0,0 +1,46 @@
package cc.winboll.studio.bytheway.activities;
import android.app.Activity;
import android.os.Bundle;
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;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/07 00:21
* @Describe BaseWinBollActivity 【继承AppCompatActivity,保留核心能力,不额外暴露方法】
* 继承链路:BaseWinBoLLActivity → AppCompatActivity → FragmentActivityAppCompat能力天然继承可用
*/
public abstract class BaseWinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "BaseWinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;
@Override
protected void onCreate(Bundle savedInstanceState) {
//AESThemeUtil.applyAppCompatTheme(this);
mThemeType = AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
super.onCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
WinBoLLActivityManager.getInstance().registeRemove(this);
super.onDestroy();
}
// 子类必须实现getTag(),确保唯一标识
@Override
public abstract String getTag();
@Override
public Activity getActivity() {
return this;
}
}
@@ -0,0 +1,76 @@
package cc.winboll.studio.bytheway.activities;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.bytheway.adapters.GpsAdapter;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.bytheway.utils.GpsUtil;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.List;
/**
* GPS定位数据列表展示页面
* 支持下拉刷新SQLite本地数据库定位轨迹信息
* @author 豆包&ZhanGSKen<zhangsken@qq.com>
* 创建时间:2026-07-02 14:49:00
* 最后编辑时间:2026-07-02 16:32:13
*/
public class GpsListActivity extends AppCompatActivity {
private SwipeRefreshLayout swipeRefresh;
private RecyclerView rvGps;
private GpsAdapter adapter;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gps_list);
LogUtils.i("GpsListActivity", "页面初始化");
initView();
initData();
setSwipeRefreshListener();
}
private void initView() {
swipeRefresh = findViewById(R.id.swipe_refresh_layout);
rvGps = findViewById(R.id.rv_gps_list);
rvGps.setLayoutManager(new LinearLayoutManager(this));
LogUtils.i("GpsListActivity", "控件初始化完成");
}
private void initData() {
LogUtils.i("GpsListActivity", "开始加载GPS数据库数据");
refreshGpsData();
}
private void setSwipeRefreshListener() {
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshGpsData();
}
});
}
private void refreshGpsData() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
final List<GpsBean> gpsDataList = GpsUtil.queryAllGps(GpsListActivity.this);
LogUtils.i("GpsListActivity", "查询GPS数据条数:" + gpsDataList.size());
adapter = new GpsAdapter(GpsListActivity.this, gpsDataList);
rvGps.setAdapter(adapter);
swipeRefresh.setRefreshing(false);
}
}, 500);
}
}
@@ -0,0 +1,33 @@
package cc.winboll.studio.bytheway.activities;
import android.app.Activity;
import android.os.Bundle;
import android.widget.CompoundButton;
import android.widget.Switch;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.bytheway.services.GpsReceiveService;
public class SettingsActivity extends Activity {
public static final String TAG = "SettingsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Switch swGpsReceiveService = findViewById(R.id.sw_gps_receive_service);
swGpsReceiveService.setChecked(GpsReceiveService.isServiceRunning());
swGpsReceiveService.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
GpsReceiveService.startGpsReceiveService(SettingsActivity.this);
} else {
GpsReceiveService.stopGpsReceiveService(SettingsActivity.this);
}
}
});
}
}
@@ -0,0 +1,64 @@
package cc.winboll.studio.bytheway.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.bytheway.utils.GpsUtil;
import java.util.List;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/02 15:10
*/
public class GpsAdapter extends RecyclerView.Adapter<GpsAdapter.GpsHolder> {
private final Context context;
private final List<GpsBean> gpsList;
public GpsAdapter(Context context, List<GpsBean> gpsList) {
this.context = context;
this.gpsList = gpsList;
}
@NonNull
@Override
public GpsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_gps, parent, false);
return new GpsHolder(view);
}
@Override
public void onBindViewHolder(@NonNull GpsHolder holder, int position) {
GpsBean bean = gpsList.get(position);
String time = GpsUtil.formatTime(bean.getReceiveTime());
holder.tvTime.setText("接收时间:" + time);
holder.tvLatLng.setText(GpsUtil.formatLatLng(bean.getLatitude(), bean.getLongitude()));
holder.tvOther.setText(GpsUtil.formatOther(bean.getAccuracy(), bean.getAltitude(), bean.getSpeed()));
}
@Override
public int getItemCount() {
return gpsList.size();
}
public static class GpsHolder extends RecyclerView.ViewHolder {
TextView tvTime, tvLatLng, tvOther;
public GpsHolder(@NonNull View itemView) {
super(itemView);
tvTime = itemView.findViewById(R.id.tv_time);
tvLatLng = itemView.findViewById(R.id.tv_lat_lng);
tvOther = itemView.findViewById(R.id.tv_other);
}
}
}
@@ -0,0 +1,44 @@
package cc.winboll.studio.bytheway.models;
import java.util.Date;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/02 14:56
*/
public class GpsBean {
private int id;
private double latitude; //纬度
private double longitude; //经度
private float accuracy; //精度
private double altitude; //海拔
private float speed; //速度
private long receiveTime; //接收时间戳
public GpsBean() {}
public GpsBean(double latitude, double longitude, float accuracy, double altitude, float speed, long receiveTime) {
this.latitude = latitude;
this.longitude = longitude;
this.accuracy = accuracy;
this.altitude = altitude;
this.speed = speed;
this.receiveTime = receiveTime;
}
//get set 方法
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public float getAccuracy() { return accuracy; }
public void setAccuracy(float accuracy) { this.accuracy = accuracy; }
public double getAltitude() { return altitude; }
public void setAltitude(double altitude) { this.altitude = altitude; }
public float getSpeed() { return speed; }
public void setSpeed(float speed) { this.speed = speed; }
public long getReceiveTime() { return receiveTime; }
public void setReceiveTime(long receiveTime) { this.receiveTime = receiveTime; }
}
@@ -0,0 +1,224 @@
package cc.winboll.studio.bytheway.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.bytheway.utils.GpsUtil;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/02 15:33
* @Describe GPS定位接收服务,通过LocationManager获取GPS数据并持久化存入SQLite数据库。单例独占模式,自带重复启动拦截,对外静态统一启停接口
*/
public class GpsReceiveService extends Service {
public static final String TAG = "GpsReceiveService";
private static final int NOTIFICATION_ID = 1001;
private static final String CHANNEL_ID = "gps_receive_service_channel";
private static boolean serviceRunning = false;
private static GpsReceiveService instance;
private int gpsReceiveCount = 0;
private GpsBean currentGpsBean;
private LocationManager locationManager;
private LocationListener locationListener;
/**
* 获取GPS定位服务运行状态
* @return true=服务正在运行, false=服务未运行
*/
public static boolean isServiceRunning() {
return serviceRunning;
}
/**
* 获取当前最新的GPS定位数据
* @return 当前GPS数据,未收到定位时返回null
*/
public static GpsBean getCurrentGpsBean() {
return serviceRunning ? instance.currentGpsBean : null;
}
/**
* 外部统一静态启动服务
*/
public static void startGpsReceiveService(final Context context) {
if (serviceRunning) {
LogUtils.i("GpsReceiveService", "服务已运行,无需重复启动");
return;
}
final Intent intent = new Intent(context, GpsReceiveService.class);
context.startService(intent);
LogUtils.i("GpsReceiveService", "GPS定位消息接收服务启动请求发送");
}
/**
* 外部统一静态关闭服务
*/
public static void stopGpsReceiveService(final Context context) {
if (!serviceRunning) {
LogUtils.i("GpsReceiveService", "服务未运行,无需关闭");
return;
}
final Intent intent = new Intent(context, GpsReceiveService.class);
context.stopService(intent);
LogUtils.i("GpsReceiveService", "GPS定位消息接收服务停止请求发送");
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
serviceRunning = true;
gpsReceiveCount = 0;
currentGpsBean = null;
startGpsLocationUpdates();
startForeground(NOTIFICATION_ID, createNotification());
LogUtils.i("GpsReceiveService", "GPS定位消息接收服务创建完成");
Toast.makeText(this, "GPS定位消息接收服务已开启", Toast.LENGTH_SHORT).show();
}
private Notification createNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"GPS定位消息接收服务",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
}
String contentText;
if (currentGpsBean != null) {
contentText = "已接收 " + gpsReceiveCount + " 次 | "
+ "纬度:" + currentGpsBean.getLatitude()
+ " 经度:" + currentGpsBean.getLongitude()
+ " 精度:" + currentGpsBean.getAccuracy() + "m";
} else {
contentText = "等待GPS定位数据...";
}
return new Notification.Builder(this, CHANNEL_ID)
.setContentTitle("ByTheWay")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
.setOngoing(true)
.build();
}
/**
* 更新前台服务通知栏显示
*/
private void updateNotification() {
final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(NOTIFICATION_ID, createNotification());
}
/**
* 启动GPS定位更新监听
*/
private void startGpsLocationUpdates() {
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
locationListener = new LocationListener() {
@Override
public void onLocationChanged(final Location location) {
handleGpsLocation(location);
}
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
LogUtils.d("GpsReceiveService", "GPS状态变化: " + provider + ", status=" + status);
}
@Override
public void onProviderEnabled(final String provider) {
LogUtils.d("GpsReceiveService", "GPS提供者已启用: " + provider);
}
@Override
public void onProviderDisabled(final String provider) {
LogUtils.d("GpsReceiveService", "GPS提供者已禁用: " + provider);
}
};
try {
// 最小时间间隔1秒,最小距离间隔0米,确保每次位置更新都能接收
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
5000L,
0f,
locationListener
);
LogUtils.i("GpsReceiveService", "GPS定位监听已启动");
} catch (SecurityException e) {
LogUtils.e("GpsReceiveService", "缺少GPS定位权限: " + e.getMessage());
} catch (Exception e) {
LogUtils.e("GpsReceiveService", "启动GPS定位监听失败: " + e.getMessage());
}
}
/**
* 处理GPS定位数据,写入SQLite数据库并更新通知栏
*/
private void handleGpsLocation(final Location location) {
final GpsBean bean = new GpsBean();
bean.setLatitude(location.getLatitude());
bean.setLongitude(location.getLongitude());
bean.setAccuracy(location.getAccuracy());
bean.setAltitude(location.getAltitude());
bean.setSpeed(location.getSpeed());
bean.setReceiveTime(System.currentTimeMillis());
currentGpsBean = bean;
GpsUtil.insertGps(getApplicationContext(), bean);
gpsReceiveCount++;
updateNotification();
LogUtils.d("GpsReceiveService", "收到GPS定位,已保存数据,累计接收 " + gpsReceiveCount + "");
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
//单例不重启模式,被杀不自动重启
return START_NOT_STICKY;
}
@Override
public IBinder onBind(final Intent intent) {
return null;
}
@Override
public void onDestroy() {
stopGpsLocationUpdates();
stopForeground(true);
serviceRunning = false;
instance = null;
LogUtils.i("GpsReceiveService", "GPS接收服务已销毁,定位监听已停止");
Toast.makeText(this, "GPS定位服务已关闭", Toast.LENGTH_SHORT).show();
super.onDestroy();
}
/**
* 停止GPS定位更新监听
*/
private void stopGpsLocationUpdates() {
if (locationManager != null && locationListener != null) {
try {
locationManager.removeUpdates(locationListener);
LogUtils.i("GpsReceiveService", "GPS定位监听已移除");
} catch (SecurityException e) {
LogUtils.e("GpsReceiveService", "移除GPS监听异常: " + e.getMessage());
}
}
}
}
@@ -0,0 +1,247 @@
package cc.winboll.studio.bytheway.utils;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* GPS数据处理模块
* 集合GPS数据解析、格式化、数据库存取等所有操作,外部统一通过本类访问
* @author ZhanGSKen<zhangsken@qq.com>
*/
public class GpsUtil {
private static final String TAG = "GpsUtil";
// Intent广播中的GPS数据键名
public static final String KEY_LATITUDE = "latitude";
public static final String KEY_LONGITUDE = "longitude";
public static final String KEY_ACCURACY = "accuracy";
public static final String KEY_ALTITUDE = "altitude";
public static final String KEY_SPEED = "speed";
public static final String KEY_RECEIVE_TIME = "receive_time";
// 时间格式
private static final String FORMAT_TIME = "yyyy-MM-dd HH:mm:ss";
// ======================== 数据解析方法 ========================
/**
* 从Intent广播数据中解析GPS定位信息,构造GpsBean对象
* @param intent GPS定位广播Intent
* @return 解析后的GpsBean对象
*/
public static GpsBean parseFromIntent(final Intent intent) {
final double latitude = intent.getDoubleExtra(KEY_LATITUDE, 0);
final double longitude = intent.getDoubleExtra(KEY_LONGITUDE, 0);
final float accuracy = intent.getFloatExtra(KEY_ACCURACY, 0);
final double altitude = intent.getDoubleExtra(KEY_ALTITUDE, 0);
final float speed = intent.getFloatExtra(KEY_SPEED, 0);
final long time = intent.getLongExtra(KEY_RECEIVE_TIME, System.currentTimeMillis());
final GpsBean bean = new GpsBean();
bean.setLatitude(latitude);
bean.setLongitude(longitude);
bean.setAccuracy(accuracy);
bean.setAltitude(altitude);
bean.setSpeed(speed);
bean.setReceiveTime(time);
return bean;
}
/**
* 从数据库游标中解析GPS定位信息,构造GpsBean对象
* 游标字段顺序:id, latitude, longitude, accuracy, altitude, speed, receive_time
* @param cursor 数据库查询游标(需已定位到目标行)
* @return 解析后的GpsBean对象
*/
public static GpsBean parseFromCursor(final Cursor cursor) {
final GpsBean bean = new GpsBean();
bean.setId(cursor.getInt(0));
bean.setLatitude(cursor.getDouble(1));
bean.setLongitude(cursor.getDouble(2));
bean.setAccuracy(cursor.getFloat(3));
bean.setAltitude(cursor.getDouble(4));
bean.setSpeed(cursor.getFloat(5));
bean.setReceiveTime(cursor.getLong(6));
return bean;
}
// ======================== 格式化方法 ========================
/**
* 将时间戳格式化为可读时间字符串
* @param timestamp 毫秒时间戳
* @return 格式化后的时间字符串
*/
public static String formatTime(final long timestamp) {
final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT_TIME, Locale.getDefault());
return sdf.format(new Date(timestamp));
}
/**
* 格式化经纬度显示文本
* @param latitude 纬度
* @param longitude 经度
* @return 格式化后的经纬度字符串
*/
public static String formatLatLng(final double latitude, final double longitude) {
return "纬度:" + latitude + " 经度:" + longitude;
}
/**
* 格式化精度、海拔、速度显示文本
* @param accuracy 精度(米)
* @param altitude 海拔(米)
* @param speed 速度(m/s
* @return 格式化后的字符串
*/
public static String formatOther(final float accuracy, final double altitude, final float speed) {
return "精度:" + accuracy + "m 海拔:" + altitude + "m 速度:" + speed + "m/s";
}
/**
* 将速度从m/s转换为km/h
* @param speedMps 速度(m/s
* @return 速度(km/h
*/
public static float speedToKmh(final float speedMps) {
return speedMps * 3.6f;
}
/**
* 格式化速度显示文本(km/h)
* @param speedMps 速度(m/s
* @return 格式化后的速度字符串
*/
public static String formatSpeed(final float speedMps) {
return speedToKmh(speedMps) + "km/h";
}
// ======================== 数据库操作方法 ========================
/**
* 插入单条GPS定位数据
* @param context 上下文
* @param bean GPS数据对象
*/
public static void insertGps(final Context context, final GpsBean bean) {
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put("latitude", bean.getLatitude());
values.put("longitude", bean.getLongitude());
values.put("accuracy", bean.getAccuracy());
values.put("altitude", bean.getAltitude());
values.put("speed", bean.getSpeed());
values.put("receive_time", bean.getReceiveTime());
db.insert(GpsDbHelper.TABLE_GPS, null, values);
LogUtils.i(TAG, "GPS数据插入成功");
db.close();
}
/**
* 查询全部GPS记录,按时间倒序排列
* @param context 上下文
* @return GPS数据列表
*/
public static List<GpsBean> queryAllGps(final Context context) {
final List<GpsBean> list = new ArrayList<GpsBean>();
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getReadableDatabase();
final Cursor cursor = db.query(GpsDbHelper.TABLE_GPS, null, null, null, null, null, "receive_time DESC");
if (cursor.moveToFirst()) {
do {
list.add(parseFromCursor(cursor));
} while (cursor.moveToNext());
}
LogUtils.i(TAG, "查询全部GPS条数:" + list.size());
cursor.close();
db.close();
return list;
}
/**
* 根据时间区间查询GPS轨迹
* @param context 上下文
* @param startTime 起始时间戳
* @param endTime 结束时间戳
* @return GPS数据列表
*/
public static List<GpsBean> queryByTime(final Context context, final long startTime, final long endTime) {
final List<GpsBean> list = new ArrayList<GpsBean>();
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getReadableDatabase();
final String where = "receive_time >= ? AND receive_time <= ?";
final String[] args = {String.valueOf(startTime), String.valueOf(endTime)};
final Cursor cursor = db.query(GpsDbHelper.TABLE_GPS, null, where, args, null, null, "receive_time ASC");
if (cursor.moveToFirst()) {
do {
list.add(parseFromCursor(cursor));
} while (cursor.moveToNext());
}
LogUtils.i(TAG, "时间段查询GPS条数:" + list.size());
cursor.close();
db.close();
return list;
}
/**
* 清空所有本地GPS历史数据
* @param context 上下文
*/
public static void clearAllGps(final Context context) {
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getWritableDatabase();
db.delete(GpsDbHelper.TABLE_GPS, null, null);
LogUtils.i(TAG, "已清空全部GPS数据库数据");
db.close();
}
// ======================== 内部类:数据库管理 ========================
/**
* SQLite数据库创建、版本升级管理
*/
private static class GpsDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "GpsLocation.db";
private static final int DB_VERSION = 1;
private static final String TABLE_GPS = "gps_table";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_GPS + " ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "latitude REAL,"
+ "longitude REAL,"
+ "accuracy REAL,"
+ "altitude REAL,"
+ "speed REAL,"
+ "receive_time INTEGER"
+ ")";
public GpsDbHelper(final Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(final SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_GPS);
onCreate(db);
}
}
}
@@ -4,13 +4,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="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="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"/>
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
@@ -19,3 +18,4 @@
android:id="@+id/aboutview"/>
</LinearLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_gps_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -6,18 +6,10 @@
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
android:id="@+id/toolbar"/>
<LinearLayout
android:orientation="vertical"
@@ -26,13 +18,12 @@
android:layout_weight="1.0"
android:gravity="center_vertical|center_horizontal">
<Button
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NFC Interface Activity"
android:onClick="onNFCInterfaceActivity"/>
android:text="ByTheWay"
android:textAppearance="?android:attr/textAppearanceLarge"/>
</LinearLayout>
</LinearLayout>
@@ -1,17 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ll_notification">
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
<Switch
android:id="@+id/sw_gps_receive_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Text"
android:id="@+id/info_tv"/>
android:text="GPS定位消息接收服务"/>
</LinearLayout>
+25
View File
@@ -0,0 +1,25 @@
<?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:padding="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_time"
android:textSize="14sp"
android:textColor="#333"/>
<TextView
android:id="@+id/tv_lat_lng"
android:textSize="13sp"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tv_other"
android:textSize="12sp"
android:layout_marginTop="2dp"
android:textColor="#666"/>
</LinearLayout>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_gps_list"
android:icon="@android:drawable/ic_menu_mapmode"
android:title="@string/action_gps_list"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/action_settings"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:icon="@android:drawable/ic_menu_info_details"
android:title="@string/action_about"
app:showAsAction="never" />
</menu>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,12 @@
<?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>
@@ -0,0 +1,29 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowDarkTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowDarkTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowDarkTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
<string name="app_name">顺道说</string>
</resources>
+12
View File
@@ -0,0 +1,12 @@
<?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>
+8
View File
@@ -0,0 +1,8 @@
<resources>
<string name="app_name">ByTheWay</string>
<string name="action_settings">设置</string>
<string name="action_gps_list">GPS列表</string>
<string name="action_about">应用介绍</string>
<string name="app_description">The [ByTheWay] project is designed to quickly schedule task reminders related to time and location. Its main feature is the ability to quickly create task-related data.</string>
</resources>
+37
View File
@@ -0,0 +1,37 @@
<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>
-100
View File
@@ -1,100 +0,0 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.contacts"
minSdkVersion 23
// 适配MIUI12
targetSdkVersion 30
versionCode 2
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.14"
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://github.com/getActivity/XXPermissions
//api 'com.github.getActivity:XXPermissions:18.63'
// 下拉控件
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
// 拼音搜索
// https://mvnrepository.com/artifact/com.github.open-android/pinyin4j
api 'com.github.open-android:pinyin4j:2.5.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'
// AndroidX 类库
/*implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
implementation 'androidx.fragment:fragment:1.1.0'
implementation 'com.google.android.material:material:1.4.0'
*/
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.12.13'
api 'cc.winboll.studio:libappbase:15.14.2'
// 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
#Sat Apr 18 21:14:59 HKT 2026
stageCount=13
libraryProject=
baseVersion=15.14
publishVersion=15.14.12
buildCount=0
baseBetaVersion=15.14.13
-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
-13
View File
@@ -1,13 +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_winbollbeta">
<!-- Put flavor specific code here -->
</application>
</manifest>
-258
View File
@@ -1,258 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.contacts">
<!-- BIND_AUTOFILL_SERVICE -->
<uses-permission android:name="android.permission.BIND_AUTOFILL_SERVICE"/>
<!-- 拨打电话 -->
<uses-permission android:name="android.permission.CALL_PHONE"/>
<!-- 读取手机状态和身份 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!-- 读取电话号码 -->
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<!-- 修改系统设置 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<!-- 读取联系人 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!-- 修改您的通讯录 -->
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<!-- GET_CONTACTS -->
<uses-permission android:name="android.permission.GET_CONTACTS"/>
<!-- 此应用可显示在其他应用上方 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 更改您的音频设置 -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<!-- 读取通话记录 -->
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<!-- 新建/修改/删除通话记录 -->
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<!-- GET_CALL_LOG -->
<uses-permission android:name="android.permission.GET_CALL_LOG"/>
<!-- 录音 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 运行“dataSync”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<!-- 运行“phoneCall”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
<!-- 运行“microphone”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<!-- BIND_CALL_SCREENING_SERVICE -->
<uses-permission android:name="android.permission.BIND_CALL_SCREENING_SERVICE"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@drawable/ic_winboll"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activities.CallActivity"
android:label="CallActivity"
android:launchMode="singleTask"
android:exported="true">
</activity>
<activity
android:name=".phonecallui.PhoneCallActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DIAL"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="tel"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DIAL"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity android:name="cc.winboll.studio.contacts.activities.SettingsActivity"/>
<service
android:name=".services.MainService"
android:foregroundServiceType="dataSync"
android:exported="false"
android:stopWithTask="false"/>
<service
android:name=".services.AssistantService"
android:exported="false"
android:stopWithTask="false"/>
<service
android:name=".phonecallui.PhoneCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="false"
android:stopWithTask="false">
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true"/>
<intent-filter>
<action android:name="android.telecom.InCallService"/>
</intent-filter>
</service>
<service
android:name=".listenphonecall.CallListenerService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false">
<intent-filter android:priority="1000">
<action android:name=".service.CallShowService"/>
</intent-filter>
</service>
<service
android:name=".services.MyCallScreeningService"
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
android:exported="true"
android:stopWithTask="false">
<intent-filter>
<action android:name="android.telecom.CallScreeningService"/>
</intent-filter>
</service>
<receiver
android:name=".receivers.MainReceiver"
android:stopWithTask="false">
<intent-filter>
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
</intent-filter>
</receiver>
<receiver
android:name=".widgets.APPStatusWidget"
android:exported="true"
android:stopWithTask="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_ACTIVE"/>
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_NOACTIVE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info"/>
</receiver>
<receiver
android:name=".widgets.APPStatusWidgetClickListener"
android:stopWithTask="false">
<intent-filter>
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
</intent-filter>
</receiver>
<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.contacts.activities.UnitTestActivity"/>
<activity android:name="cc.winboll.studio.contacts.activities.AboutActivity"/>
<service android:name="cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService"/>
</application>
</manifest>
@@ -1,313 +0,0 @@
package cc.winboll.studio.contacts;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/13 06:58:04
* @Describe Activity 栈管理工具,统一管理应用内 Activity 生命周期
* 适配:Java7 + Android API29-30 + 小米机型,优化并发安全与通话场景稳定性
*/
public class ActivityStack {
// 常量定义(核心标识+版本兼容常量)
public static final String TAG = "ActivityStack";
private static final int API_VERSION_O = 26; // Android 8.0 API26isDestroyed适配用)
// 单例与核心成员变量(按优先级排序)
private static final ActivityStack INSTANCE = new ActivityStack();
// 替换为ArrayList+同步锁:解决CopyOnWriteArrayList迭代器不能删除的崩溃,兼顾并发安全
private final List<Activity> mActivityList = new ArrayList<Activity>();
private final Handler mMainHandler = new Handler(Looper.getMainLooper()); // 复用主线程Handler,避免内存泄漏
// 单例对外暴露方法
public static ActivityStack getInstance() {
return INSTANCE;
}
// 私有构造,禁止外部实例化
private ActivityStack() {
LogUtils.d(TAG, "ActivityStack 初始化完成");
}
// ====================== 栈基础操作(添加/移除) ======================
/**
* 添加Activity到栈中,避免重复入栈
* @param activity 待添加的Activity
*/
public void addActivity(Activity activity) {
if (activity == null) {
LogUtils.w(TAG, "addActivity: activity is null, skip");
return;
}
// 同步锁:解决多线程并发添加冲突(小米机型多线程场景适配)
synchronized (mActivityList) {
if (!mActivityList.contains(activity)) {
mActivityList.add(activity);
LogUtils.d(TAG, "addActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
}
}
}
/**
* 移除Activity(不销毁,用于正常退出场景)
* @param activity 待移除的Activity
*/
public void removeActivity(Activity activity) {
if (activity == null) {
LogUtils.w(TAG, "removeActivity: activity is null, skip");
return;
}
synchronized (mActivityList) {
if (mActivityList.remove(activity)) {
LogUtils.d(TAG, "removeActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
}
}
}
// ====================== Activity状态查询(获取/判断存活) ======================
/**
* 获取栈顶有效Activity(迭代遍历替代递归,避免栈溢出,适配小米多页面场景)
* @return 栈顶有效Activity,无则返回null
*/
public Activity getTopActivity() {
synchronized (mActivityList) {
if (mActivityList.isEmpty()) {
LogUtils.w(TAG, "getTopActivity: stack is empty, return null");
return null;
}
Activity validTopActivity = null;
// 倒序遍历,优先取最顶层有效Activity,同时清理无效残留
for (int i = mActivityList.size() - 1; i >= 0; i--) {
Activity activity = mActivityList.get(i);
// 版本兼容校验:API26+才支持isDestroyed
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
validTopActivity = activity;
break;
} else {
mActivityList.remove(i);
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
LogUtils.w(TAG, "getTopActivity: remove invalid activity: " + className);
}
}
if (validTopActivity != null) {
LogUtils.d(TAG, "getTopActivity: top activity: " + validTopActivity.getClass().getSimpleName());
}
return validTopActivity;
}
}
/**
* 获取指定类的有效Activity实例(通话场景核心方法,判断页面是否存活)
* @param activityClass 目标Activity类
* @return 有效实例,无则返回null
*/
public Activity getActivity(Class<?> activityClass) {
if (activityClass == null) {
LogUtils.w(TAG, "getActivity: activityClass is null, return null");
return null;
}
synchronized (mActivityList) {
if (mActivityList.isEmpty()) {
LogUtils.w(TAG, "getActivity: stack empty, return null");
return null;
}
for (Activity activity : mActivityList) {
if (activity != null && activity.getClass().equals(activityClass) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
LogUtils.d(TAG, "getActivity: find valid activity: " + activityClass.getSimpleName());
return activity;
}
}
LogUtils.w(TAG, "getActivity: no valid activity: " + activityClass.getSimpleName());
return null;
}
}
/**
* 判断指定Activity是否存活(简化通话场景调用,避免重复判空)
* @param activityClass 目标Activity类
* @return true:存活,false:未存活
*/
public boolean isActivityAlive(Class<?> activityClass) {
boolean isAlive = getActivity(activityClass) != null;
LogUtils.d(TAG, "isActivityAlive: " + activityClass.getSimpleName() + ", result: " + isAlive);
return isAlive;
}
// ====================== Activity销毁操作(单/批量/全部) ======================
/**
* 销毁栈顶Activity(主线程执行,适配小米机型线程限制)
*/
public void finishTopActivity() {
runOnMainThread(new Runnable() {
@Override
public void run() {
synchronized (mActivityList) {
if (mActivityList.isEmpty()) {
LogUtils.w(TAG, "finishTopActivity: stack is empty, skip");
return;
}
// 先移除再校验,避免并发冲突(小米多线程场景适配)
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
if (topActivity == null) {
LogUtils.w(TAG, "finishTopActivity: top activity is null, skip");
return;
}
if (!topActivity.isFinishing() && (getSdkVersion() < API_VERSION_O || !topActivity.isDestroyed())) {
topActivity.finish();
LogUtils.d(TAG, "finishTopActivity: destroy top activity: " + topActivity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
}
}
}
});
}
/**
* 销毁指定Activity(主线程执行,避免跨线程异常)
* @param activity 待销毁的Activity
*/
public void finishActivity(final Activity activity) {
runOnMainThread(new Runnable() {
@Override
public void run() {
if (activity == null) {
LogUtils.w(TAG, "finishActivity: activity is null, skip");
return;
}
synchronized (mActivityList) {
if (mActivityList.contains(activity) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
mActivityList.remove(activity);
activity.finish();
LogUtils.d(TAG, "finishActivity: destroy activity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
}
}
}
});
}
/**
* 销毁指定类的所有Activity(核心修复:迭代器删除崩溃,通话场景核心)
* @param activityClass 目标Activity类
*/
public void finishActivity(final Class<?> activityClass) {
runOnMainThread(new Runnable() {
@Override
public void run() {
if (activityClass == null) {
LogUtils.w(TAG, "finishActivity: activityClass is null, skip");
return;
}
synchronized (mActivityList) {
if (mActivityList.isEmpty()) {
LogUtils.w(TAG, "finishActivity: stack empty, skip");
return;
}
// 核心修复:用索引遍历+倒序删除,替代迭代器删除(避免UnsupportedOperationException
for (int i = mActivityList.size() - 1; i >= 0; i--) {
Activity activity = mActivityList.get(i);
if (activity != null && activity.getClass().equals(activityClass)) {
if (!activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
mActivityList.remove(i); // 索引删除,支持ArrayList
activity.finish();
LogUtils.d(TAG, "finishActivity: destroy class activity: " + activityClass.getSimpleName() + ", stack size: " + mActivityList.size());
} else {
mActivityList.remove(i); // 清理无效残留
}
}
}
}
}
});
}
/**
* 销毁栈中所有Activity(退出应用/清空栈场景用)
*/
public void finishAllActivity() {
runOnMainThread(new Runnable() {
@Override
public void run() {
synchronized (mActivityList) {
if (mActivityList.isEmpty()) {
LogUtils.w(TAG, "finishAllActivity: stack is empty, skip");
return;
}
// 遍历销毁所有有效Activity,逐个状态校验(小米机型稳定性适配)
for (Activity activity : mActivityList) {
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
activity.finish();
LogUtils.d(TAG, "finishAllActivity: destroy activity: " + activity.getClass().getSimpleName());
}
}
mActivityList.clear();
LogUtils.d(TAG, "finishAllActivity: all activity destroyed, stack cleared");
}
}
});
}
// ====================== 栈优化与工具方法 ======================
/**
* 清理栈中所有无效Activity(null/已销毁/已结束),优化小米机型内存占用
*/
public void clearInvalidActivities() {
runOnMainThread(new Runnable() {
@Override
public void run() {
synchronized (mActivityList) {
if (mActivityList.isEmpty()) {
return;
}
// 倒序索引删除,避免遍历过程中索引错乱
for (int i = mActivityList.size() - 1; i >= 0; i--) {
Activity activity = mActivityList.get(i);
if (activity == null || activity.isFinishing() || (getSdkVersion() >= API_VERSION_O && activity.isDestroyed())) {
mActivityList.remove(i);
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "clearInvalidActivities: remove invalid activity: " + className);
}
}
LogUtils.d(TAG, "clearInvalidActivities: done, stack size: " + mActivityList.size());
}
}
});
}
/**
* 确保任务在主线程执行(Activity操作必须主线程,小米机型严格限制)
* @param runnable 待执行任务
*/
private void runOnMainThread(Runnable runnable) {
if (runnable == null) {
return;
}
// 避免不必要的线程切换,优化性能(小米机型流畅度适配)
if (Looper.getMainLooper() == Looper.myLooper()) {
runnable.run();
} else {
mMainHandler.post(runnable);
LogUtils.d(TAG, "runOnMainThread: post task to main thread");
}
}
/**
* 辅助方法:获取当前系统SDK版本(简化版本判断逻辑,统一调用)
* @return SDK版本号
*/
private int getSdkVersion() {
return android.os.Build.VERSION.SDK_INT;
}
}
@@ -1,33 +0,0 @@
package cc.winboll.studio.contacts;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/12/08 15:10:51
* @Describe 全局应用类
*/
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
public class App extends GlobalApplication {
public static final String TAG = "App";
@Override
public void onCreate() {
super.onCreate();
// 设置应用调试标志
setIsDebugging(BuildConfig.DEBUG);
// 初始化窗口管理类
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}
@@ -1,529 +0,0 @@
package cc.winboll.studio.contacts;
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.telecom.TelecomManager;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import cc.winboll.studio.contacts.activities.SettingsActivity;
import cc.winboll.studio.contacts.activities.WinBollActivity;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.fragments.CallLogFragment;
import cc.winboll.studio.contacts.fragments.ContactsFragment;
import cc.winboll.studio.contacts.fragments.LogFragment;
import cc.winboll.studio.contacts.model.MainServiceBean;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.contacts.utils.PermissionUtils;
import cc.winboll.studio.contacts.views.DunTemperatureView;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/08/30 14:32
* @Describe Contacts 主窗口(完全适配 API 30 + Java 7 语法)
* 核心优化:1. 移除电话状态监听 2. 移除通话筛选服务 3. 移除 MainService 所有相关逻辑 4. ViewPager 实现 Fragment 懒加载(仅首屏初始化)
* 问题修复:解决首屏 Fragment 空白问题(删除 setPrimaryItem 冲突逻辑+延迟首屏初始化)
*/
public final class MainActivity extends WinBollActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
// ====================== 1. 常量定义区(硬编码API版本,避免高版本依赖) ======================
public static final String TAG = "MainActivity";
public static final int REQUEST_HOME_ACTIVITY = 0;
public static final int REQUEST_ABOUT_ACTIVITY = 1;
public static final int REQUEST_APP_SETTINGS = 2;
public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS";
private static final int DIALER_REQUEST_CODE = 1;
private static final int REQUEST_REQUIRED_PERMISSIONS = 1002;
private static final int REQUEST_OVERLAY_PERMISSION = 1003;
// API版本硬编码常量(Java 7兼容,杜绝Build.VERSION_CODES高版本引用)
private static final int ANDROID_6_API = 23;
private static final int ANDROID_8_API = 26;
private static final int ANDROID_10_API = 29;
private static final int ANDROID_14_API = 34;
// ====================== 2. 静态成员区 ======================
static MainActivity _MainActivity;
// ====================== 3. 权限常量区 ======================
private final String[] REQUIRED_PERMISSIONS = PermissionUtils.BASE_PERMISSIONS;
// ====================== 4. UI控件成员区 ======================
private ADsBannerView mADsBannerView;
private LogView mLogView;
private Toolbar mToolbar;
private CheckBox cbMainService;
private TabLayout tabLayout;
private ViewPager viewPager;
private List<View> views;
private ImageView[] imageViews;
private LinearLayout linearLayout;
// ====================== 5. 业务逻辑成员区 ======================
private int currentPoint = 0;
private List<Fragment> fragmentList;
private List<String> tabTitleList;
// 记录已初始化的Fragment位置(避免重复初始化)
private boolean[] isFragmentInit;
// ====================== 6. 接口实现区 ======================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
// ====================== 7. 生命周期函数区 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "===== onCreate: 主Activity开始创建 =====");
_MainActivity = this;
// 直接初始化UI(原权限检查逻辑注释保留,按需启用)
initUIAndLogic(savedInstanceState);
MainServiceBean mainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
if (mainServiceBean != null && mainServiceBean.isEnable()) {
Intent intent = new Intent(this, MainService.class);
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService
if (Build.VERSION.SDK_INT >= 31) {
startForegroundService(intent);
} else {
startService(intent);
}
}
LogUtils.d(TAG, "===== onCreate: 主Activity创建流程结束 =====");
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LogUtils.d(TAG, "onPostCreate: 主Activity创建完成");
}
@Override
protected void onResume() {
super.onResume();
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
LogUtils.d(TAG, "onResume: 广告栏资源已恢复");
}
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "===== onDestroy: 主Activity开始销毁 =====");
// 释放广告资源
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
LogUtils.d(TAG, "onDestroy: 广告栏资源已释放");
}
// 清空Fragment相关引用,避免内存泄漏
if (fragmentList != null) {
fragmentList.clear();
fragmentList = null;
}
if (tabTitleList != null) {
tabTitleList.clear();
tabTitleList = null;
}
isFragmentInit = null;
LogUtils.d(TAG, "===== onDestroy: 主Activity销毁完成 =====");
}
// ====================== 8. 权限相关回调函数区 ======================
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
if (requestCode == REQUEST_REQUIRED_PERMISSIONS) {
String deniedPerms = PermissionUtils.getDeniedPermissions(this, permissions);
if (deniedPerms.length() == 0) {
LogUtils.d(TAG, "onRequestPermissionsResult: 所有危险权限授予成功");
checkAndRequestRemainingPermissions();
} else {
LogUtils.e(TAG, "onRequestPermissionsResult: 被拒权限:" + deniedPerms);
showPermissionDeniedDialogAndExit("应用需要「" + deniedPerms + "」权限才能正常运行,请授予权限后重新打开应用。");
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "onActivityResult: 页面回调触发,requestCode=" + requestCode + "resultCode=" + resultCode);
switch (requestCode) {
case DIALER_REQUEST_CODE:
if (resultCode == Activity.RESULT_OK) {
LogUtils.d(TAG, "onActivityResult: 设为默认拨号应用成功");
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用", Toast.LENGTH_SHORT).show();
}
break;
case REQUEST_APP_SETTINGS:
LogUtils.d(TAG, "onActivityResult: 从设置页返回,重建Activity");
recreate();
break;
case REQUEST_OVERLAY_PERMISSION:
handleOverlayPermissionResult();
break;
default:
LogUtils.w(TAG, "onActivityResult: 未知requestCode=" + requestCode);
break;
}
}
/**
* 处理悬浮窗权限申请结果
*/
private void handleOverlayPermissionResult() {
if (PermissionUtils.isOverlayPermissionGranted(this)) {
LogUtils.d(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请成功");
LogUtils.d(TAG, "handleOverlayPermissionResult: 所有权限已授予");
initUIAndLogic(null);
} else {
LogUtils.e(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请失败");
showPermissionDeniedDialogAndExit("应用需要悬浮窗权限才能展示来电弹窗,请授予后重新打开应用。");
}
}
/**
* 检查并申请剩余权限(仅保留悬浮窗)
*/
private void checkAndRequestRemainingPermissions() {
if (!PermissionUtils.isOverlayPermissionGranted(this)) {
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 悬浮窗权限未授予,跳转设置页");
PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION);
} else {
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 所有权限已授予");
initUIAndLogic(null);
}
}
/**
* 权限拒绝提示对话框(Java 7 匿名内部类实现,禁止Lambda)
*/
private void showPermissionDeniedDialogAndExit(String tip) {
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 弹出权限不足提示框");
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("权限不足,无法使用");
builder.setMessage(tip);
builder.setCancelable(false);
builder.setNegativeButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择去设置权限");
PermissionUtils.goAppDetailsSettings(MainActivity.this);
}
});
builder.setPositiveButton("确定退出", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择退出应用");
finishAndRemoveTask();
}
});
builder.show();
}
// ====================== 9. UI与业务逻辑初始化区 ======================
private void initUIAndLogic(Bundle savedInstanceState) {
if (mToolbar != null) {
LogUtils.d(TAG, "initUIAndLogic: UI已初始化,无需重复执行");
return;
}
LogUtils.d(TAG, "===== initUIAndLogic: 开始初始化UI与业务逻辑 =====");
setContentView(R.layout.activity_main);
// 1. 工具栏初始化
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
setSupportActionBar(mToolbar);
getSupportActionBar().setSubtitle(TAG);
LogUtils.d(TAG, "initUIAndLogic: 工具栏初始化完成");
// 2. TabLayout与ViewPager初始化
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
viewPager = (ViewPager) findViewById(R.id.viewPager);
initViewPagerAndTabs();
tabLayout.setupWithViewPager(viewPager);
LogUtils.d(TAG, "initUIAndLogic: ViewPager与TabLayout初始化完成");
// 3. 广告栏初始化
mADsBannerView = (ADsBannerView) findViewById(R.id.adsbanner);
LogUtils.d(TAG, "initUIAndLogic: 广告栏控件初始化完成");
// 左边盾值视图初始化(Java7分步写法,禁止链式调用)
DunTemperatureView tempViewLeft = (DunTemperatureView) findViewById(R.id.dun_temp_view_left);
tempViewLeft.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
tempViewLeft.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
int[] customColors = new int[2];
customColors[0] = Color.parseColor("#FF3366FF");
customColors[1] = Color.parseColor("#FF9900CC");
float[] positions = new float[2];
positions[0] = 0.0f;
positions[1] = 1.0f;
tempViewLeft.setGradientColors(customColors, positions);
// 文本放在温度条右侧(默认,可省略)
tempViewLeft.setTextPosition(true);
// 右边盾值视图初始化(Java7分步写法,禁止链式调用)
DunTemperatureView tempViewRight = (DunTemperatureView) findViewById(R.id.dun_temp_view_right);
tempViewRight.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
tempViewRight.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
tempViewRight.setGradientColors(customColors, positions);
// 文本放在温度条左侧
tempViewRight.setTextPosition(false);
LogUtils.d(TAG, "initUIAndLogic: 盾值视图初始化完成");
LogUtils.d(TAG, "===== initUIAndLogic: 初始化流程全部结束 =====");
}
/**
* 初始化ViewPager与Tab数据(Java7规范,泛型完整声明),添加懒加载标记
* 关键修改:延迟50ms初始化首屏,确保Fragment控件就绪;删除setPrimaryItem冲突逻辑
*/
private void initViewPagerAndTabs() {
LogUtils.d(TAG, "initViewPagerAndTabs: 开始初始化ViewPager数据");
fragmentList = new ArrayList<Fragment>();
tabTitleList = new ArrayList<String>();
// 添加Fragment实例(仅创建对象,不初始化业务逻辑)
fragmentList.add(CallLogFragment.newInstance(0));
fragmentList.add(ContactsFragment.newInstance(1));
fragmentList.add(LogFragment.newInstance(2));
tabTitleList.add("通话记录");
tabTitleList.add("联系人");
tabTitleList.add("应用日志");
// 初始化懒加载标记数组(默认均未初始化)
int fragmentCount = fragmentList.size();
isFragmentInit = new boolean[fragmentCount];
for (int i = 0; i < fragmentCount; i++) {
isFragmentInit[i] = false;
}
// 设置自定义适配器(已删除setPrimaryItem,避免初始化冲突)
LazyLoadPagerAdapter adapter = new LazyLoadPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
viewPager.setAdapter(adapter);
// 关闭预加载(设为0仅加载当前页,关键)
viewPager.setOffscreenPageLimit(0);
viewPager.addOnPageChangeListener(this);
// 关键优化:延迟50ms初始化首屏(确保Fragment已完成onCreateView,控件绑定就绪)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
initFragmentByPosition(0);
LogUtils.d(TAG, "initViewPagerAndTabs: 延迟初始化首屏Fragment,位置=0");
}
}, 50);
LogUtils.d(TAG, "initViewPagerAndTabs: ViewPager初始化完成,等待延迟初始化首屏");
}
/**
* 根据位置初始化Fragment(调用Fragment的初始化逻辑,避免重复执行)
* 优化:添加isAdded判断,确保Fragment已附加到Activity,防止上下文空指针
*/
private void initFragmentByPosition(int position) {
// 校验位置合法性 + 避免重复初始化 + 确保Fragment已附加到Activity
if (position < 0 || position >= fragmentList.size() || isFragmentInit[position]) {
return;
}
Fragment targetFragment = fragmentList.get(position);
if (targetFragment != null && targetFragment.isAdded()) {
// 触发Fragment初始化(调用各Fragment的initData方法)
if (targetFragment instanceof CallLogFragment) {
((CallLogFragment) targetFragment).initData();
} else if (targetFragment instanceof ContactsFragment) {
((ContactsFragment) targetFragment).initData();
} else if (targetFragment instanceof LogFragment) {
((LogFragment) targetFragment).initData();
}
// 标记为已初始化
isFragmentInit[position] = true;
LogUtils.d(TAG, "initFragmentByPosition: 初始化Fragment,位置=" + position + ",标题=" + tabTitleList.get(position));
} else {
LogUtils.w(TAG, "initFragmentByPosition: Fragment未附加到Activity/实例为空,位置=" + position);
}
}
// ====================== 10. 菜单相关函数区 ======================
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
LogUtils.d(TAG, "onCreateOptionsMenu: 菜单加载完成");
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.item_settings) {
LogUtils.d(TAG, "onOptionsItemSelected: 用户点击设置菜单");
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
// ====================== 11. ViewPager页面回调区(切换时初始化对应Fragment ======================
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
currentPoint = position;
LogUtils.d(TAG, "onPageSelected: 页面切换至[" + position + "],标题=" + tabTitleList.get(position));
// 切换页面时,初始化当前页Fragment(未初始化过才执行)
initFragmentByPosition(position);
}
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onClick(View v) {}
// ====================== 12. 工具函数区 ======================
/**
* 拨号工具方法(添加空指针防护)
*/
public static void dialPhoneNumber(String phoneNumber) {
if (_MainActivity == null) {
LogUtils.e(TAG, "dialPhoneNumber: MainActivity实例为空,无法拨号");
return;
}
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
LogUtils.e(TAG, "dialPhoneNumber: 拨号号码为空");
return;
}
if (PermissionUtils.checkPermission(_MainActivity, Manifest.permission.CALL_PHONE)) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:" + phoneNumber));
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
_MainActivity.startActivity(intent);
} else {
LogUtils.e(TAG, "dialPhoneNumber: 拨号权限不足,无法发起拨号");
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
}
}
/**
* 判断是否为默认拨号应用(适配API30,硬编码版本判断)
*/
public boolean isDefaultPhoneCallApp() {
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
TelecomManager manager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
if (manager != null && manager.getDefaultDialerPackage() != null) {
boolean isDefault = manager.getDefaultDialerPackage().equals(getPackageName());
LogUtils.d(TAG, "isDefaultPhoneCallApp: 是否为默认拨号应用=" + isDefault);
return isDefault;
}
}
LogUtils.d(TAG, "isDefaultPhoneCallApp: 系统版本低于Android 6,无法判断");
return false;
}
/**
* 检查服务是否正在运行(通用工具方法,添加空指针防护)
*/
public boolean isServiceRunning(Class<?> serviceClass) {
if (serviceClass == null) {
LogUtils.e(TAG, "isServiceRunning: 服务类参数为null");
return false;
}
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
if (manager == null) {
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败");
return false;
}
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]正在运行");
return true;
}
}
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]未运行");
return false;
}
// ====================== 13. 内部类定义区(Java 7 规范,禁止Lambda ======================
/**
* 自定义懒加载ViewPager适配器(删除setPrimaryItem方法,解决首屏初始化冲突)
*/
private class LazyLoadPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> fragmentList;
private final List<String> tabTitleList;
public LazyLoadPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, List<String> tabTitleList) {
super(fm);
this.fragmentList = fragmentList;
this.tabTitleList = tabTitleList;
LogUtils.d(MainActivity.TAG, "LazyLoadPagerAdapter: 初始化完成,Fragment数量=" + fragmentList.size());
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
@Override
public CharSequence getPageTitle(int position) {
return tabTitleList.get(position);
}
// 【已删除】移除setPrimaryItem方法,避免与手动初始化+onPageSelected回调冲突
}
}
@@ -1,116 +0,0 @@
package cc.winboll.studio.contacts.activities;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libaes.views.AboutView;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/31 15:15:54
* @Describe 应用介绍窗口
*/
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
// ====================== 常量定义区 ======================
public static final String TAG = "AboutActivity";
private static final String BRANCH_NAME = "contacts";
// ====================== 成员变量区 ======================
private Context mContext;
private Toolbar mToolbar;
// ====================== 接口实现区 ======================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
// ====================== 生命周期函数区 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 关于页面开始创建");
mContext = this;
setContentView(R.layout.activity_about);
// 初始化工具栏
initToolbar();
// 初始化关于页面视图
initAboutView();
// 注册Activity管理
WinBoLLActivityManager.getInstance().add(this);
LogUtils.d(TAG, "onCreate: 关于页面初始化完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: 关于页面开始销毁");
WinBoLLActivityManager.getInstance().registeRemove(this);
LogUtils.d(TAG, "onDestroy: 关于页面销毁完成");
}
// ====================== 控件初始化函数区 ======================
private void initToolbar() {
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
// Java7 适配:添加强制类型转换
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(TAG);
// 非空判断,避免空指针异常
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
private void initAboutView() {
LogUtils.d(TAG, "initAboutView: 初始化关于页面内容视图");
AboutView aboutView = createAboutView();
LinearLayout layout = (LinearLayout) findViewById(R.id.aboutviewroot_ll);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
layout.addView(aboutView, params);
LogUtils.d(TAG, "initAboutView: AboutView已添加到布局");
}
// ====================== 业务逻辑函数区 ======================
private AboutView createAboutView() {
LogUtils.d(TAG, "createAboutView: 构建APP信息并创建AboutView");
APPInfo appInfo = new APPInfo();
appInfo.setAppName("Contacts");
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
appInfo.setAppDescription("这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。");
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(BRANCH_NAME);
appInfo.setAppGitAPPSubProjectFolder(BRANCH_NAME);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Contacts");
appInfo.setAppAPKName("Contacts");
appInfo.setAppAPKFolderName("Contacts");
return new AboutView(mContext, appInfo);
}
}
@@ -1,159 +0,0 @@
package cc.winboll.studio.contacts.activities;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/20 17:15:46
* @Describe 拨号窗口
*/
public class CallActivity extends AppCompatActivity {
// ====================== 常量定义区 ======================
public static final String TAG = "CallActivity";
private static final int REQUEST_CALL_PHONE = 1;
// ====================== UI控件区 ======================
private EditText phoneNumberEditText;
private TextView callStatusTextView;
private Button dialButton;
// ====================== 业务成员区 ======================
private TelephonyManager telephonyManager;
private MyPhoneStateListener phoneStateListener;
// ====================== 生命周期函数区 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 拨号页面开始创建");
setContentView(R.layout.activity_call);
// 初始化控件
initViews();
// 初始化电话状态监听
initPhoneStateListener();
LogUtils.d(TAG, "onCreate: 拨号页面初始化完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: 拨号页面开始销毁");
// 取消电话状态监听,避免内存泄漏
if (telephonyManager != null && phoneStateListener != null) {
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
LogUtils.d(TAG, "onDestroy: 电话状态监听已取消");
}
LogUtils.d(TAG, "onDestroy: 拨号页面销毁完成");
}
// ====================== 权限回调函数区 ======================
@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_CALL_PHONE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "onRequestPermissionsResult: 拨打电话权限授予成功");
String phoneNumber = phoneNumberEditText.getText().toString().trim();
dialPhoneNumber(phoneNumber);
} else {
LogUtils.w(TAG, "onRequestPermissionsResult: 拨打电话权限被拒绝");
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
}
}
}
// ====================== 控件初始化函数区 ======================
private void initViews() {
LogUtils.d(TAG, "initViews: 初始化UI控件");
// Java7 适配:添加强制类型转换
phoneNumberEditText = (EditText) findViewById(R.id.phone_number);
dialButton = (Button) findViewById(R.id.dial_button);
callStatusTextView = (TextView) findViewById(R.id.call_status);
// 设置拨号按钮点击事件
dialButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String phoneNumber = phoneNumberEditText.getText().toString().trim();
LogUtils.d(TAG, "initViews: 拨号按钮点击,号码=" + phoneNumber);
if (phoneNumber.isEmpty()) {
Toast.makeText(CallActivity.this, "请输入电话号码", Toast.LENGTH_SHORT).show();
return;
}
// 权限检查
if (ContextCompat.checkSelfPermission(CallActivity.this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED) {
LogUtils.w(TAG, "initViews: 拨打电话权限未授予,发起权限申请");
ActivityCompat.requestPermissions(CallActivity.this,
new String[]{Manifest.permission.CALL_PHONE},
REQUEST_CALL_PHONE);
} else {
dialPhoneNumber(phoneNumber);
}
}
});
}
// ====================== 电话状态监听初始化函数区 ======================
private void initPhoneStateListener() {
LogUtils.d(TAG, "initPhoneStateListener: 初始化电话状态监听");
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
phoneStateListener = new MyPhoneStateListener();
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
// ====================== 核心业务函数区 ======================
private void dialPhoneNumber(String phoneNumber) {
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
LogUtils.e(TAG, "dialPhoneNumber: 拨打电话权限缺失,拨号失败");
return;
}
startActivity(intent);
}
// ====================== 内部电话状态监听类 ======================
private class MyPhoneStateListener extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
switch (state) {
case TelephonyManager.CALL_STATE_IDLE:
callStatusTextView.setText("电话已挂断");
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-挂断");
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
callStatusTextView.setText("正在通话中");
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-通话中");
break;
case TelephonyManager.CALL_STATE_RINGING:
callStatusTextView.setText("来电: " + incomingNumber);
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-来电,号码=" + incomingNumber);
break;
}
}
}
}
@@ -1,80 +0,0 @@
package cc.winboll.studio.contacts.activities;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/20 20:18:26
* @Describe 拨号盘窗口(跳转到系统拨号界面)
*/
public class DialerActivity extends AppCompatActivity {
// ====================== 常量定义区 ======================
public static final String TAG = "DialerActivity";
// ====================== UI控件区 ======================
private EditText phoneNumberEditText;
private Button dialButton;
// ====================== 生命周期函数区 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 拨号盘页面开始创建");
setContentView(R.layout.activity_dialer);
// 初始化UI控件与点击事件
initViews();
LogUtils.d(TAG, "onCreate: 拨号盘页面初始化完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: 拨号盘页面已销毁");
}
// ====================== 控件初始化函数区 ======================
private void initViews() {
LogUtils.d(TAG, "initViews: 初始化UI控件");
// Java7 适配:添加强制类型转换
phoneNumberEditText = (EditText) findViewById(R.id.phone_number_edit_text);
dialButton = (Button) findViewById(R.id.dial_button);
// 设置拨号按钮点击事件
dialButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String phoneNumber = phoneNumberEditText.getText().toString().trim();
LogUtils.d(TAG, "initViews: 拨号按钮点击,输入号码=" + phoneNumber);
// 空号码校验
if (phoneNumber.isEmpty()) {
LogUtils.w(TAG, "initViews: 拨号失败,号码为空");
Toast.makeText(DialerActivity.this, "请输入有效电话号码", Toast.LENGTH_SHORT).show();
return;
}
// 跳转到系统拨号界面
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
LogUtils.d(TAG, "initViews: 成功跳转到系统拨号界面");
} else {
LogUtils.e(TAG, "initViews: 跳转失败,无可用拨号应用");
Toast.makeText(DialerActivity.this, "未找到可用拨号应用", Toast.LENGTH_SHORT).show();
}
}
});
}
}
@@ -1,613 +0,0 @@
package cc.winboll.studio.contacts.activities;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.PhoneConnectRuleAdapter;
import cc.winboll.studio.contacts.bobulltoon.TomCat;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.model.MainServiceBean;
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
import cc.winboll.studio.contacts.model.RingTongBean;
import cc.winboll.studio.contacts.model.SettingsBean;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.contacts.views.DuInfoTextView;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.lang.reflect.Field;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/21 05:37:42
* @Describe Contacts 设置页面(完全适配 API 30 + Java 7 语法)
* 核心优化:1. 移除高版本API依赖 2. Java7规范写法 3. 强化内存泄漏防护 4. 版本判断硬编码 5. LogUtils统一日志管理
*/
public class SettingsActivity extends WinBollActivity implements IWinBoLLActivity {
// ====================== 常量定义区(置顶,统一管理) ======================
public static final String TAG = "SettingsActivity";
// API版本硬编码(替代Build.VERSION_CODES,适配Java7
private static final int ANDROID_6_API = 23;
// ====================== 静态成员属性区 ======================
private static DuInfoTextView sDuInfoTextView; // 规范命名:静态属性加s前缀
// ====================== 数据业务属性区 ======================
private int mStreamMaxVolume; // 铃音最大音量
private int mStreamVolume; // 当前铃音音量
private List<PhoneConnectRuleBean> mRuleList; // 通话规则列表
private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器
// ====================== UI控件属性区(统一归类,规范命名) ======================
private Toolbar mToolbar; // 顶部工具栏
private Switch mSwMainService; // 主服务开关
private SeekBar mSbVolume; // 音量调节条
private TextView mTvVolume; // 音量显示文本
private Switch mSwEnableDun; // 云盾功能开关
private EditText mEtDunTotalCount; // 云盾总次数输入框
private EditText mEtDunResumeSecondCount; // 云盾恢复秒数输入框
private EditText mEtDunResumeCount; // 云盾恢复次数输入框
private RecyclerView mRvRuleList; // 规则列表RecyclerView
private EditText mEtBoBullToonUrl; // BoBullToon地址输入框
private EditText mEtSearchPhone; // 号码查询输入框
// ====================== 接口实现区(IWinBoLLActivity规范实现) ======================
@Override
public AppCompatActivity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
// ====================== 生命周期函数区(按执行顺序排列) ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 设置页面启动");
setContentView(R.layout.activity_settings);
// 初始化核心流程(按优先级执行)
initToolbar(); // 工具栏初始化(优先)
initMainServiceSwitch();// 主服务开关初始化
initVolumeControl(); // 音量控制初始化
initRuleRecyclerView(); // 规则列表初始化
initDunSettings(); // 云盾设置初始化
initBoBullToonViews(); // BoBullToon功能初始化
LogUtils.d(TAG, "onCreate: 设置页面初始化完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: 设置页面销毁");
// 内存泄漏防护:清空所有引用(静态+成员+UI)
sDuInfoTextView = null;
mRuleList = null;
mRuleAdapter = null;
mToolbar = null;
mSwMainService = null;
mSbVolume = null;
mTvVolume = null;
mSwEnableDun = null;
mEtDunTotalCount = null;
mEtDunResumeSecondCount = null;
mEtDunResumeCount = null;
mRvRuleList = null;
mEtBoBullToonUrl = null;
mEtSearchPhone = null;
LogUtils.d(TAG, "onDestroy: 设置页面资源清理完成");
}
// ====================== 初始化函数区(按功能模块归类) ======================
/**
* 初始化顶部工具栏(后退按钮+标题)
*/
private void initToolbar() {
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
setSupportActionBar(mToolbar);
// 显示后退按钮(空指针防护)
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(TAG);
}
// 后退按钮点击事件(Java7匿名内部类)
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面");
finish();
}
});
}
/**
* 初始化主服务开关(联动MainService启停)
*/
private void initMainServiceSwitch() {
LogUtils.d(TAG, "initMainServiceSwitch: 初始化主服务开关");
mSwMainService = (Switch) findViewById(R.id.sw_mainservice);
MainServiceBean serviceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
// 加载开关状态(空指针防护)
boolean isServiceEnable = serviceBean != null && serviceBean.isEnable();
mSwMainService.setChecked(isServiceEnable);
LogUtils.d(TAG, "initMainServiceSwitch: 主服务当前状态:" + (isServiceEnable ? "启用" : "禁用"));
// 开关点击事件
mSwMainService.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean isChecked = mSwMainService.isChecked();
LogUtils.d(TAG, "initMainServiceSwitch: 主服务开关切换:" + (isChecked ? "启用" : "禁用"));
if (isChecked) {
MainService.startMainServiceAndSaveStatus(SettingsActivity.this);
} else {
MainService.stopMainServiceAndSaveStatus(SettingsActivity.this);
}
}
});
}
/**
* 初始化音量控制(SeekBar+音量显示+配置保存)
*/
private void initVolumeControl() {
LogUtils.d(TAG, "initVolumeControl: 初始化音量控制");
mSbVolume = (SeekBar) findViewById(R.id.bellvolume);
mTvVolume = (TextView) findViewById(R.id.tv_volume);
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// 空指针防护:AudioManager获取失败直接返回
if (audioManager == null) {
LogUtils.e(TAG, "initVolumeControl: AudioManager获取失败,音量控制初始化失败");
return;
}
// 初始化音量参数
mStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
mStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
mSbVolume.setMax(mStreamMaxVolume);
mSbVolume.setProgress(mStreamVolume);
updateVolumeDisplay(); // 更新音量文本显示
// 音量调节监听
mSbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
LogUtils.d(TAG, "initVolumeControl: 音量调节至:" + progress + "/" + mStreamMaxVolume);
// 实时更新系统音量+保存配置
audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0);
RingTongBean ringBean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class);
if (ringBean == null) {
ringBean = new RingTongBean();
}
ringBean.setStreamVolume(progress);
RingTongBean.saveBean(SettingsActivity.this, ringBean);
mStreamVolume = progress;
updateVolumeDisplay();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
}
/**
* 初始化通话规则列表(加载黑白名单规则)
*/
private void initRuleRecyclerView() {
LogUtils.d(TAG, "initRuleRecyclerView: 初始化规则列表");
mRvRuleList = (RecyclerView) findViewById(R.id.recycler_view);
mRvRuleList.setLayoutManager(new LinearLayoutManager(this));
// 加载规则数据
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "initRuleRecyclerView: Rules实例获取失败,列表初始化失败");
return;
}
mRuleList = rules.getPhoneBlacRuleBeanList();
mRuleAdapter = new PhoneConnectRuleAdapter(this, mRuleList);
mRvRuleList.setAdapter(mRuleAdapter);
LogUtils.d(TAG, "initRuleRecyclerView: 规则列表加载完成,共" + mRuleList.size() + "条规则");
}
/**
* 初始化云盾设置(参数加载+开关联动)
*/
private void initDunSettings() {
LogUtils.d(TAG, "initDunSettings: 初始化云盾设置");
sDuInfoTextView = (DuInfoTextView) findViewById(R.id.tv_DunInfo);
mSwEnableDun = (Switch) findViewById(R.id.sw_IsEnableDun);
mEtDunTotalCount = (EditText) findViewById(R.id.et_DunTotalCount);
mEtDunResumeSecondCount = (EditText) findViewById(R.id.et_DunResumeSecondCount);
mEtDunResumeCount = (EditText) findViewById(R.id.et_DunResumeCount);
// 加载云盾配置
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "initDunSettings: Rules实例获取失败,云盾初始化失败");
return;
}
SettingsBean dunSettings = rules.getSettingsModel();
if (dunSettings == null) {
LogUtils.e(TAG, "initDunSettings: 云盾配置获取失败");
return;
}
// 填充配置参数
mEtDunTotalCount.setText(String.valueOf(dunSettings.getDunTotalCount()));
mEtDunResumeSecondCount.setText(String.valueOf(dunSettings.getDunResumeSecondCount()));
mEtDunResumeCount.setText(String.valueOf(dunSettings.getDunResumeCount()));
mSwEnableDun.setChecked(dunSettings.isEnableDun());
// 开关联动:启用云盾时禁用参数编辑
boolean isDunEnable = dunSettings.isEnableDun();
mEtDunTotalCount.setEnabled(!isDunEnable);
mEtDunResumeSecondCount.setEnabled(!isDunEnable);
mEtDunResumeCount.setEnabled(!isDunEnable);
LogUtils.d(TAG, "initDunSettings: 云盾当前状态:" + (isDunEnable ? "启用" : "禁用"));
}
/**
* 初始化BoBullToon功能(地址配置+号码查询)
*/
private void initBoBullToonViews() {
LogUtils.d(TAG, "initBoBullToonViews: 初始化BoBullToon功能");
mEtBoBullToonUrl = (EditText) findViewById(R.id.bobulltoonurl_et);
mEtSearchPhone = (EditText) findViewById(R.id.activitysettingsEditText1);
// 加载保存的地址
Rules rules = Rules.getInstance(this);
if (rules != null) {
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
LogUtils.d(TAG, "initBoBullToonViews: 加载BoBullToon地址完成");
} else {
LogUtils.e(TAG, "initBoBullToonViews: Rules实例获取失败,地址加载失败");
}
}
// ====================== 点击事件回调区(按功能模块归类) ======================
/**
* 云盾开关点击事件(联动参数编辑权限+配置保存)
*/
public void onSW_IsEnableDun(View view) {
boolean isChecked = mSwEnableDun.isChecked();
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾开关切换:" + (isChecked ? "启用" : "禁用"));
// 联动参数编辑权限
mEtDunTotalCount.setEnabled(!isChecked);
mEtDunResumeSecondCount.setEnabled(!isChecked);
mEtDunResumeCount.setEnabled(!isChecked);
// 保存配置
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "onSW_IsEnableDun: Rules实例获取失败,配置保存失败");
mSwEnableDun.setChecked(false);
return;
}
SettingsBean dunSettings = rules.getSettingsModel();
if (dunSettings == null) {
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾配置获取失败,保存失败");
mSwEnableDun.setChecked(false);
return;
}
// 启用云盾时校验参数合法性
if (isChecked) {
try {
String totalCountStr = mEtDunTotalCount.getText().toString().trim();
String resumeSecStr = mEtDunResumeSecondCount.getText().toString().trim();
String resumeCountStr = mEtDunResumeCount.getText().toString().trim();
// 空参数校验
if (totalCountStr.isEmpty() || resumeSecStr.isEmpty() || resumeCountStr.isEmpty()) {
throw new NumberFormatException("参数不能为空");
}
// 转换参数并保存
int totalCount = Integer.parseInt(totalCountStr);
int resumeSec = Integer.parseInt(resumeSecStr);
int resumeCount = Integer.parseInt(resumeCountStr);
dunSettings.setDunTotalCount(totalCount);
dunSettings.setDunResumeSecondCount(resumeSec);
dunSettings.setDunResumeCount(resumeCount);
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾参数保存完成,总次数:" + totalCount + ",恢复秒数:" + resumeSec);
// 提示信息
String toastMsg = totalCount == 1 ? "电话骚扰防御力几乎为0" : "连拨" + totalCount + "次后接通电话";
ToastUtils.show(toastMsg);
} catch (NumberFormatException e) {
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾参数格式错误", e);
ToastUtils.show("参数格式错误,请输入整数");
mSwEnableDun.setChecked(false);
return;
}
}
// 保存开关状态并刷新配置
dunSettings.setIsEnableDun(isChecked);
rules.saveDun();
rules.reload();
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾配置保存完成");
}
/**
* 添加新通话规则(黑白名单)
*/
public void onAddNewConnectionRule(View view) {
LogUtils.d(TAG, "onAddNewConnectionRule: 添加新通话规则");
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "onAddNewConnectionRule: Rules实例获取失败,添加失败");
return;
}
mRuleList.add(new PhoneConnectRuleBean());
rules.saveRules();
mRuleAdapter.notifyDataSetChanged();
LogUtils.d(TAG, "onAddNewConnectionRule: 规则添加完成,当前共" + mRuleList.size() + "条规则");
}
/**
* 跳转默认电话应用设置
*/
public void onDefaultPhone(View view) {
LogUtils.d(TAG, "onDefaultPhone: 跳转默认电话应用设置");
startActivity(new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS));
}
/**
* 悬浮窗权限检查与请求
*/
public void onCanDrawOverlays(View view) {
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
// API6.0+校验权限
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !Settings.canDrawOverlays(this)) {
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
showDrawOverlayRequestDialog();
} else {
ToastUtils.show("悬浮窗权限已开启");
}
}
/**
* 清理BoBullToon本地数据
*/
public void onCleanBoBullToonData(View view) {
LogUtils.d(TAG, "onCleanBoBullToonData: 清理BoBullToon数据");
TomCat tomCat = TomCat.getInstance(this);
if (tomCat != null) {
tomCat.cleanBoBullToon();
ToastUtils.show("BoBullToon数据已清理");
LogUtils.d(TAG, "onCleanBoBullToonData: 数据清理完成");
} else {
LogUtils.e(TAG, "onCleanBoBullToonData: TomCat实例获取失败,清理失败");
}
}
/**
* 重置BoBullToon默认地址
*/
public void onResetBoBullToonURL(View view) {
LogUtils.d(TAG, "onResetBoBullToonURL: 重置BoBullToon地址");
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "onResetBoBullToonURL: Rules实例获取失败,重置失败");
return;
}
rules.resetDefaultBoBullToonURL();
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
ToastUtils.show("BoBullToon地址已重置为默认");
LogUtils.d(TAG, "onResetBoBullToonURL: 地址重置完成");
}
/**
* 下载BoBullToon数据(子线程执行,避免阻塞UI)
*/
public void onDownloadBoBullToon(View view) {
LogUtils.d(TAG, "onDownloadBoBullToon: 开始下载BoBullToon数据");
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "onDownloadBoBullToon: Rules实例获取失败,下载失败");
return;
}
// 校验并更新地址
String inputUrl = mEtBoBullToonUrl.getText().toString().trim();
String savedUrl = rules.getBoBullToonURL();
if (!inputUrl.equals(savedUrl)) {
rules.setBoBullToonURL(inputUrl);
LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon地址更新为:" + inputUrl);
}
// 子线程下载(Java7匿名内部类)
final TomCat tomCat = TomCat.getInstance(this);
new Thread(new Runnable() {
@Override
public void run() {
boolean downloadSuccess = tomCat != null && tomCat.downloadBoBullToon();
if (downloadSuccess) {
LogUtils.d(TAG, "onDownloadBoBullToon: 数据下载成功");
// 主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("BoBullToon下载成功");
}
});
// 重启主服务+刷新配置
MainService.restartMainService(SettingsActivity.this);
Rules.getInstance(SettingsActivity.this).reload();
} else {
LogUtils.e(TAG, "onDownloadBoBullToon: 数据下载失败");
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("BoBullToon下载失败");
}
});
}
}
}).start();
}
/**
* 查询号码是否为BoBullToon号码
*/
public void onSearchBoBullToonPhone(View view) {
LogUtils.d(TAG, "onSearchBoBullToonPhone: 执行号码查询");
String phone = mEtSearchPhone.getText().toString().trim();
// 空号码校验
if (phone.isEmpty()) {
LogUtils.w(TAG, "onSearchBoBullToonPhone: 查询号码为空,取消查询");
ToastUtils.show("请输入查询号码");
return;
}
// 执行查询
TomCat tomCat = TomCat.getInstance(this);
if (tomCat == null || !tomCat.loadPhoneBoBullToon()) {
LogUtils.w(TAG, "onSearchBoBullToonPhone: BoBullToon数据未加载,查询失败");
ToastUtils.show("请先下载BoBullToon数据");
return;
}
boolean isBoBullToon = tomCat.isPhoneBoBullToon(phone);
String resultMsg = isBoBullToon ? "是BoBullToon号码" : "非BoBullToon号码";
ToastUtils.show(resultMsg);
LogUtils.d(TAG, "onSearchBoBullToonPhone: 号码" + phone + "查询结果:" + resultMsg);
}
/**
* 跳转单元测试页面
*/
public void onUnitTest(View view) {
LogUtils.d(TAG, "onUnitTest: 跳转单元测试页面");
startActivity(new Intent(this, UnitTestActivity.class));
}
/**
* 跳转关于页面
*/
public void onAbout(View view) {
LogUtils.d(TAG, "onAbout: 跳转关于页面");
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
}
/**
* 跳转日志查看页面
*/
public void onLogView(View view) {
LogUtils.d(TAG, "onLogView: 跳转日志页面");
WinBoLLActivityManager.getInstance().startLogActivity(this);
}
// ====================== 工具方法区(通用功能+权限相关) ======================
/**
* 更新音量显示文本(当前音量/最大音量)
*/
private void updateVolumeDisplay() {
mTvVolume.setText(mStreamVolume + "/" + mStreamMaxVolume);
}
/**
* 显示悬浮窗权限请求对话框
*/
private void showDrawOverlayRequestDialog() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
jumpToDrawOverlaySettings();
}
})
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
// 解决对话框焦点问题
if (dialog.getWindow() != null) {
dialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
dialog.show();
}
/**
* 跳转悬浮窗权限设置页面(反射适配低版本)
*/
private void jumpToDrawOverlaySettings() {
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
try {
// 反射获取设置页面Action(避免高版本API依赖)
Class<?> settingsClazz = Settings.class;
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
String action = (String) actionField.get(null);
// 跳转当前应用权限设置页
Intent intent = new Intent(action);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception e) {
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
}
}
// ====================== 静态通知方法区(云盾信息更新) ======================
/**
* 通知云盾信息刷新(外部调用)
*/
public static void notifyDunInfoUpdate() {
if (sDuInfoTextView != null) {
LogUtils.d(TAG, "notifyDunInfoUpdate: 刷新云盾信息显示");
sDuInfoTextView.notifyInfoUpdate();
} else {
LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件未初始化,刷新失败");
}
}
}
@@ -1,154 +0,0 @@
package cc.winboll.studio.contacts.activities;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.activities.UnitTestActivity;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
import cc.winboll.studio.contacts.utils.IntUtils;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/02 16:07:04
* @Describe 规则单元测试页面
*/
public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivity {
// ====================== 常量定义区 ======================
public static final String TAG = "UnitTestActivity";
// ====================== UI控件区 ======================
private LogView logView;
private EditText etPhone;
// ====================== 接口实现区 ======================
@Override
public AppCompatActivity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
// ====================== 生命周期函数区 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 单元测试页面开始创建");
setContentView(R.layout.activity_unittest);
// 初始化控件
initViews();
LogUtils.d(TAG, "onCreate: 单元测试页面初始化完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: 单元测试页面开始销毁");
if (logView != null) {
// 若LogView有停止方法,建议调用避免资源泄漏
// logView.stop();
LogUtils.d(TAG, "onDestroy: LogView资源已处理");
}
LogUtils.d(TAG, "onDestroy: 单元测试页面销毁完成");
}
// ====================== 控件初始化函数区 ======================
private void initViews() {
LogUtils.d(TAG, "initViews: 初始化UI控件");
// Java7 适配:添加强制类型转换
logView = (LogView) findViewById(R.id.logview);
etPhone = (EditText) findViewById(R.id.phone_et);
// 启动日志视图
logView.start();
LogUtils.d(TAG, "initViews: LogView已启动");
}
// ====================== 点击事件测试函数区 ======================
/**
* 测试单个号码匹配规则
*/
public void onTestPhone(View view) {
LogUtils.d(TAG, "onTestPhone: 开始测试单个号码规则匹配");
String phone = etPhone.getText().toString().trim();
if (phone.isEmpty()) {
LogUtils.w(TAG, "onTestPhone: 测试号码为空,跳过匹配");
return;
}
Rules rules = Rules.getInstance(this);
boolean isAllowed = rules.isAllowed(phone);
LogUtils.d(TAG, String.format("onTestPhone: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
}
/**
* 批量测试预设号码规则匹配
*/
public void onTestMain(View view) {
LogUtils.d(TAG, "onTestMain: 开始批量测试号码规则匹配");
// 测试IntUtils工具类方法
LogUtils.d(TAG, "onTestMain: 执行 IntUtils.unittest_getIntInRange() 测试");
IntUtils.unittest_getIntInRange();
// 初始化规则实例
Rules rules = Rules.getInstance(this);
// 无规则时添加测试规则集
initTestRulesIfEmpty(rules);
// 预设测试号码列表
String[] testPhones = {
"16769764848", "16856582777", "17519703124",
"0205658955", "0108965253", "+8616769764848",
"4005816769764848", "95566"
};
// 遍历测试号码并输出结果
for (String phone : testPhones) {
boolean isAllowed = rules.isAllowed(phone);
LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
}
LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成");
new Thread(new Runnable(){
@Override
public void run() {
LimitedTimeSpecialChannelService.unitTest(UnitTestActivity.this);
}
}).start();
}
// ====================== 私有工具函数区 ======================
/**
* 规则集为空时初始化测试规则
*/
private void initTestRulesIfEmpty(Rules rules) {
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前无规则,添加测试规则集");
// 规则1:中国手机号允许
rules.add("^1[3-9]\\d{9}$", true, true);
// 规则20660区号号码允许
rules.add("^0660\\d+$", true, true);
// 规则3020区号号码允许
rules.add("^020\\d+$", true, true);
// 规则4:默认拒接所有号码
rules.add(".*", false, true);
// 保存规则到本地
rules.saveRules();
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
} else {
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
}
}
}
@@ -1,84 +0,0 @@
package cc.winboll.studio.contacts.activities;
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.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/31 15:16:45
* @Describe 应用窗口基类,统一处理主题设置与导航返回
*/
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) {
//LogUtils.d(TAG, "onCreate: 基类页面开始创建");
// 优先设置主题,再执行父类初始化
// mThemeType = getThemeType();
// setThemeStyle();
super.onCreate(savedInstanceState);
//LogUtils.d(TAG, "onCreate: 基类主题设置完成,当前主题类型=" + mThemeType);
}
// ====================== 主题相关函数区 ======================
/**
* 获取当前应用主题类型
*/
AESThemeBean.ThemeType getThemeType() {
LogUtils.d(TAG, "getThemeType: 获取应用主题类型");
// 注释的SharedPreferences逻辑保留,便于后续扩展
/*SharedPreferences sharedPreferences = getSharedPreferences(
SHAREDPREFERENCES_NAME, MODE_PRIVATE);
return AESThemeBean.ThemeType.values()[((sharedPreferences.getInt(DRAWER_THEME_TYPE, AESThemeBean.ThemeType.DEFAULT.ordinal())))];
*/
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
/**
* 应用当前主题样式
*/
void setThemeStyle() {
LogUtils.d(TAG, "setThemeStyle: 开始设置应用主题");
// 替换原注释逻辑,使用AESThemeUtil获取的主题ID
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
LogUtils.d(TAG, "setThemeStyle: 主题设置完成");
}
// ====================== 菜单与导航函数区 ======================
@Override
public boolean onOptionsItemSelected(MenuItem item) {
LogUtils.d(TAG, "onOptionsItemSelected: 菜单选项点击,itemId=" + item.getItemId());
// 处理导航栏返回按钮点击事件
// if (item.getItemId() == android.R.id.home) {
// LogUtils.d(TAG, "onOptionsItemSelected: 点击导航返回按钮,关闭当前页面");
// finish();
// return true;
// }
return super.onOptionsItemSelected(item);
}
}
@@ -1,183 +0,0 @@
package cc.winboll.studio.contacts.adapters;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.model.CallLogModel;
import cc.winboll.studio.contacts.utils.ContactUtils;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import cc.winboll.studio.contacts.dun.Rules;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/26 13:09:32
* @Describe 通话记录列表适配器
*/
public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogViewHolder> {
// ====================== 常量定义区 ======================
public static final String TAG = "CallLogAdapter";
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
// ====================== 成员变量区 ======================
private Context mContext;
private List<CallLogModel> callLogList;
private ContactUtils mContactUtils;
// ====================== 构造函数区 ======================
public CallLogAdapter(Context context, List<CallLogModel> callLogList) {
LogUtils.d(TAG, "CallLogAdapter: 初始化适配器,数据量=" + callLogList.size());
this.mContext = context;
this.callLogList = callLogList;
this.mContactUtils = ContactUtils.getInstance(mContext);
}
// ====================== 公共方法区 ======================
/**
* 重新加载联系人数据
*/
public void relaodContacts() {
LogUtils.d(TAG, "relaodContacts: 开始重新加载联系人数据");
this.mContactUtils.reloadContacts();
notifyDataSetChanged();
LogUtils.d(TAG, "relaodContacts: 联系人数据加载完成,列表已刷新");
}
// ====================== RecyclerView 重写方法区 ======================
@NonNull
@Override
public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LogUtils.d(TAG, "onCreateViewHolder: 创建列表项ViewHolder");
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_call_log, parent, false);
return new CallLogViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
LogUtils.d(TAG, "onBindViewHolder: 绑定列表项数据,position=" + position);
final CallLogModel callLog = callLogList.get(position);
// 绑定通话号码与联系人名称
String contactName = mContactUtils.getContactName(callLog.getPhoneNumber());
String phoneText = callLog.getPhoneNumber() + "" + (contactName == null ? "" : contactName);
holder.phoneNumber.setText(phoneText);
// 号码长按弹出菜单事件
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View p1) {
showPhonePopupMenu(holder.phoneNumber, callLog);
return true;
}
});
// 绑定通话状态与时间
holder.callStatus.setText(callLog.getCallStatus());
holder.callDate.setText(DATE_FORMAT.format(callLog.getCallDate()));
// 初始化滑动拨号SeekBar
initDialSeekBar(holder.dialAOHPCTCSeekBar, callLog);
}
@Override
public int getItemCount() {
return callLogList == null ? 0 : callLogList.size();
}
// ====================== 私有工具方法区 ======================
/**
* 显示号码操作弹窗菜单
*/
private void showPhonePopupMenu(View anchorView, final CallLogModel callLog) {
LogUtils.d(TAG, "showPhonePopupMenu: 弹出号码操作菜单");
PopupMenu menu = new PopupMenu(mContext, anchorView);
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
int itemId = menuItem.getItemId();
if (itemId == R.id.item_calllog_phonenumber_copy) {
// 复制号码到剪贴板
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("call_log_phone", callLog.getPhoneNumber());
clipboard.setPrimaryClip(clip);
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
LogUtils.d(TAG, "showPhonePopupMenu: 号码" + callLog.getPhoneNumber() + "已复制到剪贴板");
} else if (itemId == R.id.item_calllog_phonenumber_yundun_test) {
// 跳转到添加联系人页面
//if (Rules.getInstance(mContext).isAllowed(callLog.getPhoneNumber(), false)) {
if (Rules.getInstance(mContext).isAllowed(callLog.getPhoneNumber(), true)) {
ToastUtils.show("(✔)" + callLog.getPhoneNumber() + " Is Allowed By YunDun.");
} else {
ToastUtils.show("(✘)YunDun Defense The Phone " + callLog.getPhoneNumber() + "");
}
} else if (itemId == R.id.item_calllog_phonenumber_add_contact) {
// 跳转到添加联系人页面
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
LogUtils.d(TAG, "showPhonePopupMenu: 跳转添加联系人页面,号码=" + callLog.getPhoneNumber());
}
return true;
}
});
menu.show();
}
/**
* 初始化滑动拨号SeekBar
*/
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final CallLogModel callLog) {
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
seekBar.setBlurRightDP(80);
seekBar.setThumbOffset(0);
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
@Override
public void onOHPCommit() {
String phoneNumber = callLog.getPhoneNumber().replaceAll("\\s", "");
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
ToastUtils.show(phoneNumber);
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
});
}
// ====================== ViewHolder 内部类 ======================
public class CallLogViewHolder extends RecyclerView.ViewHolder {
TextView phoneNumber;
TextView callStatus;
TextView callDate;
AOHPCTCSeekBar dialAOHPCTCSeekBar;
public CallLogViewHolder(@NonNull View itemView) {
super(itemView);
// Java7 适配:添加强制类型转换
phoneNumber = (TextView) itemView.findViewById(R.id.phone_number);
callStatus = (TextView) itemView.findViewById(R.id.call_status);
callDate = (TextView) itemView.findViewById(R.id.call_date);
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
}
}
}
@@ -1,157 +0,0 @@
package cc.winboll.studio.contacts.adapters;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.model.ContactModel;
import cc.winboll.studio.contacts.utils.ContactUtils;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/26 13:35:44
* @Describe 联系人列表适配器
*/
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
// ====================== 常量定义区 ======================
public static final String TAG = "ContactAdapter";
// 移除未使用的 REQUEST_CALL_PHONE 常量,精简冗余代码
// ====================== 成员变量区 ======================
private Context mContext;
private List<ContactModel> contactList;
// ====================== 构造函数区 ======================
public ContactAdapter(Context context, List<ContactModel> contactList) {
LogUtils.d(TAG, "ContactAdapter: 初始化适配器,联系人数量=" + contactList.size());
this.mContext = context;
this.contactList = contactList;
}
// ====================== RecyclerView 重写方法区 ======================
@NonNull
@Override
public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LogUtils.d(TAG, "onCreateViewHolder: 创建联系人列表项ViewHolder");
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact, parent, false);
return new ContactViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
LogUtils.d(TAG, "onBindViewHolder: 绑定联系人列表项数据,position=" + position);
final ContactModel contact = contactList.get(position);
// 绑定联系人名称与号码
holder.contactName.setText(contact.getName());
holder.contactNumber.setText(contact.getNumber());
// 长按联系人条目弹出操作菜单
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContactPopupMenu(holder.llPhoneNumberMain, contact);
return true;
}
});
// 初始化滑动拨号SeekBar
initDialSeekBar(holder.dialAOHPCTCSeekBar, contact);
}
@Override
public int getItemCount() {
// 增加空指针判断,避免空列表崩溃
return contactList == null ? 0 : contactList.size();
}
// ====================== 私有工具方法区 ======================
/**
* 显示联系人操作弹窗菜单
*/
private void showContactPopupMenu(View anchorView, final ContactModel contact) {
LogUtils.d(TAG, "showContactPopupMenu: 弹出联系人操作菜单");
PopupMenu menu = new PopupMenu(mContext, anchorView);
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
int itemId = menuItem.getItemId();
if (itemId == R.id.item_contact_phonenumber_copy) {
// 复制联系人号码到剪贴板
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("contact_phone", contact.getNumber());
clipboard.setPrimaryClip(clip);
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
LogUtils.d(TAG, "showContactPopupMenu: 联系人号码" + contact.getNumber() + "已复制到剪贴板");
} else if (itemId == R.id.item_calllog_phonenumber_edit_contact) {
// 跳转到编辑联系人页面
Long contactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), contactId);
LogUtils.d(TAG, "showContactPopupMenu: 跳转编辑联系人页面,号码=" + contact.getNumber() + "ID=" + contactId);
}
return true;
}
});
menu.show();
}
/**
* 初始化滑动拨号SeekBar
*/
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final ContactModel contact) {
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
seekBar.setBlurRightDP(80);
seekBar.setThumbOffset(0);
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
@Override
public void onOHPCommit() {
String phoneNumber = contact.getNumber().replaceAll("\\s", "");
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
ToastUtils.show(phoneNumber);
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
});
}
// ====================== ViewHolder 内部类 ======================
public class ContactViewHolder extends RecyclerView.ViewHolder {
LinearLayout llPhoneNumberMain;
TextView contactName;
TextView contactNumber;
AOHPCTCSeekBar dialAOHPCTCSeekBar;
public ContactViewHolder(@NonNull View itemView) {
super(itemView);
// Java7 适配:添加强制类型转换
llPhoneNumberMain = (LinearLayout) itemView.findViewById(R.id.itemcontactLinearLayout1);
contactName = (TextView) itemView.findViewById(R.id.contact_name);
contactNumber = (TextView) itemView.findViewById(R.id.contact_number);
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
}
}
}
@@ -1,257 +0,0 @@
package cc.winboll.studio.contacts.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.views.LeftScrollView;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/02 17:27:41
* @Describe 通话规则列表适配器,支持简单查看/编辑两种视图切换
*/
public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
// ====================== 常量定义区 ======================
public static final String TAG = "PhoneConnectRuleAdapter";
private static final int VIEW_TYPE_SIMPLE = 0;
private static final int VIEW_TYPE_EDIT = 1;
private static final String NULL_RULE_TEXT = "[NULL]";
// ====================== 成员变量区 ======================
private Context mContext;
private List<PhoneConnectRuleBean> mRuleList;
// ====================== 构造函数区 ======================
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleBean> ruleList) {
LogUtils.d(TAG, "PhoneConnectRuleAdapter: 初始化适配器,规则数量=" + ruleList.size());
this.mContext = context;
this.mRuleList = ruleList;
}
// ====================== RecyclerView 重写方法区 ======================
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(mContext);
if (viewType == VIEW_TYPE_SIMPLE) {
LogUtils.d(TAG, "onCreateViewHolder: 创建简单视图ViewHolder");
View view = inflater.inflate(R.layout.view_phone_connect_rule_simple, parent, false);
return new SimpleViewHolder(parent, view);
} else {
LogUtils.d(TAG, "onCreateViewHolder: 创建编辑视图ViewHolder");
View view = inflater.inflate(R.layout.view_phone_connect_rule, parent, false);
return new EditViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
final PhoneConnectRuleBean model = mRuleList.get(position);
LogUtils.d(TAG, "onBindViewHolder: 绑定规则数据,position=" + position + ",视图类型=" + getItemViewType(position));
if (holder instanceof SimpleViewHolder) {
bindSimpleViewHolder((SimpleViewHolder) holder, model, position);
} else if (holder instanceof EditViewHolder) {
bindEditViewHolder((EditViewHolder) holder, model, position);
}
}
@Override
public int getItemCount() {
return mRuleList == null ? 0 : mRuleList.size();
}
@Override
public int getItemViewType(int position) {
return mRuleList.get(position).isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
}
// ====================== 私有视图绑定方法区 ======================
/**
* 绑定简单视图数据
*/
private void bindSimpleViewHolder(final SimpleViewHolder holder, final PhoneConnectRuleBean model, final int position) {
// 绑定规则文本,空值显示[NULL]
String ruleText = model.getRuleText().trim().isEmpty() ? NULL_RULE_TEXT : model.getRuleText().trim();
holder.tvRuleText.setText(ruleText);
// 设置复选框状态并禁用编辑
holder.checkBoxAllow.setChecked(model.isAllowConnection());
holder.checkBoxAllow.setEnabled(false);
holder.checkBoxEnable.setChecked(model.isEnable());
holder.checkBoxEnable.setEnabled(false);
// 设置左滑操作监听
holder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener() {
@Override
public void onUp() {
LogUtils.d(TAG, "onUp: 规则上移,position=" + position);
moveRuleUp(position);
holder.scrollView.smoothScrollTo(0, 0);
}
@Override
public void onDown() {
LogUtils.d(TAG, "onDown: 规则下移,position=" + position);
moveRuleDown(position);
holder.scrollView.smoothScrollTo(0, 0);
}
@Override
public void onEdit() {
LogUtils.d(TAG, "onEdit: 切换到编辑视图,position=" + position);
model.setIsSimpleView(false);
notifyItemChanged(position);
holder.scrollView.smoothScrollTo(0, 0);
}
@Override
public void onDelete() {
LogUtils.d(TAG, "onDelete: 触发规则删除确认,position=" + position);
showDeleteConfirmDialog(holder.scrollView.getContext(), model, position);
}
});
}
/**
* 绑定编辑视图数据
*/
private void bindEditViewHolder(final EditViewHolder holder, final PhoneConnectRuleBean model, final int position) {
// 绑定规则文本到输入框
holder.editText.setText(model.getRuleText());
// 绑定复选框状态
holder.checkBoxAllow.setChecked(model.isAllowConnection());
holder.checkBoxEnable.setChecked(model.isEnable());
// 确认按钮点击事件
holder.buttonConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String newRuleText = holder.editText.getText().toString().trim();
model.setRuleText(newRuleText);
model.setIsAllowConnection(holder.checkBoxAllow.isChecked());
model.setIsEnable(holder.checkBoxEnable.isChecked());
model.setIsSimpleView(true);
// 保存规则并刷新视图
Rules.getInstance(mContext).saveRules();
notifyItemChanged(position);
Toast.makeText(mContext, "保存成功", Toast.LENGTH_SHORT).show();
LogUtils.d(TAG, "bindEditViewHolder: 规则保存成功,position=" + position + ",规则内容=" + newRuleText);
}
});
}
// ====================== 私有业务工具方法区 ======================
/**
* 规则上移
*/
private void moveRuleUp(int position) {
if (position <= 0) {
ToastUtils.show("已到顶部,无法上移");
return;
}
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
swapRulePosition(ruleList, position, position - 1);
}
/**
* 规则下移
*/
private void moveRuleDown(int position) {
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
if (position >= ruleList.size() - 1) {
ToastUtils.show("已到底部,无法下移");
return;
}
swapRulePosition(ruleList, position, position + 1);
}
/**
* 交换规则位置
*/
private void swapRulePosition(ArrayList<PhoneConnectRuleBean> list, int fromPos, int toPos) {
PhoneConnectRuleBean temp = list.get(fromPos);
list.set(fromPos, list.get(toPos));
list.set(toPos, temp);
Rules.getInstance(mContext).saveRules();
notifyDataSetChanged();
LogUtils.d(TAG, "swapRulePosition: 规则位置交换完成,from=" + fromPos + "to=" + toPos);
}
/**
* 显示删除确认弹窗
*/
private void showDeleteConfirmDialog(Context dialogContext, final PhoneConnectRuleBean model, final int position) {
YesNoAlertDialog.show(dialogContext, "删除确认", "是否删除该通话规则?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
public void onYes() {
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
ruleList.remove(position);
Rules.getInstance(mContext).saveRules();
notifyDataSetChanged();
LogUtils.d(TAG, "showDeleteConfirmDialog: 规则删除成功,position=" + position);
}
@Override
public void onNo() {
LogUtils.d(TAG, "showDeleteConfirmDialog: 用户取消删除规则,position=" + position);
}
});
}
// ====================== ViewHolder 内部类区 ======================
static class SimpleViewHolder extends RecyclerView.ViewHolder {
LeftScrollView scrollView;
TextView tvRuleText;
CheckBox checkBoxAllow;
CheckBox checkBoxEnable;
public SimpleViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
super(itemView);
scrollView = (LeftScrollView) itemView.findViewById(R.id.scrollView);
// 初始化简单视图内容布局
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
tvRuleText = (TextView) viewContent.findViewById(R.id.ruletext_tv);
checkBoxAllow = (CheckBox) viewContent.findViewById(R.id.checkbox_allow);
checkBoxEnable = (CheckBox) viewContent.findViewById(R.id.checkbox_enable);
// 设置内容宽度并添加到滚动视图
scrollView.setContentWidth(parent.getWidth());
scrollView.addContentLayout(viewContent);
}
}
static class EditViewHolder extends RecyclerView.ViewHolder {
EditText editText;
CheckBox checkBoxAllow;
CheckBox checkBoxEnable;
Button buttonConfirm;
public EditViewHolder(@NonNull View itemView) {
super(itemView);
// Java7 适配:添加强制类型转换
editText = (EditText) itemView.findViewById(R.id.edit_text);
checkBoxAllow = (CheckBox) itemView.findViewById(R.id.checkbox_allow);
checkBoxEnable = (CheckBox) itemView.findViewById(R.id.checkbox_enable);
buttonConfirm = (Button) itemView.findViewById(R.id.button_confirm);
}
}
}
@@ -1,260 +0,0 @@
package cc.winboll.studio.contacts.bobulltoon;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/02 13:47:48
* @Describe 汤姆猫管家 :使用 BoBullToon 项目,对通讯地址进行筛选判断的好朋友。
*/
import android.content.Context;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class TomCat {
public static final String TAG = "TomCat";
List<String> listPhoneBoBullToon = new ArrayList<String>();
String mszBoBullToon_URL;
static volatile TomCat _TomCat;
Context mContext;
TomCat(Context context) {
mContext = context;
}
public static synchronized TomCat getInstance(Context context) {
if (_TomCat == null) {
_TomCat = new TomCat(context);
}
return _TomCat;
}
public String getDefaultBobulltoonUrl() {
return mContext.getString(R.string.default_bobulltoon_url);
}
boolean downloadAndExtractZip(String zipUrl, String destinationFolder) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(zipUrl)
.build();
try {
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
// 下载 ZIP 文件到临时位置
File tempZipFile = File.createTempFile("temp", ".zip");
try {
InputStream inputStream = response.body().byteStream();
FileOutputStream outputStream = new FileOutputStream(tempZipFile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
} catch (Exception e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
// 解压 ZIP 文件到指定文件夹
try {
ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(tempZipFile.toPath()));
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
Path targetFilePath = Paths.get(destinationFolder, zipEntry.getName());
if (zipEntry.isDirectory()) {
Files.createDirectories(targetFilePath);
} else {
Files.createDirectories(targetFilePath.getParent());
try (FileOutputStream fos = new FileOutputStream(targetFilePath.toFile())) {
byte[] buffer = new byte[1024];
int len;
while ((len = zipInputStream.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
}
zipInputStream.closeEntry();
}
} catch (Exception e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
// 删除临时 ZIP 文件
tempZipFile.delete();
LogUtils.d(TAG, "已更新 BoBullToon 数据");
return true;
} catch (Exception e) {
ToastUtils.show(e.getMessage());
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
return false;
}
}
public boolean downloadBoBullToon() {
String zipUrl = Rules.getInstance(mContext).getBoBullToonURL(); // 替换为实际的 ZIP 文件 URL
String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径
try {
// 删除旧文件
File fOldFolder = new File(destinationFolder);
if (fOldFolder.exists()) {
deleteFolderRecursive(fOldFolder);
fOldFolder.mkdirs();
LogUtils.d(TAG, "已清空 BoBullToon 数据");
}
// 更新新文件
if (downloadAndExtractZip(zipUrl, destinationFolder)) {
LogUtils.d(TAG, "ZIP 文件下载并解压成功。");
return true;
}
return false;
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
return false;
}
// 递归删除文件夹及其内容的方法
public static void deleteFolderRecursive(File file) {
// 判断是否为文件夹
if (file.isDirectory()) {
// 列出文件夹中的所有文件和子文件夹
File[] files = file.listFiles();
if (files != null) {
// 遍历并递归删除每个文件和子文件夹
for (File f : files) {
deleteFolderRecursive(f);
}
}
}
// 删除文件或空文件夹
file.delete();
}
File getWorkingFolder() {
return mContext.getExternalFilesDir(TAG);
}
public File getBoBullToonDataFolder() {
File fCheckRoot = getWorkingFolder();
if (fCheckRoot == null || !fCheckRoot.exists()) {
return fCheckRoot;
}
// 递归查找符合条件的文件夹
File targetFolder = findTargetFolder(fCheckRoot);
return targetFolder != null ? targetFolder : fCheckRoot;
}
/**
* 递归查找同时包含LICENSE和README.md文件的文件夹
*/
private File findTargetFolder(File currentFolder) {
// 检查当前文件夹是否符合条件
if (hasRequiredFiles(currentFolder)) {
return currentFolder;
}
// 查找子文件夹(Java 7不支持方法引用,用匿名内部类过滤)
File[] subFolders = currentFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory(); // 仅保留子文件夹
}
});
if (subFolders != null) {
for (File subFolder : subFolders) {
File result = findTargetFolder(subFolder);
if (result != null) {
return result;
}
}
}
return null;
}
/**
* 检查文件夹中是否同时存在LICENSE和README.md文件
*/
private boolean hasRequiredFiles(File folder) {
if (folder == null || !folder.isDirectory()) {
return false;
}
// 检查两个文件是否同时存在且均为文件(非文件夹)
File licenseFile = new File(folder, "LICENSE");
File readmeFile = new File(folder, "README.md");
return licenseFile.exists() && licenseFile.isFile()
&& readmeFile.exists() && readmeFile.isFile();
}
public void cleanBoBullToon() {
String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径
// 删除旧文件
File fOldFolder = new File(destinationFolder);
if (fOldFolder.exists()) {
deleteFolderRecursive(fOldFolder);
fOldFolder.mkdirs();
}
ToastUtils.show("已清空 BoBullToon 数据!");
LogUtils.d(TAG, "已清空 BoBullToon 数据");
}
public boolean loadPhoneBoBullToon() {
listPhoneBoBullToon.clear();
File fBoBullToon = getBoBullToonDataFolder();
if (fBoBullToon.exists()) {
LogUtils.d(TAG, String.format("getBoBullToonDataFolder() %s", getWorkingFolder()));
for (File userFolder : fBoBullToon.listFiles()) {
if (userFolder.isDirectory()) {
for (File recordFile : userFolder.listFiles()) {
listPhoneBoBullToon.add(recordFile.getName());
}
}
}
for (int i = 0; i < listPhoneBoBullToon.size(); i++) {
LogUtils.d(TAG, String.format("listPhoneBoBullToon add : %s", listPhoneBoBullToon.get(i)));
}
return true;
} else {
LogUtils.d(TAG, "fBoBullToon not exists。");
}
return false;
}
public boolean isPhoneBoBullToon(String phone) {
for (int i = 0; i < listPhoneBoBullToon.size(); i++) {
LogUtils.d(TAG, String.format("isPhoneBoBullToon(...) get(i) phone : %s", listPhoneBoBullToon.get(i)));
if (listPhoneBoBullToon.get(i).equals(phone)) {
return true;
}
}
return false;
}
}

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