Compare commits

...

102 Commits

Author SHA1 Message Date
ZhanGSKen 3fde2551f9 源码整理 2026-07-21 11:43:45 +08:00
MiMo 3a8d509f4e 忽略其他开发端源码修改,先以本机版本为基础,再另行调整。 2026-07-21 11:33:37 +08:00
MiMo da529c737b refactor(views): DunTemperatureView 颜色值提取为静态常量
- 新增 COLOR_GRADIENT_LOW/MID/HIGH 渐变颜色常量
- 新增 COLOR_BORDER 边框颜色常量
- 新增 COLOR_TEXT 文本颜色常量
- 替换 mGradientColors、mBorderColor、mTextColor 为常量引用
2026-07-21 11:25:55 +08:00
ZhanGSKen b1be995092 源码整理 2026-07-21 11:03:50 +08:00
ZhanGSKen 4247934a20 设置应用开发版本命名标准与APPBase分支对齐。 2026-07-21 10:56:44 +08:00
ZhanGSKen bf5fe296ff 切换应用基准开发航线 2026-07-21 10:52:37 +08:00
MiMo f6bc48d5d2 feat: 添加绿色通道悬浮窗调试入口
- 新增 GreenChannelFloatingWindow 悬浮窗管理类,支持 API26+ 窗口类型适配
- 新增 view_green_channel_floating.xml 悬浮窗布局,LimitedTimeGreenChannelView 占满全屏无边距
- DebugToolButtonsView 调试工具按钮组添加绿色通道按钮,点击打开悬浮窗
2026-07-20 20:47:34 +08:00
MiMo e2c9f362fc feat: 绿色通道状态广播通信机制
- 新增绿色通道状态变更本地广播 Action 和参数常量
- 新增 sendGreenChannelStateChangedBroadcast() 静态方法
- 开关切换时发送状态广播通知 Activity 更新状态栏
- 服务销毁时发送关闭广播确保状态同步
2026-07-20 14:51:55 +08:00
MiMo 7f53355005 fix: 绿色通道关闭时状态栏颜色恢复修复
- 修复状态栏默认颜色为0(透明)时无法恢复的问题
- 从主题中获取默认状态栏颜色作为fallback
- 广播接收器统一调用updateStatusBarForGreenChannel()方法
- 消除重复代码,提高可维护性
2026-07-20 14:30:39 +08:00
MiMo f09ab4036f feat: 为绿色通道开关添加持久化存储
- SettingsBean新增isEnableGreenChannel属性及JSON序列化支持
- LimitedTimeGreenChannelView新增loadSavedGreenChannelState()加载持久状态
- LimitedTimeGreenChannelView新增saveGreenChannelState()保存开关状态
- App重启后绿色通道开关状态自动恢复
2026-07-20 13:46:14 +08:00
MiMo 405ebe41db refactor: 将'特殊通道'重命名为'绿色通道'
- 重命名 LimitedTimeSpecialChannelService → LimitedTimeGreenChannelService
- 重命名 LimitedTimeSpecialChannelView → LimitedTimeGreenChannelView
- 重命名 view_limitedtimespecialchannel.xml → view_limitedtimegreenchannel.xml
- 更新 activity_settings.xml 显示文本和控件引用
- 更新 AndroidManifest.xml Service 注册
- 更新 Rules.java 和 UnitTestActivity.java 引用
- 更新所有日志和注释中的'特殊'为'绿色'

BREAKING CHANGE: 类名和布局文件名已变更,外部引用需同步更新
2026-07-20 13:22:09 +08:00
ZhanGSKen 54497d8adb 设置动态更新最新版类库的Gradle配置。 2026-07-20 12:47:24 +08:00
ZhanGSKen b50de904f2 <contacts>APK 15.20.11 release Publish. 2026-07-19 06:16:39 +08:00
ZhanGSKen 18e41a7a8d 云盾Buff加成。 2026-07-19 06:12:57 +08:00
ZhanGSKen 8713872d62 UI调整 2026-07-19 05:59:34 +08:00
ZhanGSKen b16f29a43f 把应用日志窗口调整为应用调试资源 2026-07-19 05:51:13 +08:00
MiMo ebaf487d0a 调试类型控件与应用类型控件进行分类管理。 2026-07-19 05:29:41 +08:00
MiMo d5d230c1bb feat: 新增云盾查询Fragment替换日志Fragment
- 新增 DunQueryFragment 和 DunQueryView 控件
- 主界面第三个标签页从应用日志改为云盾查询
- 移除 LogFragment 及相关布局文件
- 移除 SettingsActivity 中的 DunQueryView 控件
2026-07-19 05:07:03 +08:00
MiMo 1d0322252e refactor: 精简调试工具按钮组
移除冗余按钮,仅保留测试崩溃和布局草稿两个功能:
- 移除:清除数据、查看SP、查看日志、重启服务、应用信息
- 测试崩溃改为getString循环触发崩溃
2026-07-19 04:17:39 +08:00
MiMo 22f9eff429 feat: LayoutDraftPreViewActivity添加工具栏
仿照MainActivity为布局草稿预览窗口添加工具栏:
- 继承改为AppCompatActivity以支持Toolbar
- 添加返回按钮和页面标题
- 点击返回按钮关闭页面
2026-07-18 20:12:34 +08:00
MiMo c8e55730f3 feat: 调试工具按钮组添加布局草稿预览入口
在DebugToolButtonsView中新增布局草稿按钮,
点击后跳转LayoutDraftPreViewActivity窗口。
2026-07-18 20:05:42 +08:00
MiMo 5d73960a54 feat: 添加SettingsActivity调试工具按钮组
新增调试工具按钮组控件,仅在调试模式下动态加载:
- view_debug_tool_buttons.xml: 按钮组布局文件
- DebugToolButtonsView.java: 自定义View控件
- SettingsActivity: isDebugging()时加载调试面板

功能按钮:清除数据、查看SP、测试崩溃、查看日志、重启服务、应用信息
2026-07-18 19:58:25 +08:00
ZhanGSKen 2661bd1d5f 添加Http登录验证按钮图片资源 2026-07-18 19:38:33 +08:00
ZhanGSKen 3aaa7ab4fb 把原草稿布局文件方式改为调试预览窗口方式的类源码与资源 2026-07-18 19:37:38 +08:00
ZhanGSKen c75344c238 添加项目开发时,xml布局文件设计的辅助草稿文件。方便快捷调整视图控件属性。 2026-07-18 19:23:38 +08:00
ZhanGSKen 927dfc47ff <contacts>APK 15.20.10 release Publish. 2026-07-18 18:56:11 +08:00
MiMo 7587fba89b fix: 更新contacts模块Maven依赖版本号
libaes 15.20.16 → 15.20.21
libappbase 15.20.33 → 15.20.34

原版本号在Nexus仓库中不存在,导致编译失败。
更新为仓库中可用的最新15.20.x版本。
2026-07-18 18:42:04 +08:00
ZhanGSKen ee73bc3b78 更新源码仓库地址 2026-07-18 16:52:46 +08:00
ZhanGSKen fa95bcd873 恢复原WinBoLL项目保持的Contacts分支模块源码 2026-07-18 16:49:15 +08:00
ZhanGSKen e136045d1f Merge remote-tracking branch 'winboll_https/projects_keeper_tag' into mywinboll 2026-07-11 20:21:13 +08:00
qinglong bd635425a1 合并模块AES 同步最新时间标签aes-v15.20.18 2026-07-11 20:17:35 +08:00
BigPickle 49414a3938 App/MainActivity: 修复主题切换 crash 及工具栏主题不刷新问题
- App.onCreate 添加 AESThemeUtil.init(null) 初始化 themeStyleIDList,
  修复点击主题菜单时 IndexOutOfBoundsException 崩溃
- MainActivity.onCreate 添加 AESThemeUtil.applyAppCompatTheme(this)
  置于 super.onCreate 之前,使 recreate() 后应用已保存的主题样式
2026-07-02 01:34:58 +08:00
BigPickle 08a47768a1 MainActivity: 接入 AESThemeUtil/DevelopUtils 工具类,替换 Toolbar 为 ASupportToolbar
- MainActivity 新增 onCreateOptionsMenu/onOptionsItemSelected,
  使用 AESThemeUtil.inflateCompatThemeMenu 加载主题菜单、
  DevelopUtils.inflateMenu 加载调试菜单
- 布局文件 Toolbar 替换为 cc.winboll.studio.libaes.views.ASupportToolbar,
  移除 AppBarLayout 包裹层
- MyAppTheme 日间/夜间主题增加 aSupportToolbar 属性引用 AESASupportToolbar 样式
2026-07-02 01:26:50 +08:00
ZhanGSKen 79f161b559 fix: 修复 MyAppTheme 主题继承链,适配 Android 16(SDK 36)
- 修复 view_log.xml 自定义属性解析失败崩溃(需要 APPBaseTheme 的属性集)
- 修复 AppCompatActivity 需要 Theme.AppCompat 子主题的 IllegalStateException
- 父主题改为 Theme.AppCompat.Light.NoActionBar / Theme.AppCompat.NoActionBar
- 新增 values-night/ 日夜模式自动切换支持
- 主色调从 Teal/Orange 更换为 Blue/Amber 专业配色方案
2026-07-01 17:06:20 +08:00
LaizyBoy b14538db47 20260701_163734_738 2026-07-01 16:37:41 +08:00
ZhanGSKen c9e16a9a1e 编译配置完成 2026-07-01 15:53:23 +08:00
ZhanGSKen 42dbfb4238 加入WinBoLL框架文件 2026-07-01 15:47:18 +08:00
ZhanGSKen b274503187 20260701_154438_390 2026-07-01 15:45:11 +08:00
ZhanGSKen b50e4e02ea 修复git管理脚本 2026-07-01 04:49:24 +08:00
ZhanGSKen 84f1c820cb 修复git管理脚本 2026-07-01 04:48:30 +08:00
ZhanGSKen 0e60d75ac6 修复git管理脚本 2026-07-01 04:44:47 +08:00
ZhanGSKen 773cc9f6c1 修复git管理脚本 2026-07-01 04:42:45 +08:00
ZhanGSKen ca33dab4da 修复git管理脚本 2026-07-01 04:33:36 +08:00
ZhanGSKen eee8809485 修复git管理脚本 2026-07-01 04:11:36 +08:00
ZhanGSKen 69f79549e9 合并模块WinBoLL 同步最新时间标签winboll-v15.20.8 2026-07-01 04:10:40 +08:00
ZhanGSKen f204cd2608 修复git管理脚本 2026-07-01 04:08:42 +08:00
ZhanGSKen 2d3592690a 更新新版winboll架构 2026-07-01 03:49:09 +08:00
ZhanGSKen 576f8bb5e5 更新新版winboll架构 2026-07-01 03:48:29 +08:00
qinglong e787697d6b 合并模块AES 同步最新时间标签aes-v15.20.17 2026-06-30 19:00:05 +08:00
qinglong 3db3edcf2c 合并模块APPBase 同步最新时间标签appbase-v15.20.34 2026-06-30 19:00:05 +08:00
qinglong 643d08bf22 合并模块AES 同步最新时间标签aes-v15.20.16 2026-06-26 02:00:28 +08:00
qinglong beefb16c32 合并模块APPBase 同步最新时间标签appbase-v15.20.33 2026-06-26 02:00:28 +08:00
qinglong 878009ec9f 合并模块WinBoLL 同步最新时间标签winboll-v15.20.8 2026-06-26 02:00:28 +08:00
qinglong 2d8b5d515b 合并模块AES 同步最新时间标签aes-v15.20.15 2026-06-03 20:30:22 +08:00
qinglong 1453287667 合并模块APPBase 同步最新时间标签appbase-v15.20.26 2026-06-03 20:30:22 +08:00
qinglong 5fc59f3305 合并模块AES 同步最新时间标签aes-v15.20.14 2026-06-03 08:00:01 +08:00
qinglong b6ba73126f 合并模块WinBoLL 同步最新时间标签winboll-v15.20.7 2026-06-03 08:00:01 +08:00
qinglong bdd8d1e2b6 合并模块APPBase 同步最新时间标签appbase-v15.20.25 2026-06-03 07:00:01 +08:00
qinglong 138ca80e35 合并模块APPBase 同步最新时间标签appbase-v15.20.23 2026-06-03 06:06:41 +08:00
qinglong 7a3b54b78b 合并模块WinBoLL 同步最新时间标签winboll-v15.20.6 2026-06-02 21:00:01 +08:00
qinglong db804d1897 合并模块AES 同步最新时间标签aes-v15.20.12 2026-06-02 08:55:05 +08:00
qinglong 039c8fcd98 合并模块WinBoLL 同步最新时间标签winboll-v15.20.5 2026-06-02 04:00:01 +08:00
qinglong ae63d1ec0a 合并模块AES 同步最新时间标签aes-v15.20.11 2026-06-02 03:00:01 +08:00
qinglong 9e9486b488 合并模块WinBoLL 同步最新时间标签winboll-v15.20.4 2026-06-01 21:00:01 +08:00
qinglong 4b8967b253 合并模块WinBoLL 同步最新时间标签winboll-v15.20.3 2026-05-31 21:00:02 +08:00
qinglong b1059c3f46 合并模块AES 同步最新时间标签aes-v15.20.10 2026-05-27 20:26:41 +08:00
qinglong f3d723fbee 合并模块APPBase 同步最新时间标签appbase-v15.20.22 2026-05-27 15:00:01 +08:00
qinglong e3c30ea9a3 合并模块AES 同步最新时间标签aes-v15.20.9 2026-05-24 10:49:26 +08:00
qinglong ab4cef21f0 合并模块APPBase 同步最新时间标签appbase-v15.20.21 2026-05-24 10:00:01 +08:00
QingLong 2754a2ad7c 合并模块AES 同步最新时间标签aes-v15.20.8 2026-05-20 21:00:01 +08:00
QingLong 8876896cbc 合并模块APPBase 同步最新时间标签appbase-v15.20.20 2026-05-20 17:00:01 +08:00
QingLong 28e6a8ee78 合并模块APPBase 同步最新时间标签appbase-v15.20.19 2026-05-20 15:00:01 +08:00
QingLong 7d872fd14c 合并模块APPBase 同步最新时间标签appbase-v15.20.18 2026-05-20 04:00:01 +08:00
QingLong d6fab2133f 合并模块AES 同步最新时间标签aes-v15.20.7 2026-05-19 21:00:01 +08:00
QingLong 45821b8daa 合并模块APPBase 同步最新时间标签appbase-v15.20.17 2026-05-19 21:00:01 +08:00
QingLong edd312a525 合并模块APPBase 同步最新时间标签appbase-v15.20.16 2026-05-19 19:00:01 +08:00
QingLong aec680e36e 合并模块AES 同步最新时间标签aes-v15.20.6 2026-05-19 01:53:02 +08:00
QingLong 1c6f7783e5 合并模块AES 同步最新时间标签aes-v15.20.5 2026-05-19 01:49:06 +08:00
qinglong 193011a80d 合并模块AES 同步最新时间标签aes-v15.20.4 2026-05-17 21:00:01 +08:00
qinglong eb3d8f4806 合并模块APPBase 同步最新时间标签appbase-v15.20.15 2026-05-17 16:20:01 +08:00
ZhanGSKen aa1b701c8b 合并模块APPBase 同步最新时间标签appbase-v15.20.14 2026-05-17 15:55:39 +08:00
ZhanGSKen fc20bd56b4 合并模块APPBase 同步最新时间标签appbase-v15.20.13 2026-05-17 15:52:14 +08:00
ZhanGSKen e92538850f 合并模块APPBase 同步最新时间标签appbase-v15.20.12 2026-05-17 15:46:18 +08:00
ZhanGSKen 81b7af0ec4 1. 排序规则彻底换掉
不再按版本号排序,按标签创建时间从新到旧,永远拿到最后发布的最新标签
​
2. 增加  -f  强制覆盖,确保拉取最新文件
​
3. 你现在  appbase-v15.20.12  是后打的标签,脚本会直接优先抓取它
​
4. 无差异也会正常同步最新标签版本文件
2026-05-17 15:45:57 +08:00
ZhanGSKen e2b4bddb61 BugFix 2026-05-17 15:43:18 +08:00
ZhanGSKen cbca61ad84 1. 删掉手动拼接标签逻辑,直接取用远程完整标签名,杜绝  appbase- 空格版本
​
2. 使用  git rev-parse --verify 标签^{commit}  精准拿远程标签commit
​
3. 合并来源强制指定  origin/模块名  远程分支
​
4. 空标签直接跳过,不再出现  xxx-  非法标签
​
5. 检出语法标准: git checkout 远程分支 commit -- 目录
2026-05-17 15:37:37 +08:00
ZhanGSKen 28bc3a86e3 1. 版本排序彻底修复
按  大版本.中版本.小版本.迭代号  分段数字排序,优先抓取  appbase-v15.20.12  而不是旧版
​
2. 检出命令修正
统一使用  git checkout 提交哈希 -- 目录  语法,不再报路径不存在
2026-05-17 15:34:14 +08:00
ZhanGSKen 79f7f682b3 合并模块APPBase 来源最新标签:appbase-v15.20.11 提交点:c7184ad6df0092106e535308ba486a9503395042 2026-05-17 15:25:41 +08:00
ZhanGSKen 8fa09a9f99 demoapp、winboll、aes  没有  xxx-版本号  格式标签,跳过属于正常
​
2.  appbase  成功抓到标签和commit,只是命令语法错导致拉取失败,改完即可正常合并
2026-05-17 15:25:10 +08:00
ZhanGSKen 555c5deef9 1.  get_module_latest_tag  → 抓取远程该模块最新Tag
​
2.  get_commit_by_tag  → 用最新Tag查出唯一对应提交Commit
​
3. 校验Tag/Commit非空
​
4. 执行合并命令git checkout origin/模块名  commit哈希:模块文件夹
 
 
 
