Compare commits

...

65 Commits

Author SHA1 Message Date
LaizyBoy
042ba4b0e9 Remove ADsControlView from SettingsActivity layout
The class  was referenced in  but not present in the current module, causing an InflateException at runtime. The tag has been commented out to prevent the crash while preserving the UI layout structure. No other source files were changed.
2026-07-01 14:53:43 +08:00
50a06f028c 修复git管理脚本 2026-07-01 04:54:11 +08:00
2aed435668 修复git管理脚本 2026-07-01 04:19:41 +08:00
506a8da12c Update build metadata and clean up menu handling
- Bump libappbase to 15.20.34 and libaes to 15.20.17
- Increment buildCount in winboll/build.properties and libwinboll/build.properties (0 → 4)
- Update timestamps to current date
- Apply Java 7 source/target level
- Remove unused library activity menu item from MainActivity
2026-07-01 03:25:50 +08:00
1e40883810 修复编译参数配置 2026-07-01 03:17:14 +08:00
ea90877e6b 移除 libwinboll模块 2026-07-01 03:12:18 +08:00
1bec3dc08d Update build artifacts and dependency versions
- Bump  to 15.20.34 and  to 15.20.17 in libwinboll build.gradle (new release of the core libraries).
- Revise build timestamps in libwinboll/build.properties and winbolt/build.properties to current date.
- Increment  from 0 to 4, reflecting four incremental builds made since last snapshot.
2026-07-01 02:10:49 +08:00
ebd9b64eea <winboll>APK 15.20.8 release Publish. 2026-06-24 08:11:15 +08:00
40f8170751 升级 libappbase/libaes 依赖至最新版,移除米盟 SDK 配置
- cc.winboll.studio:libappbase 15.20.25 → 15.20.33
- cc.winboll.studio:libaes    15.20.14 → 15.20.16
- 注释米盟广告 SDK (mimo-ad-sdk) 依赖及相关 packagingOptions 配置
2026-06-24 06:57:34 +08:00
da92eb7dee 添加--no-daemon参数,解决在Termux环境下编译调用java资源环境不同的问题。 2026-06-11 21:16:33 +08:00
07c3c2e967 添加Termux终端编译发布脚本 2026-06-11 20:48:12 +08:00
Studio
4ac78cd63b <winboll>APK 15.20.7 release Publish. 2026-06-03 07:32:48 +08:00
16c3153d95 改进应用创建函数,提高应用调试能力。 2026-06-03 07:30:38 +08:00
3e65cbc326 更新类库 2026-06-03 07:16:44 +08:00
Studio
97f036bf5e <winboll>APK 15.20.6 release Publish. 2026-06-02 20:00:52 +08:00
76d93acdd5 feat(MyTermuxActivity): 创建桌面快捷方式时使用按钮自定义图标
新增 loadButtonIcon() 加载模型 iconPath 对应图片,
API 26+ 用 Icon.createWithBitmap,旧 API 用 EXTRA_SHORTCUT_ICON,
无图标时回退 ic_menu_manage。
2026-06-02 19:57:29 +08:00
7219fd0c87 feat(TermuxButton): 添加按钮自定义图标功能
- TermuxButtonModel 新增 iconPath 字段及 JSON 序列化
- MyTermuxActivity 编辑对话框增加图标导入/预览/清除
- 列表项改为 LinearLayout,图标 MATCH_PARENT 撑满高度,5dp 边距
- 图标文件保存在 getFilesDir()/termux_icons/
- 新增 strings 国际化资源(中/英)
2026-06-02 19:53:16 +08:00
756cf88b55 feat(MyTermuxActivity): 添加桌面快捷方式及指纹验证功能
- 列表项长按菜单新增"创建桌面快捷方式"
- 支持 ShortcutManager(API 26+) 和 INSTALL_SHORTCUT 广播两种方式
- TermuxCommandExecutor 所有命令执行前增加指纹验证
- 自定义AlertDialog显示命令信息(名称蓝色粗体),通过后启动BiometricPrompt
- 列表项名称蓝色粗体显示
- 新增 EXTRA_SESSION_ACTION 确保每次创建新终端会话
- MyTermuxActivity 添加 shortcut 意图处理(onCreate/onNewIntent)
2026-06-02 19:32:01 +08:00
ac8b789bcb fix(Termux): 修复按钮命令未执行及返回值反转问题
- 移除 stdbuf bash 占位,改为执行命令后进入交互终端
- 替换 \n 为 ; 以支持多行命令分段执行
- 修复 executeTerminalCommand 返回值反转逻辑
- MyTermuxActivity列表文字颜色 white→black 适配浅色主题
2026-06-02 18:09:37 +08:00
bac0a957aa refactor: 将 MyTermuxActivity 移至 termux 包并清理空目录 2026-06-02 17:01:16 +08:00
acfd4744f8 更新菜单排列方式 2026-06-02 09:33:59 +08:00
e15c8076de 更新类库 2026-06-02 09:14:33 +08:00
375635436b Merge remote-tracking branch 'origin/projects_keeper_tag' into winboll 2026-06-02 08:57:30 +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
STUDIO
e8c5cefeac <winboll>APK 15.20.5 release Publish. 2026-06-02 03:18:21 +08:00
85fb42ca97 fix: 修复主题切换时 IndexOutOfBoundsException 崩溃
- App.onCreate() 中调用 AESThemeUtil.init() 注入当前应用的
  R.style.* 主题ID列表(按 ThemeType.ordinal() 顺序排列),
  避免 Jitpack AESThemeUtil 内部 ArrayList 为空导致越界崩溃
- PatternLockActivity / SettingsActivity 删除冗余的
  AESThemeUtil.applyAppTheme(this) 调用(父类 BaseWinBoLLActivity
  已在 onCreate 中通过 setThemeStyle() 处理主题设置)
2026-06-02 03:15:35 +08:00
qinglong
ae63d1ec0a 合并模块AES 同步最新时间标签aes-v15.20.11 2026-06-02 03:00:01 +08:00
f99632cbea Merge branch 'winboll' into merge 2026-06-02 02:58:06 +08:00
c8ef451232 Merge remote-tracking branch 'origin/projects_keeper_tag' into merge 2026-06-02 02:57:59 +08:00
92e59bdb9e 更新类库 2026-06-02 02:55:10 +08:00
9ce03ea542 更新类库 2026-06-02 02:31:39 +08:00
qinglong
9e9486b488 合并模块WinBoLL 同步最新时间标签winboll-v15.20.4 2026-06-01 21:00:01 +08:00
STUDIO
b5d4036d6d <winboll>APK 15.20.4 release Publish. 2026-06-01 20:31:36 +08:00
qinglong
4b8967b253 合并模块WinBoLL 同步最新时间标签winboll-v15.20.3 2026-05-31 21:00:02 +08:00
25daecd8b5 <winboll>APK 15.20.3 release Publish. 2026-05-31 20:40:44 +08:00
7b48ca8fee 去掉应用自定义调试逻辑判断。 2026-05-31 20:39:20 +08:00
26f247b409 <winboll>APK 15.20.2 release Publish. 2026-05-31 20:28:41 +08:00
59080de7f3 更新类库,修复工具栏风格设置问题。 2026-05-27 20:33:43 +08:00
c3f84afb62 Merge remote-tracking branch 'origin/projects_keeper_tag' into winboll 2026-05-27 20:27:10 +08:00
qinglong
b1059c3f46 合并模块AES 同步最新时间标签aes-v15.20.10 2026-05-27 20:26:41 +08:00
00220a382d Merge remote-tracking branch 'origin/projects_keeper_tag' into winboll 2026-05-27 20:26:14 +08:00
qinglong
f3d723fbee 合并模块APPBase 同步最新时间标签appbase-v15.20.22 2026-05-27 15:00:01 +08:00
d0e70407f9 修改应用包权限设置更新说明 2026-05-24 11:17:27 +08:00
79eb4e3247 更新类库 2026-05-24 11:08:06 +08:00
71e6f1f03f Merge remote-tracking branch 'origin/projects_keeper_tag' into winboll 2026-05-24 11:00:11 +08:00
qinglong
e3c30ea9a3 合并模块AES 同步最新时间标签aes-v15.20.9 2026-05-24 10:49:26 +08:00
fdfae270d2 Merge remote-tracking branch 'origin/projects_keeper_tag' into winboll 2026-05-24 10:26:27 +08:00
qinglong
ab4cef21f0 合并模块APPBase 同步最新时间标签appbase-v15.20.21 2026-05-24 10:00:01 +08:00
2754a2ad7c 合并模块AES 同步最新时间标签aes-v15.20.8 2026-05-20 21:00:01 +08:00
d147f9dc08 更新类库版本,使用GitHub推向https://jitpack.io网站编译的类库仓库 2026-05-20 20:21:35 +08:00
8876896cbc 合并模块APPBase 同步最新时间标签appbase-v15.20.20 2026-05-20 17:00:01 +08:00
28e6a8ee78 合并模块APPBase 同步最新时间标签appbase-v15.20.19 2026-05-20 15:00:01 +08:00
c9272b6341 更新联系邮箱 2026-05-20 11:20:54 +08:00
7d872fd14c 合并模块APPBase 同步最新时间标签appbase-v15.20.18 2026-05-20 04:00:01 +08:00
d6fab2133f 合并模块AES 同步最新时间标签aes-v15.20.7 2026-05-19 21:00:01 +08:00
45821b8daa 合并模块APPBase 同步最新时间标签appbase-v15.20.17 2026-05-19 21:00:01 +08:00
0b5eea447b 更新基础类 2026-05-17 16:23:52 +08:00
6dbcca8fad Merge branch 'projects_keeper_tag' into winboll 2026-05-17 16:22:53 +08:00
68ebd0181b Merge branch 'projects-keeper' into winboll 2026-05-16 17:47:48 +08:00
2bf62a510a Merge branch 'projects-keeper' into winboll 2026-05-16 15:36:36 +08:00
fcc81c6e1f Restore the deleted library file 2026-05-15 20:15:27 +08:00
e813afe8b1 中文:进入 WinBoLL 分支开发阶段
英文:Entering the development stage of the WinBoLL branch
2026-05-15 20:07:12 +08:00
LaizyBoy
a8b4d31fa6 fix: 补充borderCornerRadius属性定义修复编译资源链接失败
在 winboll/src/main/res/values/attrs.xml 中添加缺失的
borderCornerRadius 属性声明,该属性被 bg_container_border.xml
引用但未在当前激活的模块中定义,导致 assembleBetaDebug 编译
时 AAPT 报错 resource attr/borderCornerRadius not found。
2026-05-15 14:15:33 +08:00
Transformers
99479ac830 修改文档:将 OriginMaster 重命名为 Projects_Keeper 2026-05-15 13:57:35 +08:00
818 changed files with 2216 additions and 69111 deletions

44
.gitignore vendored
View File

@@ -98,4 +98,46 @@ lint-results.html
/winboll.properties
/local.properties
/settings.gradle
/gradle.properties
/gradle.properties
## WinBoLL 项目配置
#.git
#.gitignore
#.gitmodules
#.gradle
#.winboll
#GenKeyStore
#LICENSE
#LICENSE-Private-Demo
#LICENSE-Private-Demo_docs
#README.md
#aes
#appbase
appkey.jks
appkey.keystore
autonfc
#build.gradle
contacts
debugtemp
gallery
gpsrelaysentinel
#gradle
gradle.properties
#gradle.properties-android-demo
#gradle.properties-androidx-demo
#gradlew
#libaes
#libappbase
libdebugtemp
libgpsrelaysentinel
#libwinboll
local.properties
#local.properties-demo
mymessagemanager
positions
powerbell
settings.gradle
#settings.gradle-demo
#winboll
winboll.properties
#winboll.properties-demo

View File

@@ -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

View File

@@ -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')

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}

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

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}"
}
}
}

View File

@@ -9,15 +9,15 @@ WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与
#### **仓库类型:功能说明**
☆ 基础项目分支 WinBoLL手机端安卓应用开发基础模板。
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
☆ 源码汇总管理 Projects_Keeper**:各类分支源码合并存档,不适宜作为开发库使用。
### 3. 源码合并管理推送路线图
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
★ WinBoLL → APPBase → OriginMaster
★ WinBoLL → AES → OriginMaster
★ WinBoLL → PowerBell → OriginMaster
★ WinBoLL → Positions → OriginMaster
★ WinBoLL → APPBase → Projects_Keeper
★ WinBoLL → AES → Projects_Keeper
★ WinBoLL → PowerBell → Projects_Keeper
★ WinBoLL → Positions → Projects_Keeper
## 二、WinBoLL 项目核心信息

View File

@@ -20,7 +20,7 @@ WinBoLL AndroidX 可视化元素类库。
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request

View File

@@ -26,7 +26,10 @@ android {
applicationId "cc.winboll.studio.aes"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
//1. Android 官方规则
//-  versionCode  类型int 整型
//- Java int 最大值2147483647
versionCode 1520000
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 19 01:52:47 HKT 2026
stageCount=7
#Tue Jun 02 08:54:20 HKT 2026
stageCount=13
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.6
publishVersion=15.20.12
buildCount=0
baseBetaVersion=15.20.7
baseBetaVersion=15.20.13

View File

@@ -5,10 +5,11 @@ 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.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import java.util.ArrayList;
public class App extends GlobalApplication {
@@ -18,8 +19,7 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
AESThemeUtil.init(null);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架

View File

@@ -84,11 +84,12 @@ public class MainActivity extends DrawerFragmentActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar_main, menu);
// if(App.isDebugging()) {
// getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_studio_debug, menu);
// }
return super.onCreateOptionsMenu(menu);
return true;
}
@Override

View File

@@ -20,7 +20,7 @@ WinBoLL 安卓手机端安卓应用开发基础类库。
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 19 18:54:18 HKT 2026
stageCount=17
#Wed May 27 14:51:29 HKT 2026
stageCount=23
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.16
publishVersion=15.20.22
buildCount=0
baseBetaVersion=15.20.17
baseBetaVersion=15.20.23

View File

@@ -37,7 +37,7 @@ public class AboutActivity extends Activity {
appInfo.setAppName("APPBase");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitName("APPBase");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);

View File

@@ -22,12 +22,6 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
// 如果应用不在调试状态,就根据编译类型设置调试状态
if (isDebugging() != true) {
setIsDebugging(BuildConfig.DEBUG);
}
// release 版调试码
//setIsDebugging(!BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());

View File

@@ -2,8 +2,8 @@ package cc.winboll.studio.appbase.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
import java.io.IOException;
/**

1
autonfc/.gitignore vendored
View File

@@ -1 +0,0 @@
/build

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/)
#### 参考文档

View File

@@ -1 +0,0 @@

View File

@@ -1,119 +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.autonfc"
minSdkVersion 23
// 适配MIUI12
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.11"
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'
// 下拉控件
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'
// OkHttp网络请求
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
// AndroidX 类库
/*api 'androidx.appcompat:appcompat:1.1.0'
//api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'*/
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.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'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
/*
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
implementation 'com.termux:terminal-emulator:0.118.0'
implementation 'com.termux:terminal-view:0.118.0'
implementation 'com.termux:termux-shared:0.118.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.15.2'
api 'cc.winboll.studio:libappbase:15.15.11'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
api fileTree(dir: 'libs', include: ['*.jar'])
}

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

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

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AutoNFC✌</string>
</resources>

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>

View File

@@ -1,344 +0,0 @@
package cc.winboll.studio.autonfc;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@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);
}
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
}
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
public void registerGlobal(Context context, String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
}
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
}
}
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
e.printStackTrace();
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable(){
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
try {
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
writeLog(e.toString());
}
throwable.printStackTrace();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) builder.append("\n");
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(Log.getStackTraceString(throwable));
return builder.toString();
}
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
} catch (Throwable e) {
e.printStackTrace();
}
}
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.copy:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
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>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:gravity="center_vertical|center_horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NFC Interface Activity"
android:onClick="onNFCInterfaceActivity"/>
</LinearLayout>
</LinearLayout>

View File

@@ -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>

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>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

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>

View File

@@ -1,4 +0,0 @@
<resources>
<string name="app_name">AutoNFC</string>
</resources>

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>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>

View File

@@ -1,40 +0,0 @@
# Contacts
源码参考自:
https://github.com/aJIEw/PhoneCallApp.git
#### 介绍
这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 gradle assembleStageRelease
#### 使用说明
在安卓系统中需要设置两个权限允许。
1.自启动权限允许。
2.省电策略-无限制权限允许。
#### 参与贡献
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/)
#### 参考文档

View File

@@ -1 +0,0 @@

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'])
}

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

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

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>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Contacts+</string>
</resources>

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>

View File

@@ -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");
}
}
});
}
// ====================== 栈优化与工具方法 ======================
/**
* 清理栈中所有无效Activitynull/已销毁/已结束),优化小米机型内存占用
*/
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;
}
}

View File

@@ -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();
}
}

View File

@@ -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回调冲突
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();
}
}
});
}
}

View File

@@ -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: 云盾信息控件未初始化,刷新失败");
}
}
}

View File

