Compare commits

...

73 Commits

Author SHA1 Message Date
7015725
7b7fbdf9cf fix: handle system gestures for ToolHub panels 2026-05-12 10:24:30 +08:00
7015725
5c95d04fab feat: add system navigation button actions 2026-05-12 10:16:31 +08:00
7015725
0037aefcb7 fix: improve ToolHub startup and cleanup stability 2026-05-12 10:00:32 +08:00
7015725
523d6e2044 docs: update README for signed ToolHub updates 2026-05-09 22:57:53 +08:00
7015725
7ce5ebeda8 polish: simplify ToolHub startup output 2026-05-07 23:52:33 +08:00
7015725
726ed4465a security: harden ToolHub update trust model 2026-05-07 23:46:46 +08:00
7015725
fdd896aca5 security: verify signed manifest before module updates 2026-05-07 23:25:41 +08:00
7015725
8f6dcdcac1 fix: keep docked ball on edge while dragging 2026-05-07 23:06:23 +08:00
7015725
637858ab9c fix: avoid docked drag first-frame flash 2026-05-07 23:02:50 +08:00
7015725
0251419dbc fix: make docked float ball drag follow finger 2026-05-07 22:58:36 +08:00
7015725
e0b774ccd9 fix: adapt float ball position across rotations 2026-05-07 22:42:06 +08:00
linshenjianlu
ed9521cdf5 fix: smooth docked float ball drag transition 2026-05-03 14:29:17 +08:00
linshenjianlu
105d1c68c5 fix: optimize docked float ball drag 2026-05-03 14:23:37 +08:00
linshenjianlu
caf0e0b598 fix(panels): 适配输入法弹出时图标选择器高度 2026-05-03 14:02:30 +08:00
linshenjianlu
22cd229da9 fix(panels): 输入法弹出时保持图标选择器位置 2026-05-03 13:57:17 +08:00
linshenjianlu
5321cbe6c7 fix(entry): 绕过 Gitea raw 缓存确保子模块热更新 2026-05-03 13:43:45 +08:00
linshenjianlu
0e7c7d6887 fix(panels): 修复图标选择搜索框输入法无法弹出 2026-05-03 13:38:48 +08:00
linshenjianlu
d93419eccc docs: 更新 README — 移除图标分类标签、自适应布局、2026-04-21 更新记录 2026-04-21 11:24:18 +08:00
linshenjianlu
b2c7cae355 feat(icon picker): 移除 ShortX 图标选择器的分类标签,简化 UI 布局
- 删除新版弹出框的 tabDefs / tabsScroll / tabsRow / refreshTabs
- 简化 filterCatalog,仅保留搜索过滤
- 删除旧版 shortxPickerState 的 activeTab / tabButtons 及关联逻辑
- 删除 getShortXIconCategory / shortxTabDefs / applyShortXTabStyles
- 调整 headerH 从 136dp 降至 110dp,腾出更多图标展示空间
- 修复删除 tabs 代码时遗留的重复 addView(searchEt) 导致 IllegalStateException
2026-04-21 11:21:30 +08:00
linshenjianlu
7d879527f4 fix(entry): sha256File 返回值转为 JS 原生字符串,修复 Rhino 中 Java String !== 比较引用导致的 SHA256 误判 2026-04-21 09:59:36 +08:00
linshenjianlu
e89eb9a904 fix(entry): 关键模块失败时暴露真实错误,__out 增加核心依赖前置检查 2026-04-21 09:57:17 +08:00
linshenjianlu
83444ba683 fix: 回滚 th_01_base.js 到原始版本排查 getProcessInfo 未定义问题 2026-04-21 08:32:17 +08:00
linshenjianlu
ed3ee6eaf6 fix(ToolHub): 关键模块加载失败时不再直接 throw,而是通过 __out 返回详细错误信息 2026-04-21 08:24:38 +08:00
linshenjianlu
fee6e0bc40 fix(ToolHub): 增强 loadScript 错误日志输出,方便排查入口报错原因 2026-04-21 08:11:40 +08:00
linshenjianlu
c32f29fdb7 fix: 回滚 th_16_entry.js 修改以排查入口报错 2026-04-21 08:08:40 +08:00
linshenjianlu
9ad01b436d fix: 代码审查6项修复
- 689处空catch块补全日志
- eval远程代码增加SHA256校验
- 删除ToolHubLogger重复定义
- getParentFile()增加null保护
- 提取buildButtonEditorPanelView内通用工具函数到文件级
- 修复HandlerThread/ValueAnimator资源泄漏
2026-04-21 07:42:23 +08:00
Hermes
7e23cd95a1 docs(readme): 更新 2026-04-20 更新记录,补充 bug 修复与代码清理详情 2026-04-20 23:15:43 +08:00
Hermes
a394684f42 refactor(ball): 清理悬浮球文字相关死代码 2026-04-20 22:40:39 +08:00
Hermes
068e4cdd3b feat(ball-settings): 统一图标弹窗并精简球显示 2026-04-20 22:32:31 +08:00
Hermes
256a8f716b fix(th_14_panels): 修复颜色弹窗确认时 recentGrid 引用异常 2026-04-20 21:46:14 +08:00
Hermes
9a0b618409 fix(th_14_panels): 强制应用弹窗选中的图标颜色 2026-04-20 21:42:30 +08:00
Hermes
f89ed88033 fix(th_14_panels): 改为独立维护 ShortX 图标颜色状态 2026-04-20 21:36:15 +08:00
Hermes
3c8c337401 fix(th_14_panels): 修复 ShortX 图标调色板确认后未应用颜色 2026-04-20 21:30:03 +08:00
Hermes
4ab42b274a fix(color-picker): 修复颜色参数回写失败问题
根因: Rhino JS 闭包中局部变量捕获异常,onSelect 回调无法访问 inputShortXIconTint

修复:
- 将输入框引用存储到 self.state._btnEditorTintInput,绕过闭包问题
- onSelect 回调从 self.state 读取输入框,同时直接更新 targetBtn.iconTint 作为双保险
- 保存按钮优先使用 targetBtn.iconTint,回退到输入框 getValue()
2026-04-20 21:00:13 +08:00
Hermes
d03f4d677f fix(color-picker): 修复颜色面板确认后无回写问题
- 确认按钮添加 try-catch,确保 onSelect 异常不阻断 closePopup
- finalColor 强制 String() 转换,避免 undefined 传入 Java setText
- onSelect 回调添加异常隔离 + invalidate() 强制刷新
- 同步两个调用点(颜色按钮 + 调色板选择按钮)
2026-04-20 20:51:28 +08:00
Hermes
86a4ed7927 feat: 弹出式颜色选择器 + 图标预览旁颜色入口
- showColorPickerPopup: 新增独立弹窗颜色选择器,支持最近使用颜色(8个)、RGB滑块实时调色、常用色网格、透明度滑块
- 图标预览区实时反馏,确定后自动保存到最近列表
- 在 ShortX 图标预览卡片旁新增“颜色”按钮,直接打开颜色选择器
- 更新 README 文档
- 修复调色板卡片未加入 form 的问题
2026-04-20 20:32:06 +08:00
Hermes
99547894b6 fix(th_14_panels): 修复ShortX图标选择器onSelect ReferenceError
删除未定义的 inputShortXIconName 引用,解除 ReferenceError 导致的回调中断,
使 updateShortXIconPreview() 能正常触发预览框刷新
2026-04-20 19:58:08 +08:00
Hermes
a39ecaf648 fix(th_14_panels): 修复ShortX图标选择器确认后预览框未刷新问题
- 将previewNameTv.setText提前到inputShortXIconTint.getValue()之前
- 为tintHex读取添加独立try-catch,避免异常阻断预览文本更新
- 为updateShortXIconPreview添加错误日志输出
2026-04-20 19:51:35 +08:00
Hermes
e5d540d1a5 fix(icon): 确定按钮绕过 guardClick 冷却,避免被其他按钮点击拦截
- createSolidButton/createFlatButton 内部使用 guardClick('ui_btn') 共享同一个 key
- 如果用户在短时间内点击了多个按钮(分类/翻页/关闭),确定按钮的点击可能被忽略
- 确定按钮现在直接创建 TextView + OnClickListener,不经 guardClick
- 添加 try-catch 捕获 onSelect 回调异常
2026-04-20 19:33:51 +08:00
Hermes
c67c0ccaf2 refactor(icon): 重写图标选择器为自适应布局 + WindowManager 直接创建
- 不再使用 showPopupOverlay,避免嵌套弹窗导致尺寸限制
- 弹窗宽度 = 屏幕宽度*0.92,自适应屏幕
- 列数、单元格宽度、每\u9875\u56fe\u6807\u6570\u6839\u636e\u5f39\u7a97\u5c3a\u5bf8\u81ea\u9002\u5e94\u8ba1\u7b97
- 网\u683c\u9ad8\u5ea6 = \u5c4f\u5e55\u9ad8\u5ea6*0.55,\u4fdd\u8bc1\u53ef\u89c1\u533a\u57df
2026-04-20 19:21:26 +08:00
Hermes
6e7a2fef7a fix(icon): 稳定翻页图标选择器,去掉 grid.post 动态测量依赖
- 固定 4 列 20 个/页,避免首次渲染时尺寸为 0 导致空白
- 去掉 resolvePageSize 动态计算
- 翻页按钮状态正确更新
2026-04-20 19:10:40 +08:00
Hermes
e07bd7e16f fix(icon): 添加资源 ID 遍历反射策略,绕过 R$drawable 混淆问题获取 ShortX 图标库
策略优先级:
1. 反射 R$drawable 类(未混淆时)
2. 直接遍历资源 ID 2131230000~2131240000(绕过混淆)
3. APK 文件扫描(最后兜底)
2026-04-20 19:08:01 +08:00
Hermes
f1b8f0a921 fix(icon): createRipple → createRippleDrawable 修复图标网格渲染崩溃
日志显示:Cannot find function createRipple in object [object Object]
原因:ui 对象上只有 createRippleDrawable,没有 createRipple。
修复:使用 self.ui.createRippleDrawable(card, pressed, radius) 替代。
2026-04-20 18:13:03 +08:00
Hermes
7c350724f6 修复图标选择器网格空白:移除 grid.post 依赖,直接使用默认尺寸渲染
问题:弹窗能打开但图标列表不显示,网格内容为空。
原因:grid.post Runnable 在后台线程中可能未执行或执行延迟。
解决:直接传入默认尺寸 (300dp x 360dp) 计算 pageSize,
然后直接调用 renderGrid(),不再等待 grid 布局完成。
2026-04-20 18:08:23 +08:00
Hermes
f13f3268c3 fix(icon): 添\u52a0 getShortXIconDrawable 方法和占位符,\u4fee\u590d\u56fe\u6807\u4e0d\u663e\u793a
- 补\u5145\u7f3a\u5931的 getShortXIconDrawable() 方法
- 图\u6807\u83b7\u53d6\u5931\u8d25\u65f6\u663e\u793a\u6d45\u8272\u5360\u4f4d\u7b26
2026-04-20 17:59:33 +08:00
Hermes
5036f48772 fix(icon): 增强诊\u65ad和\u5907\u7528\u56fe\u6807\u5217\u8868,\u4fee\u590d ShortX \u56fe\u6807\u4e0d\u663e\u793a
- APK 扫描增\u52a0 mipmap* 目\u5f55\u652f\u6301
- 增\u52a0诊\u65ad信\u606f\u663e\u793a(APK 路\u5f84、\u6587\u4ef6\u6570、\u9519\u8bef\u539f\u56e0)
- 当\u6240\u6709\u81ea\u52a8\u83b7\u53d6\u5931\u8d25\u65f6,\u56de\u9000\u5230 20 \u4e2a\u5e38\u7528\u5907\u7528\u56fe\u6807\u5217\u8868
2026-04-20 17:56:40 +08:00
Hermes
ec64ef07d9 fix(icon): 宽松 ShortX 图标库匹配规则,修复图标不显示问题
- 将 APK 扫描正则从 ic_remix_ 放宽到所有 ic_ 开头的图标
- 反射获取 R 时接受所有 ic_ 开头的字段,不再限\u5b9a ic_remix_
- 弹\u7a97\u9009\u62e9\u5668\u589e\u52a0\u9519\u8bef\u63d0\u793a\u548c\u5f3a\u5236\u91cd\u8bd5\u673a\u5236
2026-04-20 17:48:14 +08:00
Hermes
f2714d5101 feat: 调色板改为弹出式 UI,支持图标颜色预览
- 按钮编辑器中调色板改为弹出窗口,不再嵌入表单
- 添加 showColorPickerPopup(),支持图标实时预览着色效果
- 包含 21 色快捷网格、RGB 滑块、透明度滑块
- 选中后自动填回表单
2026-04-20 17:40:04 +08:00
Hermes
273c299ea0 feat: ShortX 图标选择器改为弹出式 UI
- 按钮编辑器中 ShortX 图标选择改为弹出窗口,不再嵌入表单
- 添加 showPopupOverlay() 通用弹窗框架,支持搜索/分类/分页/点选
- 点击选择图标按钮直接打开弹窗,选中后自动填回表单
2026-04-20 17:35:04 +08:00
Hermes
c68f0d1209 fix(ui): 修复 GridLayout.LayoutParams 构造函数参数错误导致设置面板打不开
- GridLayout.LayoutParams 不支持 (width, height) 双参数构造
- 改用无参构造 + 分别设置 width/height 属性
2026-04-20 16:34:19 +08:00
Hermes
b5e7695d1a fix(ui): 将设置面板的图标和颜色选择器改为内嵌式,修复 AlertDialog 在后台无法弹出问题
- ball_shortx_icon: 使用内嵌 ListView + 搜索框替代 AlertDialog
- ball_color: 使用内嵌调色板网格 + 输入框替代 AlertDialog
- 添加展开/收起按钮,与按钮编辑器的交互模式保持一致
- 移除所有 AlertDialog.Builder 调用(在 ShortX 后台服务环境无法显示)
2026-04-20 16:24:20 +08:00
Hermes
f2c80112c1 feat(settings): 设置面板改造,悬浮球图标支持 ShortX 选择器与调色板
- 从设置页面移除“执行与查看器”、“悬浮球文字/大小/颜色”设置项
- BALL_ICON_RES_NAME 改为 ball_shortx_icon 类型,支持弹窗列表+搜索选择 ShortX 图标
- BALL_ICON_TINT_HEX 改为 ball_color 类型,支持调色板选色+手动输入
- 移除无用的 BALL_ICON_PKG 通用文本输入(仅保留 file 路径输入作为兜底)
- 同步更新 README Changelog
2026-04-20 16:10:17 +08:00
Hermes
2bbe9c3609 docs: 添加更新记录(Changelog)
- 按天归档,同一天多次提交合并为一条
- 记录2026-04-20全部更新:自适应布局、分页图标选择器、默认收起、返回信息中文化
2026-04-20 15:26:03 +08:00
Hermes
26e1ebefef docs: 更新 README 图标选择器交互说明
- 取消“切到 ShortX 图标时自动展开图标库”的描述
- 增加“默认收起,点击展开按钮后手动打开”的说明
- 补充选中图标后自动回填并收起的交互要点
2026-04-20 15:19:22 +08:00
Hermes
b61c27ee85 fix(panels): 图标库和调色板默认收起
- 调色板展开状态不再从 saved state 恢复,默认始终收起
- 调色板 body 初始可见性设为 GONE,避免创建时显示
- 移除切换到 ShortX 图标模式时自动展开图标库的逻辑
- 保留展开/收起按钮,用户手动点击才展开
2026-04-20 14:55:08 +08:00
Hermes
ffbbcf647f feat(panels): 常用颜色改为自适应布局,图标选择器分页优化
- 常用颜色网格从固定 4x5 改为根据屏幕宽度自动计算列数
  (与 ShortX 图标列表一致的 adaptive layout)
- 图标浮窗选择器改为翻页模式,按当前可见容量动态分页
- 修复图标选择器关闭后无法再次打开的问题
- 入口返回信息增加中文 msg/syncMsg 及 updatedModules 字段
- 同步更新 README 文档
2026-04-20 14:48:01 +08:00
Hermes Bot
fbb8b6e682 refactor: 常用颜色按色相自动排序 2026-04-20 14:37:37 +08:00
Hermes Bot
4a9aff570c feat: 图标调色板增加 RGB 调色器 2026-04-20 14:26:38 +08:00
Hermes Bot
8d02291a27 feat: 升级图标调色板并移除名称输入 2026-04-20 14:16:09 +08:00
Hermes Bot
4a91b9631f feat: 按钮图标颜色增加快捷色板 2026-04-20 13:55:36 +08:00
Hermes Bot
313688275b refactor: 图标选择器按宽度自动排列 2026-04-20 13:45:37 +08:00
Hermes Bot
6bf2be1bf1 fix: 同步入口 ToolHub.js 与本地版本 2026-04-20 13:38:03 +08:00
Hermes Bot
96275e8028 feat: 优化图标分页选择器与中文返回信息 2026-04-20 13:34:28 +08:00
Hermes Bot
2022c2d1c1 feat: 图标选择器翻页+入口中文摘要
- ToolHub.js: __out 返回添加中文摘要字段(状态/按钮数量/面板布局/日志/崩溃处理/线程模型等)
- th_07_shortcut.js: showIconPicker 改为翻页模式,动态计算每页图标数
- th_07_shortcut.js: 修复关闭后再点击打不开的问题(api.show 不提前设置 state.hidden)
2026-04-20 13:19:31 +08:00
Hermes Bot
034ed61e22 feat: ShortX 图标库选择器 + 入口更新信息返回
- th_07_shortcut.js: 新增 showIconPicker() 多宫格自适应图标选择器
- th_14_panels.js: 按钮编辑器内联图标库改为浮窗选择器
- ToolHub.js: 返回 JSON 新增 updates/errors 字段
2026-04-20 13:07:30 +08:00
root
9ef8bdae40 refactor: replace version-based update with HEAD Last-Modified check
- Remove MODULE_MANIFEST and getFileVersion()
- Add getRemoteLastModified() via HTTP HEAD
- Add getLocalLastModified() / saveLocalLastModified() caching to .lm_* files
- loadScript now checks remote Last-Modified against local cache on every run
- File missing -> download; File exists + remote newer -> download; otherwise skip
- Network failure gracefully falls back to local file
- Entry file no longer needs version bumps when submodules update
2026-04-20 12:41:13 +08:00
root
9b07a888a5 chore: bump version for shell refactor (1.0.0 -> 1.0.1)
- th_01_base.js: 1.0.1 (Base64 decode comment cleanup)
- th_10_shell.js: 1.0.1 (remove Action-first path, keep only broadcast bridge)
- th_11_action.js: 1.0.1 (toast msg update)
- ToolHub.js: MODULE_MANIFEST synced
2026-04-20 12:32:55 +08:00
root
40a60a6912 refactor: remove Action-first shell path, keep only broadcast bridge
- th_10_shell.js: clean up outdated comments, title now 'Shell: broadcast execution'
- th_11_action.js: update toast msg from 'Action + bridge both failed' to 'broadcast bridge send failed'
- th_01_base.js: remove 'Action-first path' reference in Base64 decode comment
- th_16_entry.js already has useActionFirst: false / hasShellCommand: false
2026-04-20 12:29:04 +08:00
root
e375f7d7ba docs: update README for 16-module architecture
- Sync directory tree with new th_01 ~ th_16 file listing
- Update module descriptions and line counts
- Update load order and manifest count (5 -> 16)
- Update deployment notes
2026-04-20 12:11:11 +08:00
root
c7e9b92322 refactor: split th_2_core.js into 12 modules, rename all files to 2-digit numbering
- Split th_2_core.js (4715 lines, 177KB) into:
  th_02_core.js, th_03_icon.js, th_04_theme.js, th_05_persistence.js,
  th_06_icon_parser.js, th_07_shortcut.js, th_08_content.js,
  th_09_animation.js, th_10_shell.js, th_11_action.js,
  th_12_rebuild.js, th_13_panel_ui.js
- Rename existing: th_1_base→th_01_base, th_3_panels→th_14_panels,
  th_4_extra→th_15_extra, th_5_entry→th_16_entry
- Update ToolHub.js MODULE_MANIFEST, modules array, and critical module check
2026-04-20 11:53:13 +08:00
root
c64d4c336b chore: 更新 git 仓库链接 chenziran -> linshenjianlu 2026-04-20 11:03:36 +08:00
root
6b112d011a feat: 更新入口文件与模块 — 自动下载、版本校验、权限管理、日志落盘 2026-04-20 10:40:12 +08:00
root
72702d557b docs: 更新 README — 增加自动下载、版本管理、权限控制、日志系统等说明 2026-04-20 10:29:48 +08:00
23 changed files with 9018 additions and 5552 deletions

333
README.md
View File

