Compare commits
73 Commits
a1f684cca9
...
backup-bef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b7fbdf9cf | ||
|
|
5c95d04fab | ||
|
|
0037aefcb7 | ||
|
|
523d6e2044 | ||
|
|
7ce5ebeda8 | ||
|
|
726ed4465a | ||
|
|
fdd896aca5 | ||
|
|
8f6dcdcac1 | ||
|
|
637858ab9c | ||
|
|
0251419dbc | ||
|
|
e0b774ccd9 | ||
|
|
ed9521cdf5 | ||
|
|
105d1c68c5 | ||
|
|
caf0e0b598 | ||
|
|
22cd229da9 | ||
|
|
5321cbe6c7 | ||
|
|
0e7c7d6887 | ||
|
|
d93419eccc | ||
|
|
b2c7cae355 | ||
|
|
7d879527f4 | ||
|
|
e89eb9a904 | ||
|
|
83444ba683 | ||
|
|
ed3ee6eaf6 | ||
|
|
fee6e0bc40 | ||
|
|
c32f29fdb7 | ||
|
|
9ad01b436d | ||
|
|
7e23cd95a1 | ||
|
|
a394684f42 | ||
|
|
068e4cdd3b | ||
|
|
256a8f716b | ||
|
|
9a0b618409 | ||
|
|
f89ed88033 | ||
|
|
3c8c337401 | ||
|
|
4ab42b274a | ||
|
|
d03f4d677f | ||
|
|
86a4ed7927 | ||
|
|
99547894b6 | ||
|
|
a39ecaf648 | ||
|
|
e5d540d1a5 | ||
|
|
c67c0ccaf2 | ||
|
|
6e7a2fef7a | ||
|
|
e07bd7e16f | ||
|
|
f1b8f0a921 | ||
|
|
7c350724f6 | ||
|
|
f13f3268c3 | ||
|
|
5036f48772 | ||
|
|
ec64ef07d9 | ||
|
|
f2714d5101 | ||
|
|
273c299ea0 | ||
|
|
c68f0d1209 | ||
|
|
b5e7695d1a | ||
|
|
f2c80112c1 | ||
|
|
2bbe9c3609 | ||
|
|
26e1ebefef | ||
|
|
b61c27ee85 | ||
|
|
ffbbcf647f | ||
|
|
fbb8b6e682 | ||
|
|
4a9aff570c | ||
|
|
8d02291a27 | ||
|
|
4a91b9631f | ||
|
|
313688275b | ||
|
|
6bf2be1bf1 | ||
|
|
96275e8028 | ||
|
|
2022c2d1c1 | ||
|
|
034ed61e22 | ||
|
|
9ef8bdae40 | ||
|
|
9b07a888a5 | ||
|
|
40a60a6912 | ||
|
|
e375f7d7ba | ||
|
|
c7e9b92322 | ||
|
|
c64d4c336b | ||
|
|
6b112d011a | ||
|
|
72702d557b |
333
README.md
333
README.md
@@ -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、透明度和实时预览。
|
||||||
|
|
||||||
|
**代码清理**
|
||||||
|
|
||||||
|
- 按钮编辑与悬浮球设置复用图标选择器和颜色选择器。
|
||||||
|
- 清理悬浮球文字相关死代码。
|
||||||
|
|||||||
441
ToolHub.js
441
ToolHub.js
@@ -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
1
ToolHub.js.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
55dbcacaaa31b031e9a0fcef1253c2e0403fca423ad969e0e1387815e69de3e7 ToolHub.js
|
||||||
@@ -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
124
code/th_02_core.js
Normal 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
170
code/th_03_icon.js
Normal 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
800
code/th_04_theme.js
Normal 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 - 用户 ID(API 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
365
code/th_05_persistence.js
Normal 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
532
code/th_06_icon_parser.js
Normal 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 缓存:避免频繁走 PackageManager(Drawable 可复用);并带容量上限,防止无限增长
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 策略3:APK 文件扫描(最后兜底)
|
||||||
|
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
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
209
code/th_08_content.js
Normal 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
758
code/th_09_animation.js
Normal 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
28
code/th_10_shell.js
Normal 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
320
code/th_11_action.js
Normal 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_b64(Base64)
|
||||||
|
// - 同时会补齐 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) 目标 userId:launchUserId > 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
76
code/th_12_rebuild.js
Normal 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
528
code/th_13_panel_ui.js
Normal 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
@@ -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 优先看包名,其次可回退 iconResId;android 走 iconResId;shortx 总是显示
|
// # 是否显示图标:file 只看路径;app 优先看包名,其次可回退 iconResId;android 走 iconResId;shortx 总是显示
|
||||||
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);
|
||||||
@@ -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: {
|
||||||
4714
code/th_2_core.js
4714
code/th_2_core.js
File diff suppressed because it is too large
Load Diff
72
manifest.json
Normal file
72
manifest.json
Normal 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
1
manifest.sig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mNKpW6JxqzeRVFYnEjJcxIogH2bMLbR5rExVdGHMQcOXhtAbamkl6ZLEtUG534wweU95Amku8lxWO+Pe5p2Bg7ufExHqtBgm3eFsDtftGlG7KYOakPvCluGcWZivGMrxFdUq4CsF/Y4dhi9UG9Dtr2X8UdnKQRXLIMtPW+EpDvZvheg4G+7hsEKOYyRzkX1HK8/KEUaaiDogr7Hp+rT0jPa0iCMBn3fiLAIODYMWj0mzjML7WqtBOwXBoLGSeHxJ7BwJd1USxQh+pMo0/jxf/Uas7oTue67bYpyub9v2ESAfiLIg+qijP9oQRnptvtSA99c0Qj/8BJLiXIl6AD1Qp+PhnJwmGgQg62OtJdp7ly0zPz51UbULXfPwqj9djguac1yN7qVGQroT2oo93brZMpV3iRwQw2ov2E/efFf4iXSXnd/aMbzozUNQtqINnsWfZsrjBNxuDLLHvuSUh5h19/Yok+5EczPL5iZDl/W1GNmurrDvOhnTCZjuVS7t97Zt
|
||||||
118
scripts/generate_signed_manifest.py
Executable file
118
scripts/generate_signed_manifest.py
Executable 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()
|
||||||
Reference in New Issue
Block a user