@@ -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: 当前已有规则,跳过初始化");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,280 +0,0 @@
package cc.winboll.studio.contacts.dun;
import android.content.Context;
import cc.winboll.studio.contacts.activities.SettingsActivity;
import cc.winboll.studio.contacts.bobulltoon.TomCat;
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
import cc.winboll.studio.contacts.model.SettingsBean;
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.contacts.utils.ContactUtils;
import cc.winboll.studio.contacts.utils.IntUtils;
import cc.winboll.studio.contacts.utils.RegexPPiUtils;
import cc.winboll.studio.contacts.views.DunTemperatureView;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/21 06:15:10
* @Describe 云盾防御规则(双重校验锁单例模式)
*/
public class Rules {
public static final String TAG = "Rules";
// 单例核心volatile 保证多线程可见性,禁止指令重排
private static volatile Rules sInstance;
// 上下文需使用 ApplicationContext 避免内存泄漏
private static Context sApplicationContext;
ArrayList<PhoneConnectRuleBean> _PhoneConnectRuleModelList;
Context mContext;
SettingsBean mSettingsModel;
Timer mDunResumeTimer;
/**
* 私有化构造方法,禁止外部 new 实例
*/
private Rules(Context context) {
mContext = context.getApplicationContext();
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleBean>();
reload();
}
/**
* 获取单例实例(双重校验锁,线程安全)
* @param context 上下文,建议传入 ApplicationContext
* @return Rules 唯一实例
*/
public static Rules getInstance(Context context) {
// 第一次校验:无锁,提高性能
if (sInstance == null) {
// 加锁:保证多线程下仅初始化一次
synchronized (Rules.class) {
// 第二次校验:防止多线程并发时重复创建
if (sInstance == null) {
sInstance = new Rules(context);
}
}
}
return sInstance;
}
public void reload() {
LogUtils.d(TAG, "reload()");
loadRules();
loadDun();
setDunResumTimer();
}
public void setDunResumTimer() {
if (mDunResumeTimer != null) {
mDunResumeTimer.cancel();
}
// 盾牌恢复定时器
mDunResumeTimer = new Timer();
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsBean.MIN_INTRANGE, SettingsBean.MAX_INTRANGE);
mDunResumeTimer.schedule(new TimerTask() {
@Override
public void run() {
if (mSettingsModel.getDunCurrentCount() != mSettingsModel.getDunTotalCount()) {
LogUtils.d(TAG, String.format("当前防御值为%d最大防御值为%d", mSettingsModel.getDunCurrentCount(), mSettingsModel.getDunTotalCount()));
int newDunCount = mSettingsModel.getDunCurrentCount() + mSettingsModel.getDunResumeCount();
// 设置盾值在[0DunTotalCount]之内其他值一律重置为 DunTotalCount。
newDunCount = (newDunCount > mSettingsModel.getDunTotalCount()) ?mSettingsModel.getDunTotalCount(): newDunCount;
mSettingsModel.setDunCurrentCount(newDunCount);
LogUtils.d(TAG, String.format("设置防御值为%d", newDunCount));
saveDun();
// 一键更新所有 DunTemperatureView 实例的盾值
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
SettingsActivity.notifyDunInfoUpdate();
}
}
}, 1000, ss);
}
public void loadRules() {
_PhoneConnectRuleModelList.clear();
PhoneConnectRuleBean.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
}
public void saveRules() {
LogUtils.d(TAG, String.format("saveRules()"));
PhoneConnectRuleBean.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
}
public void resetDefaultBoBullToonURL() {
mSettingsModel.setBoBullToon_URL(TomCat.getInstance(mContext).getDefaultBobulltoonUrl());
saveDun();
}
public void setBoBullToonURL(String szUrl) {
mSettingsModel.setBoBullToon_URL(szUrl);
saveDun();
}
public String getBoBullToonURL() {
return mSettingsModel.getBoBullToon_URL();
}
public void loadDun() {
mSettingsModel = SettingsBean.loadBean(mContext, SettingsBean.class);
if (mSettingsModel == null) {
mSettingsModel = new SettingsBean();
SettingsBean.saveBean(mContext, mSettingsModel);
}
}
public void saveDun() {
LogUtils.d(TAG, String.format("saveDun()"));
SettingsBean.saveBean(mContext, mSettingsModel);
}
public boolean isAllowed(String phoneNumber) {
return isAllowed(phoneNumber, false);
}
public boolean isAllowed(String phoneNumber, boolean isTest) {
// 没有启用云盾,默认允许接通任何电话
if (!mSettingsModel.isEnableDun()) {
LogUtils.d(TAG, String.format("没有启用云盾默认允许接通任何电话。isAllowed(...) return true"));
return true;
}
// 云盾防御体系
boolean isDefend = false; // 盾牌是否生效
boolean isConnect = true; // 防御结果是否连接
// 进行盾牌层数预计缩减计算
int nDunCurrentCount = mSettingsModel.getDunCurrentCount() - 1;
LogUtils.d(TAG, String.format("nDunCurrentCount : %d", nDunCurrentCount));
// 如果盾值小于1则解除防御
if (!isDefend && nDunCurrentCount < 1) {
// 盾层为1以下防御解除
LogUtils.d(TAG, "盾层为1以下防御解除");
isDefend = true;
isConnect = true;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 正则运算预防针
if (!isDefend && !RegexPPiUtils.isPPiOK(phoneNumber)) {
LogUtils.d(TAG, "正则运算预防针生效。");
isDefend = true;
isConnect = false;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 限时特殊通道打开时返回连接
if (!isDefend && LimitedTimeSpecialChannelService.isServiceRunning()) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n and Limited Time Special Channel Service Is Running.", phoneNumber));
isDefend = true;
isConnect = true;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 检验拨不通号码群
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n Is In BoBullToon", phoneNumber));
isDefend = true;
isConnect = false;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 查询通讯录是否有该联系人
boolean isPhoneInContacts = ContactUtils.getInstance(mContext).isPhoneInContacts(mContext, phoneNumber);
if (!isDefend) {
if (isPhoneInContacts) {
LogUtils.d(TAG, String.format("Phone %s is in contacts.", phoneNumber));
isDefend = true;
isConnect = true;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
} else {
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", phoneNumber));
}
}
// 正则匹配规则名单校验
if (!isDefend) {
for (int i = 0; i < _PhoneConnectRuleModelList.size(); i++) {
if (_PhoneConnectRuleModelList.get(i).isEnable()) {
String regex = _PhoneConnectRuleModelList.get(i).getRuleText();
if (Pattern.matches(regex, phoneNumber)) {
LogUtils.d(TAG, String.format("Phone Number [%s] is matched by rule : %s", phoneNumber, _PhoneConnectRuleModelList.get(i)));
isDefend = true;
isConnect = _PhoneConnectRuleModelList.get(i).isAllowConnection();
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
break;
}
}
}
}
// 如果不是规则测试时,就执行云盾防御机能。
if (isTest == false) {
if (isConnect) {
// 如果防御结果为连接,则恢复防御盾牌最大值层数
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
LogUtils.d(TAG, String.format("防御结果为连接,恢复防御盾牌最大值层数 %d", mSettingsModel.getDunTotalCount()));
saveDun();
SettingsActivity.notifyDunInfoUpdate();
} else if (isDefend) {
// 如果触发了以上某个防御模块,减少防御盾牌层数
int newDunCount = nDunCurrentCount;
LogUtils.d(TAG, String.format("新的防御层数预计为 %d", newDunCount));
// 保证盾值在[1DunTotalCount]之内其他值一律重置为 DunTotalCount。
if (newDunCount > 0 && newDunCount < mSettingsModel.getDunTotalCount()) {
mSettingsModel.setDunCurrentCount(newDunCount);
LogUtils.d(TAG, String.format("设置防御层数为 %d", newDunCount));
} else {
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
LogUtils.d(TAG, String.format("盾值不在[0%d]区间,恢复防御最大值%d", mSettingsModel.getDunTotalCount(), mSettingsModel.getDunTotalCount()));
}
saveDun();
SettingsActivity.notifyDunInfoUpdate();
}
// 一键更新所有 DunTemperatureView 实例的盾值
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
}
// 返回校验结果
LogUtils.d(TAG, String.format("返回校验结果 isConnect == %s", isConnect));
return isConnect;
}
public void add(String szPhoneConnectRule, boolean isAllowConnection, boolean isEnable) {
_PhoneConnectRuleModelList.add(new PhoneConnectRuleBean(szPhoneConnectRule, isAllowConnection, isEnable));
}
public ArrayList<PhoneConnectRuleBean> getPhoneBlacRuleBeanList() {
return _PhoneConnectRuleModelList;
}
public SettingsBean getSettingsModel() {
return mSettingsModel;
}
/**
* 可选:释放单例资源(如退出应用时调用)
*/
public static void releaseInstance() {
if (sInstance != null) {
sInstance.mDunResumeTimer.cancel();
sInstance._PhoneConnectRuleModelList.clear();
sInstance.mSettingsModel = null;
sInstance.mContext = null;
sInstance = null;
}
}
}

View File

@@ -1,258 +0,0 @@
package cc.winboll.studio.contacts.fragments;
import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.CallLog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.CallLogAdapter;
import cc.winboll.studio.contacts.model.CallLogModel;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/20 12:57:00
* @Describe 通话记录区域视图(支持懒加载,仅切换到当前页才加载数据)
*/
public class CallLogFragment extends Fragment {
// ====================== 常量定义区 ======================
public static final String TAG = "CallLogFragment";
public static final int MSG_UPDATE = 1;
private static final String ARG_PAGE = "ARG_PAGE";
private static final int REQUEST_READ_CALL_LOG = 1;
// ====================== 静态成员区 ======================
static volatile CallLogFragment _CallLogFragment;
// ====================== 页面参数区 ======================
private int mPage;
// ====================== UI控件与适配器区 ======================
private RecyclerView recyclerView;
private CallLogAdapter callLogAdapter;
private List<CallLogModel> callLogList = new ArrayList<CallLogModel>();
// ====================== 业务逻辑成员区 ======================
private Handler mHandler;
// 懒加载标记记录当前Fragment是否已初始化数据避免重复加载
private boolean isDataInited = false;
// ====================== 单例与实例化函数区 ======================
CallLogFragment() {
super();
}
public static CallLogFragment newInstance(int page) {
LogUtils.d(TAG, "newInstance: 创建通话记录Fragment实例页码=" + page);
Bundle args = new Bundle();
args.putInt(ARG_PAGE, page);
CallLogFragment fragment = new CallLogFragment();
fragment.setArguments(args);
_CallLogFragment = fragment;
return fragment;
}
// ====================== 生命周期函数区 ======================
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: Fragment创建开始");
if (getArguments() != null) {
mPage = getArguments().getInt(ARG_PAGE);
LogUtils.d(TAG, "onCreate: 读取页面参数mPage=" + mPage);
}
// Java7 兼容移除Lambda使用匿名内部类初始化Handler
mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_UPDATE) {
LogUtils.d(TAG, "handleMessage: 收到更新消息,开始读取通话记录");
readCallLog();
}
}
};
LogUtils.d(TAG, "onCreate: Fragment创建完成");
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
return inflater.inflate(R.layout.fragment_call_log, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
LogUtils.d(TAG, "onViewCreated: 视图创建完成,仅初始化控件(不加载数据)");
// 初始化RecyclerView仅绑定控件、设置布局管理器不设置数据/发起请求)
recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
// 初始化适配器(传入空列表,后续懒加载时更新数据)
callLogAdapter = new CallLogAdapter(getContext(), callLogList);
recyclerView.setAdapter(callLogAdapter);
LogUtils.d(TAG, "onViewCreated: RecyclerView控件初始化完成未加载数据");
}
@Override
public void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume: Fragment进入前台");
// 已初始化过数据 → 仅刷新(避免重复初始化,优化性能)
if (isDataInited && callLogAdapter != null) {
LogUtils.d(TAG, "onResume: 数据已初始化,仅刷新列表");
callLogAdapter.relaodContacts();
readCallLog(); // 刷新最新通话记录
LogUtils.d(TAG, "onResume: 通话记录数据刷新完成");
}
// 未初始化 → 不操作等待MainActivity调用initData触发初始化
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
LogUtils.d(TAG, "onDestroy: Handler消息已清空");
}
// 释放资源,避免内存泄漏
if (callLogList != null) {
callLogList.clear();
callLogList = null;
}
callLogAdapter = null;
recyclerView = null;
_CallLogFragment = null;
isDataInited = false;
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
}
// ====================== 权限回调函数区 ======================
@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_READ_CALL_LOG) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "onRequestPermissionsResult: 通话记录权限授予成功,开始加载数据");
mHandler.sendEmptyMessage(MSG_UPDATE);
} else {
LogUtils.e(TAG, "onRequestPermissionsResult: 通话记录权限被拒绝,无法加载数据");
}
}
}
// ====================== 懒加载核心方法供MainActivity调用 ======================
public void initData() {
// 避免重复初始化(双重防护:标记+判断)
if (isDataInited || getContext() == null) {
LogUtils.d(TAG, "initData: 数据已初始化/上下文为空,跳过");
return;
}
LogUtils.d(TAG, "initData: 开始懒加载初始化通话记录数据");
// 权限检查与数据加载原onViewCreated中的核心逻辑迁移至此
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
LogUtils.w(TAG, "initData: 读取通话记录权限未授予,发起权限申请");
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CALL_LOG}, REQUEST_READ_CALL_LOG);
} else {
LogUtils.d(TAG, "initData: 权限已授予,发送更新消息加载数据");
mHandler.sendEmptyMessage(MSG_UPDATE);
}
// 标记为已初始化(后续仅刷新,不重复初始化)
isDataInited = true;
LogUtils.d(TAG, "initData: 懒加载初始化流程完成");
}
// ====================== 业务核心函数区 ======================
private void readCallLog() {
LogUtils.d(TAG, "readCallLog: 开始读取系统通话记录");
// 避免空指针(懒加载场景下,控件可能未初始化完成)
if (callLogList == null || callLogAdapter == null || getContext() == null) {
LogUtils.w(TAG, "readCallLog: 控件/列表为空,跳过读取");
return;
}
callLogList.clear();
Cursor cursor = null;
try {
cursor = requireContext().getContentResolver().query(
CallLog.Calls.CONTENT_URI,
null,
null,
null,
CallLog.Calls.DATE + " DESC"
);
if (cursor != null) {
LogUtils.d(TAG, "readCallLog: 成功获取通话记录游标,数据条数=" + cursor.getCount());
while (cursor.moveToNext()) {
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
int callType = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
long callDateLong = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
Date callDate = new Date(callDateLong);
String callStatus = getCallStatus(callType);
callLogList.add(new CallLogModel(phoneNumber, callStatus, callDate));
}
callLogAdapter.notifyDataSetChanged();
LogUtils.d(TAG, "readCallLog: 通话记录数据解析完成,共" + callLogList.size() + "");
} else {
LogUtils.w(TAG, "readCallLog: 通话记录游标为空");
}
} catch (Exception e) {
LogUtils.e(TAG, "readCallLog: 读取通话记录异常", e);
} finally {
if (cursor != null) {
cursor.close();
LogUtils.d(TAG, "readCallLog: 游标已关闭");
}
}
}
private String getCallStatus(int callType) {
switch (callType) {
case CallLog.Calls.OUTGOING_TYPE:
return "Outgoing";
case CallLog.Calls.INCOMING_TYPE:
return "Incoming";
case CallLog.Calls.MISSED_TYPE:
return "Missed";
default:
return "Unknown";
}
}
// ====================== 外部调用函数区 ======================
public void triggerUpdate() {
LogUtils.d(TAG, "triggerUpdate: 外部触发通话记录更新");
if (isDataInited) { // 已初始化才触发更新(避免未加载时调用)
mHandler.sendEmptyMessage(MSG_UPDATE);
}
}
public static void updateCallLogFragment() {
if (_CallLogFragment != null) {
LogUtils.d(TAG, "updateCallLogFragment: 静态方法触发Fragment更新");
_CallLogFragment.triggerUpdate();
} else {
LogUtils.w(TAG, "updateCallLogFragment: Fragment实例为空无法更新");
}
}
}

View File

@@ -1,401 +0,0 @@
package cc.winboll.studio.contacts.fragments;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.ContactAdapter;
import cc.winboll.studio.contacts.model.ContactModel;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/08/30 14:32
* @Describe 联系人区域视图(支持懒加载,仅切换到当前页才加载数据)
*/
public class ContactsFragment extends Fragment {
// ====================== 常量定义区 ======================
public static final String TAG = "ContactsFragment";
private static final String ARG_PAGE = "ARG_PAGE";
private static final int REQUEST_READ_CONTACTS = 1;
private static final long DEBOUNCE_DELAY = 300; // 搜索防抖延迟
// ====================== 静态缓存区 ======================
// 全局复用联系人数据,减少重复查询
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
// ====================== 页面参数区 ======================
private int mPage;
private boolean isViewInitialized = false; // 视图初始化标记(控件绑定完成)
private boolean isDataLoaded = false; // 数据加载标记(数据+功能初始化完成)
private boolean isLazyInitCompleted = false; // 懒加载总标记供MainActivity判断
// ====================== UI控件区 ======================
private RecyclerView recyclerView;
private ContactAdapter contactAdapter;
private EditText searchEditText;
private Button btnDial;
// ====================== 数据容器区 ======================
private List<ContactModel> contactList = new ArrayList<ContactModel>();
private List<ContactModel> originalContactList = new ArrayList<ContactModel>();
// ====================== 异步工具区 ======================
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
// ====================== 实例化函数区 ======================
public static ContactsFragment newInstance(int page) {
LogUtils.d(TAG, "newInstance: 创建联系人Fragment实例页码=" + page);
Bundle args = new Bundle();
args.putInt(ARG_PAGE, page);
ContactsFragment fragment = new ContactsFragment();
fragment.setArguments(args);
return fragment;
}
// ====================== 生命周期函数区 ======================
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: Fragment创建开始");
if (getArguments() != null) {
mPage = getArguments().getInt(ARG_PAGE);
LogUtils.d(TAG, "onCreate: 读取页面参数mPage=" + mPage);
}
LogUtils.d(TAG, "onCreate: Fragment创建完成");
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
return inflater.inflate(R.layout.fragment_contacts, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
LogUtils.d(TAG, "onViewCreated: 开始初始化UI控件仅绑定不加载数据/功能)");
// 初始化RecyclerView仅绑定控件、设适配器隐藏列表
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
contactAdapter = new ContactAdapter(getActivity(), contactList);
recyclerView.setAdapter(contactAdapter);
recyclerView.setVisibility(View.GONE);
// 绑定搜索框和拨号按钮(仅赋值,不显示、不绑定事件)
searchEditText = (EditText) view.findViewById(R.id.search_edit_text);
btnDial = (Button) view.findViewById(R.id.btn_dial);
searchEditText.setVisibility(View.GONE);
btnDial.setVisibility(View.GONE);
// 标记视图控件绑定完成
isViewInitialized = true;
LogUtils.d(TAG, "onViewCreated: UI控件初始化完成未加载数据/功能)");
}
@Override
public void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume: Fragment进入前台");
// 已完成懒加载 → 仅恢复缓存数据(切回页面时刷新)
if (isLazyInitCompleted && isDataLoaded) {
LogUtils.d(TAG, "onResume: 懒加载已完成,恢复缓存数据");
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
}
// 未完成懒加载 → 不操作等待MainActivity调用initData触发
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
executor.shutdown(); // 关闭线程池
mainHandler.removeCallbacksAndMessages(null); // 清空Handler任务
// 释放本地数据引用(保留静态缓存,全局复用)
if (contactList != null) {
contactList.clear();
contactList = null;
}
if (originalContactList != null) {
originalContactList.clear();
originalContactList = null;
}
// 重置标记
isViewInitialized = false;
isDataLoaded = false;
isLazyInitCompleted = false;
LogUtils.d(TAG, "onDestroy: 异步工具+本地资源已释放");
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
LogUtils.d(TAG, "onHiddenChanged: Fragment隐藏状态变更hidden=" + hidden);
// 已完成懒加载+显示状态 → 恢复缓存数据兼容Tab切换场景
if (!hidden && isLazyInitCompleted && isDataLoaded) {
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "onHiddenChanged: 恢复缓存数据,列表已显示");
}
}
// ====================== 权限相关函数区 ======================
private void checkContactPermission() {
LogUtils.d(TAG, "checkContactPermission: 检查联系人读取权限");
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
LogUtils.w(TAG, "checkContactPermission: 权限未授予,发起申请");
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
} else {
LogUtils.d(TAG, "checkContactPermission: 权限已授予,开始加载数据");
loadContacts();
}
}
@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_READ_CONTACTS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "onRequestPermissionsResult: 联系人权限授予成功");
loadContacts();
} else {
LogUtils.e(TAG, "onRequestPermissionsResult: 联系人权限被拒绝");
ToastUtils.show("请授予联系人权限以查看联系人列表");
recyclerView.setVisibility(View.VISIBLE);
// 权限拒绝也标记懒加载完成(避免重复触发)
isLazyInitCompleted = true;
}
}
}
// ====================== 懒加载核心方法供MainActivity调用 ======================
public void initData() {
// 双重防护:避免重复初始化(标记+视图就绪判断)
if (isLazyInitCompleted || !isViewInitialized || getContext() == null) {
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
return;
}
LogUtils.d(TAG, "initData: 开始懒加载初始化(功能+数据)");
// 1. 初始化搜索、拨号功能原onResume首次进入逻辑迁移至此
initSearchAndDial();
// 2. 检查权限+加载数据原onResume首次进入逻辑迁移至此
checkContactPermission();
// 标记懒加载总流程完成(无论权限是否授予,仅执行一次)
isLazyInitCompleted = true;
LogUtils.d(TAG, "initData: 懒加载初始化流程启动完成");
}
// ====================== UI功能初始化区 ======================
private void initSearchAndDial() {
LogUtils.d(TAG, "initSearchAndDial: 初始化搜索和拨号功能");
// 显示控件
searchEditText.setVisibility(View.VISIBLE);
btnDial.setVisibility(View.VISIBLE);
// 搜索防抖监听
searchEditText.addTextChangedListener(new DebounceTextWatcher(DEBOUNCE_DELAY) {
@Override
public void onDebounceTextChanged(String query) {
filterContacts(query);
}
});
// 拨号按钮点击事件
btnDial.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String phoneNumber = searchEditText.getText().toString().replaceAll("\\s", "");
if (phoneNumber.isEmpty()) {
ToastUtils.show("请输入号码");
return;
}
LogUtils.d(TAG, "initSearchAndDial: 发起拨号,号码=" + phoneNumber);
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
LogUtils.d(TAG, "initSearchAndDial: 功能初始化完成");
}
// ====================== 数据加载与处理区 ======================
private void loadContacts() {
// 优先使用缓存数据(保留原有缓存逻辑,提升性能)
if (!sCachedOriginalList.isEmpty() && !sCachedFilteredList.isEmpty()) {
LogUtils.d(TAG, "loadContacts: 存在缓存数据,直接复用");
originalContactList.clear();
originalContactList.addAll(sCachedOriginalList);
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
isDataLoaded = true;
return;
}
// 无缓存时异步加载(保留原有异步逻辑,避免主线程阻塞)
if (!isDataLoaded) {
LogUtils.d(TAG, "loadContacts: 无缓存,异步读取联系人数据");
recyclerView.setVisibility(View.GONE);
executor.execute(new Runnable() {
@Override
public void run() {
final List<ContactModel> tempList = readContactsInBackground();
// 主线程更新UI和缓存
mainHandler.post(new Runnable() {
@Override
public void run() {
sCachedOriginalList.clear();
sCachedOriginalList.addAll(tempList);
sCachedFilteredList.clear();
sCachedFilteredList.addAll(tempList);
originalContactList.clear();
originalContactList.addAll(sCachedOriginalList);
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
isDataLoaded = true;
LogUtils.d(TAG, "loadContacts: 联系人数据加载完成,共" + contactList.size() + "");
}
});
}
});
}
}
private List<ContactModel> readContactsInBackground() {
LogUtils.d(TAG, "readContactsInBackground: 子线程读取联系人");
List<ContactModel> tempList = new ArrayList<ContactModel>();
Cursor cursor = null;
try {
cursor = requireContext().getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
},
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
do {
String name = cursor.getString(nameIndex);
String number = cursor.getString(numberIndex).replaceAll("\\s", "");
tempList.add(new ContactModel(name, number));
} while (cursor.moveToNext());
LogUtils.d(TAG, "readContactsInBackground: 成功读取" + tempList.size() + "条联系人数据");
} else {
LogUtils.w(TAG, "readContactsInBackground: 未读取到联系人数据");
}
} catch (Exception e) {
LogUtils.e(TAG, "readContactsInBackground: 读取联系人异常", e);
} finally {
if (cursor != null) {
cursor.close();
LogUtils.d(TAG, "readContactsInBackground: 游标已关闭");
}
}
return tempList;
}
private void filterContacts(String query) {
LogUtils.d(TAG, "filterContacts: 搜索过滤,关键词=" + query);
contactList.clear();
sCachedFilteredList.clear();
if (query.isEmpty()) {
contactList.addAll(originalContactList);
sCachedFilteredList.addAll(originalContactList);
} else {
String lowerQuery = query.toLowerCase();
for (ContactModel contact : originalContactList) {
boolean matchName = contact.getName().toLowerCase().contains(lowerQuery);
boolean matchPinyin = contact.getPinyin().toLowerCase().contains(lowerQuery);
boolean matchFirstLetter = contact.getPinyinFirstLetter().toLowerCase().contains(lowerQuery);
boolean matchNumber = contact.getNumber().contains(lowerQuery);
if (matchName || matchPinyin || matchFirstLetter || matchNumber) {
contactList.add(contact);
}
}
sCachedFilteredList.addAll(contactList);
}
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "filterContacts: 过滤完成,显示" + contactList.size() + "条数据");
}
// ====================== 内部防抖监听类 ======================
public abstract static class DebounceTextWatcher implements TextWatcher {
private final long debounceDelay;
private Handler handler = new Handler(Looper.getMainLooper());
private Runnable pendingRunnable;
public DebounceTextWatcher(long debounceDelay) {
this.debounceDelay = debounceDelay;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(final CharSequence s, int start, int before, int count) {
if (pendingRunnable != null) {
handler.removeCallbacks(pendingRunnable);
}
pendingRunnable = new Runnable() {
@Override
public void run() {
onDebounceTextChanged(s.toString());
}
};
handler.postDelayed(pendingRunnable, debounceDelay);
}
@Override
public void afterTextChanged(Editable s) {}
public abstract void onDebounceTextChanged(String query);
}
}