@@ -1,92 +1,311 @@
# ShortX ToolHub # ShortX ToolHub
一个模块化的 ShortX JS 浮窗工具框架,支持广播关闭、子线程模型、日志记录和可扩展面板 ShortX ToolHub 是一个面向 **ShortX / Rhino ES5 JS** 的模块化悬浮工具框架
入口文件 `ToolHub.js` 负责安全拉取、验签、校验并加载子模块;业务 UI 和功能拆分在 `code/th_*.js` 中维护,避免单个 JS 文件过大。
当前仓库地址:
```text
https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub
```
---
## 核心特性
- **模块化加载**:入口文件只做启动、同步、校验与汇总返回;功能代码拆到 16 个子模块。
- **签名更新机制**:远端 `manifest.json` 必须通过 `manifest.sig` 的 RSA 签名校验后才可信。
- **SHA256 文件校验**:每个子模块按清单中的 `sha256``size` 校验,通过后才覆盖本地文件。
- **防回滚**:入口内置 `MIN_TRUSTED_MANIFEST_VERSION`,并记录本地已信任清单版本,拒绝旧版本清单。
- **本地可信回退**:网络或远端清单异常时,不盲目覆盖;已验证过的本地模块可继续使用。
- **ShortX 图标选择器**:支持图标点选、搜索、分页、自适应列数,不再依赖手填图标名。
- **颜色选择器**支持常用色、最近色、RGB、透明度和实时预览。
- **日志记录**:启动、更新、验签、加载异常写入 `ToolHub/logs/init.log`
---
## 快速部署
### 1. 安装入口文件
复制仓库中的 `ToolHub.js`,粘贴到 ShortX 的 JS 任务中运行。
> 注意:`ToolHub.js` 是信任根,内置 RSA 公钥和最低可信清单版本。入口文件本身不能安全地自我更新;入口升级时需要手动替换一次。
### 2. 首次运行
入口会自动完成:
1. 创建 `shortx.getShortXDir()/ToolHub/code/`
2. 下载 `manifest.json``manifest.sig`
3. 使用入口内置公钥验签清单
4. 校验清单版本,防止回滚
5. 按清单下载 16 个子模块到临时文件
6. 校验 `size``sha256`
7. 校验通过后覆盖本地模块
8. 加载模块并启动悬浮球
--- ---
## 目录结构 ## 目录结构
### 实机目录
```text
shortx.getShortXDir()/
└── ToolHub/
├── code/
│ ├── th_01_base.js
│ ├── th_02_core.js
│ ├── th_03_icon.js
│ ├── th_04_theme.js
│ ├── th_05_persistence.js
│ ├── th_06_icon_parser.js
│ ├── th_07_shortcut.js
│ ├── th_08_content.js
│ ├── th_09_animation.js
│ ├── th_10_shell.js
│ ├── th_11_action.js
│ ├── th_12_rebuild.js
│ ├── th_13_panel_ui.js
│ ├── th_14_panels.js
│ ├── th_15_extra.js
│ └── th_16_entry.js
└── logs/
└── init.log
``` ```
ToolHub.js # 入口文件(粘贴到 ShortX 任务)
code/ ### 仓库目录
├── th_1_base.js # 基础工具函数、Logger、崩溃处理、进程信息
├── th_2_core.js # 核心逻辑、浮窗管理、Shell 桥接、ContentProvider ```text
├── th_3_panels.js # 面板配置、按钮定义、对话框、文本查看器 ShortX_ToolHub/
├── th_4_extra.js # 额外面板(设备信息、快捷操作等) ├── ToolHub.js
── th_5_entry.js # 入口面板定义、广播接收器注册 ── ToolHub.js.sha256
├── README.md
├── manifest.json
├── manifest.sig
├── code/
│ └── th_*.js
└── scripts/
└── generate_signed_manifest.py
``` ```
--- ---
## 部署步骤 ## 启动返回示例
### 1. 创建目录 ### 正常启动,无模块更新
在 ShortX 数据根目录下创建:
```
ShortX数据根目录/
└── ToolHub/
└── code/
```
> ShortX 数据根目录路径通过 `shortx.getShortXDir()` 获取,通常为 `/data/system/shortx_XXXXXXXX/`
### 2. 放置文件
-`ToolHub.js` 的内容粘贴到 ShortX 任务中
-`code/` 目录下的 5 个 `th_*.js` 文件放入 `ToolHub/code/`
### 3. 运行
执行 ShortX 任务,正常返回示例:
```json ```json
{ {
"ok": true, "ok": true,
"started": true, "状态": "ToolHub 启动成功",
"msg": "已按 WM 专属 HandlerThread 模型启动", "安全": "✓ 已验签 v20260507155220 / toolhub-targets-2026-rsa3072",
"closeAction": "shortx.wm.floatball.CLOSE", "同步": "✓ 子模块已是最新",
"layout": {"cols": 2, "rows": 2} "布局": "4×4",
"关闭广播": "shortx.wm.floatball.CLOSE"
} }
``` ```
### 正常启动,有模块更新
```json
{
"ok": true,
"状态": "ToolHub 启动成功",
"安全": "✓ 已验签 v20260507155220 / toolhub-targets-2026-rsa3072",
"同步": "✓ 已更新 2 个模块th_14_panels.js、th_16_entry.js",
"布局": "4×4",
"关闭广播": "shortx.wm.floatball.CLOSE",
"更新模块": ["th_14_panels.js", "th_16_entry.js"]
}
```
### 启动失败
失败时会返回:
- `ok: false`
- `状态: "ToolHub 启动失败"`
- `安全`: 当前验签/清单状态
- `错误`: 失败原因
- `加载异常`: 非关键模块加载失败列表(如存在)
--- ---
## 关闭浮窗 ## 安全更新机制
通过 adb 或 ShortX Shell 执行 当前更新链路不是旧版 `Last-Modified` 热更新,而是
```bash 1. `ToolHub.js` 内置可信 RSA 公钥。
am broadcast -a shortx.wm.floatball.CLOSE 2. 远端仓库分发:
- `manifest.json`
- `manifest.sig`
- `code/*.js`
3. 入口下载清单和签名。
4. 使用 `SHA256withRSA` 校验 `manifest.sig`
5. 检查 `manifest.keyId` 是否在入口信任列表内。
6. 检查 `manifest.version` 是否低于最低可信版本或本地已信任版本。
7. 每个模块下载到 `.tmp`
8. 校验模块 `size``sha256`
9. 校验通过才覆盖本地模块。
10. 所有模块加载正常后,保存本地可信清单版本。
当前 keyId
```text
toolhub-targets-2026-rsa3072
``` ```
--- ---
## 日志位置 ## 模块职责
```
ShortX数据根目录/ToolHub/logs/
```
日志文件按天分割,默认保留 3 天。
---
## 模块说明
| 文件 | 职责 | | 文件 | 职责 |
|------|------| |------|------|
| `th_1_base.js` | 工具函数、Logger、崩溃处理、进程信息获取 | | `th_01_base.js` | 基础工具、日志、配置校验、通用辅助 |
| `th_2_core.js` | 浮窗管理器、Shell 执行器、ContentProvider 读取器 | | `th_02_core.js` | `FloatBallAppWM` 构造、基础状态与核心工具 |
| `th_3_panels.js` | 面板配置工厂、按钮构建器、对话框、文本查看器 | | `th_03_icon.js` | 图标缓存、Bitmap 管理、悬浮球图标加载 |
| `th_4_extra.js` | 额外面板:设备信息、网络状态、快捷操作 | | `th_04_theme.js` | 主题、颜色、样式工具 |
| `th_5_entry.js` | 入口面板定义、广播接收器注册、启动流程 | | `th_05_persistence.js` | 持久化与设置数据层 |
| `th_06_icon_parser.js` | 图标解析、ShortX 内置图标扫描与回退 |
| `th_07_shortcut.js` | 快捷方式选择器 |
| `th_08_content.js` | ContentProvider 读取与通用 query |
| `th_09_animation.js` | 面板动画、吸边、显示/隐藏管理 |
| `th_10_shell.js` | Shell 执行层 |
| `th_11_action.js` | 按钮动作分发与执行 |
| `th_12_rebuild.js` | 悬浮球重建逻辑 |
| `th_13_panel_ui.js` | 设置面板通用 UI 组件 |
| `th_14_panels.js` | 设置面板、按钮编辑器、图标选择器、颜色选择器 |
| `th_15_extra.js` | 主面板与附加展示层 |
| `th_16_entry.js` | 生命周期、广播注册、启动与销毁 |
--- ---
## 注意事项 ## 图标与颜色交互
- 入口文件通过 `loadScript()` 动态加载子模块,`var` 声明通过间接 `eval` 挂到全局作用域 ### ShortX 图标选择器
- 子模块加载顺序不可更改base → core → panels → extra → entry
- 调试请查看日志文件,不通过返回 JSON 暴露内部细节 - 图标库默认收起,点击后展开。
- 支持搜索、上一页、下一页。
- 按当前可用宽度自动计算列数。
- 按可见高度计算每页容量,减少空白和滚动浪费。
- 选中图标后自动回填并收起。
- 优先运行时扫描 ShortX 可用图标,避免维护超大静态图标表。
### 弹出式颜色选择器
- 图标预览实时显示着色效果。
- 最近使用颜色最多保留 8 个。
- 常用颜色使用自适应网格。
- RGB 与透明度滑块实时同步。
- 支持一键清空,恢复跟随主题。
---
## 日志
启动日志路径:
```text
shortx.getShortXDir() + "/ToolHub/logs/init.log"
```
常见记录内容:
- 目录创建与权限处理
- manifest 下载与验签结果
- 模块下载、哈希校验与覆盖更新
- 模块加载失败
- 启动异常
- 模块体积告警(超过 200KB
---
## 维护与发布
### 修改子模块后
如果修改了 `code/*.js``ToolHub.js`,需要重新生成签名清单:
```bash
scripts/generate_signed_manifest.py
```
会更新:
```text
manifest.json
manifest.sig
ToolHub.js.sha256
```
然后再提交并推送。
### 修改 README
只改 `README.md` 不需要重新签名,因为 README 不参与手机端模块校验。
### 注意事项
- Rhino / ShortX JS 中统一使用 `var`,避免 `let` / `const`
- 顶层不要使用 `return`
- 入口文件是信任根,改动后需要用户手动替换 ShortX 任务中的入口。
- 不要把私钥提交到仓库。
- 不建议把调试细节塞进启动返回 JSON优先写日志。
---
## 更新记录
### 2026-05-09
**文档更新**
- README 改为匹配当前签名更新机制。
- 移除旧版 `Last-Modified` 热更新描述。
- 补充 manifest 验签、SHA256 校验、防回滚、本地可信回退说明。
- 更新启动返回示例为当前中文精简字段。
### 2026-05-07
**安全升级**
- 引入 `manifest.json` + `manifest.sig` 签名清单。
- 入口内置 RSA 公钥,使用 `SHA256withRSA` 验签。
- 子模块下载后按 `size``sha256` 校验。
- 增加最低可信清单版本与本地可信版本,防止回滚。
- 启动返回信息精简为 `状态 / 安全 / 同步 / 布局 / 关闭广播`
### 2026-04-21
**功能改进**
- 移除 ShortX 图标选择器分类标签,保留搜索和分页。
- 调整图标弹窗自适应布局参数,提高单页容量。
- 清理旧版分类过滤逻辑。
**Bug 修复**
- 修复删除 tabs 代码后遗留重复 `addView(searchEt)` 导致的面板显示崩溃。
### 2026-04-20
**Bug 修复**
- 修复 ShortX 图标调色板确认后未应用颜色。
- 修复颜色弹窗确认时 `recentGrid` 引用异常。
- 图标颜色与悬浮球颜色改为独立维护,避免互相覆盖。
**功能改进**
- 图标库与调色板默认收起。
- 常用颜色改为自适应布局。
- ShortX 图标浮窗选择器改为分页模式。
- 设置面板移除悬浮球文字相关设置项。
- 新增独立颜色选择器支持最近色、常用色、RGB、透明度和实时预览。
**代码清理**
- 按钮编辑与悬浮球设置复用图标选择器和颜色选择器。
- 清理悬浮球文字相关死代码。

View File

@@ -1,66 +1,427 @@
// ToolHub - 入口文件 (加载子模块并执行) // ToolHub - 入口文件 (加载子模块并执行)
// 将本文件放入 ShortX 任务th_*.js 放入 ShortX 数据根目录/ToolHub/code/ 文件夹 // 安全更新机制:入口内置 RSA 公钥,先验证 manifest.json/manifest.sig再按 SHA256 下载子模块。
// Gitea 只负责分发;未通过签名/哈希/防回滚校验时,不覆盖本地模块。
function loadScript(relPath) { var GIT_ROOT = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/";
var GIT_BASE = GIT_ROOT + "code/";
var TRUSTED_PUBLIC_KEYS = {
"toolhub-targets-2026-rsa3072": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApiyhtMDJce7dVCxH1/oDu8kbiECYoT5XXmXvR/XNYuJ/5FuL83SbpCQ3QmUnqkbfNyOFqnxac/qlbXJtx6eeSotLP1HmrKI0LGymgxG6b1FfGHBfIKNZfBLIvzVDQob+HJfshlsS1JRlW5Jhm25TMh8dJCQQQZWW/ZItbtOvPYbLwG8cnqEdX8gqyB304+r2l35GPTfxZIGEK/9PcE3AMuqwTolMJsBHtG61hmMdz3dzTTEZQoOcciGWuwr2ZW8XkF6f5SgWkC29ZxZqAxceK4FJ8BsYirpFQxVKyZ6eiYlpNiYz+pHLP2U7JTO6ImmT1rlYSS6xw2tlWf0xq72nuOPC+VzEivuEhnC4y9WBSvauRa/ViIDgQ3yXl2MajuAvGSVWRfZ5Gz5Up8PQD7vxmHT2r0fA4xq4GIvUvGCqOG/d1FRrlVyEuNhCZ7KgpEKPno7fLnC6/ftnYcN5ZNOSWwjWH/e4fBxM5s6RRIYzIY2N0f/fqsRH42lWAhX5stujAgMBAAE="
};
var DEFAULT_TRUSTED_KEY_ID = "toolhub-targets-2026-rsa3072";
var MIN_TRUSTED_MANIFEST_VERSION = 20260507152251;
var __dirChecked = false;
var __trustedManifest = null;
var __securityStatus = { ok: false, msg: "安全清单尚未校验" };
function buildNoCacheUrl(urlStr) {
var sep = String(urlStr).indexOf("?") >= 0 ? "&" : "?";
return String(urlStr) + sep + "_toolhub_ts=" + java.lang.System.currentTimeMillis();
}
function getLogPath() { return shortx.getShortXDir() + "/ToolHub/logs/init.log"; }
function getCodeDirPath() { return shortx.getShortXDir() + "/ToolHub/code/"; }
function getTrustedShaPath(relPath) { return getCodeDirPath() + ".trusted_sha_" + relPath; }
function getTrustedVersionPath() { return getCodeDirPath() + ".trusted_manifest_version"; }
function writeLog(msg) {
try { try {
var base = shortx.getShortXDir(); var f = new java.io.File(getLogPath());
var f = new java.io.File(base + "/ToolHub/code/" + relPath); var dir = f.getParentFile();
if (!f.exists()) { if (dir && !dir.exists()) dir.mkdirs();
throw "Not found: " + f.getAbsolutePath(); var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
var ts = sdf.format(new java.util.Date());
var writer = new java.io.FileWriter(f, true);
writer.write("[" + ts + "] " + String(msg) + "\n");
writer.close();
} catch (e) {}
}
function runShell(cmdArr) {
try {
var proc = java.lang.Runtime.getRuntime().exec(cmdArr);
proc.waitFor();
return proc.exitValue() === 0;
} catch (e) { return false; }
}
function checkDirPerms(path) {
try {
var proc = java.lang.Runtime.getRuntime().exec(["stat", "-c", "%u %g %a", path]);
proc.waitFor();
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(proc.getInputStream()));
var line = reader.readLine();
reader.close();
if (line) {
var parts = String(line).trim().split(/\s+/);
if (parts.length >= 3) return String(parts[0]) === "1000" && String(parts[1]) === "1000" && String(parts[2]) === "700";
} }
var r = new java.io.BufferedReader(new java.io.InputStreamReader( } catch (e) {}
new java.io.FileInputStream(f), "UTF-8")); return false;
}
function setDirPerms(path) {
runShell(["chmod", "700", path]);
runShell(["chown", "1000:1000", path]);
}
function ensureCodeDir() {
var dir = new java.io.File(getCodeDirPath());
if (!__dirChecked) {
if (!dir.exists()) {
dir.mkdirs();
setDirPerms(dir.getAbsolutePath());
writeLog("Created dir: " + dir.getAbsolutePath());
} else if (!checkDirPerms(dir.getAbsolutePath())) {
setDirPerms(dir.getAbsolutePath());
writeLog("Fixed dir perms: " + dir.getAbsolutePath());
}
__dirChecked = true;
}
if (!dir.canWrite()) throw "Dir not writable: " + dir.getAbsolutePath();
return dir;
}
function readTextFile(path) {
try {
var f = new java.io.File(path);
if (!f.exists()) return null;
var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"));
var sb = new java.lang.StringBuilder(); var sb = new java.lang.StringBuilder();
var line; var line;
while ((line = r.readLine()) != null) { while ((line = r.readLine()) != null) sb.append(line).append("\n");
sb.append(line).append("\n");
}
r.close(); r.close();
var geval = eval; return String(sb.toString());
geval(String(sb.toString())); } catch (e) { return null; }
} catch(e) { }
throw "loadScript(" + relPath + ") failed: " + e;
function writeTextFile(path, text) {
try {
var f = new java.io.File(path);
var parent = f.getParentFile();
if (parent && !parent.exists()) parent.mkdirs();
var w = new java.io.OutputStreamWriter(new java.io.FileOutputStream(f, false), "UTF-8");
w.write(String(text));
w.close();
return true;
} catch (e) { return false; }
}
function readFirstLine(path) {
var txt = readTextFile(path);
if (!txt) return null;
var parts = String(txt).split(/\r?\n/);
return parts.length > 0 ? String(parts[0]).trim() : null;
}
function sha256File(fileOrPath) {
try {
var path = String(fileOrPath);
var md = java.security.MessageDigest.getInstance("SHA-256");
var fis = new java.io.FileInputStream(path);
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
var n;
while ((n = fis.read(buf)) !== -1) md.update(buf, 0, n);
fis.close();
var digest = md.digest();
var sb = new java.lang.StringBuilder();
for (var i = 0; i < digest.length; i++) {
var hex = java.lang.Integer.toHexString(0xFF & digest[i]);
if (hex.length() === 1) sb.append("0");
sb.append(hex);
}
return String(sb.toString());
} catch (e) { return null; }
}
function saveTrustedSha(relPath, hash) { writeTextFile(getTrustedShaPath(relPath), String(hash || "")); }
function getTrustedSha(relPath) { return readFirstLine(getTrustedShaPath(relPath)); }
function getTrustedVersion() {
var line = readFirstLine(getTrustedVersionPath());
var v = line ? parseInt(String(line), 10) : 0;
return isNaN(v) ? 0 : v;
}
function saveTrustedVersion(v) { writeTextFile(getTrustedVersionPath(), String(v || 0)); }
function downloadText(urlStr) {
var url = new java.net.URL(buildNoCacheUrl(urlStr));
var conn = url.openConnection();
conn.setUseCaches(false);
conn.setConnectTimeout(10000);
conn.setReadTimeout(30000);
conn.setRequestProperty("User-Agent", "ShortX-ToolHub/secure-updater");
conn.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate");
conn.setRequestProperty("Pragma", "no-cache");
var code = conn.getResponseCode();
if (code !== 200) throw "HTTP " + code;
var r = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream(), "UTF-8"));
var sb = new java.lang.StringBuilder();
var line;
while ((line = r.readLine()) != null) sb.append(line).append("\n");
r.close();
var text = String(sb.toString());
var prefix = text.length > 200 ? text.substring(0, 200) : text;
if (prefix.indexOf("<!DOCTYPE") >= 0 || prefix.indexOf("<html") >= 0) throw "Downloaded content is HTML";
return text;
}
function downloadFile(urlStr, destFile) {
var url = new java.net.URL(buildNoCacheUrl(urlStr));
var conn = url.openConnection();
conn.setUseCaches(false);
conn.setConnectTimeout(10000);
conn.setReadTimeout(30000);
conn.setRequestProperty("User-Agent", "ShortX-ToolHub/secure-updater");
conn.setRequestProperty("Cache-Control", "no-cache, no-store, must-revalidate");
conn.setRequestProperty("Pragma", "no-cache");
var code = conn.getResponseCode();
if (code !== 200) throw "HTTP " + code;
var expectedLen = conn.getContentLength();
var inStream = conn.getInputStream();
var outStream = new java.io.FileOutputStream(destFile);
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
var n, total = 0;
while ((n = inStream.read(buf)) !== -1) { outStream.write(buf, 0, n); total += n; }
outStream.close(); inStream.close();
if (expectedLen > 0 && total !== expectedLen) throw "Size mismatch: expected=" + expectedLen + ", got=" + total;
var checkStream = new java.io.FileInputStream(destFile);
var checkBuf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 200);
var checkRead = checkStream.read(checkBuf);
checkStream.close();
if (checkRead > 0) {
var prefix = new java.lang.String(checkBuf, 0, checkRead, "UTF-8");
if (prefix.indexOf("<!DOCTYPE") >= 0 || prefix.indexOf("<html") >= 0) throw "Downloaded content is HTML, not JS";
}
return total;
}
function base64Decode(s) {
return android.util.Base64.decode(String(s).replace(/\s+/g, ""), android.util.Base64.DEFAULT);
}
function getTrustedPublicKeyB64(keyId) {
var kid = keyId ? String(keyId) : DEFAULT_TRUSTED_KEY_ID;
if (TRUSTED_PUBLIC_KEYS.hasOwnProperty(kid)) return TRUSTED_PUBLIC_KEYS[kid];
return null;
}
function verifyManifestSignature(manifestText, sigText, keyId) {
try {
var pubB64 = getTrustedPublicKeyB64(keyId);
if (!pubB64) throw "unknown manifest keyId: " + String(keyId);
var pubBytes = base64Decode(pubB64);
var sigBytes = base64Decode(sigText);
var spec = new java.security.spec.X509EncodedKeySpec(pubBytes);
var pubKey = java.security.KeyFactory.getInstance("RSA").generatePublic(spec);
var verifier = java.security.Signature.getInstance("SHA256withRSA");
verifier.initVerify(pubKey);
verifier.update(new java.lang.String(String(manifestText)).getBytes("UTF-8"));
return verifier.verify(sigBytes);
} catch (e) {
writeLog("Manifest signature verify exception: " + String(e));
return false;
} }
} }
loadScript("th_1_base.js"); function fetchTrustedManifest() {
loadScript("th_2_core.js"); try {
loadScript("th_3_panels.js"); ensureCodeDir();
loadScript("th_4_extra.js"); var manifestText = downloadText(GIT_ROOT + "manifest.json");
loadScript("th_5_entry.js"); var sigText = downloadText(GIT_ROOT + "manifest.sig");
var manifest = JSON.parse(String(manifestText));
if (!manifest || !manifest.files) throw "manifest files missing";
if (String(manifest.alg || "") !== "SHA256withRSA") throw "unsupported manifest alg: " + String(manifest.alg);
var keyId = String(manifest.keyId || DEFAULT_TRUSTED_KEY_ID);
if (!getTrustedPublicKeyB64(keyId)) throw "untrusted manifest keyId: " + keyId;
if (!verifyManifestSignature(manifestText, sigText, keyId)) throw "manifest signature invalid";
var version = parseInt(String(manifest.version || "0"), 10);
if (isNaN(version) || version <= 0) throw "invalid manifest version";
if (version < MIN_TRUSTED_MANIFEST_VERSION) throw "manifest below minimum trusted version: remote=" + version + ", min=" + MIN_TRUSTED_MANIFEST_VERSION;
var localVersion = getTrustedVersion();
if (localVersion > 0 && version < localVersion) throw "manifest rollback: remote=" + version + ", local=" + localVersion;
__trustedManifest = manifest;
__securityStatus = { ok: true, msg: "安全清单验签通过version=" + version + ", keyId=" + keyId, version: version, keyId: keyId };
writeLog(__securityStatus.msg);
return manifest;
} catch (e) {
__trustedManifest = null;
__securityStatus = { ok: false, msg: "安全清单校验失败,已停止远程拉取:" + String(e) };
writeLog(__securityStatus.msg);
return null;
}
}
function replaceFile(tmpFile, destFile) {
try {
if (destFile.exists() && !destFile.delete()) throw "delete old file failed: " + destFile.getAbsolutePath();
if (!tmpFile.renameTo(destFile)) throw "rename tmp failed: " + tmpFile.getAbsolutePath();
return true;
} catch (e) { throw String(e); }
}
function getManifestInfo(relPath) {
if (!__trustedManifest || !__trustedManifest.files) return null;
return __trustedManifest.files[relPath] || null;
}
function ensureVerifiedModule(relPath, destFile) {
var info = getManifestInfo(relPath);
if (!info || !info.sha256) throw "module not in trusted manifest: " + relPath;
var expectedHash = String(info.sha256).toLowerCase();
var expectedSize = Number(info.size || 0);
var actualHash = destFile.exists() ? sha256File(destFile.getAbsolutePath()) : null;
if (destFile.exists() && actualHash && String(actualHash).toLowerCase() === expectedHash) {
saveTrustedSha(relPath, expectedHash);
return { updated: false, size: destFile.length(), hash: actualHash };
}
var tmpFile = new java.io.File(destFile.getAbsolutePath() + ".tmp");
try { if (tmpFile.exists()) tmpFile.delete(); } catch (eDelTmp0) {}
var size = downloadFile(GIT_BASE + relPath, tmpFile);
if (expectedSize > 0 && size !== expectedSize) {
try { tmpFile.delete(); } catch (eDelSize) {}
throw "manifest size mismatch for " + relPath + ": expected=" + expectedSize + ", got=" + size;
}
var tmpHash = sha256File(tmpFile.getAbsolutePath());
if (!tmpHash || String(tmpHash).toLowerCase() !== expectedHash) {
try { tmpFile.delete(); } catch (eDelHash) {}
throw "manifest SHA256 mismatch for " + relPath + ": expected=" + expectedHash + ", actual=" + tmpHash;
}
var wasNew = !destFile.exists();
replaceFile(tmpFile, destFile);
saveTrustedSha(relPath, expectedHash);
return { updated: true, isNew: wasNew, size: size, hash: tmpHash };
}
function ensureLocalTrustedModule(relPath, destFile) {
if (!destFile.exists()) throw "安全清单不可用且本地模块不存在: " + relPath;
var trustedHash = getTrustedSha(relPath);
var actualHash = sha256File(destFile.getAbsolutePath());
if (!trustedHash || !actualHash || String(trustedHash).toLowerCase() !== String(actualHash).toLowerCase()) {
throw "安全清单不可用,本地模块也无可信 SHA256: " + relPath;
}
return { updated: false, size: destFile.length(), hash: actualHash };
}
function loadScript(relPath) {
try {
var dir = ensureCodeDir();
var f = new java.io.File(dir, relPath);
var result;
if (__trustedManifest) result = ensureVerifiedModule(relPath, f);
else result = ensureLocalTrustedModule(relPath, f);
if (result.updated) {
__moduleUpdates.push({ module: relPath, isNew: !!result.isNew, size: result.size });
writeLog("Verified update " + relPath + " (" + result.size + " bytes, sha256=" + result.hash + ")");
}
var fileSize = f.length();
if (fileSize > 200 * 1024) writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting");
var code = readTextFile(f.getAbsolutePath());
if (code === null) throw "read failed: " + f.getAbsolutePath();
var geval = eval;
geval(String(code));
} catch(e) {
var errMsg = "loadScript(" + relPath + ") failed: " + e;
try { android.util.Log.e("ToolHub", errMsg); } catch(eLog) {}
throw errMsg;
}
}
var modules = ["th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js", "th_05_persistence.js",
"th_06_icon_parser.js", "th_07_shortcut.js", "th_08_content.js", "th_09_animation.js",
"th_10_shell.js", "th_11_action.js", "th_12_rebuild.js", "th_13_panel_ui.js",
"th_14_panels.js", "th_15_extra.js", "th_16_entry.js"];
var __moduleUpdates = [];
var loadErrors = [];
var criticalModules = { "th_01_base.js": true, "th_16_entry.js": true };
fetchTrustedManifest();
for (var i = 0; i < modules.length; i++) {
try {
loadScript(modules[i]);
} catch (e) {
var modErr = "Module load failed: " + modules[i] + " -> " + String(e);
writeLog(modErr);
try { android.util.Log.e("ToolHub", modErr); } catch(eLog) {}
loadErrors.push({ module: modules[i], err: String(e) });
if (criticalModules[modules[i]]) throw "Critical module failed: " + modules[i] + " (" + String(e) + ")";
}
}
if (__trustedManifest && loadErrors.length === 0) saveTrustedVersion(__trustedManifest.version);
var __out = (function() { var __out = (function() {
if (typeof getProcessInfo !== "function") {
return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心函数 getProcessInfo 未定义,请检查 th_01_base.js 是否加载成功" };
}
if (typeof ToolHubLogger !== "function") {
return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心类 ToolHubLogger 未定义,请检查 th_01_base.js 是否加载成功" };
}
if (typeof FloatBallAppWM !== "function") {
return { ok: false, started: false, msg: "ToolHub 启动失败", securityMsg: __securityStatus.msg, err: "核心类 FloatBallAppWM 未定义,请检查 th_02_core.js / th_16_entry.js 是否加载成功" };
}
function optStr(v) { return (v === undefined || v === null) ? "" : String(v); }
function summarizeModuleUpdates(list) {
var names = [];
var created = 0;
var overwritten = 0;
var i;
for (i = 0; i < list.length; i++) {
var item = list[i] || {};
var name = optStr(item.module);
if (name) names.push(name);
if (item.isNew) created++; else overwritten++;
}
if (names.length === 0) return { count: 0, modules: [], msg: "子模块已是最新,本次未覆盖更新。" };
return { count: names.length, modules: names, msg: "本次已通过签名校验并覆盖更新 " + names.length + " 个子模块(新增 " + created + " / 覆盖 " + overwritten + "" + names.join("、") };
}
function summarizeLoadErrors(list) {
var names = [];
var i;
for (i = 0; i < list.length; i++) {
var item = list[i] || {};
var name = optStr(item.module);
if (name) names.push(name);
}
return { count: names.length, modules: names, msg: names.length ? ("有 " + names.length + " 个子模块加载失败:" + names.join("、")) : "所有子模块加载正常。" };
}
var entryInfo = getProcessInfo("entry"); var entryInfo = getProcessInfo("entry");
var logger = new ToolHubLogger(entryInfo); var logger = new ToolHubLogger(entryInfo);
installCrashHandler(logger); installCrashHandler(logger);
var app = new FloatBallAppWM(logger); var app = new FloatBallAppWM(logger);
var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE"); var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE");
var startRet = null; var startRet = null;
try { startRet = app.startAsync(entryInfo, closeRule); }
try { catch (eTop) {
startRet = app.startAsync(entryInfo, closeRule); try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch(eLog) {}
} catch (eTop) {
try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch (eLog) {}
startRet = { ok: false, err: String(eTop) }; startRet = { ok: false, err: String(eTop) };
} }
var syncInfo = summarizeModuleUpdates(__moduleUpdates);
function optStr(v) { var loadInfo = summarizeLoadErrors(loadErrors);
return (v === undefined || v === null) ? "" : String(v); var started = !!(startRet && startRet.ok);
} var layoutObj = startRet && startRet.layout || null;
var layoutText = layoutObj ? (String(layoutObj.cols || "?") + "×" + String(layoutObj.rows || "?")) : "未知";
var securityText = __securityStatus.ok
? ("✓ 已验签 v" + String(__securityStatus.version || 0) + " / " + optStr(__securityStatus.keyId))
: ("✗ " + optStr(__securityStatus.msg));
var syncText = syncInfo.count > 0
? ("✓ 已更新 " + syncInfo.count + " 个模块:" + syncInfo.modules.join("、"))
: "✓ 子模块已是最新";
var out = { var out = {
ok: true, ok: started,
started: startRet && startRet.ok, 状态: started ? "ToolHub 启动成功" : "ToolHub 启动失败",
msg: optStr(startRet && startRet.msg), 安全: securityText,
closeAction: optStr(startRet && startRet.closeAction), 同步: syncText,
layout: startRet && startRet.layout || null 布局: layoutText,
关闭广播: optStr(startRet && startRet.closeAction)
}; };
if (syncInfo.count > 0) out.更新模块 = syncInfo.modules;
if (!out.started) { if (loadInfo.count > 0) out.加载异常 = loadInfo.modules;
out.err = optStr(startRet && startRet.err); if (!started) out.错误 = optStr(startRet && startRet.err) || (loadInfo.modules && loadInfo.modules.join(", ")) || "未知错误";
}
return out; return out;
})(); })();
JSON.stringify(__out); JSON.stringify(__out, null, 2);

1
ToolHub.js.sha256 Normal file
View File

@@ -0,0 +1 @@
55dbcacaaa31b031e9a0fcef1253c2e0403fca423ad969e0e1387815e69de3e7 ToolHub.js

View File

@@ -1,3 +1,4 @@
// @version 1.0.1
// ToolHub - Android 悬浮球工具 (ShortX / Rhino ES5) // ToolHub - Android 悬浮球工具 (ShortX / Rhino ES5)
// 来源: 阿然 (xin-blog.com) // 来源: 阿然 (xin-blog.com)
// //
@@ -63,8 +64,14 @@ var ConfigValidator = {
schemas: { schemas: {
// 悬浮球核心配置 // 悬浮球核心配置
BALL_SIZE_DP: { type: "int", min: 20, max: 200, default: 45 }, BALL_SIZE_DP: { type: "int", min: 20, max: 200, default: 45 },
BALL_INIT_X: { type: "int", min: 0, max: 2000, default: 0 }, BALL_INIT_X: { type: "int", min: 0, max: 10000, default: 0 },
BALL_INIT_Y_DP: { type: "int", min: 0, max: 1000, default: 220 }, BALL_INIT_Y_DP: { type: "int", min: 0, max: 2000, default: 220 },
BALL_POS_SCREEN_W: { type: "int", min: 0, max: 10000, default: 0 },
BALL_POS_SCREEN_H: { type: "int", min: 0, max: 10000, default: 0 },
BALL_POS_X_RATIO: { type: "float", min: 0, max: 1, default: 0 },
BALL_POS_Y_RATIO: { type: "float", min: 0, max: 1, default: 0 },
BALL_POS_DOCKED: { type: "bool", default: false },
BALL_POS_DOCK_SIDE: { type: "enum", values: ["", "left", "right"], default: "" },
// 面板布局配置 // 面板布局配置
PANEL_COLS: { type: "int", min: 1, max: 6, default: 1 }, PANEL_COLS: { type: "int", min: 1, max: 6, default: 1 },
@@ -85,6 +92,7 @@ var ConfigValidator = {
BALL_ICON_RES_ID: { type: "int", min: 0, max: 999999, default: 0 }, BALL_ICON_RES_ID: { type: "int", min: 0, max: 999999, default: 0 },
BALL_ICON_SIZE_DP: { type: "int", min: 16, max: 64, default: 22 }, BALL_ICON_SIZE_DP: { type: "int", min: 16, max: 64, default: 22 },
BALL_PNG_MODE: { type: "int", min: 0, max: 2, default: 1 }, BALL_PNG_MODE: { type: "int", min: 0, max: 2, default: 1 },
BALL_IDLE_ALPHA: { type: "float", min: 0.1, max: 1.0, default: 0.6 },
// 交互配置 // 交互配置
LONG_PRESS_MS: { type: "int", min: 200, max: 2000, default: 600 }, LONG_PRESS_MS: { type: "int", min: 200, max: 2000, default: 600 },
@@ -113,7 +121,6 @@ var ConfigValidator = {
// 内容查看器配置 // 内容查看器配置
CONTENT_MAX_ROWS: { type: "int", min: 5, max: 100, default: 20 }, CONTENT_MAX_ROWS: { type: "int", min: 5, max: 100, default: 20 },
CONTENT_VIEWER_TEXT_SP: { type: "int", min: 8, max: 24, default: 12 },
// ========== 以下配置在 Schema 中但原 ConfigValidator 中缺失 ========== // ========== 以下配置在 Schema 中但原 ConfigValidator 中缺失 ==========
// 图标文件配置 // 图标文件配置
@@ -123,12 +130,6 @@ var ConfigValidator = {
BALL_ICON_RES_NAME: { type: "string", default: "" }, BALL_ICON_RES_NAME: { type: "string", default: "" },
BALL_ICON_TINT_HEX: { type: "string", default: "" }, BALL_ICON_TINT_HEX: { type: "string", default: "" },
// 悬浮球外观
BALL_IDLE_ALPHA: { type: "float", min: 0.1, max: 1.0, default: 0.6 },
BALL_TEXT: { type: "string", default: "" },
BALL_TEXT_COLOR_HEX: { type: "string", default: "" },
BALL_TEXT_SIZE_SP: { type: "int", min: 6, max: 20, default: 10 },
// 回弹动画配置 // 回弹动画配置
BOUNCE_DECAY: { type: "float", min: 0.3, max: 0.95, default: 0.72 }, BOUNCE_DECAY: { type: "float", min: 0.3, max: 0.95, default: 0.72 },
BOUNCE_MAX_SCALE: { type: "float", min: 0.6, max: 0.99, default: 0.88 }, BOUNCE_MAX_SCALE: { type: "float", min: 0.6, max: 0.99, default: 0.88 },
@@ -334,7 +335,6 @@ var CONST_BALL_ICON_RES_ID = 0;
var CONST_BALL_ICON_FILE_MAX_BYTES = 524288; var CONST_BALL_ICON_FILE_MAX_BYTES = 524288;
var CONST_BALL_ICON_FILE_MAX_PX = 512; var CONST_BALL_ICON_FILE_MAX_PX = 512;
var CONST_BALL_PNG_MODE = 1; var CONST_BALL_PNG_MODE = 1;
var CONST_BALL_ICON_TEXT_GAP_DP = 1;
var CONST_BALL_INIT_X = 0; var CONST_BALL_INIT_X = 0;
var CONST_BALL_INIT_Y_DP = 220; var CONST_BALL_INIT_Y_DP = 220;
var CONST_BALL_FALLBACK_LIGHT = "#FF005BC0"; var CONST_BALL_FALLBACK_LIGHT = "#FF005BC0";
@@ -740,9 +740,6 @@ var ConfigManager = {
BALL_ICON_RES_NAME: "", BALL_ICON_RES_NAME: "",
BALL_ICON_SIZE_DP: 22, BALL_ICON_SIZE_DP: 22,
BALL_ICON_TINT_HEX: "", BALL_ICON_TINT_HEX: "",
BALL_TEXT: "",
BALL_TEXT_SIZE_SP: 10,
BALL_TEXT_COLOR_HEX: "",
BALL_IDLE_ALPHA: 0.6, BALL_IDLE_ALPHA: 0.6,
PANEL_POS_GRAVITY: "bottom", PANEL_POS_GRAVITY: "bottom",
PANEL_CUSTOM_OFFSET_Y: 0, PANEL_CUSTOM_OFFSET_Y: 0,
@@ -765,8 +762,7 @@ var ConfigManager = {
BALL_PANEL_GAP_DP: 10, BALL_PANEL_GAP_DP: 10,
LOG_ENABLE: true, LOG_ENABLE: true,
LOG_DEBUG: true, LOG_DEBUG: true,
LOG_KEEP_DAYS: 3, LOG_KEEP_DAYS: 3
CONTENT_VIEWER_TEXT_SP: 12
}, },
defaultButtons: [ defaultButtons: [
// # 默认按钮已迁移至 buttons.json // # 默认按钮已迁移至 buttons.json
@@ -784,20 +780,15 @@ CONTENT_VIEWER_TEXT_SP: 12
{ type: "section", name: "悬浮球" }, { type: "section", name: "悬浮球" },
{ key: "BALL_SIZE_DP", name: "悬浮球大小(dp)", type: "int", min: 28, max: 120, step: 1 }, { key: "BALL_SIZE_DP", name: "悬浮球大小(dp)", type: "int", min: 28, max: 120, step: 1 },
{ key: "BALL_PANEL_GAP_DP", name: "球与面板间距(dp)", type: "int", min: 0, max: 60, step: 1 }, { key: "BALL_PANEL_GAP_DP", name: "球与面板间距(dp)", type: "int", min: 0, max: 60, step: 1 },
{ key: "BALL_ICON_TYPE", name: "图标类型(app/file/android/shortx)", type: "single_choice", options: [ { key: "BALL_ICON_TYPE", name: "图标类型", type: "single_choice", options: [
{ label: "应用图标 (app)", value: "app" }, { label: "应用图标 (app)", value: "app" },
{ label: "文件图标 (file)", value: "file" }, { label: "文件图标 (file)", value: "file" },
{ label: "系统图标 (android)", value: "android" },
{ label: "ShortX内置 (shortx)", value: "shortx" } { label: "ShortX内置 (shortx)", value: "shortx" }
]}, ]},
{ key: "BALL_ICON_PKG", name: "图标包名(app模式)", type: "text" },
{ key: "BALL_ICON_FILE_PATH", name: "图标路径(file模式)", type: "text" }, { key: "BALL_ICON_FILE_PATH", name: "图标路径(file模式)", type: "text" },
{ key: "BALL_ICON_RES_NAME", name: "ShortX图标名(file/shortx模式兜底)", type: "text" }, { key: "BALL_ICON_RES_NAME", name: "ShortX图标", type: "ball_shortx_icon" },
{ key: "BALL_ICON_TINT_HEX", name: "图标着色(#RRGGBB, 空不着色)", type: "text" }, { key: "BALL_ICON_TINT_HEX", name: "图标颜色", type: "ball_color" },
{ key: "BALL_IDLE_ALPHA", name: "闲置不透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 }, { key: "BALL_IDLE_ALPHA", name: "闲置不透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 },
{ key: "BALL_TEXT", name: "悬浮球文字", type: "text" },
{ key: "BALL_TEXT_SIZE_SP", name: "文字大小(sp)", type: "int", min: 6, max: 20, step: 1 },
{ key: "BALL_TEXT_COLOR_HEX", name: "文字颜色(#RRGGBB)", type: "text" },
{ type: "section", name: "面板布局" }, { type: "section", name: "面板布局" },
{ key: "PANEL_ROWS", name: "面板可视行数(超出滚动)", type: "int", min: 1, max: 12, step: 1 }, { key: "PANEL_ROWS", name: "面板可视行数(超出滚动)", type: "int", min: 1, max: 12, step: 1 },
@@ -842,9 +833,6 @@ CONTENT_VIEWER_TEXT_SP: 12
{ key: "LONG_PRESS_HAPTIC_ENABLE", name: "长按震动反馈", type: "bool" }, { key: "LONG_PRESS_HAPTIC_ENABLE", name: "长按震动反馈", type: "bool" },
{ key: "LONG_PRESS_VIBRATE_MS", name: "震动时长(ms)", type: "int", min: 1, max: 120, step: 1 }, { key: "LONG_PRESS_VIBRATE_MS", name: "震动时长(ms)", type: "int", min: 1, max: 120, step: 1 },
{ type: "section", name: "执行与查看器" },
{ key: "CONTENT_VIEWER_TEXT_SP", name: "查看器文字大小(sp)", type: "int", min: 9, max: 18, step: 1 },
{ type: "section", name: "日志" }, { type: "section", name: "日志" },
{ key: "LOG_ENABLE", name: "写文件日志", type: "bool" }, { key: "LOG_ENABLE", name: "写文件日志", type: "bool" },
{ key: "LOG_DEBUG", name: "详细日志DEBUG", type: "bool" }, { key: "LOG_DEBUG", name: "详细日志DEBUG", type: "bool" },
@@ -876,7 +864,7 @@ CONTENT_VIEWER_TEXT_SP: 12
var needReset = false; var needReset = false;
if (s) { if (s) {
var sStr = JSON.stringify(s); var sStr = JSON.stringify(s);
if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0) { if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0 || sStr.indexOf("ball_shortx_icon") < 0 || sStr.indexOf("ball_color") < 0) {
needReset = true; needReset = true;
} }
} else { } else {
@@ -1096,7 +1084,7 @@ function getProcessInfo(tag) {
// =======================【工具Base64 解码UTF-8】======================= // =======================【工具Base64 解码UTF-8】=======================
// # 这段代码的主要内容/用途:把 cmd_b64 还原成原始 shell 文本(仅用于 Action 优先路径;广播桥永远传 b64 // # 这段代码的主要内容/用途:把 cmd_b64 还原成原始 shell 文本
function decodeBase64Utf8(b64) { function decodeBase64Utf8(b64) {
try { try {
var s = String(b64 || ""); var s = String(b64 || "");

124
code/th_02_core.js Normal file
View File

@@ -0,0 +1,124 @@
// @version 1.0.0
function FloatBallAppWM(logger) {
this.L = logger || null;
// # 加载配置
this.config = ConfigManager.loadSettings();
this.currentPanelKey = "main";
this.panels = { main: ConfigManager.loadButtons() };
// # 更新 Logger 配置(因为 Logger 初始化时是默认值)
if (this.L) this.L.updateConfig(this.config);
this.state = {
receivers: [], // 存储广播接收器引用,用于 close 时注销
wm: null,
dm: null,
density: 1.0,
screen: { w: 0, h: 0 },
lastRotation: -1,
lastMonitorTs: 0,
ht: null,
h: null,
addedBall: false,
addedPanel: false,
addedSettings: false,
addedViewer: false,
addedMask: false,
ballRoot: null,
ballContent: null,
ballLp: null,
panel: null,
panelLp: null,
settingsPanel: null,
settingsPanelLp: null,
viewerPanel: null,
viewerPanelLp: null,
viewerPanelType: null,
mask: null,
maskLp: null,
loadedPos: null,
lastSaveTs: 0,
dragging: false,
rawX: 0,
rawY: 0,
downX: 0,
downY: 0,
docked: false,
dockSide: null,
lastMotionTs: 0,
idleDockRunnable: null,
longPressArmed: false,
longPressTriggered: false,
longPressRunnable: null,
displayListener: null,
// # 设置面板:临时编辑缓存
pendingUserCfg: null,
pendingDirty: false,
closing: false
};
// # 创建实例独立的 UI 工具对象,避免多实例共享颜色状态
this.ui = {};
var protoUi = FloatBallAppWM.prototype.ui;
for (var _uiKey in protoUi) {
this.ui[_uiKey] = protoUi[_uiKey];
}
this.ui.colors = {};
// # 初始化莫奈动态配色(传入当前主题避免重复检测)
try { this.refreshMonetColors(this.isDarkTheme()); } catch(eM) { safeLog(null, 'e', "catch " + String(eM)); }
}
// =======================【工具dp/now/clamp】======================
FloatBallAppWM.prototype.dp = function(v) { return Math.floor(Number(v) * this.state.density); };
FloatBallAppWM.prototype.sp = function(v) { try { return Math.floor(Number(v) * context.getResources().getDisplayMetrics().scaledDensity); } catch (e) { return Math.floor(Number(v) * this.state.density); } };
FloatBallAppWM.prototype.now = function() { return new Date().getTime(); };
FloatBallAppWM.prototype.clamp = function(v, min, max) { if (v < min) return min; if (v > max) return max; return v; };
FloatBallAppWM.prototype.rectIntersect = function(ax, ay, aw, ah, bx, by, bw, bh) {
return !(ax + aw <= bx || bx + bw <= ax || ay + ah <= by || by + bh <= ay);
};
// # 这段代码的主要内容/用途:安全地在 UI 线程执行代码(用于后台线程回调更新 UI
FloatBallAppWM.prototype.runOnUiThreadSafe = function(fn) {
try {
if (!fn) return;
var self = this;
// 优先使用 Activity 的 runOnUiThread否则使用 View.post
if (this.state && this.state.ballRoot) {
this.state.ballRoot.post(new java.lang.Runnable({
run: function() { try { fn.call(self); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } }
}));
} else {
// 兜底:直接执行(如果已经在 UI 线程)或尝试使用 Handler
try {
var h = new android.os.Handler(android.os.Looper.getMainLooper());
h.post(new java.lang.Runnable({
run: function() { try { fn.call(self); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } }
}));
} catch(e) {
// 最后兜底:直接执行
try { fn.call(self); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
// # 这段代码的主要内容/用途统一图标缓存LRU减少反复解码/反复走 PackageManager降低卡顿与内存波动不改变 UI 与功能)
// 优化后的图标缓存(带 Bitmap 回收,防止内存泄漏)

170
code/th_03_icon.js Normal file
View File

@@ -0,0 +1,170 @@
// @version 1.0.0
FloatBallAppWM.prototype._iconCache = {
map: {},
keys: [],
max: 80, // 减少缓存数量,降低内存压力
get: function(key) {
var item = this.map[key];
if (!item) return null;
// 移动到末尾(最近使用)
var idx = this.keys.indexOf(key);
if (idx > -1) {
this.keys.splice(idx, 1);
this.keys.push(key);
}
return item.dr;
},
put: function(key, drawable) {
// 清理旧的
if (this.map[key]) {
this._remove(key);
}
// 空间检查:超过 80% 时批量清理 20%
if (this.keys.length >= this.max * 0.8) {
var removeCount = Math.floor(this.max * 0.2);
for (var i = 0; i < removeCount && this.keys.length > 0; i++) {
var oldKey = this.keys.shift();
this._remove(oldKey);
}
}
this.keys.push(key);
this.map[key] = {dr: drawable, ts: Date.now()};
},
_remove: function(key) {
var item = this.map[key];
if (item && item.dr) {
// 关键:回收 Bitmap防止内存泄漏
try {
if (item.dr instanceof android.graphics.drawable.BitmapDrawable) {
var bmp = item.dr.getBitmap();
if (bmp && !bmp.isRecycled()) bmp.recycle();
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
delete this.map[key];
}
},
clear: function() {
for (var i = 0; i < this.keys.length; i++) {
this._remove(this.keys[i]);
}
this.keys = [];
}
};
// 兼容性封装(保持原有调用方式不变)
FloatBallAppWM.prototype._iconLruEnsure = function() {};
FloatBallAppWM.prototype._iconLruGet = function(key) {
return this._iconCache.get(key);
};
FloatBallAppWM.prototype._iconLruPut = function(key, val) {
try {
this._iconLruEnsure(120);
var k = String(key || "");
if (!k) return;
if (val == null) return;
// # 若已存在,先移除旧顺序位置
try {
var ord = this._iconLru.order;
for (var i = ord.length - 1; i >= 0; i--) {
if (ord[i] === k) { ord.splice(i, 1); break; }
}
ord.push(k);
} catch(eLru3) { safeLog(null, 'e', "catch " + String(eLru3)); }
this._iconLru.map[k] = val;
// # 超限清理:按最久未使用淘汰
try {
var maxN = Math.max(20, Math.floor(Number(this._iconLru.max || 120)));
var ord2 = this._iconLru.order;
while (ord2.length > maxN) {
var oldK = ord2.shift();
if (oldK != null) {
try { delete this._iconLru.map[oldK]; } catch(eDel) { safeLog(null, 'e', "catch " + String(eDel)); }
}
}
} catch(eLru4) { safeLog(null, 'e', "catch " + String(eLru4)); }
} catch(eLru5) { safeLog(null, 'e', "catch " + String(eLru5)); }
};
// =======================【工具悬浮球图标PNG 文件)】======================
// # 这段代码的主要内容/用途:从指定路径加载透明 PNG 作为悬浮球图标;带"文件大小/像素上限"保护;按目标尺寸采样解码,避免 system_server OOM。
FloatBallAppWM.prototype.loadBallIconDrawableFromFile = function(path, targetPx, maxBytes, maxPx) {
try {
var p = String(path || "");
if (!p) return null;
// # 统一 LRU 缓存:文件图标(按 path + targetPx + mtime + size 复用 Drawable避免反复解码
var f = new java.io.File(p);
if (!f.exists() || !f.isFile()) return null;
var ckLru = null;
try {
ckLru = "file|" + p + "@" + String(targetPx == null ? "" : targetPx) + "@" + String(f.lastModified()) + "@" + String(f.length());
var hitLru = this._iconLruGet(ckLru);
if (hitLru) return hitLru;
} catch(eLruF0) { safeLog(null, 'e', "catch " + String(eLruF0)); }
// # 文件大小限制(字节)
var limitBytes = Math.max(0, Math.floor(Number(maxBytes || 0)));
if (limitBytes > 0) {
try {
var sz = Number(f.length());
if (sz > limitBytes) return null;
} catch (eSz) { return null; }
}
// # 先只读尺寸(不解码)
var opt = new android.graphics.BitmapFactory.Options();
opt.inJustDecodeBounds = true;
try { android.graphics.BitmapFactory.decodeFile(p, opt); } catch (eB0) { return null; }
var w = Number(opt.outWidth || 0);
var h = Number(opt.outHeight || 0);
if (w <= 0 || h <= 0) return null;
// # 像素边长上限(宽/高任意一边超限则拒绝)
var limitPx = Math.max(0, Math.floor(Number(maxPx || 0)));
if (limitPx > 0) {
if (w > limitPx || h > limitPx) return null;
}
// # 计算采样倍率:按目标尺寸(一般为 iconSizePx采样
var tp = Math.max(1, Math.floor(Number(targetPx || 1)));
// # 允许解码到目标的 2 倍以内,减少锯齿又不浪费内存
var desired = Math.max(tp * 2, tp);
var sample = 1;
while ((w / sample) > desired || (h / sample) > desired) sample = sample * 2;
if (sample < 1) sample = 1;
var opt2 = new android.graphics.BitmapFactory.Options();
opt2.inJustDecodeBounds = false;
opt2.inSampleSize = sample;
opt2.inPreferredConfig = android.graphics.Bitmap.Config.ARGB_8888;
var bmp = null;
try { bmp = android.graphics.BitmapFactory.decodeFile(p, opt2); } catch (eB1) { bmp = null; }
if (bmp == null) return null;
var d = new android.graphics.drawable.BitmapDrawable(context.getResources(), bmp);
// # 写入统一 LRU 缓存
try {
if (ckLru) this._iconLruPut(ckLru, d);
} catch(eLruF1) { safeLog(null, 'e', "catch " + String(eLruF1)); }
return d;
} catch (e0) {
return null;
}
};

800
code/th_04_theme.js Normal file
View File

@@ -0,0 +1,800 @@
// @version 1.0.0
// =======================【工具:屏幕/旋转】======================
FloatBallAppWM.prototype.getScreenSizePx = function() {
var m = new android.util.DisplayMetrics();
try { this.state.wm.getDefaultDisplay().getRealMetrics(m); } catch (e) { this.state.wm.getDefaultDisplay().getMetrics(m); }
return { w: m.widthPixels, h: m.heightPixels };
};
FloatBallAppWM.prototype.getRotation = function() { try { return this.state.wm.getDefaultDisplay().getRotation(); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } return -1; };
// =======================【工具alpha/toast/vibrate】======================
FloatBallAppWM.prototype.withAlpha = function(colorInt, alpha01) { var a = Math.floor(Number(alpha01) * 255); return (colorInt & 0x00FFFFFF) | (a << 24); };
FloatBallAppWM.prototype.toast = function(msg) { try { android.widget.Toast.makeText(context, String(msg), 0).show(); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } };
FloatBallAppWM.prototype.vibrateOnce = function(ms) {
if (!this.config.LONG_PRESS_HAPTIC_ENABLE) return;
try {
var vib = context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
if (!vib) return;
var dur = Math.max(1, Math.floor(ms));
if (android.os.Build.VERSION.SDK_INT >= 26) {
var ve = android.os.VibrationEffect.createOneShot(dur, android.os.VibrationEffect.DEFAULT_AMPLITUDE);
vib.vibrate(ve);
} else {
vib.vibrate(dur);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
// =======================【工具UI样式辅助】======================
FloatBallAppWM.prototype.ui = {
// 基础颜色
colors: {
// 以下为默认回退值,实例化时会被 refreshMonetColors() 覆盖为系统莫奈色
primary: android.graphics.Color.parseColor("#005BC0"),
primaryDark: android.graphics.Color.parseColor("#041E49"),
accent: android.graphics.Color.parseColor("#00639B"),
danger: android.graphics.Color.parseColor("#BA1A1A"),
success: android.graphics.Color.parseColor("#15803d"),
warning: android.graphics.Color.parseColor("#b45309"),
bgLight: android.graphics.Color.parseColor("#F8F9FA"),
bgDark: android.graphics.Color.parseColor("#131314"),
cardLight: android.graphics.Color.parseColor("#E1E3E1"),
cardDark: android.graphics.Color.parseColor("#49454F"),
textPriLight: android.graphics.Color.parseColor("#1F1F1F"),
textPriDark: android.graphics.Color.parseColor("#E3E3E3"),
textSecLight: android.graphics.Color.parseColor("#5F6368"),
textSecDark: android.graphics.Color.parseColor("#C4C7C5"),
dividerLight: android.graphics.Color.parseColor("#747775"),
dividerDark: android.graphics.Color.parseColor("#8E918F"),
inputBgLight: android.graphics.Color.parseColor("#F8F9FA"),
inputBgDark: android.graphics.Color.parseColor("#131314"),
// Monet 扩展字段(供面板直接使用)
_monetSurface: android.graphics.Color.parseColor("#F8F9FA"),
_monetOnSurface: android.graphics.Color.parseColor("#1F1F1F"),
_monetOutline: android.graphics.Color.parseColor("#747775"),
_monetOnPrimary: android.graphics.Color.parseColor("#FFFFFF"),
_monetPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
_monetOnPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
_monetSecondary: android.graphics.Color.parseColor("#00639B"),
_monetTertiary: android.graphics.Color.parseColor("#5C5891")
},
// 创建圆角背景 (Solid)
createRoundDrawable: function(color, radiusPx) {
var d = new android.graphics.drawable.GradientDrawable();
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
d.setColor(color);
d.setCornerRadius(radiusPx);
return d;
},
// 创建圆角描边背景 (Stroke)
createStrokeDrawable: function(fillColor, strokeColor, strokeWidthPx, radiusPx) {
var d = new android.graphics.drawable.GradientDrawable();
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
if (fillColor) d.setColor(fillColor);
d.setCornerRadius(radiusPx);
d.setStroke(strokeWidthPx, strokeColor);
return d;
},
// 创建按压反馈背景 (StateList)
createRippleDrawable: function(normalColor, pressedColor, radiusPx) {
var sd = new android.graphics.drawable.StateListDrawable();
var p = this.createRoundDrawable(pressedColor, radiusPx);
var n = this.createRoundDrawable(normalColor, radiusPx);
sd.addState([android.R.attr.state_pressed], p);
sd.addState([], n);
return sd;
},
// 创建纯色按压反馈 (StateList) - 用于透明背景按钮
createTransparentRippleDrawable: function(pressedColor, radiusPx) {
var sd = new android.graphics.drawable.StateListDrawable();
var p = this.createRoundDrawable(pressedColor, radiusPx);
var n = new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT);
sd.addState([android.R.attr.state_pressed], p);
sd.addState([], n);
return sd;
},
// 辅助:创建扁平按钮
createFlatButton: function(app, txt, txtColor, onClick) {
var btn = new android.widget.TextView(context);
btn.setText(txt);
btn.setTextColor(txtColor);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setPadding(app.dp(12), app.dp(6), app.dp(12), app.dp(6));
btn.setGravity(android.view.Gravity.CENTER);
// use divider color or just low alpha text color for ripple
var rippleColor = app.withAlpha ? app.withAlpha(txtColor, 0.1) : 0x22888888;
btn.setBackground(this.createTransparentRippleDrawable(rippleColor, app.dp(8)));
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
}));
return btn;
},
// 辅助:创建实心按钮
createSolidButton: function(app, txt, bgColor, txtColor, onClick) {
var btn = new android.widget.TextView(context);
btn.setText(txt);
btn.setTextColor(txtColor);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setTypeface(null, android.graphics.Typeface.BOLD);
btn.setPadding(app.dp(16), app.dp(8), app.dp(16), app.dp(8));
btn.setGravity(android.view.Gravity.CENTER);
var pressedColor = app.withAlpha ? app.withAlpha(bgColor, 0.8) : bgColor;
btn.setBackground(this.createRippleDrawable(bgColor, pressedColor, app.dp(24)));
try { btn.setElevation(app.dp(2)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
}));
return btn;
},
// 辅助:创建带标签的输入组(支持粘贴)
createInputGroup: function(app, label, initVal, isMultiLine, hint) {
var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setPadding(0, 0, 0, app.dp(12));
var topLine = new android.widget.LinearLayout(context);
topLine.setOrientation(android.widget.LinearLayout.HORIZONTAL);
topLine.setGravity(android.view.Gravity.CENTER_VERTICAL);
box.addView(topLine);
var lb = new android.widget.TextView(context);
lb.setText(label);
lb.setTextColor(this.colors.textSecLight); // 默认用浅色主题副文本色,外部可覆盖
try { if (app.isDarkTheme && app.isDarkTheme()) lb.setTextColor(this.colors.textSecDark); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
lb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
var lpLb = new android.widget.LinearLayout.LayoutParams(0, -2);
lpLb.weight = 1;
topLine.addView(lb, lpLb);
var et = new android.widget.EditText(context);
et.setText(initVal ? String(initVal) : "");
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setTextColor(this.colors.textPriLight);
try { if (app.isDarkTheme && app.isDarkTheme()) et.setTextColor(this.colors.textPriDark); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
// 输入框背景优化
var strokeColor = this.colors.dividerLight;
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = this.colors.dividerDark; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var bg = this.createStrokeDrawable(this.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
try { if (app.isDarkTheme && app.isDarkTheme()) bg = this.createStrokeDrawable(this.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
et.setBackground(bg);
et.setPadding(app.dp(8), app.dp(8), app.dp(8), app.dp(8));
if (hint) et.setHint(hint);
if (isMultiLine) {
et.setSingleLine(false);
et.setMaxLines(4);
} else {
et.setSingleLine(true);
}
// 粘贴功能
var pasteBtn = this.createFlatButton(app, "粘贴", this.colors.accent, function() {
try {
var cb = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE);
if (cb.hasPrimaryClip()) {
var item = cb.getPrimaryClip().getItemAt(0);
if (item) {
var txt = item.getText();
if (txt) {
var st = String(txt);
var old = String(et.getText());
if (old.length > 0) et.setText(old + st);
else et.setText(st);
et.setSelection(et.getText().length());
}
}
} else {
app.toast("剪贴板为空");
}
} catch (eP) {
app.toast("粘贴失败: " + eP);
}
});
// 调整粘贴按钮样式使其更紧凑
pasteBtn.setPadding(app.dp(8), app.dp(2), app.dp(8), app.dp(2));
pasteBtn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
topLine.addView(pasteBtn);
box.addView(et);
// 错误提示
var errTv = new android.widget.TextView(context);
errTv.setTextColor(this.colors.danger);
errTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 10);
errTv.setVisibility(android.view.View.GONE);
box.addView(errTv);
var self = this;
return {
view: box,
input: et,
getValue: function() { return String(et.getText()); },
setError: function(msg) {
if (msg) {
errTv.setText(msg);
errTv.setVisibility(android.view.View.VISIBLE);
et.setBackground(self.createRoundDrawable(app.withAlpha(self.colors.danger, 0.2), app.dp(4)));
} else {
errTv.setVisibility(android.view.View.GONE);
var strokeColor = self.colors.dividerLight;
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = self.colors.dividerDark; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var normalBg = self.createStrokeDrawable(self.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
try { if (app.isDarkTheme && app.isDarkTheme()) normalBg = self.createStrokeDrawable(self.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
et.setBackground(normalBg);
}
}
};
},
// 辅助:创建标准面板容器
createStyledPanel: function(app, paddingDp) {
var isDark = app.isDarkTheme();
var bgColor = isDark ? this.colors.bgDark : this.colors.bgLight;
var panel = new android.widget.LinearLayout(context);
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
var bgDr = new android.graphics.drawable.GradientDrawable();
bgDr.setColor(bgColor);
bgDr.setCornerRadius(app.dp(16));
panel.setBackground(bgDr);
try { panel.setElevation(app.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var p = (paddingDp !== undefined) ? app.dp(paddingDp) : app.dp(16);
panel.setPadding(p, p, p, p);
return panel;
},
// 辅助:创建标准标题栏容器
createStyledHeader: function(app, paddingBottomDp) {
var header = new android.widget.LinearLayout(context);
header.setOrientation(android.widget.LinearLayout.HORIZONTAL);
header.setGravity(android.view.Gravity.CENTER_VERTICAL);
var pb = (paddingBottomDp !== undefined) ? app.dp(paddingBottomDp) : app.dp(8);
header.setPadding(0, 0, 0, pb);
return header;
},
// 辅助:创建占位符(撑开空间)
createSpacer: function(app) {
var dummy = new android.view.View(context);
var dummyLp = new android.widget.LinearLayout.LayoutParams(0, 1);
dummyLp.weight = 1;
dummy.setLayoutParams(dummyLp);
return dummy;
}
};
// =======================【工具:主题/类莫奈颜色】======================
// # 主题调试日志工具(仅打印,不改变逻辑)
function _th_hex(c) {
try {
var Integer = Packages.java.lang.Integer;
var s = Integer.toHexString(c);
while (s.length < 8) s = "0" + s;
return "0x" + s;
} catch (e) {
try { return String(c); } catch (e2) { return "<?>"; }
}
}
function _th_argb(c) {
try {
var Color = Packages.android.graphics.Color;
var ci = Math.floor(Number(c));
if (isNaN(ci)) return "NaN";
return "a=" + Color.alpha(ci) + " r=" + Color.red(ci) + " g=" + Color.green(ci) + " b=" + Color.blue(ci);
} catch (e) {
return "argb_err";
}
}
function _th_log(L, level, msg) {
try {
if (!L) return;
if (level === "e" && L.e) { L.e(msg); return; }
if (level === "w" && L.w) { L.w(msg); return; }
if (level === "i" && L.i) { L.i(msg); return; }
if (L.d) { L.d(msg); return; }
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
// =======================【莫奈动态取色工具】======================
var MonetColorProvider = {
_cacheLight: null,
_cacheDark: null,
_getResColor: function(resName, fallbackHex) {
try {
var res = android.content.res.Resources.getSystem();
var id = res.getIdentifier(resName, "color", "android");
if (id > 0) return res.getColor(id, null);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try { return android.graphics.Color.parseColor(fallbackHex); } catch (e) { return 0; }
},
_getFallbackLight: function() {
return {
// 使用更接近 AOSP Monet 标准值的 fallback日间 primary 更饱和、对比度更高
primary: android.graphics.Color.parseColor("#005BC0"),
onPrimary: android.graphics.Color.parseColor("#FFFFFF"),
primaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
onPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
secondary: android.graphics.Color.parseColor("#00639B"),
secondaryContainer: android.graphics.Color.parseColor("#C2E4FF"),
tertiary: android.graphics.Color.parseColor("#5C5891"),
surface: android.graphics.Color.parseColor("#F8F9FA"),
onSurface: android.graphics.Color.parseColor("#1F1F1F"),
surfaceVariant: android.graphics.Color.parseColor("#E1E3E1"),
onSurfaceVariant: android.graphics.Color.parseColor("#5F6368"),
outline: android.graphics.Color.parseColor("#747775"),
outlineVariant: android.graphics.Color.parseColor("#C4C7C5"),
error: android.graphics.Color.parseColor("#BA1A1A"),
errorContainer: android.graphics.Color.parseColor("#F9DEDC"),
onErrorContainer: android.graphics.Color.parseColor("#410E0B")
};
},
_getFallbackDark: function() {
return {
primary: android.graphics.Color.parseColor("#A8C7FA"),
onPrimary: android.graphics.Color.parseColor("#062E6F"),
primaryContainer: android.graphics.Color.parseColor("#0842A0"),
onPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
secondary: android.graphics.Color.parseColor("#7FCFFF"),
secondaryContainer: android.graphics.Color.parseColor("#004A77"),
tertiary: android.graphics.Color.parseColor("#C2C5DD"),
surface: android.graphics.Color.parseColor("#131314"),
onSurface: android.graphics.Color.parseColor("#E3E3E3"),
surfaceVariant: android.graphics.Color.parseColor("#49454F"),
onSurfaceVariant: android.graphics.Color.parseColor("#C4C7C5"),
outline: android.graphics.Color.parseColor("#8E918F"),
outlineVariant: android.graphics.Color.parseColor("#49454F"),
error: android.graphics.Color.parseColor("#F2B8B5"),
errorContainer: android.graphics.Color.parseColor("#8C1D18"),
onErrorContainer: android.graphics.Color.parseColor("#F9DEDC")
};
},
_loadMonet: function(isDark) {
var c = isDark ? this._getFallbackDark() : this._getFallbackLight();
try {
var res = android.content.res.Resources.getSystem();
var map = isDark ? {
primary: "system_accent1_200",
onPrimary: "system_accent1_800",
primaryContainer: "system_accent1_700",
onPrimaryContainer: "system_accent1_100",
secondary: "system_accent2_200",
secondaryContainer: "system_accent2_700",
tertiary: "system_accent3_200",
surface: "system_neutral1_900",
onSurface: "system_neutral1_100",
surfaceVariant: "system_neutral2_700",
onSurfaceVariant: "system_neutral2_200",
outline: "system_neutral2_400",
outlineVariant: "system_neutral2_700",
error: "system_accent3_200",
errorContainer: "system_accent3_800",
onErrorContainer: "system_accent3_100"
} : {
primary: "system_accent1_600",
onPrimary: "system_accent1_0",
primaryContainer: "system_accent1_100",
onPrimaryContainer: "system_accent1_900",
secondary: "system_accent2_600",
secondaryContainer: "system_accent2_100",
tertiary: "system_accent3_600",
surface: "system_neutral1_10",
onSurface: "system_neutral1_900",
surfaceVariant: "system_neutral2_100",
onSurfaceVariant: "system_neutral2_700",
outline: "system_neutral2_500",
outlineVariant: "system_neutral2_200",
error: "system_accent3_600",
errorContainer: "system_accent3_100",
onErrorContainer: "system_accent3_900"
};
for (var name in map) {
try {
var id = res.getIdentifier(map[name], "color", "android");
if (id > 0) c[name] = res.getColor(id, null);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return c;
},
getColors: function(isDark) {
if (isDark) {
if (!this._cacheDark) this._cacheDark = this._loadMonet(true);
return this._cacheDark;
} else {
if (!this._cacheLight) this._cacheLight = this._loadMonet(false);
return this._cacheLight;
}
},
invalidate: function() {
this._cacheLight = null;
this._cacheDark = null;
}
};
// =======================【兼容兜底themeTextInt/themeBgInt】======================
// 这段代码的主要内容/用途:兼容旧代码或异步回调里误引用 themeTextInt/themeBgInt 导致 ReferenceError 崩溃。
// 说明:当前版本文字色应通过 getPanelTextColorInt(bgInt) 获取;这里仅作为"兜底全局变量",避免回调炸线程。
// 声明全局变量(避免 ReferenceError
var themeBgInt = 0;
var themeTextInt = 0;
// =======================【API 兼容性辅助函数】======================
// 这段代码的主要内容/用途:处理 Android API 级别差异,避免在旧版本上崩溃
/**
* 安全创建 UserHandle兼容 API 17 以下)
* @param {number} userId - 用户 ID
* @returns {android.os.UserHandle} UserHandle 对象或 null
*/
function createUserHandle(userId) {
try {
// UserHandle.of 在 API 17+ 可用
if (android.os.Build.VERSION.SDK_INT >= 17) {
return android.os.UserHandle.of(userId);
}
// API 17 以下返回当前用户句柄
return android.os.Process.myUserHandle();
} catch (e) {
return null;
}
}
/**
* 安全启动 Activity 跨用户(兼容 API 17 以下)
* @param {Context} ctx - Context
* @param {Intent} intent - Intent
* @param {number} userId - 用户 IDAPI 17+ 有效)
*/
function startActivityAsUserSafe(ctx, intent, userId) {
try {
if (android.os.Build.VERSION.SDK_INT >= 17 && userId !== 0) {
var uh = android.os.UserHandle.of(userId);
ctx.startActivityAsUser(intent, uh);
} else {
ctx.startActivity(intent);
}
} catch (e) {
// 降级到普通启动
try {
ctx.startActivity(intent);
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
}
FloatBallAppWM.prototype.isDarkTheme = function() {
// 0) 优先检查用户强制设置 (0=跟随系统, 1=白天, 2=黑夜)
var mode = 0;
try { mode = Math.floor(Number(this.config.THEME_MODE || 0)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
if (mode === 2) return true;
if (mode === 1) return false;
// mode === 0 (or others) -> Fallback to system detection
// 这段代码的主要内容/用途:更稳健地判断当前是否处于"夜间/暗色"模式(并打印调试日志)。
// 说明system_server 场景下 Configuration.uiMode 偶发不一致,因此再用 UiModeManager 兜底交叉验证。
var result = false;
var from = "unknown";
try {
// # 1) 优先用 Configuration最快
var uiMode = context.getResources().getConfiguration().uiMode;
var nightMask = (uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK);
if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_YES) { result = true; from = "Configuration(UI_MODE_NIGHT_YES)"; }
else if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_NO) { result = false; from = "Configuration(UI_MODE_NIGHT_NO)"; }
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
if (from === "unknown") {
try {
// # 2) 再用 UiModeManager更"系统态"
var um = context.getSystemService(android.content.Context.UI_MODE_SERVICE);
if (um) {
var nm = um.getNightMode();
if (nm === android.app.UiModeManager.MODE_NIGHT_YES) { result = true; from = "UiModeManager(MODE_NIGHT_YES)"; }
else if (nm === android.app.UiModeManager.MODE_NIGHT_NO) { result = false; from = "UiModeManager(MODE_NIGHT_NO)"; }
else { from = "UiModeManager(mode=" + String(nm) + ")"; }
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
// # 3) 实在判断不了,就按"非暗色"处理,避免自动主题背景黑成一片
if (from === "unknown") { result = false; from = "fallback(false)"; }
// 仅在状态改变时打印日志,避免刷屏
var logKey = String(result) + "|" + from + "|" + mode;
if (this._lastDarkThemeLog !== logKey) {
this._lastDarkThemeLog = logKey;
try { _th_log(this.L, "d", "[theme] isDarkTheme=" + String(result) + " via=" + from + " mode=" + mode); } catch(e3) { safeLog(null, 'e', "catch " + String(e3)); }
}
// # 主题切换时刷新莫奈配色(传入 result 避免递归)
// 注:构造函数中会初始化,这里只在构造完成后的切换时触发
if (this._lastDarkResult !== undefined && this._lastDarkResult !== result) {
this._lastDarkResult = result;
try { this.refreshMonetColors(result); } catch(eM) { safeLog(null, 'e', "catch " + String(eM)); }
} else if (this._lastDarkResult === undefined) {
this._lastDarkResult = result;
}
return result;
};
FloatBallAppWM.prototype.refreshMonetColors = function(forceDark) {
try {
var isDark = (forceDark !== undefined) ? forceDark : this.isDarkTheme();
var m = MonetColorProvider.getColors(isDark);
var ml = MonetColorProvider.getColors(false);
var md = MonetColorProvider.getColors(true);
var c = this.ui.colors;
// 浅色配色
c.bgLight = ml.surface;
c.cardLight = ml.surfaceVariant;
c.textPriLight = ml.onSurface;
c.textSecLight = ml.onSurfaceVariant;
c.dividerLight = ml.outline;
c.inputBgLight = ml.surface;
// 深色配色
c.bgDark = md.surface;
c.cardDark = md.surfaceVariant;
c.textPriDark = md.onSurface;
c.textSecDark = md.onSurfaceVariant;
c.dividerDark = md.outline;
c.inputBgDark = md.surface;
// 当前主题配色(随主题切换)
c.primary = m.primary;
// primaryDark 修正为 onPrimaryContainer日间为深蓝(#041E49),夜间为浅蓝(#D3E3FD),符合"深色变体"语义
c.primaryDark = m.onPrimaryContainer;
c.accent = m.secondary;
c.danger = m.error;
// success/warning 优化对比度:日间更深确保可见,夜间保持适度亮度不刺眼
c.success = isDark ? android.graphics.Color.parseColor("#4ade80") : android.graphics.Color.parseColor("#15803d");
c.warning = isDark ? android.graphics.Color.parseColor("#fbbf24") : android.graphics.Color.parseColor("#b45309");
// 扩展:完整 Monet 语义字段(供面板方法直接使用)
c._monetSurface = m.surface;
c._monetOnSurface = m.onSurface;
c._monetOutline = m.outline;
c._monetOnPrimary = m.onPrimary;
c._monetPrimaryContainer = m.primaryContainer;
c._monetOnPrimaryContainer = m.onPrimaryContainer;
c._monetSecondary = m.secondary;
c._monetTertiary = m.tertiary;
try { _th_log(this.L, "d", "[monet] refreshed isDark=" + isDark + " primary=" + _th_hex(c.primary) + " primaryDark=" + _th_hex(c.primaryDark) + " accent=" + _th_hex(c.accent)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} catch (e) {
try { _th_log(this.L, "e", "[monet] refresh err=" + String(e)); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
};
FloatBallAppWM.prototype.getMonetAccentForBall = function() {
// 这段代码的主要内容/用途尝试读取系统动态强调色Monet/accent失败则使用兜底颜色并打印命中信息。
// 优化点:
// 1) 日间使用 500/600 档(更深、对比度更高),夜间使用 400/300 档(柔和、不刺眼)
// 2) 移除 system_neutral1_* 中性灰色(不是强调色)
var res = context.getResources();
var dark = this.isDarkTheme();
var names = dark
? ["system_accent1_400", "system_accent1_300", "system_accent2_400"]
: ["system_accent1_500", "system_accent1_600", "system_accent2_500"];
var i;
for (i = 0; i < names.length; i++) {
try {
var id = res.getIdentifier(names[i], "color", "android");
if (id > 0) {
var c = res.getColor(id, null);
var logKey = "hit|" + names[i] + "|" + c;
if (this._lastAccentLog !== logKey) {
this._lastAccentLog = logKey;
try { _th_log(this.L, "d", "[theme] hit accent=" + names[i] + " id=" + String(id) + " c=" + _th_hex(c) + " " + _th_argb(c)); } catch(eL0) { safeLog(null, 'e', "catch " + String(eL0)); }
}
return c;
}
} catch (e) {
try { _th_log(this.L, "w", "[theme] err accent=" + names[i] + " e=" + String(e)); } catch(eL2) { safeLog(null, 'e', "catch " + String(eL2)); }
}
}
var fbHex = dark
? (this.config.BALL_FALLBACK_DARK || CONST_BALL_FALLBACK_DARK)
: (this.config.BALL_FALLBACK_LIGHT || CONST_BALL_FALLBACK_LIGHT);
var fb = android.graphics.Color.parseColor(fbHex);
var logKeyFb = "miss_all|" + fb;
if (this._lastAccentLog !== logKeyFb) {
this._lastAccentLog = logKeyFb;
try { _th_log(this.L, "w", "[theme] accent miss all, fallback=" + _th_hex(fb) + " " + _th_argb(fb)); } catch(eL3) { safeLog(null, 'e', "catch " + String(eL3)); }
}
return fb;
};
FloatBallAppWM.prototype.updateBallContentBackground = function(contentView) {
try {
var ballColor = this.getMonetAccentForBall();
var dark = this.isDarkTheme();
var alpha01 = dark ? this.config.BALL_RIPPLE_ALPHA_DARK : this.config.BALL_RIPPLE_ALPHA_LIGHT;
var rippleColor = this.withAlpha(ballColor, alpha01);
// # 自定义 PNG/APP 模式下:背景透明
var fillColor = ballColor;
var _usedKind = "none";
try { _usedKind = this.state.usedIconKind || "none"; } catch(eK) { safeLog(null, 'e', "catch " + String(eK)); }
try {
var _pngModeBg = Number(this.config.BALL_PNG_MODE || 0);
if ((_pngModeBg === 1 && _usedKind === "file") || _usedKind === "app") {
fillColor = android.graphics.Color.TRANSPARENT;
}
} catch(eBg) { safeLog(null, 'e', "catch " + String(eBg)); }
var content = new android.graphics.drawable.GradientDrawable();
content.setShape(android.graphics.drawable.GradientDrawable.OVAL);
content.setColor(fillColor);
// # 描边:根据球体颜色亮度自动选择白/黑描边,确保任何背景下都可见
if (_usedKind !== "file" && _usedKind !== "app") {
try {
var Color = android.graphics.Color;
var lum = (Color.red(fillColor)*0.299 + Color.green(fillColor)*0.587 + Color.blue(fillColor)*0.114) / 255.0;
var strokeInt = lum > 0.55
? Color.parseColor("#33000000") // 浅球用半透明黑边
: Color.parseColor("#55FFFFFF"); // 深球用半透明白边
content.setStroke(this.dp(1), strokeInt);
} catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
}
var mask = new android.graphics.drawable.GradientDrawable();
mask.setShape(android.graphics.drawable.GradientDrawable.OVAL);
mask.setColor(android.graphics.Color.WHITE);
contentView.setBackground(new android.graphics.drawable.RippleDrawable(
android.content.res.ColorStateList.valueOf(rippleColor),
content,
mask
));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
FloatBallAppWM.prototype.safeParseColor = function(hex, fallbackInt) {
// 这段代码的主要内容/用途:安全解析 #RRGGBB/#AARRGGBB解析失败直接回退避免 system_server 抛异常。
try { return android.graphics.Color.parseColor(String(hex)); } catch (e) { return fallbackInt; }
};
FloatBallAppWM.prototype.getPanelBgColorInt = function() {
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的背景颜色(不再依赖自动亮度推断)。
var isDark = this.isDarkTheme();
var dayBgHex = (this.config.THEME_DAY_BG_HEX != null) ? String(this.config.THEME_DAY_BG_HEX) : null;
var nightBgHex = (this.config.THEME_NIGHT_BG_HEX != null) ? String(this.config.THEME_NIGHT_BG_HEX) : null;
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
if (dayBgHex === "#FAF4E3") dayBgHex = null;
if (nightBgHex === "#191928") nightBgHex = null;
// # 未配置时使用莫奈 surface 色作为回退
var dayFallback = this.ui.colors.bgLight || android.graphics.Color.parseColor("#F8F9FA");
var nightFallback = this.ui.colors.bgDark || android.graphics.Color.parseColor("#131314");
var base = isDark
? (nightBgHex ? this.safeParseColor(nightBgHex, nightFallback) : nightFallback)
: (dayBgHex ? this.safeParseColor(dayBgHex, dayFallback) : dayFallback);
// # 继承原配置面板背景透明度0~1
var a = 1.0;
try { a = Number(this.config.PANEL_BG_ALPHA); } catch (e1) { a = 0.85; }
if (!(a >= 0.0 && a <= 1.0)) a = 0.85;
var out = this.withAlpha(base, a);
try { _th_log(this.L, "d", "[t]bg isDark=" + isDark + " o=" + _th_hex(out)); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return out;
};
FloatBallAppWM.prototype.getPanelTextColorInt = function(bgInt) {
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的文字颜色(不再依赖自动亮度推断)。
var isDark = this.isDarkTheme();
var dayTextHex = (this.config.THEME_DAY_TEXT_HEX != null) ? String(this.config.THEME_DAY_TEXT_HEX) : null;
var nightTextHex = (this.config.THEME_NIGHT_TEXT_HEX != null) ? String(this.config.THEME_NIGHT_TEXT_HEX) : null;
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
if (dayTextHex === "#333333") dayTextHex = null;
if (nightTextHex === "#E6E6F0") nightTextHex = null;
// # 未配置时使用莫奈 onSurface 色作为回退
var dayFallback = this.ui.colors.textPriLight || android.graphics.Color.parseColor("#1F1F1F");
var nightFallback = this.ui.colors.textPriDark || android.graphics.Color.parseColor("#E3E3E3");
if (!isDark) return dayTextHex ? this.safeParseColor(dayTextHex, dayFallback) : dayFallback;
return nightTextHex ? this.safeParseColor(nightTextHex, nightFallback) : nightFallback;
};
FloatBallAppWM.prototype.applyTextColorRecursive = function(v, colorInt) {
// 这段代码的主要内容/用途:递归设置面板内所有 TextView 的文字颜色(标题/按钮文字/描述/设置项等)。
try {
if (!v) return;
if (v instanceof android.widget.TextView) {
v.setTextColor(colorInt);
}
if (v instanceof android.view.ViewGroup) {
var i, n = v.getChildCount();
for (i = 0; i < n; i++) {
this.applyTextColorRecursive(v.getChildAt(i), colorInt);
}
}
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
};
FloatBallAppWM.prototype.updatePanelBackground = function(panelView) {
// 这段代码的主要内容/用途:统一为"主面板/设置面板/查看器面板"应用背景与文字颜色(自动/亮/暗三档),并输出调试日志(命中哪个颜色)。
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setCornerRadius(this.dp(22));
var bgInt = this.getPanelBgColorInt();
bg.setColor(bgInt);
// 轻量描边:亮色时更明显,暗色时也保留一点边界(不提供自定义输入,避免设置页复杂化)
var sw = this.dp(1);
var isDark = this.isDarkTheme();
var outlineColor = this.ui.colors._monetOutline || (isDark ? android.graphics.Color.parseColor("#8E918F") : android.graphics.Color.parseColor("#747775"));
var stroke = this.withAlpha(outlineColor, isDark ? 0.26 : 0.20);
try { bg.setStroke(sw, stroke); } catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
panelView.setBackground(bg);
var tc = this.getPanelTextColorInt(bgInt);
try { themeBgInt = bgInt; themeTextInt = tc; } catch(eT) { safeLog(null, 'e', "catch " + String(eT)); }
this.applyTextColorRecursive(panelView, tc);
try { _th_log(this.L, "d", "[t]apply bg=" + _th_hex(bgInt) + " tx=" + _th_hex(tc)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try {
_th_log(this.L, "d",
"[theme:apply] isDark=" + isDark +
" bg=" + _th_hex(bgInt) + " " + _th_argb(bgInt) +
" text=" + _th_hex(tc) + " " + _th_argb(tc) +
" stroke=" + _th_hex(stroke)
);
} catch(eL0) { safeLog(null, 'e', "catch " + String(eL0)); }
} catch (e) {
try { _th_log(this.L, "e", "[theme:apply] err=" + String(e)); } catch(eL1) { safeLog(null, 'e', "catch " + String(eL1)); }
}
};

365
code/th_05_persistence.js Normal file
View File

@@ -0,0 +1,365 @@
// @version 1.0.0
// =======================【工具:面板位置持久化】======================
FloatBallAppWM.prototype.savePanelState = function(key, state) {
if (!key || !state) return;
try {
if (!this.config.PANEL_STATES) this.config.PANEL_STATES = {};
this.config.PANEL_STATES[key] = state;
// 节流或立即保存? 面板拖动结束通常不频繁,立即保存即可
// 但为了避免连续事件,还是可以复用 savePos 的节流逻辑,或者直接保存
ConfigManager.saveSettings(this.config);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
FloatBallAppWM.prototype.loadPanelState = function(key) {
if (!key || !this.config.PANEL_STATES) return null;
return this.config.PANEL_STATES[key];
};
// =======================【工具:位置持久化】======================
FloatBallAppWM.prototype.savePos = function(x, y) {
try {
var di = this.getDockInfo ? this.getDockInfo() : { ballSize: this.dp(this.config.BALL_SIZE_DP || 45) };
var sw = (this.state && this.state.screen) ? Number(this.state.screen.w || 0) : 0;
var sh = (this.state && this.state.screen) ? Number(this.state.screen.h || 0) : 0;
var ballSize = Number(di.ballSize || this.dp(this.config.BALL_SIZE_DP || 45));
var maxX = Math.max(0, sw - ballSize);
var maxY = Math.max(0, sh - ballSize);
var persistX = Math.floor(Number(x || 0));
var persistY = Math.floor(Number(y || 0));
var dockSide = "";
var docked = !!(this.state && this.state.docked);
// 吸边裁剪态下 Window x 可能是 screenW - visiblePx持久化时改存“完整球”的逻辑坐标。
if (docked) {
dockSide = String(this.state.dockSide || "");
if (dockSide === "right" && sw > 0) persistX = maxX;
else if (dockSide === "left") persistX = 0;
} else if (sw > 0) {
persistX = this.clamp(persistX, 0, maxX);
}
if (sh > 0) persistY = this.clamp(persistY, 0, maxY);
this.config.BALL_INIT_X = persistX;
this.config.BALL_INIT_Y_DP = Math.floor(persistY / this.state.density);
// 新增位置元数据:用于不同分辨率/横竖屏之间按比例或按吸边侧恢复,避免横屏落在屏幕中间。
this.config.BALL_POS_SCREEN_W = sw;
this.config.BALL_POS_SCREEN_H = sh;
this.config.BALL_POS_X_RATIO = maxX > 0 ? (persistX / maxX) : 0;
this.config.BALL_POS_Y_RATIO = maxY > 0 ? (persistY / maxY) : 0;
this.config.BALL_POS_DOCKED = docked;
this.config.BALL_POS_DOCK_SIDE = dockSide;
return ConfigManager.saveSettings(this.config);
} catch (e) { return false; }
};
FloatBallAppWM.prototype.loadSavedPos = function() {
var di = this.getDockInfo ? this.getDockInfo() : { ballSize: this.dp(this.config.BALL_SIZE_DP || 45) };
var ballSize = Number(di.ballSize || this.dp(this.config.BALL_SIZE_DP || 45));
var sw = (this.state && this.state.screen) ? Number(this.state.screen.w || 0) : 0;
var sh = (this.state && this.state.screen) ? Number(this.state.screen.h || 0) : 0;
var maxX = Math.max(0, sw - ballSize);
var maxY = Math.max(0, sh - ballSize);
var x = Number(this.config.BALL_INIT_X || 0);
var y = this.dp(Number(this.config.BALL_INIT_Y_DP || 100));
try {
var savedW = Number(this.config.BALL_POS_SCREEN_W || 0);
var savedH = Number(this.config.BALL_POS_SCREEN_H || 0);
var hasMeta = savedW > 0 && savedH > 0;
var docked = (typeof parseBooleanLike === "function") ? parseBooleanLike(this.config.BALL_POS_DOCKED, false) : !!this.config.BALL_POS_DOCKED;
var side = String(this.config.BALL_POS_DOCK_SIDE || "");
if (hasMeta && (savedW !== sw || savedH !== sh)) {
var xr = Number(this.config.BALL_POS_X_RATIO);
var yr = Number(this.config.BALL_POS_Y_RATIO);
if (isNaN(xr)) xr = 0;
if (isNaN(yr)) yr = 0;
x = Math.round(this.clamp(xr, 0, 1) * maxX);
y = Math.round(this.clamp(yr, 0, 1) * maxY);
}
if (docked || side === "left" || side === "right") {
if (side === "right") x = maxX;
else if (side === "left") x = 0;
} else if (!hasMeta && sw > sh && sw > 0) {
// 兼容旧版:只存了竖屏像素 x。横屏启动时旧的“右侧 x”会落在屏幕中部这里按短边推断并贴回右边。
var portraitMaxXGuess = Math.max(0, Math.min(sw, sh) - ballSize);
if (x > Math.round(portraitMaxXGuess * 0.55) && x < Math.round(maxX * 0.85)) {
x = maxX;
}
}
} catch (e) {}
x = this.clamp(Math.floor(x), 0, maxX);
y = this.clamp(Math.floor(y), 0, maxY);
return { x: x, y: y };
};
FloatBallAppWM.prototype.trySavePosThrottled = function(x, y) {
var t = this.now();
if (t - this.state.lastSaveTs < this.config.SAVE_THROTTLE_MS) return false;
this.state.lastSaveTs = t;
return this.savePos(x, y);
};
// =======================【工具:配置持久化】======================
FloatBallAppWM.prototype.saveConfig = function(obj) {
try {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
this.config[k] = obj[k];
}
}
if (this.L) this.L.updateConfig(this.config);
return ConfigManager.saveSettings(this.config);
} catch (e) { return false; }
};
// =======================【设置面板schema】======================
FloatBallAppWM.prototype.getConfigSchema = function() {
return ConfigManager.loadSchema();
};
// =======================【设置面板:临时编辑缓存】======================
FloatBallAppWM.prototype.beginEditConfig = function() {
try {
var schema = this.getConfigSchema();
var p = {};
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
p[k] = this.config[k];
}
this.state.pendingUserCfg = p;
this.state.pendingDirty = false;
return true;
} catch (e0) {
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
return false;
}
};
FloatBallAppWM.prototype.getPendingValue = function(k) {
if (this.state.pendingUserCfg && this.state.pendingUserCfg.hasOwnProperty(k)) return this.state.pendingUserCfg[k];
return this.config[k];
};
FloatBallAppWM.prototype.setPendingValue = function(k, v) {
if (!this.state.pendingUserCfg) this.beginEditConfig();
this.state.pendingUserCfg[k] = v;
this.state.pendingDirty = true;
if (this.state.previewMode) {
this.refreshPreview(k);
}
};
FloatBallAppWM.prototype.getEffectiveConfig = function() {
if (!this.state.previewMode || !this.state.pendingUserCfg) return this.config;
var cfg = {};
for (var k in this.config) { cfg[k] = this.config[k]; }
for (var k in this.state.pendingUserCfg) { cfg[k] = this.state.pendingUserCfg[k]; }
return cfg;
};
FloatBallAppWM.prototype.refreshPreview = function(changedKey) {
if (this.state.closing) return;
var self = this;
// Post to next tick to avoid destroying view during event dispatch (fixes crash on switch toggle)
if (this.state.h) {
this.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() { self._refreshPreviewInternal(changedKey); }
}));
} else {
self._refreshPreviewInternal(changedKey);
}
};
FloatBallAppWM.prototype._refreshPreviewInternal = function(changedKey) {
if (this.state.closing) return;
var originalConfig = this.config;
try {
// 使用临时配置
this.config = this.getEffectiveConfig();
var needBall = false;
var needPanel = false;
if (!changedKey) {
needBall = true;
needPanel = true;
} else {
// 根据修改的 key 判断需要刷新什么,避免全量刷新导致闪烁
if (changedKey.indexOf("BALL_") === 0) needBall = true;
if (changedKey.indexOf("PANEL_") === 0) needPanel = true;
// 球大小改变会影响面板位置
if (changedKey === "BALL_SIZE_DP") needPanel = true;
}
// 1. 刷新悬浮球 (保持面板不关闭)
if (needBall) {
this.rebuildBallForNewSize(true);
}
// 2. 刷新主面板预览
if (needPanel) {
// 如果当前没有显示主面板,则创建并显示;如果已显示,则替换
var panel = this.buildPanelView("main");
// 计算位置 (使用当前球的位置)
var maxH = Math.floor(this.state.screen.h * 0.75);
panel.measure(
android.view.View.MeasureSpec.makeMeasureSpec(this.state.screen.w, android.view.View.MeasureSpec.AT_MOST),
android.view.View.MeasureSpec.makeMeasureSpec(maxH, android.view.View.MeasureSpec.AT_MOST)
);
var pw = panel.getMeasuredWidth();
var ph = panel.getMeasuredHeight();
if (ph > maxH) ph = maxH;
var bx = this.state.ballLp.x;
var by = this.state.ballLp.y;
var px = this.computePanelX(bx, pw);
var py = by;
// 尝试调整 Y
var r = this.tryAdjustPanelY(px, py, pw, ph, bx, by);
var finalX = r.ok ? r.x : px;
var finalY = r.ok ? r.y : this.clamp(py, 0, this.state.screen.h - ph);
// 优化闪烁:先添加新面板,再移除旧面板 (这样新面板会在最上层,符合预览需求)
var oldPanel = this.state.panel;
var oldAdded = this.state.addedPanel;
// 添加新面板 (addPanel 会更新 this.state.panel)
// 注意addPanel 中已为 main 添加 FLAG_NOT_FOCUSABLE所以即使在最上层也不会抢走 Settings 的输入焦点
this.addPanel(panel, finalX, finalY, "main");
// 移除旧面板
if (oldAdded && oldPanel) {
try { this.state.wm.removeView(oldPanel); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}
} catch(e) {
safeLog(this.L, 'e', "refreshPreview err=" + e);
} finally {
this.config = originalConfig;
}
};
FloatBallAppWM.prototype.persistUserCfgFromObject = function(obj) {
// # 这段代码的主要内容/用途:从临时编辑对象里按 schema 白名单抽取并保存(跳过 section 标题等无 key 项)
try {
var schema = this.getConfigSchema();
var out = {};
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
out[k] = obj[k];
}
return this.saveConfig(out);
} catch (e0) { return false; }
};
FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
try {
if (k === "LOG_ENABLE") {
try {
if (this.L) {
this.L.enable = !!this.config.LOG_ENABLE;
this.L.cfg.LOG_ENABLE = !!this.config.LOG_ENABLE;
this.L.i("apply LOG_ENABLE=" + String(this.config.LOG_ENABLE));
}
} catch(eLE) { safeLog(null, 'e', "catch " + String(eLE)); }
return;
}
if (k === "LOG_DEBUG") {
try {
if (this.L) {
this.L.debug = !!this.config.LOG_DEBUG;
this.L.cfg.LOG_DEBUG = !!this.config.LOG_DEBUG;
this.L.i("apply LOG_DEBUG=" + String(this.config.LOG_DEBUG));
}
} catch(eLD) { safeLog(null, 'e', "catch " + String(eLD)); }
return;
}
if (k === "LOG_KEEP_DAYS") {
try {
var n = Math.max(1, Math.floor(Number(this.config.LOG_KEEP_DAYS || 3)));
this.config.LOG_KEEP_DAYS = n;
if (this.L) {
this.L.keepDays = n;
this.L.cfg.LOG_KEEP_DAYS = n;
this.L.i("apply LOG_KEEP_DAYS=" + String(n));
this.L.cleanupOldFiles();
}
} catch(eLK) { safeLog(null, 'e', "catch " + String(eLK)); }
return;
}
if (k === "BALL_SIZE_DP" || k === "BALL_PNG_MODE" || k === "BALL_ICON_TYPE" || k === "BALL_ICON_FILE_PATH" || k === "BALL_ICON_RES_ID" || k === "BALL_ICON_RES_NAME" || k === "BALL_ICON_SIZE_DP" || k === "BALL_ICON_TINT_HEX") { this.rebuildBallForNewSize(); return; }
if (k === "PANEL_ROWS" || k === "PANEL_COLS" ||
k === "PANEL_ITEM_SIZE_DP" || k === "PANEL_GAP_DP" ||
k === "PANEL_PADDING_DP" || k === "PANEL_ICON_SIZE_DP" ||
k === "PANEL_LABEL_ENABLED" || k === "PANEL_LABEL_TEXT_SIZE_SP" ||
k === "PANEL_LABEL_TOP_MARGIN_DP") {
if (this.state.addedPanel) this.hideMainPanel();
if (this.state.addedSettings) this.hideSettingsPanel();
if (this.state.addedViewer) this.hideViewerPanel();
return;
}
if (k === "EDGE_VISIBLE_RATIO") {
if (this.state.addedBall && this.state.docked) {
this.state.docked = false;
this.snapToEdgeDocked(false);
}
return;
}
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
};
FloatBallAppWM.prototype.commitPendingUserCfg = function() {
try {
if (!this.state.pendingUserCfg) return { ok: false, reason: "no_pending" };
var schema = this.getConfigSchema();
var changedKeys = [];
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
var oldV = this.config[k];
var newV = this.state.pendingUserCfg[k];
if (String(oldV) !== String(newV)) {
this.config[k] = newV;
changedKeys.push(k);
}
}
this.persistUserCfgFromObject(this.state.pendingUserCfg);
var j;
for (j = 0; j < changedKeys.length; j++) {
if (changedKeys[j] === "BALL_SIZE_DP") { this.applyImmediateEffectsForKey("BALL_SIZE_DP"); break; }
}
for (j = 0; j < changedKeys.length; j++) {
if (changedKeys[j] !== "BALL_SIZE_DP") this.applyImmediateEffectsForKey(changedKeys[j]);
}
this.state.pendingDirty = false;
safeLog(this.L, 'i', "commit settings changed=" + JSON.stringify(changedKeys));
return { ok: true, changed: changedKeys };
} catch (e0) {
safeLog(this.L, 'e', "commitPendingUserCfg err=" + String(e0));
return { ok: false, err: String(e0) };
}
};

532
code/th_06_icon_parser.js Normal file
View File

@@ -0,0 +1,532 @@
// @version 1.0.0
// =======================【工具:吸边数据】======================
FloatBallAppWM.prototype.getDockInfo = function() {
var ballSize = this.dp(this.config.BALL_SIZE_DP);
var visible = Math.max(1, Math.round(ballSize * this.config.EDGE_VISIBLE_RATIO));
var hidden = ballSize - visible;
return { ballSize: ballSize, visiblePx: visible, hiddenPx: hidden };
};
// =======================【工具:图标解析】======================
// =======================【工具:快捷方式图标文件路径】======================
FloatBallAppWM.prototype.getShortcutIconFilePath = function(pkg, shortcutId, userId) {
// # 主要用途:把快捷方式图标持久化到 shortcut_icons 目录,供按钮页/按钮管理页稳定显示(桌面移除后仍可显示)
try {
var p = (pkg == null) ? "" : String(pkg);
var s = (shortcutId == null) ? "" : String(shortcutId);
var u = (userId == null) ? "0" : String(userId);
// # 文件名去非法字符,避免路径注入或创建失败
function _sn(v) {
try {
var t = String(v == null ? "" : v);
t = t.replace(/[^a-zA-Z0-9._-]+/g, "_");
if (t.length > 120) t = t.substring(0, 120);
return t;
} catch(e) { return ""; }
}
var dir = String(APP_ROOT_DIR) + "/shortcut_icons";
var fn = _sn(p) + "__" + _sn(s) + "__u" + _sn(u) + ".png";
return dir + "/" + fn;
} catch (e0) {
return "";
}
};
FloatBallAppWM.prototype.resolveIconDrawable = function(btn) {
// # 主要用途:解析面板按钮图标(优先 app 包名图标,其次自定义 resId最后兜底
try {
if (!btn) return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
// # 0) 优先检查 iconPath (绝对路径)
// # 引用优化:复用 loadBallIconDrawableFromFile 安全加载逻辑
if (btn.iconPath) {
try {
var path = String(btn.iconPath);
if (path) {
// targetPx: 面板图标大小; Limit: 1MB, 1024px
var sizeDp = this.config.PANEL_ICON_SIZE_DP || 32;
var dr = this.loadBallIconDrawableFromFile(path, this.dp(sizeDp), 1048576, 1024);
if (dr) return dr;
}
} catch(ePath) { safeLog(null, 'e', "catch " + String(ePath)); }
}
// # 1) type=app 且配置了 pkg自动取应用图标
try {
var t = (btn.type == null) ? "" : String(btn.type);
if (t === "app") {
var pkg = (btn.pkg == null) ? "" : String(btn.pkg);
if (pkg.length > 0) {
// # 统一 LRU 缓存:避免频繁走 PackageManagerDrawable 可复用);并带容量上限,防止无限增长
var kApp = "app|" + pkg;
var hitApp = this._iconLruGet(kApp);
if (hitApp) return hitApp;
var pm = context.getPackageManager();
var drApp = pm.getApplicationIcon(pkg);
if (drApp != null) {
this._iconLruPut(kApp, drApp);
return drApp;
}
}
}
} catch(eApp) { safeLog(null, 'e', "catch " + String(eApp)); }
// # 1.5) type=shortcut尝试取 Shortcuts 快捷方式图标(显示与 shortcuts.js 页面一致)
// # 说明btn 需要包含 pkg + shortcutId图标获取可能较重因此做简单缓存失败则回退到应用图标。
try {
var t2 = (btn.type == null) ? "" : String(btn.type);
if (t2 === "shortcut") {
var pkg2 = (btn.pkg == null) ? "" : String(btn.pkg);
var sid2 = (btn.shortcutId == null) ? "" : String(btn.shortcutId);
if (pkg2.length > 0 && sid2.length > 0) {
// # 1.5.1) 优先从 shortcut_icons 持久化目录读取(桌面移除后仍可显示正确图标)
try {
var iconFilePath0 = this.getShortcutIconFilePath(pkg2, sid2, (btn.userId != null ? String(btn.userId) : "0"));
if (iconFilePath0) {
try {
var f0 = new java.io.File(iconFilePath0);
if (f0.exists() && f0.isFile()) {
var sizeDp0 = this.config.PANEL_ICON_SIZE_DP || 32;
var iconSizePx0 = this.dp(sizeDp0);
var dr0 = this.loadBallIconDrawableFromFile(iconFilePath0, iconSizePx0, 1048576, 1024);
if (dr0) {
// # 写入统一 LRU 缓存:同一个 shortcut 复用 Drawable避免反复解码 PNG
try {
var sk0 = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
this._iconLruPut("sc|" + sk0, dr0);
} catch(eSc0) { safeLog(null, 'e', "catch " + String(eSc0)); }
return dr0;
}
}
} catch(eF0) { safeLog(null, 'e', "catch " + String(eF0)); }
}
} catch(eFile0) { safeLog(null, 'e', "catch " + String(eFile0)); }
var skey = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
var kSc = "sc|" + skey;
var hitSc = this._iconLruGet(kSc);
if (hitSc) return hitSc;
// # 失败冷却:某些 ROM/桌面在短时间内反复查询 Shortcuts 会很慢或直接抛异常。
// # 这里对同一个 shortcut key 做短暂冷却(默认 10 秒),避免面板频繁刷新导致卡顿/ANR 风险。
if (!this._shortcutIconFailTs) this._shortcutIconFailTs = {};
var nowTs = 0;
try { nowTs = new Date().getTime(); } catch(eNow) { nowTs = 0; }
var lastFailTs = this._shortcutIconFailTs[skey];
if (lastFailTs && nowTs > 0 && (nowTs - lastFailTs) < 10000) {
// # 冷却期内直接跳过 shortcut icon 查询,走回退逻辑(应用图标)
} else {
var la = context.getSystemService(android.content.Context.LAUNCHER_APPS_SERVICE);
if (la) {
// # 修复:按钮管理页/按钮面板中,微信小程序等 shortcut 图标显示成宿主 App 图标
// # 根因(工作假设):用 setShortcutIds() 精确查询时,部分 ROM 的实现会返回"退化"的 ShortcutInfo
// # getShortcutIconDrawable 结果被降级为宿主 App 图标。
// # 方案:完全复用 shortcuts.js 的做法--先按 package + flags 拉取该包的 shortcuts 列表,再在列表里按 id 过滤命中项取图标。
// # 注意:不依赖外部 buildShortcutItemsIndex(),避免主程序环境下未加载 shortcuts.js 导致直接回退。
// # 方案:完全复用"选择快捷方式列表"的取数路径,确保微信小程序等 pinned shortcut 能拿到正确的 ShortcutInfo
// # 1) 优先直连 shortcut service选择器同款信息更完整
// # 2) 失败再回退 LauncherApps.getShortcuts
var list = null;
// # 1) shortcut service 直连(与选择器保持一致)
try {
var userIdIntForSvc = 0;
try {
var buid2 = null;
if (btn.userId != null) buid2 = btn.userId;
if (buid2 == null && btn.user != null) buid2 = btn.user;
if (buid2 != null) {
var tmpUid2 = parseInt(String(buid2), 10);
if (!isNaN(tmpUid2)) userIdIntForSvc = tmpUid2;
}
} catch(eUidSvc) { safeLog(null, 'e', "catch " + String(eUidSvc)); }
var sm2 = android.os.ServiceManager;
var shortcutSvc = null;
try { shortcutSvc = sm2.getService("shortcut"); } catch(eSm2) { shortcutSvc = null; }
if (shortcutSvc) {
var CFG_MATCH_ALL2 = 0x0000000F;
var slice2 = null;
try { slice2 = shortcutSvc.getShortcuts(String(pkg2), CFG_MATCH_ALL2, userIdIntForSvc); } catch(eS0b) { slice2 = null; }
if (slice2) {
var listObj2 = null;
try { listObj2 = slice2.getList(); } catch(eS1b) { listObj2 = null; }
if (listObj2) list = listObj2;
}
}
} catch(eSvc2) { safeLog(null, 'e', "catch " + String(eSvc2)); }
// # 2) LauncherApps 回退(当 shortcut service 不可用或返回空时)
if (list == null) {
var q = new android.content.pm.LauncherApps.ShortcutQuery();
try { q.setPackage(pkg2); } catch(eSP) { safeLog(null, 'e', "catch " + String(eSP)); }
// # 重要:必须设置 QueryFlags否则 getShortcuts 可能返回空(默认 flags=0
// # 兼容性:不同 Android/ROM 可能缺少部分 FLAG逐个 try 叠加
try {
var qFlags = 0;
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; } catch(eF1) { safeLog(null, 'e', "catch " + String(eF1)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; } catch(eF2) { safeLog(null, 'e', "catch " + String(eF2)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; } catch(eF3) { safeLog(null, 'e', "catch " + String(eF3)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; } catch(eF4) { safeLog(null, 'e', "catch " + String(eF4)); }
try { q.setQueryFlags(qFlags); } catch(eSF) { safeLog(null, 'e', "catch " + String(eSF)); }
} catch(eQF) { safeLog(null, 'e', "catch " + String(eQF)); }
// # 重要:用户句柄优先用按钮携带的 userId如有否则使用当前用户
var uh = android.os.Process.myUserHandle();
try {
var buid = null;
if (btn.userId != null) buid = btn.userId;
if (buid == null && btn.user != null) buid = btn.user;
if (buid != null) {
var uidInt = parseInt(String(buid), 10);
if (!isNaN(uidInt)) {
uh = android.os.UserHandle.of(uidInt);
}
}
} catch(eUH) { safeLog(null, 'e', "catch " + String(eUH)); }
try { list = la.getShortcuts(q, uh); } catch(eGS) { list = null; }
}
if (list && list.size && list.size() > 0) {
// # 在返回列表中按 shortcutId 精确命中
var si = null;
try {
for (var kk = 0; kk < list.size(); kk++) {
var s0 = list.get(kk);
if (s0 != null) {
var id0 = "";
try { id0 = String(s0.getId()); } catch(eId0) { id0 = ""; }
if (id0 === sid2) { si = s0; break; }
}
}
} catch(eFind) { si = null; }
if (si != null) {
// # 与 shortcuts.js 一致:优先 la.getShortcutIconDrawable(shortcutInfo, 0),再兜底 Icon.loadDrawable
var drSc = null;
try { drSc = la.getShortcutIconDrawable(si, 0); } catch(eIcon0) { drSc = null; }
if (drSc == null) {
try {
var ic = si.getIcon();
if (ic != null) {
var d2 = ic.loadDrawable(context);
if (d2 != null) drSc = d2;
}
} catch(eIcon1) { safeLog(null, 'e', "catch " + String(eIcon1)); }
}
if (drSc != null) {
try { this._iconLruPut("sc|" + skey, drSc); } catch(eSc1) { safeLog(null, 'e', "catch " + String(eSc1)); }
return drSc;
}
}
}
}
// # 如果没拿到 shortcut 图标,记录一次失败时间,触发冷却
try {
if (nowTs > 0) this._shortcutIconFailTs[skey] = nowTs;
else this._shortcutIconFailTs[skey] = new Date().getTime();
} catch(eFT) { safeLog(null, 'e', "catch " + String(eFT)); }
}
// # 回退:取应用图标,至少保证按钮有图标可见
try {
var pm2 = context.getPackageManager();
var drApp2 = pm2.getApplicationIcon(pkg2);
if (drApp2 != null) return drApp2;
} catch(eFall) { safeLog(null, 'e', "catch " + String(eFall)); }
}
}
} catch(eSc) { safeLog(null, 'e', "catch " + String(eSc)); }
// # 2) 显式指定 iconResName (String) 或 iconResId (int)
try {
if (btn.iconResName) {
var drShortx = this.resolveShortXDrawable(btn.iconResName, btn && btn.iconTint ? String(btn.iconTint) : "");
if (drShortx != null) return drShortx;
var name = this.normalizeShortXIconName(btn.iconResName, true);
// # 回退到 android 系统图标
var id = context.getResources().getIdentifier(name, "drawable", "android");
if (id > 0) return context.getResources().getDrawable(id, null);
}
if (btn.iconResId) return context.getResources().getDrawable(btn.iconResId, null);
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
// # 3) 兜底
return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return null;
};
FloatBallAppWM.prototype.getShortXResHandle = function() {
if (this._shortxResHandle) return this._shortxResHandle;
try {
var flags = 0;
try { flags = android.content.Context.CONTEXT_INCLUDE_CODE | android.content.Context.CONTEXT_IGNORE_SECURITY; } catch (eF) { flags = android.content.Context.CONTEXT_RESTRICTED; }
var sxCtx = context.createPackageContext(CONST_SHORTX_PACKAGE, flags);
this._shortxResHandle = {
ctx: sxCtx,
res: sxCtx.getResources(),
cl: sxCtx.getClassLoader(),
pkg: CONST_SHORTX_PACKAGE
};
return this._shortxResHandle;
} catch (e) {
safeLog(this.L, 'w', "getShortXResHandle failed: " + String(e));
return null;
}
};
FloatBallAppWM.prototype.normalizeShortXIconName = function(name, keepPrefix) {
try {
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
if (!s) return "";
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
if (s.indexOf(".") > 0) {
var parts = s.split(".");
s = parts[parts.length - 1];
}
if (s.indexOf("ic_remix_") === 0) {
return keepPrefix ? s : s.substring("ic_remix_".length);
}
if (s.indexOf("ic_") === 0) {
return s;
}
return keepPrefix ? ("ic_remix_" + s) : s;
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return "";
};
FloatBallAppWM.prototype.getShortXApkPaths = function() {
var out = [];
try {
var handle = this.getShortXResHandle();
var ai = null;
try { if (handle && handle.ctx) ai = handle.ctx.getApplicationInfo(); } catch (eAi0) { ai = null; }
if (ai == null) {
try { ai = context.getPackageManager().getApplicationInfo(CONST_SHORTX_PACKAGE, 0); } catch (eAi1) { ai = null; }
}
function pushPath(p) {
try {
p = String(p || "");
if (!p) return;
if (out.indexOf(p) < 0) out.push(p);
} catch(eP) { safeLog(null, 'e', "catch " + String(eP)); }
}
if (ai) {
try { pushPath(ai.sourceDir); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
try { pushPath(ai.publicSourceDir); } catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
try {
var ss = ai.splitSourceDirs;
if (ss) {
var i;
for (i = 0; i < ss.length; i++) pushPath(ss[i]);
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return out;
};
FloatBallAppWM.prototype.scanShortXIconsFromApk = function() {
var out = [];
var seen = {};
var paths = this.getShortXApkPaths();
// 宽松匹配:匹配 res/drawable* 和 res/mipmap* 下所有以 ic_ 开头的图标
var regex = /^res\/(drawable[^\/]*|mipmap[^\/]*)\/(ic_[a-z0-9_]+)\.(xml|png|webp|jpg|jpeg)$/;
var lastErr = "";
var totalFiles = 0;
var pi;
for (pi = 0; pi < paths.length; pi++) {
var zip = null;
try {
zip = new java.util.zip.ZipFile(String(paths[pi]));
var en = zip.entries();
while (en.hasMoreElements()) {
var ze = en.nextElement();
var name = String(ze.getName());
totalFiles++;
var m = regex.exec(name);
if (!m) continue;
var fullName = String(m[2]);
// 过滤掉系统图标
if (fullName.indexOf("ic_launcher") === 0 || fullName.indexOf("ic_menu_") === 0) continue;
if (seen[fullName]) continue;
seen[fullName] = true;
out.push({
name: fullName,
shortName: this.normalizeShortXIconName(fullName, false),
id: 0
});
}
} catch (eZip) {
lastErr = String(eZip);
} finally {
try { if (zip) zip.close(); } catch(eClose) { safeLog(null, 'e', "catch " + String(eClose)); }
}
}
if ((!out || out.length === 0) && lastErr) this._shortxIconCatalogError = "APK扫描: " + lastErr + " (路径数=" + paths.length + ", 文件数=" + totalFiles + ")";
return out;
};
FloatBallAppWM.prototype.getShortXIconLookupNames = function(name) {
var out = [];
try {
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
if (!s) return out;
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
if (s.indexOf(".") > 0) {
var parts = s.split(".");
s = parts[parts.length - 1];
}
function add(v) {
if (!v) return;
if (out.indexOf(v) < 0) out.push(v);
}
add(s);
if (s.indexOf("ic_remix_") === 0) {
add(s.substring("ic_remix_".length));
} else if (s.indexOf("ic_") !== 0) {
add("ic_remix_" + s);
add("ic_" + s);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return out;
};
FloatBallAppWM.prototype.resolveShortXDrawableResId = function(name) {
try {
var handle = this.getShortXResHandle();
if (!handle || !handle.res) return 0;
var cands = this.getShortXIconLookupNames(name);
var i;
for (i = 0; i < cands.length; i++) {
var resId = 0;
try { resId = handle.res.getIdentifier(String(cands[i]), "drawable", handle.pkg); } catch (e1) { resId = 0; }
if (resId > 0) return resId;
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return 0;
};
FloatBallAppWM.prototype.resolveShortXDrawable = function(name, tintHex) {
try {
var handle = this.getShortXResHandle();
if (!handle || !handle.res) return null;
var resId = this.resolveShortXDrawableResId(name);
if (resId <= 0) return null;
var dr = handle.res.getDrawable(resId, null);
if (dr && tintHex) {
try {
dr = dr.mutate();
dr.setColorFilter(android.graphics.Color.parseColor(String(tintHex)), android.graphics.PorterDuff.Mode.SRC_IN);
} catch(eTint) { safeLog(null, 'e', "catch " + String(eTint)); }
}
return dr;
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return null;
};
FloatBallAppWM.prototype.getShortXIconDrawable = function(name) {
try {
return this.resolveShortXDrawable(name, null);
} catch (e) {
safeLog(this.L, 'w', "getShortXIconDrawable failed: " + String(e));
return null;
}
};
FloatBallAppWM.prototype.getShortXIconCatalog = function(forceReload) {
if (!forceReload && this._shortxIconCatalog) return this._shortxIconCatalog;
var out = [];
this._shortxIconCatalogError = "";
try {
var handle = this.getShortXResHandle();
if (handle && handle.cl) {
// 策略1反射 R$drawable 类(未混淆时可用)
try {
var clz = handle.cl.loadClass(CONST_SHORTX_PACKAGE + ".R$drawable");
var fields = clz.getFields();
var i;
for (i = 0; i < fields.length; i++) {
try {
var f = fields[i];
var fname = String(f.getName());
if (fname.indexOf("ic_remix_") !== 0 && fname.indexOf("ic_") !== 0) continue;
out.push({
name: fname,
shortName: (fname.indexOf("ic_remix_") === 0) ? fname.substring("ic_remix_".length) : fname,
id: f.getInt(null)
});
} catch(eField) { safeLog(null, 'e', "catch " + String(eField)); }
}
} catch (eClz) {
this._shortxIconCatalogError = "R$drawable reflect: " + String(eClz);
safeLog(this.L, 'w', "getShortXIconCatalog R$drawable failed: " + String(eClz));
}
// 策略2直接遍历资源 ID绕过混淆更稳定
if (!out || out.length === 0) {
try {
var ctx = context.createPackageContext(CONST_SHORTX_PACKAGE, android.content.Context.CONTEXT_IGNORE_SECURITY);
var res = ctx.getResources();
var startId = 2131230000;
var endId = 2131240000;
var count = 0;
for (var id = startId; id < endId && count < 20000; id++) {
try {
var rname = res.getResourceName(id);
if (rname && rname.indexOf("/ic_remix_") > 0) {
var parts = rname.split("/");
var resName = parts[parts.length - 1];
out.push({
name: resName,
shortName: resName.substring("ic_remix_".length),
id: id
});
count++;
}
} catch (eId) {
// ID 无效,跳过
}
}
} catch (eRes) {
this._shortxIconCatalogError = (this._shortxIconCatalogError ? this._shortxIconCatalogError + "; " : "") + "res scan: " + String(eRes);
safeLog(this.L, 'w', "getShortXIconCatalog res scan failed: " + String(eRes));
}
}
}
// 策略3APK 文件扫描(最后兜底)
if (!out || out.length === 0) {
out = this.scanShortXIconsFromApk();
}
out.sort(function(a, b) {
var aw = a.name.indexOf("ic_remix_") === 0 ? 1 : 0;
var bw = b.name.indexOf("ic_remix_") === 0 ? 1 : 0;
if (aw !== bw) return aw - bw;
var as = String(a.shortName || a.name);
var bs = String(b.shortName || b.name);
return as < bs ? -1 : (as > bs ? 1 : 0);
});
if (!out || out.length === 0) {
if (!this._shortxIconCatalogError) this._shortxIconCatalogError = "所有策略均未获取到图标";
} else {
this._shortxIconCatalogError = "";
}
} catch (e) {
this._shortxIconCatalogError = String(e);
safeLog(this.L, 'w', "getShortXIconCatalog failed: " + String(e));
}
this._shortxIconCatalog = out;
return out;
};

1855
code/th_07_shortcut.js Normal file

File diff suppressed because it is too large Load Diff

209
code/th_08_content.js Normal file
View File

@@ -0,0 +1,209 @@
// @version 1.0.0
// =======================【Content解析 settings URI】======================
// 这段代码的主要内容/用途:识别 content://settings/(system|secure|global)/KEY 并用 Settings.* get/put 更稳
FloatBallAppWM.prototype.parseSettingsUri = function(uriStr) {
try {
var s = String(uriStr || "");
if (s.indexOf("content://settings/") !== 0) return null;
// content://settings/system/accelerometer_rotation
var rest = s.substring("content://settings/".length);
var parts = rest.split("/");
if (!parts || parts.length < 1) return null;
var table = String(parts[0] || "");
var key = "";
if (parts.length >= 2) key = String(parts[1] || "");
if (table !== "system" && table !== "secure" && table !== "global") return null;
return { table: table, key: key };
} catch (e) { return null; }
};
FloatBallAppWM.prototype.settingsGetStringByTable = function(table, key) {
try {
var cr = context.getContentResolver();
if (table === "system") return android.provider.Settings.System.getString(cr, String(key));
if (table === "secure") return android.provider.Settings.Secure.getString(cr, String(key));
if (table === "global") return android.provider.Settings.Global.getString(cr, String(key));
return null;
} catch (e) { return null; }
};
FloatBallAppWM.prototype.settingsPutStringByTable = function(table, key, value) {
try {
var cr = context.getContentResolver();
if (table === "system") return android.provider.Settings.System.putString(cr, String(key), String(value));
if (table === "secure") return android.provider.Settings.Secure.putString(cr, String(key), String(value));
if (table === "global") return android.provider.Settings.Global.putString(cr, String(key), String(value));
return false;
} catch (e) { return false; }
};
// =======================【Content通用 query】======================
// 这段代码的主要内容/用途ContentResolver.query 并把 Cursor 转成文本(用于查看器面板)
FloatBallAppWM.prototype.contentQueryToText = function(uriStr, projection, selection, selectionArgs, sortOrder, maxRows) {
var out = { ok: false, uri: String(uriStr || ""), rows: 0, text: "", err: "" };
var cr = null;
var cur = null;
try {
cr = context.getContentResolver();
var uri = android.net.Uri.parse(String(uriStr));
var projArr = null;
if (projection && projection.length) {
projArr = java.lang.reflect.Array.newInstance(java.lang.String, projection.length);
var i0;
for (i0 = 0; i0 < projection.length; i0++) projArr[i0] = String(projection[i0]);
}
var sel = (selection === undefined || selection === null) ? null : String(selection);
var selArgsArr = null;
if (selectionArgs && selectionArgs.length) {
selArgsArr = java.lang.reflect.Array.newInstance(java.lang.String, selectionArgs.length);
var i1;
for (i1 = 0; i1 < selectionArgs.length; i1++) selArgsArr[i1] = String(selectionArgs[i1]);
}
var so = (sortOrder === undefined || sortOrder === null) ? null : String(sortOrder);
cur = cr.query(uri, projArr, sel, selArgsArr, so);
if (!cur) {
out.err = "query return null cursor";
return out;
}
var colCount = cur.getColumnCount();
var cols = [];
var ci;
for (ci = 0; ci < colCount; ci++) cols.push(String(cur.getColumnName(ci)));
var sb = [];
sb.push("URI: " + String(uriStr));
sb.push("Columns(" + String(colCount) + "): " + cols.join(", "));
sb.push("");
var limit = Math.max(1, Math.floor(Number(maxRows || this.config.CONTENT_MAX_ROWS || 20)));
var row = 0;
while (cur.moveToNext()) {
row++;
sb.push("#" + String(row));
var cj;
for (cj = 0; cj < colCount; cj++) {
var v = "";
try {
if (cur.isNull(cj)) v = "null";
else v = String(cur.getString(cj));
} catch (eV) {
try { v = String(cur.getLong(cj)); } catch (eV2) { v = "<??>"; }
}
sb.push(" " + cols[cj] + " = " + v);
}
sb.push("");
if (row >= limit) break;
}
out.ok = true;
out.rows = row;
out.text = sb.join("\n");
return out;
} catch (e) {
out.err = String(e);
return out;
} finally {
try { if (cur) cur.close(); } catch(eC) { safeLog(null, 'e', "catch " + String(eC)); }
}
};
// =======================【Content统一入口】======================
// 这段代码的主要内容/用途:处理按钮里的 type:"content"
FloatBallAppWM.prototype.execContentAction = function(btn) {
var mode = btn.mode ? String(btn.mode) : ((btn.value !== undefined && btn.value !== null) ? "put" : "get");
var uri = btn.uri ? String(btn.uri) : "";
if (!uri) return { ok: false, err: "missing uri" };
// settings uri 优先走 Settings API
var su = this.parseSettingsUri(uri);
if (mode === "get") {
if (su && su.key) {
var v = this.settingsGetStringByTable(su.table, su.key);
return { ok: true, mode: "get", kind: "settings", table: su.table, key: su.key, value: (v === null ? "null" : String(v)) };
}
// 非 settings尝试 query 一行
var q1 = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, 1);
if (!q1.ok) return { ok: false, mode: "get", kind: "query", err: q1.err };
return { ok: true, mode: "get", kind: "query", text: q1.text, rows: q1.rows };
}
if (mode === "put") {
var val = (btn.value === undefined || btn.value === null) ? "" : String(btn.value);
if (su && su.key) {
var ok = this.settingsPutStringByTable(su.table, su.key, val);
return { ok: !!ok, mode: "put", kind: "settings", table: su.table, key: su.key, value: val };
}
// 非 settings尽力走 update(ContentValues)
try {
var cr = context.getContentResolver();
var u = android.net.Uri.parse(uri);
var cv = new android.content.ContentValues();
// 支持 btn.values = {col1: "...", col2: "..."};否则写 value 列
if (btn.values) {
var k;
for (k in btn.values) {
if (!btn.values.hasOwnProperty(k)) continue;
cv.put(String(k), String(btn.values[k]));
}
} else {
cv.put("value", val);
}
var where = (btn.selection === undefined || btn.selection === null) ? null : String(btn.selection);
var whereArgs = null;
if (btn.selectionArgs && btn.selectionArgs.length) {
whereArgs = java.lang.reflect.Array.newInstance(java.lang.String, btn.selectionArgs.length);
var i2;
for (i2 = 0; i2 < btn.selectionArgs.length; i2++) whereArgs[i2] = String(btn.selectionArgs[i2]);
}
var n = cr.update(u, cv, where, whereArgs);
return { ok: true, mode: "put", kind: "update", updated: Number(n) };
} catch (eU) {
return { ok: false, mode: "put", kind: "update", err: String(eU) };
}
}
if (mode === "query") {
var maxRows = (btn.maxRows === undefined || btn.maxRows === null) ? this.config.CONTENT_MAX_ROWS : Number(btn.maxRows);
var q = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, maxRows);
if (!q.ok) return { ok: false, mode: "query", err: q.err };
return { ok: true, mode: "query", rows: q.rows, text: q.text };
}
if (mode === "view") {
try {
var it = new android.content.Intent(android.content.Intent.ACTION_VIEW);
it.setData(android.net.Uri.parse(uri));
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(it);
return { ok: true, mode: "view" };
} catch (eV) {
return { ok: false, mode: "view", err: String(eV) };
}
}
return { ok: false, err: "unknown mode=" + mode };
};
/* =======================
下面开始WM 动画、面板、触摸、启动、输出
======================= */

758
code/th_09_animation.js Normal file
View File

@@ -0,0 +1,758 @@
// @version 1.0.0
FloatBallAppWM.prototype.animateBallLayout = function(toX, toY, toW, durMs, endCb) {
var st = this.state;
if (!st.addedBall || !st.ballRoot || !st.ballLp) { if (endCb) endCb(); return; }
var fromX = st.ballLp.x;
var fromY = st.ballLp.y;
var fromW = st.ballLp.width;
try {
var va = android.animation.ValueAnimator.ofFloat(0.0, 1.0);
va.setDuration(durMs);
try {
// 使用 OvershootInterpolator 产生轻微的回弹效果,更加生动
// 0.7 的张力适中,不会过于夸张
va.setInterpolator(new android.view.animation.OvershootInterpolator(0.7));
} catch (eI) {
try { va.setInterpolator(new android.view.animation.DecelerateInterpolator()); } catch(eI2) { safeLog(null, 'e', "catch " + String(eI2)); }
}
var self = this;
va.addUpdateListener(new android.animation.ValueAnimator.AnimatorUpdateListener({
onAnimationUpdate: function(anim) {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var f = anim.getAnimatedValue();
var nx = Math.round(fromX + (toX - fromX) * f);
var ny = Math.round(fromY + (toY - fromY) * f);
var nw = Math.round(fromW + (toW - fromW) * f);
// 性能优化:只有坐标真正变化时才请求 WindowManager 更新
if (nx !== self.state.ballLp.x || ny !== self.state.ballLp.y || nw !== self.state.ballLp.width) {
self.state.ballLp.x = nx;
self.state.ballLp.y = ny;
self.state.ballLp.width = nw;
// # 关键操作使用 safeOperation 封装
safeOperation("dockAnimation.updateViewLayout", function() {
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
}, true, self.L);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
va.addListener(new android.animation.Animator.AnimatorListener({
onAnimationStart: function() {},
onAnimationRepeat: function() {},
onAnimationCancel: function() {
try { self.state.ballAnimator = null; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
},
onAnimationEnd: function() {
try { self.state.ballAnimator = null; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try {
if (!self.state.closing && self.state.addedBall) {
self.state.ballLp.x = toX;
self.state.ballLp.y = toY;
self.state.ballLp.width = toW;
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
try { if (endCb) endCb(); } catch (eCb) { try { if (self && self.L && self.L.e) self.L.e("animateBallLayout endCb err=" + String(eCb)); } catch(eLog) { safeLog(null, 'e', "catch " + String(eLog)); } }
}
}));
this.state.ballAnimator = va;
va.start();
} catch (e0) {
try {
st.ballLp.x = toX;
st.ballLp.y = toY;
st.ballLp.width = toW;
st.wm.updateViewLayout(st.ballRoot, st.ballLp);
this.savePos(st.ballLp.x, st.ballLp.y);
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
try { if (endCb) endCb(); } catch (eCb2) { try { if (this && this.L && this.L.e) this.L.e("animateBallLayout endCb err=" + String(eCb2)); } catch(eLog2) { safeLog(null, 'e', "catch " + String(eLog2)); } }
}
};
FloatBallAppWM.prototype.playBounce = function(v) {
if (!this.config.ENABLE_BOUNCE) return;
if (!this.config.ENABLE_ANIMATIONS) return;
try { v.animate().cancel(); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
var self = this;
var i = 0;
function step() {
if (self.state.closing) return;
if (i >= self.config.BOUNCE_TIMES) {
try { v.setScaleX(1); v.setScaleY(1); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return;
}
var amp = (self.config.BOUNCE_MAX_SCALE - 1) * Math.pow(self.config.BOUNCE_DECAY, i);
var s = 1 + amp;
v.animate()
.scaleX(s)
.scaleY(s)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.OvershootInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() {
v.animate()
.scaleX(1)
.scaleY(1)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.AccelerateDecelerateInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() { i++; step(); }
}))
.start();
}
}))
.start();
}
step();
};
FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) {
try {
if (!v) return { ok: true, skipped: true };
this.state.wm.removeView(v);
return { ok: true };
} catch (e) {
safeLog(this.L, 'w', "removeView fail which=" + String(whichName || "") + " err=" + String(e));
return { ok: false, err: String(e), where: whichName || "" };
}
};
FloatBallAppWM.prototype.hideMask = function() {
if (!this.state.addedMask) return;
if (!this.state.mask) return;
this.safeRemoveView(this.state.mask, "mask");
this.state.mask = null;
this.state.maskLp = null;
this.state.addedMask = false;
};
FloatBallAppWM.prototype.hideMainPanel = function() {
if (!this.state.addedPanel) return;
if (!this.state.panel) return;
this.safeRemoveView(this.state.panel, "panel");
this.state.panel = null;
this.state.panelLp = null;
this.state.addedPanel = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideMainPanel");
};
FloatBallAppWM.prototype.hideSettingsPanel = function() {
if (!this.state.addedSettings) return;
if (!this.state.settingsPanel) return;
this.safeRemoveView(this.state.settingsPanel, "settingsPanel");
this.state.settingsPanel = null;
this.state.settingsPanelLp = null;
this.state.addedSettings = false;
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideSettingsPanel");
};
FloatBallAppWM.prototype.hideViewerPanel = function() {
if (!this.state.addedViewer) return;
if (!this.state.viewerPanel) return;
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
this.state.viewerPanel = null;
this.state.viewerPanelLp = null;
this.state.viewerPanelType = null;
this.state.addedViewer = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideViewerPanel");
};
FloatBallAppWM.prototype.handlePanelBack = function(which, reason) {
// 这段代码的主要内容/用途:适配全面屏系统返回手势/返回键,让 ToolHub 设置类 UI 能按“上一级 -> 关闭”退出。
try {
if (this.state.closing) return false;
var w = which ? String(which) : "";
if (!w && this.state.addedViewer) w = String(this.state.viewerPanelType || "viewer");
if (!w && this.state.addedSettings) w = "settings";
if (!w && this.state.addedPanel) w = "main";
if (this.state.addedViewer) {
var vt = String(this.state.viewerPanelType || w || "viewer");
if (vt === "btn_editor") {
if (this.state.editingButtonIndex !== null && this.state.editingButtonIndex !== undefined) {
this.state.editingButtonIndex = null;
this.state.keepBtnEditorState = true;
this.showPanelAvoidBall("btn_editor");
return true;
}
this.hideViewerPanel();
this.showPanelAvoidBall("settings");
return true;
}
if (vt === "schema_editor") {
if (this.state.editingSchemaIndex !== null && this.state.editingSchemaIndex !== undefined) {
this.state.editingSchemaIndex = null;
this.state.keepSchemaEditorState = true;
this.showPanelAvoidBall("schema_editor");
return true;
}
this.hideViewerPanel();
this.showPanelAvoidBall("settings");
return true;
}
this.hideViewerPanel();
return true;
}
if (this.state.addedSettings) {
this.state.previewMode = false;
if (this.state.addedPanel) this.hideMainPanel();
this.hideSettingsPanel();
return true;
}
if (this.state.addedPanel) {
this.hideMainPanel();
return true;
}
} catch (e) {
safeLog(this.L, 'e', "handlePanelBack fail reason=" + String(reason || "") + " err=" + String(e));
}
return false;
};
FloatBallAppWM.prototype.handleSystemUiDismiss = function(reason) {
// 这段代码的主要内容/用途:系统 Home/最近任务手势发生时关闭 ToolHub 面板,只保留悬浮球,避免 overlay 残留在桌面/多任务上。
try {
var r = String(reason || "");
if (r === "homekey" || r === "recentapps" || r === "fs_gesture" || r === "gestureNav") {
this.hideAllPanels();
return true;
}
} catch (e) {
safeLog(this.L, 'e', "handleSystemUiDismiss fail: " + String(e));
}
return false;
};
FloatBallAppWM.prototype.attachPanelSystemKeyHandler = function(panel, which) {
try {
if (!panel) return;
var self = this;
panel.setFocusable(true);
panel.setFocusableInTouchMode(true);
panel.setOnKeyListener(new android.view.View.OnKeyListener({
onKey: function(v, keyCode, event) {
try {
if (!event) return false;
if (event.getAction() !== android.view.KeyEvent.ACTION_UP) return false;
if (keyCode === android.view.KeyEvent.KEYCODE_BACK) return self.handlePanelBack(which, "back_key");
if (keyCode === android.view.KeyEvent.KEYCODE_ESCAPE) return self.handlePanelBack(which, "escape_key");
} catch (e) { safeLog(self.L, 'e', "panel key handler fail: " + String(e)); }
return false;
}
}));
panel.post(new java.lang.Runnable({ run: function() { try { panel.requestFocus(); } catch(eFocus) {} } }));
} catch (e) {
safeLog(this.L, 'e', "attachPanelSystemKeyHandler fail which=" + String(which || "") + " err=" + String(e));
}
};
FloatBallAppWM.prototype.clearHeavyCaches = function(reason) {
// 这段代码的主要内容/用途:在所有面板都关闭后,主动清理"图标/快捷方式"等重缓存,降低 system_server 常驻内存。
// 说明:仅清理缓存引用,不强行 recycle Bitmap避免误伤仍被使用的 Drawable。
// # 防抖5秒内相同 reason 不重复清理
var now = Date.now();
var cacheKey = "_lastClear_" + (reason || "default");
var lastClear = this.state[cacheKey] || 0;
if (now - lastClear < 5000) {
return; // 5秒内已清理过跳过
}
this.state[cacheKey] = now;
try { this._iconLru = null; } catch(eLruClr) { safeLog(null, 'e', "catch " + String(eLruClr)); }
try { this._shortcutIconFailTs = {}; } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
// # Shortcuts 相关全局缓存(按钮编辑页/快捷方式选择器可能会创建)
try { if (typeof __scIconCache !== "undefined") __scIconCache = {}; } catch(e3) { safeLog(null, 'e', "catch " + String(e3)); }
try { if (typeof __scAppLabelCache !== "undefined") __scAppLabelCache = {}; } catch(e4) { safeLog(null, 'e', "catch " + String(e4)); }
// # 记录一次清理日志(精简:只记录关键 reason且 5秒防抖
var keyReasons = ["memory_pressure", "screen_changed", "close"];
var isKeyReason = keyReasons.indexOf(reason) >= 0;
try {
if (isKeyReason && this.L && this.L.i) {
this.L.i("clearHeavyCaches reason=" + String(reason));
}
} catch(e5) { safeLog(null, 'e', "catch " + String(e5)); }
};
FloatBallAppWM.prototype._clearHeavyCachesIfAllHidden = function(reason) {
// 这段代码的主要内容/用途:只在"主面板/设置/查看器"全部关闭后清理缓存,避免页面切换时反复重建导致卡顿。
try {
if (!this.state.addedPanel && !this.state.addedSettings && !this.state.addedViewer) {
this.clearHeavyCaches(reason || "all_hidden");
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
FloatBallAppWM.prototype.hideAllPanels = function() {
this.hideMainPanel();
this.hideSettingsPanel();
this.hideViewerPanel();
this.hideMask();
this._clearHeavyCachesIfAllHidden("hideAllPanels");
};
FloatBallAppWM.prototype.showMask = function() {
if (this.state.addedMask) return;
if (this.state.closing) return;
var self = this;
var mask = new android.widget.FrameLayout(context);
// 遮罩层背景:轻微的黑色半透明,提升层次感
try { mask.setBackgroundColor(android.graphics.Color.parseColor("#33000000")); } catch (e0) {
mask.setBackgroundColor(0x33000000);
}
mask.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) {
var a = e.getAction();
if (a === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
self.hideAllPanels();
return true;
}
return true;
}
}));
var lp = new android.view.WindowManager.LayoutParams(
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = 0;
lp.y = 0;
try {
this.state.wm.addView(mask, lp);
this.state.mask = mask;
this.state.maskLp = lp;
this.state.addedMask = true;
// 简单的淡入动画
try {
if (this.config.ENABLE_ANIMATIONS) {
mask.setAlpha(0);
mask.animate().alpha(1).setDuration(200).start();
} else {
mask.setAlpha(1);
}
} catch(eAnim) { safeLog(null, 'e', "catch " + String(eAnim)); }
} catch (e1) {
safeLog(this.L, 'e', "add mask fail err=" + String(e1));
this.state.addedMask = false;
}
};
FloatBallAppWM.prototype.snapToEdgeDocked = function(withAnim, forceSide) {
if (this.state.closing) return;
// 移除对面板/Mask的检查允许在任何情况下强制吸边如果调用方逻辑正确
// 如果需要保护,调用方自己判断
if (this.state.dragging) return;
var di = this.getDockInfo();
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var snapLeft;
if (forceSide === "left") snapLeft = true;
else if (forceSide === "right") snapLeft = false;
else {
// 默认根据中心点判断
var centerX = this.state.ballLp.x + Math.round(ballSize / 2);
snapLeft = centerX < Math.round(this.state.screen.w / 2);
}
var targetW = visible;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
if (snapLeft) {
this.state.dockSide = "left";
this.state.docked = true;
try { this.state.ballContent.setX(-hidden); } catch(eL) { safeLog(null, 'e', "catch " + String(eL)); }
if (withAnim) {
this.animateBallLayout(0, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU1) { safeLog(null, 'e', "catch " + String(eU1)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
safeLog(this.L, 'd', "dock left x=0 y=" + String(targetY) + " w=" + String(targetW));
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
return;
}
this.state.dockSide = "right";
this.state.docked = true;
try { this.state.ballContent.setX(0); } catch(eR) { safeLog(null, 'e', "catch " + String(eR)); }
var x2 = this.state.screen.w - visible;
if (withAnim) {
this.animateBallLayout(x2, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = x2;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU2) { safeLog(null, 'e', "catch " + String(eU2)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
// # 日志精简dock 事件添加防抖10秒内不重复记录相同边
var dockNow = Date.now();
var lastDock = this.state._lastDockLog || 0;
if (dockNow - lastDock > 10000) {
safeLog(this.L, 'i', "dock right x=" + String(x2) + " y=" + String(targetY));
this.state._lastDockLog = dockNow;
}
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
};
FloatBallAppWM.prototype.undockToFull = function(withAnim, endCb) {
if (this.state.closing) { if (endCb) endCb(); return; }
if (!this.state.docked) { if (endCb) endCb(); return; }
if (!this.state.addedBall) { if (endCb) endCb(); return; }
var di = this.getDockInfo();
var ballSize = di.ballSize;
var targetW = ballSize;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
try { this.state.ballContent.setX(0); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
if (this.state.dockSide === "left") {
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(0, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU1) { safeLog(null, 'e', "catch " + String(eU1)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
safeLog(this.L, 'i', "undock from left");
return;
}
var x = this.state.screen.w - ballSize;
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(x, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = x;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU2) { safeLog(null, 'e', "catch " + String(eU2)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
// # 日志精简undock 事件改为 INFO 级别,且记录方向
var undockSide = this.state.dockSide || "right";
safeLog(this.L, 'i', "undock from " + undockSide);
};
FloatBallAppWM.prototype.cancelDockTimer = function() {
try { if (this.state.idleDockRunnable && this.state.h) this.state.h.removeCallbacks(this.state.idleDockRunnable); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
this.state.idleDockRunnable = null;
};
FloatBallAppWM.prototype.armDockTimer = function() {
if (this.state.closing) return;
if (!this.state.h) return;
if (!this.state.addedBall) return;
if (this.state.docked) return;
this.cancelDockTimer();
var hasPanel = (this.state.addedPanel || this.state.addedSettings || this.state.addedViewer || this.state.addedMask);
var targetMs = hasPanel ? this.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : this.config.DOCK_AFTER_IDLE_MS;
var self = this;
this.state.idleDockRunnable = new java.lang.Runnable({
run: function() {
try {
if (self.state.closing) return;
if (self.state.docked) return;
if (self.state.dragging) return;
var hasPanel2 = (self.state.addedPanel || self.state.addedSettings || self.state.addedViewer || self.state.addedMask);
var needMs = hasPanel2 ? self.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : self.config.DOCK_AFTER_IDLE_MS;
var idle = self.now() - self.state.lastMotionTs;
if (idle < needMs) { self.armDockTimer(); return; }
// if (hasPanel2) self.hideAllPanels(); // 用户要求不再自动关闭面板
if (self.config.ENABLE_SNAP_TO_EDGE) {
self.snapToEdgeDocked(true);
}
} catch (e0) {
if (self.L) self.L.e("dockTimer run err=" + String(e0));
}
}
});
this.state.h.postDelayed(this.state.idleDockRunnable, targetMs);
};
FloatBallAppWM.prototype.touchActivity = function() {
this.state.lastMotionTs = this.now();
this.armDockTimer();
}
// # 点击防抖与安全执行
// 这段代码的主要内容/用途:防止在悬浮面板上快速/乱点导致重复 add/remove、状态机被打穿从而引发 system_server 异常重启。
FloatBallAppWM.prototype.guardClick = function(key, cooldownMs, fn) {
try {
var now = android.os.SystemClock.uptimeMillis();
if (!this.state._clickGuards) this.state._clickGuards = {};
var last = this.state._clickGuards[key] || 0;
var cd = (cooldownMs != null ? cooldownMs : INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS);
if (now - last < cd) return false;
this.state._clickGuards[key] = now;
try {
fn && fn();
} catch (e1) {
safeLog(this.L, 'e', "guardClick err key=" + String(key) + " err=" + String(e1));
}
return true;
} catch (e0) {
// 兜底:绝不让点击回调异常冒泡到 system_server
try { fn && fn(); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return true;
}
};
FloatBallAppWM.prototype.safeUiCall = function(tag, fn) {
try {
fn && fn();
} catch (e) {
safeLog(this.L, 'e', "safeUiCall err tag=" + String(tag || "") + " err=" + String(e));
}
};
;
FloatBallAppWM.prototype.onScreenChangedReflow = function() {
if (this.state.closing) return;
if (!this.state.addedBall) return;
var di = this.getDockInfo();
var oldW = this.state.screen.w;
var oldH = this.state.screen.h;
var newScreen = this.getScreenSizePx();
var newW = newScreen.w;
var newH = newScreen.h;
if (newW <= 0 || newH <= 0) return;
if (oldW <= 0) oldW = newW;
if (oldH <= 0) oldH = newH;
this.state.screen = { w: newW, h: newH };
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var oldMaxX = Math.max(1, oldW - ballSize);
var oldMaxY = Math.max(1, oldH - ballSize);
var newMaxX = Math.max(1, newW - ballSize);
var newMaxY = Math.max(1, newH - ballSize);
var xRatio = this.state.ballLp.x / oldMaxX;
var yRatio = this.state.ballLp.y / oldMaxY;
var mappedX = Math.round(xRatio * newMaxX);
var mappedY = Math.round(yRatio * newMaxY);
mappedX = this.clamp(mappedX, 0, newMaxX);
mappedY = this.clamp(mappedY, 0, newMaxY);
if (this.state.docked) {
this.state.ballLp.y = mappedY;
this.state.ballLp.width = visible;
if (this.state.dockSide === "left") {
this.state.ballLp.x = 0;
try { this.state.ballContent.setX(-hidden); } catch(eL) { safeLog(null, 'e', "catch " + String(eL)); }
} else {
this.state.ballLp.x = newW - visible;
try { this.state.ballContent.setX(0); } catch(eR) { safeLog(null, 'e', "catch " + String(eR)); }
}
// 重新进入闲置变暗逻辑(如果需要)
try { this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA); } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
} else {
this.state.ballLp.x = mappedX;
this.state.ballLp.y = mappedY;
this.state.ballLp.width = ballSize;
try { this.state.ballContent.setX(0); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
try { this.state.ballContent.setAlpha(1.0); } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
}
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU) { safeLog(null, 'e', "catch " + String(eU)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
safeLog(this.L, 'i', "screen reflow w=" + String(newW) + " h=" + String(newH) + " x=" + String(this.state.ballLp.x) + " y=" + String(this.state.ballLp.y));
};
FloatBallAppWM.prototype.setupDisplayMonitor = function() {
if (this.state.closing) return;
try {
var dm = context.getSystemService(android.content.Context.DISPLAY_SERVICE);
if (!dm) return;
this.state.dm = dm;
this.state.lastRotation = this.getRotation();
var self = this;
var listener = new JavaAdapter(android.hardware.display.DisplayManager.DisplayListener, {
onDisplayAdded: function(displayId) {},
onDisplayRemoved: function(displayId) {},
onDisplayChanged: function(displayId) {
try {
if (self.state.closing) return;
var nowTs = self.now();
if (nowTs - self.state.lastMonitorTs < self.config.SCREEN_MONITOR_THROTTLE_MS) return;
self.state.lastMonitorTs = nowTs;
self.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var rot = self.getRotation();
var sz = self.getScreenSizePx();
var changed = false;
if (rot !== self.state.lastRotation) { self.state.lastRotation = rot; changed = true; }
if (sz.w !== self.state.screen.w || sz.h !== self.state.screen.h) changed = true;
if (changed) {
self.cancelDockTimer();
self.onScreenChangedReflow();
self.touchActivity();
}
} catch (e1) {
if (self.L) self.L.e("displayChanged run err=" + String(e1));
}
}
}));
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
}
});
this.state.displayListener = listener;
dm.registerDisplayListener(listener, this.state.h);
safeLog(this.L, 'i', "display monitor registered");
} catch (e2) {
safeLog(this.L, 'e', "setupDisplayMonitor err=" + String(e2));
}
};
FloatBallAppWM.prototype.stopDisplayMonitor = function() {
try { if (this.state.dm && this.state.displayListener) this.state.dm.unregisterDisplayListener(this.state.displayListener); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
this.state.displayListener = null;
this.state.dm = null;
};

28
code/th_10_shell.js Normal file
View File

@@ -0,0 +1,28 @@
// @version 1.0.1
// =======================【Shell广播桥执行】======================
// 仅通过广播桥发送 shell 命令,由外部接收器实际执行。
// 注意system_server 进程本身不直接执行 shell。
FloatBallAppWM.prototype.execShellSmart = function(cmdB64, needRoot) {
var ret = { ok: false, via: "", err: "" };
try {
var action = String(this.config.SHELL_BRIDGE_ACTION || CONST_SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
var it = new android.content.Intent(action);
// 广播协议cmd_b64 + root + from
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_CMD, String(cmdB64));
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_ROOT, !!needRoot);
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_FROM, "ToolHub");
context.sendBroadcast(it);
ret.ok = true;
ret.via = "BroadcastBridge";
safeLog(this.L, 'i', "shell via broadcast ok action=" + action + " root=" + String(!!needRoot));
} catch (eB) {
ret.err = "Broadcast err=" + String(eB);
safeLog(this.L, 'e', "shell via broadcast fail err=" + String(eB));
}
return ret;
};

320
code/th_11_action.js Normal file
View File

@@ -0,0 +1,320 @@
// @version 1.0.1
// =======================【WM 线程:按钮动作执行】======================
FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
// # 点击防抖
// 这段代码的主要内容/用途:防止在按钮面板上连续/乱点导致重复执行与 UI 状态机冲突(可能触发 system_server 异常重启)。
if (!this.guardClick("btn_exec_" + String(idx), 380, null)) return;
try {
if (!btn || !btn.type) {
this.toast("按钮#" + idx + " 未配置");
safeLog(this.L, 'w', "btn#" + String(idx) + " no type");
return;
}
var t = String(btn.type);
safeLog(this.L, 'i', "btn click idx=" + String(idx) + " type=" + t + " title=" + String(btn.title || ""));
if (t === "open_settings") {
this.showPanelAvoidBall("settings");
return;
}
if (t === "open_viewer") {
var logPath = (this.L && this.L._filePathForToday) ? this.L._filePathForToday() : "";
if (!logPath) logPath = PATH_LOG_DIR + "/ShortX_ToolHub_" + (new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date())) + ".log";
var content = FileIO.readText(logPath);
if (!content) content = "(日志文件不存在或为空: " + logPath + ")";
if (content.length > 30000) {
content = "[...前略...]\n" + content.substring(content.length - 30000);
}
// 简单的按行倒序,方便查看最新日志
try {
var lines = content.split("\n");
if (lines.length > 1) {
content = lines.reverse().join("\n");
}
} catch(eRev) { safeLog(null, 'e', "catch " + String(eRev)); }
this.showViewerPanel("今日日志 (倒序)", content);
return;
}
if (t === "toast") {
var msg = "";
if (btn.text !== undefined && btn.text !== null) msg = String(btn.text);
else if (btn.title) msg = String(btn.title);
else msg = "按钮#" + idx;
this.toast(msg);
return;
}
if (t === "app") {
var pkg = btn.pkg ? String(btn.pkg) : "";
if (!pkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
var it = context.getPackageManager().getLaunchIntentForPackage(pkg);
if (!it) { this.toast("无法启动 " + pkg); return; }
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
// # 系统级跨用户启动Context.startActivityAsUser
// 这段代码的主要内容/用途:支持"主应用/分身应用"选择,避免弹出选择器或误启动到另一用户。
// 说明:当未配置 launchUserId 时,默认使用 0主用户失败则回退 startActivity。
var launchUid = 0;
try {
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) launchUid = parseInt(String(btn.launchUserId), 10);
} catch(eLU0) { launchUid = 0; }
if (isNaN(launchUid)) launchUid = 0;
try {
// 运行日志:记录跨用户启动参数(便于定位分身启动失败原因)
safeLog(this.L, 'i', "startAsUser(app) idx=" + idx + " pkg=" + pkg + " launchUserId=" + launchUid);
if (launchUid !== 0) {
context.startActivityAsUser(it, android.os.UserHandle.of(launchUid));
} else {
context.startActivity(it);
}
} catch (eA) {
// # 兜底:某些 ROM/权限限制下 startActivityAsUser 可能抛异常,回退普通启动
try { context.startActivity(it); } catch(eA2) { safeLog(null, 'e', "catch " + String(eA2)); }
this.toast("启动失败");
safeLog(this.L, 'e', "start app fail pkg=" + pkg + " uid=" + String(launchUid) + " err=" + String(eA));
}
return;
}
if (t === "shell") {
// # 这段代码的主要内容/用途:执行 shell支持 cmd 明文 与 cmd_b64最终会确保发送/执行的是"真正的 base64"
// # 修复点:历史配置里有些按钮把"明文命令"误存进 cmd_b64或 b64 被破坏),会导致广播接收端解码失败→看起来"没效果"。
var cmdB64 = (btn.cmd_b64 !== undefined && btn.cmd_b64 !== null) ? String(btn.cmd_b64) : "";
var cmdPlain = (btn.cmd !== undefined && btn.cmd !== null) ? String(btn.cmd) : "";
// # 1) 只有明文但没有 b64自动补齐 b64避免特殊字符在多层字符串传递中被破坏
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64x = encodeBase64Utf8(cmdPlain);
if (b64x && b64x.length > 0) cmdB64 = String(b64x);
} catch(eB64a) { safeLog(null, 'e', "catch " + String(eB64a)); }
}
// # 2) cmd_b64 非空但无法解码:把它当作"明文命令"重新编码(保证广播桥/Action 都能吃到正确命令)
// # 说明decodeBase64Utf8 返回空串通常意味着 b64 非法或被破坏;而真实命令不太可能是空串。
if (cmdB64 && cmdB64.length > 0) {
try {
var testPlain = decodeBase64Utf8(cmdB64);
if ((!testPlain || testPlain.length === 0) && (!cmdPlain || cmdPlain.length === 0)) {
cmdPlain = String(cmdB64);
cmdB64 = "";
}
} catch(eB64b) { safeLog(null, 'e', "catch " + String(eB64b)); }
}
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64y = encodeBase64Utf8(cmdPlain);
if (b64y && b64y.length > 0) cmdB64 = String(b64y);
} catch(eB64c) { safeLog(null, 'e', "catch " + String(eB64c)); }
}
if (!cmdB64 || cmdB64.length === 0) {
this.toast("按钮#" + idx + " 缺少 cmd/cmd_b64");
safeLog(this.L, 'e', "shell missing cmd idx=" + String(idx));
return;
}
// # 广播桥接收端默认以 root 执行,强制使用 root
var needRoot = true;
var r = this.execShellSmart(cmdB64, needRoot);
if (r && r.ok) return;
this.toast("shell 广播桥发送失败");
safeLog(this.L, 'e', "shell all failed cmd_b64=" + cmdB64 + " ret=" + JSON.stringify(r || {}));
return;
}
if (t === "broadcast") {
// 这段代码的主要内容/用途:发送自定义广播(兼容 btn.extra / btn.extras并对 Shell 广播桥shortx.toolhub.SHELL做额外兼容cmd/cmd_b64/root
var action = btn.action ? String(btn.action) : "";
if (!action) { this.toast("按钮#" + idx + " 缺少 action"); return; }
var it2 = new android.content.Intent(action);
// # 1) 兼容字段extra / extras两种都认
var ex = null;
try {
if (btn.extras) ex = btn.extras;
else if (btn.extra) ex = btn.extra;
} catch (eEx0) { ex = null; }
// # 2) 写入 extras支持 number / boolean / string其他类型一律转字符串
if (ex) {
try {
var k;
for (k in ex) {
if (!ex.hasOwnProperty(k)) continue;
var v = ex[k];
if (typeof v === "number") it2.putExtra(String(k), Number(v));
else if (typeof v === "boolean") it2.putExtra(String(k), !!v);
else it2.putExtra(String(k), String(v));
}
} catch(eE) { safeLog(null, 'e', "catch " + String(eE)); }
}
// # 3) 对"Shell 广播桥"做额外兼容:
// - 你可以在 cfg 里写 extra.cmd明文或 extra.cmd_b64Base64
// - 同时会补齐 root/from并且把 cmd 明文也塞一份,方便外部 MVEL 直接读取 cmd 进行验证
try {
var bridgeAction = String(this.config.SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
if (action === bridgeAction) {
var kCmdB64 = String(this.config.SHELL_BRIDGE_EXTRA_CMD || "cmd_b64");
var kFrom = String(this.config.SHELL_BRIDGE_EXTRA_FROM || "from");
var kRoot = String(this.config.SHELL_BRIDGE_EXTRA_ROOT || "root");
var cmdPlain = "";
var cmdB64 = "";
try { cmdB64 = String(it2.getStringExtra(kCmdB64) || ""); } catch (eC0) { cmdB64 = ""; }
try { cmdPlain = String(it2.getStringExtra("cmd") || ""); } catch (eC1) { cmdPlain = ""; }
// # 有明文但没 b64自动补 b64
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64x = encodeBase64Utf8(cmdPlain);
if (b64x && b64x.length > 0) {
cmdB64 = b64x;
it2.putExtra(kCmdB64, String(cmdB64));
}
} catch(eC2) { safeLog(null, 'e', "catch " + String(eC2)); }
}
// # 有 b64 但没明文:也补一份明文(便于外部规则验证;真正执行仍建议用 cmd_b64
if ((!cmdPlain || cmdPlain.length === 0) && cmdB64 && cmdB64.length > 0) {
try {
var decoded = decodeBase64Utf8(cmdB64);
if (decoded && decoded.length > 0) {
cmdPlain = decoded;
it2.putExtra("cmd", String(cmdPlain));
}
} catch(eC3) { safeLog(null, 'e', "catch " + String(eC3)); }
}
// # root广播桥接收端默认以 root 执行,强制传递 true
try {
if (!it2.hasExtra(kRoot)) {
it2.putExtra(kRoot, true);
}
} catch (eR0) {
try {
it2.putExtra(kRoot, true);
} catch(eR1) { safeLog(null, 'e', "catch " + String(eR1)); }
}
// # root 类型纠正:如果外部 cfg 用了字符串 "true"/"false",这里纠正为 boolean避免外部 getBooleanExtra 读不到
try {
if (it2.hasExtra(kRoot)) {
var bdl = it2.getExtras();
if (bdl) {
var raw = bdl.get(kRoot);
if (raw != null) {
var rawStr = String(raw);
if (rawStr === "true" || rawStr === "false") {
it2.removeExtra(kRoot);
it2.putExtra(kRoot, rawStr === "true");
}
}
}
}
} catch(eRB) { safeLog(null, 'e', "catch " + String(eRB)); }
// # from标识来源便于外部执行器做白名单/审计)
try {
if (!it2.hasExtra(kFrom)) it2.putExtra(kFrom, "ToolHub@system_server");
} catch (eF0) { try { it2.putExtra(kFrom, "ToolHub@system_server"); } catch(eF1) { safeLog(null, 'e', "catch " + String(eF1)); } }
if (this.L) {
try {
this.L.i("broadcast(shell_bridge) action=" + action + " cmd_len=" + String(cmdPlain ? cmdPlain.length : 0) +
" cmd_b64_len=" + String(cmdB64 ? cmdB64.length : 0) + " root=" + String(it2.getBooleanExtra(kRoot, false)));
} catch(eLg) { safeLog(null, 'e', "catch " + String(eLg)); }
}
}
} catch(eSB) { safeLog(null, 'e', "catch " + String(eSB)); }
try { context.sendBroadcast(it2); } catch (eB) { this.toast("广播失败"); safeLog(this.L, 'e', "broadcast fail action=" + action + " err=" + String(eB)); }
return;
}
if (t === "shortcut") {
// 这段代码的主要内容/用途:仅使用 JavaScript(startActivityAsUser) 执行快捷方式,取消 Shell 与所有兜底,避免弹出主/分身选择器。
// 说明:
// 1) 运行时只执行按钮字段 shortcutJsCode由"选择快捷方式列表"点选自动生成,可手动微调)
// 2) 不再调用 am start不再回退 LauncherApps.startShortcut用户要求取消 shell、取消兜底
// 3) 目标 userIdlaunchUserId > userId用于锁定主/分身)
var spkg = btn.pkg ? String(btn.pkg) : "";
var sid = btn.shortcutId ? String(btn.shortcutId) : "";
var iu = (btn.intentUri != null) ? String(btn.intentUri) : "";
var uid = 0;
try { uid = (btn.userId != null) ? parseInt(String(btn.userId), 10) : 0; } catch(eUid0) { uid = 0; }
if (isNaN(uid)) uid = 0;
// # 启动 userId 优先级launchUserId > userId
try {
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) {
var lu0 = parseInt(String(btn.launchUserId), 10);
if (!isNaN(lu0)) uid = lu0;
}
} catch(eLu0) { safeLog(null, 'e', "catch " + String(eLu0)); }
if (!spkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
if (!sid) { this.toast("按钮#" + idx + " 缺少 shortcutId"); return; }
// # JavaScript 执行:只执行 shortcutJsCode
var jsCode = (btn.shortcutJsCode != null) ? String(btn.shortcutJsCode) : "";
if (!jsCode || jsCode.length === 0) {
this.toast("按钮#" + idx + " 未配置 JS 启动代码");
return;
}
try {
// # 提供少量上下文变量给脚本使用(可选)
// - __sc_intentUri: 当前按钮 intentUri
// - __sc_userId: 当前目标 userId已合并 launchUserId
var __sc_intentUri = iu;
var __sc_userId = uid;
var rjs = eval(jsCode);
// # 约定:返回值以 ok 开头视为成功;以 err 开头视为失败(失败也不兜底)
var sret = (rjs == null) ? "" : String(rjs);
if (sret.indexOf("ok") === 0) {
safeLog(this.L, 'i', "shortcut(js-only) ok pkg=" + spkg + " id=" + sid + " user=" + String(uid));
return;
}
safeLog(this.L, 'e', "shortcut(js-only) fail pkg=" + spkg + " id=" + sid + " user=" + String(uid) + " ret=" + sret);
this.toast("快捷方式 JS 启动失败: " + sret);
return;
} catch (eJsSc) {
safeLog(this.L, 'e', "shortcut(js-only) exception pkg=" + spkg + " id=" + sid + " err=" + eJsSc);
this.toast("快捷方式 JS 异常: " + String(eJsSc));
return;
}
}
this.toast("未知 type=" + t);
safeLog(this.L, 'w', "unknown btn type=" + t);
} catch (eBtn) {
try { this.toast("按钮执行异常"); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
safeLog(this.L, 'e', "execButtonAction crash idx=" + String(idx) + " err=" + String(eBtn));
}
};

76
code/th_12_rebuild.js Normal file
View File

@@ -0,0 +1,76 @@
// @version 1.0.0
// =======================【新增:改大小后安全重建悬浮球】======================
FloatBallAppWM.prototype.rebuildBallForNewSize = function(keepPanels) {
if (this.state.closing) return false;
if (!this.state.wm) return false;
if (!this.state.addedBall) return false;
if (!this.state.ballRoot) return false;
if (!this.state.ballLp) return false;
if (this.state.dragging) return false;
var oldSize = this.state.ballLp.height;
if (!oldSize || oldSize <= 0) oldSize = this.getDockInfo().ballSize;
var oldX = this.state.ballLp.x;
var oldY = this.state.ballLp.y;
var oldCenterX = oldX + Math.round(oldSize / 2);
var oldCenterY = oldY + Math.round(oldSize / 2);
if (!keepPanels) {
this.hideAllPanels();
}
this.cancelDockTimer();
this.state.docked = false;
this.state.dockSide = null;
this.safeRemoveView(this.state.ballRoot, "ballRoot-rebuild");
this.state.ballRoot = null;
this.state.ballContent = null;
this.state.ballLp = null;
this.state.addedBall = false;
this.createBallViews();
var di = this.getDockInfo();
var newSize = di.ballSize;
var newX = oldCenterX - Math.round(newSize / 2);
var newY = oldCenterY - Math.round(newSize / 2);
var maxX = Math.max(0, this.state.screen.w - newSize);
var maxY = Math.max(0, this.state.screen.h - newSize);
newX = this.clamp(newX, 0, maxX);
newY = this.clamp(newY, 0, maxY);
var lp = new android.view.WindowManager.LayoutParams(
newSize,
newSize,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = newX;
lp.y = newY;
try {
this.state.wm.addView(this.state.ballRoot, lp);
this.state.ballLp = lp;
this.state.addedBall = true;
} catch (eAdd) {
try { this.toast("重建悬浮球失败: " + String(eAdd)); } catch(eT) { safeLog(null, 'e', "catch " + String(eT)); }
safeLog(this.L, 'e', "rebuildBall add fail err=" + String(eAdd));
return false;
}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
this.touchActivity();
safeLog(this.L, 'i', "rebuildBall ok size=" + String(newSize) + " x=" + String(newX) + " y=" + String(newY));
return true;
};

528
code/th_13_panel_ui.js Normal file
View File

@@ -0,0 +1,528 @@
// @version 1.0.0
// =======================【设置面板UI右上角确认】======================
FloatBallAppWM.prototype.createSectionHeader = function(item, parent) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var color = C.primary;
var h = new android.widget.TextView(context);
h.setText(String(item.name || ""));
h.setTextColor(color);
h.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 15);
h.setTypeface(null, android.graphics.Typeface.BOLD);
h.setPadding(this.dp(16), this.dp(24), this.dp(16), this.dp(8));
parent.addView(h);
};
FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivider) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var textColor = isDark ? C.textPriDark : C.textPriLight;
var secColor = isDark ? C.textSecDark : C.textSecLight;
var dividerColor = isDark ? C.dividerDark : C.dividerLight;
var primary = C.primary;
var switchOff = isDark ? (0xFF555555 | 0) : (0xFFCCCCCC | 0);
// 增加内边距
var padH = this.dp(16);
var padV = this.dp(16);
// 分割线 (顶部)
if (needDivider) {
var line = new android.view.View(context);
var lineLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
1 // 1px
);
lineLp.setMargins(padH, 0, padH, 0);
line.setLayoutParams(lineLp);
line.setBackgroundColor(dividerColor);
parent.addView(line);
}
// 容器
var row = new android.widget.LinearLayout(context);
row.setOrientation(android.widget.LinearLayout.VERTICAL);
// 增加点击波纹反馈
try {
var outValue = new android.util.TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
row.setBackgroundResource(outValue.resourceId);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
row.setPadding(padH, padV, padH, padV);
var self = this;
if (item.type === "bool") {
// === 开关类型 ===
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setClickable(true);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
row.addView(tv);
var sw = new android.widget.Switch(context);
try { sw.setTextOn(""); sw.setTextOff(""); } catch(eT) { safeLog(null, 'e', "catch " + String(eT)); }
// 优化开关颜色
try {
var states = [
[android.R.attr.state_checked],
[-android.R.attr.state_checked]
];
var thumbColors = [primary, switchOff];
var trackColors = [self.withAlpha(primary, 0.5), self.withAlpha(switchOff, 0.5)];
var thumbList = new android.content.res.ColorStateList(states, thumbColors);
var trackList = new android.content.res.ColorStateList(states, trackColors);
sw.setThumbTintList(thumbList);
sw.setTrackTintList(trackList);
} catch(eColor) { safeLog(null, 'e', "catch " + String(eColor)); }
var cur = !!self.getPendingValue(item.key);
sw.setChecked(cur);
// 监听器
sw.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
onCheckedChanged: function(btn, checked) {
try {
self.touchActivity();
self.setPendingValue(item.key, !!checked);
if (self.L) self.L.d("pending " + String(item.key) + "=" + String(!!checked));
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
}
}));
// 点击行也触发开关
row.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
sw.setChecked(!sw.isChecked());
}
}));
row.addView(sw);
parent.addView(row);
} else if (item.type === "int" || item.type === "float") {
// === 数值类型 (SeekBar) ===
// 垂直布局:上面是 标题+数值,下面是 SeekBar
// 第一行:标题 + 数值
var line1 = new android.widget.LinearLayout(context);
line1.setOrientation(android.widget.LinearLayout.HORIZONTAL);
line1.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
line1.addView(tv);
var valTv = new android.widget.TextView(context);
valTv.setTextColor(primary);
valTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
valTv.setTypeface(null, android.graphics.Typeface.BOLD);
line1.addView(valTv);
row.addView(line1);
// 第二行SeekBar
var sb = new android.widget.SeekBar(context);
var sbLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
sbLp.topMargin = self.dp(16); // 增加间距
sb.setLayoutParams(sbLp);
// 优化 SeekBar 颜色
try {
sb.getThumb().setTint(primary);
sb.getProgressDrawable().setTint(primary);
} catch(eColor) { safeLog(null, 'e', "catch " + String(eColor)); }
// 配置 SeekBar
var min = (item.min !== undefined) ? Number(item.min) : 0;
var max = (item.max !== undefined) ? Number(item.max) : 100;
var step = (item.step !== undefined) ? Number(item.step) : 1;
var curV = Number(self.getPendingValue(item.key));
if (isNaN(curV)) curV = min;
curV = self.clamp(curV, min, max);
var maxP = Math.floor((max - min) / step);
if (maxP < 1) maxP = 1;
sb.setMax(maxP);
var curP = Math.floor((curV - min) / step);
if (curP < 0) curP = 0;
if (curP > maxP) curP = maxP;
sb.setProgress(curP);
function formatVal(v) {
if (item.type === "float") return String(Math.round(v * 1000) / 1000);
return String(Math.round(v));
}
function computeValByProgress(p) {
var v = min + p * step;
v = self.clamp(v, min, max);
if (item.type === "int") v = Math.round(v);
if (item.type === "float") v = Math.round(v * 1000) / 1000;
return v;
}
valTv.setText(formatVal(curV));
sb.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({
onProgressChanged: function(seek, progress, fromUser) {
try {
self.touchActivity();
var v = computeValByProgress(progress);
valTv.setText(formatVal(v));
if (fromUser) {
self.setPendingValue(item.key, v);
}
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
},
onStartTrackingTouch: function() { try { self.touchActivity(); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); } },
onStopTrackingTouch: function() { try { self.touchActivity(); } catch(e3) { safeLog(null, 'e', "catch " + String(e3)); } }
}));
row.addView(sb);
parent.addView(row);
} else if (item.type === "action") {
// === 动作按钮 ===
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setClickable(true);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
row.addView(tv);
// 样式化文本按钮
var btn = new android.widget.TextView(context);
btn.setText("打开");
btn.setTextColor(primary);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setTypeface(null, android.graphics.Typeface.BOLD);
btn.setGravity(android.view.Gravity.CENTER);
btn.setPadding(self.dp(16), self.dp(8), self.dp(16), self.dp(8));
// 透明波纹背景
btn.setBackground(self.ui.createTransparentRippleDrawable(primary, self.dp(16)));
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
self.touchActivity();
if (item.action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
row.addView(btn);
// 行点击也触发
row.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
self.touchActivity();
if (item.action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
parent.addView(row);
} else if (item.type === "text") {
// === 文本输入 ===
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var et = new android.widget.EditText(context);
var curVal = self.getPendingValue(item.key);
if (curVal === undefined || curVal === null) curVal = "";
et.setText(String(curVal));
et.setTextColor(textColor);
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setBackground(self.ui.createRoundDrawable(isDark ? C.inputBgDark : C.inputBgLight, self.dp(6)));
et.setPadding(self.dp(8), self.dp(8), self.dp(8), self.dp(8));
et.setSingleLine(true);
var etLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
etLp.topMargin = self.dp(8);
et.setLayoutParams(etLp);
// Explicitly request keyboard on click
et.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
v.requestFocus();
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(v, 0);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
et.addTextChangedListener(new android.text.TextWatcher({
beforeTextChanged: function(s, start, count, after) {},
onTextChanged: function(s, start, before, count) {},
afterTextChanged: function(s) {
try {
self.touchActivity();
self.setPendingValue(item.key, String(s));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
row.addView(et);
parent.addView(row);
} else if (item.type === "single_choice") {
// === 单选类型 (RadioGroup) ===
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var rg = new android.widget.RadioGroup(context);
rg.setOrientation(android.widget.RadioGroup.VERTICAL);
var rgLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
rgLp.topMargin = self.dp(8);
rg.setLayoutParams(rgLp);
var curVal = String(self.getPendingValue(item.key) || "");
if (!curVal) curVal = "auto"; // default
var options = item.options || [];
for (var i = 0; i < options.length; i++) {
(function(opt) {
var rb = new android.widget.RadioButton(context);
rb.setText(String(opt.label));
rb.setTextColor(textColor);
rb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
// 颜色优化
try {
var states = [[android.R.attr.state_checked], [-android.R.attr.state_checked]];
var colors = [primary, secColor];
rb.setButtonTintList(new android.content.res.ColorStateList(states, colors));
} catch(eC) { safeLog(null, 'e', "catch " + String(eC)); }
rb.setId(android.view.View.generateViewId ? android.view.View.generateViewId() : i);
// Check state
if (String(opt.value) === curVal) {
rb.setChecked(true);
}
rb.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
onCheckedChanged: function(btn, checked) {
if (checked) {
try {
self.touchActivity();
self.setPendingValue(item.key, String(opt.value));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}
}));
rg.addView(rb);
})(options[i]);
}
row.addView(rg);
parent.addView(row);
} else if (item.type === "ball_shortx_icon") {
// === 悬浮球 ShortX 图标选择器(复用按钮图标同款弹窗)===
row.setOrientation(android.widget.LinearLayout.VERTICAL);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var iconRow = new android.widget.LinearLayout(context);
iconRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
iconRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
iconRow.setPadding(0, self.dp(8), 0, 0);
var previewIv = new android.widget.ImageView(context);
var previewIvLp = new android.widget.LinearLayout.LayoutParams(self.dp(36), self.dp(36));
previewIvLp.rightMargin = self.dp(10);
previewIv.setLayoutParams(previewIvLp);
previewIv.setScaleType(android.widget.ImageView.ScaleType.FIT_CENTER);
iconRow.addView(previewIv);
var nameTv = new android.widget.TextView(context);
nameTv.setTextColor(secColor);
nameTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
var nameTvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1);
nameTv.setLayoutParams(nameTvLp);
iconRow.addView(nameTv);
function refreshBallShortXPreview() {
try {
var curIconName0 = String(self.getPendingValue(item.key) || "");
var curTint0 = String(self.getPendingValue("BALL_ICON_TINT_HEX") || "");
nameTv.setText(curIconName0 || "未选择");
if (curIconName0) {
var dr0 = self.resolveShortXDrawable(curIconName0, curTint0);
if (dr0) previewIv.setImageDrawable(dr0);
else previewIv.setImageDrawable(null);
} else {
previewIv.setImageDrawable(null);
}
} catch(ePreview0) { safeLog(null, 'e', "catch " + String(ePreview0)); }
}
refreshBallShortXPreview();
var btnPick = self.ui.createFlatButton(self, "选择图标", primary, function() {
self.touchActivity();
self.showShortXIconPickerPopup({
currentName: String(self.getPendingValue(item.key) || ""),
currentTint: String(self.getPendingValue("BALL_ICON_TINT_HEX") || ""),
onSelect: function(name) {
try {
var selectedName = String(name || "");
self.setPendingValue(item.key, selectedName);
self.setPendingValue("BALL_ICON_TYPE", "shortx");
refreshBallShortXPreview();
} catch(ePickBallIcon) {
safeLog(self.L, 'e', "ball shortx picker err=" + String(ePickBallIcon));
}
}
});
});
iconRow.addView(btnPick);
var gapView = new android.view.View(context);
gapView.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(8), 1));
iconRow.addView(gapView);
var btnClear = self.ui.createFlatButton(self, "清空", secColor, function() {
self.touchActivity();
try {
self.setPendingValue(item.key, "");
refreshBallShortXPreview();
} catch(eClearBallIcon) { safeLog(null, 'e', "catch " + String(eClearBallIcon)); }
});
iconRow.addView(btnClear);
row.addView(iconRow);
parent.addView(row);
} else if (item.type === "ball_color") {
// === 悬浮球图标颜色选择器(复用按钮图标同款弹窗)===
row.setOrientation(android.widget.LinearLayout.VERTICAL);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var colorRow = new android.widget.LinearLayout(context);
colorRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
colorRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
colorRow.setPadding(0, self.dp(8), 0, 0);
var colorDot = new android.view.View(context);
var colorDotLp = new android.widget.LinearLayout.LayoutParams(self.dp(28), self.dp(28));
colorDotLp.rightMargin = self.dp(10);
colorDot.setLayoutParams(colorDotLp);
colorRow.addView(colorDot);
var colorValueTv = new android.widget.TextView(context);
colorValueTv.setTextColor(secColor);
colorValueTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
var colorValueLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1);
colorValueTv.setLayoutParams(colorValueLp);
colorRow.addView(colorValueTv);
function refreshBallColorPreview() {
try {
var curHex0 = String(self.getPendingValue(item.key) || "");
colorValueTv.setText(curHex0 || "默认");
if (curHex0) {
colorDot.setBackground(self.ui.createRoundDrawable(android.graphics.Color.parseColor(curHex0), self.dp(14)));
} else {
colorDot.setBackground(self.ui.createRoundDrawable(0xFFCCCCCC | 0, self.dp(14)));
}
} catch(eDot0) {
try { colorDot.setBackground(self.ui.createRoundDrawable(0xFFCCCCCC | 0, self.dp(14))); } catch(eDot1) { safeLog(null, 'e', "catch " + String(eDot1)); }
colorValueTv.setText("默认");
}
}
refreshBallColorPreview();
var btnColor = self.ui.createFlatButton(self, "选择颜色", primary, function() {
self.touchActivity();
self.showColorPickerPopup({
currentColor: String(self.getPendingValue(item.key) || ""),
currentIconName: String(self.getPendingValue("BALL_ICON_RES_NAME") || ""),
onSelect: function(colorHex) {
try {
self.setPendingValue(item.key, String(colorHex || ""));
refreshBallColorPreview();
} catch(ePickBallColor) {
safeLog(self.L, 'e', "ball color picker err=" + String(ePickBallColor));
}
}
});
});
colorRow.addView(btnColor);
var gapColorView = new android.view.View(context);
gapColorView.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(8), 1));
colorRow.addView(gapColorView);
var btnClearColor = self.ui.createFlatButton(self, "清空", secColor, function() {
self.touchActivity();
try {
self.setPendingValue(item.key, "");
refreshBallColorPreview();
} catch(eClearBallColor) { safeLog(null, 'e', "catch " + String(eClearBallColor)); }
});
colorRow.addView(btnClearColor);
row.addView(colorRow);
parent.addView(row);
} else {
// 兜底文本
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(secColor);
row.addView(tv);
parent.addView(row);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
// @version 1.0.0
FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) { FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
var self = this; var self = this;
var isDark = this.isDarkTheme(); var isDark = this.isDarkTheme();
@@ -15,7 +16,7 @@ FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
bgDr.setColor(bgColor); bgDr.setColor(bgColor);
bgDr.setCornerRadius(this.dp(16)); bgDr.setCornerRadius(this.dp(16));
panel.setBackground(bgDr); panel.setBackground(bgDr);
try { panel.setElevation(this.dp(8)); } catch(e){} try { panel.setElevation(this.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
panel.setPadding( panel.setPadding(
this.dp(16), this.dp(16),
@@ -38,8 +39,8 @@ FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
panel.addView(sep); panel.addView(sep);
var scroll = new android.widget.ScrollView(context); var scroll = new android.widget.ScrollView(context);
try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch (eOS) {} try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch(eOS) { safeLog(null, 'e', "catch " + String(eOS)); }
try { scroll.setVerticalScrollBarEnabled(true); } catch (eSB) {} try { scroll.setVerticalScrollBarEnabled(true); } catch(eSB) { safeLog(null, 'e', "catch " + String(eSB)); }
// 给内容加一点边距 // 给内容加一点边距
var contentBox = new android.widget.LinearLayout(context); var contentBox = new android.widget.LinearLayout(context);
@@ -52,11 +53,11 @@ FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
tv.setTextColor(codeColor); tv.setTextColor(codeColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.CONTENT_VIEWER_TEXT_SP || 12)); tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.CONTENT_VIEWER_TEXT_SP || 12));
// 增加行距优化阅读 // 增加行距优化阅读
try { tv.setLineSpacing(this.dp(4), 1.0); } catch(eLS) {} try { tv.setLineSpacing(this.dp(4), 1.0); } catch(eLS) { safeLog(null, 'e', "catch " + String(eLS)); }
// 使用等宽字体显示代码/日志 // 使用等宽字体显示代码/日志
try { tv.setTypeface(android.graphics.Typeface.MONOSPACE); } catch(eTF) {} try { tv.setTypeface(android.graphics.Typeface.MONOSPACE); } catch(eTF) { safeLog(null, 'e', "catch " + String(eTF)); }
// WindowManager 环境下禁用文本选择,否则长按/选择会因缺少 Token 崩溃 // WindowManager 环境下禁用文本选择,否则长按/选择会因缺少 Token 崩溃
try { tv.setTextIsSelectable(false); } catch (eSel) {} try { tv.setTextIsSelectable(false); } catch(eSel) { safeLog(null, 'e', "catch " + String(eSel)); }
contentBox.addView(tv); contentBox.addView(tv);
@@ -95,7 +96,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
bgDr.setColor(this.withAlpha(bgColor, this.config.PANEL_BG_ALPHA)); bgDr.setColor(this.withAlpha(bgColor, this.config.PANEL_BG_ALPHA));
bgDr.setCornerRadius(this.dp(16)); bgDr.setCornerRadius(this.dp(16));
panel.setBackground(bgDr); panel.setBackground(bgDr);
try { panel.setElevation(this.dp(8)); } catch(e){} try { panel.setElevation(this.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var padDp = this.config.PANEL_PADDING_DP; var padDp = this.config.PANEL_PADDING_DP;
panel.setPadding( panel.setPadding(
@@ -148,9 +149,9 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
} }
var scroll = new android.widget.ScrollView(context); var scroll = new android.widget.ScrollView(context);
try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch (eOS) {} try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch(eOS) { safeLog(null, 'e', "catch " + String(eOS)); }
try { scroll.setVerticalScrollBarEnabled(false); } catch (eSB) {} try { scroll.setVerticalScrollBarEnabled(false); } catch(eSB) { safeLog(null, 'e', "catch " + String(eSB)); }
try { scroll.setFillViewport(true); } catch (eFV) {} try { scroll.setFillViewport(true); } catch(eFV) { safeLog(null, 'e', "catch " + String(eFV)); }
var scrollLp = new android.widget.LinearLayout.LayoutParams(contentW, scrollH); var scrollLp = new android.widget.LinearLayout.LayoutParams(contentW, scrollH);
scroll.setLayoutParams(scrollLp); scroll.setLayoutParams(scrollLp);
@@ -169,7 +170,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
// var totalCells = totalBtns > minCells ? totalBtns : minCells; // var totalCells = totalBtns > minCells ? totalBtns : minCells;
var rowCount = Math.ceil(totalCells / cols2); var rowCount = Math.ceil(totalCells / cols2);
try { grid.setRowCount(rowCount); } catch (eRC) {} try { grid.setRowCount(rowCount); } catch(eRC) { safeLog(null, 'e', "catch " + String(eRC)); }
grid.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, { grid.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) { self.touchActivity(); return false; } onTouch: function(v, e) { self.touchActivity(); return false; }
@@ -192,7 +193,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
// 单元格背景:如果是有功能的按钮,给个卡片背景;否则透明 // 单元格背景:如果是有功能的按钮,给个卡片背景;否则透明
if (btnCfg) { if (btnCfg) {
cell.setBackground(self.ui.createRoundDrawable(cardColor, self.dp(12))); cell.setBackground(self.ui.createRoundDrawable(cardColor, self.dp(12)));
try { cell.setElevation(self.dp(2)); } catch(e){} try { cell.setElevation(self.dp(2)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} else { } else {
// 空格子占位 // 空格子占位
} }
@@ -205,7 +206,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
// 但 resolveIconDrawable 逻辑比较复杂,这里暂时不强制染色,除非用户配置了 TINT // 但 resolveIconDrawable 逻辑比较复杂,这里暂时不强制染色,除非用户配置了 TINT
if (!isDark && btnCfg && !btnCfg.type && !btnCfg.pkg) { if (!isDark && btnCfg && !btnCfg.type && !btnCfg.pkg) {
// 简单的系统图标在亮色模式下可能看不清,染成深色 // 简单的系统图标在亮色模式下可能看不清,染成深色
try { iv.setColorFilter(C.textPriLight, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF){} try { iv.setColorFilter(C.textPriLight, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF) { safeLog(null, 'e', "catch " + String(eCF)); }
} }
} }
@@ -223,7 +224,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, this.config.PANEL_LABEL_TEXT_SIZE_SP); tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, this.config.PANEL_LABEL_TEXT_SIZE_SP);
tv.setTextColor(textColor); tv.setTextColor(textColor);
tv.setGravity(android.view.Gravity.CENTER); tv.setGravity(android.view.Gravity.CENTER);
try { tv.setLines(1); tv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eL){} try { tv.setLines(1); tv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eL) { safeLog(null, 'e', "catch " + String(eL)); }
var tvLp = new android.widget.LinearLayout.LayoutParams( var tvLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, // 宽度填满,方便居中 android.widget.LinearLayout.LayoutParams.MATCH_PARENT, // 宽度填满,方便居中
@@ -250,8 +251,8 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
})); }));
})(i, btnCfg); })(i, btnCfg);
} else { } else {
try { iv.setAlpha(0); } catch (eA0) {} try { iv.setAlpha(0); } catch(eA0) { safeLog(null, 'e', "catch " + String(eA0)); }
try { cell.setClickable(false); } catch (eC0) {} try { cell.setClickable(false); } catch(eC0) { safeLog(null, 'e', "catch " + String(eC0)); }
} }
grid.addView(cell); grid.addView(cell);
@@ -436,11 +437,15 @@ FloatBallAppWM.prototype.addPanel = function(panel, x, y, which) {
lp.x = x; lp.x = x;
lp.y = y; lp.y = y;
try { if (this.attachPanelSystemKeyHandler) this.attachPanelSystemKeyHandler(panel, which); } catch (eKeyAttach) { safeLog(this.L, 'e', "attach panel key fail which=" + String(which) + " err=" + String(eKeyAttach)); }
try { this.state.wm.addView(panel, lp); } catch (eAdd) { safeLog(this.L, 'e', "addPanel fail which=" + String(which) + " err=" + String(eAdd)); return; } try { this.state.wm.addView(panel, lp); } catch (eAdd) { safeLog(this.L, 'e', "addPanel fail which=" + String(which) + " err=" + String(eAdd)); return; }
if (which === "main") { this.state.panel = panel; this.state.panelLp = lp; this.state.addedPanel = true; } if (which === "main") { this.state.panel = panel; this.state.panelLp = lp; this.state.addedPanel = true; }
else if (which === "settings") { this.state.settingsPanel = panel; this.state.settingsPanelLp = lp; this.state.addedSettings = true; } else if (which === "settings") { this.state.settingsPanel = panel; this.state.settingsPanelLp = lp; this.state.addedSettings = true; }
else { this.state.viewerPanel = panel; this.state.viewerPanelLp = lp; this.state.addedViewer = true; } else { this.state.viewerPanel = panel; this.state.viewerPanelLp = lp; this.state.viewerPanelType = which; this.state.addedViewer = true; }
try { panel.requestFocus(); } catch (eReqFocus) {}
try { try {
if (this.config.ENABLE_ANIMATIONS) { if (this.config.ENABLE_ANIMATIONS) {
@@ -459,7 +464,7 @@ FloatBallAppWM.prototype.addPanel = function(panel, x, y, which) {
panel.setScaleY(1); panel.setScaleY(1);
panel.setAlpha(1); panel.setAlpha(1);
} }
} catch (eA) {} } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
// # 日志防抖5秒内相同面板类型不重复记录 // # 日志防抖5秒内相同面板类型不重复记录
var now = Date.now(); var now = Date.now();
@@ -515,8 +520,8 @@ FloatBallAppWM.prototype.showPanelAvoidBall = function(which) {
// 需要处理 newPanel 的 LayoutParams // 需要处理 newPanel 的 LayoutParams
var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0); var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0);
contentLp.weight = 1; contentLp.weight = 1;
try { newPanel.setBackground(null); } catch(e){} // 移除背景,使用 Container 背景 try { newPanel.setBackground(null); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } // 移除背景,使用 Container 背景
try { newPanel.setElevation(0); } catch(e){} try { newPanel.setElevation(0); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
newPanel.setLayoutParams(contentLp); newPanel.setLayoutParams(contentLp);
container.addView(newPanel); container.addView(newPanel);
@@ -650,7 +655,7 @@ FloatBallAppWM.prototype.showPanelAvoidBall = function(which) {
self.touchActivity(); self.touchActivity();
} catch (e) { } catch (e) {
if (self.L) self.L.e("showPanelAvoidBall callback err=" + String(e)); if (self.L) self.L.e("showPanelAvoidBall callback err=" + String(e));
try { self.toast("面板显示失败: " + String(e)); } catch (et) {} try { self.toast("面板显示失败: " + String(e)); } catch(et) { safeLog(null, 'e', "catch " + String(et)); }
} }
}; };
@@ -690,7 +695,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
bgDr.setColor(isDark ? C.bgDark : C.bgLight); bgDr.setColor(isDark ? C.bgDark : C.bgLight);
bgDr.setCornerRadius(this.dp(12)); bgDr.setCornerRadius(this.dp(12));
container.setBackground(bgDr); container.setBackground(bgDr);
try { container.setElevation(this.dp(8)); } catch(e){} try { container.setElevation(this.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
// Header // Header
var header = new android.widget.LinearLayout(context); var header = new android.widget.LinearLayout(context);
@@ -716,7 +721,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
else self.hideAllPanels(); else self.hideAllPanels();
}); });
btnClose.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4)); btnClose.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4));
try { btnClose.setElevation(this.dp(25)); } catch(e){} // Ensure on top of resize handles try { btnClose.setElevation(this.dp(25)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } // Ensure on top of resize handles
header.addView(btnClose); header.addView(btnClose);
// Spacer to avoid overlap with Top-Right resize handle // Spacer to avoid overlap with Top-Right resize handle
@@ -732,8 +737,8 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Add Content // Add Content
// 移除 content 原有的背景和 elevation避免重复 // 移除 content 原有的背景和 elevation避免重复
try { contentView.setBackground(null); } catch(e){} try { contentView.setBackground(null); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try { contentView.setElevation(0); } catch(e){} try { contentView.setElevation(0); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0); var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0);
contentLp.weight = 1; contentLp.weight = 1;
@@ -745,7 +750,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Bottom-Right Corner) - Invisible // Resize Handle (Bottom-Right Corner) - Invisible
var handleBR = new android.view.View(context); var handleBR = new android.view.View(context);
try { handleBR.setElevation(this.dp(20)); } catch(e){} try { handleBR.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleBRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleBRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleBRLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.END; handleBRLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.END;
handleBRLp.rightMargin = 0; handleBRLp.rightMargin = 0;
@@ -754,7 +759,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Bottom-Left Corner) - Invisible // Resize Handle (Bottom-Left Corner) - Invisible
var handleBL = new android.view.View(context); var handleBL = new android.view.View(context);
try { handleBL.setElevation(this.dp(20)); } catch(e){} try { handleBL.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleBLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleBLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleBLLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.START; handleBLLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.START;
handleBLLp.bottomMargin = 0; handleBLLp.bottomMargin = 0;
@@ -763,7 +768,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Top-Left Corner) - Invisible // Resize Handle (Top-Left Corner) - Invisible
var handleTL = new android.view.View(context); var handleTL = new android.view.View(context);
try { handleTL.setElevation(this.dp(20)); } catch(e){} try { handleTL.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleTLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleTLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleTLLp.gravity = android.view.Gravity.TOP | android.view.Gravity.START; handleTLLp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
handleTLLp.topMargin = 0; handleTLLp.topMargin = 0;
@@ -772,7 +777,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Top-Right Corner) - Invisible // Resize Handle (Top-Right Corner) - Invisible
var handleTR = new android.view.View(context); var handleTR = new android.view.View(context);
try { handleTR.setElevation(this.dp(20)); } catch(e){} try { handleTR.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleTRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleTRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleTRLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END; handleTRLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END;
handleTRLp.topMargin = 0; handleTRLp.topMargin = 0;
@@ -866,7 +871,7 @@ FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerVi
lp.x = targetX; lp.x = targetX;
lp.y = targetY; lp.y = targetY;
try { wm.updateViewLayout(rootView, lp); } catch(e){} try { wm.updateViewLayout(rootView, lp); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
} }
return true; return true;
@@ -937,10 +942,10 @@ FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerVi
lpCur.height = initialH; lpCur.height = initialH;
lpCur.x = initialX; lpCur.x = initialX;
lpCur.y = initialY; lpCur.y = initialY;
try { wm.updateViewLayout(rootView, lpCur); } catch(e){} try { wm.updateViewLayout(rootView, lpCur); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
resizing = true; resizing = true;
} catch(e) {} } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
}); });
self.state.h.postDelayed(longPressRunnable, 300); // 300ms hold to activate resize self.state.h.postDelayed(longPressRunnable, 300); // 300ms hold to activate resize
@@ -1021,7 +1026,7 @@ FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerVi
lp.y = Math.round(newY); lp.y = Math.round(newY);
} }
try { wm.updateViewLayout(rootView, lp); } catch(e){} try { wm.updateViewLayout(rootView, lp); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
} }
return true; return true;
@@ -1137,7 +1142,7 @@ FloatBallAppWM.prototype.showViewerPanel = function(title, text) {
}; };
FloatBallAppWM.prototype.cancelLongPressTimer = function() { FloatBallAppWM.prototype.cancelLongPressTimer = function() {
try { if (this.state.longPressRunnable && this.state.h) this.state.h.removeCallbacks(this.state.longPressRunnable); } catch (e) {} try { if (this.state.longPressRunnable && this.state.h) this.state.h.removeCallbacks(this.state.longPressRunnable); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
this.state.longPressArmed = false; this.state.longPressArmed = false;
this.state.longPressRunnable = null; this.state.longPressRunnable = null;
}; };
@@ -1182,14 +1187,45 @@ FloatBallAppWM.prototype.armLongPress = function() {
FloatBallAppWM.prototype.setupTouchListener = function() { FloatBallAppWM.prototype.setupTouchListener = function() {
var slop = this.dp(this.config.CLICK_SLOP_DP); var slop = this.dp(this.config.CLICK_SLOP_DP);
var self = this; var self = this;
// 速度追踪
var velocityTracker = null; var velocityTracker = null;
var lastTouchX = 0;
var lastTouchY = 0;
// 限制 WM 更新频率,避免过热/卡顿
var lastUpdateTs = 0; var lastUpdateTs = 0;
var downDocked = false;
var downDockSide = null;
var startRawX = 0;
var startRawY = 0;
var logicalDownX = 0;
var logicalDownY = 0;
var grabOffsetX = 0;
var grabOffsetY = 0;
function recycleVelocityTracker() {
try { if (velocityTracker) velocityTracker.recycle(); } catch (e) { safeLog(null, "e", "velocityTracker recycle fail: " + String(e)); }
velocityTracker = null;
}
function cancelBallAnimator() {
try {
if (self.state.ballAnimator) {
self.state.ballAnimator.cancel();
self.state.ballAnimator = null;
}
if (self.state.ballContent) {
try { self.state.ballContent.animate().cancel(); } catch (eAnim) {}
try { self.state.ballContent.setScaleX(1.0); self.state.ballContent.setScaleY(1.0); } catch (eScale) {}
}
} catch (e) { safeLog(null, "e", "cancelBallAnimator fail: " + String(e)); }
}
function getLogicalBallX(di) {
try {
if (self.state.docked) {
if (self.state.dockSide === "right") return self.state.screen.w - di.ballSize;
return 0;
}
return self.state.ballLp.x;
} catch (e) { return 0; }
}
return new JavaAdapter(android.view.View.OnTouchListener, { return new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) { onTouch: function(v, e) {
@@ -1198,152 +1234,147 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
var a = e.getAction(); var a = e.getAction();
var di = self.getDockInfo(); var di = self.getDockInfo();
if (velocityTracker == null) {
velocityTracker = android.view.VelocityTracker.obtain();
}
velocityTracker.addMovement(e);
if (a === android.view.MotionEvent.ACTION_DOWN) { if (a === android.view.MotionEvent.ACTION_DOWN) {
recycleVelocityTracker();
try {
velocityTracker = android.view.VelocityTracker.obtain();
velocityTracker.addMovement(e);
} catch (eVT) { velocityTracker = null; }
self.touchActivity(); self.touchActivity();
cancelBallAnimator();
try { v.setAlpha(1.0); } catch (eA0) {}
try { v.setPressed(true); } catch (eP0) {}
try { v.drawableHotspotChanged(e.getX(), e.getY()); } catch (eH0) {}
// 恢复不透明度 downDocked = !!self.state.docked;
try { v.setAlpha(1.0); } catch(eA) {} downDockSide = self.state.dockSide;
startRawX = e.getRawX();
startRawY = e.getRawY();
logicalDownX = getLogicalBallX(di);
logicalDownY = self.state.ballLp.y;
grabOffsetX = startRawX - logicalDownX;
grabOffsetY = startRawY - logicalDownY;
if (self.state.docked) { self.state.rawX = startRawX;
self.undockToFull(false, null); self.state.rawY = startRawY;
self.touchActivity(); self.state.downX = logicalDownX;
} self.state.downY = logicalDownY;
self.state.rawX = e.getRawX();
self.state.rawY = e.getRawY();
self.state.downX = self.state.ballLp.x;
self.state.downY = self.state.ballLp.y;
self.state.dragging = false; self.state.dragging = false;
lastUpdateTs = 0;
lastTouchX = e.getRawX(); if (self.config.ENABLE_ANIMATIONS && !downDocked) {
lastTouchY = e.getRawY(); try { v.animate().cancel(); v.animate().scaleX(0.9).scaleY(0.9).setDuration(100).start(); }
catch (eS0) { safeLog(null, "e", "press scale fail: " + String(eS0)); }
try { v.setPressed(true); } catch (eP) {} } else {
try { v.drawableHotspotChanged(e.getX(), e.getY()); } catch (eH) {} try { v.animate().cancel(); v.setScaleX(1.0); v.setScaleY(1.0); } catch (eS1) {}
// 按下缩小反馈
if (self.config.ENABLE_ANIMATIONS) {
try {
v.animate().scaleX(0.9).scaleY(0.9).setDuration(100).start();
} catch(eS){}
} }
self.armLongPress(); self.armLongPress();
// # 日志精简touch DOWN 只在 DEBUG 且非频繁触发时记录
// if (self.L) self.L.d("touch DOWN rawX=" + String(self.state.rawX) + " rawY=" + String(self.state.rawY));
return true; return true;
} }
try { if (velocityTracker) velocityTracker.addMovement(e); } catch (eVT2) {}
if (a === android.view.MotionEvent.ACTION_MOVE) { if (a === android.view.MotionEvent.ACTION_MOVE) {
self.touchActivity(); self.touchActivity();
var curRawX = e.getRawX(); var curRawX = e.getRawX();
var curRawY = e.getRawY(); var curRawY = e.getRawY();
var dx = Math.round(curRawX - self.state.rawX); var dx = Math.round(curRawX - startRawX);
var dy = Math.round(curRawY - self.state.rawY); var dy = Math.round(curRawY - startRawY);
lastTouchX = curRawX;
lastTouchY = curRawY;
if (!self.state.dragging) { if (!self.state.dragging) {
if (Math.abs(dx) > slop || Math.abs(dy) > slop) { if (Math.abs(dx) > slop || Math.abs(dy) > slop) {
self.state.dragging = true; self.state.dragging = true;
self.cancelLongPressTimer(); self.cancelLongPressTimer();
// # 日志精简drag start 只在 DEBUG 时记录 try { self.hideAllPanels(); } catch (eHide) {}
// if (self.L) self.L.d("drag start dx=" + String(dx) + " dy=" + String(dy)); cancelBallAnimator();
// 吸边态拖拽不再弹出完整球;先保持可见条沿边移动,彻底避免“弹到手指处”的首帧闪现。
} }
} }
if (self.state.dragging) { if (self.state.dragging) {
self.state.ballLp.x = self.state.downX + dx; if (downDocked) {
self.state.ballLp.y = self.state.downY + dy; var edgeY = self.clamp(logicalDownY + dy, 0, self.state.screen.h - di.ballSize);
self.state.docked = true;
self.state.ballLp.x = self.clamp(self.state.ballLp.x, 0, self.state.screen.w - di.ballSize); self.state.dockSide = downDockSide || self.state.dockSide || "right";
self.state.ballLp.y = self.clamp(self.state.ballLp.y, 0, self.state.screen.h - di.ballSize); self.state.ballLp.width = di.visiblePx;
self.state.ballLp.y = edgeY;
self.state.ballLp.width = di.ballSize; if (self.state.dockSide === "left") {
try { self.state.ballContent.setX(0); } catch (e0) {} self.state.ballLp.x = 0;
try { self.state.ballContent.setX(-di.hiddenPx); } catch (eLX) {}
var now = java.lang.System.currentTimeMillis(); } else {
if (now - lastUpdateTs > 10) { // 10ms 节流 self.state.ballLp.x = self.state.screen.w - di.visiblePx;
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU) {} try { self.state.ballContent.setX(0); } catch (eRX) {}
lastUpdateTs = now; }
try { self.state.ballContent.setAlpha(1.0); } catch (eAEdge) {}
} else {
var targetX = Math.round(curRawX - grabOffsetX);
var targetY = Math.round(curRawY - grabOffsetY);
self.state.docked = false;
self.state.dockSide = null;
self.state.ballLp.width = di.ballSize;
self.state.ballLp.x = self.clamp(targetX, 0, self.state.screen.w - di.ballSize);
self.state.ballLp.y = self.clamp(targetY, 0, self.state.screen.h - di.ballSize);
try { self.state.ballContent.setX(0); } catch (eX2) {}
try { self.state.ballContent.setAlpha(1.0); } catch (eA2) {}
} }
self.hideAllPanels(); var now = java.lang.System.currentTimeMillis();
// 拖拽中不频繁保存位置,只在 UP 时保存 if (lastUpdateTs === 0 || now - lastUpdateTs > 10) {
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); }
catch (eU) { safeLog(null, "e", "drag updateViewLayout fail: " + String(eU)); }
lastUpdateTs = now;
}
} }
return true; return true;
} }
if (a === android.view.MotionEvent.ACTION_UP || a === android.view.MotionEvent.ACTION_CANCEL) { if (a === android.view.MotionEvent.ACTION_UP || a === android.view.MotionEvent.ACTION_CANCEL) {
self.touchActivity(); self.touchActivity();
try { v.setPressed(false); } catch (eP2) {} try { v.setPressed(false); } catch (eP2) {}
self.cancelLongPressTimer(); self.cancelLongPressTimer();
// 恢复缩放
if (self.config.ENABLE_ANIMATIONS) { if (self.config.ENABLE_ANIMATIONS) {
try { try { v.animate().cancel(); v.animate().scaleX(1.0).scaleY(1.0).setDuration(150).start(); }
v.animate().scaleX(1.0).scaleY(1.0).setDuration(150).start(); catch (eS2) { safeLog(null, "e", "release scale fail: " + String(eS2)); }
} catch(eS){}
} else { } else {
try { v.setScaleX(1); v.setScaleY(1); } catch(eS){} try { v.setScaleX(1); v.setScaleY(1); } catch (eS3) {}
} }
if (self.state.longPressTriggered) { if (self.state.longPressTriggered) {
self.resetLongPressState(); self.resetLongPressState();
if (velocityTracker) { velocityTracker.recycle(); velocityTracker = null; } recycleVelocityTracker();
return true; return true;
} }
if (!self.state.dragging && a === android.view.MotionEvent.ACTION_UP) { if (!self.state.dragging && a === android.view.MotionEvent.ACTION_UP) {
if (downDocked && self.state.docked) {
try { self.undockToFull(false, null); } catch (eUndock) {}
}
try { self.playBounce(v); } catch (eB) {} try { self.playBounce(v); } catch (eB) {}
if (self.state.addedPanel) self.hideMainPanel(); if (self.state.addedPanel) self.hideMainPanel();
else self.showPanelAvoidBall("main"); else self.showPanelAvoidBall("main");
// # 日志精简click 事件记录为 INFO 级别(关键操作)
if (self.L) self.L.i("click -> toggle main"); if (self.L) self.L.i("click -> toggle main");
} else { } else if (self.state.dragging) {
// 拖拽结束 try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU2) {}
// 确保最后位置被更新
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU) {}
var forceSide = null; var forceSide = null;
// 计算速度 try {
if (velocityTracker) { if (velocityTracker) {
velocityTracker.computeCurrentVelocity(1000); velocityTracker.computeCurrentVelocity(1000);
var vx = velocityTracker.getXVelocity(); var vx = velocityTracker.getXVelocity();
// 简单的 fling 判定
if (vx > 1000) forceSide = "right"; if (vx > 1000) forceSide = "right";
else if (vx < -1000) forceSide = "left"; else if (vx < -1000) forceSide = "left";
}
} catch (eV) {}
// # 日志精简drag end 只在 DEBUG 时记录 self.state.dragging = false;
// if (self.L) self.L.d("drag end vx=" + vx); if (self.config.ENABLE_SNAP_TO_EDGE) self.snapToEdgeDocked(true, forceSide);
} else self.savePos(self.state.ballLp.x, self.state.ballLp.y);
if (self.config.ENABLE_SNAP_TO_EDGE) {
// 立即吸附,带动画,支持 fling 方向
self.snapToEdgeDocked(true, forceSide);
} else {
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
}
}
if (velocityTracker) {
velocityTracker.recycle();
velocityTracker = null;
} }
recycleVelocityTracker();
self.state.dragging = false; self.state.dragging = false;
self.touchActivity();
self.resetLongPressState(); self.resetLongPressState();
return true; return true;
} }
@@ -1359,7 +1390,7 @@ FloatBallAppWM.prototype.createBallViews = function() {
var root = new android.widget.FrameLayout(context); var root = new android.widget.FrameLayout(context);
root.setClipToPadding(true); root.setClipToPadding(true);
root.setClipChildren(true); root.setClipChildren(true);
try { root.setElevation(this.dp(6)); } catch(e){} try { root.setElevation(this.dp(6)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var content = new android.widget.FrameLayout(context); var content = new android.widget.FrameLayout(context);
var lp = new android.widget.FrameLayout.LayoutParams(di.ballSize, di.ballSize); var lp = new android.widget.FrameLayout.LayoutParams(di.ballSize, di.ballSize);
@@ -1369,7 +1400,6 @@ try {
var iconResId = Number(this.config.BALL_ICON_RES_ID || 0); var iconResId = Number(this.config.BALL_ICON_RES_ID || 0);
var iconType = this.config.BALL_ICON_TYPE ? String(this.config.BALL_ICON_TYPE) : "android"; var iconType = this.config.BALL_ICON_TYPE ? String(this.config.BALL_ICON_TYPE) : "android";
var iconFilePath = (this.config.BALL_ICON_FILE_PATH == null) ? "" : String(this.config.BALL_ICON_FILE_PATH); var iconFilePath = (this.config.BALL_ICON_FILE_PATH == null) ? "" : String(this.config.BALL_ICON_FILE_PATH);
var textStr = (this.config.BALL_TEXT == null) ? "" : String(this.config.BALL_TEXT);
// # 是否显示图标file 只看路径app 优先看包名,其次可回退 iconResIdandroid 走 iconResIdshortx 总是显示 // # 是否显示图标file 只看路径app 优先看包名,其次可回退 iconResIdandroid 走 iconResIdshortx 总是显示
var showIcon = false; var showIcon = false;
@@ -1384,10 +1414,9 @@ try {
showIcon = (iconResId > 0); showIcon = (iconResId > 0);
} }
if (!showIcon && textStr.length === 0) showIcon = true; if (!showIcon) showIcon = true;
var showText = textStr.length > 0; if (showIcon) {
if (showIcon || showText) {
var box = new android.widget.LinearLayout(context); var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL); box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setGravity(android.view.Gravity.CENTER); box.setGravity(android.view.Gravity.CENTER);
@@ -1399,9 +1428,6 @@ try {
box.setLayoutParams(boxLp); box.setLayoutParams(boxLp);
var tintHex = (this.config.BALL_ICON_TINT_HEX == null) ? "" : String(this.config.BALL_ICON_TINT_HEX); var tintHex = (this.config.BALL_ICON_TINT_HEX == null) ? "" : String(this.config.BALL_ICON_TINT_HEX);
var textColorHex = (this.config.BALL_TEXT_COLOR_HEX == null) ? "" : String(this.config.BALL_TEXT_COLOR_HEX);
var defaultColor = android.graphics.Color.WHITE;
if (showIcon) { if (showIcon) {
var iv = new android.widget.ImageView(context); var iv = new android.widget.ImageView(context);
@@ -1423,7 +1449,7 @@ try {
} else { } else {
safeLog(this.L, 'w', "Ball icon file load failed: " + iconFilePath); safeLog(this.L, 'w', "Ball icon file load failed: " + iconFilePath);
} }
} catch (eF) {} } catch(eF) { safeLog(null, 'e', "catch " + String(eF)); }
} }
// # 2) app加载应用图标 (file 失败也会尝试 app) // # 2) app加载应用图标 (file 失败也会尝试 app)
@@ -1438,7 +1464,7 @@ try {
usedKind = "app"; usedKind = "app";
} }
} }
} catch (eA) {} } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
} }
// # 2.5) shortx专门加载 ShortX 内置图标(也作为 file 模式的兜底) // # 2.5) shortx专门加载 ShortX 内置图标(也作为 file 模式的兜底)
@@ -1459,7 +1485,7 @@ try {
safeLog(this.L, 'i', "File icon failed, fallback to shortx icon"); safeLog(this.L, 'i', "File icon failed, fallback to shortx icon");
} }
} }
} catch (eShortx2) {} } catch(eShortx2) { safeLog(null, 'e', "catch " + String(eShortx2)); }
} }
// # 3) android或所有兜底走资源 id优先尝试 ShortX 内置图标) // # 3) android或所有兜底走资源 id优先尝试 ShortX 内置图标)
@@ -1473,13 +1499,13 @@ try {
if (usedDrawable != null) { if (usedDrawable != null) {
usedKind = "shortx"; usedKind = "shortx";
} }
} catch (eShortx) {} } catch(eShortx) { safeLog(null, 'e', "catch " + String(eShortx)); }
} }
if (usedDrawable != null) { if (usedDrawable != null) {
iv.setImageDrawable(usedDrawable); iv.setImageDrawable(usedDrawable);
} else if (iconResId > 0) { } else if (iconResId > 0) {
try { iv.setImageResource(iconResId); usedKind = "android"; } catch (eR) {} try { iv.setImageResource(iconResId); usedKind = "android"; } catch(eR) { safeLog(null, 'e', "catch " + String(eR)); }
} else { } else {
// # 没有任何可用图标,直接不加到布局 // # 没有任何可用图标,直接不加到布局
usedKind = "none"; usedKind = "none";
@@ -1509,49 +1535,20 @@ try {
try { try {
var tintColor2 = android.graphics.Color.parseColor(tintHex); var tintColor2 = android.graphics.Color.parseColor(tintHex);
iv.setColorFilter(tintColor2, android.graphics.PorterDuff.Mode.SRC_IN); iv.setColorFilter(tintColor2, android.graphics.PorterDuff.Mode.SRC_IN);
} catch (eTint2) {} } catch(eTint2) { safeLog(null, 'e', "catch " + String(eTint2)); }
} else if (usedKind === "android") { } else if (usedKind === "android") {
try { iv.setColorFilter(android.graphics.Color.WHITE, android.graphics.PorterDuff.Mode.SRC_IN); } catch (eCF) {} try { iv.setColorFilter(android.graphics.Color.WHITE, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF) { safeLog(null, 'e', "catch " + String(eCF)); }
} else { } else {
try { iv.clearColorFilter(); } catch (eCL) {} try { iv.clearColorFilter(); } catch(eCL) { safeLog(null, 'e', "catch " + String(eCL)); }
} }
box.addView(iv); box.addView(iv);
} }
} }
if (showText) {
var tv = new android.widget.TextView(context);
tv.setText(textStr);
tv.setGravity(android.view.Gravity.CENTER);
try { tv.setIncludeFontPadding(false); } catch (eFP) {}
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.BALL_TEXT_SIZE_SP || 10));
var txtColor = defaultColor;
if (textColorHex.length > 0) {
try { txtColor = android.graphics.Color.parseColor(textColorHex); } catch (eTC) {}
} else if (tintHex.length > 0) {
// # 如果没单独指定文字颜色,则跟随图标颜色
try { txtColor = android.graphics.Color.parseColor(tintHex); } catch (eTC2) {}
}
tv.setTextColor(txtColor);
// # 设置一点点阴影,提高可读性
try { tv.setShadowLayer(1.2, 0, 1.0, 0x66000000); } catch (eSH) {}
// # 图标与文字间距
if (showIcon) {
var gap = this.dp(Number(this.config.BALL_ICON_TEXT_GAP_DP || 1));
var padTop = Math.max(0, gap);
tv.setPadding(0, padTop, 0, 0);
}
box.addView(tv);
}
content.addView(box); content.addView(box);
} }
} catch (eBallInner) {} } catch(eBallInner) { safeLog(null, 'e', "catch " + String(eBallInner)); }
this.updateBallContentBackground(content); this.updateBallContentBackground(content);
@@ -1559,7 +1556,7 @@ try {
// # 阴影控制file/app 模式下不加阴影(避免透明背景带黑框) // # 阴影控制file/app 模式下不加阴影(避免透明背景带黑框)
var _uk = this.state.usedIconKind; var _uk = this.state.usedIconKind;
if (_uk !== "file" && _uk !== "app") { if (_uk !== "file" && _uk !== "app") {
try { root.setElevation(this.dp(6)); } catch(e){} try { root.setElevation(this.dp(6)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
content.setClickable(true); content.setClickable(true);

View File

@@ -1,3 +1,4 @@
// @version 1.0.0
function runOnMainSync(fn, timeoutMs) { function runOnMainSync(fn, timeoutMs) {
if (!fn) return { ok: false, error: "empty-fn" }; if (!fn) return { ok: false, error: "empty-fn" };
try { try {
@@ -118,12 +119,12 @@ FloatBallAppWM.prototype.close = function() {
} }
} catch (eIcon) {} } catch (eIcon) {}
try { try {
if (self.__scIconLoaderSingleton && self.__scIconLoaderSingleton.ht) { if (this.__scIconLoaderSingleton && this.__scIconLoaderSingleton.ht) {
if (android.os.Build.VERSION.SDK_INT >= 18) self.__scIconLoaderSingleton.ht.quitSafely(); if (android.os.Build.VERSION.SDK_INT >= 18) this.__scIconLoaderSingleton.ht.quitSafely();
else self.__scIconLoaderSingleton.ht.quit(); else this.__scIconLoaderSingleton.ht.quit();
} }
} catch (eScIcon) {} } catch (eScIcon) {}
try { self.__scIconLoaderSingleton = null; } catch (eScIcon2) {} try { this.__scIconLoaderSingleton = null; } catch (eScIcon2) {}
safeLog(this.L, 'i', "close done"); safeLog(this.L, 'i', "close done");
@@ -157,8 +158,8 @@ FloatBallAppWM.prototype.dispose = function() {
// # 清理单例引用 // # 清理单例引用
try { try {
if (self.__shortcutPickerSingleton === this.__shortcutPickerSingleton) { if (this.__shortcutPickerSingleton) {
self.__shortcutPickerSingleton = null; this.__shortcutPickerSingleton = null;
} }
} catch (e) {} } catch (e) {}
@@ -244,51 +245,94 @@ FloatBallAppWM.prototype.startAsync = function(entryProcInfo, closeRule) {
); );
if (cfgRcv) this.state.receivers.push(cfgRcv); if (cfgRcv) this.state.receivers.push(cfgRcv);
h.post(new JavaAdapter(java.lang.Runnable, { var sysDlgRcv = registerReceiverOnMain("android.intent.action.CLOSE_SYSTEM_DIALOGS", function(ctx, intent) {
run: function() { try {
try { var reason = "";
self.state.wm = context.getSystemService(android.content.Context.WINDOW_SERVICE); try { reason = String(intent.getStringExtra("reason") || ""); } catch (eReason) { reason = ""; }
self.state.density = context.getResources().getDisplayMetrics().density; h.post(new JavaAdapter(java.lang.Runnable, {
run: function() { try { self.handleSystemUiDismiss(reason); } catch (eSysDlg) {} }
if (self.L) self.L.updateConfig(self.config); }));
} catch (eSysDlgOuter) {}
self.state.loadedPos = self.loadSavedPos(); });
if (sysDlgRcv) this.state.receivers.push(sysDlgRcv);
self.state.screen = self.getScreenSizePx();
self.state.lastRotation = self.getRotation();
self.createBallViews();
self.state.ballLp = self.createBallLayoutParams();
var startBox = { ok: false, err: "启动确认超时", added: false };
var startLatch = new java.util.concurrent.CountDownLatch(1);
var posted = false;
try {
posted = h.post(new JavaAdapter(java.lang.Runnable, {
run: function() {
try { try {
self.state.wm.addView(self.state.ballRoot, self.state.ballLp); self.state.wm = context.getSystemService(android.content.Context.WINDOW_SERVICE);
self.state.addedBall = true; self.state.density = context.getResources().getDisplayMetrics().density;
} catch (eAdd) {
try { self.toast("悬浮球 addView 失败: " + String(eAdd)); } catch (eT) {}
if (self.L) self.L.fatal("addView ball fail err=" + String(eAdd));
self.state.addedBall = false;
try { self.close(); } catch (eC) {}
return;
}
self.setupDisplayMonitor(); if (self.L) self.L.updateConfig(self.config);
self.touchActivity();
if (self.L) { self.state.screen = self.getScreenSizePx();
self.L.i("start ok actionClose=" + String(self.config.ACTION_CLOSE_ALL)); self.state.lastRotation = self.getRotation();
self.L.i("ball x=" + String(self.state.ballLp.x) + " y=" + String(self.state.ballLp.y) + " sizeDp=" + String(self.config.BALL_SIZE_DP)); self.state.loadedPos = self.loadSavedPos();
self.createBallViews();
self.state.ballLp = self.createBallLayoutParams();
try {
self.state.wm.addView(self.state.ballRoot, self.state.ballLp);
self.state.addedBall = true;
startBox.added = true;
} catch (eAdd) {
startBox.ok = false;
startBox.err = "悬浮球 addView 失败: " + String(eAdd);
try { self.toast(startBox.err); } catch (eT) {}
if (self.L) self.L.fatal("addView ball fail err=" + String(eAdd));
self.state.addedBall = false;
try { self.close(); } catch (eC) {}
return;
}
self.setupDisplayMonitor();
self.touchActivity();
startBox.ok = true;
startBox.err = "";
if (self.L) {
self.L.i("start ok actionClose=" + String(self.config.ACTION_CLOSE_ALL));
self.L.i("ball x=" + String(self.state.ballLp.x) + " y=" + String(self.state.ballLp.y) + " sizeDp=" + String(self.config.BALL_SIZE_DP));
}
} catch (eAll) {
startBox.ok = false;
startBox.err = "启动异常: " + String(eAll);
try { self.toast(startBox.err); } catch (eTT2) {}
if (self.L) self.L.fatal("start runnable err=" + String(eAll));
try { self.close(); } catch (eC2) {}
} finally {
try { startLatch.countDown(); } catch (eLatch) {}
} }
} catch (eAll) {
try { self.toast("启动异常: " + String(eAll)); } catch (eTT2) {}
if (self.L) self.L.fatal("start runnable err=" + String(eAll));
try { self.close(); } catch (eC2) {}
} }
}));
} catch (ePost) {
posted = false;
startBox.ok = false;
startBox.err = "启动任务投递失败: " + String(ePost);
try { startLatch.countDown(); } catch (eLatch2) {}
}
if (!posted) {
startBox.ok = false;
if (!startBox.err) startBox.err = "启动任务投递失败";
} else {
try {
var done = startLatch.await(2500, java.util.concurrent.TimeUnit.MILLISECONDS);
if (!done && self.L) self.L.e("start confirm timeout; addView result unknown");
} catch (eWait) {
startBox.ok = false;
startBox.err = "启动确认等待异常: " + String(eWait);
} }
})); }
return { return {
ok: true, ok: !!startBox.ok,
msg: "已按 WM 专属 HandlerThread 模型启动Shell 默认 Action失败广播桥兜底Content URI 已启用)", err: String(startBox.err || ""),
msg: startBox.ok ? "悬浮球 addView 已确认成功" : String(startBox.err || "启动失败"),
preCloseBroadcastSent: preCloseSent, preCloseBroadcastSent: preCloseSent,
closeAction: String(this.config.ACTION_CLOSE_ALL), closeAction: String(this.config.ACTION_CLOSE_ALL),
receiverRegisteredOnMain: { receiverRegisteredOnMain: {

File diff suppressed because it is too large Load Diff

72
manifest.json Normal file
View File

@@ -0,0 +1,72 @@
{
"alg": "SHA256withRSA",
"files": {
"th_01_base.js": {
"sha256": "30c121902ef5a0006a5daabca8443ed6cbf6e2508209604c48f89855d0690034",
"size": 52546
},
"th_02_core.js": {
"sha256": "f44f88f0ce3f44f0d1675e55a9f0b66d831caa7819dda54aef6e308ec27faaeb",
"size": 3934
},
"th_03_icon.js": {
"sha256": "717f7f37474d3616c2cd944581455f600020a850ec8812100d0546ec1302c987",
"size": 5598
},
"th_04_theme.js": {
"sha256": "b815acfee30e56458e61c033ab3fddfe5d8d0c52ec172c419bad7187fcb341ba",
"size": 36083
},
"th_05_persistence.js": {
"sha256": "d80787c2810839ebbe499e93db3df33d6e8d2d6b6ae71644ce351db0f36e4d3e",
"size": 14077
},
"th_06_icon_parser.js": {
"sha256": "25b95a5df634a7ee359f3ab798e4d3154a71c24016f7b4bf8a658096644b2484",
"size": 22909
},
"th_07_shortcut.js": {
"sha256": "7b2dbd1e35c636cca4ccce335dfb9e0b972342972ce012116ff4bbcfc438caa1",
"size": 72992
},
"th_08_content.js": {
"sha256": "8a76f15dfd1292081cba4b2dd218424be66540350e2807065421a6176a86c2db",
"size": 7938
},
"th_09_animation.js": {
"sha256": "7120d208910955a2a163c4ad535b2eca674f7a0c2c462ef2f03bad11c9511933",
"size": 27541
},
"th_10_shell.js": {
"sha256": "0ed793079c2f6ba7d29f4c0d411705cb72419f45f572cbe37ed32ac16527a8bc",
"size": 1094
},
"th_11_action.js": {
"sha256": "a0142d26621f3d076bd1b749f2885af2c0806c9f206e362a3b3680a5d2312b31",
"size": 13545
},
"th_12_rebuild.js": {
"sha256": "7b820e813d2dd8866778fefe8bfeb6aca227bb1a32a89d318de830178f19824f",
"size": 2362
},
"th_13_panel_ui.js": {
"sha256": "19e5e1c346051aafdda6253ed7c25ef5fbc4f883de66f656c9575d97e9d6ffc8",
"size": 20386
},
"th_14_panels.js": {
"sha256": "cf18cb06a9e221b671360f94040542c1c27e8776bb5037abe4f4b0f3de3e1073",
"size": 217347
},
"th_15_extra.js": {
"sha256": "ed56b19a5a798a785c024eb931140ded69d16921d2e87ad4ccd861df1c1907d8",
"size": 62936
},
"th_16_entry.js": {
"sha256": "e7c99c3dfbd6aedab05551426955081ae6cae034754f2f557cefa01dc75dc001",
"size": 12777
}
},
"keyId": "toolhub-targets-2026-rsa3072",
"schema": 2,
"version": 20260512022403
}

1
manifest.sig Normal file
View File

@@ -0,0 +1 @@
mNKpW6JxqzeRVFYnEjJcxIogH2bMLbR5rExVdGHMQcOXhtAbamkl6ZLEtUG534wweU95Amku8lxWO+Pe5p2Bg7ufExHqtBgm3eFsDtftGlG7KYOakPvCluGcWZivGMrxFdUq4CsF/Y4dhi9UG9Dtr2X8UdnKQRXLIMtPW+EpDvZvheg4G+7hsEKOYyRzkX1HK8/KEUaaiDogr7Hp+rT0jPa0iCMBn3fiLAIODYMWj0mzjML7WqtBOwXBoLGSeHxJ7BwJd1USxQh+pMo0/jxf/Uas7oTue67bYpyub9v2ESAfiLIg+qijP9oQRnptvtSA99c0Qj/8BJLiXIl6AD1Qp+PhnJwmGgQg62OtJdp7ly0zPz51UbULXfPwqj9djguac1yN7qVGQroT2oo93brZMpV3iRwQw2ov2E/efFf4iXSXnd/aMbzozUNQtqINnsWfZsrjBNxuDLLHvuSUh5h19/Yok+5EczPL5iZDl/W1GNmurrDvOhnTCZjuVS7t97Zt

View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Generate ToolHub signed manifest and entry checksum.
Security notes:
- Private key stays outside the repo by default: ~/.hermes/toolhub_signing/private_key.pem
- The script prints git status/diff summary before signing. Use --yes in automation.
"""
import argparse
import base64
import hashlib
import json
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CODE_DIR = ROOT / "code"
PRIVATE_KEY = Path.home() / ".hermes" / "toolhub_signing" / "private_key.pem"
MANIFEST = ROOT / "manifest.json"
SIG = ROOT / "manifest.sig"
ENTRY = ROOT / "ToolHub.js"
ENTRY_SHA = ROOT / "ToolHub.js.sha256"
DEFAULT_KEY_ID = "toolhub-targets-2026-rsa3072"
MODULES = [
"th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js",
"th_05_persistence.js", "th_06_icon_parser.js", "th_07_shortcut.js",
"th_08_content.js", "th_09_animation.js", "th_10_shell.js",
"th_11_action.js", "th_12_rebuild.js", "th_13_panel_ui.js",
"th_14_panels.js", "th_15_extra.js", "th_16_entry.js",
]
def sha256_file(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def git_output(args):
try:
return subprocess.check_output(["git", *args], cwd=ROOT, text=True, stderr=subprocess.STDOUT)
except Exception as e:
return f"<git {' '.join(args)} failed: {e}>\n"
def print_review_summary() -> None:
print("== ToolHub signing review ==")
print("-- git status --")
status = git_output(["status", "--short"])
print(status.rstrip() or "clean")
print("-- git diff --stat --")
diff_stat = git_output(["diff", "--stat"])
print(diff_stat.rstrip() or "no unstaged diff")
print("-- staged diff --stat --")
staged = git_output(["diff", "--cached", "--stat"])
print(staged.rstrip() or "no staged diff")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--yes", action="store_true", help="skip interactive confirmation after review summary")
ap.add_argument("--key-id", default=DEFAULT_KEY_ID)
ap.add_argument("--version", type=int, default=0, help="manifest version; default UTC yyyyMMddHHmmss")
args = ap.parse_args()
if not PRIVATE_KEY.exists():
raise SystemExit(f"Private key not found: {PRIVATE_KEY}")
if not ENTRY.exists():
raise SystemExit(f"Entry file not found: {ENTRY}")
print_review_summary()
if not args.yes:
ans = input("Sign this manifest? Type YES to continue: ").strip()
if ans != "YES":
raise SystemExit("aborted")
files = {}
for name in MODULES:
path = CODE_DIR / name
if not path.exists():
raise SystemExit(f"Missing module: {path}")
files[name] = {
"sha256": sha256_file(path),
"size": path.stat().st_size,
}
version = args.version or int(time.strftime("%Y%m%d%H%M%S", time.gmtime()))
manifest = {
"schema": 2,
"version": version,
"keyId": args.key_id,
"alg": "SHA256withRSA",
"files": files,
}
data = (json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
MANIFEST.write_bytes(data)
sig_bin = subprocess.check_output([
"openssl", "dgst", "-sha256", "-sign", str(PRIVATE_KEY), str(MANIFEST)
])
SIG.write_text(base64.b64encode(sig_bin).decode("ascii") + "\n", encoding="utf-8")
entry_hash = sha256_file(ENTRY)
ENTRY_SHA.write_text(f"{entry_hash} ToolHub.js\n", encoding="utf-8")
print("== signed manifest ==")
print(f"manifest_version={manifest['version']}")
print(f"key_id={manifest['keyId']}")
print(f"signed_files={len(files)}")
print(f"ToolHub.js_sha256={entry_hash}")
if __name__ == "__main__":
main()