5. 仅添加对应目录,生成带标签+commit的提交日志
​
6. 无标签直接跳过,不产生任何操作
2026-05-17 15:22:31 +08:00
ZhanGSKen 3707891e0b 1. 调用函数 → 查出模块标签 + 标签绑定提交Commit哈希
​
2. 判定标签/Commit为空直接跳过
​
3. 合并命令: git checkout origin/模块名  commit哈希:模块文件夹
​
4. 仅添加当前模块目录,精准提交
​
5. 提交日志同时记录标签名+提交哈希,方便回溯
2026-05-17 15:17:40 +08:00
ZhanGSKen 0c9c6c9fd4 1. 调试日志重定向  >&2
彻底杜绝调试文字混入标签变量, MOD_TAG  只存纯净标签名
​
2. 拉取标签写法修正
旧: git checkout 标签名 目录 
新: git checkout origin/tags/标签名 -- 目录 
直接从远程标签仓库拉取对应提交点文件夹,100%命中标签代码
​
3. 精准add提交
放弃  git add . ,只添加当前合并的模块文件夹,避免乱提交
​
4. 强制全量同步标签
脚本开头双重 fetch --tags ,保证本地能读取所有远程版本标签
​
5. 空标签直接拦截
匹配不到标签完全跳过,不产生空提交、无效覆盖
​
6. 严格锁定本地分支,防止合并错分支
2026-05-17 15:11:21 +08:00
ZhanGSKen be3d042ade 设置合并最新标签的模块文件夹。取消合并最新提交的文件夹。 2026-05-17 14:22:14 +08:00
qinglong 33dc49267e 合并 APPBase 项目, 调试:模块[appbase] 远程分支最新Commit = 74299bf9b2 2026-05-17 13:00:02 +08:00
ZhanGSKen e3166b639e 操作步骤调整 2026-05-17 12:13:50 +08:00
ZhanGSKen 3d26bbe090 提交内容整理 2026-05-17 12:08:22 +08:00
ZhanGSKen cd375cefc5 直接取远程分支 origin/xxx 最新全量 Commit,不限制子目录
​
2. 优化标签匹配:只要标签前缀是  模块名-  就匹配
​
3. 保留你原有全部脚本结构,只改  get_module_latest_tag  函数
2026-05-17 11:35:41 +08:00
ZhanGSKen 025c095bcd 提交备注自动带上标签名 2026-05-17 11:18:06 +08:00
ZhanGSKen c88a0a52a8 修复标签获取bug 2026-05-17 11:09:45 +08:00
ZhanGSKen dc4eeefa55 更正分支环境检测名称 2026-05-17 11:04:46 +08:00
ZhanGSKen c7d79e160b 添加Projects_Keeper_Tag仓库合并管理脚本 2026-05-17 10:50:04 +08:00
qinglong 771f591a3a 合并 APPBase 项目 2026-05-16 17:30:01 +08:00
779 changed files with 4349 additions and 56334 deletions
+3 -3
View File
@@ -94,8 +94,8 @@ lint-results.html
## 忽略 AndroidIDE 临时文件夹
.androidide
## WinBoLL 基础应用(避免上传敏感配置)
## MyWinBoLL 基础应用(避免上传敏感配置)
/winboll.properties
/local.properties
/settings.gradle
/gradle.properties
#/settings.gradle
#/gradle.properties
@@ -1,5 +1,5 @@
#!/system/bin/sh
## 合并其他项目分支的模块源码到projects-keeper分支。
## 合并其他项目分支的模块源码到projects_keeper分支。
# ====================== 0. 进入目标目录 ======================
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper"
@@ -36,7 +36,7 @@ fi
# ====================== 3. Git 分支检查 ======================
CUR_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
TARGET_BRANCH="projects-keeper"
TARGET_BRANCH="projects_keeper"
if [ "$CUR_BRANCH" != "$TARGET_BRANCH" ]; then
echo "错误:当前不在 $TARGET_BRANCH 分支!"
@@ -72,7 +72,6 @@ libaes
libappbase
libdebugtemp
libgpsrelaysentinel
libwinboll
local.properties-demo
mymessagemanager
positions
@@ -82,13 +81,14 @@ winboll
winboll.properties-demo
)
# ====================== 5. 获取当前目录真实文件列表 ======================
# ====================== 5. 获取当前目录真实文件列表(兼容过滤 . .. ======================
REAL_ITEMS=()
# 使用固定排序ls,自动过滤 . 和 ..,不会进入比对数组
while IFS= read -r line; do
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(ls -a)
done < <(LC_COLLATE=C ls -a1 --color=none)
# ====================== 6. 差异比对函数 ======================
check_diff() {
@@ -158,7 +158,7 @@ echo -e "## 对象列表结束
## 合并 APP 项目
MERGE_APP_PROJECT_LIST=(
DemoAPP
WinBoLL
)
echo -e "#@@@ 开始合并应用型模块源码 @@@#
## 目标合并对象列表:"
@@ -166,14 +166,13 @@ echo -e "#@@@ 开始合并应用型模块源码 @@@#
for item in "${MERGE_APP_PROJECT_LIST[@]}"; do
echo "正在合并 $item 项目 ..."
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
git checkout origin/$item_lower $item_lower
git checkout origin/$item_lower $item_lower
git add .
git commit -m "合并 $item 项目"
done
## 合并 LIB 项目
MERGE_LIB_PROJECT_LIST=(
WinBoLL
APPBase
AES
)
@@ -183,10 +182,10 @@ echo -e "#@@@ 开始合并类库型模块源码 @@@#
for item in "${MERGE_LIB_PROJECT_LIST[@]}"; do
echo "正在合并 $item 项目 ..."
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
git checkout origin/$item_lower $item_lower lib$item_lower
git checkout origin/$item_lower $item_lower lib$item_lower
git add .
git commit -m "合并 $item 项目"
done
echo '正在推送 Projects_Keeper 项目'
git push
git push
@@ -0,0 +1,158 @@
#!/system/bin/sh
## 逻辑:按时间取最新标签 → 拉取该标签目录合并
# 按创建时间取模块最新标签(最新打的标签排最后)
get_latest_module_tag(){
local mod=$1
git for-each-ref --sort=-creatordate --format='%(refname)' refs/tags/${mod}-* \
| grep -v '\^{}' \
| head -1 \
| sed 's/refs\/tags\///'
}
# 通过标签获取commit
get_commit_from_tag(){
local tag=$1
git rev-parse --short "${tag}^{commit}"
}
# 工作目录
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper_Tag"
echo "进入目录:${TARGET_DIR}"
cd "${TARGET_DIR}" || exit 1
# 同步远程
echo "========================================"
echo "同步远程分支与全部版本标签"
echo "========================================"
git fetch origin --prune
git fetch origin --tags
echo "同步完成"
echo ""
# 校验目标分支
NOW_BRANCH=$(git symbolic-ref --short HEAD)
TARGET_BRANCH="projects_keeper_tag"
if [ "${NOW_BRANCH}" != "${TARGET_BRANCH}" ];then
echo "错误:请先切换到 ${TARGET_BRANCH} 分支"
exit 1
fi
# 目录结构校验白名单(不含 . ..)
MERGE_OBJECTS_LIST=(
.git
.gitignore
.gitmodules
.winboll
GenKeyStore
LICENSE
LICENSE-Private-Demo
LICENSE-Private-Demo_docs
README.md
aes
appbase
autonfc
build.gradle
contacts
debugtemp
gallery
gpsrelaysentinel
gradle
gradle.properties-android-demo
gradle.properties-androidx-demo
gradlew
libaes
libappbase
libdebugtemp
libgpsrelaysentinel
local.properties-demo
mymessagemanager
positions
powerbell
settings.gradle-demo
winboll
winboll.properties-demo
)
REAL_ITEMS=()
# 标准排序ls输出,循环强制过滤 . 和 ..
while IFS= read -r line; do
# 跳过虚拟目录 . 和 ..
if [[ "$line" != "." && "$line" != ".." ]]; then
REAL_ITEMS+=("$line")
fi
done < <(LC_COLLATE=C ls -a1 --color=none)
check_diff(){
local miss=() extra=()
for i in "${MERGE_OBJECTS_LIST[@]}";do
[[ ! " ${REAL_ITEMS[@]} " =~ " ${i} " ]] && miss+=("$i")
done
for i in "${REAL_ITEMS[@]}";do
[[ ! " ${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
}
check_diff
echo -e "#@@@ 按时间获取最新标签合并模块源码 @@@#"
# 应用型模块
MERGE_APP_PROJECT_LIST=(WinBoLL)
echo -e "---------- 应用型模块 ----------"
for name in "${MERGE_APP_PROJECT_LIST[@]}";do
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
tag=$(get_latest_module_tag "${low_name}")
if [[ -z "${tag}" ]];then
echo "跳过 ${low_name}:无远程标签"
continue
fi
commit=$(get_commit_from_tag "${tag}")
if [[ -z "${commit}" ]];then
echo "跳过 ${low_name}:标签无有效提交点"
continue
fi
echo "模块:${low_name} 最新时间标签:${tag} 提交ID:${commit}"
# 强制拉取覆盖
git checkout -f "${tag}" -- "${low_name}"
git add "${low_name}"
git commit -m "合并模块${name} 同步最新时间标签${tag}"
done
# 类库模块
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')
tag=$(get_latest_module_tag "${low_name}")
if [[ -z "${tag}" ]];then
echo "跳过 ${low_name}:无远程标签"
continue
fi
commit=$(get_commit_from_tag "${tag}")
if [[ -z "${commit}" ]];then
echo "跳过 ${low_name}:标签无有效提交点"
continue
fi
echo "模块:${low_name} 最新时间标签:${tag} 提交ID:${commit}"
git checkout -f "${tag}" -- "${low_name}" "lib${low_name}"
git add "${low_name}" "lib${low_name}"
git commit -m "合并模块${name} 同步最新时间标签${tag}"
done
echo "全部模块合并执行完毕"
echo "执行推送:git push"
git push
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/bash
# ==============================================================================
# WinBoLL 应用发布脚本
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
# 依赖:build.properties、app_update_description.txt(项目根目录下)
# 使用:./script_name.sh <APP_NAME>
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
# ==============================================================================
# ==================== 常量定义 ====================
# 脚本退出码
EXIT_CODE_SUCCESS=0
EXIT_CODE_ERR_NO_APP_NAME=2
EXIT_CODE_ERR_WORK_DIR=1
EXIT_CODE_ERR_GIT_CHECK=1
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
# Gradle 任务(正式发布)
GRADLE_TASK_PUBLISH="assembleStageRelease"
# Gradle 任务(调试用,注释备用)
# GRADLE_TASK_DEBUG="assembleBetaDebug"
# aapt2本地覆盖参数
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
# 禁用Gradle守护进程
GRADLE_NO_DAEMON="--no-daemon"
# ==================== 函数定义 ====================
# 检查Git源码是否已完全提交(无未提交变更)
# 返回值:0=已完全提交,1=存在未提交变更
function checkGitSources() {
# 配置Git安全目录(解决权限问题)
git config --global --add safe.directory "$(pwd)"
# 检查是否有未提交的变更
if [[ -n $(git diff --stat) ]]; then
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
return 1
fi
echo "[INFO] Git源码检查通过:所有变更已提交。"
return 0
}
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
# 返回值:1=用户选择是,0=用户选择否
function askAddWorkflowsTag() {
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
if [[ $answer =~ ^[Yy]$ ]]; then
return 1
else
return 0
fi
}
# 添加WinBoLL正式标签
# 参数:$1=应用名称(项目根目录名)
# 返回值:0=标签添加成功,1=标签已存在/添加失败
function addWinBoLLTag() {
local app_name=$1
local build_prop_path="${app_name}/build.properties"
# 从build.properties中提取publishVersion
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
if [[ -z ${publish_version} ]]; then
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
return 1
fi
echo "[INFO] 从${build_prop_path}读取到publishVersion${publish_version}"
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>
local tag="${app_name}-v${publish_version}"
echo "[INFO] 准备添加WinBoLL标签:${tag}"
# 检查标签是否已存在
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
echo "[ERROR] WinBoLL标签${tag}已存在!"
return 1
fi
# 添加带注释的标签(注释来自app_update_description.txt
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
echo "[INFO] WinBoLL标签${tag}添加成功!"
return 0
}
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
# 参数:$1=应用名称(项目根目录名)
# 返回值:0=标签添加成功,1=标签已存在/添加失败
function addWorkflowsTag() {
local app_name=$1
local build_prop_path="${app_name}/build.properties"
# 从build.properties中提取baseBetaVersion
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
if [[ -z ${base_beta_version} ]]; then
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
return 1
fi
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion${base_beta_version}"
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta
local tag="${app_name}-v${base_beta_version}-beta"
echo "[INFO] 准备添加Workflows标签:${tag}"
# 检查标签是否已存在
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
echo "[ERROR] Workflows标签${tag}已存在!"
return 1
fi
# 添加带注释的标签(注释来自app_update_description.txt
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
echo "[INFO] Workflows标签${tag}添加成功!"
return 0
}
# ==================== 主流程开始 ====================
echo "============================================="
echo " WinBoLL 应用发布脚本"
echo "============================================="
# 1. 检查应用名称参数是否指定
if [ -z "$1" ]; then
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
exit ${EXIT_CODE_ERR_NO_APP_NAME}
fi
APP_NAME=$1
echo "[INFO] 待发布应用名称:${APP_NAME}"
# 2. 检查并切换到项目根目录(确保build.properties存在)
echo "[INFO] 当前工作目录:$(pwd)"
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
cd ..
echo "[INFO] 切换后工作目录:$(pwd)"
fi
# 验证最终工作目录是否正确
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
exit ${EXIT_CODE_ERR_WORK_DIR}
fi
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
# 3. 检查Git源码状态
echo "---------------------------------------------"
echo " 步骤1:检查Git源码状态"
echo "---------------------------------------------"
checkGitSources
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] Git源码检查失败,脚本终止!"
exit ${EXIT_CODE_ERR_GIT_CHECK}
fi
# 4. 编译Stage Release版本APK(携带aapt2覆盖参数 + --no-daemon
echo "---------------------------------------------"
echo " 步骤2:编译Stage Release APK"
echo "---------------------------------------------"
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
# 调试用(注释正式任务,启用调试任务)
# bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_DEBUG}
bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_PUBLISH}
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] Gradle编译任务失败!"
exit 1
fi
echo "[INFO] Stage Release APK编译成功!"
# 5. 添加WinBoLL正式标签
echo "---------------------------------------------"
echo " 步骤3:添加WinBoLL标签"
echo "---------------------------------------------"
addWinBoLLTag ${APP_NAME}
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
fi
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
# echo "---------------------------------------------"
# echo " 步骤4:添加Workflows标签(可选)"
# echo "---------------------------------------------"
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
# askAddWorkflowsTag
# nAskAddWorkflowsTag=$?
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
# addWorkflowsTag ${APP_NAME}
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
# exit 1
# fi
# fi
# 7. 清理更新描述文件
echo "---------------------------------------------"
echo " 步骤5:清理更新描述文件"
echo "---------------------------------------------"
echo "" > "${APP_NAME}/app_update_description.txt"
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
# 8. 提交并推送源码与标签
echo "---------------------------------------------"
echo " 步骤6:提交并推送源码"
echo "---------------------------------------------"
git add .
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
echo "[INFO] 源码提交成功,开始推送..."
# 推送源码到远程仓库
git push origin
# 推送标签到远程仓库
git push origin --tags
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
echo "[INFO] 源码与标签推送成功!"
else
echo "[ERROR] 源码与标签推送失败!"
exit 1
fi
# ==================== 主流程结束 ====================
echo "============================================="
echo " WinBoLL 应用发布完成!"
echo "============================================="
exit ${EXIT_CODE_SUCCESS}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/bash
# aapt2本地覆盖参数
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
# Gradle禁用守护进程参数
GRADLE_NO_DAEMON="--no-daemon"
# 检查是否指定了将要发布的类库名称
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ]; then
echo "No Library name specified : $0"
exit 2
fi
## 正式发布使用
git pull && bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishReleasePublicationToWinBoLLReleaseRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
## 调试使用
#bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishSnapshotWinBoLLPublicationToWinBoLLSnapshotRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
+56 -27
View File
@@ -122,7 +122,6 @@ android {
// 如果正在调试,就拷贝到 WinBoLL 备份管理文件夹
//
if(variant.flavorName == "beta"&&variant.buildType.name == "debug"){
//File outBuildBckDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/${variant.buildType.name}")
File outBuildBckDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}")
// 创建目标路径目录
if(!outBuildBckDir.exists()) {
@@ -130,6 +129,7 @@ android {
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
}
if(outBuildBckDir.exists()) {
def targetApkFile = new File(outBuildBckDir, outputFileName)
copy{
from file.outputFile
into outBuildBckDir
@@ -138,6 +138,14 @@ android {
}
println "Output APK (WinBoLLStudio): " + outBuildBckDir.getAbsolutePath() + "/${outputFileName}"
}
// ========== 设置文件权限为775 ==========
if(targetApkFile.exists()){
exec {
commandLine 'chmod', '775', targetApkFile.absolutePath
}
println "Set file permission to 775 : ${targetApkFile.absolutePath}"
}
// 检查编译标志位配置
assert (winbollBuildProps['buildCount'] != null)
assert (winbollBuildProps['libraryProject'] != null)
@@ -160,8 +168,7 @@ android {
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
println "\n\n>>> Library Project build.properties saved.\n\n";
}
@@ -172,16 +179,12 @@ android {
//
if(variant.flavorName == "stage"&&variant.buildType.name == "release"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/tag/")
// 创建目标路径目录
if(!outTagDir.exists()) {
@@ -192,12 +195,10 @@ android {
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在,构建拷贝任务停止
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制一个备份
// 复制完整版APK
def fullApkFile = new File(outTagDir, outputFileName)
copy{
from file.outputFile
into outTagDir
@@ -206,7 +207,16 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 复制一个并重命名为短版本名
// 设置权限775。
if(fullApkFile.exists()){
exec {
commandLine 'chmod', '775', fullApkFile.absolutePath
}
println "Set file permission to 775 : ${fullApkFile.absolutePath}"
}
// 复制短版本名APK
def shortApkFile = new File(outTagDir, szCommonTagAPKName)
copy{
from file.outputFile
into outTagDir
@@ -215,6 +225,14 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
// 设置权限775。
if(shortApkFile.exists()){
exec {
commandLine 'chmod', '775', shortApkFile.absolutePath
}
println "Set file permission to 775 : ${shortApkFile.absolutePath}"
}
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['publishVersion'] != null)
@@ -239,14 +257,11 @@ android {
fos.close();
if(winbollBuildProps['libraryProject'] != "") {
// 如果应用 build.properties 文件设置了类库模块项目文件名
// 就拷贝一份新的编译标志配置到类库项目文件夹
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
assert(winbollBuildPropsFile.exists())
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
@@ -263,17 +278,12 @@ android {
// 如果正在调试发布版,就只生成和输出APK文件,不处理 Git 仓库提交与更新问题。
//
if(variant.flavorName == "stage"&&variant.buildType.name == "debug"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}/")
// 创建目标路径目录
if(!outTagDir.exists()) {
@@ -284,13 +294,11 @@ android {
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在,构建拷贝任务停止
println '如果是在调试 Stage 版应用包构建,请删除(注:在debug目录)现有的 Stage 应用包('+targetAPK.getAbsolutePath()+')。再编译一次。'
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制一个备份
// 复制完整版APK
def debugFullApk = new File(outTagDir, outputFileName)
copy{
from file.outputFile
into outTagDir
@@ -299,7 +307,16 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 复制一个并重命名为短版本名
// 权限设为775。
if(debugFullApk.exists()){
exec {
commandLine 'chmod', '775', debugFullApk.absolutePath
}
println "Set file permission to 775 : ${debugFullApk.absolutePath}"
}
// 复制短版本名APK
def debugShortApk = new File(outTagDir, szCommonTagAPKName)
copy{
from file.outputFile
into outTagDir
@@ -308,8 +325,13 @@ android {
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
//不保存编译标志配置
// 权限设为775
if(debugShortApk.exists()){
exec {
commandLine 'chmod', '775', debugShortApk.absolutePath
}
println "Set file permission to 775 : ${debugShortApk.absolutePath}"
}
}
}
@@ -328,6 +350,13 @@ android {
}
println "Output APK (Common): " + outCommonDir.getAbsolutePath() + "/${commandAPKName}"
}
// 额外输出文件设置775权限
if(apkFile.exists()){
exec {
commandLine 'chmod', '775', apkFile.absolutePath
}
println "Set file permission to 775 : ${apkFile.absolutePath}"
}
}
}
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+97
View File
@@ -0,0 +1,97 @@
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
# LICENSE-Private-Demo
# WinBoLL 源码公共转私有继承开发规范守则
## 核心声明
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
## 二、公私分支授权标识文件定义(风控核心依据)
### 1. 公共开源分支唯一标识
**文件名:LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
### 2. 私有开发分支唯一标识
**文件名:LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
## 四、公共转私有标准化操作步骤(锁死合并风险)
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
>
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
## 五、人为操作责任界定(核心补充条款)
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注:文档部分内容可能由 AI 生成)
+1 -1
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
+4 -5
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"
@@ -36,10 +39,6 @@ android {
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 13:11:28 HKT 2026
stageCount=4
#Sat Jul 11 20:07:00 HKT 2026
stageCount=19
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.3
publishVersion=15.20.18
buildCount=0
baseBetaVersion=15.20.4
baseBetaVersion=15.20.19
@@ -65,7 +65,7 @@ public class AboutActivity extends BaseWinBoLLActivity {
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("AES");
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
@@ -5,10 +5,14 @@ package cc.winboll.studio.aes;
* @Date 2024/06/13 19:03:58
* @Describe AES应用类
*/
import android.view.Gravity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
public class App extends GlobalApplication {
@@ -17,13 +21,25 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
WinBoLLActivityManager.init(this);
try {
super.onCreate();
ToastUtils.init(this);
WinBoLLActivityManager.init(this);
AESThemeUtil.init(null);
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override
@@ -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
@@ -1,11 +1,7 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import cc.winboll.studio.libaes.views.ADsControlView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -20,7 +16,7 @@ public class SettingsActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
//ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override
@@ -6,12 +6,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>
+1 -1
View File
@@ -5,7 +5,7 @@
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
+1 -1
View File
@@ -5,7 +5,7 @@
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
+1 -1
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
+4 -1
View File
@@ -26,7 +26,10 @@ android {
applicationId "cc.winboll.studio.appbase"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
//1. Android 官方规则
//-  versionCode  类型:int 整型
//- Java int 最大值:2147483647
versionCode 1520000
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
+5 -5
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat May 16 03:50:01 CST 2026
stageCount=10
#Sun Jun 28 20:55:33 HKT 2026
stageCount=35
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.9
buildCount=6
baseBetaVersion=15.20.10
publishVersion=15.20.34
buildCount=0
baseBetaVersion=15.20.35
@@ -1,8 +1,11 @@
package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,16 +24,24 @@ 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());
try {
super.onCreate();
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
/**
@@ -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;
/**
-34
View File
@@ -1,34 +0,0 @@
# AutoNFC
#### 介绍
NFC 卡应用,主要管理 NFC 卡接触手机的动作响应,NFC 接触状态用于作为其他应用激活活动动作的启动令牌。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh autonfc
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Mar 16 18:30:19 GMT 2026
stageCount=0
libraryProject=
baseVersion=15.11
publishVersion=15.0.0
buildCount=54
baseBetaVersion=15.0.1
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-51
View File
@@ -1,51 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.autonfc">
<uses-permission android:name="android.permission.NFC"/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".nfc.NFCInterfaceActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<!-- NFC 绑定服务 -->
<service
android:name=".nfc.AutoNFCService"
android:exported="false"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
</application>
</manifest>
@@ -1,180 +0,0 @@
package cc.winboll.studio.autonfc;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.IBinder;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.autonfc.nfc.ActionDialog;
import cc.winboll.studio.autonfc.nfc.AutoNFCService;
import cc.winboll.studio.autonfc.nfc.NFCInterfaceActivity;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent;
private AutoNFCService mService;
private boolean mBound = false;
// 服务连接
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
AutoNFCService.LocalBinder binder = (AutoNFCService.LocalBinder) service;
mService = binder.getService();
mBound = true;
LogUtils.d(TAG, "onServiceConnected: 服务已绑定");
// 关键:把 Activity 传给 Service,用于回调
mService.attachActivity(MainActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
mService = null;
LogUtils.d(TAG, "onServiceDisconnected: 服务已断开");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// 初始化 NFC
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, 0);
LogUtils.d(TAG, "onCreate() -> NFC 监听已绑定到 MainActivity");
}
@Override
protected void onStart() {
super.onStart();
// 绑定服务
Intent intent = new Intent(this, AutoNFCService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
LogUtils.d(TAG, "onStart: 绑定服务");
}
@Override
protected void onStop() {
super.onStop();
// 解绑服务
if (mBound) {
unbindService(mConnection);
mBound = false;
LogUtils.d(TAG, "onStop: 解绑服务");
}
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume() -> 开启 NFC 前台分发");
if (mNfcAdapter != null) {
mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
}
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause() -> 关闭 NFC 前台分发");
if (mNfcAdapter != null) {
mNfcAdapter.disableForegroundDispatch(this);
}
}
// NFC 卡片靠近唯一入口
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
LogUtils.d(TAG, "onNewIntent() -> 检测到 NFC 卡片");
// 把 NFC 事件交给 Service 处理
if (mBound && mService != null) {
mService.handleNfcIntent(intent);
} else {
LogUtils.e(TAG, "服务未绑定,无法处理 NFC");
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_log) {
LogActivity.startLogActivity(this);
return true;
}
return super.onOptionsItemSelected(item);
}
public void onNFCInterfaceActivity(View view) {
startActivity(new Intent(this, NFCInterfaceActivity.class));
}
// ========================= 【新增】关键方法:由 Service 回调来弹出对话框 =========================
/**
* Service 解析完 NFC 数据后,回调此方法在 Activity 中弹出对话框
*/
public void showNfcActionDialog(final String nfcData) {
LogUtils.d(TAG, "showNfcActionDialog() -> Activity 存活,安全弹出对话框");
// Activity 正在运行,直接弹框,绝对不会报 BadTokenException
final ActionDialog dialog = new ActionDialog(this);
dialog.setNfcData(nfcData);
dialog.setButtonClickListener(new ActionDialog.OnButtonClickListener() {
@Override
public void onBuildClick() {
LogUtils.d(TAG, "点击 Build");
if (mService != null) {
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD, nfcData);
}
dialog.dismiss();
}
@Override
public void onViewClick() {
LogUtils.d(TAG, "点击 View");
if (mService != null) {
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD_VIEW, nfcData);
}
dialog.dismiss();
}
@Override
public void onCancelClick() {
dialog.dismiss();
}
});
dialog.show();
}
}
@@ -1,66 +0,0 @@
package cc.winboll.studio.autonfc.models;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/03/16 09:38
*/
public class NfcTermuxCmd {
private String script; // 要执行的预制脚本名(如 auth.sh)
private String[] args; // 脚本参数
private String workDir; // 工作目录
private boolean background; // 是否后台执行
private String resultDir; // 结果输出目录(可为 null
public NfcTermuxCmd() {
}
public NfcTermuxCmd(String script, String[] args, String workDir, boolean background, String resultDir) {
this.script = script;
this.args = args;
this.workDir = workDir;
this.background = background;
this.resultDir = resultDir;
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public String[] getArgs() {
return args;
}
public void setArgs(String[] args) {
this.args = args;
}
public String getWorkDir() {
return workDir;
}
public void setWorkDir(String workDir) {
this.workDir = workDir;
}
public boolean isBackground() {
return background;
}
public void setBackground(boolean background) {
this.background = background;
}
public String getResultDir() {
return resultDir;
}
public void setResultDir(String resultDir) {
this.resultDir = resultDir;
}
}
@@ -1,123 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Dialog;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import cc.winboll.studio.autonfc.R;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 自定义对话框类,用于与用户交互,展示 NFC 相关操作选项
* 兼容 Java 7 语法
*
* @author 豆包&ZhanGSKen
* @create 2025-08-15
* @lastModify 2026-03-17
*/
public class ActionDialog extends Dialog {
private static final String TAG = "ActionDialog";
private String mNfcData;
private OnButtonClickListener mClickListener;
/**
* 构造函数
*/
public ActionDialog(Context context) {
super(context);
initDialog();
}
/**
* 设置 NFC 数据
*/
public void setNfcData(String nfcData) {
this.mNfcData = nfcData;
LogUtils.d(TAG, "setNfcData() -> " + nfcData);
}
/**
* 设置点击监听
*/
public void setButtonClickListener(OnButtonClickListener listener) {
this.mClickListener = listener;
}
/**
* 初始化布局
*/
private void initDialog() {
setTitle("请选择操作");
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(20, 20, 20, 20);
addButtons(layout);
setContentView(layout);
}
/**
* 添加按钮
*/
private void addButtons(LinearLayout layout) {
// Build 按钮
Button btnBuild = createButton("Build", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 Build");
if (mClickListener != null) {
mClickListener.onBuildClick();
}
}
});
layout.addView(btnBuild);
// View 按钮
Button btnView = createButton("View", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 View");
if (mClickListener != null) {
mClickListener.onViewClick();
}
}
});
layout.addView(btnView);
// 取消按钮
Button btnCancel = createButton("Cancel", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 Cancel");
dismiss();
}
});
layout.addView(btnCancel);
}
/**
* 创建按钮
*/
private Button createButton(String text, View.OnClickListener listener) {
Button button = new Button(getContext());
button.setText(text);
button.setPadding(10, 10, 10, 10);
button.setOnClickListener(listener);
return button;
}
/**
* 回调接口
*/
public interface OnButtonClickListener {
void onBuildClick();
void onViewClick();
void onCancelClick();
}
}
@@ -1,202 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.Binder;
import android.os.IBinder;
import cc.winboll.studio.autonfc.MainActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.nio.charset.Charset;
import java.util.Arrays;
public class AutoNFCService extends Service {
public static final String TAG = "AutoNFCService";
// ================= 已修改:更新为 Beta 包名 =================
public static final String ACTION_BUILD = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD";
public static final String ACTION_BUILD_VIEW = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD_VIEW";
private final IBinder mBinder = new LocalBinder();
private String mNfcData;
private MainActivity mActivity; // 持有 Activity 引用,用于回调
// ========================= 生命周期 =========================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate() -> 服务创建");
// 移除:startForeground(NOTIFICATION_ID, buildNotification());
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy() -> 服务已停止");
mActivity = null; // 释放引用
}
// ========================= 服务绑定 =========================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind() -> 服务被绑定");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
LogUtils.d(TAG, "onUnbind() -> 服务解绑");
// 移除:stopForeground(true);
stopSelf();
return super.onUnbind(intent);
}
// ========================= 对外暴露方法 =========================
/**
* 绑定 Activity,用于回调显示对话框
*/
public void attachActivity(MainActivity activity) {
this.mActivity = activity;
}
/**
* 处理 NFC 意图
*/
public void handleNfcIntent(Intent intent) {
LogUtils.d(TAG, "handleNfcIntent() -> 开始处理");
if (intent == null) {
LogUtils.e(TAG, "handleNfcIntent() -> 参数 intent 为空");
return;
}
String action = intent.getAction();
LogUtils.d(TAG, "handleNfcIntent() -> Action = " + action);
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
LogUtils.d(TAG, "handleNfcIntent() -> 匹配 NFC 动作");
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag == null) {
LogUtils.e(TAG, "handleNfcIntent() -> Tag 为空");
return;
}
LogUtils.d(TAG, "handleNfcIntent() -> Tag ID = " + bytesToHexString(tag.getId()));
LogUtils.d(TAG, "handleNfcIntent() -> Tech List = " + Arrays.toString(tag.getTechList()));
parseNdefData(tag);
}
}
// ========================= 内部业务 =========================
private void parseNdefData(Tag tag) {
LogUtils.d(TAG, "parseNdefData() -> 开始解析");
if (tag == null) return;
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
LogUtils.e(TAG, "parseNdefData() -> 不支持 NDEF 格式");
return;
}
try {
ndef.connect();
NdefMessage msg = ndef.getNdefMessage();
if (msg == null || msg.getRecords() == null || msg.getRecords().length == 0) {
LogUtils.w(TAG, "parseNdefData() -> 卡片无数据");
return;
}
NdefRecord record = msg.getRecords()[0];
byte[] payload = record.getPayload();
int langLen = payload[0] & 0x3F;
int start = 1 + langLen;
if (start < payload.length) {
mNfcData = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
LogUtils.d(TAG, "parseNdefData() -> 读卡成功: " + mNfcData);
// 关键:回调给 Activity 弹框,此时 Activity 一定是存活状态
if (mActivity != null) {
mActivity.showNfcActionDialog(mNfcData);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "parseNdefData() -> 读取失败", e);
} finally {
try {
ndef.close();
} catch (Exception e) {
// 忽略关闭异常
}
}
}
/**
* 执行 Termux 命令
*/
public void executeTermuxCommand(String action, String nfcData) {
LogUtils.d(TAG, "executeTermuxCommand() -> 开始执行");
if (nfcData == null || nfcData.isEmpty()) {
ToastUtils.show("数据错误");
return;
}
try {
LogUtils.d(TAG, "executeTermuxCommand() -> 发送指令: " + nfcData);
Intent bridgeIntent = new Intent(action);
// ================= 已修改:使用 Beta 包名 =================
bridgeIntent.setClassName(
"cc.winboll.studio.winboll.beta",
"cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity"
);
bridgeIntent.putExtra(Intent.EXTRA_TEXT, nfcData);
bridgeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(bridgeIntent);
ToastUtils.show("指令已发送");
} catch (Exception e) {
LogUtils.e(TAG, "executeTermuxCommand() -> 发送失败", e);
ToastUtils.show("发送失败");
}
}
// ========================= 工具方法 =========================
private String bytesToHexString(byte[] bytes) {
if (bytes == null || bytes.length == 0) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
// ========================= Binder =========================
public class LocalBinder extends Binder {
public AutoNFCService getService() {
return AutoNFCService.this;
}
}
}
@@ -1,230 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.autonfc.R;
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class NFCInterfaceActivity extends Activity {
public static final String TAG = "NFCInterfaceActivity";
private EditText et_script;
private EditText et_args;
private EditText et_workDir;
private EditText et_background;
private EditText et_resultDir;
private TextView tvResult;
private TextView tvStatus;
private NfcAdapter mNfcAdapter;
private PendingIntent mNfcPendingIntent;
private Tag mCurrentTag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nfc_interface);
initView();
initNfc();
}
private void initView() {
et_script = findViewById(R.id.et_script);
et_args = findViewById(R.id.et_args);
et_workDir = findViewById(R.id.et_workDir);
et_background = findViewById(R.id.et_background);
et_resultDir = findViewById(R.id.et_resultDir);
tvResult = findViewById(R.id.tv_result);
tvStatus = findViewById(R.id.tv_status);
}
private void initNfc() {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter == null) {
tvStatus.setText("设备不支持NFC");
return;
}
if (!mNfcAdapter.isEnabled()) {
tvStatus.setText("请开启NFC");
return;
}
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mNfcPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, PendingIntent.FLAG_UPDATE_CURRENT);
tvStatus.setText("NFC已启动,等待卡片靠近");
}
@Override
protected void onResume() {
super.onResume();
if (mNfcAdapter != null && mNfcAdapter.isEnabled()) {
mNfcAdapter.enableForegroundDispatch(this, mNfcPendingIntent, null, null);
}
}
@Override
protected void onPause() {
super.onPause();
if (mNfcAdapter != null) {
mNfcAdapter.disableForegroundDispatch(this);
}
mCurrentTag = null;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
mCurrentTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (mCurrentTag == null) return;
tvStatus.setText("卡片已连接,解析中...");
readNfc();
}
// -------------------------------------------------------------------------
// 读取 NFC(完全委托给工具类)
// -------------------------------------------------------------------------
private void readNfc() {
try {
NfcTermuxCmd cmd = NfcUtils.readTag(mCurrentTag);
if (cmd == null) {
tvStatus.setText("读取成功:标签为空");
tvResult.setText("");
// 清空窗体
clearUiFields();
return;
}
// 核心改动:读取成功后,同时更新详情显示 和 窗体输入框
updateUiWithCmd(cmd);
} catch (Exception e) {
LogUtils.e(TAG, "readNfc 失败", e);
tvStatus.setText("读取失败:" + e.getMessage());
// 出错时清空窗体
clearUiFields();
}
}
// -------------------------------------------------------------------------
// 新增:根据读取到的 Cmd 填充 UI(详情 + 窗体)
// -------------------------------------------------------------------------
private void updateUiWithCmd(NfcTermuxCmd cmd) {
if (cmd == null) return;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date());
String show = "【读取时间】 " + time + "\n\n"
+ "【解析结果】\n"
+ "script: " + cmd.getScript() + "\n"
+ "args: " + (cmd.getArgs() != null ? String.join(", ", cmd.getArgs()) : "[]") + "\n"
+ "workDir: " + cmd.getWorkDir() + "\n"
+ "background: " + cmd.isBackground() + "\n"
+ "resultDir: " + cmd.getResultDir();
tvResult.setText(show);
tvStatus.setText("读取成功!");
// 👇 关键逻辑:自动填入窗体(每次读取后都会覆盖输入框)
et_script.setText(cmd.getScript() != null ? cmd.getScript() : "");
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
et_background.setText(String.valueOf(cmd.isBackground()));
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
}
// -------------------------------------------------------------------------
// 辅助:清空所有输入框
// -------------------------------------------------------------------------
private void clearUiFields() {
et_script.setText("");
et_args.setText("");
et_workDir.setText("");
et_background.setText("");
et_resultDir.setText("");
}
// -------------------------------------------------------------------------
// 写入按钮(委托给工具类)
// -------------------------------------------------------------------------
public void onWriteClick(View view) {
if (mCurrentTag == null) {
showToast("请先靠近卡片");
return;
}
try {
NfcTermuxCmd cmd = buildCmdFromUI();
NfcUtils.writeTag(mCurrentTag, cmd);
tvStatus.setText("写入成功!");
showToast("写入成功");
readNfc(); // 写入后重读,此时会自动填入窗体
} catch (Exception e) {
LogUtils.e(TAG, "写入失败", e);
tvStatus.setText("写入失败:" + e.getMessage());
showToast("写入失败");
}
}
// -------------------------------------------------------------------------
// 填充调试数据
// -------------------------------------------------------------------------
public void onFillTestDataClick(View view) {
String testJson = "{\"script\":\"BuildWinBoLLProject.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}";
try {
NfcTermuxCmd cmd = NfcUtils.jsonToCmd(testJson);
et_script.setText(cmd.getScript());
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
et_background.setText(String.valueOf(cmd.isBackground()));
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
showToast("调试数据已填入");
} catch (Exception e) {
showToast("解析失败");
}
}
// -------------------------------------------------------------------------
// 从 UI 构建 NfcTermuxCmd
// -------------------------------------------------------------------------
private NfcTermuxCmd buildCmdFromUI() {
String script = et_script.getText().toString().trim();
String argsStr = et_args.getText().toString().trim();
String workDir = et_workDir.getText().toString().trim();
String bgStr = et_background.getText().toString().trim();
String resultDir = et_resultDir.getText().toString().trim();
NfcTermuxCmd cmd = new NfcTermuxCmd();
cmd.setScript(script);
cmd.setArgs(argsStr.isEmpty() ? new String[0] : argsStr.split(","));
cmd.setWorkDir(workDir.isEmpty() ? null : workDir);
cmd.setBackground("true".equalsIgnoreCase(bgStr));
cmd.setResultDir(resultDir.isEmpty() ? null : resultDir);
return cmd;
}
private void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
}
@@ -1,78 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
import java.util.HashMap;
import java.util.Map;
public class NfcStateMonitor {
private static Map<String, OnNfcStateListener> sListenerMap = new HashMap<>();
private static boolean sIsRunning = false;
public static void startMonitor() {
if (sIsRunning) return;
sListenerMap = new HashMap<>();
sIsRunning = true;
}
public static void stopMonitor() {
if (!sIsRunning) return;
sIsRunning = false;
if (sListenerMap != null) {
sListenerMap.clear();
sListenerMap = null;
}
}
// 你原来的方法名:registerListener
public static void registerListener(String key, OnNfcStateListener listener) {
if (!sIsRunning || listener == null) return;
sListenerMap.put(key, listener);
}
public static void unregisterListener(String key) {
if (!sIsRunning || key == null) return;
sListenerMap.remove(key);
}
public static void notifyNfcConnected() {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcConnected();
}
}
public static void notifyNfcDisconnected() {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcDisconnected();
}
}
public static void notifyReadSuccess(String data) {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcReadSuccess(data);
}
}
public static void notifyReadFail(String error) {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcReadFail(error);
}
}
public static void notifyWriteSuccess() {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcWriteSuccess();
}
}
public static void notifyWriteFail(String error) {
if (!sIsRunning) return;
for (OnNfcStateListener l : sListenerMap.values()) {
l.onNfcWriteFail(error);
}
}
}
@@ -1,136 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/03/16 14:26
*/
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
import cc.winboll.studio.libappbase.LogUtils;
import java.nio.charset.Charset;
import java.util.Locale;
public class NfcUtils {
public static final String TAG = "NfcUtils";
private static Gson sGson = new Gson();
// -------------------------------------------------------------------------
// 读取 NFC 标签并解析为 NfcTermuxCmd
// -------------------------------------------------------------------------
public static NfcTermuxCmd readTag(Tag tag) throws Exception {
if (tag == null) {
LogUtils.e(TAG, "readTag: tag is null");
return null;
}
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
LogUtils.e(TAG, "readTag: 不支持 NDEF");
return null;
}
try {
ndef.connect();
NdefMessage msg = ndef.getNdefMessage();
if (msg == null) return null;
NdefRecord[] records = msg.getRecords();
if (records == null || records.length == 0) return null;
byte[] payload = records[0].getPayload();
int status = payload[0] & 0xFF;
int langLen = status & 0x3F;
int start = 1 + langLen;
if (start >= payload.length) return null;
String json = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
LogUtils.d(TAG, "readTag: 提取 JSON -> " + json);
return sGson.fromJson(json, NfcTermuxCmd.class);
} finally {
if (ndef != null && ndef.isConnected()) {
ndef.close();
}
}
}
// -------------------------------------------------------------------------
// 写入 NfcTermuxCmd 到 NFC 标签
// -------------------------------------------------------------------------
public static void writeTag(Tag tag, NfcTermuxCmd cmd) throws Exception {
if (tag == null) throw new Exception("tag is null");
String json = sGson.toJson(cmd);
writeJson(tag, json);
}
// -------------------------------------------------------------------------
// 写入原始 JSON 字符串到 NFC
// -------------------------------------------------------------------------
public static void writeJson(Tag tag, String json) throws Exception {
if (tag == null) throw new Exception("tag is null");
Ndef ndef = Ndef.get(tag);
if (ndef == null) throw new Exception("标签不支持 NDEF");
try {
ndef.connect();
int maxSize = ndef.getMaxSize();
int realSize = json.getBytes(Charset.forName("UTF-8")).length;
if (realSize > maxSize) {
throw new Exception("数据过大 (" + realSize + ") > 容量 (" + maxSize + ")");
}
NdefRecord record = createTextRecord(json, true);
NdefMessage msg = new NdefMessage(new NdefRecord[]{record});
ndef.writeNdefMessage(msg);
LogUtils.d(TAG, "writeJson: 写入成功");
} finally {
if (ndef != null && ndef.isConnected()) {
ndef.close();
}
}
}
// -------------------------------------------------------------------------
// 创建 NFC 文本记录
// -------------------------------------------------------------------------
public static NdefRecord createTextRecord(String text, boolean isUtf8) {
byte[] langBytes = "en".getBytes(Charset.forName("US-ASCII"));
byte[] textBytes = text.getBytes(Charset.forName(isUtf8 ? "UTF-8" : "UTF-16"));
int status = isUtf8 ? 0 : 0x80;
status |= langBytes.length & 0x3F;
byte[] data = new byte[1 + langBytes.length + textBytes.length];
data[0] = (byte) status;
System.arraycopy(langBytes, 0, data, 1, langBytes.length);
System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);
return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
}
// -------------------------------------------------------------------------
// 辅助:JSON -> NfcTermuxCmd
// -------------------------------------------------------------------------
public static NfcTermuxCmd jsonToCmd(String json) throws JsonSyntaxException {
return sGson.fromJson(json, NfcTermuxCmd.class);
}
// -------------------------------------------------------------------------
// 辅助:NfcTermuxCmd -> JSON
// -------------------------------------------------------------------------
public static String cmdToJson(NfcTermuxCmd cmd) {
return sGson.toJson(cmd);
}
}
@@ -1,11 +0,0 @@
package cc.winboll.studio.autonfc.nfc;
public interface OnNfcStateListener {
void onNfcConnected(); // 无参数!
void onNfcDisconnected();
void onNfcReadSuccess(String data);
void onNfcReadFail(String error);
void onNfcWriteSuccess();
void onNfcWriteFail(String error);
}
@@ -1,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>
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="脚本名称 script:"/>
<EditText
android:id="@+id/et_script"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="如 auth.sh"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="参数 args (逗号分隔):"/>
<EditText
android:id="@+id/et_args"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="user1,pass123"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="工作目录 workDir:"/>
<EditText
android:id="@+id/et_workDir"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="/data/data/com.termux/files/home"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="后台执行 background (true/false):"/>
<EditText
android:id="@+id/et_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="结果目录 resultDir:"/>
<EditText
android:id="@+id/et_resultDir"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="/data/data/com.termux/files/log"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:onClick="onFillTestDataClick"
android:text="填入调试数据 (BuildWinBoLLProject.sh)"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:onClick="onWriteClick"
android:text="写入 NFC (NfcTermuxCmd JSON)"/>
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="状态"/>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:scrollbars="vertical"
android:textSize="14sp"/>
</LinearLayout>
</ScrollView>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_log"
android:title="启动日志"/>
</menu>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#FF9800</color>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<resources>
<string name="app_name">AutoNFC</string>
</resources>
-11
View File
@@ -1,11 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
+23 -39
View File
@@ -18,44 +18,30 @@ def genVersionName(def versionName){
}
android {
// 适配MIUI12
compileSdkVersion 30
compileSdkVersion 30
buildToolsVersion "30.0.3"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
applicationId "cc.winboll.studio.contacts"
minSdkVersion 23
// 适配MIUI12
minSdkVersion 26
targetSdkVersion 30
versionCode 2
versionCode 3
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.14"
versionName "15.21"
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'
// 拼音搜索
@@ -72,29 +58,27 @@ dependencies {
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 'androidx.cardview:cardview:1.0.0'
api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.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'
//注意:以下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'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
// WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libaes:15.20.21'
//api 'cc.winboll.studio:libappbase:15.20.34'
api 'cc.winboll.studio:libaes:15.21.+'
api 'cc.winboll.studio:libappbase:15.21.+'
api fileTree(dir: 'libs', include: ['*.jar'])
}
+16 -6
View File
@@ -1,8 +1,18 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Apr 18 21:14:59 HKT 2026
stageCount=13
<<<<<<< HEAD
#Tue Jul 21 11:13:12 CST 2026
stageCount=12
libraryProject=
baseVersion=15.14
publishVersion=15.14.12
buildCount=0
baseBetaVersion=15.14.13
baseVersion=15.21
publishVersion=15.20.11
buildCount=22
baseBetaVersion=15.20.12
=======
#Tue Jul 21 02:54:31 GMT 2026
stageCount=0
libraryProject=
baseVersion=15.21
publishVersion=15.21.0
buildCount=1
baseBetaVersion=15.21.1
>>>>>>> origin/contacts
+7 -2
View File
@@ -3,6 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.contacts">
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- BIND_AUTOFILL_SERVICE -->
<uses-permission android:name="android.permission.BIND_AUTOFILL_SERVICE"/>
@@ -251,8 +254,10 @@
<activity android:name="cc.winboll.studio.contacts.activities.AboutActivity"/>
<service android:name="cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService"/>
<service android:name="cc.winboll.studio.contacts.services.LimitedTimeGreenChannelService"/>
<activity android:name="cc.winboll.studio.contacts.unittest.LayoutDraftPreViewActivity"/>
</application>
</manifest>
</manifest>
@@ -16,9 +16,6 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
// 设置应用调试标志
setIsDebugging(BuildConfig.DEBUG);
// 初始化窗口管理类
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
@@ -31,15 +31,13 @@ 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.fragments.DunQueryFragment;
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;
@@ -76,8 +74,6 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
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;
@@ -136,10 +132,6 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
@Override
protected void onResume() {
super.onResume();
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
LogUtils.d(TAG, "onResume: 广告栏资源已恢复");
}
}
@Override
@@ -151,11 +143,6 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
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();
@@ -293,10 +280,6 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
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());
@@ -335,10 +318,10 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
// 添加Fragment实例(仅创建对象,不初始化业务逻辑)
fragmentList.add(CallLogFragment.newInstance(0));
fragmentList.add(ContactsFragment.newInstance(1));
fragmentList.add(LogFragment.newInstance(2));
fragmentList.add(DunQueryFragment.newInstance(2));
tabTitleList.add("通话记录");
tabTitleList.add("联系人");
tabTitleList.add("应用日志");
tabTitleList.add("云盾查询");
// 初始化懒加载标记数组(默认均未初始化)
int fragmentCount = fragmentList.size();
@@ -382,8 +365,8 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
((CallLogFragment) targetFragment).initData();
} else if (targetFragment instanceof ContactsFragment) {
((ContactsFragment) targetFragment).initData();
} else if (targetFragment instanceof LogFragment) {
((LogFragment) targetFragment).initData();
} else if (targetFragment instanceof DunQueryFragment) {
((DunQueryFragment) targetFragment).initData();
}
// 标记为已初始化
isFragmentInit[position] = true;
@@ -1,38 +1,22 @@
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;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
import androidx.appcompat.widget.Toolbar;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/31 15:15:54
* @Describe 应用介绍窗口
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/11 12:55
* @Describe AboutActivity
*/
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
@Override
public Activity getActivity() {
return this;
}
@@ -41,76 +25,32 @@ public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
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);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
LogUtils.d(TAG, "onCreate: 关于页面初始化完成");
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
@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");
private APPInfo genDefaultAppInfo() {
String branchName = "contacts";
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.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("Contacts");
appInfo.setAppGitOwner("ZhanGSKen");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Contacts");
appInfo.setAppAPKName("Contacts");
appInfo.setAppAPKFolderName("Contacts");
return new AboutView(mContext, appInfo);
return appInfo;
}
}
@@ -29,6 +29,7 @@ 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.DebugToolButtonsView;
import cc.winboll.studio.contacts.views.DuInfoTextView;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
@@ -98,6 +99,9 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit
initDunSettings(); // 云盾设置初始化
initBoBullToonViews(); // BoBullToon功能初始化
// 调试模式下加载调试工具按钮组
initDebugTools();
LogUtils.d(TAG, "onCreate: 设置页面初始化完成");
}
@@ -300,6 +304,26 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit
}
}
/**
* 初始化调试工具按钮组(仅在调试模式下加载)
*/
private void initDebugTools() {
LogUtils.d(TAG, "initDebugTools: 检查调试模式状态");
if (cc.winboll.studio.libappbase.GlobalApplication.isDebugging()) {
LogUtils.d(TAG, "initDebugTools: 调试模式已开启,加载调试工具按钮组");
android.widget.LinearLayout contentLayout = (android.widget.LinearLayout) findViewById(R.id.activity_settings_content);
if (contentLayout != null) {
DebugToolButtonsView debugView = new DebugToolButtonsView(this);
contentLayout.addView(debugView);
LogUtils.d(TAG, "initDebugTools: 调试工具按钮组加载完成");
} else {
LogUtils.e(TAG, "initDebugTools: 内容布局未找到,加载失败");
}
} else {
LogUtils.d(TAG, "initDebugTools: 调试模式未开启,跳过加载");
}
}
// ====================== 点击事件回调区(按功能模块归类) ======================
/**
* 云盾开关点击事件(联动参数编辑权限+配置保存)
@@ -382,6 +406,21 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit
LogUtils.d(TAG, "onAddNewConnectionRule: 规则添加完成,当前共" + mRuleList.size() + "条规则");
}
/**
* 初始化测试规则
*/
public void onInitTestRules(View view) {
LogUtils.d(TAG, "onInitTestRules: 初始化测试规则");
Rules rules = Rules.getInstance(this);
if (rules == null) {
LogUtils.e(TAG, "onInitTestRules: Rules实例获取失败");
return;
}
initTestRulesIfEmpty(rules);
mRuleAdapter.notifyDataSetChanged();
ToastUtils.show("测试规则初始化完成");
}
/**
* 跳转默认电话应用设置
*/
@@ -528,15 +567,24 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
}
// ====================== 工具方法区(通用功能+权限相关) ======================
/**
* 跳转日志查看页面
* 规则集为空时初始化测试规则
*/
public void onLogView(View view) {
LogUtils.d(TAG, "onLogView: 跳转日志页面");
WinBoLLActivityManager.getInstance().startLogActivity(this);
public static void initTestRulesIfEmpty(Rules rules) {
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前无规则,添加测试规则集");
rules.add("^1[3-9]\\d{9}$", true, true);
rules.add("^0660\\d+$", true, true);
rules.add("^020\\d+$", true, true);
rules.add(".*", false, true);
rules.saveRules();
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
} else {
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
}
}
// ====================== 工具方法区(通用功能+权限相关) ======================
/**
* 更新音量显示文本(当前音量/最大音量)
*/
@@ -5,9 +5,9 @@ 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.activities.SettingsActivity;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
import cc.winboll.studio.contacts.services.LimitedTimeGreenChannelService;
import cc.winboll.studio.contacts.utils.IntUtils;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
@@ -103,7 +103,7 @@ public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivit
// 初始化规则实例
Rules rules = Rules.getInstance(this);
// 无规则时添加测试规则集
initTestRulesIfEmpty(rules);
SettingsActivity.initTestRulesIfEmpty(rules);
// 预设测试号码列表
String[] testPhones = {
@@ -122,33 +122,9 @@ public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivit
new Thread(new Runnable(){
@Override
public void run() {
LimitedTimeSpecialChannelService.unitTest(UnitTestActivity.this);
LimitedTimeGreenChannelService.unitTest(UnitTestActivity.this);
}
}).start();
}
// ====================== 私有工具函数区 ======================
/**
* 规则集为空时初始化测试规则
*/
private void initTestRulesIfEmpty(Rules rules) {
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前无规则,添加测试规则集");
// 规则1:中国手机号允许
rules.add("^1[3-9]\\d{9}$", true, true);
// 规则20660区号号码允许
rules.add("^0660\\d+$", true, true);
// 规则3020区号号码允许
rules.add("^020\\d+$", true, true);
// 规则4:默认拒接所有号码
rules.add(".*", false, true);
// 保存规则到本地
rules.saveRules();
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
} else {
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
}
}
}
@@ -1,9 +1,16 @@
package cc.winboll.studio.contacts.activities;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.model.SettingsBean;
import cc.winboll.studio.contacts.services.LimitedTimeGreenChannelService;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
@@ -21,6 +28,8 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
// ====================== 成员变量区 ======================
protected volatile AESThemeBean.ThemeType mThemeType;
private android.content.BroadcastReceiver mGreenChannelReceiver;
private int mDefaultStatusBarColor = 0; // 默认状态栏颜色
// ====================== 接口实现区 ======================
@Override
@@ -42,6 +51,114 @@ public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivi
// setThemeStyle();
super.onCreate(savedInstanceState);
//LogUtils.d(TAG, "onCreate: 基类主题设置完成,当前主题类型=" + mThemeType);
// 保存默认状态栏颜色
saveDefaultStatusBarColor();
// 检测绿色通道状态,设置顶部状态栏背景色
updateStatusBarForGreenChannel();
// 注册绿色通道状态变更广播接收器
registerGreenChannelReceiver();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 反注册绿色通道状态变更广播接收器
unregisterGreenChannelReceiver();
}
/**
* 根据绿色通道状态更新顶部状态栏背景色
* 绿色通道开启时状态栏背景为绿色,其他情况不改变
*/
private void updateStatusBarForGreenChannel() {
// API 21+ 才支持设置状态栏颜色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
boolean isGreenChannelEnabled = false;
Rules rules = Rules.getInstance(getApplicationContext());
if (rules != null) {
SettingsBean settings = rules.getSettingsModel();
if (settings != null) {
isGreenChannelEnabled = settings.isEnableGreenChannel();
}
}
if (isGreenChannelEnabled) {
// 绿色通道开启,设置状态栏为绿色
getWindow().setStatusBarColor(android.graphics.Color.GREEN);
LogUtils.d(TAG, "updateStatusBarForGreenChannel: 绿色通道已开启,状态栏背景设为绿色");
} else {
// 绿色通道关闭,恢复默认状态栏颜色
// 如果默认颜色为0(透明),则获取主题默认颜色
int restoreColor = mDefaultStatusBarColor;
if (restoreColor == 0) {
// 从主题中获取默认状态栏颜色
android.util.TypedValue typedValue = new android.util.TypedValue();
if (getTheme().resolveAttribute(android.R.attr.statusBarColor, typedValue, true)) {
restoreColor = typedValue.data;
}
}
getWindow().setStatusBarColor(restoreColor);
LogUtils.d(TAG, "updateStatusBarForGreenChannel: 绿色通道已关闭,状态栏背景已恢复, color=0x" + Integer.toHexString(restoreColor));
}
}
}
/**
* 保存默认状态栏颜色(在设置绿色通道颜色之前调用)
*/
private void saveDefaultStatusBarColor() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mDefaultStatusBarColor = getWindow().getStatusBarColor();
// 如果获取到的颜色为0(透明),尝试从主题获取默认颜色
if (mDefaultStatusBarColor == 0) {
android.util.TypedValue typedValue = new android.util.TypedValue();
if (getTheme().resolveAttribute(android.R.attr.statusBarColor, typedValue, true)) {
mDefaultStatusBarColor = typedValue.data;
}
}
LogUtils.d(TAG, "saveDefaultStatusBarColor: 保存默认状态栏颜色=0x" + Integer.toHexString(mDefaultStatusBarColor));
}
}
/**
* 注册绿色通道状态变更本地广播接收器
*/
private void registerGreenChannelReceiver() {
LogUtils.d(TAG, "registerGreenChannelReceiver: 注册绿色通道状态变更广播接收器");
mGreenChannelReceiver = new android.content.BroadcastReceiver() {
@Override
public void onReceive(android.content.Context context, Intent intent) {
if (LimitedTimeGreenChannelService.ACTION_GREEN_CHANNEL_STATE_CHANGED.equals(intent.getAction())) {
boolean isEnabled = intent.getBooleanExtra(LimitedTimeGreenChannelService.EXTRA_GREEN_CHANNEL_ENABLED, false);
LogUtils.d(TAG, "registerGreenChannelReceiver: 收到绿色通道状态变更广播, isEnabled=" + isEnabled);
// 调用统一方法更新状态栏颜色
updateStatusBarForGreenChannel();
}
}
};
IntentFilter filter = new IntentFilter(LimitedTimeGreenChannelService.ACTION_GREEN_CHANNEL_STATE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mGreenChannelReceiver, filter);
LogUtils.d(TAG, "registerGreenChannelReceiver: 广播接收器注册完成");
}
/**
* 反注册绿色通道状态变更本地广播接收器
*/
private void unregisterGreenChannelReceiver() {
if (mGreenChannelReceiver != null) {
LogUtils.d(TAG, "unregisterGreenChannelReceiver: 反注册绿色通道状态变更广播接收器");
LocalBroadcastManager.getInstance(this).unregisterReceiver(mGreenChannelReceiver);
mGreenChannelReceiver = null;
LogUtils.d(TAG, "unregisterGreenChannelReceiver: 反注册完成");
}
}
// ====================== 主题相关函数区 ======================
@@ -69,7 +69,7 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV
}
@Override
public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
public void onBindViewHolder(@NonNull final CallLogViewHolder holder, int position) {
LogUtils.d(TAG, "onBindViewHolder: 绑定列表项数据,position=" + position);
final CallLogModel callLog = callLogList.get(position);
@@ -54,7 +54,7 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV
}
@Override
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
public void onBindViewHolder(@NonNull final ContactViewHolder holder, int position) {
LogUtils.d(TAG, "onBindViewHolder: 绑定联系人列表项数据,position=" + position);
final ContactModel contact = contactList.get(position);
@@ -5,7 +5,7 @@ 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.LimitedTimeGreenChannelService;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.contacts.utils.ContactUtils;
import cc.winboll.studio.contacts.utils.IntUtils;
@@ -172,9 +172,9 @@ public class Rules {
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));
// 限时绿色通道打开时返回连接
if (!isDefend && LimitedTimeGreenChannelService.isServiceRunning()) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n and Limited Time Green Channel Service Is Running.", phoneNumber));
isDefend = true;
isConnect = true;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
@@ -0,0 +1,284 @@
package cc.winboll.studio.contacts.floatingwindow;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PixelFormat;
import android.os.Build;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.services.LimitedTimeGreenChannelService;
import cc.winboll.studio.contacts.utils.PermissionUtils;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2026/07/20 15:00:00
* @Describe 绿色通道悬浮窗管理类
* 负责悬浮窗的创建、显示、隐藏和销毁
* 适配 Android API 26+ Java 7 语法规范
*/
public class GreenChannelFloatingWindow {
// ====================== 常量定义区 ======================
public static final String TAG = "GreenChannelFloatingWindow";
// Android版本常量(适配Java7
private static final int ANDROID_8_API = 26;
private static final int ANDROID_10_API = 29;
private static final int ANDROID_19_API = 19;
// ====================== 成员变量区 ======================
private Context mContext;
private WindowManager mWindowManager;
private WindowManager.LayoutParams mWindowParams;
private View mFloatingView;
private boolean mIsShowing;
private android.content.BroadcastReceiver mGreenChannelReceiver;
// ====================== 构造函数区 ======================
/**
* 构造函数
* @param context 上下文
*/
public GreenChannelFloatingWindow(Context context) {
this.mContext = context;
this.mIsShowing = false;
LogUtils.d(TAG, "GreenChannelFloatingWindow: 构造函数调用完成");
}
// ====================== 公共方法区 ======================
/**
* 显示悬浮窗
* @return true=显示成功,false=显示失败(权限不足或已显示)
*/
public boolean show() {
LogUtils.d(TAG, "show: 尝试显示悬浮窗");
// 检查是否已显示
if (mIsShowing) {
LogUtils.w(TAG, "show: 悬浮窗已显示,跳过重复显示");
return true;
}
// 检查悬浮窗权限
if (!PermissionUtils.isOverlayPermissionGranted(mContext)) {
LogUtils.e(TAG, "show: 悬浮窗权限未授予,无法显示");
return false;
}
// 初始化窗口管理器
if (!initWindowManager()) {
LogUtils.e(TAG, "show: 窗口管理器初始化失败");
return false;
}
// 初始化悬浮窗视图
if (!initFloatingView()) {
LogUtils.e(TAG, "show: 悬浮窗视图初始化失败");
return false;
}
// 显示悬浮窗
try {
mWindowManager.addView(mFloatingView, mWindowParams);
mIsShowing = true;
// 注册绿色通道状态变更广播接收器
registerGreenChannelReceiver();
LogUtils.d(TAG, "show: 悬浮窗显示成功");
return true;
} catch (SecurityException e) {
LogUtils.e(TAG, "show: 悬浮窗显示失败(安全异常)", e);
return false;
} catch (Exception e) {
LogUtils.e(TAG, "show: 悬浮窗显示异常", e);
return false;
}
}
/**
* 隐藏悬浮窗
*/
public void dismiss() {
LogUtils.d(TAG, "dismiss: 尝试隐藏悬浮窗");
if (!mIsShowing) {
LogUtils.d(TAG, "dismiss: 悬浮窗未显示,跳过隐藏操作");
return;
}
// 反注册绿色通道状态变更广播接收器
unregisterGreenChannelReceiver();
if (mFloatingView != null && mWindowManager != null) {
try {
mWindowManager.removeView(mFloatingView);
mIsShowing = false;
LogUtils.d(TAG, "dismiss: 悬浮窗隐藏成功");
} catch (Exception e) {
LogUtils.e(TAG, "dismiss: 悬浮窗隐藏异常", e);
}
}
}
/**
* 检查悬浮窗是否正在显示
* @return true=正在显示,false=未显示
*/
public boolean isShowing() {
return mIsShowing;
}
/**
* 获取悬浮窗视图
* @return 悬浮窗视图,未初始化时返回null
*/
public View getFloatingView() {
return mFloatingView;
}
// ====================== 私有方法区 ======================
/**
* 初始化窗口管理器
* @return true=初始化成功,false=初始化失败
*/
private boolean initWindowManager() {
if (mWindowManager == null) {
mWindowManager = (WindowManager) mContext.getApplicationContext()
.getSystemService(Context.WINDOW_SERVICE);
if (mWindowManager == null) {
LogUtils.e(TAG, "initWindowManager: WindowManager获取失败");
return false;
}
LogUtils.d(TAG, "initWindowManager: WindowManager初始化成功");
}
// 初始化窗口参数
initWindowParams();
return true;
}
/**
* 初始化窗口参数(适配API26+窗口类型)
*/
private void initWindowParams() {
mWindowParams = new WindowManager.LayoutParams();
// 窗口位置:顶部居中
mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
// 窗口大小:宽度全屏,高度自适应
mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT;
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 窗口格式:半透明
mWindowParams.format = PixelFormat.TRANSLUCENT;
// 窗口类型(适配API26+
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
LogUtils.d(TAG, "initWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY");
} else if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
LogUtils.d(TAG, "initWindowParams: API26-28 悬浮窗类型=TYPE_APPLICATION_OVERLAY");
} else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
LogUtils.d(TAG, "initWindowParams: API26以下 悬浮窗类型=TYPE_PHONE");
}
// 窗口标志:无焦点(不抢占输入)、全屏布局
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
// API19+ 添加透明状态栏/导航栏标志
if (Build.VERSION.SDK_INT >= ANDROID_19_API) {
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
}
LogUtils.d(TAG, "initWindowParams: 窗口参数初始化完成");
}
/**
* 初始化悬浮窗视图
* @return true=初始化成功,false=初始化失败
*/
private boolean initFloatingView() {
if (mFloatingView != null) {
LogUtils.d(TAG, "initFloatingView: 视图已初始化,跳过重复创建");
return true;
}
try {
// 创建FrameLayout拦截返回键
FrameLayout keyInterceptorLayout = new FrameLayout(mContext) {
@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);
}
};
// 加载悬浮窗布局
mFloatingView = LayoutInflater.from(mContext)
.inflate(R.layout.view_green_channel_floating, keyInterceptorLayout);
LogUtils.d(TAG, "initFloatingView: 悬浮窗视图初始化完成");
return true;
} catch (Exception e) {
LogUtils.e(TAG, "initFloatingView: 悬浮窗视图初始化异常", e);
return false;
}
}
/**
* 注册绿色通道状态变更广播接收器
* 接收 LimitedTimeGreenChannelView 发出的通道开启与关闭消息
*/
private void registerGreenChannelReceiver() {
LogUtils.d(TAG, "registerGreenChannelReceiver: 注册绿色通道状态变更广播接收器");
mGreenChannelReceiver = new android.content.BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (LimitedTimeGreenChannelService.ACTION_GREEN_CHANNEL_STATE_CHANGED.equals(intent.getAction())) {
boolean isEnabled = intent.getBooleanExtra(
LimitedTimeGreenChannelService.EXTRA_GREEN_CHANNEL_ENABLED, false);
LogUtils.d(TAG, "registerGreenChannelReceiver: 收到绿色通道状态变更广播, isEnabled=" + isEnabled);
// 如果绿色通道关闭,关闭悬浮窗
if (!isEnabled) {
LogUtils.d(TAG, "registerGreenChannelReceiver: 绿色通道已关闭,准备关闭悬浮窗");
dismiss();
}
}
}
};
IntentFilter filter = new IntentFilter(LimitedTimeGreenChannelService.ACTION_GREEN_CHANNEL_STATE_CHANGED);
LocalBroadcastManager.getInstance(mContext).registerReceiver(mGreenChannelReceiver, filter);
LogUtils.d(TAG, "registerGreenChannelReceiver: 广播接收器注册完成");
}
/**
* 反注册绿色通道状态变更广播接收器
*/
private void unregisterGreenChannelReceiver() {
if (mGreenChannelReceiver != null) {
LogUtils.d(TAG, "unregisterGreenChannelReceiver: 反注册绿色通道状态变更广播接收器");
LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mGreenChannelReceiver);
mGreenChannelReceiver = null;
LogUtils.d(TAG, "unregisterGreenChannelReceiver: 反注册完成");
}
}
}
@@ -0,0 +1,76 @@
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;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/19
* @Describe 云盾查询区域视图(支持懒加载,仅切换到当前页才加载)
*/
public class DunQueryFragment extends Fragment {
public static final String TAG = "DunQueryFragment";
private static final String ARG_PAGE = "ARG_PAGE";
private int mPage;
private boolean isViewInitialized = false;
private boolean isLazyInitCompleted = false;
public static DunQueryFragment newInstance(int page) {
LogUtils.d(TAG, "newInstance: 创建云盾查询Fragment实例,页码=" + page);
Bundle args = new Bundle();
args.putInt(ARG_PAGE, page);
DunQueryFragment fragment = new DunQueryFragment();
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_dun_query, container, false);
isViewInitialized = true;
LogUtils.d(TAG, "onCreateView: Fragment布局加载完成");
return view;
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
isViewInitialized = false;
isLazyInitCompleted = false;
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
}
public void initData() {
if (isLazyInitCompleted || !isViewInitialized || getContext() == null) {
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
return;
}
LogUtils.d(TAG, "initData: 开始懒加载初始化");
isLazyInitCompleted = true;
LogUtils.d(TAG, "initData: 懒加载初始化完成");
}
}
@@ -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正常启动");
}
}
@@ -2,7 +2,7 @@ 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.models.libs1520000.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
@@ -2,7 +2,7 @@ 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.models.libs1520000.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
@@ -2,7 +2,7 @@ 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.models.libs1520000.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
@@ -6,7 +6,7 @@ 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.models.libs1520000.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
@@ -28,6 +28,7 @@ public class SettingsBean extends BaseBean {
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 static final String JSON_KEY_GREEN_CHANNEL_ENABLE = "isEnableGreenChannel";
// ====================== 成员变量区 ======================
// 云盾防御层数量
@@ -40,6 +41,8 @@ public class SettingsBean extends BaseBean {
private int dunResumeCount;
// 是否启用云盾
private boolean isEnableDun;
// 是否启用绿色通道
private boolean isEnableGreenChannel;
// BoBullToon 应用模块数据请求地址
private String szBoBullToon_URL;
@@ -53,6 +56,7 @@ public class SettingsBean extends BaseBean {
this.dunResumeSecondCount = 60;
this.dunResumeCount = 1;
this.isEnableDun = false;
this.isEnableGreenChannel = false;
this.szBoBullToon_URL = "";
LogUtils.d(TAG, "SettingsModel: 默认构造初始化完成 | 云盾默认配置加载完毕");
}
@@ -61,17 +65,20 @@ public class SettingsBean extends BaseBean {
* 带参构造,初始化自定义配置并校验数值范围
*/
public SettingsBean(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount,
int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
int dunResumeCount, boolean isEnableDun, boolean isEnableGreenChannel,
String szBoBullToon_URL) {
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
this.isEnableDun = isEnableDun;
this.isEnableGreenChannel = isEnableGreenChannel;
this.szBoBullToon_URL = szBoBullToon_URL == null ? "" : szBoBullToon_URL;
LogUtils.d(TAG, "SettingsModel: 带参构造初始化完成 | 总层数=" + this.dunTotalCount
+ " | 当前层数=" + this.dunCurrentCount + " | 恢复间隔=" + this.dunResumeSecondCount
+ " | 恢复层数=" + this.dunResumeCount + " | 云盾启用=" + this.isEnableDun);
+ " | 恢复层数=" + this.dunResumeCount + " | 云盾启用=" + this.isEnableDun
+ " | 绿色通道启用=" + this.isEnableGreenChannel);
}
// ====================== 私有工具方法区 ======================
@@ -136,6 +143,15 @@ public class SettingsBean extends BaseBean {
this.isEnableDun = isEnableDun;
}
public boolean isEnableGreenChannel() {
return isEnableGreenChannel;
}
public void setIsEnableGreenChannel(boolean isEnableGreenChannel) {
LogUtils.d(TAG, "setIsEnableGreenChannel: 绿色通道启用状态更新为" + isEnableGreenChannel);
this.isEnableGreenChannel = isEnableGreenChannel;
}
public String getBoBullToon_URL() {
return szBoBullToon_URL;
}
@@ -164,6 +180,7 @@ public class SettingsBean extends BaseBean {
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_GREEN_CHANNEL_ENABLE).value(isEnableGreenChannel());
jsonWriter.name(JSON_KEY_URL).value(getBoBullToon_URL());
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
}
@@ -187,6 +204,8 @@ public class SettingsBean extends BaseBean {
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
} else if (JSON_KEY_DUN_ENABLE.equals(name)) {
setIsEnableDun(jsonReader.nextBoolean());
} else if (JSON_KEY_GREEN_CHANNEL_ENABLE.equals(name)) {
setIsEnableGreenChannel(jsonReader.nextBoolean());
} else if (JSON_KEY_URL.equals(name)) {
setBoBullToon_URL(jsonReader.nextString());
} else {
@@ -13,17 +13,17 @@ 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 限时特殊通道服务
* @Describe 限时绿色通道服务
* 提供安全的服务启动令牌校验及循环任务管理
* 新增功能
* 1. 计时过程中实时输出剩余秒数
* 2. 服务销毁前发送本地广播通知应用内其他组件并携带剩余秒数信息
* 3. 每秒发送一次倒计时状态的本地广播
*/
public class LimitedTimeSpecialChannelService extends Service {
public class LimitedTimeGreenChannelService extends Service {
// ========================= 常量定义 =========================
public static final String TAG = "LimitedTimeSpecialChannelService";
public static final String TAG = "LimitedTimeGreenChannelService";
public static final String EXTRA_DELAY_MILLIS = "EXTRA_DELAY_MILLIS";
private static final String EXTRA_SECURITY_TOKEN = "EXTRA_SECURITY_TOKEN";
@@ -32,6 +32,10 @@ public class LimitedTimeSpecialChannelService extends Service {
// 倒计时心跳广播 Action
public static final String ACTION_COUNTDOWN_TICK = "cc.winboll.studio.contacts.services.ACTION_COUNTDOWN_TICK";
// 绿色通道状态变更广播 Action
public static final String ACTION_GREEN_CHANNEL_STATE_CHANGED = "cc.winboll.studio.contacts.services.ACTION_GREEN_CHANNEL_STATE_CHANGED";
// 广播携带的额外参数绿色通道是否开启
public static final String EXTRA_GREEN_CHANNEL_ENABLED = "EXTRA_GREEN_CHANNEL_ENABLED";
// 广播携带的额外参数剩余秒数
public static final String EXTRA_REMAINING_SECONDS = "EXTRA_REMAINING_SECONDS";
// 广播携带的额外参数总定时秒数
@@ -44,7 +48,7 @@ public class LimitedTimeSpecialChannelService extends Service {
*/
public static final String mValidToken = "VALID_TOKEN_" + System.currentTimeMillis();
private static volatile LimitedTimeSpecialChannelService sInstance = null;
private static volatile LimitedTimeGreenChannelService sInstance = null;
private static volatile boolean sIsServiceRunning = false;
// ========================= 成员变量 =========================
@@ -73,7 +77,7 @@ public class LimitedTimeSpecialChannelService extends Service {
}
// 构建Intent传递参数
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
Intent intent = new Intent(context, LimitedTimeGreenChannelService.class);
intent.putExtra(EXTRA_SECURITY_TOKEN, mValidToken);
intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis);
@@ -93,7 +97,7 @@ public class LimitedTimeSpecialChannelService extends Service {
LogUtils.w(TAG, "停止失败,上下文为null");
return;
}
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
Intent intent = new Intent(context, LimitedTimeGreenChannelService.class);
context.stopService(intent);
LogUtils.i(TAG, "服务停止命令已发出");
}
@@ -106,6 +110,18 @@ public class LimitedTimeSpecialChannelService extends Service {
return sIsServiceRunning;
}
/**
* 公共静态方法发送绿色通道状态变更广播
* @param context 上下文
* @param isEnabled 绿色通道是否开启
*/
public static void sendGreenChannelStateChangedBroadcast(Context context, boolean isEnabled) {
LogUtils.i(TAG, "sendGreenChannelStateChangedBroadcast: 发送绿色通道状态变更广播, isEnabled=" + isEnabled);
Intent intent = new Intent(ACTION_GREEN_CHANNEL_STATE_CHANGED);
intent.putExtra(EXTRA_GREEN_CHANNEL_ENABLED, isEnabled);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
/**
* 核心单元测试方法
* 执行完整的单元测试流程
@@ -0,0 +1,47 @@
package cc.winboll.studio.contacts.unittest;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/18 19:35
* @Describe 布局控件快速便捷辅助草稿
*/
public class LayoutDraftPreViewActivity extends AppCompatActivity {
public static final String TAG = "LayoutDraftPreViewActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 布局草稿预览窗口启动");
setContentView(R.layout.activity_layoutdraft_preview);
initToolbar();
LogUtils.d(TAG, "onCreate: 初始化完成");
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
Toolbar toolbar = (Toolbar) findViewById(R.id.activity_layoutdraft_preview_toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(TAG);
}
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面");
finish();
}
});
LogUtils.d(TAG, "initToolbar: 工具栏初始化完成");
}
}
@@ -0,0 +1,154 @@
package cc.winboll.studio.contacts.views;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.floatingwindow.GreenChannelFloatingWindow;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/18
* @Describe 调试工具按钮组控件:提供常用调试功能快捷入口,仅在调试模式下显示
*/
public class DebugToolButtonsView extends LinearLayout {
private static final String TAG = "DebugToolButtonsView";
private Context mContext;
private GreenChannelFloatingWindow mGreenChannelFloatingWindow;
public DebugToolButtonsView(Context context) {
super(context);
initView(context);
}
public DebugToolButtonsView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public DebugToolButtonsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
LogUtils.d(TAG, "initView: 初始化调试工具按钮组");
this.mContext = context;
setOrientation(VERTICAL);
LayoutInflater.from(context).inflate(R.layout.view_debug_tool_buttons, this, true);
initButtons();
LogUtils.d(TAG, "initView: 调试工具按钮组初始化完成");
}
private void initButtons() {
Button btnCrash = (Button) findViewById(R.id.btn_debug_crash);
Button btnLayoutPreview = (Button) findViewById(R.id.btn_debug_layout_preview);
Button btnUnitTest = (Button) findViewById(R.id.btn_debug_unit_test);
Button btnLogView = (Button) findViewById(R.id.btn_debug_logview);
Button btnGreenChannel = (Button) findViewById(R.id.btn_debug_green_channel);
btnCrash.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onTestCrash();
}
});
btnLayoutPreview.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onOpenLayoutPreview();
}
});
btnUnitTest.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onOpenUnitTest();
}
});
btnLogView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onLogView();
}
});
btnGreenChannel.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onOpenGreenChannel();
}
});
}
private void onTestCrash() {
LogUtils.d(TAG, "onTestCrash: 触发测试崩溃");
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
((android.app.Activity) mContext).getString(i);
}
}
private void onOpenLayoutPreview() {
LogUtils.d(TAG, "onOpenLayoutPreview: 打开布局草稿预览窗口");
try {
Intent intent = new Intent();
intent.setClassName(mContext, "cc.winboll.studio.contacts.unittest.LayoutDraftPreViewActivity");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
LogUtils.d(TAG, "onOpenLayoutPreview: 跳转成功");
} catch (Exception e) {
LogUtils.e(TAG, "onOpenLayoutPreview: 跳转失败", e);
}
}
private void onOpenUnitTest() {
LogUtils.d(TAG, "onOpenUnitTest: 打开单元测试窗口");
try {
Intent intent = new Intent();
intent.setClassName(mContext, "cc.winboll.studio.contacts.activities.UnitTestActivity");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
LogUtils.d(TAG, "onOpenUnitTest: 跳转成功");
} catch (Exception e) {
LogUtils.e(TAG, "onOpenUnitTest: 跳转失败", e);
}
}
/**
* 跳转日志查看页面
*/
private void onLogView() {
LogUtils.d(TAG, "onLogView: 打开日志窗口");
LogActivity.startLogActivity(getContext(), true);
}
/**
* 打开绿色通道悬浮窗
*/
private void onOpenGreenChannel() {
LogUtils.d(TAG, "onOpenGreenChannel: 打开绿色通道悬浮窗");
if (mGreenChannelFloatingWindow == null) {
mGreenChannelFloatingWindow = new GreenChannelFloatingWindow(mContext);
}
if (mGreenChannelFloatingWindow.isShowing()) {
LogUtils.d(TAG, "onOpenGreenChannel: 悬浮窗已显示,跳过");
return;
}
boolean result = mGreenChannelFloatingWindow.show();
if (result) {
LogUtils.d(TAG, "onOpenGreenChannel: 悬浮窗显示成功");
} else {
LogUtils.e(TAG, "onOpenGreenChannel: 悬浮窗显示失败,可能缺少悬浮窗权限");
}
}
}
@@ -0,0 +1,103 @@
package cc.winboll.studio.contacts.views;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/19
* @Describe 云盾拦截查询视图控件:实时查询电话号码的云盾拦截状态
*/
public class DunQueryView extends LinearLayout {
private static final String TAG = "DunQueryView";
private Context mContext;
private EditText mEtPhone;
private TextView mTvResult;
public DunQueryView(Context context) {
super(context);
initView(context);
}
public DunQueryView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public DunQueryView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
LogUtils.d(TAG, "initView: 初始化云盾查询视图");
this.mContext = context;
setOrientation(VERTICAL);
LayoutInflater.from(context).inflate(R.layout.view_dun_query, this, true);
initViews();
initListeners();
LogUtils.d(TAG, "initView: 云盾查询视图初始化完成");
}
private void initViews() {
mEtPhone = (EditText) findViewById(R.id.et_dun_query_phone);
mTvResult = (TextView) findViewById(R.id.tv_dun_query_result);
}
private void initListeners() {
mEtPhone.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
queryDunStatus(s.toString().trim());
}
});
}
private void queryDunStatus(String phoneNumber) {
if (phoneNumber.isEmpty()) {
mTvResult.setText("请输入电话号码");
mTvResult.setTextColor(mContext.getResources().getColor(R.color.gray_500));
LogUtils.d(TAG, "queryDunStatus: 输入为空,显示默认提示");
return;
}
Rules rules = Rules.getInstance(mContext);
if (rules == null) {
mTvResult.setText("云盾规则未初始化");
mTvResult.setTextColor(mContext.getResources().getColor(R.color.gray_500));
LogUtils.e(TAG, "queryDunStatus: Rules实例获取失败");
return;
}
boolean isAllowed = rules.isAllowed(phoneNumber, true);
if (isAllowed) {
mTvResult.setText("这个号码很新,什么防御都不会启动。");
mTvResult.setTextColor(mContext.getResources().getColor(R.color.green_normal));
LogUtils.d(TAG, "queryDunStatus: 号码" + phoneNumber + " 允许接通");
} else {
mTvResult.setText("你说我像云像雾像雨又像风~~~ 怎么来来去去,怎么也又播不通~~~");
mTvResult.setTextColor(mContext.getResources().getColor(R.color.red_normal));
LogUtils.d(TAG, "queryDunStatus: 号码" + phoneNumber + " 被拦截");
}
}
}
@@ -13,79 +13,79 @@ import android.os.Message;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.WeakHashMap;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/03/19 14:04:20
* @Author 豆包&Big Pickle&MiMo&ZhanGSKen<zhangsken@qq.com>
* @CreateDate 2025/03/19 14:04:20
* @LastEdit 2026/07/21 11:40:00
* @Describe 云盾华氏度热力视图,垂直盾值温度视图控件(带颜色渐变+静态Handler更新)
* 采用绘图方式展示盾值温度,填充色随盾值比例渐变,支持设置文本在温度条左侧/右侧,底部对齐竖排显示
* 温度条宽度=5dp,文本区宽度固定=5dp,整体左右边距=0,无任何多余空白间距
* 1. 原生Canvas绘图渲染盾值温度,填充色随盾值比例动态渐变
* 2. 支持竖排数值文本切换至温度条左侧/右侧,文本底部对齐展示
* 3. 固定尺寸规范:温度条宽度5dp、文本区域宽度5dp,控件左右无外边距
* 4. 全局静态Handler统一分发盾值更新,WeakHashMap弱引用缓存控件防止内存泄漏
* 5. 代码完全兼容Java7语法,无Lambda、无try-with-resources、无高版本API
*/
public class DunTemperatureView extends View {
// ====================== 常量定义区 ======================
public static final String TAG = "DunTemperatureView";
// 控件默认高度
private static final int DEFAULT_HEIGHT = 200;
// 温度条宽度(5dp)、文本区宽度(固定5dp)
private static final int THERMOMETER_WIDTH_DP = 5;
private static final int TEXT_AREA_WIDTH_DP = 5;
// 填充区域内边距(左右=0,上下=2避免贴边)
private static final int FILL_PADDING_HORIZONTAL = 0;
private static final int FILL_PADDING_VERTICAL = 2;
// 竖排文本字间距
private static final float TEXT_CHAR_SPACING = 8f;
// Handler消息标识
public static final int MSG_UPDATE_DUN_VALUE = 0x01;
// 消息参数Key
public static final String KEY_MAX_VALUE = "max_value";
public static final String KEY_CURRENT_VALUE = "current_value";
private static final int DEFAULT_HEIGHT = 200;
private static final int THERMOMETER_WIDTH_DP = 5;
private static final int TEXT_AREA_WIDTH_DP = 5;
private static final int FILL_PADDING_HORIZONTAL = 0;
private static final int FILL_PADDING_VERTICAL = 2;
private static final float TEXT_CHAR_SPACING = 8f;
private static final int COLOR_GRADIENT_LOW = Color.GREEN;
private static final int COLOR_GRADIENT_MID = Color.YELLOW;
private static final int COLOR_GRADIENT_HIGH = Color.RED;
private static final int COLOR_BORDER = Color.parseColor("#FF444444");
private static final int COLOR_TEXT = Color.parseColor("#FF000000");
// ====================== 静态成员区 ======================
// 弱引用缓存控件实例,避免内存泄漏
private static WeakHashMap<DunTemperatureView, Object> sViewCache = new WeakHashMap<>();
// 静态Handler,处理跨线程更新消息
private static final WeakHashMap<DunTemperatureView, Object> sViewCache = new WeakHashMap<DunTemperatureView, Object>();
private static Handler sStaticHandler;
// ====================== 成员变量区 ======================
// 画笔相关
// ====================== 实例成员变量区 ======================
private Paint mThermometerPaint;
private Paint mFillPaint;
private Paint mTextPaint;
// 尺寸参数(dp转px后的值)
private int mThermometerWidth;
private int mTextAreaWidth;
private int mMaxValue = 100; // 最高盾值
private int mCurrentValue = 0; // 当前盾值
private RectF mThermometerRect; // 温度条矩形区域
// 渐变颜色配置(低→中→高 对应绿→黄→红)
private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
private float[] mGradientPositions = {0.0f, 0.5f, 1.0f};
// 布局配置:true=文本在温度条右侧(默认),false=文本在温度条左侧
private int mMaxValue = 100;
private int mCurrentValue = 0;
private RectF mThermometerRect;
private int[] mGradientColors = new int[]{COLOR_GRADIENT_LOW, COLOR_GRADIENT_MID, COLOR_GRADIENT_HIGH};
private float[] mGradientPositions = new float[]{0.0f, 0.5f, 1.0f};
private boolean isTextOnRight = true;
// 其他颜色配置
private int mBorderColor = Color.parseColor("#FF444444");
private int mTextColor = Color.parseColor("#FF000000");
private int mBorderColor = COLOR_BORDER;
private int mTextColor = COLOR_TEXT;
// ====================== 静态代码块 ======================
static {
// 初始化静态Handler,绑定主线程Looper
sStaticHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
public void handleMessage(final Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_UPDATE_DUN_VALUE) {
// 获取消息中的盾值参数
int maxValue = msg.getData().getInt(KEY_MAX_VALUE, 100);
int currentValue = msg.getData().getInt(KEY_CURRENT_VALUE, 0);
LogUtils.d(TAG, "sStaticHandler: 收到更新消息,max=" + maxValue + ", current=" + currentValue);
final int targetMsgWhat = msg.what;
if (targetMsgWhat == MSG_UPDATE_DUN_VALUE) {
final int maxValue = msg.getData().getInt(KEY_MAX_VALUE, 100);
final int currentValue = msg.getData().getInt(KEY_CURRENT_VALUE, 0);
LogUtils.d(TAG, "handleMessage receive update params, max=" + maxValue + ", current=" + currentValue);
// 遍历缓存的控件实例,更新所有实例
for (DunTemperatureView view : sViewCache.keySet()) {
if (view != null && view.isShown()) {
view.setMaxValue(maxValue);
view.setCurrentValue(currentValue);
final DunTemperatureView targetView = view;
if (targetView != null && targetView.isShown()) {
targetView.setMaxValue(maxValue);
targetView.setCurrentValue(currentValue);
}
}
}
@@ -94,203 +94,194 @@ public class DunTemperatureView extends View {
}
// ====================== 构造函数区 ======================
public DunTemperatureView(Context context) {
public DunTemperatureView(final Context context) {
super(context);
init();
}
public DunTemperatureView(Context context, AttributeSet attrs) {
public DunTemperatureView(final Context context, final AttributeSet attrs) {
super(context, attrs);
init();
}
public DunTemperatureView(Context context, AttributeSet attrs, int defStyleAttr) {
public DunTemperatureView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
// ====================== 初始化方法 ======================
// ====================== 初始化私有方法 ======================
/**
* 初始化画笔和参数
* 初始化画笔、尺寸参数并将实例存入全局缓存
*/
private void init() {
LogUtils.d(TAG, "init: 开始初始化云盾温度视图控件");
// dp转px(适配不同分辨率)
mThermometerWidth = dp2px(getContext(), THERMOMETER_WIDTH_DP);
mTextAreaWidth = dp2px(getContext(), TEXT_AREA_WIDTH_DP);
LogUtils.d(TAG, "init: 温度条宽度5dp转px=" + mThermometerWidth + ",文本区宽度5dp转px=" + mTextAreaWidth);
final Context context = getContext();
LogUtils.d(TAG, "init start");
mThermometerWidth = dp2px(context, THERMOMETER_WIDTH_DP);
mTextAreaWidth = dp2px(context, TEXT_AREA_WIDTH_DP);
LogUtils.d(TAG, "init dp convert result, thermometerWidth=" + mThermometerWidth + ", textAreaWidth=" + mTextAreaWidth);
// 温度条边框画笔(宽度1px,适配5dp宽度)
mThermometerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mThermometerPaint.setColor(mBorderColor);
mThermometerPaint.setStyle(Paint.Style.STROKE);
mThermometerPaint.setStrokeWidth(1);
// 温度条填充画笔(支持渐变)
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFillPaint.setStyle(Paint.Style.FILL);
// 文本画笔(适配5dp窄文本区,文字居中绘制)
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(18); // 缩小字号适配5dp窄文本区(避免文字超出)
mTextPaint.setTextSize(18);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setFakeBoldText(true); // 文字加粗,提升窄区域可读性
mTextPaint.setFakeBoldText(true);
// 初始化温度条矩形
mThermometerRect = new RectF();
// 将当前实例加入静态缓存
sViewCache.put(this, null);
LogUtils.d(TAG, "init: 云盾温度视图控件初始化完成,实例已加入缓存");
LogUtils.d(TAG, "init complete, add view to static cache");
}
// ====================== 工具方法区 ======================
/**
* dp 转 px(适配不同屏幕分辨率)
* dp单位转换px,多分辨率屏幕适配
* @param context 上下文
* @param dpValue dp数值
* @return 对应px像素值
*/
private int dp2px(Context context, float dpValue) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dpValue,
context.getResources().getDisplayMetrics()
);
private int dp2px(final Context context, final float dpValue) {
final float density = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * density + 0.5f);
}
// ====================== 对外控制方法 ======================
// ====================== UI布局控制方法 ======================
/**
* 设置文本相对于温度条的位置
* @param isOnRight true=文本在温度条右侧(默认)false=文本在温度条左侧
* 设置文本温度条的左右位置
* @param isOnRight true=右侧false=左侧
*/
public void setTextPosition(boolean isOnRight) {
public void setTextPosition(final boolean isOnRight) {
this.isTextOnRight = isOnRight;
invalidate(); // 刷新布局绘制
LogUtils.d(TAG, "setTextPosition input param=" + isOnRight);
invalidate();
}
/**
* 获取当前文本位置配置
* @return true=右侧,false=左侧
*/
public boolean isTextOnRight() {
return isTextOnRight;
}
// ====================== 对外静态方法区 ======================
// ====================== 全局静态更新入口 ======================
/**
* 静态外部方法:发送消息更新所有 DunTemperatureView 实例的盾值
* 可在子线程中调用
* @param maxValue 最高盾值
* 全局静态方法,统一更新所有控件盾值,支持子线程调用
* @param maxValue 最大盾值
* @param currentValue 当前盾值
*/
public static void updateDunValue(int maxValue, int currentValue) {
public static void updateDunValue(final int maxValue, final int currentValue) {
if (sStaticHandler == null) {
LogUtils.w(TAG, "updateDunValue: 静态Handler未初始化");
LogUtils.w(TAG, "updateDunValue static handler uninitialized");
return;
}
// 封装参数到消息
Message msg = sStaticHandler.obtainMessage(MSG_UPDATE_DUN_VALUE);
LogUtils.d(TAG, "static updateDunValue call, input max=" + maxValue + ", current=" + currentValue);
final Message msg = sStaticHandler.obtainMessage(MSG_UPDATE_DUN_VALUE);
msg.getData().putInt(KEY_MAX_VALUE, maxValue);
msg.getData().putInt(KEY_CURRENT_VALUE, currentValue);
// 发送消息
sStaticHandler.sendMessage(msg);
}
// ====================== 对外实例方法 ======================
// ====================== 盾值读写实例方法 ======================
/**
* 设置最盾值
* @param maxValue 最高盾值(需大于0
* 设置最盾值,非法值会拦截并打印日志
* @param maxValue 目标最大值,必须大于0
*/
public void setMaxValue(int maxValue) {
public void setMaxValue(final int maxValue) {
if (maxValue <= 0) {
LogUtils.w(TAG, "setMaxValue: 最高盾值必须大于0,当前值=" + maxValue);
LogUtils.w(TAG, "setMaxValue invalid input value=" + maxValue);
return;
}
this.mMaxValue = maxValue;
// 限制当前值不超过最大值
mCurrentValue = Math.min(mCurrentValue, maxValue);
invalidate(); // 重绘控件
LogUtils.d(TAG, "setMaxValue update max=" + maxValue + ", clamp current=" + mCurrentValue);
invalidate();
}
/**
* 设置当前盾值
* @param currentValue 当前盾值(范围 0~maxValue
* 设置当前盾值,自动限制0~maxValue区间
* @param currentValue 目标当前盾值
*/
public void setCurrentValue(int currentValue) {
int oldValue = this.mCurrentValue;
public void setCurrentValue(final int currentValue) {
final int oldVal = this.mCurrentValue;
this.mCurrentValue = Math.max(0, Math.min(currentValue, mMaxValue));
if (oldValue != this.mCurrentValue) {
LogUtils.d(TAG, "setCurrentValue: 当前盾值从" + oldValue + "更新为" + mCurrentValue);
invalidate(); // 重绘控件
if (oldVal != this.mCurrentValue) {
LogUtils.d(TAG, "setCurrentValue change value from " + oldVal + " to " + mCurrentValue);
invalidate();
}
}
/**
* 获取当前盾值
*/
public int getCurrentValue() {
return mCurrentValue;
}
/**
* 获取最高盾值
*/
public int getMaxValue() {
return mMaxValue;
}
// ====================== 渐变、颜色自定义配置 ======================
/**
* 设置自定义渐变颜色
* @param colors 渐变颜色数组(至少2种颜色)
* @param positions 颜色位置数组(与colors长度一致,0.0~1.0
* 自定义渐变颜色数组与位置节点
* @param colors 颜色数组,长度≥2
* @param positions 渐变节点数组,长度与colors一致
*/
public void setGradientColors(int[] colors, float[] positions) {
public void setGradientColors(final int[] colors, final float[] positions) {
if (colors == null || colors.length < 2 || positions == null || positions.length != colors.length) {
LogUtils.w(TAG, "setGradientColors: 渐变颜色参数不合法,颜色数组长度=" + (colors == null ? "null" : colors.length));
LogUtils.w(TAG, "setGradientColors invalid param, color array length=" + (colors == null ? "null" : colors.length));
return;
}
this.mGradientColors = colors;
this.mGradientPositions = positions;
LogUtils.d(TAG, "setGradientColors: 自定义渐变颜色已设置");
LogUtils.d(TAG, "setGradientColors custom gradient config applied");
invalidate();
}
/**
* 设置温度条边框颜色
* 更新温度条边框颜色
* @param color 目标色值
*/
public void setBorderColor(int color) {
public void setBorderColor(final int color) {
this.mBorderColor = color;
mThermometerPaint.setColor(color);
LogUtils.d(TAG, "setBorderColor: 边框颜色已更新");
LogUtils.d(TAG, "setBorderColor update border color");
invalidate();
}
/**
* 设置文本颜色
* 更新竖排文本颜色
* @param color 目标色值
*/
public void setTextColor(int color) {
public void setTextColor(final int color) {
this.mTextColor = color;
mTextPaint.setColor(color);
LogUtils.d(TAG, "setTextColor: 文本颜色已更新");
LogUtils.d(TAG, "setTextColor update text color");
invalidate();
}
// ====================== 生命周期方法 ======================
// ====================== 视图生命周期回调 ======================
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 控件从窗口移除时,从缓存中清除,避免内存泄漏
sViewCache.remove(this);
LogUtils.d(TAG, "onDetachedFromWindow: 控件实例已从缓存移除");
LogUtils.d(TAG, "onDetachedFromWindow remove view from static cache");
}
// ====================== 测量绘制 ======================
// ====================== 测量绘制工具私有方法 ======================
/**
* 测量辅助函数
* 测量尺寸计算工具
* @param defaultSize 默认尺寸
* @param measureSpec 测量规格
* @return 最终计算尺寸
*/
private int measureSize(int defaultSize, int measureSpec) {
private int measureSize(final int defaultSize, final int measureSpec) {
int result = defaultSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
@@ -302,90 +293,83 @@ public class DunTemperatureView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 强制控件整体左右内边距=0,彻底消除外部边距
setPadding(0, getPaddingTop(), 0, getPaddingBottom());
// 控件宽度=温度条宽度 + 文本区宽度(均为5dp转px,无额外空白)
int defaultWidth = mThermometerWidth + mTextAreaWidth;
int width = measureSize(defaultWidth, widthMeasureSpec);
int height = measureSize(DEFAULT_HEIGHT, heightMeasureSpec);
final int defaultWidth = mThermometerWidth + mTextAreaWidth;
final int width = measureSize(defaultWidth, widthMeasureSpec);
final int height = measureSize(DEFAULT_HEIGHT, heightMeasureSpec);
setMeasuredDimension(width, height);
// 根据文本位置配置,计算温度条矩形坐标
float thermometerLeft, thermometerRight;
if (isTextOnRight) {
// 文本在右侧:温度条靠左,右接文本区
float thermometerLeft;
float thermometerRight;
final boolean textRightFlag = isTextOnRight;
if (textRightFlag) {
thermometerLeft = 0;
thermometerRight = thermometerLeft + mThermometerWidth;
} else {
// 文本在左侧:温度条靠右,左接文本区
thermometerLeft = width - mThermometerWidth;
thermometerRight = width;
}
float thermometerTop = getPaddingTop();
float thermometerBottom = height - getPaddingBottom();
final float thermometerTop = getPaddingTop();
final float thermometerBottom = height - getPaddingBottom();
mThermometerRect.set(thermometerLeft, thermometerTop, thermometerRight, thermometerBottom);
LogUtils.v(TAG, "onMeasure: 文本位置=" + (isTextOnRight ? "右侧" : "左侧") + ",控件尺寸=" + width + "x" + height + ",温度条区域=" + mThermometerRect.toShortString());
LogUtils.v(TAG, "onMeasure textPos=" + (textRightFlag ? "RIGHT" : "LEFT") + ", view size=" + width + "x" + height);
}
@Override
protected void onDraw(Canvas canvas) {
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
// 1. 绘制温度条边框5dp宽度,小圆角适配)
// 绘制温度条边框
canvas.drawRoundRect(mThermometerRect, 3, 3, mThermometerPaint);
// 2. 计算填充高度(根据当前值占最大值的比例
float fillRatio = (float) mCurrentValue / mMaxValue;
float fillHeight = mThermometerRect.height() * fillRatio;
float fillTop = mThermometerRect.bottom - fillHeight;
// 计算渐变填充区域高度比例
final float fillRatio = (float) mCurrentValue / mMaxValue;
final float fillHeight = mThermometerRect.height() * fillRatio;
final float fillTop = mThermometerRect.bottom - fillHeight;
// 3. 绘制渐变填充部分(左右贴边框,无间距)
// 绘制渐变填充色块
if (fillHeight > 0) {
RectF fillRect = new RectF(
mThermometerRect.left + FILL_PADDING_HORIZONTAL,
fillTop + FILL_PADDING_VERTICAL,
mThermometerRect.right - FILL_PADDING_HORIZONTAL,
mThermometerRect.bottom - FILL_PADDING_VERTICAL
final RectF fillRect = new RectF(
mThermometerRect.left + FILL_PADDING_HORIZONTAL,
fillTop + FILL_PADDING_VERTICAL,
mThermometerRect.right - FILL_PADDING_HORIZONTAL,
mThermometerRect.bottom - FILL_PADDING_VERTICAL
);
LinearGradient gradient = new LinearGradient(
fillRect.centerX(), fillRect.bottom,
fillRect.centerX(), fillRect.top,
mGradientColors,
mGradientPositions,
Shader.TileMode.CLAMP
final LinearGradient gradient = new LinearGradient(
fillRect.centerX(), fillRect.bottom,
fillRect.centerX(), fillRect.top,
mGradientColors,
mGradientPositions,
Shader.TileMode.CLAMP
);
mFillPaint.setShader(gradient);
canvas.drawRoundRect(fillRect, 2, 2, mFillPaint);
mFillPaint.setShader(null);
}
// 4. 绘制文本(5dp固定宽度文本区,底部对齐竖排,文字居中)
String text = String.format("%d/%d", mCurrentValue, mMaxValue);
if (text.isEmpty()) return;
// 竖排文本绘制逻辑
final String text = String.format("%d/%d", mCurrentValue, mMaxValue);
if (text.length() == 0) {
return;
}
float textBaseX;
if (isTextOnRight) {
// 文本在右侧:X=温度条右边缘 + 文本区宽度的一半(紧贴温度条)
final boolean textRightFlag = isTextOnRight;
if (textRightFlag) {
textBaseX = mThermometerRect.right + (mTextAreaWidth / 2f);
} else {
// 文本在左侧:X=文本区宽度的一半(紧贴控件左边缘)
textBaseX = mTextAreaWidth / 2f;
}
// 文本绘制参数(适配5dp窄区域,缩小字间距提升紧凑度)
float singleCharHeight = mTextPaint.getTextSize() + (TEXT_CHAR_SPACING - 2f); // 字间距减为6f
float totalTextHeight = (singleCharHeight * text.length()) - (TEXT_CHAR_SPACING - 2f);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float charBottomOffset = fontMetrics.bottom;
final float singleCharHeight = mTextPaint.getTextSize() + (TEXT_CHAR_SPACING - 2f);
final Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
final float charBottomOffset = fontMetrics.bottom;
final float startTextY = getHeight() - getPaddingBottom() - charBottomOffset;
// 文本起始Y(底部对齐控件底部,无间距)
float startTextY = getHeight() - getPaddingBottom() - charBottomOffset;
// 逐字竖排绘制(文字居中于5dp文本区,无超出)
for (int i = 0; i < text.length(); i++) {
char singleChar = text.charAt(i);
float currentTextY = startTextY - (i * singleCharHeight);
final char singleChar = text.charAt(i);
final float currentTextY = startTextY - (i * singleCharHeight);
canvas.drawText(String.valueOf(singleChar), textBaseX, currentTextY, mTextPaint);
}
}
@@ -13,7 +13,9 @@ import android.widget.LinearLayout;
import android.widget.Switch;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.model.SettingsBean;
import cc.winboll.studio.contacts.services.LimitedTimeGreenChannelService;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
@@ -21,7 +23,7 @@ import cc.winboll.studio.libappbase.ToastUtils;
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/04/18 15:05:00 (GMT+8)
* @LastEditTime 2026/04/22 17:30:00 (GMT+8)
* @Describe 限时特殊通道视图
* @Describe 限时绿色通道视图
* 功能说明
* 1. Switch 打开时EditText 不可编辑布局背景变为绿色加减按钮不可用
* 2. Switch 关闭时EditText 可编辑布局背景恢复为白色加减按钮恢复可用
@@ -31,10 +33,10 @@ import cc.winboll.studio.libappbase.ToastUtils;
* 6. 新增快速设置按钮(btn_thisSeconds)点击直接设置为当前量级3600秒
* 7. [新增] 服务销毁时恢复EditText显示为倒计时剩余秒数
*/
public class LimitedTimeSpecialChannelView extends LinearLayout {
public class LimitedTimeGreenChannelView extends LinearLayout {
// ====================== 常量定义 =========================
public static final String TAG = "LimitedTimeSpecialChannelView";
public static final String TAG = "LimitedTimeGreenChannelView";
// 定义秒数增量的量级
private static final long SECONDS_INCREMENT = 3600;
@@ -50,17 +52,17 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
private android.content.BroadcastReceiver mCountdownReceiver;
// ====================== 构造函数 =========================
public LimitedTimeSpecialChannelView(Context context) {
public LimitedTimeGreenChannelView(Context context) {
super(context);
initView(context);
}
public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs) {
public LimitedTimeGreenChannelView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs, int defStyleAttr) {
public LimitedTimeGreenChannelView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
@@ -71,19 +73,22 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
this.mContext = context;
// 加载布局文件
LayoutInflater.from(context).inflate(R.layout.view_limitedtimespecialchannel, this, true);
LayoutInflater.from(context).inflate(R.layout.view_limitedtimegreenchannel, this, true);
LogUtils.i(TAG, "布局文件加载完成");
// 绑定控件ID
bindViews();
// 初始化所有按钮点击事件
initButtons();
// 初始化开关状态
// 初始化开关状态从持久化存储加载
initSwitchState();
// 注册广播监听
registerServiceDestroyedListener();
registerCountdownTickListener();
// 加载保存的绿色通道启用状态并设置Switch
loadSavedGreenChannelState();
LogUtils.i(TAG, "视图初始化流程结束");
}
@@ -191,15 +196,75 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
// 根据开关状态更新UI可操作状态
updateUIState(!isChecked);
// 保存绿色通道启用状态到持久化存储
saveGreenChannelState(isChecked);
// 发送绿色通道状态变更广播
LimitedTimeGreenChannelService.sendGreenChannelStateChangedBroadcast(mContext, isChecked);
if (isChecked) {
startLimitedTimeSpecialChannelService();
startLimitedTimeGreenChannelService();
} else {
LimitedTimeSpecialChannelService.stopService(mContext);
LimitedTimeGreenChannelService.stopService(mContext);
}
}
});
}
/**
* 从持久化存储加载绿色通道启用状态并设置Switch
*/
private void loadSavedGreenChannelState() {
Rules rules = Rules.getInstance(mContext);
if (rules == null) {
LogUtils.e(TAG, "loadSavedGreenChannelState: Rules实例获取失败");
return;
}
SettingsBean settings = rules.getSettingsModel();
if (settings == null) {
LogUtils.e(TAG, "loadSavedGreenChannelState: SettingsBean获取失败");
return;
}
boolean savedState = settings.isEnableGreenChannel();
LogUtils.i(TAG, "loadSavedGreenChannelState: 加载保存的绿色通道状态=" + savedState);
// 设置Switch状态不会触发监听器因为监听器在initSwitchState中已设置
if (mSwEnable != null && mSwEnable.isChecked() != savedState) {
mSwEnable.setChecked(savedState);
// 手动更新UI状态
updateUIState(!savedState);
LogUtils.i(TAG, "loadSavedGreenChannelState: Switch状态已恢复为" + (savedState ? "开启" : "关闭"));
}
// 如果保存的状态是开启启动服务
if (savedState && !LimitedTimeGreenChannelService.isServiceRunning()) {
LogUtils.i(TAG, "loadSavedGreenChannelState: 启动绿色通道服务");
startLimitedTimeGreenChannelService();
}
}
/**
* 保存绿色通道启用状态到持久化存储
* @param isEnabled 是否启用
*/
private void saveGreenChannelState(boolean isEnabled) {
Rules rules = Rules.getInstance(mContext);
if (rules == null) {
LogUtils.e(TAG, "saveGreenChannelState: Rules实例获取失败,保存失败");
return;
}
SettingsBean settings = rules.getSettingsModel();
if (settings == null) {
LogUtils.e(TAG, "saveGreenChannelState: SettingsBean获取失败,保存失败");
return;
}
settings.setIsEnableGreenChannel(isEnabled);
rules.saveDun();
LogUtils.i(TAG, "saveGreenChannelState: 绿色通道启用状态已保存为" + isEnabled);
}
/**
* 统一更新UI可操作状态
* @param enabled 可用状态
@@ -222,11 +287,11 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
*/
private void registerServiceDestroyedListener() {
LogUtils.i(TAG, "注册服务销毁广播监听");
IntentFilter filter = new IntentFilter(LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED);
IntentFilter filter = new IntentFilter(LimitedTimeGreenChannelService.ACTION_SERVICE_DESTROYED);
LocalBroadcastManager.getInstance(mContext).registerReceiver(new android.content.BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED.equals(intent.getAction())) {
if (LimitedTimeGreenChannelService.ACTION_SERVICE_DESTROYED.equals(intent.getAction())) {
LogUtils.i(TAG, "收到服务销毁广播,执行恢复操作");
// 1. 恢复开关状态
@@ -236,7 +301,7 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
// 2. [核心修改] 检索并设置倒计时剩余秒数参数
// 从广播附加数据中获取之前服务发送的剩余秒数
long remainingSeconds = intent.getLongExtra(LimitedTimeSpecialChannelService.EXTRA_REMAINING_SECONDS, 0);
long remainingSeconds = intent.getLongExtra(LimitedTimeGreenChannelService.EXTRA_REMAINING_SECONDS, 0);
// 实际倒计时消耗冲正
remainingSeconds -= 1;
// 倒计时秒数0值保底
@@ -252,6 +317,10 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
LogUtils.i(TAG, "已将EditText设置为剩余秒数: " + remainingSeconds);
}
// 5. 保存绿色通道关闭状态并发送广播
saveGreenChannelState(false);
LimitedTimeGreenChannelService.sendGreenChannelStateChangedBroadcast(mContext, false);
//ToastUtils.show("服务已销毁,恢复倒计时剩余状态");
}
}
@@ -266,8 +335,8 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
mCountdownReceiver = new android.content.BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (LimitedTimeSpecialChannelService.ACTION_COUNTDOWN_TICK.equals(intent.getAction())) {
long remainingSeconds = intent.getLongExtra(LimitedTimeSpecialChannelService.EXTRA_REMAINING_SECONDS, 0);
if (LimitedTimeGreenChannelService.ACTION_COUNTDOWN_TICK.equals(intent.getAction())) {
long remainingSeconds = intent.getLongExtra(LimitedTimeGreenChannelService.EXTRA_REMAINING_SECONDS, 0);
if (mEtSeconds != null) {
mEtSeconds.setText(String.valueOf(remainingSeconds));
}
@@ -278,7 +347,7 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
}
};
IntentFilter filter = new IntentFilter(LimitedTimeSpecialChannelService.ACTION_COUNTDOWN_TICK);
IntentFilter filter = new IntentFilter(LimitedTimeGreenChannelService.ACTION_COUNTDOWN_TICK);
LocalBroadcastManager.getInstance(mContext).registerReceiver(mCountdownReceiver, filter);
LogUtils.i(TAG, "倒计时广播注册成功");
}
@@ -286,8 +355,8 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
/**
* 启动限时特殊通道服务
*/
private void startLimitedTimeSpecialChannelService() {
LogUtils.i(TAG, "准备启动限时特殊通道服务");
private void startLimitedTimeGreenChannelService() {
LogUtils.i(TAG, "准备启动限时绿色通道服务");
String secondsStr = mEtSeconds.getText().toString().trim();
if (secondsStr.isEmpty()) {
LogUtils.w(TAG, "输入框为空,使用默认值 3600 秒");
@@ -299,8 +368,8 @@ public class LimitedTimeSpecialChannelView extends LinearLayout {
long delayMillis = seconds * 1000;
LogUtils.i(TAG, "启动参数 - 输入秒数: " + seconds + ", 转换毫秒: " + delayMillis);
//ToastUtils.show("调用 LimitedTimeSpecialChannelService 服务");
LimitedTimeSpecialChannelService.startService(mContext, delayMillis);
//ToastUtils.show("调用 LimitedTimeGreenChannelService 服务");
LimitedTimeGreenChannelService.startService(mContext, delayMillis);
} catch (NumberFormatException e) {
LogUtils.e(TAG, "输入的秒数格式错误: " + secondsStr, e);
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#ff000000"
android:pathData="M5.8,10C5.4,8.8 4.3,8 3,8C1.3,8 0,9.3 0,11S1.3,14 3,14C4.3,14 5.4,13.2 5.8,12H7V14H9V12H11V10H5.8M3,12C2.4,12 2,11.6 2,11S2.4,10 3,10 4,10.4 4,11 3.6,12 3,12M16,4C13.8,4 12,5.8 12,8S13.8,12 16,12 20,10.2 20,8 18.2,4 16,4M16,10.1C14.8,10.1 13.9,9.2 13.9,8C13.9,6.8 14.8,5.9 16,5.9C17.2,5.9 18.1,6.8 18.1,8S17.2,10.1 16,10.1M16,13C13.3,13 8,14.3 8,17V20H24V17C24,14.3 18.7,13 16,13M22.1,18.1H9.9V17C9.9,16.4 13,14.9 16,14.9C19,14.9 22.1,16.4 22.1,17V18.1Z"/>
</vector>
+14 -12
View File
@@ -1,21 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/activityBackgroundColor">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutviewroot_ll"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>
@@ -5,8 +5,10 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libappbase.LogView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/logview"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/activity_layoutdraft_preview_toolbar"/>
</LinearLayout>
@@ -12,11 +12,6 @@
android:layout_height="wrap_content"
android:id="@+id/activitymainToolbar1"/>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
@@ -17,6 +17,7 @@
android:id="@+id/activitymainToolbar1"/>
<LinearLayout
android:id="@+id/activity_settings_content"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
@@ -57,7 +58,6 @@
android:id="@+id/tv_DunInfo"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
@@ -67,12 +67,12 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="特殊通道设置:"/>
android:text="绿色通道设置:"/>
<cc.winboll.studio.contacts.views.LimitedTimeSpecialChannelView
<cc.winboll.studio.contacts.views.LimitedTimeGreenChannelView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/view_LimitedTimeSpecialChannel"/>
android:id="@+id/view_LimitedTimeGreenChannel"/>
</LinearLayout>
@@ -325,29 +325,18 @@
android:background="@drawable/recycler_view_border"
android:layout_margin="5dp"/>
<TextView
android:layout_width="wrap_content"
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="单元测试:"/>
android:text="初始化测试规则"
android:onClick="onInitTestRules"
android:layout_margin="5dp"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LogView"
android:onClick="onLogView"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unit Test"
android:onClick="onUnitTest"/>
</LinearLayout>
<TextView
@@ -369,11 +358,6 @@
</LinearLayout>
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
<cc.winboll.studio.contacts.views.DunQueryView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"/>
android:layout_height="wrap_content"/>
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:padding="12dp"
android:background="@drawable/recycler_view_border">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="调试工具"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/red_normal"
android:layout_marginBottom="8dp"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:id="@+id/btn_debug_green_channel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="绿色通道"
android:textSize="12sp"
android:layout_margin="2dp"/>
<Button
android:id="@+id/btn_debug_logview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="应用日志"
android:textSize="12sp"
android:layout_margin="2dp"/>
<Button
android:id="@+id/btn_debug_unit_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="单元测试"
android:textSize="12sp"
android:layout_margin="2dp"/>
<Button
android:id="@+id/btn_debug_layout_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="布局草稿"
android:textSize="12sp"
android:layout_margin="2dp"/>
<Button
android:id="@+id/btn_debug_crash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试崩溃"
android:textSize="12sp"
android:layout_margin="2dp"/>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:padding="12dp"
android:background="@drawable/recycler_view_border">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="云盾拦截查询"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/red_normal"
android:layout_marginBottom="8dp"/>
<EditText
android:id="@+id/et_dun_query_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入电话号码查询拦截状态"
android:inputType="phone"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tv_dun_query_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请输入电话号码"
android:textSize="14sp"
android:textColor="@color/gray_500"/>
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_green_channel_floating"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white"
android:elevation="4dp">
<cc.winboll.studio.contacts.views.LimitedTimeGreenChannelView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/view_LimitedTimeGreenChannel"/>
</LinearLayout>
+64
View File
@@ -10,4 +10,68 @@
<color name="white">#FFFFFFFF</color>
<color name="lightgray">#FFE0E0E0</color>
<!-- 基础黑白(必含,适配文字/背景) -->
<color name="black">#000000</color>
<!-- 基础色系(按钮/强调色常用) -->
<color name="blue_light">#4A90E2</color>
<color name="blue_normal">#2196F3</color>
<color name="blue_dark">#1976D2</color>
<color name="green_light">#66BB6A</color>
<color name="green_normal">#4CAF50</color>
<color name="green_dark">#388E3C</color>
<color name="red_light">#EF5350</color>
<color name="red_normal">#F44336</color>
<color name="red_dark">#D32F2F</color>
<color name="yellow_light">#FFF59D</color>
<color name="yellow_normal">#FFC107</color>
<color name="yellow_dark">#FFA000</color>
<color name="orange_normal">#FF9800</color>
<color name="purple_normal">#9C27B0</color>
<!-- 透明色(遮罩/背景叠加) -->
<color name="transparent">#00000000</color>
<color name="black_transparent_50">#80000000</color>
<!-- 不透明灰色(常用深浅梯度) -->
<color name="gray_100">#F5F5F5</color>
<color name="gray_200">#EEEEEE</color>
<color name="gray_300">#E0E0E0</color>
<color name="gray_400">#BDBDBD</color>
<color name="gray_500">#9E9E9E</color>
<color name="gray_600">#757575</color>
<color name="gray_700">#616161</color>
<color name="gray_800">#424242</color>
<color name="gray_900">#212121</color>
<!-- 半透明灰色 -->
<color name="gray_transparent_30">#4D9E9E9E</color>
<color name="gray_transparent_50">#809E9E9E</color>
<color name="gray_transparent_70">#B39E9E9E</color>
<color name="gray_light">#EEE</color>
<color name="gray_mid">#999</color>
<color name="gray_dark">#666</color>
<color name="gray_black">#333</color>
<!-- 遮罩/蒙层 -->
<color name="mask_gray">#809E9E9E</color>
<color name="bg_overlay_gray">#4D424242</color>
<!-- 按钮灰色 -->
<color name="btn_gray_normal">#9E9E9E</color>
<color name="btn_gray_pressed">#757575</color>
<color name="btn_gray_disabled">#E0E0E0</color>
<!-- 主题颜色 (适配 libappbase) -->
<color name="colorText">#FF000000</color>
<color name="colorTextBackgound">#FFF5F5F5</color>
<color name="mainWindowBackgroundColor">#FFF5F5F5</color>
<color name="mainWindowTextColor">#FF000000</color>
<color name="mainWindowDarkBackgroundColor">#FFF5F5F5</color>
<color name="mainWindowDarkTextColor">#FF000000</color>
<color name="toolbarTextColor">#FFFFFFFF</color>
<color name="toolbarBackgroundColor">#FF196ABC</color>
<color name="debugTextColor">#FF808080</color>
</resources>
+2 -1
View File
@@ -2,6 +2,7 @@
<resources>
<string name="app_name">Contacts</string>
<string name="default_bobulltoon_url">https://gitea.winboll.cc/Studio/BoBullToon/archive/main.zip</string>
<string name="app_description">This is a mobile dialer app that can block nuisance calls by matching regular expressions.</string>
<string name="default_bobulltoon_url">https://gitea.winboll.cc/ZhanGSKen/BoBullToon/archive/main.zip</string>
</resources>
+34 -30
View File
@@ -7,42 +7,46 @@
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowContentOverlay">@null</item>
<!-- libappbase自定义属性 -->
<item name="themeDebug">@style/DebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowDarkTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
<item name="debugTextColor">@color/debugTextColor</item>
</style>
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<style name="DebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="colorTittle">?attr/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
<item name="debugTextColor">@color/debugTextColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
<style name="GlobalCrashActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorTittle">@color/colorAccent</item>
<item name="colorTittleBackgound">@color/colorPrimary</item>
<item name="colorText">@color/colorAccent</item>
<item name="colorTextBackgound">@color/colorPrimaryDark</item>
</style>
<!-- 方案2:带 ActionBar 主题(如需系统默认标题栏,启用此方案) -->
<!--
<style name="MyAppTheme" parent="Theme.MaterialComponents.Light">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:textSizeHeadline">18sp</item>
<item name="android:textSizeBody">16sp</item>
<item name="android:textSizeSubtitle">14sp</item>
<item name="android:textSizeCaption">12sp</item>
</style>
<style name="GlobalCrashActivityTheme" parent="Theme.MaterialComponents.Light">
<item name="colorTittle">@color/colorAccent</item>
<item name="colorTittleBackgound">@color/colorPrimary</item>
<item name="colorText">@color/colorAccent</item>
<item name="colorTextBackgound">@color/colorPrimaryDark</item>
<item name="android:textSizeHeadline">20sp</item>
<item name="android:textSizeBody">14sp</item>
<item name="android:textSizeButton">16sp</item>
</style>
-->
</resources>
@@ -6,4 +6,7 @@
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.8.0.12</domain>
</domain-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.8.0.4</domain>
</domain-config>
</network-security-config>
-1
View File
@@ -1 +0,0 @@
-120
View File
@@ -1,120 +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.debugtemp"
minSdkVersion 23
// 适配MIUI12
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.0"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
}
}
}
dependencies {
api project(':libdebugtemp')
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'])
}
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Fri May 01 17:09:11 HKT 2026
stageCount=57
libraryProject=libdebugtemp
baseVersion=15.0
publishVersion=15.0.56
buildCount=0
baseBetaVersion=15.0.57
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DebugTemp☆</string>
</resources>
@@ -1,340 +0,0 @@
package cc.winboll.studio.debugtemp;
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);
//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();
}
}
}
@@ -1,38 +0,0 @@
package cc.winboll.studio.debugtemp;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.LibraryActivity;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libappbase.ToastUtils;
public class MainActivity extends AppCompatActivity {
LogView mLogView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mLogView = findViewById(R.id.logview);
ToastUtils.show("onCreate");
}
@Override
protected void onResume() {
super.onResume();
mLogView.start();
}
public void onLibraryActivity(View view) {
startActivity(new Intent(this, LibraryActivity.class));
}
}
@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

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