View File

@@ -1,118 +0,0 @@
package cc.winboll.studio.contacts.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/20 12:58:15
* @Describe 应用日志区域视图(支持懒加载,仅切换到当前页才启动日志)
*/
public class LogFragment extends Fragment {
// ====================== 常量定义区 ======================
public static final String TAG = "LogFragment";
private static final String ARG_PAGE = "ARG_PAGE";
// ====================== 页面参数区 ======================
private int mPage;
// ====================== UI控件区 ======================
private LogView mLogView;
// ====================== 懒加载标记区 ======================
private boolean isViewInitialized = false; // 视图控件绑定完成标记
private boolean isLazyInitCompleted = false; // 懒加载总流程完成标记
private boolean isLogViewStarted = false; // LogView启动状态标记
// ====================== 实例化函数区 ======================
public static LogFragment newInstance(int page) {
LogUtils.d(TAG, "newInstance: 创建日志Fragment实例页码=" + page);
Bundle args = new Bundle();
args.putInt(ARG_PAGE, page);
LogFragment fragment = new LogFragment();
fragment.setArguments(args);
return fragment;
}
// ====================== 生命周期函数区 ======================
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: Fragment创建开始");
if (getArguments() != null) {
mPage = getArguments().getInt(ARG_PAGE);
LogUtils.d(TAG, "onCreate: 读取页面参数mPage=" + mPage);
}
LogUtils.d(TAG, "onCreate: Fragment创建完成");
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
View view = inflater.inflate(R.layout.fragment_log, container, false);
// Java7 适配添加强制类型转换仅初始化LogView控件不启动
mLogView = (LogView) view.findViewById(R.id.logview);
LogUtils.d(TAG, "onCreateView: LogView控件初始化完成未启动");
// 标记视图控件绑定完成
isViewInitialized = true;
return view;
}
@Override
public void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume: Fragment进入前台");
// 已完成懒加载 → 仅重启LogView切回页面时恢复日志显示
if (isLazyInitCompleted && mLogView != null && !isLogViewStarted) {
mLogView.start();
isLogViewStarted = true;
LogUtils.d(TAG, "onResume: LogView已重启恢复日志显示");
}
// 未完成懒加载 → 不操作等待MainActivity调用initData触发
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
if (mLogView != null) {
// 若LogView有停止方法必须调用避免后台持续占用资源根据实际API调整
// mLogView.stop(); // 关键释放LogView资源防止内存泄漏
LogUtils.d(TAG, "onDestroy: LogView资源已释放");
}
// 重置所有标记,避免重建时状态异常
mLogView = null;
isViewInitialized = false;
isLazyInitCompleted = false;
isLogViewStarted = false;
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
}
// ====================== 懒加载核心方法供MainActivity调用 ======================
public void initData() {
// 双重防护:避免重复初始化(标记+视图就绪+控件非空)
if (isLazyInitCompleted || !isViewInitialized || mLogView == null || getContext() == null) {
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
return;
}
LogUtils.d(TAG, "initData: 开始懒加载初始化启动LogView");
// 核心启动LogView原onCreateView中的start逻辑迁移至此
mLogView.start();
isLogViewStarted = true;
// 标记懒加载总流程完成(仅执行一次)
isLazyInitCompleted = true;
LogUtils.d(TAG, "initData: 懒加载初始化完成LogView正常启动");
}
}

View File

@@ -1,38 +0,0 @@
package cc.winboll.studio.contacts.handlers;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/02/14 03:51:40
*/
import android.os.Handler;
import android.os.Message;
import cc.winboll.studio.contacts.services.MainService;
import java.lang.ref.WeakReference;
public class MainServiceHandler extends Handler {
public static final String TAG = "MainServiceHandler";
public static final int MSG_REMINDTHREAD = 0;
WeakReference<MainService> serviceWeakReference;
public MainServiceHandler(MainService service) {
serviceWeakReference = new WeakReference<MainService>(service);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REMINDTHREAD: // 处理下载完成消息更新UI
{
// 显示提醒消息
//
//LogUtils.d(TAG, "显示提醒消息");
MainService mainService = serviceWeakReference.get();
if (mainService != null) {
mainService.appenMessage((String)msg.obj);
}
break;
}
}
}
}

View File

@@ -1,392 +0,0 @@
package cc.winboll.studio.contacts.listenphonecall;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.model.MainServiceBean;
import cc.winboll.studio.contacts.phonecallui.PhoneCallActivity;
import cc.winboll.studio.contacts.phonecallui.PhoneCallService;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Describe 通话监听服务(无前台服务),负责监听通话状态、显示通话悬浮窗、跳转通话界面
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 避免内存泄漏
*/
public class CallListenerService extends Service {
// ====================== 常量定义区精准适配API29-30无冗余 ======================
public static final String TAG = "CallListenerService";
// Android版本常量仅保留适配必需版本精简无用定义
private static final int ANDROID_8_API = 26; // 悬浮窗类型适配API26+必需)
private static final int ANDROID_10_API = 29; // API29+ 悬浮窗权限/参数适配
private static final int ANDROID_19_API = 19; // 透明状态栏/导航栏适配
// 延迟初始化参数(让出主线程,避免启动阻塞)
private static final long DELAY_INIT_MS = 100L;
// ====================== 成员属性区(按功能归类,命名规范) ======================
// 延迟初始化核心
private Handler mDelayHandler; // 延迟处理器避免onCreate阻塞
// 通话监听核心
private TelephonyManager mTelephonyManager; // 电话管理器(监听通话状态)
private PhoneStateListener mPhoneStateListener;// 通话状态监听回调
private String mCallNumber; // 当前通话号码
private boolean mIsCallingIn; // 是否为来电true=来电false=去电)
// 悬浮窗核心
private WindowManager mWindowManager; // 窗口管理器(添加/移除悬浮窗)
private WindowManager.LayoutParams mWindowParams;// 悬浮窗参数配置
private View mPhoneCallView; // 通话悬浮窗根视图
private TextView mTvCallNumber; // 悬浮窗号码显示控件
private Button mBtnOpenApp; // 悬浮窗跳转APP按钮
private boolean mHasShown; // 悬浮窗显示状态标记(避免重复操作)
// ====================== Service生命周期方法区按执行顺序排列 ======================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 =====");
// 延迟初始化所有逻辑(让出主线程,避免启动阻塞,提升启动速度)
initDelayHandlerAndLogic();
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动完成 =====");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "onStartCommand: 服务被启动startId=" + startId);
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY自动重启" : "默认模式"));
return startMode;
}
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind: 服务无需绑定返回null");
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "===== onDestroy: 通话监听服务开始销毁 =====");
// 全量清理资源,彻底避免内存泄漏
dismissFloatWindow(); // 移除悬浮窗
unregisterPhoneStateListener();// 注销通话监听
clearDelayHandler(); // 清空延迟任务
resetAllReferences(); // 置空所有成员属性
LogUtils.d(TAG, "===== onDestroy: 通话监听服务销毁完成 =====");
}
// ====================== 延迟初始化方法区(非阻塞启动,提升稳定性) ======================
/**
* 初始化延迟处理器,执行核心逻辑(通话监听+悬浮窗)
*/
private void initDelayHandlerAndLogic() {
mDelayHandler = new Handler(Looper.getMainLooper());
mDelayHandler.postDelayed(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "initDelayHandlerAndLogic: 开始延迟初始化核心逻辑");
initPhoneStateListener(); // 初始化通话状态监听
initFloatWindow(); // 初始化通话悬浮窗
LogUtils.d(TAG, "initDelayHandlerAndLogic: 延迟初始化完成,服务就绪");
}
}, DELAY_INIT_MS);
}
/**
* 初始化通话状态监听注册TelephonyManager响应通话状态变化
*/
private void initPhoneStateListener() {
// 1. 创建通话状态监听回调
mPhoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int callState, String incomingNumber) {
super.onCallStateChanged(callState, incomingNumber);
mCallNumber = incomingNumber;
LogUtils.d(TAG, "onCallStateChanged: 通话状态变化,状态=" + getCallStateDesc(callState) + ",号码=" + incomingNumber);
// 响应不同通话状态
switch (callState) {
case TelephonyManager.CALL_STATE_IDLE:
// 通话空闲(挂断/未通话):隐藏悬浮窗
dismissFloatWindow();
break;
case TelephonyManager.CALL_STATE_RINGING:
// 来电响铃标记来电状态更新UI并显示悬浮窗
mIsCallingIn = true;
updateFloatWindowUI();
showFloatWindow();
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
// 通话中(接听/拨号更新UI并显示悬浮窗
updateFloatWindowUI();
showFloatWindow();
break;
}
}
};
// 2. 注册通话监听(非空校验,避免崩溃)
mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
if (mTelephonyManager != null) {
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
LogUtils.d(TAG, "initPhoneStateListener: 通话状态监听注册成功");
} else {
LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager获取失败监听注册失败");
}
}
/**
* 初始化通话悬浮窗(配置参数+加载布局+绑定事件适配API29-30
*/
private void initFloatWindow() {
// 1. 获取窗口管理器(非空校验,避免后续崩溃)
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
if (mWindowManager == null) {
LogUtils.e(TAG, "initFloatWindow: WindowManager获取失败悬浮窗初始化失败");
return;
}
// 2. 配置悬浮窗参数精准适配API29+,兼容悬浮窗权限)
initFloatWindowParams();
// 3. 加载悬浮窗布局(添加返回键拦截,避免误关闭)
FrameLayout keyInterceptorLayout = new FrameLayout(this) {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// 拦截返回键,保障通话时悬浮窗正常显示
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
LogUtils.d(TAG, "dispatchKeyEvent: 拦截悬浮窗返回键事件");
return true;
}
return super.dispatchKeyEvent(event);
}
};
mPhoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, keyInterceptorLayout);
// 4. 绑定悬浮窗控件,设置跳转按钮事件
bindFloatWindowViews();
LogUtils.d(TAG, "initFloatWindow: 悬浮窗初始化完成");
}
/**
* 配置悬浮窗参数适配API29+窗口类型,确保正常显示)
*/
private void initFloatWindowParams() {
mWindowParams = new WindowManager.LayoutParams();
// 窗口位置:顶部居中
mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
// 窗口大小:宽度全屏,高度自适应
mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT;
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 固定竖屏显示
mWindowParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
// 窗口格式:半透明
mWindowParams.format = PixelFormat.TRANSLUCENT;
// 窗口类型API29+ 强制用 TYPE_APPLICATION_OVERLAY需悬浮窗权限
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
LogUtils.d(TAG, "initFloatWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY需开启悬浮窗权限");
} else if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
// 窗口标志:无焦点(不抢占输入)、全屏布局、兼容透明状态栏/导航栏
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
if (Build.VERSION.SDK_INT >= ANDROID_19_API) {
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
}
}
/**
* 绑定悬浮窗控件,设置跳转通话详情页事件
*/
private void bindFloatWindowViews() {
mTvCallNumber = (TextView) mPhoneCallView.findViewById(R.id.tv_call_number);
mBtnOpenApp = (Button) mPhoneCallView.findViewById(R.id.btn_open_app);
// 跳转按钮点击事件
mBtnOpenApp.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(mCallNumber)) {
LogUtils.w(TAG, "bindFloatWindowViews: 通话号码为空,跳过跳转");
return;
}
LogUtils.d(TAG, "bindFloatWindowViews: 点击跳转通话详情页,号码=" + mCallNumber);
PhoneCallService.CallType callType = mIsCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT;
PhoneCallActivity.actionStart(CallListenerService.this, mCallNumber, callType);
}
});
}
// ====================== 悬浮窗功能逻辑区(显示/隐藏/更新UI ======================
/**
* 显示通话悬浮窗(避免重复添加,防止窗口泄露)
*/
private void showFloatWindow() {
if (!mHasShown && mPhoneCallView != null && mWindowManager != null) {
try {
mWindowManager.addView(mPhoneCallView, mWindowParams);
mHasShown = true;
LogUtils.d(TAG, "showFloatWindow: 悬浮窗显示成功");
} catch (SecurityException e) {
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示失败(无悬浮窗权限,需引导用户开启)", e);
} catch (Exception e) {
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示异常", e);
}
} else {
LogUtils.d(TAG, "showFloatWindow: 悬浮窗已显示/组件未初始化,跳过显示");
}
}
/**
* 隐藏通话悬浮窗(避免重复移除,防止崩溃)
*/
private void dismissFloatWindow() {
if (mHasShown && mPhoneCallView != null && mWindowManager != null) {
try {
mWindowManager.removeView(mPhoneCallView);
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗隐藏成功");
} catch (Exception e) {
LogUtils.e(TAG, "dismissFloatWindow: 悬浮窗隐藏异常", e);
} finally {
mHasShown = false;
mIsCallingIn = false; // 重置来电状态标记
}
} else {
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗已隐藏/组件未初始化,跳过隐藏");
}
}
/**
* 更新悬浮窗UI显示格式化号码+通话类型图标)
*/
private void updateFloatWindowUI() {
if (mTvCallNumber == null || TextUtils.isEmpty(mCallNumber)) {
LogUtils.w(TAG, "updateFloatWindowUI: 控件未初始化/号码为空,更新失败");
return;
}
// 格式化11位手机号3-4-4分隔提升可读性
String formattedNumber = formatPhoneNumber(mCallNumber);
mTvCallNumber.setText(formattedNumber);
// 设置通话类型图标(来电/去电区分)
int iconResId = mIsCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
mTvCallNumber.setCompoundDrawablesWithIntrinsicBounds(
null, null, getResources().getDrawable(iconResId), null
);
LogUtils.d(TAG, "updateFloatWindowUI: 悬浮窗UI更新完成号码=" + formattedNumber + ",类型=" + (mIsCallingIn ? "来电" : "去电"));
}
// ====================== 资源清理方法区(服务销毁时全量释放) ======================
/**
* 注销通话状态监听释放TelephonyManager资源
*/
private void unregisterPhoneStateListener() {
if (mTelephonyManager != null && mPhoneStateListener != null) {
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
LogUtils.d(TAG, "unregisterPhoneStateListener: 通话监听已注销");
}
mTelephonyManager = null;
mPhoneStateListener = null;
}
/**
* 清空延迟处理器(移除未执行任务,避免内存泄漏)
*/
private void clearDelayHandler() {
if (mDelayHandler != null) {
mDelayHandler.removeCallbacksAndMessages(null);
mDelayHandler = null;
LogUtils.d(TAG, "clearDelayHandler: 延迟处理器已清空");
}
}
/**
* 置空所有成员属性(彻底释放引用,避免内存泄漏)
*/
private void resetAllReferences() {
mCallNumber = null;
mPhoneCallView = null;
mWindowParams = null;
mWindowManager = null;
mTvCallNumber = null;
mBtnOpenApp = null;
}
// ====================== 工具方法区(通用辅助功能,独立归类) ======================
/**
* 格式化手机号11位手机号3-4-4分隔非11位保持原格式
* @param phoneNum 待格式化的手机号
* @return 格式化后的号码
*/
public static String formatPhoneNumber(String phoneNum) {
if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
String formatted = phoneNum.substring(0, 3) + "-"
+ phoneNum.substring(3, 7) + "-"
+ phoneNum.substring(7);
LogUtils.d(TAG, "formatPhoneNumber: 号码格式化,原=" + phoneNum + ",新=" + formatted);
return formatted;
}
LogUtils.d(TAG, "formatPhoneNumber: 非11位号码无需格式化号码=" + phoneNum);
return phoneNum;
}
/**
* 转换通话状态为文字描述(便于日志查看,快速定位问题)
* @param callState 通话状态TelephonyManager常量
* @return 状态描述文字
*/
private String getCallStateDesc(int callState) {
switch (callState) {
case TelephonyManager.CALL_STATE_IDLE:
return "空闲(挂断/未通话)";
case TelephonyManager.CALL_STATE_RINGING:
return "响铃(来电)";
case TelephonyManager.CALL_STATE_OFFHOOK:
return "通话中(接听/拨号)";
default:
return "未知状态";
}
}
}

View File

@@ -1,43 +0,0 @@
package cc.winboll.studio.contacts.model;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.Date;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/26 13:10:57
* @Describe 通话记录数据模型
*/
public class CallLogModel {
// ====================== 常量定义区 ======================
public static final String TAG = "CallLogModel";
// ====================== 成员变量区 ======================
private String phoneNumber;
private String callStatus;
private Date callDate;
// ====================== 构造函数区 ======================
public CallLogModel(String phoneNumber, String callStatus, Date callDate) {
// 去除号码中的空格并初始化
this.phoneNumber = phoneNumber.replaceAll("\\s", "");
this.callStatus = callStatus;
this.callDate = callDate;
LogUtils.d(TAG, "CallLogModel: 初始化通话记录模型 | 号码=" + this.phoneNumber
+ " | 状态=" + this.callStatus + " | 时间=" + this.callDate);
}
// ====================== Getter 方法区 ======================
public String getPhoneNumber() {
return phoneNumber;
}
public String getCallStatus() {
return callStatus;
}
public Date getCallDate() {
return callDate;
}
}

View File

@@ -1,135 +0,0 @@
package cc.winboll.studio.contacts.model;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/08/30 14:32
* @Describe 联系人信息数据模型,支持姓名转全拼和拼音首字母
*/
public class ContactModel {
// ====================== 常量定义区 ======================
public static final String TAG = "ContactModel";
// 汉字匹配正则常量,避免重复创建
private static final String CHINESE_CHAR_REGEX = "[\\u4e00-\\u9fa5]";
// ====================== 成员变量区 ======================
private String name;
private String number;
private String pinyin;
private String pinyinFirstLetter;
// ====================== 构造函数区 ======================
public ContactModel(String name, String number) {
LogUtils.d(TAG, "ContactModel: 开始初始化联系人模型");
this.name = name == null ? "" : name;
// 去除号码空格,空值处理为""
this.number = number == null ? "" : number.replaceAll("\\s", "");
// 初始化拼音和拼音首字母
this.pinyin = convertToPinyin(this.name);
this.pinyinFirstLetter = convertToPinyinFirstLetter(this.name);
LogUtils.d(TAG, "ContactModel: 联系人初始化完成 | 姓名=" + this.name
+ " | 号码=" + this.number + " | 全拼=" + this.pinyin
+ " | 拼音首字母=" + this.pinyinFirstLetter);
}
// ====================== 拼音转换工具方法区 ======================
/**
* 姓名转为全拼(多音字默认取首个拼音)
*/
private String convertToPinyin(String chinese) {
LogUtils.d(TAG, "convertToPinyin: 开始转换姓名为全拼,姓名=" + chinese);
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
StringBuilder pinyinSb = new StringBuilder();
for (int i = 0; i < chinese.length(); i++) {
char ch = chinese.charAt(i);
// 仅处理汉字
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
if (pinyinArray != null && pinyinArray.length > 0) {
pinyinSb.append(pinyinArray[0]);
LogUtils.v(TAG, "convertToPinyin: 字符[" + ch + "]转为拼音[" + pinyinArray[0] + "]");
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
LogUtils.e(TAG, "convertToPinyin: 拼音转换异常,字符=" + ch, e);
}
} else {
pinyinSb.append(ch);
LogUtils.v(TAG, "convertToPinyin: 非汉字字符直接拼接,字符=" + ch);
}
}
String result = pinyinSb.toString();
LogUtils.d(TAG, "convertToPinyin: 全拼转换完成,结果=" + result);
return result;
}
/**
* 姓名转为拼音首字母(多音字默认取首个拼音首字母)
*/
private String convertToPinyinFirstLetter(String chinese) {
LogUtils.d(TAG, "convertToPinyinFirstLetter: 开始转换姓名为拼音首字母,姓名=" + chinese);
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
StringBuilder firstLetterSb = new StringBuilder();
for (int i = 0; i < chinese.length(); i++) {
char ch = chinese.charAt(i);
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
if (pinyinArray != null && pinyinArray.length > 0) {
char firstChar = pinyinArray[0].charAt(0);
firstLetterSb.append(firstChar);
LogUtils.v(TAG, "convertToPinyinFirstLetter: 字符[" + ch + "]转为首字母[" + firstChar + "]");
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
LogUtils.e(TAG, "convertToPinyinFirstLetter: 拼音首字母转换异常,字符=" + ch, e);
}
} else {
firstLetterSb.append(ch);
LogUtils.v(TAG, "convertToPinyinFirstLetter: 非汉字字符直接拼接,字符=" + ch);
}
}
String result = firstLetterSb.toString();
LogUtils.d(TAG, "convertToPinyinFirstLetter: 拼音首字母转换完成,结果=" + result);
return result;
}
/**
* 获取统一的拼音输出格式(小写、无音调)
* 抽离为公共方法,避免重复创建对象
*/
private HanyuPinyinOutputFormat getPinyinOutputFormat() {
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
return format;
}
// ====================== Getter 方法区 ======================
public String getName() {
return name;
}
public String getNumber() {
return number;
}
public String getPinyin() {
return pinyin;
}
public String getPinyinFirstLetter() {
return pinyinFirstLetter;
}
}

View File

@@ -1,91 +0,0 @@
package cc.winboll.studio.contacts.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/13 07:06:13
* @Describe 主服务配置实体类支持JSON序列化与反序列化
*/
public class MainServiceBean extends BaseBean {
// ====================== 常量定义区 ======================
public static final String TAG = "MainServiceBean";
private static final String JSON_KEY_IS_ENABLE = "isEnable";
// ====================== 成员变量区 ======================
private boolean isEnable;
// ====================== 构造函数区 ======================
public MainServiceBean() {
this.isEnable = false;
LogUtils.d(TAG, "MainServiceBean: 初始化实体类,默认状态为禁用");
}
// ====================== Getter & Setter 方法区 ======================
public void setIsEnable(boolean isEnable) {
LogUtils.d(TAG, "setIsEnable: 服务状态设置为" + isEnable);
this.isEnable = isEnable;
}
public boolean isEnable() {
return isEnable;
}
// ====================== 重写 BaseBean 抽象方法区 ======================
@Override
public String getName() {
String className = MainServiceBean.class.getName();
LogUtils.v(TAG, "getName: 获取类名=" + className);
return className;
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将实体类写入JSON");
super.writeThisToJsonWriter(jsonWriter);
// 写入服务启用状态字段
jsonWriter.name(JSON_KEY_IS_ENABLE).value(this.isEnable);
LogUtils.d(TAG, "writeThisToJsonWriter: JSON写入完成isEnable=" + this.isEnable);
}
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
// 优先调用父类方法处理通用字段
if (super.initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
return true;
}
// 处理当前类专属字段
if (JSON_KEY_IS_ENABLE.equals(name)) {
this.isEnable = jsonReader.nextBoolean();
LogUtils.d(TAG, "initObjectsFromJsonReader: 读取字段[" + name + "]值=" + this.isEnable);
return true;
}
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别字段=" + name);
return false;
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON读取实体类数据");
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + name);
jsonReader.skipValue();
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON读取完成当前实体状态=" + this.isEnable);
return this;
}
}

View File

@@ -1,148 +0,0 @@
package cc.winboll.studio.contacts.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/21 09:52:10
* @Describe 电话黑名单规则实体类支持JSON序列化与反序列化
*/
public class PhoneConnectRuleBean extends BaseBean {
// ====================== 常量定义区 ======================
public static final String TAG = "PhoneConnectRuleModel";
// JSON字段名常量避免硬编码错误
private static final String JSON_KEY_RULE_TEXT = "ruleText";
private static final String JSON_KEY_ALLOW_CONNECTION = "isAllowConnection";
private static final String JSON_KEY_IS_ENABLE = "isEnable";
// ====================== 成员变量区 ======================
private String ruleText;
private boolean isAllowConnection;
private boolean isEnable;
private boolean isSimpleView;
// ====================== 构造函数区 ======================
/**
* 默认构造,初始化默认值
*/
public PhoneConnectRuleBean() {
this.ruleText = "";
this.isAllowConnection = false;
this.isEnable = false;
this.isSimpleView = true;
LogUtils.d(TAG, "PhoneConnectRuleModel: 默认构造初始化完成 | 规则文本空串,默认禁用状态");
}
/**
* 带参构造,初始化核心规则参数
*/
public PhoneConnectRuleBean(String ruleText, boolean isAllowConnection, boolean isEnable) {
this.ruleText = ruleText == null ? "" : ruleText;
this.isAllowConnection = isAllowConnection;
this.isEnable = isEnable;
this.isSimpleView = true;
LogUtils.d(TAG, "PhoneConnectRuleModel: 带参构造初始化完成 | 规则文本=" + this.ruleText
+ " | 允许连接=" + this.isAllowConnection + " | 规则启用=" + this.isEnable);
}
// ====================== Getter & Setter 方法区 ======================
public String getRuleText() {
return ruleText;
}
public void setRuleText(String ruleText) {
String oldValue = this.ruleText;
this.ruleText = ruleText == null ? "" : ruleText;
LogUtils.d(TAG, "setRuleText: 规则文本更新 | 旧值=" + oldValue + " | 新值=" + this.ruleText);
}
public boolean isAllowConnection() {
return isAllowConnection;
}
public void setIsAllowConnection(boolean isAllowConnection) {
LogUtils.d(TAG, "setIsAllowConnection: 允许连接状态更新为" + isAllowConnection);
this.isAllowConnection = isAllowConnection;
}
public boolean isEnable() {
return isEnable;
}
public void setIsEnable(boolean isEnable) {
LogUtils.d(TAG, "setIsEnable: 规则启用状态更新为" + isEnable);
this.isEnable = isEnable;
}
public boolean isSimpleView() {
return isSimpleView;
}
public void setIsSimpleView(boolean isSimpleView) {
LogUtils.d(TAG, "setIsSimpleView: 视图模式更新 | 简洁模式=" + isSimpleView);
this.isSimpleView = isSimpleView;
}
// ====================== 重写 BaseBean 抽象方法区 ======================
@Override
public String getName() {
String className = PhoneConnectRuleBean.class.getName();
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
return className;
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化规则数据");
super.writeThisToJsonWriter(jsonWriter);
// 序列化核心字段
jsonWriter.name(JSON_KEY_RULE_TEXT).value(getRuleText());
jsonWriter.name(JSON_KEY_ALLOW_CONNECTION).value(isAllowConnection());
jsonWriter.name(JSON_KEY_IS_ENABLE).value(isEnable());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
}
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
// 优先让父类处理通用字段
if (super.initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
return true;
}
// 处理当前类专属字段
if (JSON_KEY_RULE_TEXT.equals(name)) {
setRuleText(jsonReader.nextString());
} else if (JSON_KEY_ALLOW_CONNECTION.equals(name)) {
setIsAllowConnection(jsonReader.nextBoolean());
} else if (JSON_KEY_IS_ENABLE.equals(name)) {
setIsEnable(jsonReader.nextBoolean());
} else {
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
return false;
}
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
return true;
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析规则数据");
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
jsonReader.skipValue();
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 解析后规则=" + getRuleText());
return this;
}
}

View File

@@ -1,107 +0,0 @@
package cc.winboll.studio.contacts.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/24 18:47:11
* @Describe 手机铃声设置参数类支持JSON序列化与反序列化
*/
public class RingTongBean extends BaseBean {
// ====================== 常量定义区 ======================
public static final String TAG = "AudioRingTongBean";
private static final String JSON_KEY_STREAM_VOLUME = "streamVolume";
// 铃声音量范围常量参考AudioManager标准
private static final int VOLUME_MIN = 0;
private static final int VOLUME_MAX = 100;
// ====================== 成员变量区 ======================
private int streamVolume;
// ====================== 构造函数区 ======================
/**
* 默认构造,铃声音量初始化为最大值
*/
public RingTongBean() {
this.streamVolume = VOLUME_MAX;
LogUtils.d(TAG, "RingTongBean: 默认构造初始化 | 铃声音量=" + this.streamVolume);
}
/**
* 带参构造,初始化指定铃声音量
*/
public RingTongBean(int streamVolume) {
// 音量值范围校验,避免非法值
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
LogUtils.d(TAG, "RingTongBean: 带参构造初始化 | 原始音量=" + streamVolume + " | 校正后=" + this.streamVolume);
}
// ====================== Getter & Setter 方法区 ======================
public int getStreamVolume() {
return streamVolume;
}
public void setStreamVolume(int streamVolume) {
int oldVolume = this.streamVolume;
// 音量值范围校验
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
LogUtils.d(TAG, "setStreamVolume: 铃声音量更新 | 旧值=" + oldVolume + " | 新值=" + this.streamVolume);
}
// ====================== 重写 BaseBean 抽象方法区 ======================
@Override
public String getName() {
String className = RingTongBean.class.getName();
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
return className;
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化铃声音量参数");
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name(JSON_KEY_STREAM_VOLUME).value(getStreamVolume());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成 | 音量值=" + getStreamVolume());
}
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
// 优先调用父类处理通用字段
if (super.initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
return true;
}
// 处理当前类专属字段
if (JSON_KEY_STREAM_VOLUME.equals(name)) {
setStreamVolume(jsonReader.nextInt());
LogUtils.v(TAG, "initObjectsFromJsonReader: 解析字段[" + name + "]值=" + this.streamVolume);
return true;
}
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
return false;
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析铃声音量参数");
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
jsonReader.skipValue();
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 最终音量值=" + this.streamVolume);
return this;
}
}

View File

@@ -1,216 +0,0 @@
package cc.winboll.studio.contacts.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
import cc.winboll.studio.contacts.utils.IntUtils;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/02 19:51:40
* @Describe 应用设置数据模型支持云盾防御配置与JSON序列化
*/
public class SettingsBean extends BaseBean {
// ====================== 常量定义区 ======================
public static final String TAG = "SettingsModel";
// 数值范围常量
public static final int MAX_INTRANGE = 666666;
public static final int MIN_INTRANGE = 1;
// JSON字段名常量消除硬编码
private static final String JSON_KEY_DUN_TOTAL = "dunTotalCount";
private static final String JSON_KEY_DUN_CURRENT = "dunCurrentCount";
private static final String JSON_KEY_DUN_RESUME_SECOND = "dunResumeSecondCount";
private static final String JSON_KEY_DUN_RESUME_COUNT = "dunResumeCount";
private static final String JSON_KEY_DUN_ENABLE = "isEnableDun";
private static final String JSON_KEY_URL = "szBoBullToon_URL";
// ====================== 成员变量区 ======================
// 云盾防御层数量
private int dunTotalCount;
// 当前云盾防御层
private int dunCurrentCount;
// 防御层恢复时间间隔(秒钟)
private int dunResumeSecondCount;
// 每次恢复防御层数
private int dunResumeCount;
// 是否启用云盾
private boolean isEnableDun;
// BoBullToon 应用模块数据请求地址
private String szBoBullToon_URL;
// ====================== 构造函数区 ======================
/**
* 默认构造,初始化默认配置
*/
public SettingsBean() {
this.dunTotalCount = 6;
this.dunCurrentCount = 6;
this.dunResumeSecondCount = 60;
this.dunResumeCount = 1;
this.isEnableDun = false;
this.szBoBullToon_URL = "";
LogUtils.d(TAG, "SettingsModel: 默认构造初始化完成 | 云盾默认配置加载完毕");
}
/**
* 带参构造,初始化自定义配置并校验数值范围
*/
public SettingsBean(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount,
int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
this.isEnableDun = isEnableDun;
this.szBoBullToon_URL = szBoBullToon_URL == null ? "" : szBoBullToon_URL;
LogUtils.d(TAG, "SettingsModel: 带参构造初始化完成 | 总层数=" + this.dunTotalCount
+ " | 当前层数=" + this.dunCurrentCount + " | 恢复间隔=" + this.dunResumeSecondCount
+ " | 恢复层数=" + this.dunResumeCount + " | 云盾启用=" + this.isEnableDun);
}
// ====================== 私有工具方法区 ======================
/**
* 数值范围校验,确保参数在 MIN~MAX 区间内
*/
private int getSettingsModelRangeInt(int origin) {
int result = IntUtils.getIntInRange(origin, MIN_INTRANGE, MAX_INTRANGE);
if (result != origin) {
LogUtils.w(TAG, "getSettingsModelRangeInt: 数值校正 | 原始值=" + origin + " | 校正后=" + result);
}
return result;
}
// ====================== Getter & Setter 方法区 ======================
public int getDunTotalCount() {
return dunTotalCount;
}
public void setDunTotalCount(int dunTotalCount) {
int oldValue = this.dunTotalCount;
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
LogUtils.d(TAG, "setDunTotalCount: 总防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunTotalCount);
}
public int getDunCurrentCount() {
return dunCurrentCount;
}
public void setDunCurrentCount(int dunCurrentCount) {
int oldValue = this.dunCurrentCount;
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
LogUtils.d(TAG, "setDunCurrentCount: 当前防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunCurrentCount);
}
public int getDunResumeSecondCount() {
return dunResumeSecondCount;
}
public void setDunResumeSecondCount(int dunResumeSecondCount) {
int oldValue = this.dunResumeSecondCount;
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
LogUtils.d(TAG, "setDunResumeSecondCount: 恢复间隔更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeSecondCount);
}
public int getDunResumeCount() {
return dunResumeCount;
}
public void setDunResumeCount(int dunResumeCount) {
int oldValue = this.dunResumeCount;
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
LogUtils.d(TAG, "setDunResumeCount: 恢复层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeCount);
}
public boolean isEnableDun() {
return isEnableDun;
}
public void setIsEnableDun(boolean isEnableDun) {
LogUtils.d(TAG, "setIsEnableDun: 云盾启用状态更新为" + isEnableDun);
this.isEnableDun = isEnableDun;
}
public String getBoBullToon_URL() {
return szBoBullToon_URL;
}
public void setBoBullToon_URL(String boBullToon_URL) {
String oldValue = this.szBoBullToon_URL;
this.szBoBullToon_URL = boBullToon_URL == null ? "" : boBullToon_URL;
LogUtils.d(TAG, "setBoBullToon_URL: 请求地址更新 | 旧值=" + oldValue + " | 新值=" + this.szBoBullToon_URL);
}
// ====================== 重写 BaseBean 抽象方法区 ======================
@Override
public String getName() {
String className = SettingsBean.class.getName();
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
return className;
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化设置数据");
super.writeThisToJsonWriter(jsonWriter);
// 写入所有配置字段
jsonWriter.name(JSON_KEY_DUN_TOTAL).value(getDunTotalCount());
jsonWriter.name(JSON_KEY_DUN_CURRENT).value(getDunCurrentCount());
jsonWriter.name(JSON_KEY_DUN_RESUME_SECOND).value(getDunResumeSecondCount());
jsonWriter.name(JSON_KEY_DUN_RESUME_COUNT).value(getDunResumeCount());
jsonWriter.name(JSON_KEY_DUN_ENABLE).value(isEnableDun());
jsonWriter.name(JSON_KEY_URL).value(getBoBullToon_URL());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
}
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
// 优先调用父类处理通用字段
if (super.initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
return true;
}
// 处理当前类专属配置字段
if (JSON_KEY_DUN_TOTAL.equals(name)) {
setDunTotalCount(getSettingsModelRangeInt(jsonReader.nextInt()));
} else if (JSON_KEY_DUN_CURRENT.equals(name)) {
setDunCurrentCount(getSettingsModelRangeInt(jsonReader.nextInt()));
} else if (JSON_KEY_DUN_RESUME_SECOND.equals(name)) {
setDunResumeSecondCount(getSettingsModelRangeInt(jsonReader.nextInt()));
} else if (JSON_KEY_DUN_RESUME_COUNT.equals(name)) {
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
} else if (JSON_KEY_DUN_ENABLE.equals(name)) {
setIsEnableDun(jsonReader.nextBoolean());
} else if (JSON_KEY_URL.equals(name)) {
setBoBullToon_URL(jsonReader.nextString());
} else {
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
return false;
}
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
return true;
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析设置数据");
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
jsonReader.skipValue();
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 云盾配置加载完毕");
return this;
}
}

View File

@@ -1,362 +0,0 @@
package cc.winboll.studio.contacts.phonecallui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.contacts.ActivityStack;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.Timer;
import java.util.TimerTask;
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
/**
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/14 21:01
* @Describe 接打电话界面(单例模式 + 适配API29 - 30 + 小米机型兼容性优化)
* 功能:单例通话窗口、来电/去电显示、通话计时、免提控制、锁屏显示
*/
public class PhoneCallActivity extends Activity implements View.OnClickListener {
// 常量定义区(核心常量+小米适配标识)
public static final String TAG = "PhoneCallActivity";
private static final int MSG_CLOSE_ACTIVITY = 0x001;
private static final String MI_ADAPT_TAG = "MiAdapt";
private static final String TOAST_CALLING = "通话进行中,无法重复创建通话窗口";
private static final long CLOSE_DELAY_MS = 100; // 小米机型关闭延迟时间
// 静态属性区(单例核心+全局工具对象)
private static volatile boolean sIsActivityAlive = false;
private static Handler sCloseHandler;
// 控件属性区(按界面布局顺序排列)
private TextView mTvCallNumberLabel;
private TextView mTvCallNumber;
private TextView mTvPickUp;
private TextView mTvCallingTime;
private TextView mTvHangUp;
// 业务属性区(按依赖优先级排列)
private PhoneCallManager mPhoneCallManager;
private PhoneCallService.CallType mCallType;
private String mPhoneNumber;
private Timer mOnGoingCallTimer;
private int mCallingTime;
private boolean isClosing = false; // 新增:避免重复关闭页面
// 对外静态接口(单例启动+外部关闭)
public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) {
if (context == null || phoneNumber == null || callType == null) {
LogUtils.e(TAG, "actionStart: 入参为空,启动失败");
return;
}
if (sIsActivityAlive) {
LogUtils.w(TAG, MI_ADAPT_TAG + " 已有活跃通话窗口,拒绝重复启动");
Toast.makeText(context, TOAST_CALLING, Toast.LENGTH_SHORT).show();
return;
}
LogUtils.d(TAG, MI_ADAPT_TAG + " 启动通话界面,号码=" + phoneNumber + ",类型=" + callType.name());
Intent intent = new Intent(context, PhoneCallActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra("call_type", callType);
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
context.startActivity(intent);
}
public static void closePhoneCallActivity() {
LogUtils.d(TAG, "closePhoneCallActivity: 收到外部关闭指令");
if (sIsActivityAlive && sCloseHandler != null) {
sCloseHandler.sendEmptyMessage(MSG_CLOSE_ACTIVITY);
LogUtils.d(TAG, "closePhoneCallActivity: 关闭消息已发送");
} else {
LogUtils.w(TAG, "closePhoneCallActivity: 页面已销毁或Handler未初始化关闭跳过");
}
}
// 生命周期方法区(按执行流程排序)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始创建SDK版本=" + Build.VERSION.SDK_INT);
// 单例双重校验,防止异常场景多实例
if (sIsActivityAlive) {
Toast.makeText(this, TOAST_CALLING, Toast.LENGTH_SHORT).show();
LogUtils.w(TAG, MI_ADAPT_TAG + " 拦截重复创建,即将关闭当前实例");
finish();
return;
}
sIsActivityAlive = false;
setContentView(R.layout.activity_phone_call);
ActivityStack.getInstance().addActivity(this);
adaptLockScreenAndXiaomi();
initHandler();
initData();
initView();
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面创建完成");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始销毁");
sIsActivityAlive = false;
isClosing = false;
stopTimer();
// 销毁通话管理器
if (mPhoneCallManager != null) {
mPhoneCallManager.destroy();
mPhoneCallManager = null;
LogUtils.d(TAG, "销毁通话管理器资源");
}
// 销毁Handler避免内存泄漏
if (sCloseHandler != null) {
sCloseHandler.removeCallbacksAndMessages(null);
sCloseHandler = null;
LogUtils.d(TAG, "销毁关闭Handler");
}
ActivityStack.getInstance().removeActivity(this);
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面销毁完成");
}
@Override
protected void onStop() {
super.onStop();
if (isFinishing()) {
sIsActivityAlive = false;
LogUtils.d(TAG, MI_ADAPT_TAG + " 页面即将关闭,重置单例标记");
}
}
// 点击事件回调
@Override
public void onClick(View v) {
if (v == null) {
LogUtils.w(TAG, "onClick: 点击控件为空,忽略操作");
return;
}
switch (v.getId()) {
case R.id.tv_phone_pick_up:
LogUtils.d(TAG, "onClick: 触发接听操作");
answerCall();
break;
case R.id.tv_phone_hang_up:
LogUtils.d(TAG, "onClick: 触发挂断操作,当前通话时长=" + mCallingTime + "");
hangUpCall();
break;
default:
LogUtils.w(TAG, "onClick: 未知点击事件控件ID=" + v.getId());
}
}
// 初始化方法区(按初始化顺序排列)
private void initHandler() {
sCloseHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_CLOSE_ACTIVITY) {
LogUtils.d(TAG, "handleMessage: 收到关闭消息,执行挂断逻辑");
hangUpCall();
}
}
};
LogUtils.d(TAG, "initHandler: 关闭Handler初始化完成");
}
private void initData() {
LogUtils.d(TAG, "initData: 开始初始化业务数据");
mPhoneCallManager = PhoneCallManager.getInstance(this);
Intent intent = getIntent();
if (intent == null) {
LogUtils.e(TAG, "initData: 启动Intent为空终止初始化");
removeFromRecentsAndFinish();
return;
}
mPhoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
mCallType = (PhoneCallService.CallType) intent.getSerializableExtra("call_type");
if (mPhoneNumber == null || mCallType == null) {
LogUtils.e(TAG, "initData: 通话号码或类型解析失败");
removeFromRecentsAndFinish();
return;
}
mOnGoingCallTimer = new Timer();
mCallingTime = 0;
LogUtils.d(TAG, "initData: 业务数据初始化完成,号码=" + mPhoneNumber);
}
private void initView() {
LogUtils.d(TAG, "initView: 开始初始化界面控件");
// 修复沉浸式导航栏语法,适配小米全面屏
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
// 绑定控件
mTvCallNumberLabel = findViewById(R.id.tv_call_number_label);
mTvCallNumber = findViewById(R.id.tv_call_number);
mTvPickUp = findViewById(R.id.tv_phone_pick_up);
mTvCallingTime = findViewById(R.id.tv_phone_calling_time);
mTvHangUp = findViewById(R.id.tv_phone_hang_up);
// 设置控件属性
mTvCallNumber.setText(formatPhoneNumber(mPhoneNumber));
mTvPickUp.setOnClickListener(this);
mTvHangUp.setOnClickListener(this);
// 区分来电/去电UI样式
if (PhoneCallService.CallType.CALL_IN == mCallType) {
mTvCallNumberLabel.setText("来电号码");
mTvPickUp.setVisibility(View.VISIBLE);
mTvCallingTime.setVisibility(View.GONE);
} else if (PhoneCallService.CallType.CALL_OUT == mCallType) {
mTvCallNumberLabel.setText("呼叫号码");
mTvPickUp.setVisibility(View.GONE);
mTvCallingTime.setVisibility(View.VISIBLE);
mTvCallingTime.setText("通话中00:00");
if (mPhoneCallManager != null) {
mPhoneCallManager.openSpeaker();
LogUtils.d(TAG, MI_ADAPT_TAG + " 去电模式自动开启免提");
}
startCallTimer();
}
LogUtils.d(TAG, "initView: 界面控件初始化完成");
}
// 小米机型专属适配方法
private void adaptLockScreenAndXiaomi() {
LogUtils.d(TAG, MI_ADAPT_TAG + " 执行锁屏适配逻辑");
Window window = getWindow();
if (window == null) {
LogUtils.e(TAG, MI_ADAPT_TAG + " Window对象为空适配失败");
return;
}
int flags = WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
// 小米机型额外添加解锁屏标志解决MIUI锁屏拦截问题
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
LogUtils.d(TAG, MI_ADAPT_TAG + " 已添加小米机型专属锁屏适配标志");
}
window.addFlags(flags);
// 适配API29+锁屏新接口
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setShowWhenLocked(true);
setTurnScreenOn(true);
LogUtils.d(TAG, MI_ADAPT_TAG + " 适配API29+锁屏接口完成");
}
}
// 通话核心业务方法
private void answerCall() {
LogUtils.d(TAG, "answerCall: 执行接听操作");
if (mPhoneCallManager == null) {
LogUtils.e(TAG, "answerCall: 通话管理器为空,接听失败");
return;
}
mPhoneCallManager.answer();
mTvPickUp.setVisibility(View.GONE);
mTvCallingTime.setVisibility(View.VISIBLE);
mTvCallingTime.setText("通话中00:00");
startCallTimer();
LogUtils.d(TAG, "answerCall: 接听操作完成,启动通话计时");
}
private void hangUpCall() {
if (isClosing) {
LogUtils.w(TAG, "hangUpCall: 挂断操作已执行,无需重复调用");
return;
}
LogUtils.d(TAG, "hangUpCall: 执行挂断操作,当前时长=" + mCallingTime + "");
isClosing = true;
stopTimer();
if (mPhoneCallManager != null) {
mPhoneCallManager.disconnect();
LogUtils.d(TAG, "hangUpCall: 通话连接已断开");
}
// 延迟关闭页面,适配小米机型通话时序
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
removeFromRecentsAndFinish();
}
}, CLOSE_DELAY_MS);
}
// 任务栈清理方法
private void removeFromRecentsAndFinish() {
if (isFinishing()) {
LogUtils.d(TAG, "removeFromRecentsAndFinish: 页面已在关闭中,无需重复操作");
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAndRemoveTask();
LogUtils.d(TAG, MI_ADAPT_TAG + " 移除任务栈并关闭页面");
} else {
finish();
LogUtils.d(TAG, "兼容低版本,关闭页面");
}
}
// 计时工具方法
private void startCallTimer() {
LogUtils.d(TAG, "startCallTimer: 启动通话计时器");
if (mOnGoingCallTimer == null) {
mOnGoingCallTimer = new Timer();
}
mOnGoingCallTimer.schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mCallingTime++;
mTvCallingTime.setText("通话中:" + formatCallingTime(mCallingTime));
}
});
}
}, 0, 1000);
}
private void stopTimer() {
LogUtils.d(TAG, "stopTimer: 停止通话计时器");
if (mOnGoingCallTimer != null) {
mOnGoingCallTimer.cancel();
mOnGoingCallTimer = null;
}
mCallingTime = 0;
}
// 辅助工具方法:格式化通话时长
private String formatCallingTime(int seconds) {
int minute = seconds / 60;
int second = seconds % 60;
String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute);
String secondStr = second < 10 ? "0" + second : String.valueOf(second);
return minuteStr + ":" + secondStr;
}
}

View File

@@ -1,204 +0,0 @@
package cc.winboll.studio.contacts.phonecallui;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import android.telecom.Call;
import android.telecom.VideoProfile;
import androidx.annotation.RequiresApi;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/15 20:11
* @Describe 通话核心管理类
* 功能:接听/挂断通话、免提控制、资源释放适配API29-30及小米机型
*/
@RequiresApi(api = Build.VERSION_CODES.Q) // 匹配目标适配区间API29
public class PhoneCallManager {
// 常量定义区
public static final String TAG = "PhoneCallManager";
private static final String MI_ADAPT_TAG = "MiDeviceAdapt"; // 小米适配标识
private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
private static final int AUDIO_MODE_BACKUP = -1; // 音频模式备份默认值
// 成员属性区按依赖优先级排序移除静态call避免跨组件冲突
private Context mContext;
private AudioManager mAudioManager;
private int mAudioModeBackup; // 备份原始音频模式,避免影响其他应用
private boolean mIsSpeakerOpened; // 免提状态标记,防止重复切换
// 构造方法(单例化改造,避免多实例冲突)
private static volatile PhoneCallManager sInstance;
public static PhoneCallManager getInstance(Context context) {
if (context == null) {
LogUtils.e(TAG, "getInstance: 上下文为空,初始化失败");
return null;
}
if (sInstance == null) {
synchronized (PhoneCallManager.class) {
if (sInstance == null) {
sInstance = new PhoneCallManager(context.getApplicationContext()); // 用应用上下文,避免内存泄漏
}
}
}
return sInstance;
}
// 私有构造,禁止外部实例化
private PhoneCallManager(Context context) {
LogUtils.d(TAG, MI_ADAPT_TAG + " 初始化通话管理类");
this.mContext = context;
this.mAudioModeBackup = AUDIO_MODE_BACKUP;
this.mIsSpeakerOpened = false;
initAudioManager();
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理类初始化完成");
}
// 初始化辅助方法
private void initAudioManager() {
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
if (mAudioManager != null) {
// 备份原始音频模式(小米机型切换后需恢复,避免外放异常)
mAudioModeBackup = mAudioManager.getMode();
LogUtils.d(TAG, "音频管理器初始化成功,原始模式备份:" + mAudioModeBackup);
} else {
LogUtils.e(TAG, "音频管理器初始化失败,将影响通话音频控制");
}
}
// 核心业务方法(按使用场景排序,强化小米适配+容错)
/**
* 接听电话,默认音频通话模式
*/
public void answer() {
LogUtils.d(TAG, "执行接听通话操作");
// 从PhoneCallService的静态管理器获取通话对象统一数据源
Call currentCall = PhoneCallService.PhoneCallManager.call;
if (currentCall == null) {
LogUtils.e(TAG, "接听失败:通话对象为空");
return;
}
// 校验通话状态,避免重复接听(小米机型状态变更延迟)
if (currentCall.getState() != Call.STATE_RINGING) {
LogUtils.w(TAG, MI_ADAPT_TAG + " 非响铃状态,无需接听,当前状态:" + currentCall.getState());
return;
}
try {
currentCall.answer(VIDEO_PROFILE_AUDIO_ONLY);
openSpeaker(); // 接听后自动开免提
LogUtils.d(TAG, "通话接听成功,自动开启免提");
} catch (SecurityException e) {
LogUtils.e(TAG, MI_ADAPT_TAG + " 接听权限不足需android.permission.ANSWER_PHONE_CALLS", e);
} catch (IllegalStateException e) {
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法接听", e);
} catch (Exception e) {
LogUtils.e(TAG, "接听通话异常", e);
}
}
/**
* 断开通话(支持来电拒接、通话中挂断)
*/
public void disconnect() {
LogUtils.d(TAG, "执行断开通话操作");
Call currentCall = PhoneCallService.PhoneCallManager.call;
if (currentCall == null) {
LogUtils.e(TAG, "挂断失败:通话对象为空");
return;
}
// 校验通话状态,避免重复挂断
if (currentCall.getState() == Call.STATE_DISCONNECTED) {
LogUtils.w(TAG, MI_ADAPT_TAG + " 通话已断开,无需重复操作");
return;
}
try {
currentCall.disconnect();
closeSpeaker(); // 挂断后关闭免提+恢复音频模式
LogUtils.d(TAG, "通话断开成功");
} catch (SecurityException e) {
LogUtils.e(TAG, MI_ADAPT_TAG + " 挂断权限不足需android.permission.CALL_PHONE", e);
} catch (IllegalStateException e) {
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法挂断", e);
} catch (Exception e) {
LogUtils.e(TAG, "断开通话异常", e);
}
}
/**
* 打开免提适配小米机型音频通道切换解决MIUI音频混乱
*/
public void openSpeaker() {
LogUtils.d(TAG, "执行打开免提操作");
if (mAudioManager == null) {
LogUtils.e(TAG, "打开免提失败:音频管理器未初始化");
return;
}
if (mIsSpeakerOpened) {
LogUtils.w(TAG, "免提已开启,无需重复操作");
return;
}
try {
// 小米机型适配步骤1. 设置通话模式 2. 关闭静音 3. 开启免提(固定顺序)
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
mAudioManager.setStreamMute(AudioManager.STREAM_VOICE_CALL, false); // 确保通话音频不静音
mAudioManager.setSpeakerphoneOn(true);
mIsSpeakerOpened = true;
LogUtils.d(TAG, MI_ADAPT_TAG + " 免提开启成功,当前模式:" + mAudioManager.getMode());
} catch (SecurityException e) {
LogUtils.e(TAG, MI_ADAPT_TAG + " 音频控制权限不足", e);
} catch (Exception e) {
LogUtils.e(TAG, "打开免提异常", e);
}
}
/**
* 新增:关闭免提(挂断/切换场景调用,修复小米音频残留)
*/
public void closeSpeaker() {
LogUtils.d(TAG, "执行关闭免提操作");
if (mAudioManager == null || !mIsSpeakerOpened) {
LogUtils.w(TAG, "免提未开启或音频管理器为空,无需操作");
return;
}
try {
mAudioManager.setSpeakerphoneOn(false);
// 恢复原始音频模式(关键:小米机型不恢复会导致其他应用外放异常)
if (mAudioModeBackup != AUDIO_MODE_BACKUP) {
mAudioManager.setMode(mAudioModeBackup);
LogUtils.d(TAG, MI_ADAPT_TAG + " 恢复原始音频模式:" + mAudioModeBackup);
}
mIsSpeakerOpened = false;
LogUtils.d(TAG, "免提关闭成功");
} catch (Exception e) {
LogUtils.e(TAG, MI_ADAPT_TAG + " 关闭免提异常", e);
}
}
/**
* 销毁资源,避免内存泄漏+音频残留(适配小米内存管理)
*/
public void destroy() {
LogUtils.d(TAG, "开始销毁通话管理资源");
closeSpeaker(); // 销毁前强制关闭免提+恢复音频模式
// 释放资源(应用上下文无需主动置空,避免空指针)
mAudioManager = null;
sInstance = null; // 单例置空,下次重新初始化
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理资源销毁完成");
}
/**
* 新增获取当前免提状态供UI层同步显示
*/
public boolean isSpeakerOpened() {
return mIsSpeakerOpened;
}
}

View File

@@ -1,284 +0,0 @@
package cc.winboll.studio.contacts.phonecallui;
import android.media.AudioManager;
import android.telecom.Call;
import android.telecom.InCallService;
import android.telephony.TelephonyManager;
import androidx.annotation.RequiresApi;
import cc.winboll.studio.contacts.ActivityStack;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.fragments.CallLogFragment;
import cc.winboll.studio.contacts.model.RingTongBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @see PhoneCallActivity
* @see android.telecom.InCallService
* 适配Java7 语法 + Android API29 - 30 | 移除录音功能 | 强化小米设备稳定性与容错性
*/
@RequiresApi(api = 29)
public class PhoneCallService extends InCallService {
// 常量定义区
public static final String TAG = "PhoneCallService";
// 小米设备适配标识,便于日志区分
private static final String MI_DEVICE_TAG = "MiDeviceAdapt";
// 成员属性区(按依赖顺序排列)
private Call.Callback mCallCallback;
private AudioManager mAudioManager;
// 内部枚举类(通话类型定义)
public enum CallType {
CALL_IN, // 来电
CALL_OUT // 去电
}
// Service生命周期方法区按执行流程排序
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话监听服务启动");
initAudioManager();
initCallCallback();
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务初始化完成");
}
@Override
public void onCallAdded(Call call) {
super.onCallAdded(call);
LogUtils.d(TAG, "检测到新通话");
if (call == null) {
LogUtils.e(TAG, "通话对象为空,跳过处理");
return;
}
// 双重校验回调,避免重复注册
if (mCallCallback != null) {
call.registerCallback(mCallCallback);
}
// 绑定通话对象到管理器供UI层调用
PhoneCallManager.call = call;
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话回调注册成功,对象绑定完成");
CallType callType = judgeCallType(call);
if (callType != null) {
handleValidCall(call, callType);
} else {
LogUtils.w(TAG, "无法识别通话类型,状态码:" + call.getState());
}
}
@Override
public void onCallRemoved(Call call) {
super.onCallRemoved(call);
LogUtils.d(TAG, "通话结束,开始清理资源");
if (call != null && mCallCallback != null) {
call.unregisterCallback(mCallCallback);
LogUtils.d(TAG, "通话回调已注销");
}
// 延迟置空通话对象避免UI层挂断时对象已被释放适配小米机型时序
new Thread(new Runnable() {
@Override
public void run() {
try {
// 延迟200ms确保PhoneCallActivity挂断逻辑执行完成
Thread.sleep(200);
PhoneCallManager.call = null;
} catch (InterruptedException e) {
LogUtils.e(TAG, MI_DEVICE_TAG + " 延迟置空通话对象异常", e);
}
}
}).start();
PhoneCallActivity.closePhoneCallActivity();
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话资源清理完成");
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "服务开始销毁");
CallLogFragment.updateCallLogFragment();
// 释放资源,适配小米设备内存管理,避免内存泄漏
mCallCallback = null;
mAudioManager = null;
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务销毁完成");
}
// 初始化方法区
private void initAudioManager() {
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
if (mAudioManager == null) {
LogUtils.e(TAG, MI_DEVICE_TAG + " 获取音频管理器失败");
} else {
LogUtils.d(TAG, MI_DEVICE_TAG + " 音频管理器初始化成功");
}
}
private void initCallCallback() {
mCallCallback = new Call.Callback() {
@Override
public void onStateChanged(Call call, int state) {
super.onStateChanged(call, state);
if (call == null) {
LogUtils.e(TAG, "onStateChanged: 通话对象为空");
return;
}
String stateDesc = getCallStateDesc(state);
LogUtils.d(TAG, "通话状态变更:" + stateDesc + "(状态码:" + state + "");
switch (state) {
case Call.STATE_DISCONNECTED:
// 双重校验,避免重复关闭页面
if (ActivityStack.getInstance().getActivity(PhoneCallActivity.class) != null) {
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
LogUtils.d(TAG, "通话界面已关闭");
}
break;
case Call.STATE_ACTIVE:
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话进入活跃状态,适配音频通道");
break;
default:
break;
}
}
};
LogUtils.d(TAG, "通话状态回调初始化完成");
}
// 核心业务处理方法区
private CallType judgeCallType(Call call) {
if (call == null) {
LogUtils.e(TAG, "judgeCallType: 通话对象为空");
return null;
}
int callState = call.getState();
if (callState == Call.STATE_RINGING) {
LogUtils.d(TAG, "识别为来电");
return CallType.CALL_IN;
} else if (callState == Call.STATE_CONNECTING) {
LogUtils.d(TAG, "识别为去电");
return CallType.CALL_OUT;
}
return null;
}
private boolean handleValidCall(Call call, CallType callType) {
if (call == null || callType == null) {
LogUtils.e(TAG, "handleValidCall: 通话对象或类型为空");
return false;
}
Call.Details callDetails = call.getDetails();
if (callDetails == null || callDetails.getHandle() == null) {
LogUtils.e(TAG, "通话详情缺失,处理终止");
return false;
}
String phoneNumber = callDetails.getHandle().getSchemeSpecificPart();
LogUtils.d(TAG, "处理通话:号码=" + phoneNumber + ",类型=" + callType.name());
if (mAudioManager == null) {
LogUtils.e(TAG, "音频管理器未初始化");
PhoneCallActivity.actionStart(this, phoneNumber, callType);
return true;
}
if (checkRulesAndHandleRingerVolumeControl(phoneNumber, call)) {
PhoneCallActivity.actionStart(this, phoneNumber, callType);
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话界面启动成功");
return true;
}
return false;
}
private boolean checkRulesAndHandleRingerVolumeControl(String phoneNumber, Call call) {
if (mAudioManager == null || phoneNumber == null || call == null) {
LogUtils.e(TAG, "checkRulesAndHandleRingerVolumeControl: 入参为空");
return false;
}
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
LogUtils.d(TAG, "当前铃声音量:" + currentVolume);
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
if (ringTongBean == null) {
ringTongBean = new RingTongBean();
RingTongBean.saveBean(this, ringTongBean);
LogUtils.d(TAG, "初始化默认铃音配置");
}
final int configVolume = ringTongBean.getStreamVolume();
try {
// 小米机型适配:调整音量时添加权限校验
if (currentVolume != configVolume) {
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
LogUtils.d(TAG, MI_DEVICE_TAG + " 铃声音量调整为配置值:" + configVolume);
}
} catch (SecurityException e) {
LogUtils.e(TAG, "音量调整失败,权限不足", e);
return false;
}
// 校验拦截规则
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
LogUtils.d(TAG, "号码" + phoneNumber + "命中拦截规则");
try {
// 拦截时静音并挂断
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
call.disconnect();
LogUtils.d(TAG, MI_DEVICE_TAG + " 拦截通话已挂断并静音");
// 延迟恢复音量,适配小米机型音频通道延迟
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
if (mAudioManager != null) {
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
LogUtils.d(TAG, MI_DEVICE_TAG + " 延迟恢复铃音配置");
}
} catch (InterruptedException e) {
LogUtils.e(TAG, "恢复音量线程中断", e);
}
}
}).start();
} catch (SecurityException e) {
LogUtils.e(TAG, "拦截静音失败", e);
return false;
}
return false;
}
return true;
}
// 辅助工具方法区:解析通话状态描述
private String getCallStateDesc(int state) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
return "响铃中";
case TelephonyManager.CALL_STATE_OFFHOOK:
return "通话中";
case TelephonyManager.CALL_STATE_IDLE:
return "空闲";
case Call.STATE_ACTIVE:
return "通话活跃";
case Call.STATE_CONNECTING:
return "连接中";
case Call.STATE_DISCONNECTED:
return "已断开";
default:
return "未知状态";
}
}
// 静态内部类:统一管理通话对象,避免跨组件对象混乱
public static class PhoneCallManager {
public static Call call;
}
}

View File

@@ -1,98 +0,0 @@
package cc.winboll.studio.contacts.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.lang.ref.WeakReference;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/13 06:58:04
* @Describe 主要广播接收器,监听系统开机广播并自动启动主服务
*/
public class MainReceiver extends BroadcastReceiver {
// ====================== 常量定义区 ======================
public static final String TAG = "MainReceiver";
// 监听的系统广播 Action
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
// ====================== 成员变量区 ======================
// 使用弱引用关联 MainService避免内存泄漏
private WeakReference<MainService> mMainServiceWeakRef;
// ====================== 构造函数区 ======================
public MainReceiver(MainService service) {
this.mMainServiceWeakRef = new WeakReference<>(service);
LogUtils.d(TAG, "MainReceiver: 初始化完成,已关联 MainService 实例");
}
// ====================== 重写 BroadcastReceiver 核心方法 ======================
@Override
public void onReceive(Context context, Intent intent) {
// 空值校验,避免空指针异常
if (context == null) {
LogUtils.e(TAG, "onReceive: Context 为 null无法处理广播");
return;
}
if (intent == null || intent.getAction() == null) {
LogUtils.w(TAG, "onReceive: 接收到空 Intent 或空 Action");
return;
}
String action = intent.getAction();
LogUtils.d(TAG, "onReceive: 接收到广播 | Action=" + action);
// 处理开机完成广播
if (ACTION_BOOT_COMPLETED.equals(action)) {
LogUtils.i(TAG, "onReceive: 监听到开机完成广播,自动启动 MainService");
ToastUtils.show("设备开机,启动拨号主服务");
MainService.startMainService(context);
} else {
LogUtils.i(TAG, "onReceive: 接收到未处理的广播 | Action=" + action);
ToastUtils.show("收到广播:" + action);
}
}
// ====================== 广播注册/注销方法区 ======================
/**
* 注册广播接收器,监听指定系统广播
* @param context 上下文对象
*/
public void registerAction(Context context) {
if (context == null) {
LogUtils.e(TAG, "registerAction: Context 为 null注册失败");
return;
}
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_BOOT_COMPLETED);
// 可按需添加其他监听的 Action
// intentFilter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
context.registerReceiver(this, intentFilter);
LogUtils.d(TAG, "registerAction: 广播接收器注册成功 | 监听 Action=" + ACTION_BOOT_COMPLETED);
}
/**
* 注销广播接收器,释放资源(解决 mMainReceiver.unregisterAction(this) 调用缺失问题)
* @param context 上下文对象
*/
public void unregisterAction(Context context) {
if (context == null) {
LogUtils.e(TAG, "unregisterAction: Context 为 null注销失败");
return;
}
try {
context.unregisterReceiver(this);
LogUtils.d(TAG, "unregisterAction: 广播接收器注销成功");
} catch (IllegalArgumentException e) {
LogUtils.w(TAG, "unregisterAction: 广播接收器未注册,无需注销", e);
}
}
}

View File

@@ -1,251 +0,0 @@
package cc.winboll.studio.contacts.services;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import cc.winboll.studio.contacts.model.MainServiceBean;
import cc.winboll.studio.contacts.utils.NotificationManagerUtils;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/14 03:38:31
* @Describe 守护进程服务,用于监控并保活主服务 MainService
* 适配 Android 12+ 后台服务启动限制,支持前台服务运行
* 兼容 Java 7 语法 & 低版本 SDK 编译
* 移除无关的 microphone 类型配置,修复前台服务类型不匹配崩溃
*/
public class AssistantService extends Service {
// ====================== 常量定义区 ======================
public static final String TAG = "AssistantService";
// 前台服务通知配置
private static final String FOREGROUND_CHANNEL_ID = "assistant_service_foreground_channel";
private static final int FOREGROUND_NOTIFICATION_ID = 1002;
// 修复:前台服务类型改为 dataSync(0x00000001),与 Manifest 保持一致,移除 microphone 类型
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001;
// Android 版本常量硬编码Java 7 兼容)
private static final int ANDROID_8_API = 26; // 通知渠道最低版本
private static final int ANDROID_10_API = 29; // 前台服务类型最低支持版本
private static final int ANDROID_12_API = 31; // 后台启动限制最低版本
// 重试延迟时间(避免频繁触发后台启动限制)
private static final long RETRY_DELAY_MS = 3000L;
// ====================== 成员变量区 ======================
private MainServiceBean mMainServiceBean;
private MyServiceConnection mMyServiceConnection;
private MainService mMainService;
private boolean mIsBound = false;
private volatile boolean mIsThreadAlive = false;
// ====================== Binder 内部类 ======================
/**
* 对外暴露服务实例的 Binder
*/
public class MyBinder extends Binder {
public AssistantService getService() {
LogUtils.d(TAG, "MyBinder.getService: 获取 AssistantService 实例");
return AssistantService.this;
}
}
// ====================== ServiceConnection 内部类 ======================
/**
* 主服务连接状态监听回调
*/
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (service == null) {
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的 IBinder 为 null");
mIsBound = false;
return;
}
try {
MainService.MyBinder binder = (MainService.MyBinder) service;
mMainService = binder.getService();
mIsBound = true;
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 主服务绑定成功 | MainService=" + mMainService);
} catch (ClassCastException e) {
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e);
mIsBound = false;
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务连接断开");
mMainService = null;
mIsBound = false;
// 尝试重新绑定主服务(如果配置为启用)
reloadMainServiceConfig();
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 延迟重试绑定主服务");
wakeupAndBindMain();
}
}
}
// ====================== 对外方法区 ======================
/**
* 设置线程存活状态
*/
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
this.mIsThreadAlive = isThreadAlive;
LogUtils.d(TAG, "setIsThreadAlive: 线程存活状态变更 | " + isThreadAlive);
}
/**
* 获取线程存活状态
*/
public boolean isThreadAlive() {
return mIsThreadAlive;
}
// ====================== 前台服务辅助方法 ======================
/**
* 创建前台服务通知Android 8.0+ 必须配置渠道)
*/
// private Notification createForegroundNotification() {
// // 1. 创建通知渠道API 26+ 必需)
// if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
// NotificationChannel channel = new NotificationChannel(
// FOREGROUND_CHANNEL_ID,
// "守护服务",
// NotificationManager.IMPORTANCE_LOW
// );
// channel.setDescription("守护服务后台运行,保障主服务存活");
// // 空指针防护
// NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// if (manager != null) {
// manager.createNotificationChannel(channel);
// LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功");
// }
// }
//
// // 2. 构建通知Java 7 分步设置,取消链式调用简化)
// Notification.Builder builder;
// if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
// builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
// } else {
// builder = new Notification.Builder(this);
// }
// builder.setSmallIcon(R.drawable.ic_launcher);
// builder.setContentTitle("守护服务运行中");
// builder.setContentText("正在监控主服务状态");
// builder.setPriority(Notification.PRIORITY_LOW);
// builder.setOngoing(true); // 不可手动取消
//
// return builder.build();
// }
// ====================== Service 生命周期方法区 ======================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate: 守护服务创建");
// 初始化主服务连接回调
if (mMyServiceConnection == null) {
mMyServiceConnection = new MyServiceConnection();
LogUtils.d(TAG, "onCreate: 初始化 MyServiceConnection 完成");
}
// 初始化运行状态
setIsThreadAlive(false);
// 启动守护逻辑
assistantService();
}
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
return new MyBinder();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
// 每次启动都执行守护逻辑,确保主服务存活
assistantService();
// START_STICKY服务被杀死后系统尝试重启
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: 守护服务销毁");
// 停止线程并解除主服务绑定
setIsThreadAlive(false);
if (mIsBound && mMyServiceConnection != null) {
try {
unbindService(mMyServiceConnection);
LogUtils.d(TAG, "onDestroy: 解除主服务绑定成功");
} catch (IllegalArgumentException e) {
LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e);
}
mIsBound = false;
}
mMainService = null;
}
// ====================== 核心守护逻辑方法区 ======================
/**
* 守护服务核心逻辑:检查配置并保活主服务
*/
private void assistantService() {
LogUtils.d(TAG, "assistantService: 执行守护逻辑");
// 加载主服务配置
reloadMainServiceConfig();
if (mMainServiceBean == null) {
LogUtils.e(TAG, "assistantService: 主服务配置加载失败,终止守护逻辑");
return;
}
LogUtils.d(TAG, "assistantService: 主服务启用状态 | " + mMainServiceBean.isEnable());
// 配置启用且线程未存活时,唤醒并绑定主服务
if (mMainServiceBean.isEnable() && !isThreadAlive()) {
setIsThreadAlive(true);
wakeupAndBindMain();
} else if (!mMainServiceBean.isEnable()) {
setIsThreadAlive(false);
LogUtils.d(TAG, "assistantService: 主服务已禁用,停止保活");
}
}
/**
* 唤醒并绑定主服务 MainService适配后台启动限制
*/
private void wakeupAndBindMain() {
if (mMyServiceConnection == null) {
LogUtils.e(TAG, "wakeupAndBindMain: MyServiceConnection 未初始化,绑定失败");
return;
}
Intent intent = new Intent(this, MainService.class);
// 根据应用前后台状态选择启动方式Android 12+ 后台用 startForegroundService
startForegroundService(intent);
// BIND_IMPORTANT提高绑定优先级主服务被杀时会回调断开
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService");
}
// ====================== 辅助方法区 ======================
/**
* 重新加载主服务配置
*/
private void reloadMainServiceConfig() {
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
LogUtils.d(TAG, "reloadMainServiceConfig: 主服务配置重新加载完成 | " + mMainServiceBean);
}
}

View File

@@ -1,327 +0,0 @@
package cc.winboll.studio.contacts.services;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/04/18 15:00:00 (GMT+8)
* @LastEditTime 2026/04/22 17:20:00 (GMT+8)
* @Describe 限时特殊通道服务
* 提供安全的服务启动、令牌校验及循环任务管理
* 新增功能:
* 1. 计时过程中实时输出剩余秒数
* 2. 服务销毁前,发送本地广播通知应用内其他组件,并携带剩余秒数信息
* 3. 每秒发送一次倒计时状态的本地广播
*/
public class LimitedTimeSpecialChannelService extends Service {
// ========================= 常量定义 =========================
public static final String TAG = "LimitedTimeSpecialChannelService";
public static final String EXTRA_DELAY_MILLIS = "EXTRA_DELAY_MILLIS";
private static final String EXTRA_SECURITY_TOKEN = "EXTRA_SECURITY_TOKEN";
// 本地广播 Action 常量
public static final String ACTION_SERVICE_DESTROYED = "cc.winboll.studio.contacts.services.ACTION_SERVICE_DESTROYED";
// 倒计时心跳广播 Action
public static final String ACTION_COUNTDOWN_TICK = "cc.winboll.studio.contacts.services.ACTION_COUNTDOWN_TICK";
// 广播携带的额外参数:剩余秒数
public static final String EXTRA_REMAINING_SECONDS = "EXTRA_REMAINING_SECONDS";
// 广播携带的额外参数:总定时秒数
public static final String EXTRA_TOTAL_SECONDS = "EXTRA_TOTAL_SECONDS";
// ========================= 静态变量 =========================
/**
* 公共静态私有字段:安全校验令牌
* 确保全局可访问且值不可变更
*/
public static final String mValidToken = "VALID_TOKEN_" + System.currentTimeMillis();
private static volatile LimitedTimeSpecialChannelService sInstance = null;
private static volatile boolean sIsServiceRunning = false;
// ========================= 成员变量 =========================
private final Handler mHandler = new Handler(Looper.getMainLooper());
private long mTotalMillis = 0; // 总定时时长
private long mRemainingMillis = 0; // 剩余时长
private LocalBroadcastManager mLocalBroadcastManager; // 本地广播管理器实例
// ========================= 公共静态方法 =========================
/**
* 公共静态方法:启动服务
* @param context 上下文
* @param delayMillis 定时时长(毫秒)
*/
public static void startService(Context context, long delayMillis) {
LogUtils.i(TAG, "调用静态入口方法 startService");
LogUtils.i(TAG, "入参 - context: " + context + ", delayMillis: " + delayMillis);
if (context == null) {
LogUtils.w(TAG, "启动失败上下文为null");
return;
}
if (isServiceRunning()) {
LogUtils.i(TAG, "服务已运行,忽略重复启动请求");
return;
}
// 构建Intent传递参数
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
intent.putExtra(EXTRA_SECURITY_TOKEN, mValidToken);
intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis);
context.startService(intent);
LogUtils.i(TAG, "服务启动命令已发出");
}
/**
* 公共静态方法:停止服务
* @param context 上下文
*/
public static void stopService(Context context) {
LogUtils.i(TAG, "调用静态入口方法 stopService");
LogUtils.i(TAG, "入参 - context: " + context);
if (context == null) {
LogUtils.w(TAG, "停止失败上下文为null");
return;
}
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
context.stopService(intent);
LogUtils.i(TAG, "服务停止命令已发出");
}
/**
* 公共静态方法:查询服务运行状态
* @return true if running
*/
public static boolean isServiceRunning() {
return sIsServiceRunning;
}
/**
* 【核心单元测试方法】
* 执行完整的单元测试流程
* @param context 上下文
*/
public static void unitTest(Context context) {
LogUtils.i(TAG, "=== 开始执行单元测试 ===");
// 测试1: 初始状态应为未运行
boolean initialState = isServiceRunning();
LogUtils.i(TAG, "测试1 - 初始状态检查: " + (initialState ? "失败(不应运行)" : "成功(未运行)"));
// 启动服务设置5秒定时
startService(context, 5000L);
// 核心修复:同步等待服务启动完成
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
LogUtils.e(TAG, "单元测试等待被中断", e);
}
// 测试3: 验证服务已启动
boolean runningState = isServiceRunning();
LogUtils.i(TAG, "测试3 - 运行状态检查: " + (runningState ? "成功(运行中)" : "失败(未运行)"));
// 测试4: 尝试重复启动
LogUtils.i(TAG, "测试4 - 尝试重复启动(预期忽略)");
startService(context, 5000L);
// 测试5: 等待服务自动停止
LogUtils.i(TAG, "测试5 - 等待服务自动停止(5秒)...");
try {
// 等待超过服务的5秒运行时间确保能观测到销毁状态
Thread.sleep(6000);
} catch (InterruptedException e) {
LogUtils.e(TAG, "测试等待被中断", e);
}
// 验证最终状态
boolean finalState = isServiceRunning();
LogUtils.i(TAG, "测试5 - 最终状态检查: " + (!finalState ? "成功(已销毁)" : "失败(仍运行)"));
LogUtils.i(TAG, "=== 单元测试执行完成 ===");
}
// ========================= 生命周期 =========================
@Override
public void onCreate() {
super.onCreate();
LogUtils.i(TAG, "服务 onCreate创建实例");
sInstance = this;
sIsServiceRunning = true;
// 初始化本地广播管理器
mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
LogUtils.i(TAG, "本地广播管理器初始化完成");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.i(TAG, "服务 onStartCommand处理启动请求");
LogUtils.i(TAG, "入参 - intent: " + intent + ", flags: " + flags + ", startId: " + startId);
if (!isValidToken(intent)) {
LogUtils.w(TAG, "安全校验失败,拒绝启动服务");
stopSelf();
return START_NOT_STICKY;
}
long delayMillis = intent.getLongExtra(EXTRA_DELAY_MILLIS, 0);
if (delayMillis <= 0) {
LogUtils.w(TAG, "无效的定时时长: " + delayMillis + ",服务将退出");
stopSelf();
return START_NOT_STICKY;
}
// 初始化总时长和剩余时长
mTotalMillis = delayMillis;
mRemainingMillis = delayMillis;
LogUtils.i(TAG, "初始化定时时长: " + (mTotalMillis / 1000) + "");
startLoopTask();
// 设置自动停止定时器
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
LogUtils.i(TAG, "定时时长结束,准备停止服务");
stopSelf();
}
}, delayMillis);
return START_STICKY;
}
@Override
public void onDestroy() {
LogUtils.i(TAG, "服务 onDestroy准备销毁");
// 发送本地广播,通知应用内其他组件服务即将销毁
// 此处携带剩余秒数信息,方便接收端知晓具体的结束状态
sendServiceDestroyedBroadcast();
// 重置运行状态
sIsServiceRunning = false;
sInstance = null;
// 清理所有回调
mHandler.removeCallbacksAndMessages(null);
// 执行父类销毁
super.onDestroy();
LogUtils.i(TAG, "服务销毁流程完成");
}
/**
* 发送服务销毁的本地广播
* 确保应用内所有注册了该广播接收器的组件都能收到通知
* [新增] 携带当前剩余秒数信息
*/
private void sendServiceDestroyedBroadcast() {
LogUtils.i(TAG, "准备发送服务销毁本地广播");
try {
Intent intent = new Intent(ACTION_SERVICE_DESTROYED);
// 新增:将当前剩余秒数放入广播附加数据中
intent.putExtra(EXTRA_REMAINING_SECONDS, mRemainingMillis / 1000);
mLocalBroadcastManager.sendBroadcast(intent);
LogUtils.i(TAG, "服务销毁广播发送成功Action: " + ACTION_SERVICE_DESTROYED);
LogUtils.i(TAG, "广播附加数据 - 剩余秒数: " + (mRemainingMillis / 1000));
} catch (Exception e) {
LogUtils.e(TAG, "发送服务销毁广播失败", e);
}
}
/**
* 发送倒计时心跳的本地广播
* 每秒执行一次,携带当前剩余秒数和总时长
*/
private void sendCountdownTickBroadcast() {
try {
Intent intent = new Intent(ACTION_COUNTDOWN_TICK);
intent.putExtra(EXTRA_REMAINING_SECONDS, mRemainingMillis / 1000);
intent.putExtra(EXTRA_TOTAL_SECONDS, mTotalMillis / 1000);
mLocalBroadcastManager.sendBroadcast(intent);
// 日志已精简,只在关键节点打印
} catch (Exception e) {
LogUtils.e(TAG, "发送倒计时广播失败", e);
}
}
@Override
public IBinder onBind(Intent intent) {
LogUtils.i(TAG, "服务 onBind不支持绑定操作");
// 不支持绑定
return null;
}
// ========================= 私有辅助方法 =========================
/**
* 校验令牌有效性
*/
private boolean isValidToken(Intent intent) {
LogUtils.i(TAG, "调用 isValidToken 方法进行安全校验");
LogUtils.i(TAG, "入参 - intent: " + intent);
if (intent == null) {
return false;
}
String incomingToken = intent.getStringExtra(EXTRA_SECURITY_TOKEN);
LogUtils.i(TAG, "接收到外部传入令牌: " + incomingToken);
LogUtils.i(TAG, "本地校验令牌: " + mValidToken);
return mValidToken.equals(incomingToken);
}
/**
* 启动循环任务
*/
private void startLoopTask() {
LogUtils.i(TAG, "启动倒计时循环任务");
mHandler.removeCallbacks(mLoopTaskRunnable);
mHandler.postDelayed(mLoopTaskRunnable, 1000);
}
/**
* 循环任务心跳
* 核心逻辑先递减时长再发送广播确保0秒能被正确发送
*/
private final Runnable mLoopTaskRunnable = new Runnable() {
@Override
public void run() {
if (sIsServiceRunning) {
// 核心修复:先递减剩余时长
if (mRemainingMillis >= 1000) {
mRemainingMillis -= 1000;
} else {
// 兜底防止负数直接置为0
mRemainingMillis = 0;
}
// 计算并发送当前剩余秒数
long remainingSeconds = mRemainingMillis / 1000;
// 日志已精简只在非0状态打印避免日志刷屏
if (remainingSeconds > 0) {
LogUtils.i(TAG, "循环心跳:剩余 " + remainingSeconds + "");
}
// 发送倒计时广播
sendCountdownTickBroadcast();
// 递归调用,形成循环
startLoopTask();
}
}
};
}

View File

@@ -1,593 +0,0 @@
package cc.winboll.studio.contacts.services;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.AudioManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import cc.winboll.studio.contacts.bobulltoon.TomCat;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
import cc.winboll.studio.contacts.model.MainServiceBean;
import cc.winboll.studio.contacts.model.RingTongBean;
import cc.winboll.studio.contacts.receivers.MainReceiver;
import cc.winboll.studio.contacts.utils.NotificationManagerUtils;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/13 06:56:41
* @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动
* 严格适配 Android API 30 + Java 7 语法规范 | 解决前台服务启动超时崩溃
* 核心优化1. 移除延迟启动逻辑 2. 标准化日志管理 3. 强化资源清理 4. 结构分层重构
*/
public class MainService extends Service {
// ====================== 常量定义区全硬编码无高版本API依赖 ======================
public static final String TAG = "MainService";
public static final int MSG_UPDATE_STATUS = 0;
// 铃声音量监控参数(定时检查+恢复)
private static final long VOLUME_CHECK_DELAY = 1000L; // 首次检查延迟1s
private static final long VOLUME_CHECK_PERIOD = 60000L; // 后续每60s检查一次
// 前台服务配置固定ID+渠道,避免重复创建)
private static final String FOREGROUND_CHANNEL_ID = "main_service_foreground_channel";
private static final int FOREGROUND_NOTIFICATION_ID = 1001;
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001; // dataSync类型硬编码
// Android版本常量替代Build.VERSION_CODES适配Java7
private static final int ANDROID_8_API = 26; // Android 8.0
private static final int ANDROID_10_API = 29; // Android 10
private static final int ANDROID_12_API = 31; // Android 12
// 守护服务重绑定延迟(仅保留核心重试逻辑)
private static final long RETRY_DELAY_MS = 3000L;
// ====================== 静态成员属性区全局共享实例统一前缀s ======================
private static MainService sMainServiceInstance; // 主服务全局实例
private static volatile TomCat sTomCatInstance; // 号码识别核心实例volatile保证可见性
// ====================== 成员属性区(业务+UI+资源统一前缀m ======================
private volatile boolean mIsServiceRunning; // 服务运行状态标记volatile防指令重排
private MainServiceBean mMainServiceBean; // 服务配置实体(启用状态存储)
private MainServiceHandler mMainServiceHandler; // 服务消息处理器(主线程通信)
private MyServiceConnection mServiceConnection; // 守护服务连接实例
private AssistantService mAssistantService; // 绑定的守护服务实例
private boolean mIsAssistantBound; // 守护服务绑定状态标记
private MainReceiver mMainReceiver; // 全局广播接收器(监听系统事件)
private Timer mVolumeCheckTimer; // 铃声音量检查定时器(定时恢复配置)
// ====================== 内部类Binder服务绑定通信优先定义 ======================
public class MyBinder extends Binder {
/**
* 外部组件绑定服务时,获取主服务实例
* @return MainService 主服务实例
*/
public MainService getService() {
LogUtils.d(TAG, "MyBinder.getService: 外部获取主服务实例");
return MainService.this;
}
}
// ====================== 内部类ServiceConnection守护服务绑定回调 ======================
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (service == null) {
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的IBinder为空绑定失败");
mIsAssistantBound = false;
return;
}
try {
// 类型转换获取守护服务实例
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
mAssistantService = binder.getService();
mIsAssistantBound = true;
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 守护服务绑定成功");
} catch (ClassCastException e) {
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder类型转换失败", e);
mIsAssistantBound = false;
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 守护服务连接断开");
mAssistantService = null;
mIsAssistantBound = false;
// 服务启用状态下,重试绑定守护服务(主服务存活核心保障)
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: " + RETRY_DELAY_MS + "ms后重试绑定守护服务");
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
wakeupAndBindAssistantService();
}
}, RETRY_DELAY_MS);
} else {
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务已禁用,跳过重试绑定");
}
}
}
// ====================== 对外静态方法区(服务启停/重启/状态查询,全局调用) ======================
/**
* 检查号码是否在BoBullToon库中外部组件调用静态入口
* @param phone 待查询号码
* @return true=是BoBullToon号码false=否/初始化失败
*/
public static boolean isPhoneInBoBullToon(String phone) {
if (sTomCatInstance != null && phone != null && !phone.isEmpty()) {
boolean result = sTomCatInstance.isPhoneBoBullToon(phone);
LogUtils.d(TAG, "isPhoneInBoBullToon: 号码" + phone + "查询结果:" + (result ? "" : ""));
return result;
}
LogUtils.w(TAG, "isPhoneInBoBullToon: TomCat未初始化或号码为空查询失败");
return false;
}
/**
* 停止主服务(仅停止,不修改配置)
* @param context 上下文(非空校验)
*/
public static void stopMainService(Context context) {
if (context == null) {
LogUtils.e(TAG, "stopMainService: 上下文为空,无法停止服务");
return;
}
LogUtils.d(TAG, "stopMainService: 执行停止主服务操作");
context.stopService(new Intent(context, MainService.class));
}
/**
* 启动主服务(仅启动,不修改配置)
* @param context 上下文(非空校验)
*/
public static void startMainService(Context context) {
if (context == null) {
LogUtils.e(TAG, "startMainService: 上下文为空,无法启动服务");
return;
}
LogUtils.d(TAG, "startMainService: 执行启动主服务操作(前台服务模式)");
Intent intent = new Intent(context, MainService.class);
context.startForegroundService(intent);
}
/**
* 重启主服务(先停后启,需服务已启用)
* @param context 上下文(非空校验)
*/
public static void restartMainService(Context context) {
if (context == null) {
LogUtils.e(TAG, "restartMainService: 上下文为空,无法重启服务");
return;
}
LogUtils.d(TAG, "restartMainService: 执行主服务重启流程");
MainServiceBean config = MainServiceBean.loadBean(context, MainServiceBean.class);
if (config != null && config.isEnable()) {
stopMainService(context);
startMainService(context);
LogUtils.i(TAG, "restartMainService: 主服务重启完成");
} else {
LogUtils.w(TAG, "restartMainService: 服务未启用或配置为空,跳过重启");
}
}
/**
* 停止服务并保存禁用状态(更新配置+停止服务)
* @param context 上下文(非空校验)
*/
public static void stopMainServiceAndSaveStatus(Context context) {
if (context == null) {
LogUtils.e(TAG, "stopMainServiceAndSaveStatus: 上下文为空,操作失败");
return;
}
LogUtils.d(TAG, "stopMainServiceAndSaveStatus: 保存禁用状态并停止服务");
MainServiceBean config = new MainServiceBean();
config.setIsEnable(false);
MainServiceBean.saveBean(context, config);
stopMainService(context);
}
/**
* 启动服务并保存启用状态(更新配置+启动服务,先停后启避免重复)
* @param context 上下文(非空校验)
*/
public static void startMainServiceAndSaveStatus(Context context) {
if (context == null) {
LogUtils.e(TAG, "startMainServiceAndSaveStatus: 上下文为空,操作失败");
return;
}
LogUtils.d(TAG, "startMainServiceAndSaveStatus: 保存启用状态并启动服务");
MainServiceBean config = new MainServiceBean();
config.setIsEnable(true);
MainServiceBean.saveBean(context, config);
stopMainService(context); // 先停止旧服务,避免冲突
startMainService(context);
}
// ====================== 核心工具方法区(服务状态检查+前台通知创建,通用功能) ======================
/**
* 补充消息追加方法(外部组件向服务发送消息)
* @param message 待追加消息(空值防护)
*/
public void appenMessage(String message) {
String msg = message == null ? "null" : message;
LogUtils.d(TAG, "appenMessage: 接收外部消息:" + msg);
if (mMainServiceHandler != null) {
android.os.Message handlerMsg = android.os.Message.obtain();
handlerMsg.what = MSG_UPDATE_STATUS;
handlerMsg.obj = msg;
mMainServiceHandler.sendMessage(handlerMsg);
LogUtils.d(TAG, "appenMessage: 消息已发送至Handler处理");
} else {
LogUtils.w(TAG, "appenMessage: MainServiceHandler未初始化消息发送失败");
}
}
/**
* 创建前台服务通知Android8.0+需渠道,低版本兼容)
* @return Notification 前台服务通知实例
*/
// private Notification createForegroundNotification() {
// // 1. Android8.0+创建通知渠道(必需,否则通知不显示)
// if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
// NotificationChannel channel = new NotificationChannel(
// FOREGROUND_CHANNEL_ID,
// "拨号主服务",
// NotificationManager.IMPORTANCE_LOW
// );
// channel.setDescription("主服务后台运行,保障通话监听与号码识别功能正常");
// channel.setSound(null, null); // 关闭通知声音
// channel.enableVibration(false); // 关闭振动
//
// NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// if (notificationManager != null) {
// notificationManager.createNotificationChannel(channel);
// LogUtils.d(TAG, "createForegroundNotification: Android8.0+通知渠道创建成功");
// } else {
// LogUtils.e(TAG, "createForegroundNotification: NotificationManager获取失败渠道创建失败");
// }
// }
//
// // 2. 构建通知实例分版本兼容Builder
// Notification.Builder builder;
// if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
// builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
// } else {
// builder = new Notification.Builder(this);
// }
// builder.setSmallIcon(R.drawable.ic_launcher);
// builder.setContentTitle("拨号服务运行中");
// builder.setContentText("后台保障通话监听与号码识别,请勿手动关闭");
// builder.setPriority(Notification.PRIORITY_LOW); // 低优先级,不打扰用户
// builder.setOngoing(true); // 不可手动清除,保障服务存活
//
// LogUtils.d(TAG, "createForegroundNotification: 前台服务通知构建完成");
// return builder.build();
// }
/**
* 检查指定服务是否正在运行通过ActivityManager查询
* @param serviceClass 待检查服务类
* @return true=运行中false=未运行/查询失败
*/
private boolean isServiceRunning(Class<?> serviceClass) {
if (serviceClass == null) {
LogUtils.e(TAG, "isServiceRunning: 服务类为空,检查失败");
return false;
}
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败检查失败");
return false;
}
// 遍历运行中服务,匹配类名
for (ActivityManager.RunningServiceInfo serviceInfo : activityManager.getRunningServices(Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(serviceInfo.service.getClassName())) {
LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "正在运行");
return true;
}
}
LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "未运行");
return false;
}
// ====================== Service生命周期方法区按执行顺序创建→绑定→启动→销毁 ======================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "===== onCreate: 主服务开始创建 =====");
sMainServiceInstance = this;
mIsServiceRunning = false;
// 初始化核心组件(无延迟,直接初始化)
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
mServiceConnection = new MyServiceConnection();
mMainServiceHandler = new MainServiceHandler(this);
// 初始化音量监控定时器(服务启动即开启,保障音量配置)
initVolumeCheckTimer();
// 执行核心业务启动逻辑(无延迟,优先启动)
startCoreBusiness();
LogUtils.d(TAG, "===== onCreate: 主服务创建完成 =====");
}
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind: 主服务被外部组件绑定Intent=" + (intent != null ? intent.getAction() : "null"));
return new MyBinder();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "onStartCommand: 主服务被启动startId=" + startId);
// 重复启动时再次执行核心业务(避免服务被杀死后重启失败)
startCoreBusiness();
// 服务启用则返回START_STICKY系统杀死后自动重启否则默认行为
int result = (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (result == START_STICKY ? "START_STICKY自动重启" : "默认模式"));
return result;
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁,全量清理资源 =====");
// 1. 停止音量监控定时器(释放线程资源)
cancelVolumeCheckTimer();
// 2. 解除守护服务绑定(避免内存泄漏)
if (mIsAssistantBound && mServiceConnection != null) {
try {
unbindService(mServiceConnection);
LogUtils.d(TAG, "onDestroy: 守护服务绑定已解除");
} catch (IllegalArgumentException e) {
LogUtils.w(TAG, "onDestroy: 解除守护服务绑定失败(服务未绑定)", e);
}
mIsAssistantBound = false;
mServiceConnection = null;
}
// 3. 注销广播接收器(释放系统资源)
if (mMainReceiver != null) {
mMainReceiver.unregisterAction(this);
mMainReceiver = null;
LogUtils.d(TAG, "onDestroy: 全局广播接收器已注销");
}
// 4. 停止所有子服务(强制停止,避免子服务残留)
stopService(new Intent(this, AssistantService.class));
stopService(new Intent(this, CallListenerService.class));
stopService(new Intent(this, MyCallScreeningService.class));
LogUtils.d(TAG, "onDestroy: 所有子服务已强制停止");
// 5. 清空所有引用(静态+成员彻底避免内存泄漏不修改配置Bean
sMainServiceInstance = null;
sTomCatInstance = null;
mMainServiceHandler = null;
mAssistantService = null;
mMainServiceBean = null;
// 标记服务为未运行
mIsServiceRunning = false;
LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成,资源全清理 =====");
}
// ====================== 核心业务逻辑区(服务启动核心流程,无延迟) ======================
/**
* 启动核心业务(主服务核心入口,避免重复启动)
*/
private synchronized void startCoreBusiness() {
// 服务已运行则直接返回,避免重复执行启动流程
if (mIsServiceRunning) {
LogUtils.d(TAG, "startCoreBusiness: 主服务已运行,跳过重复启动");
return;
}
mIsServiceRunning = true;
// 重新加载最新配置(避免配置修改后未生效)
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
if (mMainServiceBean == null || !mMainServiceBean.isEnable()) {
LogUtils.w(TAG, "startCoreBusiness: 服务配置未启用或配置为空,启动流程终止");
mIsServiceRunning = false;
stopSelf(); // 未启用则主动停止服务
return;
}
LogUtils.i(TAG, "startCoreBusiness: 服务配置已启用,启动核心流程");
// 1. 优先启动前台服务(避免前台服务启动超时崩溃,核心优先级)
try {
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(this);
notificationManagerUtils.startForegroundServiceNotify(this, "主要拨号服务已启动。");
LogUtils.i(TAG, "startCoreBusiness: 前台服务启动成功通知ID=" + FOREGROUND_NOTIFICATION_ID);
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "startCoreBusiness: 前台服务启动失败(服务类型不匹配)", e);
mIsServiceRunning = false;
stopSelf();
return;
} catch (Exception e) {
LogUtils.e(TAG, "startCoreBusiness: 前台服务启动异常", e);
mIsServiceRunning = false;
stopSelf();
return;
}
// 2. 绑定守护服务(保障主服务存活,防系统杀死)
wakeupAndBindAssistantService();
// 3. 初始化核心业务组件(号码识别+黑白名单规则)
initTomCatComponent();
initRulesConfig();
// 4. 启动通话监听相关服务(无延迟,直接启动,保障功能生效)
startCallRelatedServices();
LogUtils.i(TAG, "startCoreBusiness: 主服务核心流程启动完成,服务正常运行");
}
/**
* 唤醒并绑定守护服务分版本适配启动方式Android10+前台服务启动)
*/
private void wakeupAndBindAssistantService() {
if (mServiceConnection == null) {
LogUtils.e(TAG, "wakeupAndBindAssistantService: 服务连接实例未初始化,绑定失败");
return;
}
Intent assistantIntent = new Intent(this, AssistantService.class);
// Android10+应用后台启动服务需用前台服务模式
LogUtils.d(TAG, "wakeupAndBindAssistantService: Android10+,前台服务模式启动守护服务");
startService(assistantIntent);
// 绑定守护服务BIND_IMPORTANT高优先级绑定断开时回调
bindService(assistantIntent, mServiceConnection, Context.BIND_IMPORTANT);
LogUtils.d(TAG, "wakeupAndBindAssistantService: 守护服务启动并发起绑定请求");
}
// ====================== 铃声音量监控方法区(定时检查+恢复配置) ======================
/**
* 初始化音量检查定时器(先取消旧定时器,避免重复创建)
*/
private void initVolumeCheckTimer() {
cancelVolumeCheckTimer();
mVolumeCheckTimer = new Timer();
mVolumeCheckTimer.schedule(new TimerTask() {
@Override
public void run() {
checkAndRestoreRingerVolume();
}
}, VOLUME_CHECK_DELAY, VOLUME_CHECK_PERIOD);
LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量监控定时器启动,周期" + VOLUME_CHECK_PERIOD + "ms");
}
/**
* 检查并恢复铃声音量(对比配置值,不一致则恢复,保障用户配置生效)
*/
private void checkAndRestoreRingerVolume() {
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
if (audioManager == null) {
LogUtils.e(TAG, "checkAndRestoreRingerVolume: AudioManager获取失败音量检查终止");
return;
}
// 加载音量配置(无配置则初始化默认值)
RingTongBean ringConfig = RingTongBean.loadBean(this, RingTongBean.class);
if (ringConfig == null) {
ringConfig = new RingTongBean();
RingTongBean.saveBean(this, ringConfig);
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 音量配置未存在,初始化默认配置");
return;
}
try {
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
int configVolume = ringConfig.getStreamVolume();
// 音量不一致则恢复配置值
if (currentVolume != configVolume) {
audioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃声音量已恢复,配置值=" + configVolume + ",原当前值=" + currentVolume);
} else {
LogUtils.v(TAG, "checkAndRestoreRingerVolume: 铃声音量与配置一致,无需调整");
}
} catch (SecurityException e) {
LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足,恢复失败", e);
}
}
/**
* 取消音量检查定时器释放Timer资源避免内存泄漏
*/
private void cancelVolumeCheckTimer() {
if (mVolumeCheckTimer != null) {
mVolumeCheckTimer.cancel();
mVolumeCheckTimer = null;
LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量监控定时器已取消");
}
}
// ====================== 辅助初始化方法区(业务组件初始化,统一归类) ======================
/**
* 初始化TomCat组件号码识别核心加载本地数据
*/
private void initTomCatComponent() {
sTomCatInstance = TomCat.getInstance(this);
if (sTomCatInstance.loadPhoneBoBullToon()) {
LogUtils.d(TAG, "initTomCatComponent: BoBullToon号码库加载成功");
} else {
LogUtils.w(TAG, "initTomCatComponent: BoBullToon号码库未下载加载失败不影响服务运行");
}
}
/**
* 初始化黑白名单规则配置(加载本地规则,保障通话筛选生效)
*/
private void initRulesConfig() {
Rules rules = Rules.getInstance(this);
if (rules != null) {
rules.loadRules();
LogUtils.d(TAG, "initRulesConfig: 黑白名单通话规则加载完成");
} else {
LogUtils.e(TAG, "initRulesConfig: Rules实例获取失败通话规则加载失败");
}
}
/**
* 初始化广播接收器(监听系统事件,如开机启动、网络变化)
*/
private void initMainReceiver() {
if (mMainReceiver == null) {
mMainReceiver = new MainReceiver(this);
mMainReceiver.registerAction(this);
LogUtils.d(TAG, "initMainReceiver: 全局广播接收器注册完成");
} else {
LogUtils.d(TAG, "initMainReceiver: 广播接收器已初始化,跳过重复注册");
}
}
/**
* 启动通话相关服务(通话监听+通话筛选,核心功能服务)
*/
private void startCallRelatedServices() {
// 启动通话监听服务
try {
Intent callListenerIntent = new Intent(this, CallListenerService.class);
startService(callListenerIntent);
LogUtils.d(TAG, "startCallRelatedServices: CallListenerService启动成功");
} catch (Exception e) {
LogUtils.e(TAG, "startCallRelatedServices: CallListenerService启动失败", e);
}
// 启动通话筛选服务API10+生效,低版本兼容)
try {
Intent screeningIntent = new Intent(this, MyCallScreeningService.class);
startService(screeningIntent);
LogUtils.d(TAG, "startCallRelatedServices: MyCallScreeningService启动成功");
} catch (Exception e) {
LogUtils.e(TAG, "startCallRelatedServices: MyCallScreeningService启动失败", e);
}
}
}

View File

@@ -1,246 +0,0 @@
package cc.winboll.studio.contacts.services;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.telecom.CallScreeningService;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import cc.winboll.studio.contacts.model.MainServiceBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Describe 通话筛选服务(无前台服务),负责识别通话类型、拦截指定号码、处理正常通话逻辑
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 强化空指针防护 | 无冗余代码
*/
@RequiresApi(api = 29) // 父类 CallScreeningService 最低要求 API29明确标注
public class MyCallScreeningService extends CallScreeningService {
// ====================== 常量定义区全硬编码无高版本API依赖 ======================
public static final String TAG = "MyCallScreeningService";
// 通话方向常量(硬编码替代 Call.Details 高版本字段适配API29-30
private static final int CALL_DIRECTION_INCOMING = 1; // 来电
private static final int CALL_DIRECTION_OUTGOING = 2; // 外拨
// ====================== 成员属性区(精简必要属性,命名规范) ======================
private Context mContext; // 上下文对象,避免重复调用 getApplicationContext()
// ====================== Service生命周期方法区按执行顺序排列 ======================
@Override
public void onCreate() {
super.onCreate();
mContext = this;
LogUtils.d(TAG, "===== onCreate: 通话筛选服务启动 =====");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "onStartCommand: 服务被启动startId=" + startId);
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY自动重启" : "默认模式"));
return startMode;
}
@Override
public void onDestroy() {
super.onDestroy();
// 置空上下文,释放引用,避免内存泄漏
mContext = null;
LogUtils.d(TAG, "===== onDestroy: 通话筛选服务销毁完成 =====");
}
// ====================== 核心通话筛选方法区(父类抽象方法实现) ======================
/**
* 核心通话筛选入口API29-30标准方法100%兼容)
* 功能:识别通话号码/类型、拦截指定号码、处理正常通话
*/
@Override
@RequiresApi(api = 29)
public void onScreenCall(@NonNull android.telecom.Call.Details details) {
LogUtils.d(TAG, "===== onScreenCall: 开始筛选通话 =====");
// 1. 安全获取通话号码多层空指针防护Java7规范写法
String phoneNumber = getSafePhoneNumber(details);
// 2. 识别通话方向(来电/外拨/未知)
int callDirection = details.getCallDirection();
String callTypeDesc = getCallDirectionDesc(callDirection);
int callState = getCallStateByDirection(callDirection);
LogUtils.d(TAG, "筛选结果:通话类型=" + callTypeDesc + ",号码=" + phoneNumber);
// 3. 自定义拦截逻辑示例拦截指定号码10086可按需扩展黑白名单
boolean isNeedBlock = isTargetBlockNumber(phoneNumber);
// 4. 构建筛选响应Java7分步调用不使用链式写法避免兼容问题
CallResponse callResponse = buildCallScreeningResponse(isNeedBlock);
// 5. 提交筛选结果(必须调用父类方法,完成拦截/放行逻辑)
respondToCall(details, callResponse);
// 6. 分场景处理后续逻辑(拦截日志/正常通话业务)
handleCallAfterScreening(phoneNumber, callTypeDesc, isNeedBlock);
LogUtils.d(TAG, "===== onScreenCall: 通话筛选完成 =====");
}
// ====================== 业务逻辑方法区(按功能拆分,低耦合) ======================
/**
* 安全获取通话号码(多层空指针+空字符串防护,避免崩溃)
*/
private String getSafePhoneNumber(android.telecom.Call.Details details) {
String phoneNumber = "未知号码";
if (details == null) {
LogUtils.w(TAG, "getSafePhoneNumber: 通话详情为空,无法获取号码");
return phoneNumber;
}
Uri handle = details.getHandle();
if (handle != null) {
String schemePart = handle.getSchemeSpecificPart();
if (schemePart != null && !schemePart.trim().isEmpty()) {
phoneNumber = schemePart.trim();
LogUtils.d(TAG, "getSafePhoneNumber: 成功获取号码,原始值=" + schemePart + ",处理后=" + phoneNumber);
} else {
LogUtils.w(TAG, "getSafePhoneNumber: 号码格式异常schemePart=" + schemePart);
}
} else {
LogUtils.w(TAG, "getSafePhoneNumber: 通话 handle 为空,无法获取号码");
}
return phoneNumber;
}
/**
* 判断是否为目标拦截号码可扩展黑白名单逻辑当前示例拦截10086
*/
private boolean isTargetBlockNumber(String phoneNumber) {
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
LogUtils.w(TAG, "isTargetBlockNumber: 号码为空,不拦截");
return false;
}
// 示例拦截逻辑:拦截 10086实际可替换为黑白名单查询
boolean isBlock = "10086".equals(phoneNumber.trim());
if (isBlock) {
LogUtils.d(TAG, "isTargetBlockNumber: 命中拦截规则,号码=" + phoneNumber);
} else {
LogUtils.d(TAG, "isTargetBlockNumber: 未命中拦截规则,号码=" + phoneNumber);
}
return isBlock;
}
/**
* 构建通话筛选响应(按需配置拦截/放行参数适配API29-30
*/
private CallResponse buildCallScreeningResponse(boolean isNeedBlock) {
CallResponse.Builder responseBuilder = new CallResponse.Builder();
// 拦截配置:是否禁止通话+是否拒绝通话(两者配合实现拦截)
responseBuilder.setDisallowCall(isNeedBlock);
responseBuilder.setRejectCall(isNeedBlock);
// 日志/通知配置:拦截的通话跳过日志和通知,正常通话保留
responseBuilder.setSkipCallLog(isNeedBlock);
responseBuilder.setSkipNotification(isNeedBlock);
CallResponse response = responseBuilder.build();
LogUtils.d(TAG, "buildCallScreeningResponse: 响应构建完成,拦截状态=" + isNeedBlock);
return response;
}
/**
* 筛选后分场景处理(拦截日志/正常通话业务扩展)
*/
private void handleCallAfterScreening(String phoneNumber, String callTypeDesc, boolean isNeedBlock) {
if (isNeedBlock) {
// 拦截场景:仅打日志(可扩展:添加拦截记录、本地存储等)
LogUtils.d(TAG, "handleCallAfterScreening: 已拦截通话,类型=" + callTypeDesc + ",号码=" + phoneNumber);
} else {
// 正常通话场景:处理业务逻辑(可扩展:通话记录、广播通知、号码识别等)
int callState = getCallStateByDirection(getCallDirectionFromDesc(callTypeDesc));
handleNormalCallBusiness(phoneNumber, callState);
}
}
/**
* 正常通话业务处理(核心业务扩展入口,强化空指针防护)
*/
private void handleNormalCallBusiness(String phoneNumber, int callState) {
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
LogUtils.w(TAG, "handleNormalCallBusiness: 号码为空,跳过业务处理");
return;
}
String callStateDesc = getCallStateDesc(callState);
LogUtils.d(TAG, "handleNormalCallBusiness: 处理正常通话业务,号码=" + phoneNumber + ",状态=" + callStateDesc);
// 此处可扩展业务逻辑(示例):
// 1. 保存通话记录到本地
// 2. 发送广播通知其他组件(如通话监听服务)
// 3. 调用号码识别接口,匹配联系人信息
}
// ====================== 工具辅助方法区(统一归类,复用性强) ======================
/**
* 通话方向转文字描述(便于日志查看,快速定位场景)
*/
private String getCallDirectionDesc(int callDirection) {
switch (callDirection) {
case CALL_DIRECTION_INCOMING:
return "来电";
case CALL_DIRECTION_OUTGOING:
return "外拨";
default:
return "未知通话";
}
}
/**
* 文字描述转通话方向(配合业务逻辑反向匹配,避免重复判断)
*/
private int getCallDirectionFromDesc(String callTypeDesc) {
if ("来电".equals(callTypeDesc)) {
return CALL_DIRECTION_INCOMING;
} else if ("外拨".equals(callTypeDesc)) {
return CALL_DIRECTION_OUTGOING;
} else {
return -1; // 未知方向
}
}
/**
* 通话方向转 TelephonyManager 状态(统一状态标准,便于业务复用)
*/
private int getCallStateByDirection(int callDirection) {
switch (callDirection) {
case CALL_DIRECTION_INCOMING:
return TelephonyManager.CALL_STATE_RINGING; // 来电=响铃中
case CALL_DIRECTION_OUTGOING:
return TelephonyManager.CALL_STATE_OFFHOOK; // 外拨=通话中
default:
return TelephonyManager.CALL_STATE_IDLE; // 未知=空闲
}
}
/**
* TelephonyManager 状态转文字描述(统一日志格式,提升可读性)
*/
private String getCallStateDesc(int callState) {
switch (callState) {
case TelephonyManager.CALL_STATE_RINGING:
return "响铃中";
case TelephonyManager.CALL_STATE_OFFHOOK:
return "通话中";
case TelephonyManager.CALL_STATE_IDLE:
return "空闲";
default:
return "未知状态";
}
}
}

View File

@@ -1,104 +0,0 @@
package cc.winboll.studio.contacts.threads;
import android.content.Context;
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
import cc.winboll.studio.libappbase.LogUtils;
import java.lang.ref.WeakReference;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/02/14 03:46:44
* @Describe 主服务后台工作线程,负责定时轮询与消息调度
*/
public class MainServiceThread extends Thread {
// ====================== 常量定义区 ======================
public static final String TAG = "MainServiceThread";
// 线程休眠周期1秒
private static final long THREAD_SLEEP_INTERVAL = 1000L;
// ====================== 静态成员变量区 ======================
private static volatile MainServiceThread sInstance;
// ====================== 成员变量区 ======================
// 线程运行控制标记
private volatile boolean mIsExit;
private volatile boolean mIsStarted;
// 弱引用持有上下文和Handler避免内存泄漏
private WeakReference<Context> mContextWeakRef;
private WeakReference<MainServiceHandler> mHandlerWeakRef;
// ====================== 私有构造函数 ======================
private MainServiceThread(Context context, MainServiceHandler handler) {
this.mContextWeakRef = new WeakReference<>(context);
this.mHandlerWeakRef = new WeakReference<>(handler);
this.mIsExit = false;
this.mIsStarted = false;
LogUtils.d(TAG, "MainServiceThread: 线程实例初始化完成");
}
// ====================== 单例获取方法 ======================
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
// 若已有实例,先标记退出并销毁旧实例
if (sInstance != null) {
LogUtils.d(TAG, "getInstance: 存在旧线程实例,标记退出");
sInstance.setIsExit(true);
sInstance = null;
}
// 创建新线程实例
sInstance = new MainServiceThread(context, handler);
LogUtils.d(TAG, "getInstance: 新线程实例已创建");
return sInstance;
}
// ====================== 运行状态控制方法 ======================
public void setIsExit(boolean isExit) {
this.mIsExit = isExit;
LogUtils.d(TAG, "setIsExit: 线程退出标记已更新 | " + isExit);
}
public boolean isExit() {
return mIsExit;
}
public void setIsStarted(boolean isStarted) {
this.mIsStarted = isStarted;
}
public boolean isStarted() {
return mIsStarted;
}
// ====================== 线程核心执行方法 ======================
@Override
public void run() {
// 防止重复启动
if (mIsStarted) {
LogUtils.w(TAG, "run: 线程已启动,避免重复执行");
return;
}
// 标记线程启动状态
mIsStarted = true;
LogUtils.i(TAG, "run: 线程开始运行");
// 线程主循环
while (!mIsExit) {
try {
// 此处可添加业务逻辑(如定时任务、消息分发)
Thread.sleep(THREAD_SLEEP_INTERVAL);
} catch (InterruptedException e) {
LogUtils.e(TAG, "run: 线程休眠被中断", e);
// 恢复线程中断状态
Thread.currentThread().interrupt();
}
}
// 线程退出清理
mIsStarted = false;
mContextWeakRef.clear();
mHandlerWeakRef.clear();
sInstance = null;
LogUtils.i(TAG, "run: 线程正常退出");
}
}

View File

@@ -1,268 +0,0 @@
package cc.winboll.studio.contacts.utils;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import cc.winboll.studio.contacts.MainActivity;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/27 14:27
* @Describe 应用权限设置页跳转工具类,适配主流手机厂商的权限页路径,跳转失败时降级到应用详情页
*/
public class AppGoToSettingsUtil {
// ====================== 常量定义区 ======================
public static final String TAG = "AppGoToSettingsUtil";
// 跳转设置页的 Activity 结果码,复用 MainActivity 的请求码
public static final int ACTIVITY_RESULT_APP_SETTINGS = MainActivity.REQUEST_APP_SETTINGS;
// 主流手机厂商品牌常量
private static final String MANUFACTURER_HUAWEI = "Huawei";
private static final String MANUFACTURER_MEIZU = "Meizu";
private static final String MANUFACTURER_XIAOMI = "Xiaomi";
private static final String MANUFACTURER_SONY = "Sony";
private static final String MANUFACTURER_OPPO = "OPPO";
private static final String MANUFACTURER_LG = "LG";
private static final String MANUFACTURER_VIVO = "vivo";
private static final String MANUFACTURER_SAMSUNG = "samsung";
private static final String MANUFACTURER_LETV = "Letv";
private static final String MANUFACTURER_ZTE = "ZTE";
private static final String MANUFACTURER_YULONG = "YuLong";
private static final String MANUFACTURER_LENOVO = "LENOVO";
// ====================== 成员变量区 ======================
// 标记当前跳转的是应用详情页(true)还是厂商权限页(false)
public static boolean isAppSettingOpen = false;
// ====================== 核心跳转方法区 ======================
/**
* 跳转到对应品牌手机的系统权限设置页,跳转失败则降级到应用详情页
* @param activity 上下文 Activity
*/
public static void goToSetting(Activity activity) {
// 空值校验,避免空指针异常
if (activity == null) {
LogUtils.e(TAG, "goToSetting: Activity 为 null无法跳转设置页");
return;
}
String manufacturer = Build.MANUFACTURER;
LogUtils.d(TAG, "goToSetting: 当前设备厂商 | " + manufacturer);
// 根据厂商跳转对应权限页
switch (manufacturer) {
case MANUFACTURER_HUAWEI:
gotoHuaweiSetting(activity);
break;
case MANUFACTURER_MEIZU:
gotoMeizuSetting(activity);
break;
case MANUFACTURER_XIAOMI:
gotoXiaomiSetting(activity);
break;
case MANUFACTURER_SONY:
gotoSonySetting(activity);
break;
case MANUFACTURER_OPPO:
gotoOppoSetting(activity);
break;
case MANUFACTURER_LG:
gotoLgSetting(activity);
break;
case MANUFACTURER_LETV:
gotoLetvSetting(activity);
break;
default:
LogUtils.w(TAG, "goToSetting: 未适配当前厂商,跳转应用详情页");
openAppDetailSetting(activity);
break;
}
}
// ====================== 各厂商权限页跳转方法区 ======================
/**
* 跳转华为手机权限设置页
*/
private static void gotoHuaweiSetting(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.huawei.systemmanager",
"com.huawei.permissionmanager.ui.MainActivity"));
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoHuaweiSetting: 跳转华为权限设置页成功");
} catch (Exception e) {
LogUtils.e(TAG, "gotoHuaweiSetting: 跳转失败,降级到应用详情页", e);
openAppDetailSetting(activity);
}
}
/**
* 跳转魅族手机权限设置页
*/
private static void gotoMeizuSetting(Activity activity) {
try {
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("packageName", activity.getPackageName());
activity.startActivity(intent);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoMeizuSetting: 跳转魅族权限设置页成功");
} catch (Exception e) {
LogUtils.e(TAG, "gotoMeizuSetting: 跳转失败,降级到应用详情页", e);
openAppDetailSetting(activity);
}
}
/**
* 跳转小米手机权限设置页
*/
private static void gotoXiaomiSetting(Activity activity) {
try {
// 适配 MIUI 8/9 及以上版本
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
intent.setClassName("com.miui.securitycenter",
"com.miui.permcenter.permissions.PermissionsEditorActivity");
intent.putExtra("extra_pkgname", activity.getPackageName());
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI8+)成功");
} catch (Exception e) {
try {
// 适配 MIUI 5/6/7 版本
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
intent.setClassName("com.miui.securitycenter",
"com.miui.permcenter.permissions.AppPermissionsEditorActivity");
intent.putExtra("extra_pkgname", activity.getPackageName());
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI5-7)成功");
} catch (Exception e1) {
LogUtils.e(TAG, "gotoXiaomiSetting: 所有版本适配失败,降级到应用详情页", e1);
openAppDetailSetting(activity);
}
}
}
/**
* 跳转索尼手机权限设置页
*/
private static void gotoSonySetting(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.sonymobile.cta",
"com.sonymobile.cta.SomcCTAMainActivity"));
activity.startActivity(intent);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoSonySetting: 跳转索尼权限设置页成功");
} catch (Exception e) {
LogUtils.e(TAG, "gotoSonySetting: 跳转失败,降级到应用详情页", e);
openAppDetailSetting(activity);
}
}
/**
* 跳转OPPO手机权限设置页
*/
private static void gotoOppoSetting(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.color.safecenter",
"com.color.safecenter.permission.PermissionManagerActivity"));
activity.startActivity(intent);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoOppoSetting: 跳转OPPO权限设置页成功");
} catch (Exception e) {
LogUtils.e(TAG, "gotoOppoSetting: 跳转失败,降级到应用详情页", e);
openAppDetailSetting(activity);
}
}
/**
* 跳转LG手机权限设置页
*/
private static void gotoLgSetting(Activity activity) {
try {
Intent intent = new Intent("android.intent.action.MAIN");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.android.settings",
"com.android.settings.Settings$AccessLockSummaryActivity"));
activity.startActivity(intent);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoLgSetting: 跳转LG权限设置页成功");
} catch (Exception e) {
LogUtils.e(TAG, "gotoLgSetting: 跳转失败,降级到应用详情页", e);
openAppDetailSetting(activity);
}
}
/**
* 跳转乐视手机权限设置页
*/
private static void gotoLetvSetting(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.letv.android.letvsafe",
"com.letv.android.letvsafe.PermissionAndApps"));
activity.startActivity(intent);
isAppSettingOpen = false;
LogUtils.d(TAG, "gotoLetvSetting: 跳转乐视权限设置页成功");
} catch (Exception e) {
LogUtils.e(TAG, "gotoLetvSetting: 跳转失败,降级到应用详情页", e);
openAppDetailSetting(activity);
}
}
// ====================== 降级跳转方法区 ======================
/**
* 跳转系统设置主界面
*/
public static void gotoSystemConfig(Activity activity) {
if (activity == null) {
LogUtils.e(TAG, "gotoSystemConfig: Activity 为 null无法跳转");
return;
}
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivity(intent);
LogUtils.d(TAG, "gotoSystemConfig: 跳转系统设置主界面成功");
}
/**
* 获取应用详情页的 Intent
*/
private static Intent getAppDetailSettingIntent(Activity activity) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
return intent;
}
/**
* 打开应用详情设置页
*/
public static void openAppDetailSetting(Activity activity) {
if (activity == null) {
LogUtils.e(TAG, "openAppDetailSetting: Activity 为 null无法跳转");
return;
}
activity.startActivityForResult(getAppDetailSettingIntent(activity), ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = true;
LogUtils.d(TAG, "openAppDetailSetting: 跳转应用详情设置页成功");
}
}

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