Compare commits
282
Commits
d76f8bc4ba
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db672cbe8f | ||
|
|
281f4153ee | ||
|
|
ee5796747d | ||
|
|
479d93a3bf | ||
|
|
adaed09f94 | ||
|
|
0531603779 | ||
|
|
b52e6a08bc | ||
|
|
2a2e6988ae | ||
|
|
1f692ae2a7 | ||
|
|
c9f0cbaab6 | ||
|
|
2cfac55b4e | ||
|
|
f9c2ea42a4 | ||
|
|
0ab23161b6 | ||
|
|
316d9d42c7 | ||
|
|
dfde95b020 | ||
|
|
f2551a28a2 | ||
|
|
bcb11e012f | ||
|
|
62b3603ade | ||
|
|
72c6e9889d | ||
|
|
8d2c098deb | ||
|
|
9600f10352 | ||
|
|
e69b8efeeb | ||
|
|
c2635daf3f | ||
|
|
a7bd6c44d9 | ||
|
|
40ac1e40f0 | ||
|
|
0412994953 | ||
|
|
3b5ffece57 | ||
|
|
cfcb94b8d2 | ||
|
|
31c12892d6 | ||
|
|
4d9ba46a7b | ||
|
|
805cfe11cc | ||
|
|
c811913c06 | ||
|
|
278e7eaa93 | ||
|
|
d9950d75eb | ||
|
|
e23bdca26a | ||
|
|
22f9dab2ce | ||
|
|
98b9169b86 | ||
|
|
4e60f440f6 | ||
|
|
38be5e78bc | ||
|
|
8a115f6583 | ||
|
|
294ed6ffc7 | ||
|
|
7f4e18b0ed | ||
|
|
35a636ef42 | ||
|
|
aa225a1973 | ||
|
|
61ef0f6de9 | ||
|
|
31879f3302 | ||
|
|
b1d607d43f | ||
|
|
2ca386973b | ||
|
|
e0bd503261 | ||
|
|
707bbd8a02 | ||
|
|
8557da6fca | ||
|
|
03868d048a | ||
|
|
7d4906828d | ||
|
|
2c69070cc4 | ||
|
|
e60642217a | ||
|
|
68833cf642 | ||
|
|
30ffe469c8 | ||
|
|
132622cbc1 | ||
|
|
6b52740ff1 | ||
|
|
a73bcb9841 | ||
|
|
a9c94c7221 | ||
|
|
39465ea1ef | ||
|
|
f392ee5c48 | ||
|
|
184ebd0ac9 | ||
|
|
e56c837f4f | ||
|
|
8be4634a61 | ||
|
|
47c638d65b | ||
|
|
20f4727714 | ||
|
|
2ffb92c038 | ||
|
|
7c7d891570 | ||
|
|
e87ccdf061 | ||
|
|
f40abbd494 | ||
|
|
a64ae06eb6 | ||
|
|
68710d7ed8 | ||
|
|
5e67100231 | ||
|
|
06f872faf2 | ||
|
|
68f91f7de2 | ||
|
|
4ce7a268a8 | ||
|
|
ddc8d35eb9 | ||
|
|
626a87a30d | ||
|
|
2826c1e905 | ||
|
|
b364b6371a | ||
|
|
501867b362 | ||
|
|
d0a5901ffb | ||
|
|
a2aec783a7 | ||
|
|
b3ce719742 | ||
|
|
8e68254ce4 | ||
|
|
4d0a8450e9 | ||
|
|
b712088daa | ||
|
|
3fea237306 | ||
|
|
2db87b2c5c | ||
|
|
7a276f609e | ||
|
|
6bab304660 | ||
|
|
491db5069c | ||
|
|
b23c6d53c4 | ||
|
|
5c174224f7 | ||
|
|
c69fb92284 | ||
|
|
211c5a556a | ||
|
|
2f22e26de7 | ||
|
|
3c5612f7c9 | ||
|
|
45c633868f | ||
|
|
effce6d0db | ||
|
|
2dc4cad454 | ||
|
|
f3ae06d7cd | ||
|
|
7e68313981 | ||
|
|
6eccb17f9b | ||
|
|
62ec405dca | ||
|
|
b988d64653 | ||
|
|
18014eb2b9 | ||
|
|
76a22a6a91 | ||
|
|
e8218b8ae9 | ||
|
|
a82f3f9dfa | ||
|
|
8971f6067b | ||
|
|
8c165bb8e6 | ||
|
|
ef6eca86b1 | ||
|
|
4ecddb5ea3 | ||
|
|
71b2dafecc | ||
|
|
1f6d390e1c | ||
|
|
a3679e3688 | ||
|
|
0da68e4972 | ||
|
|
eb07415f7b | ||
|
|
8c677f127f | ||
|
|
2e5b814059 | ||
|
|
7ca3e5bd84 | ||
|
|
29e13b2b5e | ||
|
|
671f0d1835 | ||
|
|
26a140e00f | ||
|
|
dce12c63a2 | ||
|
|
4827cc8d92 | ||
|
|
76eb2f4d1c | ||
|
|
42718385cc | ||
|
|
b5da7d7151 | ||
|
|
b89b91c9c4 | ||
|
|
ec8e43e1fd | ||
|
|
e448b9bfda | ||
|
|
e802cc678a | ||
|
|
cd9d9da3bd | ||
|
|
3d70a3dd2c | ||
|
|
c2ee7626a4 | ||
|
|
d8241cd2d6 | ||
|
|
a8c51be1fc | ||
|
|
e67cc50afa | ||
|
|
5663acfbb9 | ||
|
|
8a43f17653 | ||
|
|
4e0e7441fd | ||
|
|
cd7329d141 | ||
|
|
de18cef02c | ||
|
|
986e3814bc | ||
|
|
cf8b0da3bd | ||
|
|
a70bcaac95 | ||
|
|
ea645ce2a6 | ||
|
|
9d5301f931 | ||
|
|
a3d6e21db5 | ||
|
|
986944ff24 | ||
|
|
2bfe1ed796 | ||
|
|
9d93f1b1ef | ||
|
|
aeb9176101 | ||
|
|
c7ef1ebb04 | ||
|
|
bcc016e50e | ||
|
|
54750a63c5 | ||
|
|
121d30ec95 | ||
|
|
19f7e59927 | ||
|
|
54e737a4a2 | ||
|
|
524e5d6967 | ||
|
|
57676766c0 | ||
|
|
bc9ba3693f | ||
|
|
c730333776 | ||
|
|
67aa5fc7a8 | ||
|
|
7a405866af | ||
|
|
908fc53805 | ||
|
|
44a790dc1d | ||
|
|
68ce46a5d8 | ||
|
|
202caaf66b | ||
|
|
bcef8e092e | ||
|
|
3611cbbbde | ||
|
|
41427fdcb3 | ||
|
|
0294553096 | ||
|
|
10690bc348 | ||
|
|
97b741cf56 | ||
|
|
33414de7c4 | ||
|
|
55f0dfe91d | ||
|
|
54857bc0fd | ||
|
|
d7bf62f448 | ||
|
|
3397a702aa | ||
|
|
54a6e828b7 | ||
|
|
8588a6b453 | ||
|
|
9d54cf89c5 | ||
|
|
85e1d03157 | ||
|
|
54d2791f48 | ||
|
|
c3abc3fee4 | ||
|
|
e948a0a44e | ||
|
|
5a5e995ebd | ||
|
|
553a7d5d9e | ||
|
|
33d3059038 | ||
|
|
34f2834704 | ||
|
|
06307dc4b2 | ||
|
|
0cae95c9cc | ||
|
|
3b6a73ac10 | ||
|
|
cd4e2c247f | ||
|
|
b37dc23cd9 | ||
|
|
7ba0fde3e6 | ||
|
|
d12164305d | ||
|
|
30e2ec901f | ||
|
|
8e54b2c150 | ||
|
|
f0b73e119d | ||
|
|
d8eeef5c57 | ||
|
|
52edf0c9e4 | ||
|
|
239345bfd2 | ||
|
|
a5f675750b | ||
|
|
a01e5ca18a | ||
|
|
4534b840c2 | ||
|
|
51281e609f | ||
|
|
19e8fc93ed | ||
|
|
fcff0b0ac1 | ||
|
|
79a0fed336 | ||
|
|
cfeb9b775b | ||
|
|
4a49901bc1 | ||
|
|
980d161972 | ||
|
|
7e7e45ae26 | ||
|
|
1e97dbf54a | ||
|
|
d4928224a5 | ||
|
|
7bd6962ccb | ||
|
|
ed2ae10320 | ||
|
|
9e25db77cc | ||
|
|
dbee37e1d6 | ||
|
|
8f8ffa605b | ||
|
|
5aabede0cd | ||
|
|
7db8197f9c | ||
|
|
bce6a9a1f4 | ||
|
|
0eb866d716 | ||
|
|
677c0c47ac | ||
|
|
250c9ae8b1 | ||
|
|
6a4d330a26 | ||
|
|
bdd9e898f4 | ||
|
|
ab4dfb4f9c | ||
|
|
ad9778476a | ||
|
|
820d1e52bf | ||
|
|
9a4ea10c64 | ||
|
|
70aec677ce | ||
|
|
80bc191593 | ||
|
|
c1353ce28f | ||
|
|
109d48747f | ||
|
|
84fd0a87aa | ||
|
|
805fcf179a | ||
|
|
e1e1331e44 | ||
|
|
243f328499 | ||
|
|
c8cd6c3a54 | ||
|
|
104bfe2d21 | ||
|
|
56c569f3a9 | ||
|
|
6200a41b71 | ||
|
|
614694ad4a | ||
|
|
4bafdb5d69 | ||
|
|
44d170413d | ||
|
|
997f89f879 | ||
|
|
d8898afa09 | ||
|
|
ca2290e691 | ||
|
|
7cb1bdace4 | ||
|
|
f149e24f28 | ||
|
|
793a807ce3 | ||
|
|
57509cd582 | ||
|
|
697fd6ba9b | ||
|
|
e0136213fe | ||
|
|
9bbc3a1467 | ||
|
|
284c6ae6fd | ||
|
|
213ac28c95 | ||
|
|
942bdece78 | ||
|
|
ade0a57353 | ||
|
|
94874107b6 | ||
|
|
33b4ae703c | ||
|
|
d8b95c0aab | ||
|
|
4e3f41a5a3 | ||
|
|
f23720f934 | ||
|
|
1a239c5eab | ||
|
|
673f85e4ab | ||
|
|
4e3edd7952 | ||
|
|
6d1189eadc | ||
|
|
21ff5e82ea | ||
|
|
1cfd124211 | ||
|
|
ee104f1b18 | ||
|
|
5fac5e380d | ||
|
|
92086129c5 | ||
|
|
49c31f1901 |
No files matched your search
+20
-1
@@ -75,6 +75,25 @@ font/src/**/*.ttc
|
||||
patch_v2/sdk/test_p1p2p3_repro
|
||||
|
||||
# ncm_app vendor 源码与构建产物(体积大;版本/获取方式见 VENDOR_NOTES.md,构建脚本可复现)
|
||||
patch_v2/ncm_app/vendor/
|
||||
# 例外:dr_flac 单头文件入库(§19 FLAC 解码依赖,532KB 单文件可入库)
|
||||
patch_v2/ncm_app/vendor/*
|
||||
!patch_v2/ncm_app/vendor/dr_flac/
|
||||
patch_v2/ncm_app/out/
|
||||
patch_v2/sdk/test_sdk_fixes
|
||||
|
||||
# SDK 矢量字(§10.1):FreeType vendor 源码/构建产物(体积大;版本 2.14.3,build_sdk.sh 可复现
|
||||
# 拉取+裁剪构建 -DFT_DISABLE_* 静态 musl)与宿主测试二进制
|
||||
patch_v2/sdk/vendor/
|
||||
patch_v2/sdk/test_meow_ui_ft
|
||||
|
||||
# 诊断工具构建产物(源码入库,二进制免)
|
||||
patch_v2/tools/sndprobe
|
||||
patch_v2/tools/tonepcm
|
||||
patch_v2/tools/tonepcm2
|
||||
patch_v2/ncm_app/cache_evict_sim
|
||||
patch_v2/ncm_app/pagination_sim
|
||||
patch_v2/ncm_app/audiod_chain_sim
|
||||
patch_v2/ncm_app/window_sim
|
||||
patch_v2/ncm_app/cover_chain_sim
|
||||
patch_v2/ncm_app/player_flow_sim
|
||||
patch_v2/ncm_app/degrade_sim
|
||||
@@ -74,10 +74,11 @@
|
||||
| `author` | — | 保留字段 |
|
||||
| `fullscreen` | — | app 级全屏:`1`=进入全屏(状态栏**收合至提示线** + **保留左缘返回/顶部提示线/下滑通知中心**,见 MEOWOS_SPEC_REF §5.1);activity 级用它 `route.<id>.fullscreen`。**app.json `fullscreen` 字段此前未解析/已定未做 → 本次实现**(launcher 启动/切 activity 据声明预设 SDK 全屏,env `MEOW_FULLSCREEN`)[源:用户 2026-08-30] |
|
||||
| `fonts` | — | 字库档位声明:`"full"` / `"gbk"` / `"common"`(app 需大字库时可声明 `"full"`,默认小字库 `common`≈4k;launcher 据此为该 app 加载大字库 `gbk`≈20k,从已扫描的 `MeowOS/fonts/<family>/<weight>/` 里按同 family(其次同 weight)选大字库,M2.6)。**注意**:与 `"fonts":["<handle>"]`(应用自带字库句柄数组)**同名二义**,见 MEOWOS_SPEC_REF §7 A-14 [源:用户 2026-08-30] |
|
||||
| `depends` | — | **二进制依赖声明**(launcher/nyai 启动前检查):`"curl,jq:optional,ffmpeg:required"`(JSON 值串)或行式 `depends=curl,jq:optional,ffmpeg:required`。逗号分隔多项,每项 `<name>[:<mod>]`;**无标注默认 required**,也接受 `:opt`/`:req` 缩写(=optional/required),未知修饰符按 required 处理并 WARN。App 某些复杂功能若依赖现成 Linux 二进制(**板载 `/usr/bin/<name>` 或卡上 `/usr/data/mnt/sd_0/MeowOS/opt/bin/<name>`,均按 X_OK 查**,两类任命中即满足)能效更好时声明。**launcher 启动前检查(仅启动时,不做周期复查)**:required 缺失 → **不启动**(toast「缺依赖: <name>」+ 日志,沿用 CANNOT START 风格);optional 缺失 → 仅 WARN 放行。[源:用户 2026-09-01] |
|
||||
|
||||
- 目录扫描深度 1 级:只枚举 `MeowAPPs/` 下的直接子目录
|
||||
- 应用计数上限 **8**(超出忽略;单屏 8 行)
|
||||
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":`、`"exec":`、`"package":` 后的引号字符串(及扩展 icon/framework/immersive/route./`awake.<id>`(keep_awake)/service.),**`fullscreen`(本次实现)与 `fonts`(档位串) 已解析**,不做 JSON 校验(多行/单行均可)[源:审计 2026-08-30][源:用户 2026-08-30]
|
||||
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":`、`"exec":`、`"package":` 后的引号字符串(及扩展 icon/framework/immersive/route./`awake.<id>`(keep_awake)/service./**`depends`**(二进制依赖,line 解析)),**`fullscreen`(本次实现)与 `fonts`(档位串) 已解析**,不做 JSON 校验(多行/单行均可)[源:审计 2026-08-30][源:用户 2026-08-30]
|
||||
- 列表刷新时机:每次菜单打开时重新扫描(TF 卡热插拔后立即可见)
|
||||
|
||||
### 2.2 边界情况行为(launcher 实现,全部真机验证)
|
||||
@@ -162,7 +163,7 @@
|
||||
|
||||
**约定(MeowOS 界面 = launcher 主菜单/选单/电源菜单,是唯一允许映射物理键的地方)**:
|
||||
|
||||
> ⚠️ **适用范围**:**除了电源键菜单(`show_power_menu`,允许"电源=确认、音量=导航")以外,所有按键信号由 MeowOS 全局接管**——**任何界面(含 MeowOS 自身的设置页 / 应用列表 / 选单、以及 MeowAPP)都不得私自定义按键行为**。物理键的语义只有一份:电源=息屏/亮屏/长按菜单/8s强关,音量=调系统音量,播放/上下曲=媒体控制(未落地前「抓住」)。任何界面(如系统设置页)**不得**把电源键/播放键映射成"返回/确认"这类自定义行为;界面导航一律走触摸(返回按钮/左缘左滑),按键语义统一由系统判。
|
||||
> ⚠️ **适用范围**:**除了电源键菜单(`show_power_menu`,允许"电源=确认、音量=导航")以外,所有按键信号由 MeowOS 全局接管**——**任何界面(含 MeowOS 自身的设置页 / 应用列表 / 选单、以及 MeowAPP)都不得私自定义按键行为**。物理键的语义只有一份:电源=息屏/亮屏/长按菜单/8s强关,音量=调系统音量,播放/上下曲=媒体控制(**已落地**,M2.81 起路由最近活跃媒体会话;无会话时「抓住」)。任何界面(如系统设置页)**不得**把电源键/播放键映射成"返回/确认"这类自定义行为;界面导航一律走触摸(返回按钮/左缘左滑),按键语义统一由系统判。
|
||||
|
||||
| 键 | 系统语义(APP 收到的是语义事件,不是裸 keycode) |
|
||||
|---|---|
|
||||
@@ -184,16 +185,16 @@
|
||||
|
||||
| 特性 | 行为 |
|
||||
|---|---|
|
||||
| 虚拟返回键 | 看门狗独立 mmap `/dev/fb0`(RGB565 自适应),每 100ms 重画左上角 48×48 返回按钮(深色底+白箭头);读 `/dev/input/event1` 触摸,按下落在按钮区 → `SIGTERM` 应用 → 1s 后 `SIGKILL`(应用卡死也能退出)。**v6.4 防抖**(针对"启动误杀"):`last_x/y=-1` 起始(无坐标的按下丢弃)、BTN_TOUCH 按实际值匹配边沿(消除失同步翻转)、仅短按 ≤600ms 有效、启动后先 drain 一次、**冷却期 2.5s**(实测幽灵触摸窗口 1.2–2.25s;此窗内返回键不响应,改 `COOLDOWN_MS` 可调) |
|
||||
| 沉浸模式 | 应用写 `/tmp/meowapp_state` 内容为 `1` → 看门狗不画按钮、只识别「左缘 24px 内起点向右滑 >50px」手势 → 发 `SIGUSR1`;写 `0` 或删除 → 恢复虚拟键 |
|
||||
| 虚拟返回键 | **已移除(M2.21 起)**:全局左缘返回手势取代左上角返回键,看门狗不再画按钮。历史行为:独立 mmap fb0 画 48×48 返回键 + 命中 SIGTERM(保留备查) |
|
||||
| 沉浸模式 | 应用写 `/tmp/meowapp_state` 内容为 `1` → 看门狗识别「左缘 24px 内起点向右滑 >50px」手势。**M2.89 起 SIGUSR1 只对「裸沉浸 App」(非 framework=1 且 immersive)发送**(236:SDK 框架 App 走 `on_swipe` 处理返回,发 SIGUSR1 会误杀——ncm signal=16 崩溃的根因);SDK 侧另有 SIGUSR1 护栏(`sigusr1_guard`,记日志+吞掉)双保险 |
|
||||
| 看门狗自退出 | 应用进程消失(`kill(pid,0)` ESRCH)或 launcher SIGTERM → 退出 |
|
||||
| 强制退出兜底 | 应用运行期(launcher 处于 waitpid):**音量减连按 3 次**(间隔 < `hotkey_seq_gap`,与主序列同阈值)→ SIGTERM → 0.5s → SIGKILL → 恢复。⚠️ 响应音量键的应用(如示例 hello)会先自行退出(`exit:key`),强杀序列是**不响应音量键/卡死应用**的兜底 |
|
||||
| 长按电源 | 硬件级强制关机(设备固有),最终兜底 |
|
||||
| 测试模式 | `meow_watchdog <pid> --sim-tap N` / `--sim-swipe N`,无触摸设备也可验证 |
|
||||
|
||||
**应用协议约定**:
|
||||
- 默认(非沉浸)由看门狗画返回键,应用**不必**处理返回手势
|
||||
- **⚠️ 返回键信号 = `SIGTERM`**(看门狗 `handle_touch` 按钮命中 → `kill(app_pid, SIGTERM)`→1s→`SIGKILL`);`SIGUSR1` **仅**是沉浸左缘滑动手势,不是返回键。com 与 SIGTERM 默认终止会**不画 BYE** 直接死,故示例 hello 安装 `SIGTERM` 处理器:画 BYE + `exit(0)`,使返回键呈现确定性的「bye → 立即退出」(v6.8,Bug 2)。
|
||||
- 默认(非沉浸)SDK 框架 App 由 SDK `on_swipe` 处理左缘返回;**看门狗虚拟返回键已移除(M2.21)**,返回语义全走手势
|
||||
- **⚠️ `SIGUSR1` 仅**是裸沉浸 App 的左缘滑动手势(M2.89 起看门狗不对 framework=1 的 SDK App 发),不是返回键;框架 App 另有 SDK `sigusr1_guard` 护栏吞掉漏网 SIGUSR1。裸 App 的 SIGTERM 默认终止会**不画 BYE** 直接死,故示例 hello 安装 `SIGTERM` 处理器:画 BYE + `exit(0)`(v6.8,Bug 2)。
|
||||
- **⚠️ 应用启动后须防 goodix 幽灵触摸**(v6.8,Bug 2 根因):goodix 在启动触摸的尾部发出一段幽灵触摸(实测启动后 1.2-2.25s,§18.16),无防护时应用会**误把幽灵当下按而立即退出画 BYE**,且 launcher 菜单会把同一幽灵重放成行点击而**重开应用**(用户看到「bye 后又回 hello 主界面、需再点一次」)。hello v6.8 增加 `COOLDOWN_MS=2500`:启动后前 2.5s 忽略一切按下边沿,只有用户**真正**的下按才触发退出。launcher 侧同步在应用退出后 drain + 冷却(见 §4.7)。
|
||||
- 沉浸应用写 `/tmp/meowapp_state`,处理 `SIGUSR1` 作为「返回事件」
|
||||
- 应用异常卡死 → 虚拟返回键(若可见)→ SIGKILL;沉浸态卡死 → 音量减连按 3 次(强杀序列)
|
||||
@@ -278,7 +279,7 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
|
||||
|
||||
### 4.5 统一日志系统(MeowAPPs 组件共用)
|
||||
|
||||
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowOS/logs/meow_<bootts>.log`(如 `meow_20260828_020123.log`;`bootts` = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件)。**系统日志**(launcher/watchdog 等 MeowOS 组件)写此文件;**应用日志**另按 `logs/<app_id>/<ts>_<pid>.log` 分文件(`<app_id>` 即该应用目录;见本 §4.5「组件环境变量」)。⚠️ `meow_bootmenu` 仍用 `MeowAPPs/logs`(代码侧不一致;bootmenu 与 launcher 日志目录**待统一为 `MeowOS/logs`**,[源:审计 2026-08-30])。旧布局 `logs/apps/<app_id>/logs/` 已废弃:launcher 启动时一次性迁移到 `logs/<app_id>/` 后删除
|
||||
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowOS/logs/meow_<bootts>.log`(如 `meow_20260828_020123.log`;`bootts` = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件)。**系统日志**(launcher/watchdog 等 MeowOS 组件)写此文件;**应用日志**另按 `logs/<app_id>/<ts>_<pid>.log` 分文件(`<app_id>` 即该应用目录;见本 §4.5「组件环境变量」)。`meow_bootmenu` 用 `logs/meow_boot_<bootts>.log`(同目录,文件名前缀区分——早期「仍用 MeowAPPs/logs」注记已过期,现统一在 `MeowOS/logs`)。旧布局 `logs/apps/<app_id>/logs/` 已废弃:launcher 启动时一次性迁移到 `logs/<app_id>/` 后删除
|
||||
- **无 TF 卡 / 卡晚挂载(S90 早于卡挂载)**:**开不了卡日志**——日志落 **/tmp 启动缓冲**(`/tmp/meow_boot.log`,上限 256KB 滚动),**绝不写板载存储**(/usr/data 只剩 ~10MB);**卡出现后自动迁移**(主循环 ~1s 周期重试 + 输入设备重试循环内也检查;无需开菜单)——`log_poll_card()` 打开卡日志并把 /tmp 缓冲整段搬入后删除 /tmp 文件
|
||||
- **容量上限**:logs/ 目录总量 1GB,开机时超限按 mtime 删最旧
|
||||
- **组件环境变量**:
|
||||
@@ -312,6 +313,8 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
|
||||
|
||||
## 5. 音频通路(未来应用的出声方案)
|
||||
|
||||
> **2026-09-01 更新**:常驻模式(MeowOS 主控、播放器未运行)下的出声方案**已由 `meow_audiod` 独占音频守护落地(M2.90)**——守护独占 hw:0,0、App 经 SDK `meow_audio_*` 喂 PCM(MEOW_APP_SDK §5.9 / `devdocs/plan/AUDIOD_DESIGN.md`)。本节下述 v3 候选为「会话覆盖模式(HibyOS 热键唤起、SIGSTOP 冻结播放器)」的历史推演,仅在该模式下仍有参考意义;常驻模式直开 hw:0,0 的旧结论见 §3.7。
|
||||
|
||||
参考 ANALYSIS.md 第 11 节结论:
|
||||
|
||||
- 声卡 = 内部唯一 card0(CS43198 双 DAC),**无混音**,谁打开谁独占;hiby_player 常驻持有 `hw:0,0`。
|
||||
@@ -456,10 +459,11 @@ Description: A simulated app
|
||||
|
||||
**与 launcher 一致**:install/export 复用 M2.12 `meow_pkg_install/export` 同套语义(tar.gz、gzip 管道、包名=文件名茎、同名先备份、校验 app.json、数据 init);exFAT 无符号链接,数据目录由 MeowOS 经 `MEOW_DATA_DIR/CACHE_DIR` 注入,CLI 只建目录不建链接。
|
||||
|
||||
### 3.7 音频通路与媒体键(2026-08-31 实测定案,覆盖 §5 旧推论)
|
||||
- **MeowOS 常驻模式(boot 直进,hiby_player 未运行)声卡空闲**:真机实测 `fuser /dev/snd/pcmC0D0p /dev/snd/controlC0` 无任何占用、`pidof hiby_player` 不存在——**MeowAPP 可直接 open `hw:0,0` 出声**(audio.output 设置键即此);无混音,后开者失败,音频焦点=先占先得(媒体会话/MPRIS 落地后再规范抢占语义)。
|
||||
### 3.7 音频通路与媒体键(2026-08-31 实测定案,覆盖 §5 旧推论;**M2.90 起被 meow_audiod 收口,见末段**)
|
||||
- **MeowOS 常驻模式(boot 直进,hiby_player 未运行)声卡空闲**:真机实测 `fuser /dev/snd/pcmC0D0p /dev/snd/controlC0` 无任何占用、`pidof hiby_player` 不存在。~~MeowAPP 可直接 open `hw:0,0` 出声~~ → **M2.90 起改走 `meow_audiod` 独占音频守护**:守护独占 hw:0,0,App 经 SDK `meow_audio_*` 客户端喂 PCM(见 MEOW_APP_SDK §5.9 / `devdocs/plan/AUDIOD_DESIGN.md`);独占+抢占(后开流赢,旧流收 PREEMPTED)、DAC 原生率跟随无 SRC、路由自动跟随插孔(meow_route 并入退役);App 直开仅 `MEOW_AUDIO_DIRECT=1` 调试用。媒体键路由最近活跃媒体会话不变。
|
||||
- **会话覆盖模式(HibyOS 运行中热键唤起)**:SIGSTOP 冻结播放器,声卡仍被其持有,App 无声(§5 旧结论,仅适用该模式)。
|
||||
- **物理媒体键路由**:MeowOS 与 HibyOS 不同时运行,不存在「转发给 hiby」问题;MeowOS 界面下播放/上下曲 = 全局媒体控制,路由给**最近活跃媒体会话**(**媒体会话已落地** M2.81:`MSG_META`(0x72)/`MSG_PLAYCTL`(0x73) + meow_media 最近活跃源(active_seq,30s 超时回收)+ 通知中心媒体卡片 + Now Playing 大页;OS.meow.ncm 是首真实媒体源),无会话时「抓住」不动作。
|
||||
- **物理媒体键路由**:MeowOS 与 HibyOS 不同时运行,不存在「转发给 hiby」问题;MeowOS 界面下播放/上下曲 = 全局媒体控制,路由给**最近活跃媒体会话**(**媒体会话已落地** M2.81:`MSG_META`(0x72)/`MSG_PLAYCTL`(0x73) + meow_media 最近活跃源(active_seq,30s 超时回收)+ 通知中心媒体卡片 + Now Playing 大页;**dev.player.ncm**(原 os.meow.ncm,agent-221 改名)是首真实媒体源),无会话时「抓住」不动作。**262 起物理播放/暂停键(164) 恒发 PLAYCTL `cmd=6`(toggle)**,由源侧按当前态翻转(修 SDK 本地态失真恒发「播放」)。
|
||||
- **audiod 加固批(M2.91-96,详见 MEOW_APP_SDK §5.9 / docs/MEOWOS_SPEC_REF §4)**:开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁);长流护栏 + ALSA 几何收窄 period=1024×4(根治长流 ~12s 停摆);音量反转四路统一(CS43198 寄存器=衰减量,`dac=255-pct*255/100`);`dac_vol_mute`=255 真静音(炸耳修复)。
|
||||
|
||||
### 3.8 版本号规则(2026-08-31 定案,对齐 Android)
|
||||
- `app.json` 增加 **`version_code`**(纯数字,单调递增,**安装/升级比较的唯一依据**);`version` 仅作显示字符串(versionName 角色,可为 `1.2.3-beta` 等任意形态)。
|
||||
|
||||
+8
-8
@@ -2,14 +2,14 @@
|
||||
|
||||
> 目标:把 R3 Pro II 打造成可扩展的"第二系统"。**MeowOS 作为主控层优先启动、拥有屏幕与输入;HibyOS 播放器降级为 MeowOS 启动的一个内置系统应用**(Android home/launcher 模型)。MeowLauncher(即 MeowOS 核心)、MeowAPPs、MeowPanel 均为其组成部分。
|
||||
> 本文档是路线图与分阶段计划。当前技术档案见 `ANALYSIS.md`,应用规范见 `APPS.md`,框架协议见 `FRAMEWORK_DESIGN.md`。
|
||||
> **⚠️ 本文档为公开路线图,自身已过时**:里程碑/阶段划分以**实际进度为准**(权威现状见 `devdocs/WORK_STATE.md`,本仓库 gitignore 不入库)。截至 **M2.83(0.7.78)** 已达成(M2.69→M2.83 全链路:launcher 拆分/控件库/系统框架专项服务/主界面 MD3 双布局/M3 导航栈+M5 通知跳转/媒体会话+Now Playing/SDK nav+media 封装/字库启动竞态修复);下方「当前主线」为实际推进序列,**按依赖因果排列,非平行**;旧「阶段拓扑序」仅作背景参考,勿按它安排开发。
|
||||
> **⚠️ 本文档为公开路线图,自身已过时**:里程碑/阶段划分以**实际进度为准**(权威现状见 `devdocs/WORK_STATE.md`,本仓库 gitignore 不入库)。截至 **M2.96(0.7.91)** 已达成(M2.69→M2.96 全链路:launcher 拆分/控件库/系统框架专项服务/主界面 MD3 双布局/M3 导航栈+M5 通知跳转/媒体会话+Now Playing/SDK nav+media 封装/字库启动竞态修复/**amsd 一期+二期(picker 契约)/签名鉴权 2B/流媒体客户端三期/MeowPanel 实时画面·遥屏/中文拼音 IME/开机选单抢屏回归修复/MIUIx 顶级主题/meow_audiod 独占音频守护(meow_route 并入退役)/流媒体客户端 A2 迁 audiod/M2.91-96 音频加固批(FPGA 开机武装根治 PCM 死锁·长流护栏+几何收窄·音量反转四路统一·炸耳修复)/充电态权威链/插孔消抖/amsd 注册自愈**);下方「当前主线」为实际推进序列,**按依赖因果排列,非平行**;旧「阶段拓扑序」仅作背景参考,勿按它安排开发。
|
||||
|
||||
---
|
||||
|
||||
## 当前主线(按依赖因果 [源:2026-08-30])
|
||||
|
||||
> 每项必须在其依赖落地后才推进(不在依赖未满足时抢先实现上层)。✅=已达成。
|
||||
> **进度注(2026-08-31)**:主线 1-7 已基本达成——阶段 B 通知中心 UI(M2.65+39)、启动器 SDK 改造(M2.69 拆分 + M2.79 主界面 MD3 双布局 + 视图框架)、系统基建线(M2.67+ svcd 子进程化 + sysui 总监 + 专项服务)、M3 导航栈+M5 通知整合+媒体会话(M2.80/M2.81)、SDK 缺失件(meow_ui_nav/media/config)、蓝牙/WiFi 子页(M2.76)、MeowPanel(M2.72+)。当前新主线见 `devdocs/WORK_STATE.md`(**在跑 agent-212 amsd 一期**)。
|
||||
> **进度注(2026-08-31)**:主线 1-7 已基本达成——阶段 B 通知中心 UI(M2.65+39)、启动器 SDK 改造(M2.69 拆分 + M2.79 主界面 MD3 双布局 + 视图框架)、系统基建线(M2.67+ svcd 子进程化 + sysui 总监 + 专项服务)、M3 导航栈+M5 通知整合+媒体会话(M2.80/M2.81)、SDK 缺失件(meow_ui_nav/media/config)、蓝牙/WiFi 子页(M2.76)、MeowPanel(M2.72+)。当前新主线见 `devdocs/WORK_STATE.md`(**在跑 agent-235 SDK 绘制性能回退 hello demo 滑动卡 / agent-236 顶部下滑手势分流+watchdog SIGUSR1 误杀框架 App**;amsd agent-212 一期+二期已落地)。
|
||||
|
||||
1. **阶段 B 通知中心 UI(进行中)** —— 依赖 M2 通知存储 + SDK 控件基座 + B2a 后端。顶部快捷按钮/亮度滑条走 `meow_settings_*` 零旁路;通知列表读 launcher `meow_notif`;手势(状态栏下拉开、上滑/点外关、左缘返回关)。
|
||||
2. **启动器 SDK 改造** —— 依赖上游 SDK 控件基座(阶段 B 沉淀的 MD3 基座)。状态栏收敛单一 SDK 全局(launcher 退役 `draw_statusbar`);主界面/设置页/电源菜单迁到 SDK 控件;MD3 化、日/夜主题、全局 UI 统一。(见 FRAMEWORK_DESIGN §10.4)
|
||||
@@ -91,7 +91,7 @@
|
||||
### 依赖链 B【UI 基建】
|
||||
```
|
||||
[渲染地基 M2.16 ✅]
|
||||
└─ [SDK/控件层 meow_ui, B1] ← 几乎全部 UI 的底座(设置页/通知中心/媒体播放界面/网易云都依赖它)
|
||||
└─ [SDK/控件层 meow_ui, B1] ← 几乎全部 UI 的底座(设置页/通知中心/媒体播放界面/流媒体客户端都依赖它)
|
||||
└─ [B2a 设置后端 meow_settings_* + settings.conf + listener] ← 设置的单一数据源
|
||||
└─ [B2b 实体设置页 UI] ──┐
|
||||
└─ [C MeowPanel Web 面板] ─┘(B2b 与 C 都消费同一 B2a 后端,天然镜像)
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
### 依赖链 D【应用主体】
|
||||
```
|
||||
[E 网易云客户端] ← 跑在链C 全链路(M3/M4/媒体会话)+ 链B(SDK)之上
|
||||
[E 流媒体客户端(dev.player.ncm)] ← 跑在链C 全链路(M3/M4/媒体会话)+ 链B(SDK)之上
|
||||
```
|
||||
|
||||
### 关键因果修正(相对旧平行表)
|
||||
@@ -132,8 +132,8 @@
|
||||
### 阶段 D(Activity/通知框架)—— 即链 C 的集合
|
||||
> 通知中心/MeowPanel/应用生态的前置。协议设计已就绪(FRAMEWORK_DESIGN.md)。实现:M0 IPC 骨架 → M1 路由表 → M2 通知存储 → M3 导航/后台服务 → M4 媒体会话。M0/M1 已完成(0.7.10,M2.15)。
|
||||
|
||||
### 阶段 E:网易云客户端(链接 C + 链 B 之后,跑在框架上)
|
||||
- 播放器「背光时间」需在应用内管理;应用退出按阶段 D 的导航语义。
|
||||
### 阶段 E:流媒体客户端(dev.player.ncm;链接 C + 链 B 之后,跑在框架上)
|
||||
- 播放器「背光时间」需在应用内管理;应用退出按阶段 D 的导航语义。**(已落地并迭代至三期续:core 库/复古 UI/歌单分页/封面/拼音搜索/设置页/缓存 LRU/滑动窗口/FLAC 无损;音频输出经 meow_audiod)**
|
||||
|
||||
### 阶段 F(可选项/远期)
|
||||
- HiByLink 协议逆向 → 播放器级无线遥控;音乐库数据库管理(sqlite3 CLI 已具备);更多应用(播客/TTS/时钟等)。
|
||||
@@ -169,7 +169,7 @@
|
||||
2. 阶段 B:状态栏升级 + 通知中心 + 快捷按钮 + 亮度滑条 + MD3 化
|
||||
3. 阶段 C:MeowPanel(含实时画面 + 远程控制菜单)
|
||||
4. 阶段 D:Activity/通知 IPC 框架(先协议文档 → FRAMEWORK_DESIGN.md 已就绪)
|
||||
5. 阶段 E:网易云客户端
|
||||
5. 阶段 E:流媒体客户端
|
||||
6. 阶段 F:HiByLink 逆向、更多应用
|
||||
|
||||
> 用户优先级:系统框架(阶段 0 + D)优先,MeowPanel 与网易云随后。
|
||||
> 用户优先级:系统框架(阶段 0 + D)优先,MeowPanel 与流媒体客户端随后。
|
||||
+12
-7
@@ -1,6 +1,6 @@
|
||||
# MeowOS 阶段 D:Activity / 通知 IPC 框架设计文档
|
||||
|
||||
> 状态:设计稿 v1(2026-08-28)。先出协议与架构,本阶段只设计、不实现。
|
||||
> 状态:协议/架构参考稿(2026-08-28 起草;**以下协议已随 M2.15-M2.96 各里程碑落地**,消息号以 `patch_v2/meow_launcherd/meow_ipc.h` 为准)。本阶段的「只设计、不实现」为起草期口径;M0-M3/M5/§10.5/媒体会话/amsd/meow_audiod(独占音频守护,规范 `devdocs/plan/AUDIOD_DESIGN.md`)已实现,**M4 后台服务保活+后台播放已落地(2026-09-01,见 §9 进度)**,M6 加固待排期。
|
||||
> 前置:ANALYSIS.md §18(Launcher 全架构)、§11(播放器 unix socket 先例)、APPS.md、DEVELOPMENT_PLAN.md 阶段 0/D。
|
||||
> 实现语言:启动器 C(musl 静态 MIPS32r2,zig-mipsel-cc);应用同为 C 静态 MIPS。
|
||||
> 补充:本文件由 plan 子代理产出设计,主会话落盘,并附「后台运行/后台播放权限」(用户追加)。
|
||||
@@ -58,8 +58,10 @@ struct ipc_hdr { /* LE */
|
||||
| 0x0032 | MSG_ACT_RESULT | ↓ | 返回链路 |
|
||||
| 0x0040 | MSG_NAV | ↑ | 上报导航/请求退出(**M3 导航栈已落地**) |
|
||||
| 0x0041 | MSG_SHOW_ACTIVITY | ↓ | 令前端应用显示某屏 |
|
||||
| 0x0070 | MSG_PICKER_CALL | ↑ | 请求跨 App picker 调用(发起方 A → 持有方 B;**仅预留协议位**,本期不实现) |
|
||||
| 0x0071 | MSG_PICKER_RESULT | ↓ | picker 返回值(候选结果回 A;仅预留协议位) |
|
||||
| 0x0070 | MSG_PICKER_CALL | ↑ | 请求跨 App picker 调用(发起方 A → 持有方 B;**已实现**,amsd 二期 agent-225:`am picker`/`picker-status`,files_app 注册 `pick_file`) |
|
||||
| 0x0071 | MSG_PICKER_RESULT | ↓ | picker 返回值(候选结果回 A;**已实现**,amsd 二期) |
|
||||
| 0x00A0 | MSG_AMS_START | → | amsd→launcher:请求拉起某 App(pkg + 可选 activity,含 picker 上下文 `MEOW_PICKER_*`;**已实现** agent-212/225) |
|
||||
| 0x00A1 | MSG_AMS_KILL | → | amsd→launcher:请求终止某 App(SIGTERM 语义;**已实现**) |
|
||||
| 0x0072 | MSG_META | ↑ | 媒体会话上报元数据/进度/状态(**附录 B 已落地**,最近活跃源回收) |
|
||||
| 0x0073 | MSG_PLAYCTL | ↓ | 播放控制下发 / ↑ 控制器转发到最近活跃媒体会话(**已落地**) |
|
||||
| 0x00F0 | MSG_SYS | ↓ | 系统事件(守护关闭/服务死亡/播放器状态) |
|
||||
@@ -115,8 +117,10 @@ M0 IPC 骨架(含 meow_ipc 测试钩子)→ M1 路由表+解析 → M2 通
|
||||
### 进度(阶段 D)
|
||||
- ✅ **M0 IPC 骨架**(0.7.10,M2.15):`meow_launcherd` 加 IPC 线程 + AF_UNIX `/usr/data/meow_ipc.sock`(unlink+bind+chmod0666+listen4)+ 帧解析(16B 头 magic=0x4D4F5701/version=1/type/req_id/len,LE)+ `MSG_HELLO/ACK/ERR` + eventfd 唤醒主循环 + 无锁 SPSC 内存环(`__sync`)+ 日志埋点。遵守 §1/§6.2:IPC 线程只读帧+解析+入队,主循环 `meow_ipc_process()` 单写者改状态,跨线程 eventfd,主循环 10ms 热键轮询每圈非阻塞排空(eventfd 失败时 10ms 轮询兜底)。测试钩子:`meow_ipc`(`patch_v2/meow_launcherd/meow_ipc.c`,静态 MIPS,与 evtool 同定位)+ launcher `--ipc-only N [--ipc-sock <path>]`。验证:`meow_ipc hello`→ACK、播放器不动(MeowOS 模式下无播放器即满足)。
|
||||
- ✅ **M1 路由表 + 解析**(0.7.10,M2.15):`app.json` 扁平行 `route.<id>=path` / `framework=true` / `service.<svc>=mode:restart:max:window_ms` / `icon` 解析入 `g_accts` 注册表(主循环单写者);`MSG_ACT_DECLARE`(运行时并集覆盖静态)/`MSG_ACT_QUERY`/`MSG_ACT_RESULT`(深度+链路);`resolve_chain(app,activity)` 返回 `[root..target]` 链路与深度——`route` 缺失(app 未声明路由)→ 深度 1 兜底 `chain=activity`;声明了路由但 activity 非 route 键 → `MEOW_ERR_NOENT`(activity 未注册)。验证:`meow_ipc query <app> <activity>` 返回链路(qemu + 真机 MeowOS 模式均通)。
|
||||
- ✅ **M2 通知存储+分发**、**M3 导航+前台会话改造+看门狗返回**、**M5 通知中心 UI 整合**(详见下,M2.65-M2.82 已落地);⬜ **M4 后台服务保活**(`MSG_REGISTER` 后台服务登记)、**M6 加固**:待排期。
|
||||
- **M3 已落地(M2.80,agent-202 · 85b4aea)**:`nav_stack`(≤16 深)+ `MSG_NAV` REPORT/EXIT + `launch_activity` 链路注入(env `MEOW_ACTIVITY`/`MEOW_CHAIN`)+ 栈根退出回主屏;通知点击按 `route_status`(0 拉起/1 NO ROUTE/2 SOURCE NOT FOUND/-1 NONE 关面板不动作);picker 协议位预留(0x0070/0x0071);裸 App 兼容。
|
||||
- ✅ **M2 通知存储+分发**、**M3 导航+前台会话改造+看门狗返回**、**M5 通知中心 UI 整合**(详见下,M2.65-M2.82 已落地);✅ **M4 后台服务保活**(2026-09-01 落地,见下);⬜ **M6 加固**:待排期。
|
||||
- **M4 已落地(2026-09-01)**:`app.json` 新增 `background`/`background_audio` 解析入注册表(`struct app`/`meow_acct` 同名字段);`MSG_REGISTER`/`MSG_SERVICE_OK`/`MSG_SERVICE_STOP`/`MSG_SYS` 负载结构落地(`m_register_t`/`m_service_stop_t`/`m_sys_t`,MEOW_SYS_SHUTDOWN=1/SVC_DEAD=2);新模块 `meow_bgplay.c/h`(自包含+ops 注入,宿主可单测):**后台播放保活**——声明 `background_audio` 且媒体会话 PLAYING 的 App nav-exit 时**不终止**,进程+音频流保留(UI 冻结走 §10.5 `/tmp/meow_notif_active` SDK present 自抑,**非字面 SIGSTOP**:冻结同进程播放线程会断供 PCM,§10.5 真机实证),点图标/媒体卡片回前台=原进程恢复(chain/picker 忽略),停播后再退出=真退 SIGTERM;**无头后台服务**——MSG_REGISTER spawn/stop/reap + 退避重启 1/2/4/10s cap + `max_restart` 熔断(清该 App 通知 + 注册连接广播 MSG_SYS{SVC_DEAD},重注册复位熔断);卡拔例外(二进制在卡上的后台 App/服务仍 SIGTERM 防 SIGBUS)、崩溃照收、关机前 `meow_bgplay_kill_all` 有界回收(含 MSG_SYS{SHUTDOWN} 广播);主界面应用列表「·后台」标注(1s tick 按 change_seq 刷新);tick 挂载点=`meow_ipc_process` 末尾(主循环单写者排空点)。宿主单测 `test_bgplay.c` 50 项全过;amsd 侧 BG 状态展示=§6.3 顺延。
|
||||
- **M3 已落地(M2.80,agent-202 · 85b4aea)**:`nav_stack`(≤16 深)+ `MSG_NAV` REPORT/EXIT + `launch_activity` 链路注入(env `MEOW_ACTIVITY`/`MEOW_CHAIN`)+ 栈根退出回主屏;通知点击按 `route_status`(0 拉起/1 NO ROUTE/2 SOURCE NOT FOUND/-1 NONE 关面板不动作);裸 App 兼容。
|
||||
- **amsd(一期 agent-212 · 5fbf307 + 二期 agent-225)已落地**:活动管理器守护(svcd 托管 `trigger=boot`)+ `am` CLI(status/list/history/start/kill/`resolve`/`picker`)。二期将路由表迁 amsd(amsd 路由权威)+ **picker 契约**(`MSG_PICKER_CALL=0x70`/`MSG_PICKER_RESULT=0x71`,`am picker`/`picker-status`;files_app 注册 `picker.pick_file` 选文件返回路径)+ `MSG_AMS_START/KILL=0x00A0/0x00A1`(amsd→launcher 决策执行)。参见 `devdocs/plan/AMSD_DESIGN.md`。
|
||||
- **M5 通知中心 UI 整合已落地(M2.65-M2.81)**:通知中心 + 媒体卡片 + Now Playing(媒体会话见附录 B,已落地:`meow_media.c` 最近活跃源、30s 超时回收、`MSG_META`/`MSG_PLAYCTL`)。
|
||||
- **§10.5 App 前台唤起通知中心已落地(M2.82,agent-206 · 9b41957)**:`MSG_NOTIF_OPEN=0x28`/`CLOSE=0x29` + SDK `meow_ui_nav`/`meow_ui_media` 封装。
|
||||
- ✅ **渲染地基(0.7.11,M2.16,前置基座)**:所有 UI 统一走 **RAM 离屏后备缓冲 `back_buf`(480×720×2=691,200B) + `meow_present`(dirty)** = `FBIO_WAITFORVSYNC` 等垂直同步 + 单次原子 memcpy(脏区或全帧)到 fb0(buffer0,yoffset=0)——无渐进/无中间态/无撕裂。脏区 `meow_dirty_add`(矩形合并集、相交吸 bbox、>12 转全屏、裁可视区);状态栏时钟/右侧格按需局部标脏。整合铺垫:框架后续 LAUNCH/NOTIFY/前台会话的绘制复用该机制(而非旧「全屏 clear→重绘到 buffer0」路径)。详情见 ANALYSIS.md §18.43。刷新率调度(display.refresh_rate 60/30/15 + refresh_always_max + 自适应降档 governor)归口 PERF_SCHEDULER.md。
|
||||
@@ -144,6 +148,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最
|
||||
├─ meow_launcherd 系统 UI 渲染宿主(主界面/设置页/电源菜单/全局状态栏/通知中心/全局手势)
|
||||
├─ meow_svcd 服务管理器(服务=独立子进程;逐个子进程崩溃隔离)
|
||||
│ ├─ meow_mdns mDNS 应答器(板载 /usr/data/meow/services.d/mdnsd.conf,trigger=boot)
|
||||
│ ├─ meow_audiod 独占音频守护(M2.90;独占 hw:0,0+抢占+路由并入;HibyOS 模式门控 exit(0) 不启动——规范 devdocs/plan/AUDIOD_DESIGN.md)
|
||||
│ └─ 未来媒体服务 / 其它 services.d 服务(TF MeowOS/services.d/…)
|
||||
├─ 未来媒体服务 / MeowPanel(后置)
|
||||
```
|
||||
@@ -253,7 +258,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最
|
||||
|
||||
## 附录 B(用户追加):默认播放控制界面 / 正在播放(Media Session)
|
||||
|
||||
**需求**:MeowOS 启动器内置一个**默认播放控制界面**(Now Playing)。其他播放类应用(如网易云)在后台播放时,通过接口提供**实时播放数据**(元数据、歌词、封面等),供启动器的播放界面显示;启动器界面也能向应用下发**播放控制指令**(播放/暂停/上一首/下一首/进度)。
|
||||
**需求**:MeowOS 启动器内置一个**默认播放控制界面**(Now Playing)。其他播放类应用(如流媒体客户端 dev.player.ncm)在后台播放时,通过接口提供**实时播放数据**(元数据、歌词、封面等),供启动器的播放界面显示;启动器界面也能向应用下发**播放控制指令**(播放/暂停/上一首/下一首/进度)。
|
||||
|
||||
**设计**(在 §1 IPC 基础上新增"媒体会话"消息):
|
||||
- **上报(应用→启动器)`MSG_META`**:实时元数据。负载:
|
||||
@@ -274,7 +279,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最
|
||||
- 流式更新(播放位置/进度)用高频 `MSG_META`(或拆出轻量 `MSG_PROGRESS`,由应用按 ~1s 推送);元数据变化低频。
|
||||
- **指令(启动器→应用)`MSG_PLAYCTL`**:
|
||||
```c
|
||||
struct m_playctl { uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek */ int64_t seek_ms; };
|
||||
struct m_playctl { uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停toggle(物理键164恒发6,由源侧按当前态翻转;262) */ int64_t seek_ms; };
|
||||
```
|
||||
- 启动器播放界面上的按钮/滑条 → 通过该应用的**服务连接**下发。
|
||||
- **注册**:播放应用声明 `MF_PLAYBACK` 并注册**媒体会话**(`MSG_REGISTER` 带 `svc=media_session` 或 `HELLO.flags` 含 PLAYBACK)→ 成为当前"正在播放"提供的源。
|
||||
|
||||
@@ -95,7 +95,10 @@ watchdog 主循环(`for(;;)`)加按键轮询(`read /dev/input/event0/event
|
||||
偏差(实现以稳妥为准,记录如下):
|
||||
1. **8s 强关拆分**:设计给 watchdog,但为避免与菜单内 `pm_interacted` 冲突(用户正点选时被强关抢先),落地为「launcher 菜单内 8s 强关(pm_interacted 保护)+ watchdog 8s 兜底(仅当菜单未接管,即 `/tmp/meow_power_menu_active` 未出现)」。
|
||||
2. **音量跳步**:watchdog 每按下边沿按 `audio.volume_step` 一步(不做 launcher 的线性渐变);短按与 launcher 菜单一致,长按不连续爬(媒体落地后可补)。launcher 应用返回时 `settings_load()` 同步。
|
||||
3. **左右滑切屏**:菜单触摸段已留 `menu swipe (dx,dy) — no launch` 判定,未实现切换逻辑。
|
||||
4. **媒体键**:仅抓消费(日志 `media key captured (code N) -> consumed, no route`),路由待阶段 D。
|
||||
3. **左右滑切屏**:**已落地(M2.81 Now Playing 横划页)**——主界面左滑进 Now Playing 大页;左缘返回手势保留。此处旧注「未实现切换逻辑」作废。
|
||||
4. **媒体键**:**已落地(M2.81 起路由最近活跃媒体会话)**——播放/上下曲路由到最近活跃媒体会话(MSG_PLAYCTL),无会话时「抓住」不动作。此处旧注「路由待阶段 D」作废。
|
||||
5. **插孔消抖(2026-09-01,真机日志实证)**:4.4/3.5 插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住,launcher vol_ramp 曾从 24% 一路爬到上限 50(日志 225391 ramp 起 → 227213 爬满 → 紧随 audiod「jack changed」)。修:launcher 与 watchdog 音量键处理统一读 `/sys/class/switch/{headset,balance}/state`(节流读缓存:launcher 1s / watchdog 500ms,绝不在热路径每圈读文件),插孔状态变化后 **800ms 内忽略音量键边沿**(vol_ramp_begin 不起、在爬 ramp 中止、watchdog 单步忽略);另有 **ramp 总时长上限 VOL_RAMP_MAX_MS=3000** 兜底,防任何「持续按住」假象一次打满。~~SDK 输入层需同款消抖~~ → **SDK 侧已补齐(56c569f,meow_ui.a 12cdd5d7,hello/files/流媒体客户端重链热更)**:框架 App 前台时的音量键路径同款 800ms 消抖窗。
|
||||
6. **音量反转统一(M2.95,2026-09-01)**:CS43198 音量寄存器 = **衰减量**(0=最响 255=静音),旧映射方向反(0 档炸耳、50 档最低)。launcher/audio.c + audiod + **watchdog `wd_vol_adj`** + SDK vol 四路统一 `dac = 255 - pct*255/100`。**watchdog 存盘修复(M2.93)**:`wd_vol_save` 旧 O_TRUNC 整文件重写 settings.conf 曾毁配置(整文件被抹/重复行/丢 max+step)→ 改读改写单键+去重(与 SDK 同款语义)。
|
||||
7. **播放/暂停键(164) toggle(262,2026-09-01)**:SDK `meow_ui_media` 旧按本地 `g_last_state` 判 play/pause,流媒体客户端走自有 ipc_media 线程不经 SDK meta → 恒发 cmd=1 播放、播放中按键无效。修:164 **恒发 `MSG_PLAYCTL cmd=6`(toggle)**,由源侧按当前态翻转(launcher 透传不动结构;媒体卡片/NP 页按钮按 launcher 侧 META state 判,本就准,维持现状)。同批修:App 前台状态栏音量%/亮度实时刷新(launcher 叠加层 1s 节流 stat settings.conf,mtime 变才重读同步 g_sys)。
|
||||
|
||||
真机验收要点(§7):应用内电源单击/长按菜单/8s 强关、音量±调音量、播放/上下曲无反应(被抓住)、launcher 菜单同样一致、电源菜单任何界面可弹(含应用内)、息屏后任意键唤醒。
|
||||
+885
-19
File diff suppressed because it is too large.
Load diff
@@ -3,9 +3,18 @@
|
||||
> 把 HiBy R3 Pro II 便携播放器(君正 X1600,MIPS32r2 / mipsel / musl 静态软浮点,480×720,无 GPU 软渲染)变成一个可扩展的「第二系统 + 应用框架」。
|
||||
> MeowOS 与官方 HibyOS 共存于同一台设备:开机由引导选单二选一;启动后 MeowOS 作为主控层,提供自己的桌面 UI、应用框架、服务管理与系统组件。
|
||||
>
|
||||
> 版本:**M2.83(0.7.78)**(M2.69→M2.83 全链路自刷验收:控件库/主界面 MD3 双布局/M3 导航栈/M5 通知跳转/媒体会话/Now Playing/SDK nav+media 封装/字库启动竞态修复)。版本号单点 = 板上 `/etc/meowos_version`,见「构建」。本仓库是一个**自带 SDK、包管理、媒体会话、网页面板、网易云客户端的完整第三方系统**。
|
||||
> 版本:**M6.35(0.8.35,已上卡)**。M6.19→M6.35 连续十七版推卡自刷验收——**M6.19**:ncm 后台化 UI 叠层/点击穿透+再进 ncm 桌面叠层+播放页新旧 UI 叠层抢刷脏区(软自抑帧移交不完整,恢复边沿强制整帧重画)+PIN 亮屏亮度跳+通知中心封面显歌单封面;**M6.20**:播放页右滑返回+顶部下滑通知中心+状态栏时钟 1s;**M6.21-22**:播放页返回修复(`np_exit` 退出外层主循环)+方向感知手势;**M6.23**:watchdog 亮屏亮度按用户设置恢复;**M6.24**:App 内按电源键锁屏时序;**M6.25**:bootmenu 开机菜单亮度方案 A;**M6.26**:播放页三键命中+媒体卡片体 FOCUS;**M6.27**:netmgrd 流量统计收编(pub_bytes/去 lo)+面板公网流量卡;**M6.28**:收编桌面 Now Playing 双页歌词页+歌词数据源+蓝牙 P1(netmgrd BT_CTL/bt_page);**M6.29**:播放页任意右滑返回桌面;**M6.30**:收编批5 取色+蓝牙 P2(audiod glibc-dynamic)+面板蓝牙卡;**M6.31**:桌面歌词页封面修复+alsa.conf DEV 自动识别;**M6.32**:重编 meow_ui.a+重链四 App(播放页进度条取色+ncm 列表 gap)+面板透传;**M6.33**:收编 launcher 播放页占位符移除(已有独立歌词页)+封面「空→复原」恢复修复(test_np_cover 14 项 PASS);**M6.34**:收编**批①后台常驻+宽限期**(全 App 入后台表+30min `MEOW_BG_GRACE_MS` 宽限超时回收+声明常驻/媒体活跃无限期保活+槽满 LRU+app.json `background_keepalive` 三权限位+amsd `EV BG/FG` 上报)+**audiod A3/DSD**(24/32bit 高清 byte-exact+独立 `AUDIO_FMT_DOP` DoP DSD 透传+音量收编 v2 双组写)+**meow_img_pixels 访问器上收**+**面板拆分+屏幕·遥控合并页**(前端三层+左屏右遥控 MD3 SVG 矢量按钮);**M6.35**:收编**批③后台管理页**(桌面顶栏「后台」入口+`bg_page` 后台列表+每行「杀」按钮+顶栏「一键清」,`!framework` 门控不杀框架)+**amsd FG/BG 收编**(`57e19cde`)+**i18n settings/lock 补抽**。M6.35 之后工作区已提交(暂未打包,并入 M6.36):批①叠加/穿透根治(`/tmp/meow_notif_active` 内容=前台归属 app_id+SDK present/input 共享判定+launcher 7 处转场)、主界面顶栏后台按钮图标美化、主界面列表圆角卡 8px 外距、后台管理页行 MD3 圆角卡;**MD3 对齐在跑**(agent-435,参考 `third_party/md-material-web` 最新 M3)。M6.10→M6.18:M7 launcher 重写第 1-5 期(M6.10-14:虚拟化宫格/设置页卡顿修复/OOM 通用化/自动切歌根治)+(M6.15)meowpanel jv_free 泄漏修复+(M6.16)Hello demo 滚动卡顿修复+(M6.17)歌词 UI+(M6.18)媒体通知封面+封面分辨率。M6.1→M6.9 全链自刷验收——**M6.1**:netmgrd 期3/powermgr 期2/面板 PIN 鉴权+首页仪表盘/WiFi 录入 UI/服务名统一 meow_*;**M6.2**:锁屏 PIN 全量(`system.pin` 同源面板)+VOL_DOWN×3 框架门控+显示名 name[49](amsd/launcher UTF-8 整序列截断);**M6.3**:设置页 5s 修复(309 网络异步步骤机+跳过字体 sha 校验)+310 锁屏三连(输入穿透/解锁状态栏消失/App 死亡残影);**M6.4**:310 二期 **EVIOCGRAB 锁屏独占触控**(未冻结 App 锁屏输入隔离)+主界面应用名截断;**M6.5**:audiod 315 孤儿会话两段式回收(客户端崩溃/OOM 杀/正常退出后会话完整回收,STATE 回干净无流基线);**M6.6**:锁屏 310 三期——App 内手动息屏也触发锁屏 + 锁屏脏区全屏不透明底 + 300ms 重铺;**M6.7**:锁屏页自睡(空闲超时自动灭屏,294 遗留补;0.8.7 补刷吸收 fw_content 事故修复——拆分 watchdog+meow_audio 两行);**M6.8**:310 四轮 锁屏自睡 min(系统息屏,30s);**M6.9**:netmgrd 期4(SDK 自愿通道/forget/RSSI/AAAA 过滤/iptables 评估)+powermgr 期3(policy/标定工具)固化 + 327 M7.0b launcher 试点(电源菜单/Now Playing 控件化+主题薄壳+i18n 抽串);历史:M2.69→M2.96 全链路自刷验收:控件库/主界面 MD3 双布局/M3 导航栈/M5 通知跳转/媒体会话/Now Playing/SDK nav+media 封装/字库启动竞态修复**/M2.84-88**:installed 图标修复+残帧pan加固/物理键EVIOCGKEY双源/签名鉴权 2B/meow_route 音频路由/amsd 二期(picker 契约)/流媒体客户端二期(封面·歌词后台·大歌单分页)/MeowPanel 实时画面+遥控+通知实时化/中文拼音 IME**/M2.89**:开机选单抢屏回归修复+watchdog SIGUSR1 不误杀框架 App+amsd 失联修复+顶部下滑手势分流**/M2.90**:**meow_audiod 独占音频守护**(AUDIOD_DESIGN,meow_route 并入退役)+MIUIx 顶级主题+流媒体客户端文案中文化+面板字体库 tar.gz 导入导出**/M2.91-96**:audiod 开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁)+svc_power 充电态权威链(mp2731)+amsd 注册自愈(`am rescan`)/Now Line truncated
|
||||
>
|
||||
> 许可证:Apache-2.0(见 [`LICENSE`](LICENSE))。本仓库为非官方第三方项目。
|
||||
> 主仓**公开可克隆**:`https://git.yeij.top/AskaEth/MeowOS-For-R3P2.git`(维护者推送走
|
||||
> `ssh://git@git.yeij.top:2222/AskaEth/MeowOS-For-R3P2.git`)。
|
||||
> **第三方组件声明(LGPL)**:阅读器 `os.meow.reader` 静态链接 **libmobi(LGPL-3.0)**。
|
||||
> 其 NOTICE + GPL-3.0 + LGPL-3.0 全文随 `.mpk` 的 `resources/licenses/` 分发(见
|
||||
> `patch_v2/reader_app/NOTICE`、`patch_v2/reader_app/resources/licenses/`);libmobi 在
|
||||
> `build_reader.sh` 锁定 commit `906274205c11944b628da1c553b255acb1af7c55`;替换/重链指引见
|
||||
> `patch_v2/reader_app/README_LGPL_RELINK.md`。阅读器自有代码仍为 Apache-2.0(LGPL §1 豁免)。
|
||||
>
|
||||
> **公开 SDK 仓(面向第三方开发者)**:`ssh://git@git.yeij.top:2222/AskaEth/MeowOS-SDK.git`(MIT;只含 `sdk/`(meow_ui)+ `examples/hello/` + `docs/`,自包含可构建,不含系统组件/密钥)。由本仓经 `git subtree split` 拆出,增量同步见 `scripts/publish_sdk.sh`。
|
||||
|
||||
---
|
||||
|
||||
@@ -15,9 +24,10 @@
|
||||
|
||||
- **双系统共存**:官方 HibyOS 原样保留;开机引导选单决定本次进入哪个系统(MeowOS / HibyOS),两者互斥运行,任一时刻只跑一个。
|
||||
- **应用框架**:一套 C 应用 SDK(`meow_ui`),第三方 App 用统一控件 / 输入 / 主题 / 字体 / 通知 API 开发,无需自读帧缓冲、自读输入、自管返回——把每个 App 都要踩一遍的坑收口到框架里。
|
||||
- **系统组件**:状态栏、通知中心、电源菜单、全局左缘返回手势、主界面(应用列表,**MD3 卡片宫格/列表双布局**)、设置页、服务管理器、mDNS 广播;系统进程总监 `meow_sysui`;专项子服务 `meow_svc_perf`(性能/系统只读聚合,含真实 network)与 `meow_svc_power`(关机/重启/升级/回 HiBy,含升级写保护 + dac 真实音量 + 读级鉴权放宽);系统级崩溃捕获 `meow_crash` + App 崩溃直报;.mpk **Ed25519 签名验签** `meow_verify`(见 §2.9);MeowPanel 外部管理面板(HTTP/JSON 后端 + Web 前端,支持可选密码鉴权,见 §2.10)。
|
||||
- **媒体会话 / Now Playing**:`MSG_META`/`MSG_PLAYCTL`(0x72/0x73)媒体会话(最近活跃源,30s 超时回收)+ 通知中心**媒体卡片** + 主界面左滑 **Now Playing 大页**;媒体键路由到最近活跃媒体会话。
|
||||
- **应用生态**:**网易云 os.meow.ncm**(`core/`:mbedtls+jsmn+qrcodegen+minimp3 ≈438KB;v1 复古 UI:登录二维码/我创建的·我收藏的双页签/搜索/歌单详情/黑胶播放页/设置,ALSA 直开 hw:0,0 播放,登录 803 已通);外置**系统文件管理器 os.meow.files**;`meow_ui` SDK 配套结构化列表 `W_LISTVIEW`、通用长按、SDK nav/media 封装、视图框架(可嵌套 view + 事件路由 + 绘制期 clip);App 通知查询/清除 IPC(0x25-27)与**点击通知跳转路由**(route_status)。
|
||||
- **系统组件**:状态栏、通知中心、电源菜单、全局左缘返回手势、主界面(应用列表,**MD3/MIUIx 顶级主题×明暗**,卡片宫格/列表双布局)、设置页、服务管理器、mDNS 广播;系统进程总监 `meow_sysui`;专项子服务 `meow_svc_perf`(性能/系统只读聚合,含真实 network)与 `meow_svc_power`(关机/重启/升级/回 HiBy,含升级写保护 + dac 真实音量 + 读级鉴权放宽);**独占音频守护 `meow_audiod`**(M2.90:App 经 SDK `meow_audio_*` 喂 PCM,守护独占 `hw:0,0`——DAC 原生率跟随无 SRC、格式归一、4.4/3.5 路由自动跟随插孔、抢占+EOF 回收、CLI status/tone/route/vol,规范 `devdocs/plan/AUDIOD_DESIGN.md`);系统级崩溃捕获 `meow_crash` + App 崩溃直报;.mpk **Ed25519 签名验签** `meow_verify`(见 §2.9);**网络管理服务 `meow_netmgrd`**(M6.0 期1 / M6.1 期3:接口统一层/连接普查三实体/DNS 代理/策略引擎防自锁,HibyOS 只读)与**功耗管理服务 `meow_powermgr`**(M6.0 期1 / M6.1 期2:功耗查看/分解/历史/事件,HibyOS 只读);MeowPanel 外部管理面板(HTTP/JSON 后端 + Web 前端,支持可选密码鉴权 + 4 位 PIN 访问级鉴权 + 首页仪表盘 + 字体库 tar.gz 导入导出,见 §2.10)。
|
||||
- **媒体会话 / Now Playing**:`MSG_META`/`MSG_PLAYCTL`(0x72/0x73)媒体会话(最近活跃源,30s 超时回收)+ 通知中心**媒体卡片** + 主界面左滑 **Now Playing 大页**(播放页⇄歌词页双页,方向感知手势:播放页右→左进歌词/左缘右滑回桌面、歌词页左→右回播放);媒体键路由到最近活跃媒体会话。
|
||||
- **App 后台常驻框架(M6.34 起,`devdocs/plan/APP_BACKGROUND_FRAMEWORK.md`)**:App 返回桌面**不立即杀**——`meow_media_app_resident` 按 App 自身会话判活跃 + `meow_bg_should_keep` 三参(keepalive/媒体活跃/声明)+ app.json **`background_keepalive`** 声明 + amsd `EV BG/FG` 上报;**宽限期**`MEOW_BG_GRACE_MS`(30min)全 App 入后台表,未声明超时回收(SIGTERM→SIGKILL 真退序列),声明常驻/媒体活跃**无限期保活**,槽满 LRU;**批③后台管理页**(M6.35,桌面顶栏「后台」入口):列出每个后台常驻 App(名字+CPU/内存+状态)+ 每行「杀」按钮 + 顶栏「一键清」(`!framework` 门控不杀框架 App),数据源=amsd FG/BG + svc_perf 同口径 /proc 采样。批②底部上滑回桌面+home 指示条待做、批④面板后台列表排队。
|
||||
- **应用生态**:**流媒体客户端 dev.player.ncm**(`core/`:mbedtls+jsmn+qrcodegen+minimp3 ≈438KB;复古 UI:登录二维码/我创建的·我收藏的双页签/搜索/歌单详情/黑胶播放页/设置,登录 803 已通;二期:专辑封面/歌词后台加载/大歌单分页;三期:歌单真分页/真实专辑封面/歌词锁修复/搜索接拼音 IME 中文搜索/设置页(音质 5 档+缓存上限+清理缓存)/缓存 LRU 淘汰接生产路径/文案全中文;三期续:歌曲对象瘦身 1324B→608B+256 首滑动窗口(任意长度歌单)/封面与缓冲 UX 修复(消除 worker 忙等 100% CPU)/FLAC 无损(dr_flac)+缓存键=歌曲 ID+音质档位/物理播放键 toggle 全链路;2026-09-02 批次:真机四连症状(豆腐/歌词一行/封面加载/音量键 SIGKILL)+返回首页竞态 staging+内存瘦身(封面 8.28MB→0.78MB)+流媒体专项六项(档位保真/封面 4xx 不重试/收藏显隐/缓存命名迁移/topbar 位图图标)+三死因闭环(OOM/误杀/歌词栈溢出)+封面链分诊+风控加固(接口 120s 缓存+游客令牌)+**M6.10 批次**(批B UI/批C 播放后台/338 VIP3 档+多歌手/336 根因——每次 audio_out_open 建短连接致 audiod fd 风暴已修;卡上热更 `bef4b0e1`,内存 40→49.2MB OOM 疑 page cache 待真机取证);**2026-09-03 桌面歌词/取色/蓝牙批**:桌面 Now Playing「播放页⇄歌词页」双页+歌词链路批1-5(SDK `meow_lyric` 通用歌词组件+`meow_lyric_view` 歌词页渲染控件+media 会话歌词数据源 `meow_media_lyric_*`/MSG_META_LYRIC+方向感知手势)+封面双色渐变取色(`meow_img_color`)+ncm 底部 mini 播放栏/播放模式三态+实时播放列表/列表项 8px 外距;**蓝牙 P0/P1/P2**(`aplay -D bluealsa` 真机出声打通/netmgrd BT_CTL 权威收口+launcher 蓝牙子页重做/audiod OUT_BT 输出目标+音量 AVRCP 隔离+alsa.conf DEV 自动识别))。**A2 起音频输出走 `meow_audiod`**(SINK_AUDIOD 默认后端,SDK `meow_audio_*` 客户端,被抢占回调→暂停;旧 ALSA 直开仅 `MEOW_AUDIO_DIRECT=1` 调试);外置**系统文件管理器 os.meow.files**;`meow_ui` SDK 配套结构化列表 `W_LISTVIEW`、通用长按、SDK nav/media 封装、视图框架(可嵌套 view + 事件路由 + 绘制期 clip);App 通知查询/清除 IPC(0x25-27)与**点击通知跳转路由**(route_status)。
|
||||
- **进程隔离**:系统 UI 渲染进程与 App 进程相互独立,App 卡死不会冻结整个 UI。
|
||||
- **包管理**:`nyai`(固件内置 `/usr/bin/nyai`)支持 .mpk 安装/升级/导出/列表/索引,走 **version_code 安卓式版本规则**(缺 code 拒装、防降级)+ 签名分级(有签必验、`os.meow.*` 无签拒装、第三方直装告警)+ 导出过滤与体检。
|
||||
|
||||
@@ -43,18 +53,21 @@
|
||||
|
||||
```
|
||||
init (rcS)
|
||||
├─ /etc/init.d/S70meow_sysui 系统进程总监(meow_sysui,S70,M2.71 引入;监管 svcd,launcher 由 bootmenu 拥有)
|
||||
├─ /etc/init.d/S70meow_sysui 系统进程总监(meow_sysui,S70,M2.71 引入;监管 svcd + launcher,M2.83 起 launcher 移交 sysui)
|
||||
├─ /etc/init.d/S89meow_svcd 开机自启 meow_svcd(服务管理器)
|
||||
│ ├─ meow_mdns 及 services.d 服务 = 独立子进程
|
||||
│ └─ 专项子服务(一期)= meow_svc_perf / meow_svc_power
|
||||
│ ├─ meow_audiod 独占音频守护(M2.90,含路由/格式归一;HibyOS 模式不启动)
|
||||
│ ├─ 专项子服务 = meow_svc_perf / meow_svc_power
|
||||
│ ├─ meow_netmgrd 网络管理服务(M6.0 期1 / M6.1 期3:接口统一层+连接普查三实体+DNS 代理+策略引擎;HibyOS 只读)
|
||||
│ └─ meow_powermgr 功耗管理服务(M6.0 期1 / M6.1 期2:功耗查看/分解 top/历史/事件;HibyOS 只读)
|
||||
└─ /etc/init.d/S90meow_bootmenu Supervisor / 开机引导选单
|
||||
├─(选 MeowOS)execv meow_launcherd ← 系统 UI 渲染宿主
|
||||
│ └─ 用户点 App → fork/exec MeowAPP + meow_watchdog(每 App 一个)
|
||||
└─(选 HiByOS)exec hiby_player
|
||||
```
|
||||
|
||||
- **进程总监 `meow_sysui`(M2.71 已落地,`/usr/bin/meow_sysui` + `S70meow_sysui`)**:监管系统进程,**一个崩→只拉起它,绝不级联、绝不拖垮自身**。自身最小依赖、不渲染、不承载重逻辑 → 最稳。当前默认只监护 `meow_svcd`(launcher 仍由 bootmenu 的 `execv` 拥有,`enabled=0` 防双重拉起,移交见后端让位);`meow_mdns` 是 svcd 的服务子进程(不双重管理)。配置 `sysui.d/` 目录式(复用 services.d 语义),见 `patch_v2/meow_sysui/meow_sysui.md` 与 `docs/MEOWOS_SPEC_REF.md` §4。
|
||||
- **专项子服务(M2.73 已落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:新增两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能 / 系统只读聚合,含 network)、`meow_svc_power`(关机 / 重启 / 升级 / 回 HiBy 系统动作,含升级写保护 + dac 真实音量)。launcher 电源菜单与 MeowPanel 改经这些服务 IPC 触发,不再直连 `sys_do_reboot`/爬 `/proc`。电源 CLI `meow_svc_power <cmd>`(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot)。**二期**补关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签。详见 §2.9。
|
||||
- **进程总监 `meow_sysui`(M2.71 已落地,`/usr/bin/meow_sysui` + `S70meow_sysui`)**:监管系统进程,**一个崩→只拉起它,绝不级联、绝不拖垮自身**。自身最小依赖、不渲染、不承载重逻辑 → 最稳。默认监护 `meow_svcd` + `meow_launcherd`(**M2.83 起 launcher 生命周期移交 sysui**:`sysui.d/launcher.conf` `enabled=1`(restart=always/backoff=3000/max_restart=5);bootmenu 在 MeowOS 模式让位——sysui 在则不 exec launcher);`meow_mdns` 是 svcd 的服务子进程(不双重管理)。配置 `sysui.d/` 目录式(复用 services.d 语义),见 `patch_v2/meow_sysui/meow_sysui.md` 与 `docs/MEOWOS_SPEC_REF.md` §4。
|
||||
- **专项子服务(M2.73 已落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:新增两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能 / 系统只读聚合,含 network)、`meow_svc_power`(关机 / 重启 / 升级 / 回 HiBy 系统动作,含升级写保护 + dac 真实音量)。launcher 电源菜单与 MeowPanel 改经这些服务 IPC 触发,不再直连 `sys_do_reboot`/爬 `/proc`。电源 CLI `meow_svc_power <cmd>`(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot)。**二期已落地**:关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签(agent-185 2A / agent-221 2B)。详见 §2.9。
|
||||
- **隔离保证(现已落地)**:系统组件各自独立进程;App 进程与系统 UI 渲染进程独立;svcd 的**每个服务是独立子进程**(服务崩只重启该服务,不拖垮 svcd)。任何 App / 服务崩溃都不冻结系统 UI。
|
||||
|
||||
### 2.2 双系统引导(bootmenu)
|
||||
@@ -67,14 +80,16 @@ init (rcS)
|
||||
|
||||
### 2.3 launcher —— 系统 UI 渲染宿主
|
||||
|
||||
`/usr/bin/meow_launcherd`是 MeowOS 的核心进程,负责渲染**系统级 UI**。**二进制名不变**(`/usr/bin/meow_launcherd`,bootmenu `LAUNCHER_BIN` 指向它),源码 M2.69 起由单文件 `patch_v2/meow_launcherd/meow_launcherd.c`(9778 行)**拆分为多模块**(`patch_v2/meow_launcherd/`,当前含 `main_menu/statusbar/settings_page/power_menu/notif_center/app_launcher/fb_compositor/audio/backlight/net/about/font_theme/keys/util` + 后期新增 `subpage/wifi_page/bt_page`(蓝牙/WiFi 独立子页)、`now_playing`(Now Playing 大页)、`meow_media`(媒体会话,最近活跃源 + 30s 超时回收)+ 共享头 `meow_launcher_internal.h`;`build_launcher.sh` 列模块)——逻辑更清晰、便于并行开发,行为不变。
|
||||
`/usr/bin/meow_launcherd`是 MeowOS 的核心进程,负责渲染**系统级 UI**。**二进制名不变**(`/usr/bin/meow_launcherd`,bootmenu `LAUNCHER_BIN` 指向它),源码 M2.69 起由单文件 `patch_v2/meow_launcherd/meow_launcherd.c`(9778 行)**拆分为多模块**(`patch_v2/meow_launcherd/`,当前含 `main_menu/statusbar/settings_page/power_menu/notif_center/app_launcher/fb_compositor/audio/backlight/net/about/font_theme/keys/util` + 后期新增 `subpage/wifi_page/bt_page`(蓝牙/WiFi 独立子页)、`now_playing`(Now Playing 大页)、`meow_media`(媒体会话,最近活跃源 + 30s 超时回收)、`bg_page`(批③后台管理页)+ `meow_bgplay`/`meow_bgmgr`(批①后台常驻保活/管理逻辑)+ 共享头 `meow_launcher_internal.h`;`build_launcher.sh` 列模块)——逻辑更清晰、便于并行开发,行为不变。
|
||||
|
||||
负责渲染:
|
||||
|
||||
- **状态栏(统一系统进程渲染)**:由 launcher 作为**唯一渲染主体**(主界面与 App 前台用**同一个**状态栏实例,App 不画、不做「样式一样的两套实现」);右侧 **MD3 图标 + 变量值**(时钟/电量/WiFi/蓝牙/音量,数据源唯一由 launcher 统一喂/取,两边必然一致);**全屏收合**:状态栏向上收出屏顶、只留提示线,**顶部保留 28px 热区**(仍可下滑通知中心)+ 左缘指示条;随主题 `ui.style` 换图标集。
|
||||
- 通知中心(顶部下滑展开,launcher 渲染,功能见 §2.6);
|
||||
- 电源菜单、全局左缘返回手势、全屏提示线;
|
||||
- 主界面(应用列表)、设置页。
|
||||
- 主界面(应用列表)、设置页、**后台管理页**(`bg_page`,M6.35:桌面顶栏「后台」入口,列出后台常驻 App + 每行杀 + 一键清);
|
||||
- **后台常驻框架**(M6.34):App 返回桌面保活 + 宽限期回收(`meow_bgplay`),amsd `EV BG/FG` 上报数据源;
|
||||
- **锁屏(M6.2 起)**:`lock_screen.c` 4 位 PIN(`system.pin` 同源面板 sha256+salt)+ 息屏唤醒锁 `system.pin_lock_on_wake` + 错 5 锁 5 分钟持久化 + 电源菜单「锁定」(作废一次性绕过标志)+ 设置「安全→锁屏 PIN」页(设/改需旧、清免旧)+ `/tmp/meow_pin_unlock_once` 一次性解锁恢复口;M6.4 起锁屏用 **EVIOCGRAB 独占触控**杜绝输入穿透下层 App。
|
||||
|
||||
**渲染与 App 进程隔离**:launcher 是系统进程,App 是独立进程。App 卡死/崩溃只影响该 App,系统 UI(状态栏/返回/通知中心)始终可用。
|
||||
|
||||
@@ -106,6 +121,13 @@ init (rcS)
|
||||
- **M2.73 控件库修复(`meow_ui.a` md5 `e8341975`,见 `MEOW_APP_SDK.md` §5.8)**:`W_LIST`(含 LazyColumn/LazyRow)**视口裁剪**——滚动时只绘制落在视口内的行/分隔线/文本,杜绝「行滚出视口把内容画到视口外并标脏上屏」导致的标题栏/底部脏区(SDK 全局生效,非 App 侧)。IME(`meow_ui_ime.c`):键盘区背景快照/恢复(无残影)、配色走主题、shift 大小写、阻塞 `meow_ui_text_input` 喂 `on_tick`(避免进度条卡死)、OK(回车)= 提交并关闭 / ESC = 取消并关闭、回显条=实时输入回显(非候选/联想,候选框留中文输入法)。渲染:局部刷新不再清底重画单个控件(改 z 栈重画),消除重叠区残留。
|
||||
- **M2.76+ 新增**:`W_LISTVIEW` **结构化列表控件**(§5.5 A②落地:图标/标题/副标题/右侧操作 arrow·switch·check·more、单选/多选/禁态、行右 1/3 操作区命中、视口裁剪、全语义色);**通用长按事件** `MEOW_EV_LONG_PRESS`(坐标式 + `meow_ui_set_long_press_cb/ms` opt-in,默认 500ms,移动>12px 取消、长按后抬手抑制 TAP/SWIPE);**视图框架**(`meow_ui_view.c`:可嵌套容器 + 六生命周期 + `page_push/pop/replace` 包装 activity 栈 + 广播注册表 z 序 + `meow_view_dispatch_event` z 降序子>父命中即消费 + **绘制期通用 clip**`g_clip_stack[16]` 根除越界脏区);**SDK 级 z 序已并入视图框架**(自动注册 + 子>父 + `set_z(999)` 置顶 + 收敛 live_btn),hello App 层 `d_paint_region` 临时方案退役。
|
||||
- **M2.82 新增**:SDK **nav/media 封装**——`meow_ui_nav.c`(env 栈初始化 + report/request_exit)、`meow_ui_media.c`(懒连断线重连,meta 上报 + playctl 接收);SDK 顶部下滑手势→IPC 请求 launcher 展开通知中心(§10.5,App 进程不渲染系统 UI)。
|
||||
- **M2.84-88 新增**:**amsd 活动管理器**(一期 agent-212 生命周期注册表+`am` CLI,二期 agent-225 路由决策化+**picker 契约** `MSG_PICKER_CALL/RESULT 0x70/71` + `MSG_AMS_START/KILL`);**签名鉴权 2B**(agent-221 多公钥数组 + v2 签名块);**meow_route 音频输出路由**自动跟随插孔(agent-220);**中文拼音 IME**(agent-229 `meow_pinyin.c` 静态表 403 音节+329 词,组合条+候选栏+翻页+中英切换+九键数字盘);**App 内物理音量/媒体键**(agent-226 EVIOCGKEY 位图轮询+120ms 去重);**开机亮度 stop100 竞态**(agent-230);**流媒体客户端二期**(agent-222 专辑封面/歌词后台/大歌单分页)+ 歌单详情回退(agent-227);**MeowPanel 阶段 C**(agent-224 实时画面 `/api/screen.jpeg` + 遥控 `/api/input/tap`)+ 通知实时化+屏幕增量帧(agent-234)。
|
||||
- **M2.89/90 新增**:**开机选单抢屏回归修复**(sysui S70 早拉 launcher 误入主屏→改怠速等 mode 文件,HibyOS 选单/热键链路恢复);**watchdog SIGUSR1 仅裸沉浸 App** + SDK sigusr1 护栏(修框架 App 误杀);**amsd 失联三修**(陈旧 revents 丢连接/收发超时/自动重连);**MIUIx 顶级主题**(ui.style 一键切换,暖底橙调大圆角);**meow_audiod 独占音频守护**(A1 全量,含 SDK 客户端与 `meow_audio` CLI;meow_route 并入退役);**MeowPanel 字体库 tar.gz 导入/导出** + 文件管理自然排序;**流媒体客户端**:搜索接拼音 IME、设置页(音质/缓存上限/清理缓存)、缓存 LRU 淘汰接线、文案全中文。
|
||||
- **M2.91-96 新增**:**audiod 开机武装 hbc3000 FPGA**(路由线程启动无条件写 Output Port Switch,根治「未烧 FPGA 时 PCM 第二次 start 内核死锁」);**svc_power 充电态权威链**(mp2731-charger/status,修 battery/status 恒 Discharging)+ 状态栏充电闪电图标;**amsd 注册自愈**(开机早于卡挂载扫空→3s 快重试+30s 合并重扫+`am rescan`);**Now Playing 死循环修复**(空态 100% CPU/卡死)+ NP 页电源键/自动息屏;**插孔消抖**(4.4/3.5 插拔 ADC 幽灵音量键:launcher/watchdog/SDK 三路 800ms 消抖窗 + ramp 3s 上限);**audiod 长流护栏 + 几何收窄 period=1024×4**(根治长流 ~12s 停摆:ring 有界等待/timedwait/popen 有界收割/O_NONBLOCK+poll 写/wedge 自救/心跳 watchdog);**watchdog `wd_vol_save` O_TRUNC 毁 settings.conf 修复**(读改写单键+去重);**音量反转四路统一**(CS43198 音量寄存器=衰减量:0 最响/255 静音,launcher/audiod/watchdog/SDK 统一 `dac=255-pct*255/100`);**炸耳安全修复**(`dac_vol_mute` 旧写 0=最大响度 → 改 255 真静音);**SDK 插孔消抖补齐**(meow_ui.a 12cdd5d7)+ **物理播放/暂停键(164) 改发 PLAYCTL cmd=6 toggle** + App 前台状态栏音量%实时刷新。
|
||||
- **M6.0-6.1 新增**:**网络管理服务 `meow_netmgrd`**(期1 接口统一层 WiFi/BT/rfkill/IP/DNS 只读 + 期2 连接普查三实体(app:/svc:/proc:)+DNS 代理 127.0.0.1:53 双行 resolv 零断流 + 期3 策略引擎防自锁(dropbear/wpa_supplicant/udhcpc 等拒 block)/SOCK_DESTROY 真机 no-op→仅 DNS 层生效/面板白名单/WiFi save_config;CLI `meow_net`,M6.0 期1/M6.1 期3,设计见 `devdocs/plan/NETMGR_DESIGN.md`);**功耗管理服务 `meow_powermgr`**(期0 调研 current_now/cpufreq 不存在/背光 0-101 + 期1 权威充电链复用/经验模型 14 系数 + 期2 history 30s 环形/top 功耗分解/events/mp2731 current_now 实测充电功率(放电仍 estimated);CLI `meow_power`,M6.0 期1/M6.1 期2,设计见 `devdocs/plan/POWER_MANAGER_DESIGN.md`);**设置页迁 netmgr IPC + WiFi 新网络录入 UI**(扫 SSID→IME 输密码→connect,286);**服务名统一 meow_***(S89 迁移 audiod.conf/mdnsd.conf→meow_audiod.conf/meow_mdns.conf,用户定案 cd4e2c2);**MeowPanel 增强**(4 位 PIN 访问级鉴权 + 首页仪表盘 `/api/overview` + 电源/进程服务页 + 系统页补强,PANEL_ENHANCE_PLAN 批1)。
|
||||
- **M6.2-6.4 新增(SDK 演进批次,meow_ui.a 逐批重链四 App,API 全部追加式零改动)**:**公共头拆分**(278:`meow_ui.h` 922→67 行伞头 + `include/` 16 子头,API 零改动、`meow_ui.a` md5 不变、三 App 免重链);**布局/滚动/动画基座**(292:`meow_box` 线性布局纯计算器(固定/weight/wrap/对齐/嵌套,opt-in `meow_view_set_layout`)+ `meow_scroll` 滚动物理(速度采样+fling+边界回弹+snap)+ `meow_anim` 动画基座(静态池≤16+缓动+脏区联动,挂 on_tick 零开销));**事件分发形式化**(293 §6.4:单一仲裁者——z 序自顶向下命中+模态哨兵+down 捕获+唯一焦点,dialog/sheet/list/scroll 已迁,穿透矩阵 M1-M4 全 PASS);**ListView 虚拟化**(295 §5.5:vsource count/bind 数据源+固定 item 池(视口+2)+多行富 item+完全自绘钩子);**字符串资源 i18n**(299 §13:`meow_str`/`meow_strf`/`meow_str_set_locale`,内嵌 zh_cn/en_us 双语+TF 覆盖表+locale 持久化);**sb_icons 动作图标 + topbar 位图**(307:15 个 MD3 动作图标 `MEOW_ICON_*` + `meow_topappbar_set_item_icon`);**`meow_img_downscale`**(308:图片降采样 ≤maxdim,流媒体客户端封面 8.28MB→0.78MB)。详见 `MEOW_APP_SDK.md` §5.5/§6.4/§12/§13 与 `docs/MEOWOS_SPEC_REF.md` §3。
|
||||
- **M6.5-6.6 新增**:**SDK 矢量字**(316:`meow_ft_*` §10.1——把阅读器 R1/R2 已验证的 FreeType 管线整体上收到 SDK,按需读文件零整载 + glyph LRU 512 + 灰阶 blend565,与位图字库**并存**、App 显式调 `meow_ft_*` 才用,加载失败/无卡自动降级位图(不豆腐不崩);不调 ft 的 App **链接零增量**(独立 .o + vendor 裁剪构建);**SDK 演进主体收官**);**audiod 孤儿会话两段式回收**(315,M6.5:客户端崩溃/OOM 杀(sig=9)/正常退出后会话完整回收,STATE 回干净无流基线);**流媒体客户端 2026-09-02 批次**:**SIGSEGV 根因**(314:歌词解析栈上 227KB 临时数组 vs 128KB 线程栈→挪堆,真机复现修前 3.7s/修后连播两次过);**封面链分诊与修复**(317:Referer+桌面 UA 解 403 + webp 魔数内容型先判不解码 + 200 解码失败=源站死对象返 JSON 错误体实锤 + 死链/403/webp 幂等快跳不重试 + 缩略图 `param=300y300`)+**封面死链兜底链**(原 picUrl 失败→web 端 song/detail 免登录重取活链→歌单封面兜底)+**歌词页封面等比居中**(contain,四周主题底,方图不再变形);**`track/all` 接口验证否决**(318:该接口在服务端不存在,8 形态全业务码 404,维持 detail+song_detail 现状);**外部接口地图**(319:流媒体客户端外部 API 调研笔记落盘 `devdocs/plan/`);**风控加固**(320:接口响应 120s 短 TTL 缓存 + 游客 MUSIC_A 匿名令牌 + 死代码清理)。详见 `MEOW_APP_SDK.md` §10.1 与 `docs/MEOWOS_SPEC_REF.md` §3.5。
|
||||
- **M2.80-83 已落地**:M3 导航栈+M5 通知跳转(M2.80)、媒体会话+Now Playing(M2.81)、SDK nav/media 封装(M2.82)、字库启动竞态修复(M2.83)。
|
||||
- 既有 `meow_btn/slider/switch/list_create` 签名一字不改(向后兼容),Options/状态机走新增挂载器。
|
||||
|
||||
### 2.5 服务
|
||||
@@ -115,12 +137,14 @@ init (rcS)
|
||||
- 目录式 `key=value`:`exec`(必填)/`args`/`env`/`enabled`/`restart`/`backoff_ms`/`max_restart`/`trigger`(`boot`/`card-mount`/`wifi-up`,默认 `card-mount`)。
|
||||
- 状态机 `DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;CLI(SSH 直呼):`status` / `reload` / `start|stop|restart <name|all>` / `pid`。
|
||||
- **M2.73 悬垂指针修复(md5 `da6ebc66`)**:`scan_and_reconcile` 用 `*s=tmp` 整体拷贝后 `s->argv[]` 曾指向已销毁栈帧 → `execv`/`/proc/pid/cmdline` 错乱;新增 `svc_rebuild_argv` 重定位到服务自身 `exec`/`argbuf`(删除服务后同 tick 拉起不再错位)。
|
||||
- **meow_mdns(mDNS 应答器,自研)**:`/usr/bin/meow_mdns`,**IPv4-only**(该设备内核无 IPv6,完整 mDNSResponder/SAResponder 均无法运行),应答 `<host>.local` 的 A 记录与 `_ssh._tcp` 的 PTR/SRV/TXT。由 `meow_svcd` 托管(板载 `services.d/mdnsd.conf`,`trigger=boot`)。
|
||||
- **meow_mdns(mDNS 应答器,自研)**:`/usr/bin/meow_mdns`,**IPv4-only**(该设备内核无 IPv6,完整 mDNSResponder/SAResponder 均无法运行),应答 `<host>.local` 的 A 记录与 `_ssh._tcp` 的 PTR/SRV/TXT。由 `meow_svcd` 托管(板载 `services.d/meow_mdns.conf`,`trigger=boot`)。
|
||||
- **hostname = `meowos`**(设置键 `system.hostname`,缺省 `meowos`;`S89meow_svcd` 开机 `sethostname`)→ 即 `meowos.local`。
|
||||
- **M2.78 竞态修复**:无可用 IPv4 不再 `exit(2)`(改内部 2s 等待重试,SIGTERM 可打断);5s 周期检测掉线/换 IP → 重新 join 组播 + A 记录实时刷新(修 IP 变更还带旧地址);接口 down 时零应答守卫;`MEOW_MDNS_IF` 落地。
|
||||
- 注:手机热点会丢弃设备→主机的组播应答,`avahi-resolve meowos.local` 在上网热点下会超时(网络层热点隔离,非应答器缺陷),主机可用 DNS 兜底。
|
||||
- **meow_crash(系统级崩溃捕获)**:svcd 服务异常退出上报(AF_UNIX best-effort,不崩 svcd)+ dmesg 增量扫 segfault/oops;落盘 TF `MeowOS/logs/crash_<bootts>.log`(无卡回退 /tmp + 迁移);CLI `dump`/`tail`;`S89` 预置 `crash.conf`。launcher 另在 `launch_app` waitpid 回收处 **notify_app_crash**(信号杀/非0退出上报,干净 exit 不报)。
|
||||
- **meow_verify / meow_sign(包签名,二期 2A 最小闭环)**:`meow_verify`(Ed25519 验签 CLI,公钥固化固件,TweetNaCl+SHA256 单文件);`meow_sign`(宿主 keygen/sign,尾部 180B footer 签名块,私钥仅 `.keys/` 不入库);nyai 与 launcher `meow_pkg_install` **验签分级**(有签必验 / `os.meow.*` 无签拒装 / 第三方直装告警 / 三重绑定 / 原签存 `.mpk.src` 导出保留);svc_power IPC 鉴权(SO_PEERCRED→/proc/pid/exe→包名/系统身份 allowlist)。
|
||||
- **meow_netmgrd(网络管理服务,M6.0 期1 / M6.1 期3)**:`/usr/bin/meow_netmgrd`(svcd 托管板载 `services.d/meow_netmgrd.conf`,`trigger=boot`;HibyOS 只读降级,RSS ≤1MB)。**接口统一层**(iface 枚举+热插拔 / WiFi `wpa_cli` 有界收割 / BT `hciconfig`+`bluetoothctl` / rfkill / IP 路由 DNS 只读 / udhcpc 续租释放仅 MeowOS);**连接普查**(2s `/proc/net/{tcp,udp}`,inode→pid→**三实体** `app:<包名>`/`svc:<服务名>`/`proc:<comm>` 归因);**DNS 代理**(127.0.0.1:53,双行 resolv.conf+atexit 恢复+5s 自检,崩溃零断流);**策略引擎**(`/data/meow/netmgrd/net_policy.conf`,防自锁双层:dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init 拒 block+加载校验强制复位;**SOCK_DESTROY 真机 no-op(内核 4.4 接受不销毁)→ 仅 DNS 层生效,`meow_net policy show` 明示 `enforce=dns_only`**)。双形态 CLI **`meow_net`**(`status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help`)。设计见 `devdocs/plan/NETMGR_DESIGN.md`。
|
||||
- **meow_powermgr(功耗管理服务,M6.0 期1 / M6.1 期2)**:`/usr/bin/meow_powermgr`(svcd 托管板载 `services.d/meow_powermgr.conf`,`trigger=boot`;HibyOS 只读降级)。**期0 事实**:`battery/current_now`/`cpufreq` **不存在**→实时功率只能 derived+estimated 双轨、背光量程 **0-101**(非 255)、mp2731 有 current_now(充电功率可测)、rfkill×4、无 thermal。**期1**:权威充电态链复用 + 经验模型 14 系数(`/data/meow/powermgr/power_model.conf`)。**期2**:`history`(30s 环形 4h 不落盘)+`top`(功耗分解)+`events`(充放电/低电/亮灭屏/背光跳变)+模型细化(WiFi 未关联档/BT rfkill·ACL 档)+ **mp2731 current_now 实测充电功率**(charge_confidence=measured,放电仍 estimated)。双形态 CLI **`meow_power`**(`status/estimate/model/history/top/events`)。设计见 `devdocs/plan/POWER_MANAGER_DESIGN.md`。
|
||||
|
||||
### 2.6 通知系统
|
||||
|
||||
@@ -155,12 +179,12 @@ init (rcS)
|
||||
|
||||
### 2.9 系统框架 / 专项服务
|
||||
|
||||
把**系统复用功能收敛到系统框架**(`devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`,一期已落地):meow_sysui 派生 → meow_svcd 托管 → IPC。新增两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务:
|
||||
把**系统复用功能收敛到系统框架**(`devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`,一期已落地):meow_sysui 派生 → meow_svcd 托管 → IPC。新增 **svcd 托管、板载 `trigger=boot`** 的专项子服务(perf/power 一期;**netmgrd/powermgr** 二期——网络管理/功耗管理,见 §2.5 服务清单):
|
||||
|
||||
- **`meow_svc_perf`**(性能 / 系统只读聚合,IPC `/usr/data/meow_perf.sock`,`MSG_PERF_QUERY(0x0050)→MSG_PERF_RESULT(0x0051)`);**`meow_svc_power`**(关机 / 重启 / 升级 / 回 HiBy 系统动作,IPC `/usr/data/meow_power.sock`,`MSG_POWER_ACTION(0x0060)→MSG_POWER_STATUS(0x0061)`,服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73 已落地**:`meow_svc_perf` 增 **network** 子集(真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`),并入 SYSTEM 子集输出;`meow_svc_power` **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启——**只完整性、不签名**,海贝官方固件无签名,签名会挡自刷)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(弃 amixer dummy)。**M2.78 读级鉴权放宽(agent-189)**:`STATUS`/`BATTERY`/`REBOOT_REASON`/`UPGRADE_STATUS`/`dac status` 读级不再查 uid(任意调用者);动作级维持 `os.meow.*`/系统身份白名单。当前 md5:perf `7ea196de`、power `c6ddf3f8`。
|
||||
- **`meow_svc_perf`**(性能 / 系统只读聚合,IPC `/usr/data/meow_perf.sock`,`MSG_PERF_QUERY(0x0050)→MSG_PERF_RESULT(0x0051)`);**`meow_svc_power`**(关机 / 重启 / 升级 / 回 HiBy 系统动作,IPC `/usr/data/meow_power.sock`,`MSG_POWER_ACTION(0x0060)→MSG_POWER_STATUS(0x0061)`,服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73 已落地**:`meow_svc_perf` 增 **network** 子集(真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`),并入 SYSTEM 子集输出;`meow_svc_power` **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启——**只完整性、不签名**,海贝官方固件无签名,签名会挡自刷)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(弃 amixer dummy)。**M2.78 读级鉴权放宽(agent-189)**:`STATUS`/`BATTERY`/`REBOOT_REASON`/`UPGRADE_STATUS`/`dac status` 读级不再查 uid(任意调用者);动作级维持 `os.meow.*`/系统身份白名单。**M2.88 签名鉴权 2B(agent-221)**:svc_power 面板身份精确路径绑定(删启发式)。当前 md5:perf `7ea196de`、power `306df962`。
|
||||
- **电源 CLI `meow_svc_power <cmd>`**(root 免鉴权、单一命令、**不覆盖**系统 shutdown/reboot):`shutdown` / `reboot[--hiby]`(强制回 MeowOS)/ `upgrade`(写 mtd5,含升级写保护)/ `set-default-meow` / `set-default-hiby` + `status` / `battery` / `reboot-reason` / `upgrade-status` / `screen on|off|timeout<秒>` / `dac on|off|status`。
|
||||
- launcher 电源菜单与 MeowPanel 均**改为经这些服务 IPC 触发**,不再直连 `sys_do_reboot` / 爬 `/proc`。
|
||||
- **二期**:关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签(防伪造包名,解包验签 / 导出保留签名)。
|
||||
- **二期望签鉴权已落地(agent-185 2A / agent-221 2B)**:关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签(防伪造包名,解包验签 / 导出保留签名);`meow_verify` 多公钥数组 + v2 签名块(含 ts,兼容 v1)+ svc_power 面板身份精确路径绑定(删启发式)。
|
||||
- **开机默认**:权威标记 `/data/meow_boot_default`(单值 O_TRUNC:`0`=MeowOS / `1`=HiByOS)。
|
||||
|
||||
### 2.10 MeowPanel(外部管理面板)
|
||||
@@ -169,9 +193,11 @@ init (rcS)
|
||||
- **后端**:`patch_v2/meowpanel/meowpanel.c` 单进程零第三方依赖 C 静态二进制,内嵌 HTTP/JSON 服务器(8080)+ 静态托管(`/`→index.html、`/assets/*`)+ 框架专项服务 IPC 客户端 + `settings.conf` 设置镜像。**读侧透传** `meow_svc_perf` IPC(`/api/{status,perf,system}`,单一权威源,不各自爬 /proc)、**动作侧**经 `meow_svc_power` IPC(`/api/system/{reboot,shutdown,ota}`)。
|
||||
- **能力**:设置镜像(`settings.conf` 解析 `key = value` 带空格 + 精确匹配,防 `audio.volume` 误配 `audio.volume_max`)、文件管理(白名单 `MeowOS/Music/MeowData/MeowAPPs/book`,`..` 逃逸→403;**目录优先+名称不区分大小写排序、条目上限 1024**;上传 8MB 上限、文件名净化、双重白名单、`read>1MB` 截断)、**通知中心后端**(`/api/notifications` 读 TF 归档环形 200 条 + 单删 + dismiss_all)、应用(`nyai install/export/remove`,包名=`app.json.package`;`nyai` 路径固件内置 `/usr/bin/nyai` 优先+卡上回退)、按键注入(`/dev/input/event*` evdev,只注入音量/电源/播放/上下曲,**禁返回(158)/主页(172)/确认(28)/退出(96)**)、日志(`MeowOS/logs`)。
|
||||
- **OTA 检查升级 bug 已修**:改读 `GET /api/system` 返回的 `ota` 字段,不再误触 `POST /api/system/ota` 触发升级。
|
||||
- **M2.78 可选密码鉴权(agent-196,默认关)**:`panel.auth_enabled`(默认0) + `panel.auth_pass`(带盐 sha256 不明文);token 会话(`/dev/urandom`,TTL 12h,内存+卡持久化,静态屏蔽 session 文件);未启用零回归,启用后 `/api/*` 全要会话;前端登录页 + 401 回跳 + 设置卡片。
|
||||
- **M2.78 可选密码鉴权(agent-196,默认关)**:`panel.auth_enabled`(默认0) + `panel.auth_pass`(带盐 sha256 不明文);token 会话(`/dev/urandom`,TTL 12h,内存+卡持久化,静态屏蔽 session 文件);未启用零回归,启用后 `/api/*` 全要会话;前端登录页 + 401 回跳 + 设置卡片。**面板增强(PANEL_ENHANCE_PLAN 批1,2026-09-01 用户定案)**:4 位 **PIN 访问级鉴权**(面板访问即需登录,与未来锁屏同源 `system.pin` 预留,未设 PIN 首访引导设置、未设前写端点拒绝;token 会话 3 天或重启失效,进程内存);前端五页:**首页仪表盘**(`/api/overview` 聚合)+ 音频电平(rms/peak 只读)+ 电源页只读 + 进程/服务页(svcd/amsd)+ 系统页补强 + 网络健壮性 + PIN 卡;后端只读端点 `power/overview/ams/svcd` + audio 电平键;音量 clamp `vol_max`+锥度提示 / 防自锁禁停 / 确认弹窗三红线。详见 `devdocs/plan/PANEL_ENHANCE_PLAN.md`)。
|
||||
- **面板批2 + 加固(2026-09-02)**:崩溃/日志中心 + 音频路由页 + 音量写经 clamp `vol_max` + 后端补 `reboot_reason`;**JSON 输出 UTF-8 加固**(`sA_str_n` 单遍校验、非法序列换 `?`、IPC 原始 JSON 门禁降级,防任一上游坏字节拖垮整页);**overview 自校验纪律**:`/api/overview` 服务端发出前先自查/严格平衡检查(坏则 dump `/tmp/overview_bad.json` + 500),前端非 JSON 自动回传原始字节到 `/api/debug/report_bad_json`(落 `/tmp/overview_client_bad.txt`);**教训**:后端 `jv_parse` **容忍尾随垃圾**,而浏览器 `JSON.parse` **不容忍**——net 块多写一个 `}` 即「Extra data」破页(batch2 DNS 并入事故),故自校验必须做与浏览器口径对齐的**严格括号/闭合平衡检查**。
|
||||
- **⚠️ 鉴权默认关**:内网信任模型(`reboot/shutdown/ota/apps/删除/上传` 等危险端点默认无鉴权,**勿把 8080 暴露到不可信网络**);`meow_svc_power` 服务端做 `SO_PEERCRED uid==0` 基础校验。
|
||||
- **前端**:`patch_v2/meowpanel/web/`(MD3/MIUIx 双主题 + 宝塔式 + 浏览日夜);`/usr/bin/nyai` 装 `.mpk`;launcher 的 `meow_pkg_export` / `nyai export` 导出 tar.gz(busybox tar + gzip,不需 zip)。详见 `patch_v2/meowpanel/README.md`。
|
||||
- **M6.34 前后端拆分 + 屏幕·遥控合并页(2dc4cad)**:后端 `meowpanel.c`(6372 行)拆为 `src/*.c`(20 个,按 API 域)+ `mp.h`(235 函数原型、20 端点 output byte-identical、0 warning);前端拆 `mp_shared.js`/`mp_pages.js`/`meowpanel.js` 三层;新增**屏幕·遥控合并页**(左=设备实时画面、右=MD3 SVG 矢量按钮无 emoji),替代旧的分离屏幕/遥控页;重编后热更 TF 卡。面板后端热更只写白名单 `MeowOS/opt/MeowPanel/`。
|
||||
|
||||
---
|
||||
|
||||
@@ -184,15 +210,16 @@ init (rcS)
|
||||
- `meow_launcherd/` launcher(系统 UI 宿主,**多模块**:`main_menu/statusbar/settings_page/power_menu/notif_center/app_launcher/fb_compositor/audio/backlight/net/about/font_theme/keys/util` + `subpage/wifi_page/bt_page`(蓝牙/WiFi 子页)`now_playing`(Now Playing)`meow_media`(媒体会话)`meow_ipc`/`meow_settings`),+ `meow_launcher_internal.h` + `build_launcher.sh`
|
||||
- `sdk/` meow_ui SDK 源码 + `build_sdk.sh`(→ `meow_ui.a`;含**统一控件库**:widget/theme/activity/global/dialog/ime/img/input/log/notify/sys/vol/view(视图框架)draw(绘制 z 序/注册表)nav/media(导航/媒体会话封装))
|
||||
- `hello_app/` 示例 App(`app.c` + `ui/{home,full,demo}.c` + `ui/common/chrome.c` + `logic/{notify,state}.c` + `build_hello.sh`)
|
||||
- `ncm_app/` 网易云客户端源码(`core/`:mbedtls 裁剪+jsmn+qrcodegen+minimp3;UI v1 + 复古配色;vendor/ 与 out/ gitignore 不入库)
|
||||
- `ncm_app/` 流媒体客户端源码(dev.player.ncm;`core/`:mbedtls 裁剪+jsmn+qrcodegen+minimp3+dr_flac;UI 复古配色;vendor/ 与 out/ gitignore 不入库)
|
||||
- `files_app/` 外置系统文件管理器 os.meow.files(app.c + ui/{browser,viewer}.c + ui/common/chrome.c + logic/fsx.c)
|
||||
- `meow_sysui/` 系统进程总监(`meow_sysui` + `S70meow_sysui` + `meow_sysui.md`)
|
||||
- `meow_svcd/` 服务管理器;`meow_watchdog/` 应用看门狗;`meowpkg/` 包管理(`nyai.c`);`uinput_tap/`
|
||||
- `meow_crash/` 系统级崩溃捕获;`meow_verify/` `.mpk` Ed25519 验签;`meow_sign/` 宿主签名工具;`meow_svc_perf/`、`meow_svc_power/` 专项子服务;`meowpanel/` MeowPanel 外置面板后端 + `web/` 前端;`meow_amsd/` amsd 活动管理器(agent-212 规划,设计见 `devdocs/plan/AMSD_DESIGN.md`)
|
||||
- `meow_crash/` 系统级崩溃捕获;`meow_verify/` `.mpk` Ed25519 验签;`meow_sign/` 宿主签名工具;`meow_svc_perf/`、`meow_svc_power/`、`meow_netmgrd/`、`meow_powermgr/` 专项子服务(性能/电源动作/网络管理/功耗管理);`meowpanel/` MeowPanel 外置面板后端 + `web/` 前端;`meow_amsd/` amsd 活动管理器(一期 agent-212 + 二期路由决策/picker 契约 agent-225,设计见 `devdocs/plan/AMSD_DESIGN.md`);`meow_audiod/` **独占音频守护 + `meow_audio` CLI**(M2.90,规范见 `devdocs/plan/AUDIOD_DESIGN.md`;`meow_route/` 已并入退役,源码保留归档);`tools/` 诊断小工具(sndprobe/tonepcm 等)
|
||||
- **`mips-repo/`(入库)**—— mipsel 交叉编译软件仓库:`bins/`(`meow_mdns` / `mdnsd` 等二进制工具 + `build.sh` + `manifest.md`)、`mpk/`(`.mpk` 应用包)。
|
||||
- **`docs/`(入库)**—— `MEOWOS_SPEC_REF.md` 权威速查总索引。
|
||||
- **`font/`(入库)**—— 字形系统:`gen/gen_font.py` 生成器、`meow_font.c/.h` 渲染库、`demo/`、`src/`(字体源文件)。
|
||||
- **`bootlogos/`(入库)**—— 启动 / 关机 logo。
|
||||
- **`third_party/`(入库)**—— 第三方参考/依赖归拢目录(与 MeowOS 源码 `patch_v2/` 分离、不混入构建):`CNMPlayer`(流媒体客户端功能对照)、`md-material-web`(**最新 Material 3 参考**,MD3 对齐依据,agent-435 按其对所有组件细化)。
|
||||
- **`LICENSE`** —— Apache-2.0。
|
||||
- **顶层文档(入库)**—— `APPS.md`、`MEOW_APP_SDK.md`、`FRAMEWORK_DESIGN.md`、`SYSTEM_SETTINGS.md`、`NOTIFICATION_SYSTEM.md`、`TF_CARD_LAYOUT.md`、`DEVELOPMENT_PLAN.md`、`GESTURE_BACK_DESIGN.md`、`KEY_CONTROL_DESIGN.md`、`PERF_SCHEDULER.md`、`MEOWOS_API.md`。
|
||||
- **本地构建资产(根目录 gitignore;但 `scripts/build/` 的 `README.md`/`build_fw_apps.sh`/`fw_content.conf` 已 `git add -f` 入库)**—— 其余(`fw_apps/` 固件组装产物、官方固件基线 rootfs、交叉工具链、本地开发文档、设备密钥等)不入库。详见「构建」与「部署」。
|
||||
@@ -286,7 +313,7 @@ patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hel
|
||||
| `fullscreen` | — | app 级全屏:状态栏**收合至提示线** + 保留左缘返回 / 顶部提示线 / 下滑通知中心(activity 级用 `route.<id>.fullscreen`) |
|
||||
| `fonts` | — | 字库档位声明 `"full" | "gbk" | "common"`(默认 `common`≈4k) |
|
||||
|
||||
其他扩展(launcher 极简解析、不校验 JSON):`icon` / `framework`(导航栈承载)/ `immersive` / `route.<id>`(路由表,`framework=1` 时看门狗返回发 SIGUSR1)/ `awake.<id>`(keep_awake)/ `service.<svc>`。应用数上限 8,扫描深度 1 级。
|
||||
其他扩展(launcher 极简解析、不校验 JSON):`icon` / `framework`(导航栈承载)/ `immersive` / `route.<id>`(路由表,`framework=1` 时看门狗返回发 SIGUSR1)/ `awake.<id>`(keep_awake)/ `service.<svc>` / **`background_keepalive`**(后台常驻声明,M6.34:`1`=返回桌面**无限期保活**不清;缺省/`0` 走 30min 宽限期回收;播放类 App 若带 `background_audio` 且媒体活跃也无限期保活)。应用数上限 8,扫描深度 1 级。
|
||||
|
||||
### 6.2 链接与入口
|
||||
|
||||
@@ -336,6 +363,7 @@ patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hel
|
||||
| `docs/MEOWOS_SPEC_REF.md` | **权威速查总索引**(首选入口):路径 / 常量 / 接口 / 约定 + 「已知矛盾 / 待确认」清单 |
|
||||
| `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(AF_UNIX socket、路由表、导航栈、后台权限、媒体会话、freedesktop/MPRIS/XDG 对齐);§10 进程模型 / 系统 UI 归属 / 专项子服务 |
|
||||
| `MEOW_APP_SDK.md` | SDK 设计:架构分层、绘制/控件/图片/输入/主题/字体/通知 API、三层控件模型、Activity 栈、动画与性能;**§5.8 控件层架构升级(统一控件/Options/状态机/布局 + Material 子集)、§8.1 主题增强(MD3/MIUIx × 明暗 + 背景源)** |
|
||||
| `docs/APP_DEV_GUIDE.md` | **面向第三方开发者的上手指南**:环境→最小 App→工程结构→构建打包→签名安装→部署调试→SDK 能力速查→红线→FAQ |
|
||||
| `APPS.md` | MeowAPPs 应用规范:目录、`app.json` 字段、生命周期、边界、返回键、`.mpk` 包与 `nyai` |
|
||||
| `NOTIFICATION_SYSTEM.md` | 通知系统:freedesktop 标准、配额、TTL/转储、存储分层 |
|
||||
| `SYSTEM_SETTINGS.md` | 设置子系统:统一存储 + `meow_settings_*` C API + 变更通知 + 导入导出 |
|
||||
|
||||
+21
-5
@@ -23,7 +23,7 @@
|
||||
| | `display.theme_mode` | enum | `manual` | 主题判定方式:`manual`(=读 `display.theme`)/`auto`(按深色时段)。**B2b 新增** |
|
||||
| | `display.theme_dark_start` | int 分钟 0-1439 | `0` | 深色时段开始(如 19:00=1140);`start==end` 视为未配置 → auto 回退 manual。**B2b 新增** |
|
||||
| | `display.theme_dark_end` | int 分钟 0-1439 | `0` | 深色时段结束(如 07:00=420);`start>end`=跨午夜。**B2b 新增** |
|
||||
| | `display.background` | enum/图片 | `black` | **由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度;可选集+默认随 style/mode 一起变):**MD3×dark**=黑/深蓝/墨绿(默认黑)、**MD3×light**=白/浅蓝/浅绿(默认白)、**MIUIx×dark/light**各一套(草案)。每种配色定义**深/浅两套 surface/bg**,随 `display.theme` 切换(修复"背景仅对主页生效/仅深蓝/浅色不搭")。亦可为图片路径 `MeowAPPs/backgrounds/xx.png`(图片本身即观感,深浅作用于文字/角色色,待定案)。见 MEOW_APP_SDK §8.1 |
|
||||
| | `display.background` | enum/图片 | `black` | **由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度;可选集+默认随 style/mode 一起变):**MD3×dark**=黑/深蓝/墨绿(默认黑)、**MD3×light**=白/浅蓝/浅绿(默认白)、**MIUIx×dark/light**各一套(草案)。每种配色定义**深/浅两套 surface/bg**,随 `display.theme` 切换(修复"背景仅对主页生效/仅深蓝/浅色不搭")。亦可为图片路径 `MeowAPPs/backgrounds/xx.png`(**定案**:图片 **cover 居中裁剪**,深浅作用于文字/角色色、背景图不随深浅换,叠加层固定跟明暗主题)。见 MEOW_APP_SDK §8.1 |
|
||||
| **电源** | `power.longpress_menu_ms` | int | 3000 | 长按电源弹电源菜单阈值 |
|
||||
| | `power.forceoff_ms` | int | 8000 | 长按电源强关阈值 |
|
||||
| | `power.defeat_hotkey` | string | `power_voldown` | 切系统/切 MeowOS 热键组合 |
|
||||
@@ -44,7 +44,7 @@
|
||||
| | `font.class_other` | path | *(空=跟全局)* | 多字体:其他字符 所用字体 |
|
||||
| | `font.load_full` | int 0/1 | 0 | **加载大字库(M2.5x)**:1=**强制全系统用大字库(`gbk`≈20k 档)**,覆盖 app 未声明/覆盖默认 `font.file`;0=默认小字库(`common`≈4k),仅 app 声明 `fonts:"full"` 才大字库 [源:用户 2026-08-30] |
|
||||
| | `font.size.title` / `font.size.body` | int | 28/20 | 标题/正文字号 |
|
||||
| | `ui.style` | enum | `md3` | `md3`(默认,Material You)/`miuix`(MIUIx,**未实现**)——**顶级主题风格族**(定义整套配色风格/控件形态/圆角/间距/描边),对应 SDK `MEOW_STYLE_MD3`/`MEOW_STYLE_MIUIX`(MEOW_APP_SDK §8.1) |
|
||||
| | `ui.style` | enum | `md3` | `md3`(默认,Material You)/`miuix`(MIUIx,**M2.90 已实现**:完整 light/dark 配色+控件形态参数,见 MEOW_APP_SDK §8.1.3)——**顶级主题风格族**(定义整套配色风格/控件形态/圆角/间距/描边),对应 SDK `MEOW_STYLE_MD3`/`MEOW_STYLE_MIUIX`(MEOW_APP_SDK §8.1)。`g_defs` 值域 `"md3,miuix"`(label "Material You,MIUIx");存量 `"miui"` 兼容映射为 `miuix`。**状态栏图标集仍 MD3 一套**(无 per-style 换集接口,MIUIx 仅换配色) |
|
||||
| **通知** | `notify.center_enabled` | bool | true | 下拉通知中心开关 |
|
||||
| | `notify.app_quota` | int | 16 | 每应用通知配额 |
|
||||
| | `notify.categories` | int | 4 | 通知类型数上限(每 App,M2.53)[源:审计 2026-08-30]|
|
||||
@@ -53,19 +53,35 @@
|
||||
| | `notify.dnd` | bool | false | 通知中心「勿扰」面板内开关(通知列表上方小 toggle;开=只显示 `urgency>=2`(HIGH/CRITICAL),关=全部;面板内读写 `meow_settings_*`,零旁路)[源:用户 2026-08-30] |
|
||||
| **日志** | `log.level` | enum | `info` | `error/warn/info/debug` |
|
||||
| | `log.to_card` | bool | true | 写 TF 卡(无卡回退内存环) |
|
||||
| **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(MeowOS 直写 CS43198 DAC;读回为追踪值) |
|
||||
| **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(audiod/launcher/watchdog/SDK 四路统一写 CS43198 DAC;读回为追踪值)。⚠️ **寄存器=衰减量**(0=最响/255=静音,M2.95 四路统一映射 `dac=255-pct*255/100`;非线性 0.5dB/步,感知 dB taper 待做) |
|
||||
| | `audio.volume_max` | int 0-100 | 100 | MeowOS 内部音量上限(clamp;**不与 HibyOS user.ini 上限同步**——其带 CRC 有风险,已定案不做;用户可自设 MeowOS 上限) |
|
||||
| | `audio.volume_step` | int 1-5 | **1** | 音量键跳步(默认 1%,可配 1-5,最大 5) |
|
||||
| | `audio.output` | string | `hw:0,0` | MeowAPPs 音频输出设备 |
|
||||
| | `audio.output` | string | `hw:0,0` | 音频输出设备。**M2.90 起声卡由 `meow_audiod` 独占**,App 不再直开(经 SDK `meow_audio_*`);本键仅作 audiod 开卡目标/调试直开(`MEOW_AUDIO_DIRECT=1`)参考 |
|
||||
|
||||
> **音频子系统(M2.90 起)**:`meow_audiod` 独占音频守护收口全部声卡交互——路由自动跟随插孔(4.4 优先,`meow_audio route` 可手动)、音量 `MSG_AUDIO_VOLUME` v1 透传写 DAC numid=1/2 + 本 `audio.volume` 键(与 watchdog/launcher 同源,不抢主权;v2 收编另定)。规范 `devdocs/plan/AUDIOD_DESIGN.md`。**M2.91-96 加固**:开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁)/长流护栏+period=1024×4 几何(根治长流停摆)/音量反转四路统一/炸耳修复(`dac_vol_mute`=255 真静音)。**⚠️ 音量存盘纪律(M2.93)**:写 `audio.volume` 一律**读改写单键**——watchdog `wd_vol_save` 旧用 O_TRUNC 整文件重写曾毁掉 settings.conf(真机实证整文件被抹/重复行/丢 max+step),已修;任何组件不得整文件重写本配置。
|
||||
| **网络** | `network.wifi_enabled` | bool | true | WiFi 开关(写 wlan0;设置页开关经 `wifi_up.sh`/`ip link set wlan0`,`ensure_wifi()` 尊重此值——关=0 则不再自动连网) |
|
||||
| | `network.bluetooth_enabled` | bool | false | 蓝牙开关(写 `hciconfig hci0 up/down`;默认关=少耗电) |
|
||||
| **系统** | `system.hostname` | string | `meowos` | 主机名(RFC1035 首标签;bootmenu 的 S89meow_svcd 开机读它 `sethostname`,缺省 `meowos`;供自研 mDNS 应答器 `meow_mdns` 公布 `<host>.local`)[源:M2.54] |
|
||||
| | `system.pin` | string | 空 | **锁屏 4 位 PIN**:`<salt_hex>:<sha256_hex(salt||pin)>`(sha256+salt,格式与面板 `panel.pin` 全等,绝无明文)。空=未设(锁屏关闭)。**panel.pin 迁移**:`panel.pin` 已设而本键未设时锁屏接受 `panel.pin`,首次验证成功后并入本键;此后以本键为准。**兜底**:SSH root 删本键即解锁(无其它后门)[源:用户 2026-09-02 锁屏定案] |
|
||||
| | `system.pin_lock_on_wake` | int 0/1 | **1** | 息屏唤醒后锁屏(默认开);0=关(唤醒直进主屏,电源菜单「锁定」手动锁仍可用)。开机不锁(boot 进主屏不拦) |
|
||||
| | `system.pin_lock_until` | int(epoch 秒) | 0 | 错 PIN 锁定截止戳。**持久化到 settings**(防 launcher 重启清零;面板进程内存方案重启失效,此处落盘);截止前锁屏页禁输入并倒计 |
|
||||
| | `system.pin_fails` | int | 0 | 连续错 PIN 计数(0-4;达 5 触发锁定并清零)。持久化防「关机重启重试」 |
|
||||
|
||||
> **锁屏行为(2026-09-02 用户定案,实现权威见 `lock_screen.c`/`lock_screen_ui.c`/`lock_settings.c`)**:
|
||||
> - **触发**:息屏唤醒后(默认开,`system.pin_lock_on_wake` 可关)+ 电源菜单「锁定」主动锁;**开机不锁**。
|
||||
> - **主动锁**(电源菜单「锁定」):立即熄屏 + 写 `/tmp/meow_pin_locked`(主动锁标志,重启消失)→
|
||||
> 无论 `system.pin_lock_on_wake` 是否开,唤醒必走锁屏;解锁(PIN 或绕过)时清除。同时 `rm /tmp/meow_pin_unlock_once` 作废挂起的绕过。
|
||||
> - **面板一次性解锁恢复口**:`/tmp/meow_pin_unlock_once`(tmpfs 标志,panel 端点创建)存在即被锁屏页**消费删除并放行进入系统一次**
|
||||
> (不校验 PIN、不清除 `system.pin` 本身、不重置失败计数;重启标志自然消失)。launcher 只做检查/消费侧。
|
||||
> - **系统设置「安全 → 锁屏 PIN」页**:状态(未设置/已设置/锁定中+剩余)/ 设置(4 位双输确认)/
|
||||
> 修改(需旧 PIN 一次)/ 清除(**免旧 PIN**,物理持机即授权,仅二次确认弹窗)/ `system.pin_lock_on_wake` 开关。
|
||||
> - **清除兜底**:SSH root 删 `system.pin` 键即解锁;`lck_pin_clear()` 同义(settings API)。
|
||||
|
||||
> **字体尺寸标记(M2.5x,随「加载大字库」一起落地)**:字体/主题选择器**每个字体选项标注库大小标记**,如 `MiSans Regular(common≈4k)` / `gbk≈20k` / `full`,直观标注码位/体积量级,让用户知道所选字库大小(M2.6 含字重:`<name> <Weight>(<marker>)`)。对应 `MeowOS/fonts/<family>/<weight>/font.json` 的 `charset`(`common|gbk|full`)+ `glyph_count`;档位枚举 `MEOW_FONT_TIER_COMMON`(默认/≈4k)/`GBK`(大/≈20k)/`FULL`(全/可选);`gen_font.py --charset common|gbk|full [--weight <name>]` 按档生成。定案见 MEOWOS_SPEC_REF §5.2「字体档位/大字库」与 §5.7.8 [源:用户 2026-08-30]。
|
||||
|
||||
> 新增设置项时:在清单加一行 + 存储 schema + API 常量/枚举 + 实体 UI 控件 + web 面板控件(四端同步)。
|
||||
|
||||
> **主题三键语义(2026-08-30 定案,实现权威见 MEOW_APP_SDK §8.1)**:`ui.style`(MD3/MIUIx)=**顶级主题风格族**(定义整套配色/控件形态/圆角/描边);`display.theme`(dark/light)=该 style 下的**明暗**;`display.background` =**由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度,可选集+默认随 style/mode 一起变)。三者关系:**切明暗 → 同 style 浅/深背景自动切换**(修复"浅色不搭");**切 style → 整套配色+背景换族**(修复"背景仅对主页生效/只有深色")——同时把 `display.background` 从"仅 launcher 主页/仅深色"改为"**全局背景源**"(状态栏/未声明背景 App/主页同步;App 显式声明背景则覆盖,App 声明优先)。`meow_ui_theme` 旧字段 `bg/fg/accent/dim` 保留映射到 MD3 语义色(§5.8.4)。**注**:`ui.style` 值域现状 `meow_settings.c g_defs` 为 `"md3,miui"`(label "Material You,MIUI");本定案将第二风格命名为 **`miuix`**(对应 `MEOW_STYLE_MIUIX`),实现时把 `g_defs` 的 `miui` 对齐为 `miuix`(待定案——MIUIx 未实现,当前仅 md3 生效)。
|
||||
> **主题三键语义(2026-08-30 定案,实现权威见 MEOW_APP_SDK §8.1)**:`ui.style`(MD3/MIUIx)=**顶级主题风格族**(定义整套配色/控件形态/圆角/描边);`display.theme`(dark/light)=该 style 下的**明暗**;`display.background` =**由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度,可选集+默认随 style/mode 一起变)。三者关系:**切明暗 → 同 style 浅/深背景自动切换**(修复"浅色不搭");**切 style → 整套配色+背景换族**(修复"背景仅对主页生效/只有深色")——同时把 `display.background` 从"仅 launcher 主页/仅深色"改为"**全局背景源**"(状态栏/未声明背景 App/主页同步;App 显式声明背景则覆盖,App 声明优先)。`meow_ui_theme` 旧字段 `bg/fg/accent/dim` 保留映射到 MD3 语义色(§5.8.4)。**注**:`ui.style` 值域已由 `"md3,miui"` 对齐为 **`"md3,miuix"`**(label "Material You,MIUIx",对应 `MEOW_STYLE_MIUIX`);存量 `settings.conf`/备份里的 `"miui"` 在 `meow_settings.c apply_value`(SET_ENUM)里兼容映射为 `miuix`。**MIUIx 已实现**(M2.90):`meow_ui_theme.c` 为 `MEOW_STYLE_MIUIX` 提供整套 light/dark 配色+控件形态参数,`display.background` 按 style×mode 派生 MIUIx 色板;状态栏图标集仍为 MD3 一套(无 per-style 换集接口)。
|
||||
|
||||
## 2. 统一存储(单一数据源)
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# POWER_HW_FACTS — meow_powermgr 期 0 真机只读调研结论
|
||||
|
||||
> 调研时间:2026-09-01 15:43(设备 10.78.193.50,uptime 1:48,`/tmp/meow_boot_mode`=meow,固件 0.7.89+)。
|
||||
> 方式:全部只读(ls/cat/sysfs 读),未写任何节点、未碰进程、未重启。
|
||||
> 对照基准:`meow_svc_power battery` = `battery=94%(Discharging) 4275mV`。
|
||||
|
||||
## 0. 两个决定形态的关键结论(必读)
|
||||
|
||||
| 问题 | 结论 | 影响 |
|
||||
|---|---|---|
|
||||
| `battery/current_now` 存在吗? | **不存在**。battery/ 全集无 current_now/charge_now/charge_full/charge_full_design/cycle_count/temp | 实时功率**不可能 measured**;走「容量差分 derived」+「经验模型 estimated」双轨(设计 §3.3 第三档)。健康度(charge_full/design)**不可算**,只能透传 `health` 字符串 |
|
||||
| `cpufreq` 存在吗? | **不存在**(`/sys/devices/system/cpu/cpu0/cpufreq/` No such file;单核 cpu0) | 省电档**无降频控制点**;CPU 功耗项只能按 util×满载系数估算,无可控项(设计 §5.1 CPU 行降级为「仅观测」) |
|
||||
|
||||
## 1. §7 清单逐项结论
|
||||
|
||||
### 项 1:battery/ 全集
|
||||
```
|
||||
capacity=94 health=Good present=1 status=Discharging(会撒谎,勿用)
|
||||
technology=Li-ion voltage_now=4275000(µV) time_to_empty_now=8191
|
||||
```
|
||||
- `time_to_empty_now=8191`(=0x1FFF)为**恒定哨兵值**,不随负载变化 → 不可用,勿报。
|
||||
- 无 temp 节点 → 电池温度报 null(充电器温度见项 2 可替代参考)。
|
||||
|
||||
### 项 2:mp2731-charger/(权威充电态 + 充电功率可算)
|
||||
```
|
||||
status=Discharging(权威源,设计 §3.1 已实测) online=0 temp=300(=30.0°C)
|
||||
current_now=0(µA,放电时恒 0;充电时=充电电流) constant_charge_current=0/_max=4520000
|
||||
constant_charge_voltage=4200000/_max=4670000 charge_term_current=120000
|
||||
input_current_now=100000 input_current_limited=2000000 input_suspend=0
|
||||
input_voltage_settled=2280000 charge_control_limit=3000000 charge_full=0(无效)
|
||||
real_type=USB typec_power_role=sink step_charging_enabled=1
|
||||
```
|
||||
- **充电功率可估**:`constant_charge_voltage × current_now / 1e6`(充电中 current_now 非 0,待插电实测确认非 0 时刻的值域)。
|
||||
- `charge_type` 节点不存在(清单原问的 charge_type 无)。
|
||||
|
||||
### 项 3:fusb302 / ac / usb / axp_battery
|
||||
- `fusb302-typec-source/`:online✓(插线检测兜底)、voltage_now、current_max(PD 档位相关,本次=0)。
|
||||
- `ac/`:online=0;voltage_now=4600/current_now=1200 为**静态默认值**(online=0 时不变),仅 online 位可用。
|
||||
- `usb/`:online=0;current_now=780 静态默认。仅 online/present 位可用。
|
||||
- **`axp_battery/`:幻影设备(不可用)**——capacity=0、voltage_now=0、current_now=200000 **三次采样(间隔 5s)恒定不变** → 非实时电量计,是 AXP 遗留驱动的死数据。唯一有用信息:`energy_full_design=1600` + `voltage_max_design=4350000/min=3400000`——可作**标称容量参考**(1600mAh 级,与 R3 Pro II 官方 1600mAh 电池吻合;诚实标注:单位/口径未经证实,仅作续航估算的默认标称容量,可经 power_model.conf `nominal_mah` 覆盖)。
|
||||
|
||||
### 项 4:cpufreq —— **不存在**(见 §0)
|
||||
|
||||
### 项 5:rfkill —— **存在且可用**
|
||||
```
|
||||
rfkill0: type=bluetooth state=1 soft=0 hard=0
|
||||
rfkill1: type=wlan state=1 soft=0 hard=0
|
||||
rfkill2: type=wlan state=1 soft=0 hard=0
|
||||
rfkill3: type=bluetooth state=1 soft=0 hard=0
|
||||
```
|
||||
- 4 个 rfkill(2×wlan + 2×bluetooth,两个同类型并存——WiFi/BT combo 芯片双接口特征)。
|
||||
- **软关断控制点存在**(写 soft=1,本次未写只读 state)→ 期 3 策略层可用「rfkill soft block」替代 `ip link down`/`hciconfig down`(更省电)。**红线不变:策略层不直接 poke sysfs,需经 svc_power 类动作通道或新增授权写点,期 3 定案。**
|
||||
|
||||
### 项 6:背光节点全集
|
||||
```
|
||||
max_brightness=101 brightness=0 actual_brightness=0 bl_power=0 type=raw
|
||||
```
|
||||
- **⚠️ 量程是 0-101,不是设计 §3.2 假定的 0-255** → 功耗表背光分档按 0-101 归一(亮度%=brightness/101×100 再分档)。POWER_MANAGER_DESIGN §3.2 表「0-255」需修正为「0-101(R3 Pro II 实测)」。
|
||||
- 调查时刻 brightness=0(设备息屏中)→ **息屏态可用 actual_brightness==0 判定**(fb0/blank 读取为空串——驱动未实现 read,不可用)。
|
||||
|
||||
### 项 7/8:CPU 满载/背光/WiFi 标定实验
|
||||
- 本期 0 未执行:容量差分法需分钟级窗口 + 逐项对照(亮屏定档/stress 满载/WiFi 传输),且设备无 current_now 真值可借 → **留待用户在场配合的标定窗口**(每次实验 ≥10min)。
|
||||
- 方法已备:capacity 差分(94%→x%)÷ Δt × nominal_mah × 平均电压 = 平均功率;基线=息屏 WiFi 关联空闲。
|
||||
- 在标定完成前,一切 mW 数字均为**占位经验值**(设计 §10 诚实标注不变)。
|
||||
|
||||
### 项 9:WiFi 状态/计数器
|
||||
- `wpa_cli status` ✓:`wpa_state=COMPLETED`、bssid/freq/ssid/ip_address 全有(关联态可判)。
|
||||
- `/proc/net/dev` wlan0 收发计数器 ✓(bytes/packets 递增)→ 传输态由增量判,可行。
|
||||
- wpa_cli 无直接 scan 计数;扫网风暴可用 `wpa_cli scan_results` 时间戳或 wpa 日志——期 2 再定,标记**待验**。
|
||||
|
||||
### 项 10:蓝牙
|
||||
- `hciconfig hci0` ✓(Type: Primary Bus: UART,当前 DOWN,RX/TX 计数器有)。
|
||||
- `/sys/class/bluetooth/hci0/` ✓ → up/down + 连接数可判(连接数走 `hciconfig -a` 或 /sys 下 device 枚举,期 2 细化)。
|
||||
|
||||
### 项 11:audiod MSG_AUDIO_STATE
|
||||
- `meow_audio status` 实测:`active=0 app=- sid=0 rate=44100 ch=2 fmt=- written=... played=... underrun=1 dropped=0 stall=0 txdrop=0 paused=0 route_mode=auto port=4 vol=44`。
|
||||
- **字段完备**:active(流活跃)/route/port/vol 全有 → 音频链功耗项输入够用(无流≈静态 / 播放≈DAC+FPGA 档)。
|
||||
|
||||
### 项 12:thermal zones —— **不存在**
|
||||
- `/sys/class/thermal/` 目录本身不存在;`/sys/class/hwmon/` 也不存在。
|
||||
- 唯一温度源:`mp2731-charger/temp`=300(30.0°C,充电器芯片温度,非电池/SoC)。
|
||||
- → 「异常发热」辅助信号**无热源可用**,期 2 异常检测规则不含温度维度(或仅用充电器温度粗参考)。
|
||||
|
||||
## 2. 对设计文档的回填修正
|
||||
|
||||
1. **§3.2 背光量程 0-255 → 0-101**(max_brightness 实测)。
|
||||
2. **§3.3 实时功率**:current_now 路径(measured)**整层不可达**——v1 只有 derived(容量差分)+ estimated(经验模型)。UI 必须带 confidence 标注(设计已要求,此处确认强制执行)。
|
||||
3. **§3.1 健康度**:charge_full/design 不存在 → health 输出透传 `health` 字符串(Good),不输出百分比。
|
||||
4. **§3.1 续航估算**:以容量差分平均速率(%/min)为准;nominal_mah 默认 1600(axp energy_full_design 旁证,待标定)。
|
||||
5. **§5.1 CPU 行**:无 cpufreq → 「降频」行为项删除,CPU 仅观测。
|
||||
6. **§5.1 WiFi/BT 行**:rfkill soft block 可用作控制点候选(期 3 定案,v1 不动)。
|
||||
7. **息屏判定**:fb0/blank 不可读 → 用 `backlight_pwm0/actual_brightness==0` 代理(期 1 采纳)。
|
||||
8. **充电功率**:mp2731 `current_now`(µA) + `constant_charge_voltage`(µV) 可估充电功率(插电态待实测值域)。
|
||||
|
||||
## 3. 遗留 / 待验
|
||||
|
||||
- [ ] 插电态复测 mp2731 current_now/constant_charge_current 非零值域(充电功率估算校准)。
|
||||
- [ ] 标定实验(项 7/8):需用户在场,≥10min/项 × 约 5 项。
|
||||
- [ ] axp energy_full_design=1600 的单位口径(mAh? mWh?)——与官方 1600mAh 规格吻合但未经驱动源码证实。
|
||||
- [ ] wpa 扫网频率计数法(期 2)。
|
||||
- [ ] 时间校准对照:无 current_now,derived 功率的时间分辨率受 capacity 1% 粒度限制(1600mAh×1%=16mAh,~5-15min 才跳 1%)→ 续航估算 v1 精度受限,UI 需标注。
|
||||
@@ -0,0 +1,476 @@
|
||||
# MeowOS 第三方 App 开发指南(APP_DEV_GUIDE)
|
||||
|
||||
> 受众:想在 R3 Pro II / MeowOS 上发布自己 App 的第三方开发者(非 MeowOS 系统组)。
|
||||
> 从零到上架动线:**环境 → 最小 App → 工程结构 → 构建打包 → 签名安装 → 部署调试 → SDK 能力 → 红线 → FAQ**。
|
||||
> 权威口径:本文所有 API 以 `patch_v2/sdk/meow_ui.h` 为准;规范以 `MEOW_APP_SDK.md` / `APPS.md` / `docs/MEOWOS_SPEC_REF.md` 为准。凡与 `meow_ui.h` 不一致,以头文件为准。
|
||||
> 配套文档:`MEOW_APP_SDK.md`(SDK 设计全量)、`APPS.md`(加载器/生命周期)、`docs/MEOWOS_SPEC_REF.md`(速查)、`NOTIFICATION_SYSTEM.md`(通知)、`SYSTEM_SETTINGS.md`(设置)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一图看懂:App 是什么
|
||||
|
||||
一个 MeowOS App = **一个静态 MIPS32r2 ELF 二进制** + 一份 `app.json` 描述 + 若干资源,打包成 `.mpk`(= tar.gz),装到 TF 卡 `MeowAPPs/<package>/` 下。二进制**链接 `meow_ui.a` 静态库**,通过 `meow_ui.h` 声明的一套 API 拿屏幕、输入、控件、主题、字体、通知、音频——**绝不**自己 `open("/dev/fb0")` / `mmap` / `ioctl` / `open("/dev/input/event*")`。
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ MeowAPP(你的业务,只调 meow_ui API)│
|
||||
├──────────────────────────────┤
|
||||
│ meow_ui.a(静态库,含 meow_font) │
|
||||
│ 绘制/控件/图片/输入/主题/字体/通知/音频│
|
||||
├──────────────────────────────┤
|
||||
│ MeowOS 系统(launcher/amsd/audiod)│
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
设备:480×720、RGB565(代码里颜色统一 24-bit `0xRRGGBB`,SDK 内部转 RGB565);文本字号约定 **28**(标题/状态栏)/ **20**(正文/按钮)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 开发环境准备
|
||||
|
||||
### 1.1 交叉工具链
|
||||
|
||||
仓库自带了 mipsel musl 静态交叉工具链,直接用,不用自己装:
|
||||
|
||||
```sh
|
||||
$HOME/Project/R3ProII/.tools/zig-mipsel-cc # zig cc 包装(--target=mipsel-linux-musleabi, soft-float)
|
||||
$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/ # sysroot(含 libz.a 等)
|
||||
```
|
||||
|
||||
编译一个静态 MIPS 二进制(参考 `patch_v2/hello_app/build_hello.sh`):
|
||||
|
||||
```sh
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||
"$CC" -Os -static -Wall -Wextra -I"$ROOT/patch_v2/sdk" -I. \
|
||||
app.c ui/*.c logic/*.c \
|
||||
"$ROOT/patch_v2/sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o myapp
|
||||
```
|
||||
|
||||
要点:
|
||||
- 必须 `-static`(加载器只跑静态 MIPS32r2 ELF,软/硬浮点均可,见 `APPS.md §2.1`)。
|
||||
- 链 `meow_ui.a`(`build_sdk.sh` 已把 `meow_font.o` 打进库里)+ `libz.a`(meow_font 的 inflate 依赖)+ `-lpthread`。
|
||||
- `-I"$ROOT/patch_v2/sdk"` 引 `meow_ui.h`/`meow_ui_view.h` 等公共头;`-I.` 引你自己工程的头。
|
||||
- 期望 `-Wall -Wextra` 零警告(参照 hello)。
|
||||
|
||||
### 1.2 宿主编译(离屏逻辑测试)
|
||||
|
||||
SDK 依赖板载 `/dev/fb0` 与字库,宿主机没有——但你可以**链真实 SDK 目标文件 + 打桩**来跑逻辑单测(参考 `patch_v2/sdk/test_host_stub.c`):
|
||||
|
||||
- `test_host_stub.c` 提供了 `meow_font_*` / `meow_ui_global_render` 等宿主桩(无 fb → 5×7 点阵回退;无字库 → `meow_font_load` 返回 -1)。
|
||||
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的,宿主宿主验证全 PASS)。
|
||||
- 注意:`logic/` 层要写**纯 C**(不依赖 `meow_ui.h`),才能裸宿主单测;`ui/` 层才碰 SDK。
|
||||
|
||||
### 1.3 qemu-mipsel 全系统仿真
|
||||
|
||||
需要跑全链路(含内核/fb)时可用 `qemu_env/scripts/run_halley6.sh`(Ingenic X1600E 全系统仿真):
|
||||
|
||||
```sh
|
||||
qemu_env/scripts/run_halley6.sh # 引导内核到串口控制台(默认)
|
||||
qemu_env/scripts/run_halley6.sh -usbrootfs # 附加 USB rootfs 镜像
|
||||
```
|
||||
|
||||
注意:必须带 `mem=64M`(脚本已内置,去掉会 early hang);`-usbrootfs` 的 USB 数据通路在 QEMU 里仍不完整(设备未枚举),rootfs 引导会失败——多数情况下它在宿主编译 + 真机验证之间用不上,**不是常规提交流水线**。
|
||||
|
||||
### 1.4 真机前置
|
||||
|
||||
- 出站靠 TF 卡;确保卡上至少有 `MeowAPPs/`(放 App)与 `MeowData/`(App 数据,首启自动建)。
|
||||
- 设备跑 MeowOS 模式(默认)。看 `MEOWOS_SPEC_REF.md §5.4` 确认固件版本单点 `/etc/meowos_version`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 五分钟最小 App(以 hello 为模板)
|
||||
|
||||
最快路径:拷贝 `patch_v2/hello_app/` 改包名与文案。逐文件讲。
|
||||
|
||||
### 2.1 app.json 全字段表
|
||||
|
||||
`app.json` **必填**,放 `MeowAPPs/<package>/` 根目录。参考示例(`patch_v2/apps_assets/apps/os.meow.hello/app.json`):
|
||||
|
||||
```json
|
||||
{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello",
|
||||
"package":"os.meow.hello","version":"0.1","framework":1,"immersive":1,"depends":"curl:optional"}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `name` | ✓ | 列表显示名。**当前仅 ASCII**(UTF-8 中文留待 v3,需完整 UTF-8→UTF-16 转换) |
|
||||
| `exec` | ✓ | 可执行文件**绝对路径**,必须是**静态** MIPS32r2 ELF(指向 `MeowAPPs/<package>/` 内二进制) |
|
||||
| `package` | — | 反域名权威标识(= app_id = 目录名 = `.mpk` 文件名茎,三者必须一致)。**`os.meow.*` 为系统保留前缀**(见 §5),第三方用自有前缀(如 `dev.*`)。缺失回退目录名;两者存在且不同 → WARN |
|
||||
| `version` | — | 显示字符串(versionName 角色,如 `1.2.3-beta`,**不参与比较**) |
|
||||
| `version_code` | — | **纯数字,安装/升级比较的唯一依据**;缺失 → 拒装;新版 < 已装 → 拒装(防降级),相等 → 允许重装。见 §5 |
|
||||
| `author` | — | 保留字段 |
|
||||
| `framework` | — | `1`=SDK 框架 App(用 `meow_ui.a`;**推荐**)。`0`/不写=裸自绘(不推荐,见红线) |
|
||||
| `immersive` | — | `1`=沉浸。**注意(M2.89+)**:看门狗 `SIGUSR1` 只发给「**裸沉浸**」App(非 `framework=1`);SDK 框架 App 的返回走 `on_swipe`,别把 `SIGUSR1` 当返回键 |
|
||||
| `background` | — | M4:无头后台服务注册权限(`service.<svc>` 服务二进制 = App 同目录 `<svc>` 文件) |
|
||||
| `background_audio` | — | M4:后台播放保活(进程不冻结 + UI 冻结复用 §10.5 自抑);播放类 App 声明。如 `dev.player.ncm` 已声明 `background_audio=1` |
|
||||
| `fullscreen` | — | app 级全屏:状态栏**收合至提示线** + 保留左缘返回/顶部提示线/下滑通知中心。activity 级用 `route.<id>.fullscreen` |
|
||||
| `fonts` | — | 字库档位声明:`"full"` / `"gbk"` / `"common"`(默认 `common`≈4k;需大字库声明 `"full"`)。**注意**与 `"fonts":["<handle>"]`(应用自带字库句柄)同名二义,见 `MEOWOS_SPEC_REF §7 A-14` |
|
||||
| `depends` | — | **二进制依赖声明**:`"curl,jq:optional,ffmpeg:required"`(JSON 值串)或行式 `depends=curl,jq:optional,ffmpeg:required`。逗号分项,每项 `<name>[:<mod>]`;无标注默认 required,也接受 `:opt`/`:req`;未知修饰符按 required 并 WARN。启动前查板载 `/usr/bin/<name>` 或卡上 `/usr/data/mnt/sd_0/MeowOS/opt/bin/<name>`(X_OK,命一即可)。required 缺失 → **不启动**(toast「缺依赖: <name>」);optional 缺失 → 仅 WARN 放行 |
|
||||
| `icon` | — | 预留:应用列表图标 |
|
||||
|
||||
> ⚠️ 解析器极简(launcher/nyai 内实现):只匹配 `"name":`/`"exec":`/`"package":`/`framework`/`immersive`/`route.*`/`awake.*`/`service.*`/`background`/`background_audio`/`fullscreen`/`fonts`/`depends`,**不做 JSON 校验**(多行/单行均可),但字段名和引号写法要规范。
|
||||
|
||||
### 2.2 main 入口:SDK 初始化 + 事件循环 + 退出约定
|
||||
|
||||
看 `patch_v2/hello_app/app.c`,核心骨架如下:
|
||||
|
||||
```c
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_nav.h"
|
||||
#include "meow_ui_media.h"
|
||||
#include <signal.h>
|
||||
|
||||
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); } /* 干净退出:不画 BYE 直接退 */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_log(MEOW_LOG_INFO, "start");
|
||||
|
||||
/* ① SDk 初始化:开 fb / 读几何 / 载字库 / 建 back_buf。opts 宽高 0=自动读,宿主导跑用 480x720 */
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
|
||||
|
||||
/* ② 主题监听 + M3 导航上报 + 媒体会话(可选,但推荐接线) */
|
||||
meow_theme_add_listener(NULL);
|
||||
meow_ui_nav_init(); /* 读 MEOW_ACTIVITY / MEOW_CHAIN 初始化导航栈 */
|
||||
meow_ui_media_init(NULL); /* app_id = env MEOW_LOG_APP */
|
||||
meow_ui_media_on_playctl(demo_media_playctl);
|
||||
|
||||
signal(SIGTERM, on_sigterm); /* 退出兜底:SIGTERM → request_exit */
|
||||
|
||||
/* ③ 建各页 view(on_create 建控件/状态)+ 弹层实例(dialog/sheet 建一次,复用)*/
|
||||
meow_view *home = home_view_create();
|
||||
meow_view *full = full_view_create();
|
||||
meow_view *demo = demo_view_create();
|
||||
|
||||
/* ④ 进入根页:activity_push + home on_enter → home_redraw */
|
||||
meow_view_page_push(ACT_HOME);
|
||||
|
||||
/* ⑤ 事件循环回调表(on_key 物理键是系统语义,SDK 不下发业务确认/exit)*/
|
||||
meow_ui_events ev = { app_on_key, app_on_tap, app_on_swipe, app_on_tick,
|
||||
app_on_swipe_progress, app_on_touch_down, app_on_touch_move };
|
||||
meow_ui_run(&ev); /* 阻塞,直到 meow_ui_request_exit() */
|
||||
|
||||
/* ⑥ 清理:释放 view(触发各页 on_destroy)+ 弹层 + shutdown */
|
||||
meow_view_free(full); meow_view_free(demo); meow_view_free(home);
|
||||
meow_dialog_free(g_dlg); meow_sheet_free(g_sheet);
|
||||
meow_ui_shutdown();
|
||||
meow_log(MEOW_LOG_INFO, "exit");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**退出约定**(`APPS.md §3.3`):
|
||||
- 任意退出码均可,`exit(0/1)` 即可,**不要 `abort()` 留僵尸**——守护进程 `waitpid` 立即返回并恢复播放器。
|
||||
- 用 `meow_ui_request_exit()` 或**根页左缘左滑**退出;**绝不监听物理键映射 exit**(见 §8 红线)。
|
||||
- 不需要「恢复/清屏」——应用退出后系统恢复原界面,你清屏反而闪一下。
|
||||
|
||||
### 2.3 事件分发
|
||||
|
||||
`meow_ui_run` 给的平铺回调,在 `app.c` 里路由到当前页(见 `app.c` 的 `app_on_tap`/`app_on_swipe`/`app_on_tick`/`app_on_touch_*`)。典型分法:
|
||||
|
||||
- `on_tap` → 命中当前页控件(`meow_widget_hit` / `meow_listview_hit` / `meow_topappbar_hit` 等)。
|
||||
- `on_swipe` → `LEFT`=返回(`meow_view_page_pop`,根页则 `meow_ui_request_exit`);`UP/DOWN`=滚动。
|
||||
- `on_touch_down`/`on_touch_move` → 跟手拖动(slider / 列表 / 页面滚动)。
|
||||
- `on_tick` → 动画/时钟(如进度条步进)。
|
||||
- **`on_key` 仅供 SDK 内部状态,不含电源/音量/播放的确认/exit 语义**——别把它当输入。
|
||||
- 左缘返回手势:边沿上报 `on_swipe_progress`,SDK 自动画顶部进度条(`meow_ui_swipe_hint`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工程结构约定
|
||||
|
||||
hello 的分层(`hello_internal.h` 里写得很清楚),建议照抄:
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── app.c # main + 装配 + 事件分发(把 meow_ui_run 回调路由到各页)
|
||||
├── myapp_internal.h # 跨 TU 共享声明(只 extern 声明,不定义)
|
||||
├── ui/
|
||||
│ ├── home/home.c # 每页一个 view 文件(持本页视图态)
|
||||
│ ├── home/home.h
|
||||
│ ├── demo/demo.c
|
||||
│ ├── list/list.c # 页面内 CONTAINER 子视图(列表区块)
|
||||
│ ├── full.c
|
||||
│ └── common/chrome.c # 共享 chrome(TopAppBar / NavigationBar / FAB)
|
||||
└── logic/
|
||||
├── logic.h # 纯 C 接口(不依赖 meow_ui.h)
|
||||
├── state.c # 计数/状态业务 + getter/setter
|
||||
└── notify.c # 通知业务(此处才 include meow_ui.h 用 SDK 通知 API)
|
||||
```
|
||||
|
||||
**分层契约**(`logic/logic.h`):
|
||||
- ① UI→业务:只调 `logic_*` / `notify_*` 纯函数命令/getter,业务**不回调 UI**。
|
||||
- ② 业务→UI:业务态变化经 `logic_set_listener(cb, ctx)` 的**可选 push 监听器**通知 UI 刷新(`home_state_listener` 示例)。
|
||||
- ③ 每页 `meow_view` 的 `ctx` = **视图控制器**(biz-UI 绑定点),页面后续可把这层状态吸进控制器。
|
||||
|
||||
**视图框架 `meow_view`**(`patch_v2/sdk/meow_ui_view.h`):
|
||||
- 生命周期钩子:`on_create → on_enter → (事件循环) → on_leave → on_destroy`,另有 `on_layout`/`on_draw`/`on_event`。均可 NULL。
|
||||
- **页面即文件**:每页一个 `meow_view_create(id, MEOW_VIEW_ACTIVITY, &hooks, &ctrl)`,`id` 是你命名的稳定值(`ACT_HOME=0`/`ACT_FULL=1`/`ACT_DEMO=2`)。
|
||||
- 页面切换统一走 `meow_view_page_push/pop/replace`(包装 activity 栈,**on_enter/on_leave 唯一入口**),App 不再分别调 `activity_push/pop`。
|
||||
- 子视图嵌套:`meow_view_add_child` 挂 CONTAINER 子 view(如 demo 页的列表区块 `list.c`),嵌套生命周期由 SDK 递归驱动(随父页进出),无需 App 显式接管。
|
||||
- `on_draw` 为回调、view 不缓存像素(非保留);重绘由 `meow_ui_invalidate` 触发。
|
||||
- `meow_view_dispatch_event` 是「把语义事件注入当前页谓词子树路由」的入口(先子>父按 z 降序,命中即消费);未接线于 `meow_ui_run`,你可在 `on_tap`/`on_swipe` 里改用它做 SDK 原生路由。
|
||||
|
||||
---
|
||||
|
||||
## 4. 构建与打包
|
||||
|
||||
### 4.1 链接 meow_ui.a
|
||||
|
||||
见 §1.1 命令。`meow_ui.a` 由 `patch_v2/sdk/build_sdk.sh` 构建(内含 `meow_font.o`),改 SDK 源码后**重编 meow_ui.a 必须同步重编你的 App**(结构变化即 ABI 变化,见 `MEOWOS_SPEC_REF §3.4`)。
|
||||
|
||||
### 4.2 `.mpk` = tar.gz
|
||||
|
||||
`.mpk` 内容 = `app.json` + 二进制 + 资源(**不含 data/cache**,运行时按设备生成)。包名 = `.mpk` 文件名茎 = `app.json.package`(如 `os.meow.hello` → `os.meow.hello.mpk`)。
|
||||
|
||||
> **nyai 无独立 `pack` 命令**。两种出包:
|
||||
> 1. **手工打包**(从 staging 目录,`meow_sign/README §2.2` 的口径):
|
||||
> ```sh
|
||||
> mkdir -p build/os.meow.xxx
|
||||
> # 放 app.json + meow 二进制 + 资源;app.json.package == 目录名
|
||||
> tar czf os.meow.xxx.mpk os.meow.xxx # .mpk 须在 tar 根(无 <pkg>/ 包裹 → 换包时注意)
|
||||
> ```
|
||||
> 2. **导出已装应用**:设备上 `nyai export <pkg> [out.mpk]`(打 `MeowAPPs/<pkg>/`,排掉 `*.pre_*`/`*.bak_*`/`logs/` 等运行残留)。
|
||||
|
||||
`.mpk` 内部结构:
|
||||
|
||||
```
|
||||
os.meow.xxx.mpk (tar.gz)
|
||||
├── app.json # package/exec/version(+version_code)/framework/immersive/background_audio/...
|
||||
├── meow # 静态 MIPS32r2 ELF(exec 指向)
|
||||
├── <资源文件> # 只读程序资源
|
||||
└── <signature footer> # 仅签名过的包有(Ed25519 v2 184B;尾部追加)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 签名与安装分级
|
||||
|
||||
### 5.1 分级规则(`APPS.md 附录 §3` / 签名设计)
|
||||
|
||||
| 包 | 规则 |
|
||||
|---|---|
|
||||
| **`os.meow.*`(系统保留前缀)** | **必须有有效签名**;无签 / 验签失败 → 一律拒装(防伪造系统包名) |
|
||||
| 第三方(自有前缀如 `dev.*`) | 无签可**直接装**(安装时告警);**有签必须验签**(失败拒装) |
|
||||
| 所有包 | 验签 + **三重绑定** + **防降级**(见下) |
|
||||
|
||||
**防降级**:只看 `version_code`(纯数字)。新版 code < 已装 → 拒装;相等 → 允许重装;**code 缺失 → 拒装**(提示缺 version_code)。`version` 仅显示,不比较。
|
||||
|
||||
**三重绑定**(三方各司其职):
|
||||
1. 签名 footer 的 `package` == `.mpk` 文件名茎(`meow_verify` 校验)——防改名。
|
||||
2. 签名 footer 的 `package` == 解包 `app.json.package`(nyai / launcher 安装流校验)——防改 app.json。
|
||||
3. `os.meow.*` 前缀 + 必须验签通过(nyai / launcher 安装流)——防无签冒充系统包。
|
||||
|
||||
### 5.2 开发者自签名流程(不泄露任何密钥材料)
|
||||
|
||||
> 工具:`patch_v2/meow_sign/meow_sign`(**宿主端、离线维护机**,不落设备、设备只持公钥)。私钥**永不入库、永不落设备**。设备侧把关的是固件 `/usr/bin/meow_verify`(内嵌公钥数组,任一公钥通过即放行)。
|
||||
|
||||
```sh
|
||||
# ① 首次:生成/备份密钥(仅一次;若 .keys/signing.key 已存在,直接复用,勿覆盖——覆盖会令旧包全部失效)
|
||||
./patch_v2/meow_sign/meow_sign keygen .keys/signing.key
|
||||
# 私钥 .keys/signing.key(0600) 离线备份;公钥 .keys/signing.key.pub 可入库/固化进固件
|
||||
|
||||
# ② 打包(不含签名,纯 tar.gz;app.json.package 必须 == 目录名 == .mpk 文件名茎;version_code 单调递增)
|
||||
tar czf os.meow.xxx.mpk os.meow.xxx
|
||||
|
||||
# ③ 签名(默认 v2 含被签时间戳)
|
||||
./patch_v2/meow_sign/meow_sign sign os.meow.xxx.mpk .keys/signing.key
|
||||
|
||||
# ④ 产签后自测(推荐)
|
||||
./patch_v2/meow_sign/meow_sign verify os.meow.xxx.mpk .keys/signing.key.pub
|
||||
./patch_v2/meow_verify/meow_verify_host os.meow.xxx.mpk # 用固件内嵌公钥,与设备同口径
|
||||
|
||||
# ⑤ 装到设备:把 .mpk 放卡上 → nyai install
|
||||
```
|
||||
|
||||
要点:
|
||||
- `sign` 的 `.mpk` 文件名茎**必须等于包名**(否则 `meow_verify` 二次校验拒)。
|
||||
- v2 块(184B,含时间戳)需 **2B+ 固件**;`--v1` 产物(180B)兼容旧 2A 固件——固件与工具链同版本发布即可避免兼容坑。
|
||||
- 签名是**防误装 / 防伪造包名 / 防沿途篡改**,不是安全边界(见 `meow_sign/README §0`,诚实标注)。
|
||||
- **本仓库 `.keys/signing.key` 是系统主钥**——第三方开发者**不要用它**,用自己的密钥对(否则无法区分来源,且泄露私钥等于放弃系统包信任)。你自己的私钥离线备份,公钥按需提交给设备厂商固化进 `meow_verify` 公钥数组(或走公开渠道分发)。
|
||||
|
||||
### 5.3 安装
|
||||
|
||||
- 设备端:`nyai install <pkg或文件.mpk>`(固件内置 `/usr/bin/nyai` 优先,或卡上 `MeowOS/opt/bin/nyai`)。同名 = 更新(旧目录备份 `.bak_<pid>`,`MeowData/<pkg>/{data,cache}` 保留,成功自清 `.bak`)。
|
||||
- `nyai list` / `nyai info <pkg>` / `nyai remove <pkg>`(数据默认保留,`--purge` 连 MeowData 删)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 部署与调试
|
||||
|
||||
### 6.1 TF 卡目录
|
||||
|
||||
```
|
||||
/usr/data/mnt/sd_0/
|
||||
├── MeowAPPs/<package>/ # app.json + meow 二进制 + 资源(目录名 = app.json.package)
|
||||
├── MeowData/<package>/{data,cache} # App 数据(首启自动建;exFAT 无软链,用 env 注入)
|
||||
└── MeowOS/logs/<package>/ # App 日志(每次启动新文件 <ts>_<pid>.log)
|
||||
```
|
||||
|
||||
启动时注入 env(SDK `meow_config_*`/`meow_ui_nav_init`/日志都读它):
|
||||
`MEOW_DATA_DIR` / `MEOW_CACHE_DIR` / `MEOW_LOG_DIR` / `MEOW_LOG_APP`(=package) / `MEOW_LOG_LEVEL` / `MEOW_ACTIVITY` / `MEOW_CHAIN`(+ `MEOW_FONT_PNG`/`MEOW_FONT_IDX` 当前生效字体、`MEOW_FULLSCREEN`)。
|
||||
|
||||
### 6.2 面板上传
|
||||
|
||||
MeowPanel(外置 TF 卡 Web 面板,8080)可 `nyai install/export/remove`。**⚠️ 面板鉴权默认关**(内网信任模型)——`reboot/shutdown/ota/apps/删除/上传` 危险端点勿暴露公网。
|
||||
|
||||
### 6.3 am CLI(活动管理器)
|
||||
|
||||
`am` 是 amsd 的用户侧 CLI(固件 `/usr/bin/am`),连 `/usr/data/meow_ams.sock`(`--sock` / `env MEOW_AMS_SOCK` 覆盖):
|
||||
|
||||
```
|
||||
am status # 在跑 App 表(pkg/状态/pid/运行时长/activity)
|
||||
am list # 注册表 + 状态(pkg/name/state/pid/launches/crashes)
|
||||
am history [pkg] # 事件历史(每 App 最近 8 条)
|
||||
am start <pkg>[/<act>] # 拉起(root)
|
||||
am kill <pkg> # 终止(SIGTERM 语义,root)
|
||||
am rescan # 手动触发注册表重扫(卡刚挂载/热装后立即可见)
|
||||
am resolve <pkg> <activity> # 路由决策(返回 CHAIN)
|
||||
```
|
||||
|
||||
写操作(start/kill)**仅 root**。**已知坑(agent-250)**:开机早于卡挂载 → amsd 注册表扫空 + 永不重扫 → `am start` 报 `not registered`——用 `am rescan` 或等 3s 自愈/30s 合并重扫。
|
||||
|
||||
### 6.4 日志
|
||||
|
||||
- **App 日志**:`meow_log(lvl, fmt, ...)` 一行;SDK 写 `MeowOS/logs/<package>/<ts>_<pid>.log`(每次启动新文件,单文件 ≥2MB 自动续新,每 App 总量上限,无卡降级 stderr 不崩)。级别:`MEOW_LOG_DEBUG/INFO/WARN/ERROR`;`meow_log_set_level` 可选,默认 `INFO`。
|
||||
- **崩溃日志**:系统级 `meow_crash` 捕获 + App 崩溃直报。真机看 `/usr/data/meow_*` 与 `MeowOS/logs/`。
|
||||
- **离线跑**:`meow_launcherd --scan` 列应用;宿主跑逻辑单测看 stdout。
|
||||
|
||||
---
|
||||
|
||||
## 7. SDK 能力速查表
|
||||
|
||||
(完整签名以 `patch_v2/sdk/meow_ui.h` 为准——下面每行都可交叉印证。)
|
||||
|
||||
### 7.1 控件库清单(一句话)
|
||||
|
||||
| 控件 | 一句 |
|
||||
|---|---|
|
||||
| `meow_btn_create`(Filled)/ `meow_btn_create_variant`(五型:FILLED/TONAL/OUTLINED/TEXT/ELEVATED) | 按钮;命中用 `meow_widget_hit`,选中/禁用走状态机 |
|
||||
| `meow_slider_create` / `meow_switch_create` | 滑条/开关;`set/get` 取值 |
|
||||
| `meow_list_create`(label-only)/ `meow_lazycolumn_create`/`meow_lazyrow_create` | 数据源回调 + 可见区虚拟化;`on_swipe` 里 `meow_list_scroll_by` 驱动滚动 |
|
||||
| `meow_listview_create`(`W_LISTVIEW` 结构化列表) | 行=图标+标题+副标题+右侧操作+选中;`meow_listview_hit`/`meow_listview_tap_at` |
|
||||
| `meow_textfield_create`(接 IME)+ `meow_textfield_activate` | 单行文本输入(阻塞 IME) |
|
||||
| `meow_card_create` + `meow_card_set_content` / `meow_divider_create` / `meow_badge_create` | 卡片/分隔线/角标 |
|
||||
| `meow_checkbox_create` / `meow_radio_create` | 勾选/单选(走 set_selected) |
|
||||
| `meow_progress_create`(线性)/ `meow_progress_circular_create`(圆形) | 进度条(0-100,`-1`=不定) |
|
||||
| `meow_fab_create` | 悬浮按钮 |
|
||||
| `meow_topappbar_create`(标题+返回+动作)/ `meow_navigationbar_create`(底部导航)/ `meow_tabbar_create`(顶部分页) | 标题栏/导航栏/页签 |
|
||||
| `meow_dialog_create`(CONFIRM/SELECT/INPUT/INFO)/ `meow_sheet_create`(BottomSheet) | 阻塞模态;点外/左缘返回=取消 |
|
||||
| `meow_row_create`/`meow_column_create`/`meow_box_create` + `meow_layout_add`/`arrange` + `meow_layout_spacer` | 组合布局(固定尺寸子项) |
|
||||
| `meow_widget_custom_create` | 自定义画布:App 给 render/hit 回调(同 `meow_widget`) |
|
||||
| 通用 | `meow_widget_set_opts`(Modifier 式 Options)、状态机 `meow_widget_set_state`(ENABLED/SELECTED/FOCUSED/PRESSED)、`meow_widget_set_z(999)` 置顶、`meow_draw_custom_register` 注册非控件直绘 |
|
||||
|
||||
### 7.2 主题(MD3 / MIUIx × 明暗——适配义务)
|
||||
|
||||
- 主题 = **顶级风格族 `ui.style`(`MEOW_STYLE_MD3`/`MEOW_STYLE_MIUIx`)× 明暗 `display.theme`(light/dark/auto)**。
|
||||
- **适配义务**:用 `meow_theme_get()` 取的**语义色**(`primary`/`surface`/`surfaceContainer`/`outline`/`bg`/`fg`/`accent`/`dim`…),**不要硬编码颜色**;明暗都要能看(`AUTO` 会来回切)。当前 MIUIx 仅换配色(状态栏图标未 per-style 换表,是已知缺口)。
|
||||
- 字号约定:`font_title=28` / `font_body=20`(`meow_theme` 给默认)。
|
||||
- 界面底 `meow_ui_set_page_bg(th->bg)`;状态栏高度 `meow_ui_statusbar_height()`(非全屏=28,全屏=0)用于内容 inset。
|
||||
|
||||
### 7.3 字体档位声明 + 应用字库接口
|
||||
|
||||
- 档位:`MEOW_FONT_TIER_COMMON`(默认 ≈4k)/ `GBK`(≈20k+)/ `FULL`(全字符集)。App 需大字库时 `app.json "fonts":"full"`,launcher 为你加载大字库。
|
||||
- SDK:`meow_font_set_tier` / `meow_font_tier`;`meow_font_get_available_count` / `meow_font_enumerate`(读 `MeowOS/config/fonts/index`)/ `meow_font_select_by_id`(App 会话内固定用某套;丢失降级默认 + WARN)。
|
||||
- **判缺字**:`meow_font_has_glyph(cp)` 预判当前字库是否含该码位——缺字自行回退/提示。
|
||||
- 注意:App **未自带字库**则跟随系统 `font.file`(launcher 以 `MEOW_FONT_PNG`/`MEOW_FONT_IDX` env 注入);应用自带字库(`load_handle`)为**草案/未实现**。
|
||||
|
||||
### 7.4 IME(输入法)
|
||||
|
||||
- 模式:`MEOW_IME_ASCII` / `MEOW_IME_NUM` / `MEOW_IME_PINYIN`(中文拼音)。
|
||||
- 阻塞:`meow_ui_text_input(ctx)`(回车=提交+关闭、ESC/点背景=取消);`meow_textfield_activate` 聚焦并打开阻塞 IME。
|
||||
- 非阻塞:`meow_ui_text_input_async` + `meow_ime_active()`/`meow_ime_text()`/`meow_ime_composition()`(App 在 `on_tick` 里轮询)。
|
||||
- **坑**:阻塞 IME 的内层轮询必须喂 `meow_ui_activity_ping`(SDK 已做)否则进度条/息屏卡死;键盘区背景快照/恢复(防残影);回显条是**实时输入回显**非候选,候选框留中文。
|
||||
|
||||
### 7.5 音频(meow_audio_*,独占 audiod)
|
||||
|
||||
- 链路:`meow_audio_open(rate, ch, fmt)` → 返回 `stream_id > 0` → `meow_audio_write(pcm, frames)`(**阻塞**,socket 背压)→ `meow_audio_pause(sid, paused)` → `meow_audio_close(sid)`。
|
||||
- audiod 独占 `hw:0,0`;**后开流赢**,旧流收 `MSG_AUDIO_PREEMPTED` → 你的 `on_preempt` 回调 → 之后 `write` 返回负值,应重新 `open`。
|
||||
- **audiod 不在 → `open` 返回 `MEOW_ERR_NOENT`(负值),不做隐式直开回退**;`MEOW_AUDIO_DIRECT=1` 仅调试用别依赖。
|
||||
- `meow_audio_query(&state)` 读快照(活跃流/采样率/underrun/路由口/音量);`meow_audio_on_preempt(cb, ctx)` 注册被抢占回调。
|
||||
- DAC 原生率跟随、无 SRC;格式归一(S16 直通 / S32 升位 / mono 复制);4.4/3.5 路由随插孔。
|
||||
- 播放类 App 记得 `background_audio=1` + 媒体会话上报(见 §7.7)。
|
||||
|
||||
### 7.6 通知(meow_notify_* / MSG_NOTIFY)
|
||||
|
||||
- 声明:`meow_notify_register_type(cat)`(每 App ≤4 类型);发:`meow_notify_send(&n)`(返回 id);消:`meow_notify_cancel(id)`;进度:`meow_notify_update_progress(id, p)`。
|
||||
- 配额:**每 App ≤4 类型;每类型活动 ≤4(满丢最旧);媒体单例(每 App 1 条,重发替换)**。
|
||||
- `timeout_ms`:`>0`=过期毫秒数;`0`=不过期(直到 cancel);`-1`(ONGOING)=常驻。
|
||||
- 负载 `meow_notify{n}`:`category`/`urgency`/`title`/`body`/`icon`/`timeout_ms`/`target_activity`(点击跳转 Activity 名,M3 导航预留)/`progress`。字段对齐 freedesktop `Notify()`。
|
||||
- 走 IPC `MSG_NOTIFY`;10s 后自动转储到 TF `MeowOS/notifications/`(ring 10MB)。当前 SDK 内部维护活动表 + 日志落点;真实投递到通知中心 UI(launcher 渲染)为已落地链路。
|
||||
|
||||
### 7.7 媒体会话(meow_ui_media_*)
|
||||
|
||||
- `meow_ui_media_init(app_id)` / `meow_ui_media_on_playctl(cb)`(注册播放控制并启动后台接收线程)/ `meow_ui_media_meta(state, pos_ms, dur_ms, title, artist, album, artwork, lyric)`(上报元数据)/ `meow_ui_media_stop()`。
|
||||
- 控制回调 `playctl(cmd, seek_ms)`:cmd `1`=播放 `2`=暂停 `3`=上一首 `4`=下一首 `5`=seek `6`=播放/暂停 toggle(物理播放键 164 恒发 6,由你对当前态翻转——**源是状态权威**)。物理上下曲:`meow_ui_media_sys_key(code)`。
|
||||
- 声明 `MF_PLAYBACK` 的 App 首次上报 → 注册为媒体源;通知中心媒体卡片 / 主界面 Now Playing 据此显示。
|
||||
- best-effort:无 launcher/socket 静默降级,绝不崩。
|
||||
|
||||
### 7.8 后台播放声明
|
||||
|
||||
- `app.json "background_audio":1`(M4 已落地):进程**不解冻** + **UI 冻结**复用 `§10.5 自抑`(比任务书 SIGSTOP 方案优)。launcher 有 `meow_bgplay` 保活表(退避 1/2/4/10s + 熔断 + 广播);应用列表标注「·后台」。依赖 `background` 字段 + `service.<svc>`。
|
||||
- 声明后台播放后:无头时仍喂媒体会话 + 可继续 `meow_audio_*` 出声;退出时停线程/关流。
|
||||
|
||||
### 7.9 应用配置(meow_config_*)
|
||||
|
||||
- App 自有键值偏好,落 TF 数据目录(`MEOW_DATA_DIR`,无卡回退 `/tmp/meowcfg_<pid>`),**不占板载、无 schema**(与系统 `meow_settings_*` 区分)。
|
||||
- `meow_config_set/get/int/bool/remove`;写 `<data>/<sanitized_key>.cfg`(key 只允许 `[A-Za-z0-9._-]`,上限 64)。返回码 `MEOW_CFG_OK(0)` / `NOENT(-2)` 等。
|
||||
|
||||
### 7.10 网络注意事项
|
||||
|
||||
- App 一般不需要联网;确需联网时**克制请求**(见 §8 风控式节制)。
|
||||
- 系统层面(`netmgrd` 期 0/1):无 iptables 用户态 / 无 cgroup / 无 conntrack → **内核级每应用管控不可行**;主杠杆 = DNS 代理(域名级)+ 连接普查(inet_diag)+ **SDK 自愿通道**。App 走 SDK 自愿通道配合管控是后续方向;当前 App 如联网,请自律、尊重断网降级。
|
||||
|
||||
### 7.11 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
|
||||
|
||||
- **任何 UI 文案都不要硬编码**,用 `meow_str("域.名")` 按当前语言取串(示例:`meow_str("common.ok")`)。`meow_str` 永不返回 `NULL`:未知 id 原样返回 `id`,方便你打点/兜底展示。
|
||||
- **`meow_strf(out, outsz, id, ...)`**:取串后按 `snprintf` 语义格式化进调用方缓冲(自带 NUL;`out==NULL`/`outsz<=0` 返回 `-1`)。
|
||||
- **切换语言**:`meow_str_set_locale("zh_cn"|"en_us")` 即时生效;locale 存系统设置 `settings.conf` 的 `ui.locale`(默认 `zh_cn`,SDK 启动惰性读取)。App 里做语言切换页时调用它即可。
|
||||
- **覆盖/追加**:可不改二进制在 TF 卡放 `MeowData/<pkg>/strings/<locale>.txt`(每行 `id=text`,UTF-8)覆盖内嵌或新增自己的串(如 `player.pause=暂停`);无卡/无文件/无该 id → 回落内嵌双语默认表(`common.*` + `ime.*` 内置,见 `include/meow_ui_str.h`)。
|
||||
- **id 规范**:`域.名`;SDK 自带 `common.*`/`ime.*`,你自建 `域`(如 `player.*`)避免撞名。
|
||||
- **边界**:当前为 i18n 雏形,仅 `zh_cn`/`en_us` 双语**整串**取用;**不做复数规则 / BiDi / RTL / 动态字体回退**(详见 `MEOW_APP_SDK.md §13.7`)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 规范与红线
|
||||
|
||||
> 详见 `APPS.md §1.3 / §3.3 / §3.4.1`、`MEOWOS_SPEC_REF §2.4 / §5.1`。以下为最要紧的违约原因。
|
||||
|
||||
1. **物理键禁映射返回/确认/OK/翻页**。物理键是系统资源:电源=息屏/亮屏/长按菜单/8s 强关,音量=调音量,播放/上下曲=媒体控制,返回=**触屏左滑**(`MEOW_SWIPE_LEFT`)/虚拟返回(非物理键)。**绝不用 `on_key(POWER)→exit` 这类映射**;`on_key` 仅供 SDK 内部状态。界面导航一律走触摸(返回钮/左缘左滑)。
|
||||
2. **局部脏区刷新,别整屏重画**。控件/区域变化用 `meow_ui_invalidate(x,y,w,h)`(清底 + 按 z 重画相交绘制物 + 原子上屏),别 `meow_ui_flush`/整帧重画。**非控件直绘内容**(`meow_ui_text`/`meow_ui_fill_rect` 等)**必须 `meow_draw_custom_register` 注册**(声明 bbox + 绘制回调 + z),否则区域重画清底后会漏画(hello demo 的分区标题/slider 值文本就是典型注册示例)。滚动列表的视口裁剪由 SDK 全局负责(`W_LIST` 只画视口内行),别在 App 侧再做整屏。
|
||||
3. **内存预算**。设备内存很紧(QEMU `mem=64M` 参考,rootfs ≤45MB)。避免大块分配、避免大字库双驻留(大字库按档装载、卸载旧 atlas)、别把卡上大文件 mmap 当运行必需——**运行期别依赖卡上资源**(见 §9 卡移除)。
|
||||
4. **板载存储别乱写**。板载 `/usr/data` 只剩 ~10MB,是稳定框架 + KB 级设置。**App 数据一律写 `MeowData/<pkg>/`(TF,经 `MEOW_DATA_DIR`/`MEOW_CACHE_DIR` env)**;绝**不碰 NAND 分区 / mtd / `/data/recovery_all`**(恢复出厂,红线)。
|
||||
5. **系统动作经专项服务**。App/组件不直连 `sys_do_reboot`、不写 NAND/mtd5;关机/重启/升级/回 HiBy 一律经 `meow_svc_power`(IPC)。App 一般无需做,但别绕过。
|
||||
6. **风控式网络请求节制**。避免高频轮询/无限重试/后台自灌;尊重断网/弱网(优雅降级而非死循环/崩溃);与系统 netmgrd 管控共存(域名级,非内核 per-app)。
|
||||
7. **别 destroy 别占用播放器/声卡**。不要 `killall hiby_player`(触发看门狗整机重启);不要 fork 长驻后台进程(退出要「真正退出」,否则 SIGCONT 后仍占屏抢输入)。
|
||||
8. **`app.json` 别缺失 `version_code`**(拒装);别用 `os.meow.*` 前缀(除非你是系统签名 App)。
|
||||
|
||||
---
|
||||
|
||||
## 9. FAQ / 常见坑(真踩过的)
|
||||
|
||||
1. **脏区透传:局部刷新抹掉上层 chrome**。症状:拖动/点某个控件后,上层 FAB/标题栏被抹掉。根因:App 侧旧的「清底 + 只画单个控件」刷新。**修法**:改走 SDK `meow_ui_invalidate`(z 栈重画下层+上层),非控件直绘**注册 CUSTOM**(`meow_draw_custom_register`),否则清底后漏画。`meow_ui_paint_full()` 整帧按 z 重画兜底。
|
||||
2. **IME 阻塞不喂 on_tick → 进度条/息屏卡死**。阻塞 `meow_ui_text_input`/`meow_textfield_activate` 的内层轮询必须喂 `meow_ui_activity_ping`(SDK 已做);键盘区用背景快照/恢复防残影;回显条是实时输入回显非候选。
|
||||
3. **字体声明丢失 → 降级小字库**。`app.json "fonts":"full"` 丢了 / 大写不符 / 该档没生成 → 按 `common` 装载,生僻字缺字(`meow_font_has_glyph` 预判,回退或提示)。字体三件套(`font.png`+`font.idx`+`font.json`)缺一件 → 该套不进列表。
|
||||
4. **热更新 `.new` + `mv` / 覆盖运行中程序的坑**。程序主体在卡上、运行中被覆盖/替换 → 访问旧代码页可能 **SIGBUS**。**修法**:升级用 `nyai install`(同包名 = 更新:旧目录备份 `.bak_<pid>` → 重启后替换 → 数据保留 → 成功自清 `.bak`);别自己 rm/mv 运行中的可执行文件。导出时 `*.pre_*`(热更备份)、`*.bak_*` 会被过滤。
|
||||
5. **开机早于卡挂载 → amsd 注册表扫空**。`am start/list` 报 `not registered`(agent-250)。**修法**:`am rescan` 手动触发,或等 3s 快重试 / 30s 合并重扫自愈。
|
||||
6. **`SIGUSR1` 误当返回键**。M2.89 起看门狗只对「**裸沉浸**」App(非 `framework=1`)发 `SIGUSR1`;SDK 框架 App 收到会被 SDK 的 `sigusr1_guard` 护栏记日志+吞掉。**别**为「返回」装 `SIGUSR1` handler;SDK 框架 App 返回走 `on_swipe`。
|
||||
7. **goodix 幽灵触摸**。真机启动触摸尾部会发一段幽灵触摸(启动后 1.2–2.25s,见 `APPS.md §3.5`)。框架 App 走 `on_tap`/`on_swipe` 语义事件,launcher/SDK 已做 drain + 冷却;裸自绘 App 要自己加启动冷却(如 `COOLDOWN_MS`),否则会把幽灵当下按立即退出。
|
||||
8. **音频:audiod 不在 / 被抢占**。`meow_audio_open` 返回负值(`MEOW_ERR_NOENT`)→ audiod 不在,**别隐式直开**(`MEOW_AUDIO_DIRECT=1` 仅调试)。被抢占后 `write` 返回负值 → 重新 `open`。长流卡死(~12s 停摆)是早期驱动/ring 问题,已由 audiod 加固(`period=1024×4` + 长流护栏)修掉;App 侧 `write` 阻塞背压,别一 flood 灌。
|
||||
9. **物理键映射 exit**。旧 hello 曾 `on_key(KEY_POWER)→request_exit()`,违反红线——已要求删除。规范:电力键=息屏/亮屏,播放键=媒体控制,返回=左缘左滑。你的 App 别重蹈。
|
||||
10. **`meow_ui.a` ABI 变化**。改 SDK(`meow_ui_theme`/`meow_widget` 结构)即 ABI 变化 → **必须同步重编 App**(`MEOWOS_SPEC_REF §3.4`)。SDK 编译期依赖 launcher 头(`-I../meow_launcherd` 引共享 `meow_ipc.h`)是**已知设计**,不是耦合错误——运行时只链 `meow_ui.a` 不会链入 launcher 符号。
|
||||
|
||||
---
|
||||
|
||||
## 10. 上架检查单
|
||||
|
||||
- [ ] `app.json`:`package`(自有前缀)/`exec`(静态路径)/`version`+**`version_code`(单调递增)**/`framework:1`;需大字库加 `fonts`;播放类加 `background_audio`;依赖外部二进制加 `depends`。
|
||||
- [ ] 二进制是**静态** MIPS32r2 ELF;链 `meow_ui.a` + `libz.a` + `-lpthread`;`-Wall -Wextra` 零警告。
|
||||
- [ ] 颜色用主题语义色不硬编码;明暗都能看。
|
||||
- [ ] 控件/区域刷新走 `meow_ui_invalidate`;直绘内容已 `meow_draw_custom_register`。
|
||||
- [ ] 数据写 `MeowData/<pkg>/`,不写板载/NAND;退出干净 `exit(0)`;根页左滑 `meow_ui_request_exit`。
|
||||
- [ ] `os.meow.*`(系统签名)vs 第三方(自有前缀,建议也签名防篡改)。打包 `tar czf`(或 `nyai export`,文件名茎=包名)→ `meow_sign sign` → 自测 `verify`/`meow_verify_host`。
|
||||
- [ ] 真机冒烟:`am start`/`am status`/日志 `MeowOS/logs/<pkg>/`,卡移除/无网/断流降级不崩。
|
||||
@@ -0,0 +1,36 @@
|
||||
# MeowOS 固件内置(板载)二进制/组件清单
|
||||
|
||||
> **当前基线:M6.35(0.8.35,commit `b23c6d5`,已上卡)**。**本文件由 `scripts/build/fw_content.conf` 生成/手工同步——改固件内容改 conf,别改本文件。**
|
||||
> 基线 = 官方 squashfs 解包 + 下表组件。打包入口 `scripts/build/build_fw_apps.sh`(见 scripts/build/README.md)。
|
||||
|
||||
| 固件内路径 | 仓库源 | 说明 |
|
||||
|---|---|---|
|
||||
| `/usr/bin/meow_bootmenu` | `patch_v2/bootmenu/meow_bootmenu` | 引导选单 M2.85(sysui让位 agent-216) |
|
||||
| `/usr/bin/meow_launcherd` | `patch_v2/meow_launcherd/meow_launcherd` | 主屏 launcher M6.35(批①后台常驻+宽限期 `f3ae06d/45c6338` + 批③后台管理页按钮化 `211c5a5/5c17422` + i18n settings/lock `c69fb92` + 310 三期锁屏/播放页双页歌词/取色;md5 `9515c9d8`) |
|
||||
| `/usr/bin/meow_watchdog` | `patch_v2/meow_watchdog/meow_watchdog` | 应用看门狗 M2.97(音量反转 + DS1039 平方锥度 + 插孔消抖) |
|
||||
| `/usr/bin/meow_svcd` | `patch_v2/meow_svcd/meow_svcd` | 服务管理器 M2.77(+崩溃上报 meow_crash agent-187) |
|
||||
| `/usr/bin/meow_mdns` | `mips-repo/bins/meow_mdns/bin/meow_mdns` | mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194) |
|
||||
| `/usr/bin/meow_sysui` | `patch_v2/meow_sysui/meow_sysui` | 系统 UI(sysui) |
|
||||
| `/usr/bin/nyai` | `patch_v2/meowpkg/nyai` | nyai M2.97(M2.89 导出 exec 相对化 + 271 app.json depends 安装提示 agent-240/271) |
|
||||
| `/usr/bin/meow_svc_power` | `patch_v2/meow_svc_power/meow_svc_power` | 电源专项服务(充电态权威源 mp2731-charger/status,修 battery/status 撒谎恒 Discharging) |
|
||||
| `/usr/bin/meow_verify` | `patch_v2/meow_verify/meow_verify` | II期验签 2B(多公钥+v2签名块 agent-221) |
|
||||
| `/usr/bin/meow_crash` | `patch_v2/meow_crash/meow_crash` | 系统级崩溃捕获 (agent-187) |
|
||||
| `/usr/bin/meow_svc_perf` | `patch_v2/meow_svc_perf/meow_svc_perf` | 性能专项服务 M2.75(net_json 补 network 闭合括号,修 /api/system JSON 解析失败) |
|
||||
| `/usr/bin/meow_audiod` | `patch_v2/meow_audiod/meow_audiod` | 独占音频守护 M6.35(M6.0 全量+315 孤儿会话两段式回收+M6.34 A3/DSD:24/32bit 高清 byte-exact+DoP DSD 透传+音量收编 v2 双组写;改 glibc-dynamic 链 libasound;md5 `9f47c8fd`) |
|
||||
| `/usr/bin/meow_audio` | `patch_v2/meow_audiod/meow_audio` | audiod 用户侧 CLI M2.97(+monitor 实时电平表/文本电平条;status 补 rms/peak) |
|
||||
| `/usr/bin/meow_powermgr` | `patch_v2/meow_powermgr/meow_powermgr` | 功耗管理专项服务期2 M6.1(期1+history/top/events+mp2731 current_now 实测充电功率+WiFi 未关联档/BT rfkill 细化+系数路径 /data/meow/powermgr/;IPC 0xD0-D7) |
|
||||
| `/usr/bin/meow_power` | `patch_v2/meow_powermgr/meow_powermgr` | meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩) |
|
||||
| `/usr/bin/meow_netmgrd` | `patch_v2/meow_netmgrd/meow_netmgrd` | 网络管理服务期3 M6.1(期1 接口层+期2 连接普查/DNS 代理+期3 策略引擎防自锁/SOCK_DESTROY 实测 no-op 记档/面板白名单/WiFi save_config;IPC 0xE0-EB;agent-276) |
|
||||
| `/usr/bin/meow_net` | `patch_v2/meow_netmgrd/meow_netmgrd` | meow_netmgrd 的 CLI 形态(同二进制双形态,argv[0]=meow_net 即 CLI:status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help) |
|
||||
| `/etc/init.d/S70meow_sysui` | `patch_v2/meow_sysui/S70meow_sysui` | sysui 自启 M2.85(launcher.conf enabled=1+迁移) |
|
||||
| `/etc/init.d/S89meow_svcd` | `patch_v2/meow_svcd/S89meow_svcd` | svcd 自启 M6.2(+amsd.conf 热更 exec→/usr/bin 迁移) |
|
||||
| `/usr/bin/meow_amsd` | `patch_v2/meow_amsd/meow_amsd` | 活动管理器 M6.35(注册表自愈 3s 快重试+30s 合并重扫+am rescan + busy 识别修复 + 显示名 UTF-8 整序列 name[49] + **批① FG/BG 上报 `EV BG/FG`/handle_bg/handle_fg**(md5 `57e19cde`,替代 M6.34 旧 `026cad52`;amsd FG/BG 供批③后台管理页/`am status` 数据源) |
|
||||
| `/usr/bin/am` | `patch_v2/meow_amsd/am` | amsd 用户侧CLI(+resolve/picker/rescan) |
|
||||
| `/etc/init.d/S90meow_bootmenu` | `patch_v2/bootmenu/S90meow_bootmenu` | bootmenu 自启 M2.85(注释同步) |
|
||||
| `/etc/init.d/S92_03_start_music_player` | `patch_v2/bootmenu/S92_03_start_music_player` | §4.3 兜底 patch(rcS 正常路径被 S90 阻塞不执行) |
|
||||
| `/etc/init.d/S95shutdown_logo` | `patch_v2/bootmenu/S95shutdown_logo` | rcK 关机时显示 /etc/shutdown_logo.jpeg |
|
||||
| `/etc/profile.d/meow_card_path.sh` | `patch_v2/meow_launcherd/meow_card_path.sh` | SSH 会话 PATH(M2.12 起):指向 TF 卡 MeowOS/opt/bin |
|
||||
|
||||
## 官方基线自带(非本表管理)
|
||||
|
||||
官方 rootfs 自带 busybox 全套、wpa_supplicant/wpa_cli、BlueZ(bluetoothd/bluealsa 等)、hiby_player、sys_server、thttpd、strace 等(详见网络/系统盘点记录)。本表只管 MeowOS 装入/替换的组件。
|
||||
+60
-30
@@ -8,11 +8,13 @@
|
||||
|
||||
### 1.1 板载(NAND rootfs / /data)
|
||||
- 设置持久化:**板载** `/usr/data/meow/settings.conf`(UBIFS,小文件 <10KB,跨换卡保留)`[SYSTEM_SETTINGS §2][MEOWOS_API §3]`
|
||||
- **板载服务目录**:`/usr/data/meow/services.d/<name>.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `mdnsd.conf`)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]`
|
||||
- **组件持久化目录(2026-09-01 用户定案)**:`/data` = `/usr/data` 的符号链接(UBIFS rw,**不随固件**);组件的可变动持久化文件(模型系数/策略/状态缓存等)一律放 **`/data/meow/<组件名>/`**(即 `/usr/data/meow/<组件名>/`),不再平铺在 `/usr/data/meow/` 根。例:`/data/meow/powermgr/power_model.conf`、`/data/meow/netmgrd/net_policy.conf`。存量平铺件(settings.conf、services.d/、sysui.d/、meow_boot_default)保持原位不动。`[WORK_STATE 2026-09-01]`
|
||||
- **板载服务目录**:`/usr/data/meow/services.d/<name>.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `meow_audiod.conf`/`meow_mdns.conf`(**M6.1 服务名统一 meow_\***:旧 `audiod.conf`/`mdnsd.conf` 自动迁移(stop+rm+预置),cd4e2c2 用户定案)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]`
|
||||
- **主机名**:设置键 `system.hostname`(SET_STR,默认 `meowos`;RFC1035 首标签),`S89meow_svcd` 开机读 settings `sethostname`;供自研 mDNS 应答器公布 `<host>.local` `[SYSTEM_SETTINGS §1][mips-repo/bins/meow_mdns]`
|
||||
- `/etc/meowos_version`:版本唯一来源(launcher 运行时 `load_meowos_version` 读;`MEOWOS_VERSION` 宏仅作 dev 占位兜底)`[DEV_NOTES #4]`
|
||||
- **`/data/meow_boot_default`:开机默认权威标记(O_TRUNC 单值:`0`=MeowOS / `1`=HiByOS)**。由 launcher `meow_settings_save()` 在保存 `boot.default` 时同步写;bootmenu `set_boot_default_meow()`(写 0)/ launcher `set_boot_default_hiby()`(写 1)同文件同语义;bootmenu 开机读它定默认;init 读入不触发、不反向覆盖;`meow.conf` 不再承担 boot_default `[M2.61 同步修复][README][meow_settings.c][meow_bootmenu.c]`
|
||||
- `/tmp`:tmpfs,开机清空;开机模式标记 `/tmp/meow_boot_mode`、电源菜单 `/tmp/meow_power_menu{,_active}`、app 沉浸 `/tmp/meowapp_state` `[BOOT_SELECTOR §1][KEY_CONTROL §9][APPS §3.5]`
|
||||
- `/tmp`:tmpfs,开机清空;开机模式标记 `/tmp/meow_boot_mode`、电源菜单 `/tmp/meow_power_menu{,_active}`、app 沉浸 `/tmp/meowapp_state`、**锁屏** `/tmp/meow_pin_unlock_once`(一次性解锁恢复口,launcher 锁屏页消费放行一次、重启自消)/`/tmp/meow_pin_locked`(电源菜单「主动锁」标志,重启消失)`[BOOT_SELECTOR §1][KEY_CONTROL §9][APPS §3.5][WORK_STATE 2026-09-02]`
|
||||
- **锁屏 PIN(M6.2 起,与面板同源 `system.pin`,规范 `SYSTEM_SETTINGS` 锁屏小节)**:设置键 `system.pin`=`<salt_hex>:<sha256_hex(salt||pin)>`(sha256+salt,格式与面板 `panel.pin` 全等,**绝无明文**;空=未设=锁屏关;`panel.pin` 已设时锁屏接受并首次验证成功后并入本键)+`system.pin_lock_on_wake`(息屏唤醒后锁屏,int 0/1,默认 1=开;开机不锁)+`system.pin_lock_until`(错 PIN 锁定的 epoch 截止戳,持久化防 launcher 重启清零)+`system.pin_fails`(连续错 PIN 计数 0-4,达 5 触发锁定并清零,持久化)。清/改:设置「安全→锁屏 PIN」页——设/改需旧 PIN 一次、**清除免旧 PIN**(物理持机即授权,仅二次确认)、`system.pin_lock_on_wake` 开关;**兜底**:SSH root 删 `system.pin` 键即解锁(无其它后门)。**锁屏模态演进**:M6.4 §310 二期 **EVIOCGRAB 独占触控**(锁屏层独占 grab,下层 App 完全收不到锁屏触摸)+主界面应用名截断;M6.6 §310 三期——**App 内手动息屏也触发锁屏** + **锁屏脏区全屏不透明底** + **300ms 重铺**;M6.7 锁屏页自睡(空闲超时自动灭屏);M6.8 310 四轮锁屏自睡 min(系统息屏,30s);M6.9 批A **锁屏期音量/播放/上下曲键转发**(EVIOCGRAB 独占真模态下下层收不到,锁屏本层显式边沿处理:音量 `settings_volume_adj`+`vol_ramp_tick`、播放/上下曲 `meow_media_playctl`)。`[SYSTEM_SETTINGS §1][WORK_STATE 2026-09-02]`
|
||||
- 日志启动缓冲 `/tmp/meow_boot.log`(256KB 滚动,卡出现后自动迁卡)`[APPS §4.5]`
|
||||
- IPC socket:`/usr/data/meow_ipc.sock`(AF_UNIX SOCK_STREAM,0666,backlog 4)`[FRAMEWORK_DESIGN §1][WORK_STATE]`
|
||||
- 专项服务 IPC socket:`/usr/data/meow_perf.sock`(meow_svc_perf,收 `MSG_PERF_QUERY→RESULT`)与 `/usr/data/meow_power.sock`(meow_svc_power,收 `MSG_POWER_ACTION→STATUS`),均 0666、可 `--sock`/`MEOW_IPC_{PERF,POWER}_SOCK` 覆盖;power 服务端以 `SO_PEERCRED uid==0` 基础鉴权 `[meow_svc_perf.c][meow_svc_power.c][meow_ipc.h]`
|
||||
@@ -57,8 +59,9 @@
|
||||
|
||||
### 2.2 meow_ipc 消息协议(AF_UNIX,全小端 LE)
|
||||
- 帧头 16B:`magic=0x4D4F5701`('MEOW'|1)/`version=1`/`type`/`req_id`/`len`;`len` 上限 **MEOW_IPC_MAX=4096**;列表编码 `[u16 count]`+NUL 串 `[FRAMEWORK_DESIGN §1]`
|
||||
- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑ / SERVICE_OK 0x0011↓ / SERVICE_STOP 0x0012↑ / MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / NOTIFY_PROGRESS 0x0024↑ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / ACT_FOCUS 0x0033↑ / ACT_BACK 0x0034↑ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / PERF_QUERY 0x0050↑ / PERF_RESULT 0x0051↓ / POWER_ACTION 0x0060↑ / POWER_STATUS 0x0061↓ / SYS 0x00F0↓` `[FRAMEWORK_DESIGN §1][meow_ipc.h]`
|
||||
- 媒体:`MSG_META`(元数据)、`MSG_PLAYCTL`(cmd 1=播放2=暂停3=上一首4=下一首5=seek)、`MSG_PROGRESS` `[附录 B]`
|
||||
- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑(M4 已落地:无头后台服务注册,m_register_t,保持连接)/ SERVICE_OK 0x0011↓(M4:m_ack_t status=服务 pid)/ SERVICE_STOP 0x0012↑(M4:SIGTERM 不重启)/ MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / NOTIFY_PROGRESS 0x0024↑ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / ACT_FOCUS 0x0033↑ / ACT_BACK 0x0034↑ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / PERF_QUERY 0x0050↑ / PERF_RESULT 0x0051↓ / POWER_ACTION 0x0060↑ / POWER_STATUS 0x0061↓ / SYS 0x00F0↓(M4:m_sys_t,SHUTDOWN=1/SVC_DEAD=2 熔断广播)` `[FRAMEWORK_DESIGN §1][meow_ipc.h]`
|
||||
- 媒体:`MSG_META`(元数据)、`MSG_PLAYCTL`(cmd 1=播放2=暂停3=上一首4=下一首5=seek6=播放/暂停toggle——物理键164恒发6由源侧按当前态翻转,launcher 透传)、`MSG_PROGRESS` `[附录 B]`
|
||||
- 专项服务消息号(见 §4):**电源管理 `meow_powermgr`** 0x00D0 QUERY/0x00D1 RESULT/0x00D2 POLICY_GET/0x00D3 POLICY_SET(期1;SET=动作级),期2 0x00D4 HISTORY/0x00D5 HISTORY_RESULT/0x00D6 EVENTS/0x00D7 EVENTS_RESULT;**网络管理 `meow_netmgrd`** 0x00E0 STATUS_REQ/RESULT、0x00E2 IFACE_REQ/RESULT、0x00E4 WIFI_CTL(→0xE5 RESULT)、0x00E6 BT_CTL(→0xE7 RESULT)、0x00E8 CONNS_REQ/RESULT、0x00EA POLICY_GET/SET、0x00EC DNS_CTL(0x00E0-0x00EB 已用,预留 0xEE 事件订阅)。**读级任意调用者、写级(CTL/POLICY SET)仅系统身份**(沿用 svc_power SO_PEERCRED 鉴权模式)`[meow_ipc.h]`
|
||||
- IPC 线程**只读帧+解析+入队**;状态变更主循环单写者(eventfd 唤醒,10ms 热键轮询非阻塞排空)`[FRAMEWORK_DESIGN §0][§6]`
|
||||
|
||||
### 2.3 watchdog / supervisor 信号
|
||||
@@ -120,23 +123,38 @@
|
||||
> 定位:把 §5(最小控件集)与设计模板(§5.2-§5.7)收敛到**统一控件架构**;实现分两批(§5.8.8)。
|
||||
- **第一批 = 通用基座(M2.70/0.7.65)**:`meow_widget` 基座统一标准控件/自定义画布;`meow_widget_opts`(Modifier 式 Options,`meow_widget_set_opts`/`meow_widget_opts_get`,0/-1 用默认);统一状态机 `MEOW_WS_ENABLED/SELECTED/FOCUSED/PRESSED`(`meow_widget_set_state`/`meow_widget_state`/`meow_widget_enabled`/`selected`);`meow_widget_custom_create`(自定义画布,App 给 render/hit 回调,`W_CUSTOM`);布局 `meow_row/column/box`+`meow_layout_add/arrange`+`spacer`;MD3 语义色板(§5.8.4)+ 主题接口 `meow_ui_set_style`(`MEOW_STYLE_MD3/MIUIX`)`/meow_theme_get`。
|
||||
- **第二批 = Material 组件子集(M2.71/0.7.66)**:`meow_btn_create_variant`(五型)/`meow_textfield_create`(IME)/`meow_card_create`/`meow_divider_create`/`meow_badge_create`/`meow_checkbox_create`/`meow_radio_create`/`meow_progress_create`/`meow_progress_circular_create`/`meow_fab_create`/`meow_topappbar_create`/`meow_navigationbar_create`/`meow_tabbar_create`/`meow_lazycolumn_create`/`meow_lazyrow_create`/`meow_dialog_create`/`meow_sheet_create`。
|
||||
- **M2.73 控件库修复(`meow_ui.a` md5 `e8341975`,`[meow_ui_widget.c][meow_ui_ime.c]`)**:① `W_LIST`(含 LazyColumn/LazyRow,`meow_widget_draw` case W_LIST)**视口裁剪**——按 `vx0/vx1/vy0/vy1` 只绘制落在视口内的行/分隔线/文本,杜绝「行滚出视口仍画/标脏上屏」的标题栏/底部脏区(SDK 全局,非 App 侧);② **z 序**:App 侧 `hello_app/hello.c` 的 `d_paint_region` 局部刷新改按 z 栈重画(内容→标题→slider 值→chrome),根治「层动态控件实时重绘盖住/抹掉上层 FAB」;**SDK 级全局 z 序方案在评估,暂未落定**(未正式纳入 SDK 公共 API 前不要写成正式规范);③ 渲染残影/脏区:局部刷新不再「清底+只画单个控件」(改 z 栈重画),消除重叠区残留;④ **IME 行为**(`meow_ui_ime.c`):键盘区背景快照/恢复(`ime_capture`/`ime_restore`,无残影)、配色走主题(不再硬编码黑底白键)、shift 大小写、阻塞 `meow_ui_text_input` 在打字循环喂 `meow_ui_activity_ping`(重置息屏计时,进度条不卡死)、OK(回车)=提交并关闭 / ESC(X)/背景点=取消;回显条=**实时输入回显**(`"%s_"` 拼接,非候选/联想),**候选框留中文输入法再做**。
|
||||
- **M2.73 控件库修复(`meow_ui.a` md5 `e8341975`,`[meow_ui_widget.c][meow_ui_ime.c]`)**:① `W_LIST`(含 LazyColumn/LazyRow,`meow_widget_draw` case W_LIST)**视口裁剪**——按 `vx0/vx1/vy0/vy1` 只绘制落在视口内的行/分隔线/文本,杜绝「行滚出视口仍画/标脏上屏」的标题栏/底部脏区(SDK 全局,非 App 侧);② **z 序**:App 侧 `hello_app/hello.c` 的 `d_paint_region` 局部刷新改按 z 栈重画(内容→标题→slider 值→chrome),根治「层动态控件实时重绘盖住/抹掉上层 FAB」;**SDK 级全局 z 序已落地并并入视图框架**(`meow_ui_view.c` 广播注册表 z 序:自动注册 + 子>父 + `set_z(999)` 置顶 + 收敛 live_btn,agent-176 · M2.76),hello App 层 `d_paint_region` 临时方案退役(agent-195 绘制期通用 clip `g_clip_stack[16]` 根除越界脏区);③ 渲染残影/脏区:局部刷新不再「清底+只画单个控件」(改 z 栈重画),消除重叠区残留;④ **IME 行为**(`meow_ui_ime.c`):键盘区背景快照/恢复(`ime_capture`/`ime_restore`,无残影)、配色走主题(不再硬编码黑底白键)、shift 大小写、阻塞 `meow_ui_text_input` 在打字循环喂 `meow_ui_activity_ping`(重置息屏计时,进度条不卡死)、OK(回车)=提交并关闭 / ESC(X)/背景点=取消;回显条=**实时输入回显**(`"%s_"` 拼接,非候选/联想),**候选框留中文输入法再做**。
|
||||
- **兼容**:既有 `meow_btn/slider/switch/list_create` 签名一字不改(Options/状态机走新增挂载器);`meow_ui_theme`/`meow_widget` 结构变化 = **`meow_ui.a` ABI 变化** → 需同步重编 hello(§5.8.8)。
|
||||
- **主题增强(§8.1)**:主题 = **顶级风格族 `ui.style`(md3/miuix)× 明暗 `display.theme`(dark/light)** 共同决定「整套配色+控件形态+背景底」;`display.background` 为「style×mode」派生的**可选背景底选项集**(非独立维度);状态栏图标随 `ui.style` 换集。⚠️ **`miui`/`miuix` 命名待定案(保留,勿拍板)**:`MEOW_APP_SDK §8.1` 用 `miuix`;`SYSTEM_SETTINGS §1` 注明 `meow_settings.c g_defs` 现值 `"md3,miui"`(label "Material You,MIUI"),实现时把 `g_defs` 的 `miui` 对齐为 `miuix`(**MIUIx 未实现,当前仅 md3 生效**)。
|
||||
|
||||
### 3.5 SDK 扩展模块速查(构造/滚动/动画/i18n/图标库/矢量字/事件分发/ListView 虚拟化/图片降采样)
|
||||
> 权威签名 = `patch_v2/sdk/include/meow_ui_{box,anim,scroll,str,input,widgets,image,ft}.h`;蓝本 `MEOW_APP_SDK.md` §5.5/§6.4/§10.1/§12/§13。**本批全部追加式 API**——既有 App/签名零改动,`meow_ui.a` 变化需重链四 App(hello/files/reader/流媒体客户端)。
|
||||
|
||||
- **§12.1 线性布局 `meow_box`(292,纯布局计算器非控件,opt-in `meow_view_set_layout`)**:`meow_box_new(dir)`(`MEOW_BOX_H/V`)/`_free`/`_set_rect`/`_set_padding`/`_set_gap`/`_set_align(main_align,cross_align)`;子项 `meow_box_add(b,w,size,weight)`/`_add_ex`(WRAP 用 min/preferred+measure 回调)/`_add_sub`(嵌套);`size`=`MEOW_BOX_FIXED/WEIGHT/WRAP`;`align`=`START/CENTER/END/SPACE_BETWEEN/AROUND/EVENLY`、`cross`=`START/CENTER/END/STRETCH`;求解 `meow_box_arrange(b)` 把子控件 x/y/w/h 写进 `meow_widget`(`meow_widget_set_rect`)。定长 items(≤`MEOW_BOX_MAX_CHILD=24`)+嵌套深度 ≤16 静态栈,arrange 零 malloc。
|
||||
- **§12.2 动画基座 `meow_anim`(292)**:`meow_anim_start(const meow_anim_desc*)`(desc:`from/to/duration_ms/ease/update/done/ctx`;start 即 value=from+回调一次 update(0))/`_cancel`(回调 done(canceled=1))/`_active`/`_value`/`_set_region(x,y,w,h)`(动画每 tick 只 flush 影响区,不整屏);`meow_ease`=`LINEAR/OUT/IN_OUT/OVERSHOOT`+`meow_ease_apply(e,t)`;节拍 `meow_anim_tick()`(挂 on_tick / `meow_ui_feed_tick`,无动画零开销)+`meow_anim_count()`;静态池 ≤`MEOW_ANIM_MAX=16`。
|
||||
- **§12.3 滚动物理 `meow_scroll`(292)**:`meow_scroll_create(set,max,get,redraw,ctx)`/`meow_scroll_wrap(list,is_listview)`(is_listview:1=W_LISTVIEW /0=W_LIST|LazyColumn|LazyRow,redraw 只 flush 控件视口区)/`_free`/`_track(dy,t_ms)`(跟手+速度采样,内部 set_scroll clamp)+`_fling()`(加权初速+ease-out 推进+边界回弹)/`_stop`/`_velocity`/`_active`/`_offset`/`_set_snap(step)`/`_set_friction(tau_ms,默认180)`。拖动态保持现状不变,fling 由 §12.2 驱动。
|
||||
- **§13 字符串资源 / i18n `meow_str_*`(299)**:`meow_str(id)` 按当前 locale 取串(返回静态、**永不 NULL**、未知 id 原样返回 id)/`meow_strf(out,outsz,id,...)`(snprintf 语义)/`meow_str_set_locale(locale)`(切 `"zh_cn"|"en_us"`,即时生效+重载 TF 覆盖表+best-effort 持久化 `ui.locale`)/`meow_str_locale()`。四层取值:TF 覆盖表(`<pkg>/strings/<locale>.txt`)> 内嵌当前语言 > 内嵌内置默认(zh_cn) > 原样 id。热路径零 malloc;id 形如 `域.名`(common.ok / player.pause)。不做复数/BiDi/RTL/动态字回退(当前为「按 locale 取整串」雏形)。
|
||||
- **§6.4 事件分发形式化(293,单一仲裁者)**:模态作用域 `meow_ui_modal_begin/end/active/depth`(作用域存在时下层收不到触摸,未命中面板的事件由背板消费恒返回哨兵);自上而下命中 `meow_ui_hit_topdown(x,y)`/`meow_ui_widget_hit`;手势捕获 `meow_ui_gesture_begin/owner/end`(down 命中即捕获到 up,move/up 只发捕获目标);唯一焦点 `meow_ui_focus_acquire/release/owner/tap/set_lost_cb`(点外释放+回调+滚动容器转移)。传播矩阵:模态>z 序自顶向下;命中即消费;向下穿透仅当未命中。
|
||||
- **§5.5 ListView 虚拟化(295,W_LISTVIEW 扩展层,API 追加式)**:`meow_listview_set_vsource(lv,count_fn,bind_fn,ctx)`(count 每帧实时查、bind 仅在行进视口时调一次;count_fn/bind_fn 均非 NULL 才切 pool 模式,否则回退 src2);`meow_lv_item{title,subtitle,icon,icon_kind,right,on,enabled,section,user}`;`meow_listview_set_item_renderer(lv,lines,show_icon,show_right)`/`_set_item_tier`(单行 48/双行 56)/`_set_item_draw`(完全自绘覆盖默认 renderer,state=`MEOW_LISTVIEW_ST_{SELECTED,PRESSED,DISABLED}`);固定池=视口+2 槽,行离开视口不销毁、同 index 命中同槽不重绑;段头 `item->section=1`。
|
||||
- **图标库 `sb_icons`(307,Material Symbols Rounded / MD3,单通道 alpha 32×32,生成 `tools/gen_action_icons.py`)**:索引只追加——状态栏 `MEOW_SB_ICON_WIFI=0..BATT_CHG=14` 原样保留;**动作** `MEOW_ICON_SETTINGS=15..MEOW_ICON_HEART=29`(设置/刷新/搜索/播放/暂停/上下曲/返回/关闭/添加/勾选/歌词/列表/下载/收藏),经 `meow_topappbar_set_item_icon(w,idx,MEOW_ICON_*)` 使 W_TOPAPPBAR 动作项画位图(`layout 见 meow_ui_widgets.h`)。
|
||||
- **图片降采样 `meow_img_downscale(img,maxdim)`(308)**:把解码图降到 ≤maxdim 边长以省内存(流媒体客户端封面 1448² RGBA 8.28MB→cap≤480px→0.78MB,-7.65MB),返回新 `meow_img*`。
|
||||
- **§10.1 矢量字 `meow_ft_*`(316,上收阅读器已验证的 FreeType 管线,`patch_v2/sdk/meow_ui_ft.c`)**:`meow_ft_new_face(path)`(`FT_New_Face` 按需读文件零整载 anon + glyph LRU 512(key=`(px<<32)|cp`,可配 256/1024)+ 灰阶 blend565);`meow_ft_draw_*`/`meas`/`free`(App **显式**调才用,`meow_ui_text` 位图路径零改动);加载失败/无卡自动降级 `meow_font`(绝不豆腐不崩)。与位图字库**并存**;FreeType 2.14.3 vendor 进 `patch_v2/sdk/vendor/`(gitignore)+ `build_sdk.sh` 裁剪构建(`-DFT_DISABLE_ZLIB/BZIP2/PNG/HARFBUZZ/BROTLI=TRUE`,静态 musl);**不调 `meow_ft_*` 的 App 零增量**(`meow_ui_ft.o`/freetype 链接器不拉取,无任何 FT 符号)。**SDK 演进主体收官**。
|
||||
- **M7.0a SDK 基座(326,`meow_ui.a 7fc83f77`,launcher 重写前置)**:**`meow_view` 事件接线**(`meow_view_dispatch_event` 接 `meow_ui_run` 主循环 + `on_enter/on_leave` 递归 children + 动态挂子修正);**`W_ICONBTN`**(圆形命中 + 按压态,`MEOW_ICON_*` 位图/自定义画布按钮,电源菜单/播放控制用);**主题色板导出/注入** `meow_theme_export/inject`(launcher `s_ui_theme` 薄壳读 SDK,为 M7.4 主界面/各页统一 `meow_theme` 权威铺路)。
|
||||
- **SDK 新增批次(lazygrid/select/swipe/cachegov/net,`meow_ui.a c3d6371b`)**:**§12.4 2 维虚拟化宫格 `meow_lazygrid`**(M7.4 主界面前置:count+cols+视口虚拟化,item 池有界 ≤`MEOW_LAZYGRID_POOL_MAX`=96,fling/snap 复用 §12.3 meow_scroll,`set_vsource/set_item_draw/set_empty_text`+`hit/tap_at`);**§5.8.9 枚举选择器 `meow_select`**(M7.2 设置页缺口 #4:BottomSheet+`meow_listview(SINGLE)` 单选,`create/add_item/set_on_change/open(阻塞返回 idx/-1)/free` + 一次性 `meow_select_show`,选项 ≤`MEOW_SELECT_MAX_ITEMS`=32);**§5.5 左滑删除 swipe-delete**(M7.3 通知中心缺口 #9:`meow_listview_set_swipe_delete(lv,enabled,on_delete,ctx)` + `swipe_begin/move/end/active/offset`,横滑删/纵向滚/点按 tap 三分);**§14.2 磁盘缓存治理 `meow_cachegov`**(OOM page-cache 治理 A 侧:L0 曲终整文件 DONTNEED / L1 丢已读前缀 / **L2 禁止**,10s 滚窗逐级回退,`cfg{enabled,keep_hot_bytes,min_hot_ahead,drop_mode,refault_backoff}` + `register/unregister/tick/drop_finished/set_cfg/note_refault/backoff_level/download_paused/prefetch_paused`);**§14.1 网络自愿通道 `meow_ui_net`**(netmgrd 期4:`meow_net_policy_check(host)` 0=allow/1=block + `meow_net_report(rx,tx)` best-effort)。权威签名 = `patch_v2/sdk/include/meow_{lazygrid,select,cachegov}.h` / `meow_ui_net.h`;蓝本 `MEOW_APP_SDK.md` §5.5/§5.8.9/§12.4/§14。
|
||||
|
||||
---
|
||||
|
||||
## 4. SDK 之外系统组件
|
||||
- **系统进程 supervisor 树(源:2026-08-30 定案;M2.71 已落地 `meow_sysui`)**:`meow_sysui`(`patch_v2/meow_sysui/meow_sysui.c`,~113KB;`etc/init.d/S70meow_sysui` 开机拉起,早于 S89)监管系统进程,**一个崩→只拉起它,绝不级联**;`meow_launcherd`=系统 UI 渲染宿主(状态栏/通知中心/电源菜单/全局手势,与 APP 进程独立);`meow_svcd`=服务管理器(服务全子进程化,svcd 自身不死);APP 进程只渲染自己内容,崩不连累系统 UI。**系统组件渲染归系统进程(launcher),APP 进程不渲染系统 UI**(SDK 系统组件 API 仅系统进程可链,App 不可自建悬浮层);通知中心 UI 渲染主体=系统进程,数据经 IPC 从通知存储读。现网(bootmenu execv launcher + S89 svcd 自启)→ 定案(sysui 统一监管)为渐进迁移,属系统基建线。**sysui 当前默认只监护 `meow_svcd`**(`/usr/data/meow/sysui.d/<name>.conf`,目录式复用 services.d 语义;launcher 由 S90meow_bootmenu 的 execv 拥有,登记但 `enabled=0` 防双重拉起;mdns 是 svcd 服务子进程,不双重管理)。`[FRAMEWORK_DESIGN §10][MEOW_APP_SDK §6.1][NOTIFICATION_SYSTEM §9][patch_v2/meow_sysui/meow_sysui.md]`
|
||||
- **专项子服务(一期在跑 + M2.73 落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能/系统只读聚合)、`meow_svc_power`(关机/重启/升级/回 HiBy 系统动作)。launcher 电源菜单与 MeowPanel 均改经这些服务 IPC 触发(不再直连 `sys_do_reboot`/爬 `/proc`)。**IPC**:perf 走 `/usr/data/meow_perf.sock` `MSG_PERF_QUERY→RESULT`;power 走 `/usr/data/meow_power.sock` `MSG_POWER_ACTION→STATUS`(服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73**:perf 增 **network** 子集(SYSTEM 子集输出:真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`,单点权威源);power **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启;**只完整性不签名**,海贝官方固件无签名,期望 md5/hash 二期)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(`settings_get_int_trim`,弃 amixer dummy;`dac on/off` 因 cs43198 `pwr_gpio=-1` 无控制点→`MEOW_ERR_NODEV`)。新 md5:perf `fa073bf9`、power `f3e653d5`。**电源 CLI `meow_svc_power <cmd>`**(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot):`shutdown`/`reboot[--hiby]`(强制回 MeowOS)/`upgrade`(写 mtd5=`ota:kernel2`,含写保护)/`set-default-meow`/`set-default-hiby` + `status`/`battery`/`reboot-reason`/`upgrade-status`/`screen on|off|timeout<秒>`/`dac on|off|status`。**二期**:关键 IPC 只接受 **`os.meow.*`** 鉴权 + `.mpk` 打包签名验签(解包验签/导出保留签名)。`[SYSTEM_FRAMEWORK_SERVICES][APPS 附录 §3][meow_svc_perf.c][meow_svc_power.c]`
|
||||
- **系统进程 supervisor 树(源:2026-08-30 定案;M2.71 已落地 `meow_sysui`)**:`meow_sysui`(`patch_v2/meow_sysui/meow_sysui.c`,~113KB;`etc/init.d/S70meow_sysui` 开机拉起,早于 S89)监管系统进程,**一个崩→只拉起它,绝不级联**;`meow_launcherd`=系统 UI 渲染宿主(状态栏/通知中心/电源菜单/全局手势,与 APP 进程独立);`meow_svcd`=服务管理器(服务全子进程化,svcd 自身不死);APP 进程只渲染自己内容,崩不连累系统 UI。**系统组件渲染归系统进程(launcher),APP 进程不渲染系统 UI**(SDK 系统组件 API 仅系统进程可链,App 不可自建悬浮层);通知中心 UI 渲染主体=系统进程,数据经 IPC 从通知存储读。现网(bootmenu execv launcher + S89 svcd 自启)→ 定案(sysui 统一监管)为渐进迁移,属系统基建线。**已迁移(M2.83)**:sysui 默认监护 `meow_svcd` + `meow_launcherd`(`/usr/data/meow/sysui.d/<name>.conf`,目录式复用 services.d 语义;`sysui.d/launcher.conf` `enabled=1`(restart=always/backoff=3000/max_restart=5),bootmenu 在 MeowOS 模式让位——sysui 在则不 exec launcher;mdns 是 svcd 服务子进程,不双重管理)。`[FRAMEWORK_DESIGN §10][MEOW_APP_SDK §6.1][NOTIFICATION_SYSTEM §9][patch_v2/meow_sysui/meow_sysui.md]`
|
||||
- **专项子服务(一期 M2.73 已落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能/系统只读聚合)、`meow_svc_power`(关机/重启/升级/回 HiBy 系统动作)。launcher 电源菜单与 MeowPanel 均改经这些服务 IPC 触发(不再直连 `sys_do_reboot`/爬 `/proc`)。**IPC**:perf 走 `/usr/data/meow_perf.sock` `MSG_PERF_QUERY→RESULT`;power 走 `/usr/data/meow_power.sock` `MSG_POWER_ACTION→STATUS`(服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73**:perf 增 **network** 子集(SYSTEM 子集输出:真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`,单点权威源);power **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启;**只完整性不签名**,海贝官方固件无签名,期望 md5/hash 二期)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(`settings_get_int_trim`,弃 amixer dummy;`dac on/off` 因 cs43198 `pwr_gpio=-1` 无控制点→`MEOW_ERR_NODEV`)。新 md5:perf `7ea196de`(M2.75 补 network 闭合括号)、power `306df962`(2B 面板精确绑定)。**电源 CLI `meow_svc_power <cmd>`**(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot):`shutdown`/`reboot[--hiby]`(强制回 MeowOS)/`upgrade`(写 mtd5=`ota:kernel2`,含写保护)/`set-default-meow`/`set-default-hiby` + `status`/`battery`/`reboot-reason`/`upgrade-status`/`screen on|off|timeout<秒>`/`dac on|off|status`。**二期签名鉴权已落地**(agent-185 2A / agent-221 2B):关键 IPC 只接受 **`os.meow.*`** 签权 + `.mpk` 打包签名验签(解包验签/导出保留签名;多公钥数组 + v2 签名块);读级命令鉴权已放宽到任意调用者(agent-189)。**M6.10 log 模块升级(337)**:perf `65f2059f` 挂「**卡死快照+perf 基线**」(主循环 `accept`→`poll` 化 + `snap_monitor_tick`:loadavg 超 `hang_load` 阈值落 `hang_*.snap`(全量 CPU/内存/进程/socket 现场)+`perf_*.log`(监控日志,60s/次、1MB 上限、每 boot ≤4 张、采集全走单文件 /proc 读);新增 `snap`/`snap-check` 子命令)+ crash `f02c4f45`(`log_cleanup_old` 前缀扩 `perf_*`/`hang_*` 入 logs/ 1GB 总纪律)。`test_snap_monitor` 18/18。`[SYSTEM_FRAMEWORK_SERVICES][APPS 附录 §3][meow_svc_perf.c][meow_svc_power.c]`
|
||||
- 通知(SDK `meow_notify_*`,M2.53 最小版 `[meow_ui.h §9.1]`):
|
||||
- 常量:`MEOW_NOTIFY_MAX_TYPES=4`/`MEOW_NOTIFY_MAX_PER_TYPE=4`/`MEOW_NOTIFY_MAX_MEDIA=1`/`MEOW_NOTIFY_TIMEOUT_ONGOING=-1`/`MEOW_NOTIFY_TIMEOUT_NEVER=0`
|
||||
- 分类:`CAT_GENERAL/MESSAGE/EMAIL/EVENT/PROGRESS/MEDIA(=5)/COUNT`;紧急度 `URG_LOW/NORMAL/HIGH/CRITICAL`
|
||||
- `meow_notify_register_type(cat)`/`meow_notify_send(const meow_notify*)`(返回 id)/`meow_notify_cancel(id)`/`meow_notify_active_count`/`count_in_type`/`has_active`/`urgency_str`/`category_str`
|
||||
- **本阶段落点=SDK 内部活动表 + meow_log() 打日志**;真实投递通知中心 UI / D-Bus 是后续任务 `[meow_ui.h 注释]`
|
||||
- `meow_config_*`(app 配置读写,走 meow_settings 后端)——**SDK 缺失清单第 3,未定** `[WORK_STATE]`
|
||||
- `meow_media_*`(媒体会话)——**依赖阶段 D 落地,未定** `[WORK_STATE]`
|
||||
- 崩溃捕获/看门狗——**规划中未定** `[WORK_STATE SDK 缺失清单 #5]`
|
||||
- **通知中心 UI 已落地**(launcher 渲染,M2.65+);**D-Bus 桥(A-14)未实现**(待后续);真实投递走 launcher IPC `[meow_ui.h 注释]`
|
||||
- `meow_config_*`(app 配置读写,走 meow_settings 后端)——**已落地**(agent-158 · `dcba597`,见 `devdocs/plan/SDK_CONFIG_API.md`)`[WORK_STATE]`
|
||||
- `meow_media_*`(媒体会话)——**已落地**(agent-204 M2.81 媒体会话 + agent-206 M2.82 SDK `meow_ui_media.c` 懒连重连封装)`[WORK_STATE]`
|
||||
- 崩溃捕获/看门狗——**已落地**:`meow_crash`(agent-187 · `cf86cbd` 系统级崩溃捕获 + sycd 上报)+ App 崩溃直报(agent-199)+ watchdog(应用看门狗,S89 预置)`[WORK_STATE]`
|
||||
- `meow_notify_dbus`(org.freedesktop.Notifications 桥)——**已定,待实现** `[FRAMEWORK_DESIGN 附录 C]`
|
||||
- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~101KB 进固件,不入卡)`[meow_svcd.md]`:
|
||||
- 类 systemd/systemctl。**已接入系统启动链(M2.54)**:固件 `etc/init.d/S89meow_svcd` 开机自启(`-f` 前台 + 重挂循环,失败不死;置于 S84ssh 之后、S90meow_bootmenu 之前——因 S90 exec 引导选单阻塞 rcS)。由 bootmenu/launcher `fork/exec` + `waitpid` 托管仍为备选。
|
||||
@@ -144,13 +162,18 @@
|
||||
- **板载-trigger=boot 语义**:板载服务恒定存在,boot 触发不依赖卡(拔卡保留);TF 服务 boot 仍需卡。
|
||||
- **服务目录规则**:**固件工具**→板载 `/usr/data/meow/services.d/<name>.conf`(Data 区,拔卡保留);**TF 工具/第三方**→TF `MeowOS/services.d/<name>.conf`(随卡存在/消失);**双源板载优先**(TF 同名被板载屏蔽 + WARN「<name> 板载与 TF 都存在,采用板载」)。
|
||||
- 崩溃隔离:服务=svcd **独立子进程**(`fork+execv`),svcd 只 `waitpid`/`SIGCHLD` 感知;服务死不影响 svcd/supervisor;重启受 `backoff_ms` 退避防热循环。
|
||||
- 状态机:`DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;稳定运行 30s 清零 restart_count。CLI(systemctl 式,SSH 直呼):`status/reload/start|stop|restart <name|all>/pid`,示例 `meow_svcd status`/`meow_svcd restart mdnsd`/`meow_svcd stop all`/`meow_svcd reload`;控制走 AF_UNIX 单行文本 `/tmp/meow_svcd.sock`;守护默认后台,`-f/--foreground` 前台供 supervisor 托管。
|
||||
- 状态机:`DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;稳定运行 30s 清零 restart_count。CLI(systemctl 式,SSH 直呼):`status/reload/start|stop|restart <name|all>/pid`,示例 `meow_svcd status`/`meow_svcd restart meow_mdns`/`meow_svcd stop all`/`meow_svcd reload`;控制走 AF_UNIX 单行文本 `/tmp/meow_svcd.sock`;守护默认后台,`-f/--foreground` 前台供 supervisor 托管。
|
||||
- **M2.73 悬垂指针修复(md5 `da6ebc66`)**:`scan_and_reconcile` 用 `*s=tmp` 整体拷贝后,`s->argv[]` 仍指向已销毁的 `tmp` 栈帧 → `execv`/`/proc/pid/cmdline` 错乱。修法:新增 `svc_rebuild_argv(s)`,赋值/`memmove` 左移后把 `s->argv[]` 重定位到 `s->exec`/`s->argbuf` 自身成员(删除一个服务后同 tick 内被拉起不再用错位 argv)。`[meow_svcd.c]`
|
||||
- **卡上外部工具 = `MeowOS/opt/`**(`{bin,nyai,sources.list}, lib/, tools/`):MeowOS 启动时从卡加载,拔卡降级不崩。**`MeowOpt` 已废弃并入此处**;mDNS 已改为自研 `meow_mdns`(固件 `/usr/bin`),不再放卡。`[DEV_NOTES][meow_svcd.md]`。**MeowPanel 本体 = `MeowOS/opt/MeowPanel/`**(后端+前端,svcd 托管 `MeowOS/services.d/meowpanel.conf`,无卡跳过、系统核心不依赖,不进固件;md5 `11ed76ec`,详见 `patch_v2/meowpanel/README.md`)。
|
||||
- **MeowPanel(外置 TF 卡 Web 面板,M2.73 已落地)**:单进程 HTTP/JSON 后端(8080)+ 静态托管(`/`→index.html、`/assets/*`)+ `meow_svc_perf`/`meow_svc_power` IPC 客户端(读侧透传 perf 聚合、动作侧经 power)+ `settings.conf` 设置镜像(`key = value` 带空格 + 精确匹配)+ 文件白名单(`MeowOS/Music/MeowData/MeowAPPs/book`,`..` 逃逸→403)+ 应用(`nyai install/export/remove`,包名=`app.json.package`)+ 按键注入(evdev,只音量/电源/播放/上下曲,**禁返回158/主页172/确认28/退出96**)+ 日志(`MeowOS/logs`)。M2.73:系统页「以太网」→「**蓝牙**」栏(无网口有蓝牙);**OTA 检查升级 bug 已修**(改读 `GET /api/system` 的 `ota`,不再误触 `POST /api/system/ota` 升级)。**⚠️ 无鉴权**(内网信任模型;`reboot/shutdown/ota/apps/删除/上传` 危险端点勿暴露公网,power 侧有 `SO_PEERCRED uid==0` 基础校验)。`[patch_v2/meowpanel/][devdocs/plan/MEOWPANEL_PROTOCOL.md]`
|
||||
- **mDNS 应答器 = `meow_mdns`(自研,已拍板替代完整 mdnsd)**:`mips-repo/bins/meow_mdns/`,**IPv4-only**(内核无 IPv6,完整 mDNSResponder/SAResponder 均 `socket(AF_INET6)` 失败),mipsel 静态 ~79KB(md5 b23de7ce)。**进固件 `/usr/bin/meow_mdns`**(不入卡),由 meow_svcd 托管(板载 `/usr/data/meow/services.d/mdnsd.conf`,`trigger=boot`、root 即可绑 5353/组播,`cap=` 空)。功能:join `224.0.0.251:5353`,应答 `<host>.local` A(wlan0 IPv4)/`_ssh._tcp` PTR/SRV/TXT/`_services._dns-sd` PTR;支持 `MEOW_MDNS_HOST`/`_ADDR`/`_PORT`/`_TTL`/`_CF`/`_QD`/`_MCAST`/`_MIF`/`_DBG`。⚠️ **手机热点隔离**:设备→主机组播被热点丢弃,avahi 只收组播应答 → 该热点下 `avahi-resolve <host>.local` 超时(网络层限制,非应答器缺陷),主机需 DNS 兜底 `[mips-repo/bins/meow_mdns/manifest.md][meow_svcd.md]`。
|
||||
- **卡上外部工具 = `MeowOS/opt/`**(`{bin,nyai,sources.list}, lib/, tools/`):MeowOS 启动时从卡加载,拔卡降级不崩。**`MeowOpt` 已废弃并入此处**;mDNS 已改为自研 `meow_mdns`(固件 `/usr/bin`),不再放卡。`[DEV_NOTES][meow_svcd.md]`。**MeowPanel 本体 = `MeowOS/opt/MeowPanel/`**(后端+前端,svcd 托管 `MeowOS/services.d/meowpanel.conf`,无卡跳过、系统核心不依赖,不进固件;md5 `8108852f`(nyai 路径修复)→ 阶段C 后 新 md5(见 `patch_v2/meowpanel/README.md`),详见 `patch_v2/meowpanel/README.md`)。
|
||||
- **MeowPanel(外置 TF 卡 Web 面板,M2.73 已落地,阶段 C 实时画面+遥控已落地)**:单进程 HTTP/JSON 后端(8080)+ 静态托管(`/`→index.html、`/assets/*`)+ `meow_svc_perf`/`meow_svc_power` IPC 客户端(读侧透传 perf 聚合、动作侧经 power)+ `settings.conf` 设置镜像(`key = value` 带空格 + 精确匹配)+ 文件白名单(`MeowOS/Music/MeowData/MeowAPPs/book`,`..` 逃逸→403)+ 应用(`nyai install/export/remove`,包名=`app.json.package`,`nyai` 固件内置 `/usr/bin/nyai` 优先+卡上回退)+ 按键注入(evdev,只音量/电源/播放/上下曲,**禁返回158/主页172/确认28/退出96**)+ 日志(`MeowOS/logs`)。M2.73:系统页「以太网」→「**蓝牙**」栏(无网口有蓝牙);**OTA 检查升级 bug 已修**(改读 `GET /api/system` 的 `ota`,不再误触 `POST /api/system/ota` 升级)。**M2.78+ 新增**:可选密码鉴权(agent-196,默认关)、通知中心后端 `/api/notifications`(TF 归档环形 200 条)、文件域 4 bug 修复 + 上传 8MB 上限 + 双重白名单(agent-182)**。**M2.84-88 阶段 C**(agent-224):`/api/screen.jpeg`(fb0 活动帧→自研 JPEG 编码器)+ `/api/input/tap`(坐标注入,本机触摸优先 5s 仲裁)+ 屏幕增量帧 `?delta=1`(8x8 块比对,近静态 27KB→804B,agent-234)+ `/api/notifications` 实时化(优先 IPC 读 launcher 活跃集 source:ipc,agent-234)。**252 新增(2026-09-01)**:`/api/audio`(audiod MSG_AUDIO_QUERY→STATE 只读透传,不在时 available:false)+ 前端「音频」页(5s 轮询只读)与「字体」页(列表/上传进度/导出/409 冲突确认 force 重试)。**⚠️ 鉴权默认关**(内网信任模型;`reboot/shutdown/ota/apps/删除/上传` 危险端点勿暴露公网,power 侧有 `SO_PEERCRED uid==0` 基础校验)。`[patch_v2/meowpanel/][devdocs/plan/MEOWPANEL_PROTOCOL.md]`
|
||||
- **面板批2 + 加固(2026-09-02)**:崩溃/日志中心 + 音频路由页 + 音量写经 clamp `vol_max` + 后端补 `reboot_reason`;**JSON 输出 UTF-8 加固**(`sA_str_n` 单遍校验、非法序列换 `?`、IPC 原始 JSON 门禁降级,防任一上游坏字节拖垮整页,d7bf62f)。**overview 自校验纪律**:`/api/overview` 服务端发出前先自查(jv_parse)+ 与浏览器口径对齐的**严格平衡检查**,坏则 dump `/tmp/overview_bad.json` + 500(908fc53/67aa5fc/54e737a);前端非 JSON 自动回传原始字节到 `/api/debug/report_bad_json`(落 `/tmp/overview_client_bad.txt`,7a40586/524e5d6);**教训**:后端 `jv_parse` **容忍尾随垃圾**,浏览器 `JSON.parse` **不容忍**——net 块多写一个 `}` 即「Extra data」破页(批2 DNS 并入事故),故自校验必须严格括号/闭合平衡。`[patch_v2/meowpanel/][devdocs/plan/MEOWPANEL_PROTOCOL.md][WORK_STATE 2026-09-02]`
|
||||
- **mDNS 应答器 = `meow_mdns`(自研,已拍板替代完整 mdnsd)**:`mips-repo/bins/meow_mdns/`,**IPv4-only**(内核无 IPv6,完整 mDNSResponder/SAResponder 均 `socket(AF_INET6)` 失败),mipsel 静态 ~79KB(md5 b23de7ce)。**进固件 `/usr/bin/meow_mdns`**(不入卡),由 meow_svcd 托管(板载 `/usr/data/meow/services.d/meow_mdns.conf`,`trigger=boot`、root 即可绑 5353/组播,`cap=` 空)。功能:join `224.0.0.251:5353`,应答 `<host>.local` A(wlan0 IPv4)/`_ssh._tcp` PTR/SRV/TXT/`_services._dns-sd` PTR;支持 `MEOW_MDNS_HOST`/`_ADDR`/`_PORT`/`_TTL`/`_CF`/`_QD`/`_MCAST`/`_MIF`/`_DBG`。⚠️ **手机热点隔离**:设备→主机组播被热点丢弃,avahi 只收组播应答 → 该热点下 `avahi-resolve <host>.local` 超时(网络层限制,非应答器缺陷),主机需 DNS 兜底 `[mips-repo/bins/meow_mdns/manifest.md][meow_svcd.md]`。
|
||||
- `meow_settings_*`(设置后端,已定)/`meowcli`/`meow_sys_devattr`/`meow_devattr_set` `[MEOWOS_API]`
|
||||
- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`;`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]`
|
||||
- **独占音频守护 `meow_audiod`(M2.90 已落地,规范 `devdocs/plan/AUDIOD_DESIGN.md`)**:板载 `/usr/bin/meow_audiod`,svcd 托管(板载 `services.d/meow_audiod.conf`,trigger=manual;launcher 确认 MeowOS 模式后拉起;**启动门控:mode≠meow → exit(0),HibyOS 下不存在**)。IPC `/usr/data/meow_audio.sock`(帧层复用 meow_ipc.h,号段 0x00C0-0x00CA:OPEN/OPEN_ACK/WRITE/PAUSE/RESUME/STOP/PREEMPTED/QUERY/STATE/ROUTE/VOLUME)。**独占+抢占**(后开赢,旧流收 PREEMPTED;socket EOF=回收;软混音已否决,留接口位)。**DAC 原生率跟随不做 SRC**;格式归一 S16 直通/S32 升位/mono 复制;切率 mute-重配-恢复。**meow_route 已并入为路由线程并退役独立服务**(⚠️ 退役服务要清板载+TF 双源 conf,否则旧 conf 会双写路由)。**CLI `meow_audio`**(root 免鉴权):`status`/`tone [freq] [ms] [rate]`(正弦测试音,排查无声不开 App)/`route <0|2|3>`/`vol [0-100]`。SDK 客户端 `meow_audio_open/write/pause/close/query/on_preempt`(`meow_ui_audio.c`,懒连接+EPIPE 标死;无 audiod → -ENOENT 不隐式直开,`MEOW_AUDIO_DIRECT=1` 仅调试)。**M2.91-97 加固批**:①**开机武装 hbc3000 FPGA**(M2.91:路由线程启动无条件写 Output Port Switch 4.4→3/3.5→2/无→4,同值先写 5 强制重烧——根治「未烧 FPGA 时 PCM 第二次 `hbc3000_start` 内核硬挂死」,调查底稿 `devdocs/plan/AUDIO_HANG_INVESTIGATION.md`)②**长流护栏**(M2.93:ring 有界等待/cond timedwait/popen 有界收割/O_NONBLOCK+poll 写/wedge 自救/心跳 watchdog)+**几何收窄 period=1024×4**(M2.94:驱动默认巨 buffer 致桥周期中断稀缺 wedge、播 ~12s 停摆;tonepcm2 矩阵 60s 零 stall 实证)③**音量反转**(M2.95:CS43198 音量寄存器=衰减量,四路统一 `dac=255-pct*255/100`)④**炸耳安全**(M2.96:`dac_vol_mute` 写 255 真静音,旧写 0=最大响度)⑤**电平监控**(261,待 M2.97 固件化:播放线程 ~100ms 窗口整数 RMS/peak dBFS(deci-dB,逐帧判边界,-990=-inf)、`MSG_AUDIO_STATE` 追加 `rms=/peak=/lvlseq=`(ABI 兼容)、流活跃时 1s 节拍写 TF `MeowOS/logs/audio_monitor_<bootts>.log`、CLI `meow_audio monitor [秒]` 文本电平条)⑥**孤儿会话两段式回收**(315,M6.5):客户端崩溃/被杀(sig=9)/正常退出后会话完整回收(死亡即清会话+排空后清统计脚),STATE 回干净无流基线。`[patch_v2/meow_audiod/]`
|
||||
- **功耗管理服务 `meow_powermgr`(期0 调研 + 期1 骨架 M6.0 已落地,期2 M6.1,规范 `devdocs/plan/POWER_MANAGER_DESIGN.md`)**:板载 `/usr/bin/meow_powermgr`,svcd 托管(板载 `services.d/meow_powermgr.conf`,`trigger=boot`;**HibyOS 模式只读降级**——只采样电池,绝不放策略/不碰 wlan0/bt/声卡)。双形态 CLI **`meow_power`**(同二进制 argv[0] 分流)。**期0 事实**(`devdocs/plan/POWER_HW_FACTS.md`):`battery/current_now` **不存在**→实时功率只能 `derived+estimated` 双轨、`cpufreq` **不存在**→省电档无降频点、**背光量程 0-101**(非 255!「亮度跳 100」=满量程、默认 101=满亮的根因)、`axp_battery` 是幻影设备、**mp2731 有 `current_now`**(充电功率可测)、rfkill×4 存在(期3 控制点)、无 thermal/hwmon。**期1**:权威充电态链复用(mp2731-charger/status)+ 经验模型 14 系数(`/data/meow/powermgr/power_model.conf` 可调)+ IPC 0x00D0-D3(QUERY/RESULT/POLICY_GET/POLICY_SET)+ CLI `status/estimate/model`。**期2(M6.1)**:`history`(**30s 环形、4h 不落盘**)+`top`(功耗分解排序,2s)+`events`(充放电/低电/亮灭屏/背光跳变事件流)+模型细化(**WiFi 未关联档**、**BT rfkill/ACL 档**)+ **mp2731 `current_now` 实测充电功率**(`charge_confidence=measured`;放电仍 `estimated`,诚实区分)。系数/状态路径 = **`/data/meow/powermgr/`**(组件持久化目录定案,SPEC_REF §1.1)。IPC 0x00D4-D7(期2 HISTORY/EVENTS)。`[patch_v2/meow_powermgr/]`
|
||||
- **网络管理服务 `meow_netmgrd`(期1 M6.0 已落地,期2/期3 M6.1,规范 `devdocs/plan/NETMGR_DESIGN.md`)**:板载 `/usr/bin/meow_netmgrd`,svcd 托管(板载 `services.d/meow_netmgrd.conf`,`trigger=boot`);双形态 CLI **`meow_net`**(`status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help`)。**HibyOS 模式只读降级**(接口状态可查,绝不下发控制),RSS 预算 ≤1MB。期0 内核事实(`devdocs/plan/NETWORK_HW_FACTS.md`):**无 iptables 用户态/无 cgroup/无 conntrack → 内核级每应用管控不可行**,主杠杆 = **DNS 代理(域名级)+ 连接普查(inet_diag)+ SDK 自愿通道**;**双行 resolv.conf 崩溃零断流**。**期1 = 接口统一层**(iface 枚举+热插拔/WiFi `wpa_cli` 有界收割/BT `hciconfig`+`bluetoothctl`/rfkill/IP 路由 DNS 只读/udhcpc 续租释放仅 MeowOS)。**期2 = 连接普查 + DNS 代理**:2s `/proc/net/{tcp,udp}` 普查(inode→pid→**三实体** `app:<包名>`(查 amsd)/`svc:<服务名>`(查 svcd)/`proc:<comm>` 归因)+ DNS 代理(127.0.0.1:53,双行 resolv.conf+atexit 恢复+5s 自检,真机实测 enabled)+ 事件日志(1min 去重)+ CLI `apps/conns/top/dns`。**期3(M6.1)= 策略引擎**:`/data/meow/netmgrd/net_policy.conf`(组件持久化目录定案,SPEC_REF §1.1)读改写单键+原子写;**防自锁双层**(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init **拒绝置 block** + 策略文件被手工改坏时的加载校验强制复位);**SOCK_DESTROY 真机实测 = no-op(内核 4.4 接受请求但不销毁 socket)→ 连接级管控不可用,仅 DNS 层生效**(`meow_net policy show` 头明示 `enforce=dns_only`);classify_exe 面板白名单 + apps 归因修复 + WiFi `save_config`。日志 `MeowOS/logs/net_<bootts>.log`(无卡降级 /tmp)。`[patch_v2/meow_netmgrd/]`
|
||||
- **音频硬件事实(sndprobe/tonepcm2 真机实测,2026-08-31)**:播放 PCM = `/dev/snd/pcmC0D0p`(`cs43198-hifi-0`);`pcmC0D1p`=snd-soc-dummy(勿用)。能力:S16_LE/S24_LE、1-2ch、8k-768k、buffer 2048-1048576 帧、period 512-262144。⚠️ **ALSA UAPI 两坑**(流媒体客户端无声根因,勿再踩):①hw_params 必须「全放行」初始化(mask 全 1、interval [0,UINT_MAX],params_any 语义)再收窄——memset 0 的 interval=「只能取 0」求交必空 → HW_PARAMS 恒 EINVAL;②access/format/subformat mask 必须**先清零再单点**——留全 1 会被内核 choose 成 MMAP_INTERLEAVED,ch>1 时 write 恒 EINVAL。**CS43198 音量寄存器=衰减量(官方数据手册 DS1039 实证,用户供)**:PCM_VOLUME_A/B(0x0B/0x0C),0=0dB 满幅最大声、255=-127.5dB≈静音(0.5dB/步;DSD 通道独立 DSD_VOLUME_A/B)。**混音器 L/R Playback Volume 读回恒 0 是死镜像**(官方播放器走 sa_hgl_dma 硬直通不经此控制,HibyOS 对照实测同读 0),勿据此判音量。**音量寄存器=衰减量(0=最响 255=静音,M2.95 真机实测方向)**:四路(launcher/audio.c+audiod+watchdog+SDK vol)统一 `dac=255-pct*255/100`;mute 必须写 255 真静音(M2.96 炸耳修复)。混音器清单:numid=1/2 L/R Vol、3 Mute Output、4 Digital Filter、5 Soft Mute、6 NOS_EN、7 DRE_EN、8 Balance Lineout En、9 Output Port Switch(0 空闲/1 lineout/2 3.5/3 4.4/4 SPDIF;0 危险勿写;**5 = audiod 内部「强制重烧 FPGA」约定值**(M2.91 开机武装同值改写路径),非常规路由值,App/面板勿写)、10 DOP_EN。`[patch_v2/tools/sndprobe.c][tonepcm2.c][APPS §3.7]`
|
||||
- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`(**已落地 M4**:app.json `background`/`background_audio` 字段 + MSG_REGISTER 校验链;播放中退出转后台保活见 FRAMEWORK_DESIGN §9);`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]`。**M6.34+ 追加后台常驻框架**:app.json `background_keepalive`(无限期保活)+ `meow_bg_should_keep` 三参判定 + 宽限期 `MEOW_BG_GRACE_MS`(30min)回收 + amsd `EV BG/FG` 上报(批③后台管理页数据源),设计见 `devdocs/plan/APP_BACKGROUND_FRAMEWORK.md` §7 / `APP_BACKGROUND_KEEPALIVE_P1.md`。
|
||||
|
||||
---
|
||||
|
||||
@@ -165,7 +188,7 @@
|
||||
- **全屏机制(M2.5x,`app.json fullscreen` 即 app 级 + `route.<id>.fullscreen` 即 activity 级)**:launcher 启动 app/切 activity 时据声明**预先设 SDK 全屏**(env `MEOW_FULLSCREEN`)。进入/退出全屏:**状态栏向上收出屏顶、只留一条提示线高度**(非全隐藏),带上收/恢复的**自然过渡动画**。**全屏下保留**:左缘返回手势 + 顶部动态伸缩提示线;顶部下滑仍可展开通知中心,但**顶部热区缩小**(不影响全屏 app)`[源:用户 2026-08-30]`(细化既有「全屏=隐藏状态栏/隐藏返回键」的表述,见 §7 A-13)
|
||||
- 沉浸:`app.json immersive:1` → launcher 写 `/tmp/meowapp_state=1` → 隐藏虚拟返回键,手势归 SDK `[APPS §3.5][GESTURE_BACK §2.4]`
|
||||
- **横向分页设计模板(M2.5x 预留,见 `MEOW_APP_SDK.md` §5.2)**:主页/子页多页横向滑动复用的设计模板(参照 HibyOS 播放器主页横划进播放页)。与左缘返回/顶部下滑**按起始位置分流**互斥——左缘 ≤热区→**左滑返回**(§5.1);中部左/右滑→**页切换**;顶部下滑→**通知中心**。实现要点:相邻页各预渲染一张快照、切换只 blit 快照 + 跟手平移/吸附(vsync ≤60 帧、零 UI 重算)。**注**:模板 API 为「草案/待定案」,当前未实现 `[源:2026-08-30]`
|
||||
- **SDK 可复用设计模板清单(M2.5x 预留,见 `MEOW_APP_SDK.md` §5.3)**:把反复出现的界面结构抽象为可复用设计模板——①**分组设置表单** ②**列表+选择** ③**弹窗/对话框** ④**顶/底下拉面板**(A+B 先行,已展开 §5.4-§5.7)+ 媒体播放/沉浸/加载(C)、卡片/导航/列表项(D,随 app/MD3 细化)、拖拽排序/长按菜单/时间轴(E,后置),共 11 项;横向分页另见 §5.2。**所有模板与既有手势约定一致**:左缘返回 + 顶部滑条 + 状态栏收合 + 通知中心热区(本 §5.1/§3.1/A-13)——模板只在内部做命中分流,**不重定义全局手势**;顶部/底部面板与左缘返回/通知中心按起始位置与触达途径分流互斥(快捷设置触达途径为**待定案**)。**注**:A①②/B③④ 模板 API 均为「草案/待定案」,当前未实现 `[源:2026-08-30]`
|
||||
- **SDK 可复用设计模板清单(M2.5x 预留,见 `MEOW_APP_SDK.md` §5.3)**:把反复出现的界面结构抽象为可复用设计模板——①**分组设置表单** ②**列表+选择** ③**弹窗/对话框** ④**顶/底下拉面板**(A+B 先行,已展开 §5.4-§5.7)+ 媒体播放/沉浸/加载(C)、卡片/导航/列表项(D,随 app/MD3 细化)、拖拽排序/长按菜单/时间轴(E,后置),共 11 项;横向分页另见 §5.2。**所有模板与既有手势约定一致**:左缘返回 + 顶部滑条 + 状态栏收合 + 通知中心热区(本 §5.1/§3.1/A-13)——模板只在内部做命中分流,**不重定义全局手势**;顶部/底部面板与左缘返回/通知中心按起始位置与触达途径分流互斥(快捷设置触达途径为**待定案**)。**注**:A①②/B③④ 模板 API 原为「草案/待定案」。**部分已落地**:B② 列表+选择 → 结构化列表控件 `W_LISTVIEW`(M2.76 agent-186);A① 分组设置表单、B④ 顶/下方面板 已在设置页/通知中心/电源菜单以 launcher 侧实作(非独立公开 SDK 模板 API);视图框架(`meow_ui_view.c` M2.76+)与控件库已覆盖其多数能力,其余(横向分页、C/D/E)仍为草案 `[源:2026-08-30]`
|
||||
|
||||
### 5.2 主题
|
||||
- **日/夜/自动**:SDK `meow_theme` 内置 `meow_theme_light/dark`;模式键 `display.theme_mode`(SYSTEM_SETTINGS 记为 `manual`/`auto`——**与 SDK 枚举 light/dark/auto 冲突**,见 §A);AUTO 判时段 `[SYSTEM_SETTINGS §1][MEOW_APP_SDK §8]`
|
||||
@@ -178,7 +201,7 @@
|
||||
- **默认字库目录**:默认字体统一放 **`<卡根>/MeowOS/fonts/default/regular/`**(`font.png`+`font.idx`,可选 `font.json`),不再用 `MeowAPPs/fonts/font.png`;设备旧 `MeowAPPs/fonts/` 及旧 `MeowOS/fonts/<family>/`(无 weight 级)**保留**(勿删,避免破坏老约定/已有文件),仅新引用一律指向 `MeowOS/fonts/default/regular`。
|
||||
- **`font.file` 变更即 unload+reload 生效**(设置页回调);拔卡 → 回退固件内置。
|
||||
- **preset 动态化(两级扫描)**:启动/进设置页扫描一次 `MeowOS/fonts/<family>/<weight>/`,重建「字体文件」下拉(默认字体 + ≤16 套卡内 <family>/<weight>;上限防爆)。扫描仅此二时机,勿放热路径。**存量卡片迁移**:旧版扫 `MeowOS/fonts/<family>/`(图片直接在该层);迁移到 `<family>/regular/` 后,旧 `<family>/` 下不再有三件套 → 新扫默认按两级找,故若未重做成 `<family>/<weight>/` 则扫不到(需先用 mv 迁成 `<family>/<weight>/` 再重扫)。
|
||||
- **MeowPanel 预留**:`meow_font_import_zip()`/`meow_font_export_zip()` 为 TODO 声明(不实现),供 MeowPanel 后续接入(`.zip` 导入校验→解压到 `MeowOS/fonts/<family>/<weight>/`、导出打包)。
|
||||
- **MeowPanel 预留 → 已落地(2026-08-31 用户定案:**改为 `.tar.gz`、放弃 zip**,与 .mpk 同构走 busybox `gzip -dc | tar` 管道,设备必带 tar/gzip)**:面板以 `POST /api/fonts/import` + `GET /api/fonts/export` 接入(见 §5.7.6 与 `patch_v2/meowpanel/README.md` §5.11)。`meow_font_import_zip()`/`meow_font_export_zip()` 原 TODO 声明**不再按 zip 实现**,语义为 tar.gz(函数名可保留或改 `_tgz`)。导入校验→解压到 `MeowOS/fonts/<family>/<weight>/`、导出打包。
|
||||
- **多字体(M2.5x,`display.multi_font`)**:按字符类别(CJK/CJK_SYM/NUM/LATIN/LATIN_SYM/OTHER)逐字符路由,每类可各选一套字库(`font.class_*`,默认空=跟随全局 `font.file`);同 family 去重、同时装载不同字体 ≤4 套(超出 WARN「过多不同字体会增内存」并截断);开关关 = 单字体(现状)。详见 §5.7.7「多字体」。
|
||||
- **字体档位 / 大字库(M2.5x,本次实现)**:抽象出**字库大小档位**,让系统/应用/设置页都知道选的是多大字库 `[源:用户 2026-08-30]`。**定案见 `devdocs/plan/FONT_SYSTEM.md`**(字体系统定案:三级目录 `<family>/<weight>/<tier>/`、`font.json` 增 `tier`/`languages`/`label`、`charset` 改指编码、按语言选字体;本小节以下为旧口径)。
|
||||
- 档位枚举:`MEOW_FONT_TIER_COMMON`(**默认 / ≈4k** 码位)/ `MEOW_FONT_TIER_GBK`(**大 / ≈20k+**)/ `MEOW_FONT_TIER_FULL`(**全 / 可选**,码位/体积最大)。
|
||||
@@ -203,7 +226,7 @@
|
||||
- **⑨ 面板「跟手下拉、半程可回」(launcher 侧,M2.65)**:面板打开/关闭由触发式 180ms 过渡升级为**手指拖动实时跟手**——顶部热区下拉 `progress = 手指位移映射`(复用 `mnp_anim_off`,∈[0,1024],1:1 跟手);松手 `progress≥40%×1024≈410` → 时间轴补到 1024(展开);`<阈值 或 反向拖回` → 回弹到 0 收回(半程取消);关闭同理可跟手。与行左滑(⑦ 横向主导)/列表滚动(竖向溢出)/`meow_edge_back`(左缘)互斥分流。`[NOTIFICATION_SYSTEM §9]`
|
||||
|
||||
### 5.4 配置版本单点 / 刷新率 / 息屏
|
||||
- **现行:M2.73(`/etc/meowos_version` = `0.7.68`,已推卡真机验收;M2.74 / 0.7.69 待打)(M2.72/0.7.67、M2.71/0.7.66、M2.70/0.7.65、M2.69/0.7.64 …)**;版本单点 = `/etc/meowos_version`(见 §1.1,`build_fw_apps.sh -v` 写入)`[DEV_NOTES #4][fw_content.conf]`
|
||||
- **现行:M6.35(`/etc/meowos_version` = `0.8.35`,已上卡;M6.34/0.8.34、M6.33/0.8.33、M6.32/0.8.32、M6.31/0.8.31、M6.30/0.8.30、M6.29/0.8.29、M6.28/0.8.28、M6.27/0.8.27、M6.26/0.8.26、M6.25/0.8.25、M6.24/0.8.24、M6.23/0.8.23、M6.22/0.8.22、M6.21/0.8.21、M6.20/0.8.20、M6.19/0.8.19;更早 M6.18-0.8.18 …)**;版本单点 = `/etc/meowos_version`(见 §1.1,`build_fw_apps.sh -v` 写入)`[DEV_NOTES #4][fw_content.conf]`
|
||||
- 刷新率:`display.refresh_rate` enum 60/30/15(默认 60);`display.refresh_always_max` bool false(关=允许 60→30→15 降档)。governor:连续 12 帧超预算降一档,连续 4 批≈48 帧远低预算升一档 `[SYSTEM_SETTINGS §1][PERF_SCHEDULER][MIGRATION_AC §12.1]`
|
||||
- 息屏:`display.screen_off_timeout` int 秒 默认 **30**(0=常亮);`display.screen_off_mode` enum `panel`(面板下电+背光0+FBIOPBLANK)/`black`(背光0全黑+双击亮屏) `[SYSTEM_SETTINGS §1]`
|
||||
- 亮度 clamp:`display.brightness` 0-255 默认 101(clamp ≥ `display.brightness_min`);`display.brightness_min` 默认 **1**(M2.39 由 25 改 1)——**与旧文档 25 冲突见 §A** `[SYSTEM_SETTINGS §1]`
|
||||
@@ -213,7 +236,7 @@
|
||||
- `<pkg>/` = 程序主体+app.json+资源;`MeowData/<pkg>/{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]`
|
||||
- `.mpk` = tar.gz(app.json + meow 二进制 + 资源),**不含 data/cache**;导入校验 app.json/哈希→解压→数据 init→登入列表 `[APPS 附录 §3]`
|
||||
- 包管理 **CLI `nyai`**(静态 MIPS,部署 `MeowOS/opt/bin/nyai`):install/remove/list/info/search/index/export/upgrade/scan;源 `MeowOS/opt/nyai/sources.list`;索引 `Packages`(Package/Version/Filename/Size/SHA256/Depends/Description)`[APPS 附录 §5]`
|
||||
- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + **`package`(反域名权威 app_id;==目录名;缺省回退目录名)** + 扩展 `framework`/`immersive`/`icon`/`route.<id>`/`awake.<id>`(keep_awake)/`service.<svc>`/`fullscreen`(app 级;`route.<id>.fullscreen`=activity 级;**本次实现**:launcher 启动/切 activity 据声明预设 SDK 全屏,见 §5.1/§3.1/§A-12)/`fonts`(字体档位 `"full"|"gbk"|"common"`,需大字库时声明,见 §5.7.8) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30][源:用户 2026-08-30]`
|
||||
- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + **`package`(反域名权威 app_id;==目录名;缺省回退目录名)** + 扩展 `framework`/`immersive`/`icon`/`route.<id>`/`awake.<id>`(keep_awake)/`service.<svc>`(M4 起服务二进制=App 同目录 `<svc>` 文件)/**`background`/`background_audio`(M4 已落地:无头服务注册权限/后台播放保活)**/**`background_keepalive`(M6.34 起:后台常驻声明——`1`=返回桌面无限期保活不清;缺省走 30min `MEOW_BG_GRACE_MS` 宽限期回收;与 `background_audio`+媒体活跃可叠加无限期保活)**/`fullscreen`(app 级;`route.<id>.fullscreen`=activity 级;**本次实现**:launcher 启动/切 activity 据声明预设 SDK 全屏,见 §5.1/§3.1/§A-12)/`fonts`(字体档位 `"full"|"gbk"|"common"`,需大字库时声明,见 §5.7.8)/`depends`(**二进制依赖**声明:`"curl,jq:optional"` 或行式 `depends=curl,jq:optional,ffmpeg:required`;comma 分项 `<name>[:<mod>]`,mod=required|req(默认)/optional|opt;launcher 启动前查 `/usr/bin/<name>` 与 `/usr/data/mnt/sd_0/MeowOS/opt/bin/<name>`(X_OK,命一即可),required 缺失阻断启动(「缺依赖」toast)、optional 缺失仅 WARN,**仅启动时检查**;nyai install 同查并提示) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30][源:用户 2026-08-30]`
|
||||
|
||||
### 5.6 冷启动充电屏
|
||||
- 触发(**三者同时**):冷启动(`poweroff` 写 `/data/meow_cold_boot=1`;`reboot` 写 0)+ 在充电(AC online=1 或 battery status=Charging)+(开机瞬间一次性快照,判定后不回头)`[CHARGING_SCREEN §0]`
|
||||
@@ -313,12 +336,19 @@
|
||||
int lh = meow_ui_text_height_h(20, hBody);
|
||||
```
|
||||
- 说明:句柄状态为 app 进程内**全局**;同一屏需混排时,推荐每段绘制前显式 `meow_ui_use_font`,或全部走 `*_h` 显式传 handle,避免依赖上次绘制遗留的句柄状态。
|
||||
- **5.7.6 MeowPanel 上传 / 导出(.zip,预留)** `[源:meow_font.h]`
|
||||
- 复用已声明的 `meow_font_import_zip(zip_path)` / `meow_font_export_zip(family,out_zip)`(§5.2 已列为 TODO,未实现)。
|
||||
- **系统字库导入**:`.zip` 内含 `font.png+font.idx+font.json` 三件套;校验三件套就绪 + `font.json` 可解析;sha256 沿用 `font.json` 约定(建议与运行时同口径 = **WARN**,仅缺件/不可解析硬失败);`family`/`weight` 取自 `font.json.family`/`font.json.weight`(缺省 `regular`)或 zip 名;解压到 `MeowOS/fonts/<family>/<weight>/`;**覆盖冲突需先确认**;导入后应触发 launcher 重扫(刷新 §5.7.1 列表)。
|
||||
- **应用字库导入**:目标 `MeowAPPs/<pkg>/fonts/<handle>/`,与 `app.json "fonts"` 声明对齐,校验同上。
|
||||
- **导出**:系统库把 `MeowOS/fonts/<family>/<weight>/` 三件套打包;应用库把 `MeowAPPs/<pkg>/fonts/<handle>/` 打包。
|
||||
- 约定:`.zip` 布局与校验沿用 `font.json` schema(§5.2);导入/导出后的一致性(列表刷新、应用声明同步)归 MeowPanel + launcher 协作,本小节仅列接口语义。
|
||||
- **5.7.6 MeowPanel 上传 / 导出(.tar.gz,2026-08-31 用户定案:放弃 zip,与 .mpk 同构)** `[源:meow_font.h][源:patch_v2/meowpanel/README.md §5.11]`
|
||||
- **已由 MeowPanel 后端落地**:`POST /api/fonts/import`(multipart `file`=<.tar.gz>)+ `GET /api/fonts/export?family=&weight=` +
|
||||
`GET /api/fonts`(列表)。**不用 zip**——设备 busybox `gzip`/`tar` 必带,后端走 `gzip -dc | tar` 管道(与 nyai .mpk 同款),
|
||||
零新增依赖、不引入 zlib/miniz/自研 zip。原 `meow_font_import_zip(zip_path)` / `meow_font_export_zip(family,out_zip)` TODO
|
||||
声明**不再按 zip 实现**,语义即 tar.gz(函名可保留或改 `_tgz`,注明即可)。
|
||||
- **系统字库导入**:`.tar.gz` 内含 `font.png+font.idx+font.json` 三件套;校验三件套就绪 + `font.json` 可解析;
|
||||
sha256 沿用 `font.json` 约定(= **WARN**,仅缺件/不可解析硬失败);`family`/`weight` 取自 `font.json.family`/
|
||||
`font.json.weight`(缺省 `regular`);解压到 `MeowOS/fonts/<family>/<weight>/`;**覆盖冲突需先确认**(409 → 前端 confirm 后
|
||||
`force=1` 重试);导入后进设备「设置→字体」页自动重扫(扫描时机=启动/进设置页,面板**不**主动触发重扫)。
|
||||
- **应用字库导入**:目标 `MeowAPPs/<pkg>/fonts/<handle>/`,与 `app.json "fonts"` 声明对齐,校验同上(待面板后续扩展)。
|
||||
- **导出**:系统库把 `MeowOS/fonts/<family>/<weight>/` 三件套打包为 `<family>-<weight>.font.tar.gz`;应用库同理(待扩展)。
|
||||
- 约定:`.tar.gz` 布局与校验沿用 `font.json` schema(§5.2);导入/导出后的一致性(列表刷新、应用声明同步)归 MeowPanel +
|
||||
launcher 协作。**安全**:tar 解出条目逐个校验(拒绝对路径/`..`/软链/多出条目/子目录包裹),沿用 nyai 的 sanitize 思路。
|
||||
- **5.7.7 多字体(按字符类别逐字符路由,`display.multi_font`,M2.5x 新增)** `[源:meow_font.h][源:MEOWOS_SPEC_REF §5.2/§5.5]`
|
||||
- **背景**:一款字体难兼顾中文(大图集、字形全)与拉丁/数字(干净、省内存)。多字体让**不同字符类别各用一套字库**,按 codepoint 逐字符路由取字形。
|
||||
- **设置键**(launcher 读,SDK 经 env 读同一映射):
|
||||
@@ -340,10 +370,10 @@
|
||||
---
|
||||
|
||||
## 6. 标志性 TODO / 未决(文档未定,勿凭猜实现)
|
||||
- OPEN_QUESTIONS.md 待定 ❓:#1 背景图加载时机/缓存、#2 非 480×720 图如何处(拉伸/裁剪/回退)、#3 叠加层颜色联动、#4 背景色板具体枚举、#5 浅色主题文字对比度、#7 首期是否做应用图标、#8 通知来源(**最大空白**,M2 已定为 系统+IPC+D-Bus 三路)、#9 下拉手势 vs 左滑返回区分、#10 通知存储上限/FIFO、#11 媒体活跃会话判定、#12 多 app 音频混音/焦点、#13 web 实时画面 JPEG/WS、#14 远程输入注入仲裁、#16 主页左滑=侧边栏 vs 返回 `[OPEN_QUESTIONS]`
|
||||
- DEVELOPMENT_PLAN:阶段 0(bootmenu 双系统)**待实现**(M0-M4);阶段 B/E/F;M2-M6 阶段 D 里程碑未做 `[DEVELOPMENT_PLAN]`
|
||||
- 未定/待拍板:`meow_config_*`/`meow_media_*`/崩溃捕获(见 §4);MeowOS 音频输出设置(只读还是可配);OTA 依赖 MeowPanel `[WORK_STATE][OPEN_QUESTIONS #17/#18]`
|
||||
- **meow_svcd 后续(M2.54 已落地大部分)**:~~接入 supervisor 启动链~~(已改 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher `fork+exec`+`waitpid` 仍为备选);~~写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS~~(已改**板载** `/usr/data/meow/services.d/mdnsd.conf` 预置,`trigger=boot`)。剩余:`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);`wifi-up` 判定收紧。**services.d/svcd 已接入开机链(S89)**,但真机行为待验证。 `[WORK_STATE][meow_svcd.md]`
|
||||
- OPEN_QUESTIONS.md 待定 ❓:多数已由后续定案/落地覆盖——#1 背景图加载(「启动加载一次+预渲染快照不每帧缩放」已落地)、#2 图处理(**cover 居中裁剪**)、#3 叠加层颜色(**固定跟明暗主题**)、#4 背景色板枚举(**已清结**:MD3+MIUIx 色板 M2.90 落地,`display.background` 按 style×mode 派生)、#7 应用图标(**已做,app.json icon 加载缓存+首字母占位**)、#8 通知来源(**系统事件 + MeowAPP IPC + D-Bus 桥 三路**,中心 UI 已落地)、#9 下拉手势(**顶部下滑=通知中心**,与左滑返回按起始位置分流)、#10 通知存储(**TF 归档 CSV + 10MB 滚动 + 环形 200 条**)、#11 媒体活跃会话(**active_seq 最近活跃源,30s 超时回收**)、#12 多 app 音频(**已被 meow_audiod 取代(M2.90):守护独占 hw:0,0 + 抢占,App 经 SDK meow_audio_*,软混音否决**)、#13 web 实时画面(**`/api/screen.jpeg` + 增量帧**)、#14 输入注入(**本机触摸优先 5s 仲裁**)、#16 主页左滑=侧边栏 vs 返回(**Now Playing 横划页,左缘返回保留**)、#17 音频输出设置(**底层已由 audiod 收口:路由自动+CLI;设置页 UI 项留后续**)。**仍待定**:#5 浅色文字对比度(待实机观感验收)+ 2026-09-01 新增批(~~PCM 内核挂死~~**已根治**:M2.91 FPGA 开机武装 + M2.94 几何收窄/~~WiFi 录入 UI~~**已落地(M6.1 286:设置页 wifi_page/bt_page 迁 netmgr IPC + 扫 SSID→IME 输密码→connect)**/~~amsd DECLARE 残余(`am start` ACK 后 launcher 未真拉起 App)~~**已修(M6.0 274:`meow_ams_start.c` 忙/非法分支返回正错误码致 ACK 假 OK,改 pending-start 2s 重扫)**/流媒体客户端出声终验(链路技术指标已全绿:FPGA 武装+几何+监控 hwrate 健康,待用户耳机终验)/流媒体客户端 UI 专项/感知音量曲线 dB taper) `[OPEN_QUESTIONS]`
|
||||
- DEVELOPMENT_PLAN:**阶段 0(bootmenu 双系统)已落地(现网即运行中)**;阶段 B/E/F 达成;阶段 D M0-M5 已落地(**M4 后台服务保活+后台播放 2026-09-01 落地**:MSG_REGISTER/SERVICE_STOP/MSG_SYS + 退避熔断 + nav-exit 后台保活,见 FRAMEWORK_DESIGN §9)、**M6 加固 待排期** `[DEVELOPMENT_PLAN]`
|
||||
- 未定/待拍板:MeowOS 音频输出设置(只读还是可配,OPEN_QUESTIONS #17);OTA 在线「检查更新/下载」依赖 MeowPanel(本地/检查升级已可用,URL 下载待定案 #18)`[WORK_STATE][OPEN_QUESTIONS #17/#18]`
|
||||
- **meow_svcd 后续(M2.54 已落地大部分)**:~~接入 supervisor 启动链~~(已改 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher `fork+exec`+`waitpid` 仍为备选);~~写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS~~(已改**板载** `/usr/data/meow/services.d/meow_mdns.conf` 预置,`trigger=boot`;M6.1 起服务名统一为 `meow_mdns`)。剩余:`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);`wifi-up` 判定收紧。**services.d/svcd 已接入开机链(S89)**,但真机行为待验证。 `[WORK_STATE][meow_svcd.md]`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello","package":"os.meow.hello","version":"0.1","framework":1,"immersive":1}
|
||||
{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello","package":"os.meow.hello","version":"0.1","framework":1,"immersive":1,"depends":"curl:optional"}
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# S11jpeg_display_shell — 开机 logo 显示(stock 基线 + MeowOS M21 改动)
|
||||
#
|
||||
# stock 基线(md5 249220f2489f63a4c946c55d8d981ca9)的问题:
|
||||
# `cmd_jpeg_display` 画完 logo **之后**才写死 60。画图那一刻用的是驱动默认亮度,而真机
|
||||
# /module_driver/pwm_backlight.sh 是 `insmod pwm_backlight.ko ... default_brightness=0
|
||||
# max_brightness=101`,pwm_backlight.ko 的 init 会把 props.brightness 置 0 再 update_status
|
||||
# (走 PWM 0 分支)—— 即上电是**全黑**:logo 先画在灭屏上,0.1s 后才被写死的 60 照亮,
|
||||
# 一出现就是 60(≈60% 亮度,用户报「冷启动开机 logo 挺亮」)。
|
||||
# 本脚本又跑在 S21mount_ubifs 之前:那时 /usr/data 还没挂载,读不到
|
||||
# /usr/data/meow/settings.conf,所以这里**只能**写一个和用户设置无关的安全低值。
|
||||
#
|
||||
# M21 改动:
|
||||
# 1) 在画 logo **之前**写 BOOT_BL_MAX=20(≈max_brightness 101 的 1/5):logo 以「暗但可见」出现;
|
||||
# 2) 删除画完后写死 60 的那行 —— 用户亮度改由 S22brightness(/usr/data 挂载后)就位;
|
||||
# 3) 写入「只降不升」:当前值落在 **1..20**(有效且已更暗,例如 S22 已按用户设置写成 1)才不动;
|
||||
# **0 / 空 / 非数字一律照写 20** —— 驱动默认就是 0(等于灭屏),把 0 当「已有值」跳过会让本
|
||||
# 脚本一个值都写不进去(评审 P1:画图时刻亮度 0,且删掉 60 后整段 init 没有可见 logo)。
|
||||
# 4) /dev/fb0 等待、mtd5 主题判定、后台 `jpeg_display &` 语义与 stock 一致。
|
||||
#
|
||||
# 时序(三层写入者 + 运行期):
|
||||
# 驱动默认 0(灭)→ S11 写 20(画 logo 前,logo 暗而可见)→ S21mount_ubifs
|
||||
# → S22brightness(用户值,如 1)→ S70/S90 launcher/watchdog 继续按 display.brightness 维持。
|
||||
#
|
||||
# 注:rcS 用 `$i start` fork 执行本脚本,`jpeg_display &` 后父进程立即返回。所以写入必须在
|
||||
# jpeg_display() **函数体内**(子进程自己的时序),不能挪到 case 分支顶层 —— 那是父进程,
|
||||
# 会和子进程抢时序。
|
||||
# ⚠️ 也不能放在函数入口:背光模块由排在后面的 S11module_driver_default(rcS 自然序
|
||||
# S11jpeg_display_shell < S11module_driver_default)才 insmod,入口那一刻
|
||||
# /sys/class/backlight/backlight_pwm0/ 还不存在,写入必然 no-op(重定向失败还会漏一行错)。
|
||||
# 能写的时机是 /dev/fb0 出现之后 —— 同一份驱动脚本里 pwm_backlight.ko 先于 soc_fb.ko 加载,
|
||||
# fb0 一出现背光节点必定已就绪。
|
||||
|
||||
BL_BRIGHTNESS="/sys/class/backlight/backlight_pwm0/brightness"
|
||||
# 画 logo 前的亮度上限(绝对档位 0..max_brightness,不是百分比)。
|
||||
# 这是「暗但可见」的安全低值,不是用户值;用户值(display.brightness,可能更暗如 1)由 S22brightness 写入。
|
||||
BOOT_BL_MAX=20
|
||||
|
||||
# 只降不升地把背光压到 <= $1:
|
||||
# 当前读数在 1..$1 = 已有有效且更暗的值(如 S22 已写用户值 1)→ 不动;
|
||||
# 0 / 空 / 非数字(驱动默认 0 = 灭屏、节点未就绪)→ 视为「没有有效值」,照写。
|
||||
bl_cap()
|
||||
{
|
||||
cap="$1"
|
||||
cur=$(cat "$BL_BRIGHTNESS" 2>/dev/null)
|
||||
case "$cur" in
|
||||
''|*[!0-9]*) ;; # 读不到 / 非数字 → 照写
|
||||
*) [ "$cur" -ge 1 ] && [ "$cur" -le "$cap" ] && return 0 ;; # 已有 1..cap 的有效值 → 不动
|
||||
esac
|
||||
# 2>/dev/null 放在 > 之前:节点尚未就绪时连「重定向失败」的报错一起吞掉(否则每个冷启动漏一行)。
|
||||
if echo "$cap" 2>/dev/null > "$BL_BRIGHTNESS"; then
|
||||
echo "backlight: -> $cap (was ${cur:-unreadable})"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
jpeg_display()
|
||||
{
|
||||
echo "Starting display... "
|
||||
|
||||
while true
|
||||
do
|
||||
if [ -e "/dev/fb0" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.02
|
||||
done
|
||||
|
||||
echo "Wait display... "
|
||||
|
||||
# M21:画 logo 前写一次安全低亮度。此时背光节点必定已就绪(同一驱动脚本里
|
||||
# pwm_backlight.ko 先于 soc_fb.ko 加载,fb0 出现即背光已注册)。只降不升:
|
||||
# cur 落在 1..20 的有效值就不动;cur=0(驱动默认 = 灭屏)照写 20;
|
||||
# S22brightness 若已写更暗的用户值也不会被顶回 20。
|
||||
bl_cap "$BOOT_BL_MAX"
|
||||
|
||||
theme=$(nanddump -q -s 0x20000 -l 7 /dev/mtd5 -a)
|
||||
# stock 写的是 `==`(BusyBox test 接受,dash/POSIX test 不接受);语义不变,改为 `=` 以便移植与宿主验证。
|
||||
if [ "$theme" = "theme:1" ]; then
|
||||
cmd_jpeg_display /etc/logo1.jpeg
|
||||
elif [ "$theme" = "theme:2" ]; then
|
||||
cmd_jpeg_display /etc/logo2.jpeg
|
||||
else
|
||||
cmd_jpeg_display /etc/logo.jpeg
|
||||
fi
|
||||
# stock: "R1 lg 35583 need 100ms delay" —— 原先是给「画完后写 60」留的面板 settle 时间。
|
||||
# 亮度写入已移到画图之前,但该硬件 settle 保留(在后台子进程里,不阻塞 init 链)。
|
||||
sleep 0.1
|
||||
echo "Finish display... "
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
jpeg_display &
|
||||
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# S22brightness — 开机按用户设置恢复背光(MeowOS M21)
|
||||
#
|
||||
# 位置/顺序(/etc/init.d 按文件名自然序执行,见 /etc/init.d/rcS):
|
||||
# S11jpeg_display_shell 画开机 logo,画前把背光写成 20(驱动默认 0 = 灭屏 → logo 暗而可见)
|
||||
# → S21mount_ubifs 挂载 /usr/data
|
||||
# → S22brightness 本脚本:此时才读得到 /usr/data/meow/settings.conf
|
||||
# → S30dbus … → S70meow_sysui / S90meow_bootmenu launcher/watchdog 接管后继续维持用户亮度
|
||||
#
|
||||
# 为什么不能只改 S11:S11 早于 S21,/usr/data 未挂载 → settings.conf 不存在,只能写死一个安全低值
|
||||
# (且真机 S11 跑在背光模块加载之前,写入时机受 fb0 约束)。
|
||||
#
|
||||
# 行为:
|
||||
# - 读 display.brightness,以 display.brightness_min(下限,缺省 1)和
|
||||
# /sys/class/backlight/backlight_pwm0/max_brightness(上限,本机实测 101)clamp 后写入。
|
||||
# - 单位是 0..max_brightness 的**绝对档位**,不是百分比;display.brightness_min 同单位。
|
||||
# - 读不到文件 / 解析不出 display.brightness / max_brightness 读不到 → **不改背光**
|
||||
# (S11 未必写过值,真机驱动默认 0 = 灭屏,所以这里只报当前读数、不做假设),
|
||||
# 打一行日志后 exit 0。
|
||||
# - 幂等(当前值已是目标值则不写)、无 sleep / 无重试,任何运行期失败都 exit 0(不阻断 init 链)。
|
||||
|
||||
BL_BRIGHTNESS="/sys/class/backlight/backlight_pwm0/brightness"
|
||||
BL_MAX="/sys/class/backlight/backlight_pwm0/max_brightness"
|
||||
MEOW_SETTINGS="/usr/data/meow/settings.conf"
|
||||
|
||||
log()
|
||||
{
|
||||
echo "S22brightness: $*"
|
||||
}
|
||||
|
||||
# 当前背光读数(仅用于日志;读不到返回 unreadable)。
|
||||
cur_bl()
|
||||
{
|
||||
v=$(cat "$BL_BRIGHTNESS" 2>/dev/null)
|
||||
case "$v" in
|
||||
''|*[!0-9]*) printf 'unreadable' ;;
|
||||
*) printf '%s' "$v" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
# /usr/data 刚挂载时 settings.conf 可能尚不存在(新机/首次启动)或被正在写:
|
||||
# 只读一次,读不到就不改背光,不 sleep 重试(不阻塞 init 链)。
|
||||
if [ ! -r "$MEOW_SETTINGS" ]; then
|
||||
log "settings.conf 不可读($MEOW_SETTINGS),不改背光(当前 brightness=$(cur_bl))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
max=$(cat "$BL_MAX" 2>/dev/null)
|
||||
case "$max" in
|
||||
''|*[!0-9]*)
|
||||
log "max_brightness 读不到,不改背光(当前 brightness=$(cur_bl))"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
if [ "$max" -le 0 ]; then
|
||||
log "max_brightness=$max 非法,不改背光(当前 brightness=$(cur_bl))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 解析 settings.conf:<key> = <value>,容忍两侧空白/CR,忽略注释与非数字值。
|
||||
b=""
|
||||
mn=""
|
||||
while IFS='=' read -r k v; do
|
||||
k=$(printf '%s' "$k" | tr -d '[:space:]')
|
||||
v=$(printf '%s' "$v" | tr -d '[:space:]')
|
||||
case "$k" in
|
||||
display.brightness)
|
||||
case "$v" in
|
||||
''|*[!0-9]*) ;;
|
||||
*) b="$v" ;;
|
||||
esac
|
||||
;;
|
||||
display.brightness_min)
|
||||
case "$v" in
|
||||
''|*[!0-9]*) ;;
|
||||
*) mn="$v" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done < "$MEOW_SETTINGS"
|
||||
|
||||
if [ -z "$b" ]; then
|
||||
log "settings.conf 无有效 display.brightness,不改背光(当前 brightness=$(cur_bl))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ -n "$mn" ] || mn=1
|
||||
[ "$mn" -lt 1 ] && mn=1
|
||||
[ "$mn" -gt "$max" ] && mn="$max"
|
||||
|
||||
[ "$b" -lt "$mn" ] && b="$mn"
|
||||
[ "$b" -gt "$max" ] && b="$max"
|
||||
|
||||
cur=$(cat "$BL_BRIGHTNESS" 2>/dev/null)
|
||||
if [ "$cur" = "$b" ]; then
|
||||
log "背光已是 $b,跳过写入"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 2>/dev/null 放在 > 之前,避免节点不可写时重定向报错漏到控制台。
|
||||
if echo "$b" 2>/dev/null > "$BL_BRIGHTNESS"; then
|
||||
log "开机背光 -> $b(min=$mn max=$max)"
|
||||
else
|
||||
log "写背光 $b 失败,保持当前值(brightness=$(cur_bl))"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop|restart|reload)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -104,6 +104,7 @@
|
||||
#define PLAYER_BIN "/usr/bin/hiby_player"
|
||||
#define PLAYER_LOG "/usr/data/meow_player.log"
|
||||
#define LAUNCHER_BIN "/usr/bin/meow_launcherd" /* MeowOS 基础主屏(M2 接管) */
|
||||
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 板载用户设置(display.brightness,与 launcher/watchdog 同源) */
|
||||
|
||||
/* ---- 面板/背光(与 meow_launcherd 相同路径,§18.21/§红线)---- */
|
||||
#ifndef FBIOBLANK
|
||||
@@ -844,6 +845,37 @@ static void write_brightness(int val)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* M6.25:开机选单亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness)。
|
||||
* 根因:选单显示路径从不写背光,开机菜单/第二屏亮度纯继承引导早期面板高值;M2.83 起 launcher
|
||||
* 「开机选单期不抢屏」怠速,移除了 launcher 提前写用户亮度那一步 → 菜单亮在引导高值上。
|
||||
* 与 meow_watchdog wd_settings_brightness() 同源解析(同文件、同 clamp:下限 display.brightness_min
|
||||
* 默认 1=极暗但可见、上限 100=防闪屏),自包含零依赖,不链 launcher meow_settings。返回 clamped
|
||||
* 值;settings 文件不可读 → 返回 -1,调用方**不动**(退化为现状,保安全——选单阶段只写 brightness、
|
||||
* 绝不碰 FBIOBLANK/blank,§18.24 纪律)。 */
|
||||
static int bootmenu_settings_brightness(void)
|
||||
{
|
||||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
||||
if (!f) return -1;
|
||||
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
||||
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
||||
char buf[128];
|
||||
while (fgets(buf, sizeof buf, f)) {
|
||||
char *eq = strchr(buf, '=');
|
||||
if (!eq) continue;
|
||||
*eq = 0;
|
||||
char *k = buf; int kl = (int)strlen(k);
|
||||
while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0;
|
||||
char *v = eq + 1; while (*v == ' ' || *v == '\t') v++;
|
||||
int iv = atoi(v);
|
||||
if (!strcmp(k, "display.brightness_min")) { if (iv >= 1 && iv <= 100) mn = iv; }
|
||||
else if (!strcmp(k, "display.brightness")) { b = iv; }
|
||||
}
|
||||
fclose(f);
|
||||
if (b < mn) b = mn;
|
||||
if (b > 100) b = 100;
|
||||
return b;
|
||||
}
|
||||
|
||||
/* 面板 unblank(FBIOBLANK 0)。仅用于杀播放器后把屏点亮,以便 MeowOS 接管;
|
||||
* 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。
|
||||
* bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */
|
||||
@@ -1704,6 +1736,13 @@ int main(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* M6.25(方案A):选单显示前把背光按用户设置恢复(display.brightness,clamp [min||1,100])。
|
||||
* settings 不可读/值非法 → 不动(退化为现状)。只在显示前写一次,不碰 FBIOBLANK/blank(§18.24)。 */
|
||||
{
|
||||
int b = bootmenu_settings_brightness();
|
||||
if (b > 0) write_brightness(b);
|
||||
}
|
||||
|
||||
if (fb_open() != 0) return 1;
|
||||
fb_snapshot_player(); /* 快照当前显示缓冲(恢复播放器帧用) */
|
||||
fb_pan_buf0(); /* 画前 pan 到 yoffset=0(buffer0) */
|
||||
|
||||
@@ -22,7 +22,8 @@ patch_v2/files_app/
|
||||
│ ├── fsx.h # 纯业务接口(不依赖 meow_ui)
|
||||
│ └── fsx.c # 路径安全/格式化/枚举排序/虚拟化区间/写操作(可宿主测)
|
||||
└── test/
|
||||
└── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
|
||||
├── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
|
||||
└── test_row_text_fit.c # 宿主几何断言:列表行高 ≥ 双行文本块(M17)
|
||||
```
|
||||
|
||||
## 构建
|
||||
@@ -55,12 +56,20 @@ v1 无需持久化,暂未使用(启动恒进卡根 `/usr/data/mnt/sd_0/`)
|
||||
cd patch_v2/files_app
|
||||
gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||
test/test_fsx.c logic/fsx.c -o /tmp/test_fsx && /tmp/test_fsx
|
||||
|
||||
# 列表行高几何(无需 meow_ui.a;Termux 下产物落 $TMPDIR)
|
||||
gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c \
|
||||
-o "$TMPDIR/test_row_text_fit" && "$TMPDIR/test_row_text_fit"
|
||||
```
|
||||
|
||||
覆盖:路径安全(`within_sd`/`join`/`sanitize`/`dirname`/`basename`)、展示格式化(`fmt_size`/`fmt_time`)、
|
||||
目录枚举排序(`scan_dir`/`sort`)、虚拟化区间(`visible_range`/`max_scroll`)、写操作(`mkdir`/`rename`/
|
||||
`delete`,含卡外/越界拒绝)、非文本写拒绝。宿主全 PASS。
|
||||
|
||||
`test_row_text_fit` 是 M17 行高回归:目录列表行恒为双行档位,行高必须 ≥ 文本块
|
||||
`2*meow_ui_text_height(font_body) + 2`(真机 20px 字库行高 30 → 62),否则 SDK 居中后首行标题
|
||||
`ty < 视口顶` 被裁剪守卫整条跳过。断言矩阵覆盖真机五种字库行高 + 旧固定 56 的 bug 形态。
|
||||
|
||||
## 交互
|
||||
|
||||
- 触摸:行 body=进目录/开文本预览;右侧 `⋯` 或长按→BottomSheet(删除[确认弹窗]/重命名[IME]/新建目录[IME]/属性[大小·mtime]);
|
||||
@@ -78,6 +87,10 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||
位移 ≤ 容忍」→ 经 `meow_ui_set_long_press_cb` 注册的回调带坐标交给本页;`browser_on_long_press`
|
||||
用 `meow_listview_hit` 行命中打开 BottomSheet。App 层长按计时(`browser_tick`/`g_press_ms`)已删除,
|
||||
其抬手 TAP 由 SDK 抑制(长按不再触发点击)。
|
||||
- **行高按字体行高派生(M17)**:`files_row_h()` = `max(APP_ROW_H, 2*行高 + 2)`(当前主题正文
|
||||
`font_body` 的 `meow_ui_text_height`)。目录行是双行档位,行高小于双行文本块时 SDK 居中后首行
|
||||
标题落在视口顶之上被裁剪(20px 字库行高 30 → 块 62,旧的显式 56 首行名称空白)。滚动/命中/
|
||||
最大滚动量由 SDK 按同一行高换算,页脚几何独立(`r_list` 单独扣 `APP_FOOTER_H`)。
|
||||
2. **中文字形**:SDK 默认 `common` 字库(≈4k)不含 CJK 全量;exFAT 中文文件名/中文文本预览在 common 档会显示缺字豆腐。
|
||||
预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。
|
||||
3. **预览仅文本**:按扩展名白名单 + 首 512 字节 NUL 嗅探判文本;二进制/图片无预览(提示「not a text file」)。
|
||||
@@ -88,7 +101,7 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||
## 验证状态
|
||||
|
||||
- 编译:`-Wall -Wextra` 0 warning/0 error,产物 `files`(MIPS32r2 静态 ELF)。
|
||||
- 宿主:`test/test_fsx.c` 全 PASS。
|
||||
- 宿主:`test/test_fsx.c` 全 PASS;`test/test_row_text_fit.c` 32 断言全 PASS(M17 行高几何)。
|
||||
- qemu-mipsel(无 /dev/fb0 → SDK 离屏 480×720):正常运行到事件循环,导出离屏帧,
|
||||
确认 init/建页/topbar(返回+NEW)/空态(NO STORAGE CARD)渲染正确、SIGTERM 干净退出。
|
||||
- 实机目录列表渲染(有真实 TF 卡目录)需刷机/设备挂载验证——宿主模拟无法造出 `/usr/data/mnt/sd_0`。
|
||||
Binary file not shown.
@@ -37,13 +37,38 @@ void picker_cancel(void); /* 取消:发 code=MEOW_ERR
|
||||
#define APP_W 480
|
||||
#define APP_H 720
|
||||
#define APP_TOPBAR_H 56
|
||||
#define APP_ROW_H 56 /* 列表行高 */
|
||||
#define APP_ROW_H 56 /* 列表行高下限(旧固定值;实际按字体行高派生,见 files_row_h) */
|
||||
#define APP_PAD 16
|
||||
#define APP_ICON_W 26 /* 图标占宽 */
|
||||
#define APP_OP_W 52 /* 右侧操作钮占宽(触达区) */
|
||||
#define APP_FOOTER_H 26 /* 底部信息行高(列表/文本内容在其之上滚动) */
|
||||
#define APP_READ_MAX (256 * 1024) /* 文本预览读取上限 */
|
||||
|
||||
/* ---- 列表行高:按字体行高派生(M17,首行标题不显示根因修复)----
|
||||
* SDK meow_listview 的双行渲染把「标题+副标题」文本块
|
||||
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
|
||||
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),标题只在 ty >= 视口顶时才画。
|
||||
* item < block 时 ty = 行顶 - Δ:第一行(行顶 == 视口顶)标题被整条跳过、只剩副标题。
|
||||
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62,旧的显式 56 → Δ=3。
|
||||
* 故行高按当前主题正文行高派生:max(APP_ROW_H, 2*line_h + 2)(行高 30 → 62)。
|
||||
* 本页行恒为双行档位(browser_src2 填 title + subtitle)→ 用 lines=2 公式。
|
||||
* 行几何(draw / hit / max_scroll)全由 SDK 按传入的这一个值换算,故滚动与命中天然同步;
|
||||
* 页脚几何与行高无关(r_list 单独扣 APP_FOOTER_H)。 */
|
||||
#define FILES_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行距) */
|
||||
|
||||
static inline int files_row_h_for_line_h(int line_h)
|
||||
{
|
||||
if (line_h < 1) line_h = 1; /* 字体未就绪 → 退回下限 */
|
||||
int h = 2 * line_h + FILES_ROW_SUB_GAP;
|
||||
return (h > APP_ROW_H) ? h : APP_ROW_H;
|
||||
}
|
||||
/* 当前主题正文行高下的两行列表行高(页面 create 时取一次;字体档位切换走页面重建)。 */
|
||||
static inline int files_row_h(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
return files_row_h_for_line_h(th ? meow_ui_text_height(th->font_body) : 0);
|
||||
}
|
||||
|
||||
/* ---- App 级复用矩形/几何 ---- */
|
||||
typedef struct { int x, y, w, h; } app_rect_t;
|
||||
#define AR(x,y,w,h) ((app_rect_t){ (x),(y),(w),(h) })
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* test/test_row_text_fit.c — 宿主回归(M17):目录列表行高必须容纳双行文本块。
|
||||
*
|
||||
* 背景(真机根因,承接 M12/M15):SDK meow_listview 的双行渲染把
|
||||
* block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行)
|
||||
* 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。
|
||||
* 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在。
|
||||
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62,旧的
|
||||
* 显式 set_item_h(56) → ty = 行顶 - 3(本 App 目录列表首行名称空白)。
|
||||
*
|
||||
* 本测试只验 app 侧行高派生(files_row_h_for_line_h,纯整数、不链 SDK):
|
||||
* 行高 = max(APP_ROW_H, 2*行高 + FILES_ROW_SUB_GAP) → block ≤ item,首行标题不被钳掉。
|
||||
*
|
||||
* 编译/运行(宿主 gcc,只需 SDK 头,不需要 meow_ui.a):
|
||||
* cd patch_v2/files_app
|
||||
* gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c -o "$TMPDIR/test_row_text_fit" \
|
||||
* && "$TMPDIR/test_row_text_fit"
|
||||
*
|
||||
* 断言矩阵 = 真机已装字库 20px 面实测行高:source_han_serif 30(当前生效)、
|
||||
* misans/default 27、google_sans/meslo 26、source_han_sans 21;14 = 宿主 5x7 回退。
|
||||
* ① block ≤ item(否则居中偏移为负)
|
||||
* ② 首行 ty ≥ 0 → 绝对 ty_abs ≥ 视口顶(标题不被裁剪守卫跳过)
|
||||
* ③ 副标题底边 ty + block ≤ item(两行都不越行)
|
||||
* ④ item ≥ APP_ROW_H 下限(小字库/字体未就绪不缩回固定小值)
|
||||
* ⑤ 旧固定 56 在行高 30 下 ty < 0(bug 形态复现,防止再写死 56)
|
||||
*/
|
||||
#include "../files_internal.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int g_checks, g_fail;
|
||||
|
||||
static void chk(int cond, int line_h, const char *what)
|
||||
{
|
||||
g_checks++;
|
||||
if (!cond) {
|
||||
g_fail++;
|
||||
printf(" FAIL lh=%d: %s\n", line_h, what);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const int lh_all[] = { 14, 21, 26, 27, 30 };
|
||||
const int n = (int)(sizeof lh_all / sizeof lh_all[0]);
|
||||
const int list_y = 120; /* 任意列表顶(只验相对几何) */
|
||||
|
||||
for (int k = 0; k < n; k++) {
|
||||
int lh = lh_all[k];
|
||||
int item = files_row_h_for_line_h(lh);
|
||||
int block = 2 * lh + FILES_ROW_SUB_GAP;
|
||||
int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */
|
||||
|
||||
printf("lh=%2d item=%3d block=%3d ty=%d\n", lh, item, block, ty);
|
||||
|
||||
chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)");
|
||||
chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶");
|
||||
chk(ty + block <= item, lh, "副标题底边仍在行内");
|
||||
chk(item >= APP_ROW_H, lh, "行高不得小于 APP_ROW_H 下限");
|
||||
|
||||
/* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */
|
||||
chk(list_y + ty >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉");
|
||||
}
|
||||
|
||||
/* 行高随字体行高单调不减 */
|
||||
chk(files_row_h_for_line_h(30) >= files_row_h_for_line_h(26), 30, "行高随字体行高单调不减");
|
||||
|
||||
/* 真机当前生效字库(source_han_serif 20px,行高 30):block 62 → item 62 */
|
||||
chk(files_row_h_for_line_h(30) == 62, 30, "行高 30 → 派生行高 62");
|
||||
chk(2 * 30 + FILES_ROW_SUB_GAP <= files_row_h_for_line_h(30), 30, "派生行高容纳 62px 文本块");
|
||||
|
||||
/* 下限:小字库 / 字体未就绪(line_h=0)仍不缩回固定小值 */
|
||||
chk(files_row_h_for_line_h(14) == APP_ROW_H, 14, "小字库退回 APP_ROW_H 下限");
|
||||
chk(files_row_h_for_line_h(0) == APP_ROW_H, 0, "字体未就绪(行高 0)退回下限");
|
||||
|
||||
/* 回归哨兵:旧固定行高 56 + 当前默认字体(行高 30)就是 bug 形态 */
|
||||
{
|
||||
const int lh = 30;
|
||||
int block = 2 * lh + FILES_ROW_SUB_GAP; /* 62 */
|
||||
int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */
|
||||
chk(ty_legacy < 0, lh, "旧行高 56 + 行高 30 → ty<0(首行标题被跳过,bug 复现)");
|
||||
chk(files_row_h_for_line_h(lh) > APP_ROW_H, lh, "默认字体下行高必须大于旧值 56");
|
||||
}
|
||||
|
||||
printf("%s: %d checks, %d fail (font line-height matrix: %d)\n",
|
||||
g_fail ? "FAIL" : "PASS", g_checks, g_fail, n);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -421,7 +421,7 @@ static void browser_page_create(meow_view *v, void *ctx)
|
||||
/* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */
|
||||
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
||||
browser_src2, NULL, MEOW_LIST_SEL_SINGLE);
|
||||
meow_listview_set_item_h(g_list, APP_ROW_H);
|
||||
meow_listview_set_item_h(g_list, files_row_h()); /* M17:按字体行高派生,容纳双行文本块 */
|
||||
/* 空态/页脚注册为非控件绘制物(列表视口不含页脚;invalidate/paint_full 自动重画) */
|
||||
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
|
||||
browser_draw_empty, NULL);
|
||||
|
||||
@@ -39,7 +39,27 @@ static int viewer_vp_h(void) { return r_content.h - APP_FOOTER_H; }
|
||||
/* 屏幕行高(px)。 */
|
||||
static int viewer_rowh(const meow_theme *th) { return meow_ui_text_height(th->font_body) + 6; }
|
||||
|
||||
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。 */
|
||||
/* UTF-8 码位字节长(1..4):s[bi] 起、串内有效字节数 lim。
|
||||
* 首字节非法/续字节非法/序列不完整 → 保守按 1(单字节);保证折行绝不断在多字节码位中间,
|
||||
* 也保证宽测量(meow_ui_text_width 按码位测)对中文/混合文本正确。 */
|
||||
static int viewer_cp_len(const unsigned char *s, int bi, int lim)
|
||||
{
|
||||
unsigned char c = s[bi];
|
||||
int n;
|
||||
if (c < 0x80) return 1;
|
||||
else if ((c & 0xE0) == 0xC0) n = 2;
|
||||
else if ((c & 0xF0) == 0xE0) n = 3;
|
||||
else if ((c & 0xF8) == 0xF0) n = 4;
|
||||
else return 1;
|
||||
if (lim - bi < n) return 1;
|
||||
for (int i = 1; i < n; i++)
|
||||
if ((s[bi + i] & 0xC0) != 0x80) return 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。
|
||||
* 按「UTF-8 码位」逐字累加显示宽度(meow_ui_text_width 按码位测>0),到内容宽 colw 即断行;
|
||||
* 多字节(CJK)整字不断开。 */
|
||||
static int viewer_wrap_line(int li, int intra, char *out, int outsz)
|
||||
{
|
||||
if (!g_buf || li < 0 || li >= g_nline) return 0;
|
||||
@@ -49,13 +69,15 @@ static int viewer_wrap_line(int li, int intra, char *out, int outsz)
|
||||
while (len > 0 && (g_buf[start + len - 1] == '\n' || g_buf[start + len - 1] == '\r')) len--;
|
||||
int colw = viewer_colw();
|
||||
int col = 0, row = 0, outlen = 0;
|
||||
char ch[2]; ch[1] = '\0';
|
||||
for (int i = 0; i < len; i++) {
|
||||
ch[0] = g_buf[start + i];
|
||||
if (ch[0] == '\r') continue;
|
||||
int w = meow_ui_text_width(ch, 20);
|
||||
const unsigned char *s = (const unsigned char *)g_buf + start;
|
||||
for (int bi = 0; bi < len; ) {
|
||||
int clen = viewer_cp_len(s, bi, len);
|
||||
if (clen == 1 && s[bi] == '\r') { bi++; continue; }
|
||||
char cp[5]; cp[clen] = '\0';
|
||||
memcpy(cp, s + bi, (size_t)clen); bi += clen;
|
||||
int w = meow_ui_text_width(cp, 20);
|
||||
if (col + w > colw && col > 0) { row++; col = 0; }
|
||||
if (row == intra && out && outlen < outsz - 1) out[outlen++] = ch[0];
|
||||
if (row == intra && out && outlen + clen < outsz) { memcpy(out + outlen, cp, (size_t)clen); outlen += clen; }
|
||||
col += w;
|
||||
}
|
||||
if (out) out[outlen] = '\0';
|
||||
|
||||
@@ -12,7 +12,8 @@ ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||
"$CC" -Os -static -Wall -Wextra -I../sdk -I. \
|
||||
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c ui/common/chrome.c \
|
||||
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c \
|
||||
ui/demo/sdk2/sdk2.c ui/common/chrome.c \
|
||||
logic/notify.c logic/state.c \
|
||||
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread -o hello
|
||||
ls -la hello
|
||||
|
||||
Binary file not shown.
@@ -33,6 +33,31 @@ extern meow_sheet *g_sheet; /* BottomSheet(阻塞模态;main 创建
|
||||
typedef struct { int x, y, w, h; } rect_t;
|
||||
#define RX(x,y,w,h) ((rect_t){ (x), (y), (w), (h) })
|
||||
|
||||
/* ---- 两行列表行高:按字体行高派生(M17,首行标题不显示根因修复)----
|
||||
* SDK meow_listview 的双行渲染把「标题+副标题」文本块
|
||||
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
|
||||
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),标题只在 ty >= 视口顶时才画。
|
||||
* item < block 时 ty = 行顶 - Δ:第一行(行顶 == 视口顶)标题被整条跳过、只剩副标题。
|
||||
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62;demo 的 swipe 列表
|
||||
* 是双行档位(set_item_renderer(2,...)),旧的显式 48 比 block 小 14 → 首行标题必丢。
|
||||
* 故行高按当前主题正文行高派生:max(HELLO_ROW_H_MIN, 2*line_h + 2)(行高 30 → 62)。
|
||||
* 行几何(draw / hit / max_scroll)全由 SDK 按传入的这一个值换算,滚动与命中天然同步。 */
|
||||
#define HELLO_ROW_H_MIN 56
|
||||
#define HELLO_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行距) */
|
||||
|
||||
static inline int hello_row_h_for_line_h(int line_h)
|
||||
{
|
||||
if (line_h < 1) line_h = 1; /* 字体未就绪 → 退回下限 */
|
||||
int h = 2 * line_h + HELLO_ROW_SUB_GAP;
|
||||
return (h > HELLO_ROW_H_MIN) ? h : HELLO_ROW_H_MIN;
|
||||
}
|
||||
/* 当前主题正文行高下的两行列表行高(建列表时取一次;字体档位切换走内容重建)。 */
|
||||
static inline int hello_row_h(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
return hello_row_h_for_line_h(th ? meow_ui_text_height(th->font_body) : 0);
|
||||
}
|
||||
|
||||
/* ---- 视图控制器(ctx = biz-UI 绑定点)----
|
||||
* 本批控制器为薄壳:持 meow_view 回指针;页面细粒度视图态(g_ds/g_drag_* /scroll 等)留在各页
|
||||
* 模块作用域全局(设计定案 B:demo 视图态不迁 logic)。控制器未来可吸收这些状态。 */
|
||||
@@ -106,6 +131,21 @@ int list_view_down(int x, int y); /* touch_down 命中列表:置
|
||||
int list_view_move(int y); /* touch_move 拖动列表:1=在列表拖动 */
|
||||
int list_view_swipe(int del); /* swipe:列表聚焦时滚列表,1=已消费 */
|
||||
|
||||
/* ---- SDK 批次2 控件演示区 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)----
|
||||
* 复刻 list.c 模式:demo 页内容区底部追加三节——枚举选择器 meow_select / 2D 虚拟化宫格
|
||||
* meow_lazygrid / 列表行左滑删除 meow_listview_set_swipe_delete。子视图自持三节控件与视图态
|
||||
* (grid 非 meow_widget:on_draw 直绘 meow_lazygrid_draw;swipe-list 行左滑 3 相位手势),
|
||||
* 对外暴露 sdk2_build_at(demo_build_at 调)与 hit/down/move/swipe/release(demo 分发改派)。 */
|
||||
meow_view *sdk2_view_create(void); /* 建 CONTAINER 子视图(须即 meow_view_add_child) */
|
||||
meow_view *sdk2_view_get(void); /* 返回子视图(宿主测嵌套生命周期断言) */
|
||||
int sdk2_build_at(int yb_base, int ct, int scroll); /* 在 base yb 处建三节控件(owner=ACT_DEMO),返回下一 yb */
|
||||
void sdk2_release(void); /* 内容释放后:释放 grid/摘控件引用/清几何 */
|
||||
int sdk2_hit(int x, int y); /* tap 分发:select 按钮/宫格 tap/列表行 tap/行左滑松手 swipe_end */
|
||||
int sdk2_down(int x, int y); /* touch_down:宫格拖拽或列表行左滑 begin;1=本区消费 */
|
||||
int sdk2_move(int x, int y); /* touch_move:宫格滚动/列表左滑 move 或列表竖向滚动;1=消费 */
|
||||
int sdk2_swipe(int del); /* swipe UP/DOWN:宫格或列表聚焦时滚自身;1=已消费 */
|
||||
int sdk2_dragging(void); /* 1=本子视图拖拽在途(demo_swipe 早退防 on_swipe 二次滚动) */
|
||||
|
||||
/* ---- chrome(ui/common/chrome.c)---- */
|
||||
extern meow_widget *g_tb; /* TopAppBar(chrome 固定) */
|
||||
extern meow_widget *g_nav; /* NavigationBar(chrome 固定) */
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* test/test_row_text_fit.c — 宿主回归(M17):demo 的 swipe 列表行高必须容纳双行文本块。
|
||||
*
|
||||
* 背景(真机根因,承接 M12/M15):SDK meow_listview 的双行渲染把
|
||||
* block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行)
|
||||
* 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。
|
||||
* 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在。
|
||||
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62;
|
||||
* demo 的 swipe 列表是双行档位(set_item_renderer(2,...))却显式 set_item_h(48) → ty = 行顶 - 7,
|
||||
* 「notif 0」首行标题在真机上必然空白。
|
||||
*
|
||||
* 本测试只验 app 侧行高派生(hello_row_h_for_line_h,纯整数、不链 SDK):
|
||||
* 行高 = max(HELLO_ROW_H_MIN, 2*行高 + HELLO_ROW_SUB_GAP) → block ≤ item,首行标题不被钳掉。
|
||||
*
|
||||
* 编译/运行(宿主 gcc,只需 SDK 头,不需要 meow_ui.a):
|
||||
* cd patch_v2/hello_app
|
||||
* gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c -o "$TMPDIR/test_row_text_fit" \
|
||||
* && "$TMPDIR/test_row_text_fit"
|
||||
*
|
||||
* 断言矩阵 = 真机已装字库 20px 面实测行高:source_han_serif 30(当前生效)、
|
||||
* misans/default 27、google_sans/meslo 26、source_han_sans 21;14 = 宿主 5x7 回退。
|
||||
* ① block ≤ item(否则居中偏移为负)
|
||||
* ② 首行 ty ≥ 0 → 绝对 ty_abs ≥ 视口顶(标题不被裁剪守卫跳过)
|
||||
* ③ 副标题底边 ty + block ≤ item(两行都不越行)
|
||||
* ④ item ≥ HELLO_ROW_H_MIN 下限(小字库/字体未就绪不缩回固定小值)
|
||||
* ⑤ 旧固定 48 在行高 30 下 ty < 0(bug 形态复现,防止再写死 48)
|
||||
*/
|
||||
#include "../hello_internal.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int g_checks, g_fail;
|
||||
|
||||
static void chk(int cond, int line_h, const char *what)
|
||||
{
|
||||
g_checks++;
|
||||
if (!cond) {
|
||||
g_fail++;
|
||||
printf(" FAIL lh=%d: %s\n", line_h, what);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const int lh_all[] = { 14, 21, 26, 27, 30 };
|
||||
const int n = (int)(sizeof lh_all / sizeof lh_all[0]);
|
||||
const int list_y = 400; /* 任意列表顶(只验相对几何) */
|
||||
|
||||
for (int k = 0; k < n; k++) {
|
||||
int lh = lh_all[k];
|
||||
int item = hello_row_h_for_line_h(lh);
|
||||
int block = 2 * lh + HELLO_ROW_SUB_GAP;
|
||||
int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */
|
||||
|
||||
printf("lh=%2d item=%3d block=%3d ty=%d\n", lh, item, block, ty);
|
||||
|
||||
chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)");
|
||||
chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶");
|
||||
chk(ty + block <= item, lh, "副标题底边仍在行内");
|
||||
chk(item >= HELLO_ROW_H_MIN, lh, "行高不得小于 HELLO_ROW_H_MIN 下限");
|
||||
|
||||
/* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */
|
||||
chk(list_y + ty >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉");
|
||||
}
|
||||
|
||||
/* 行高随字体行高单调不减 */
|
||||
chk(hello_row_h_for_line_h(30) >= hello_row_h_for_line_h(26), 30, "行高随字体行高单调不减");
|
||||
|
||||
/* 真机当前生效字库(source_han_serif 20px,行高 30):block 62 → item 62 */
|
||||
chk(hello_row_h_for_line_h(30) == 62, 30, "行高 30 → 派生行高 62");
|
||||
chk(2 * 30 + HELLO_ROW_SUB_GAP <= hello_row_h_for_line_h(30), 30, "派生行高容纳 62px 文本块");
|
||||
|
||||
/* 下限:小字库 / 字体未就绪(line_h=0)仍不缩回固定小值 */
|
||||
chk(hello_row_h_for_line_h(14) == HELLO_ROW_H_MIN, 14, "小字库退回 HELLO_ROW_H_MIN 下限");
|
||||
chk(hello_row_h_for_line_h(0) == HELLO_ROW_H_MIN, 0, "字体未就绪(行高 0)退回下限");
|
||||
|
||||
/* 回归哨兵:旧固定行高 48 + 当前默认字体(行高 30)就是 bug 形态 */
|
||||
{
|
||||
const int lh = 30;
|
||||
int block = 2 * lh + HELLO_ROW_SUB_GAP; /* 62 */
|
||||
int ty_legacy = (48 - block) / 2; /* -7 */
|
||||
chk(ty_legacy < 0, lh, "旧行高 48 + 行高 30 → ty<0(首行标题被跳过,bug 复现)");
|
||||
chk(hello_row_h_for_line_h(lh) > 48, lh, "默认字体下行高必须大于旧值 48");
|
||||
}
|
||||
|
||||
printf("%s: %d checks, %d fail (font line-height matrix: %d)\n",
|
||||
g_fail ? "FAIL" : "PASS", g_checks, g_fail, n);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ static int g_drag_slider = 0; /* 手指按在 slider */
|
||||
int g_drag_page = 0; /* 手指按在页内容(非 slider/list):拖拽滚页面 */
|
||||
static int g_drag_page_last_y = 0; /* 页面拖拽上一帧 y */
|
||||
static int g_drag_page_delta = 0; /* 页面拖拽累计位移(节流重建,≥8px 才重建一次) */
|
||||
static uint64_t g_drag_page_tlast = 0; /* 页面拖拽上次重建时刻(359:每 poll 合并一次 rebuild) */
|
||||
|
||||
/* SDK 控件不透明:App 自记几何,用于局部 flush / 命中 / scroll 重建。 */
|
||||
|
||||
@@ -157,6 +158,7 @@ static void demo_free_content(void)
|
||||
if (g_slider_cid > 0) { meow_draw_unregister(g_slider_cid); g_slider_cid = 0; }
|
||||
g_dtitle_n = 0;
|
||||
list_view_release(); /* 列表控件已在上方 g_dslot 释放;摘容器引用/几何 */
|
||||
sdk2_release(); /* SDK 批次2 子视图:释放 grid/btn/list 控件引用 + 清几何 */
|
||||
}
|
||||
|
||||
/* 按 scroll 重建全部内容控件(owner=ACT_DEMO:调用方须已 push ACT_DEMO)。
|
||||
@@ -246,6 +248,15 @@ static void demo_build_at(int scroll)
|
||||
d_title(DCL_X, SY(yb), "MEDIA SESSION (meta)"); yb += 26;
|
||||
r_media = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_media = meow_btn_create_variant(r_media.x, r_media.y, r_media.w, r_media.h, "SEND META", MEOW_BTN_FILLED); SLOT(g_dbtn_media, r_media.x, r_media.y, r_media.w, r_media.h); yb += DCH;
|
||||
|
||||
/* ---- SDK 批次2 新控件(meow_select / meow_lazygrid / swipe-delete)----
|
||||
* 三节由 sdk2 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)承接:章节标题/宫格/select 值文本走
|
||||
* 子视图 on_draw 直绘(同列表区标题);select 按钮 + swipe 列表 widget 由 sdk2 自持、此处创建。
|
||||
* 控件创建后几何已内存;跨重建视图态(grid scroll/sel、swipe 行集/scroll、select 值)由
|
||||
* sdk2_build_at 从子视图持有态恢复。
|
||||
* 注:sdk2 置于 LazyColumn 之前(列表保持 demo 页内容**末尾**区块),确保「滚动到底列表上屏」
|
||||
* 的既有宿主 P3 断言坐标假设仍然成立(sdk2 增高在列表上方,列表仍是内容底)。 */
|
||||
yb = sdk2_build_at(yb, ct, scroll);
|
||||
|
||||
/* ---- LazyColumn(100 项列表滚动) ----
|
||||
* 本区块标题改由列表区 CONTAINER 子视图(ui/demo/list/list.c)的 on_draw 绘制(注册表→
|
||||
* invalidate 链路);列表滚动/选中由 list_view_bind 从子视图持有态恢复(不再经 g_ds)。 */
|
||||
@@ -268,7 +279,21 @@ void demo_paint(void)
|
||||
meow_ui_paint_full(); /* 整帧按 z 序重画 */
|
||||
}
|
||||
|
||||
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用 */
|
||||
/* 内容视口坐标(每次 build 同步):屏 y ∈ [g_demo_ct, g_demo_cb),x 跨 0..width。 */
|
||||
static void demo_repaint_viewport(void)
|
||||
{
|
||||
int h = g_demo_cb - g_demo_ct;
|
||||
if (h <= 0) return;
|
||||
/* 滚动重绘:只清底+按 z 重画内容视口矩形 [0, ct, w, h],不整帧。
|
||||
* 359 教训(滚动只刷视口 + 局部脏区不整屏重绘铁律):meow_ui_invalidate 内部已把绘制 clip
|
||||
* 收敛到矩形(meow_gfx_clip_push)+ meow_ui_flush_region 只上屏该区;chrome(TopAppBar/
|
||||
* NavBar/FAB,z=999,位于视口外)不触及,上一帧像素保留,滚动不再重画 chrome 字形。 */
|
||||
meow_ui_invalidate(0, g_demo_ct, meow_ui_width(), h);
|
||||
}
|
||||
|
||||
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用。
|
||||
* 滚动路径用视口局部刷(demo_repaint_viewport)替代整帧 demo_paint——每刻度重建仍发生,
|
||||
* 但不再整帧重绘(359 教训:SDK 控件内建局部刷,卡顿源于调用方整页重绘)。 */
|
||||
void demo_rebuild_to(int scroll)
|
||||
{
|
||||
int vp_h = g_demo_cb - g_demo_ct;
|
||||
@@ -279,7 +304,7 @@ void demo_rebuild_to(int scroll)
|
||||
demo_free_content();
|
||||
demo_build_at(scroll);
|
||||
g_demo_scroll = scroll;
|
||||
demo_paint();
|
||||
demo_repaint_viewport();
|
||||
}
|
||||
|
||||
/* ---- demo 交互 ---- */
|
||||
@@ -299,6 +324,8 @@ static int demo_content_tap(int x, int y)
|
||||
|
||||
/* 嵌套命中(子>父,§A.7 设计):列表区 CONTAINER 子视图先发,命中即消费。 */
|
||||
if (list_view_hit(x, y)) return 1;
|
||||
/* SDK 批次2 子视图(select 按钮/宫格/列表行左滑松手):命中即消费。 */
|
||||
if (sdk2_hit(x, y)) return 1;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
if (meow_widget_hit(g_dbtn_b[i], x, y)) { d_toggle_sel(g_dbtn_b[i], r_btn[i]); g_ds.btn[i] = meow_widget_selected(g_dbtn_b[i]); return 1; }
|
||||
@@ -344,8 +371,18 @@ static void demo_media_send(void)
|
||||
}
|
||||
|
||||
/* demo 页 on_tap 分发(chrome → 内容) */
|
||||
/* 页面拖拽抬手把节流窗内遗留的 delta 一次性重建(视口局部刷)——slow drag 结束为 TAP 时也精确落点。 */
|
||||
static void demo_flush_page_delta(void)
|
||||
{
|
||||
if (g_drag_page && (g_drag_page_delta >= 1 || g_drag_page_delta <= -1)) {
|
||||
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
|
||||
g_drag_page_delta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void demo_on_tap(int x, int y)
|
||||
{
|
||||
demo_flush_page_delta(); /* 手势结束:TAP 抬手边沿(<SWIPE_THRESH),把遗留页面 delta 落到位 */
|
||||
if (g_tb) {
|
||||
int ah = meow_topappbar_hit(g_tb, x, y);
|
||||
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
|
||||
@@ -389,44 +426,59 @@ static void demo_slider_drag(int x)
|
||||
meow_ui_invalidate(r_slider_txt.x, r_slider_txt.y, r_slider_txt.w, r_slider_txt.h); /* 重画值文本 */
|
||||
}
|
||||
|
||||
/* demo 页 on_touch_down:锁一种拖拽(slider > list > 页内容)。
|
||||
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口)。 */
|
||||
/* demo 页 on_touch_down:锁一种拖拽(slider > list > sdk2 > 页内容)。
|
||||
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口);
|
||||
* SDK 批次2 子视图走 sdk2_down(宫格拖拽/列表行左滑 begin,子>父收口)。 */
|
||||
void demo_touch_down(int x, int y)
|
||||
{
|
||||
g_drag_slider = 0; g_drag_list = 0; g_drag_page = 0; g_drag_page_delta = 0;
|
||||
if (!g_demo) return;
|
||||
if (meow_widget_hit(g_dslider, x, y)) { g_drag_slider = 1; demo_slider_drag(x); return; }
|
||||
if (list_view_down(x, y)) return; /* 列表区:置 focus+drag_list(list.c) */
|
||||
if (sdk2_down(x, y)) return; /* SDK 批次2:宫格/列表行左滑 begin(sdk2.c) */
|
||||
g_demo_focus = 0;
|
||||
if (y >= g_demo_ct && y < g_demo_cb && x >= 0 && x < meow_ui_width()) {
|
||||
g_drag_page = 1; g_drag_page_last_y = y;
|
||||
}
|
||||
}
|
||||
|
||||
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / 页面滚动节流重建)。 */
|
||||
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / sdk2 滚动 / 页面滚动节流重建)。 */
|
||||
void demo_touch_move(int x, int y)
|
||||
{
|
||||
if (!g_demo) return;
|
||||
if (g_drag_slider) { demo_slider_drag(x); return; }
|
||||
if (list_view_move(y)) return; /* 列表拖动(list.c) */
|
||||
if (sdk2_move(x, y)) return; /* SDK 批次2:宫格滚动/列表左滑 move/列表竖向滚动(sdk2.c) */
|
||||
if (g_drag_page) {
|
||||
int dy = y - g_drag_page_last_y; g_drag_page_last_y = y;
|
||||
g_drag_page_delta += -dy; /* 手指上移(y 减) → scroll 增(显示更低内容),与 on_swipe 一致 */
|
||||
if (g_drag_page_delta >= 8 || g_drag_page_delta <= -8) {
|
||||
/* 359 教训:滚动期间每个 poll 合并一次 rebuild(勿每触摸事件各自重建/上屏)。
|
||||
* 累计位移超阈值(≥8) 且(距上次 ≥16ms 或 已是大步 ≥32)才重建一次;未到窗则继续
|
||||
* 累积 delta(不丢),下个窗口一次性重建到精确位置。 */
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
int big = (g_drag_page_delta >= 8 || g_drag_page_delta <= -8);
|
||||
int merge = (now - g_drag_page_tlast >= 16) ||
|
||||
(g_drag_page_delta >= 32) || (g_drag_page_delta <= -32);
|
||||
if (big && merge) {
|
||||
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
|
||||
g_drag_page_tlast = now;
|
||||
g_drag_page_delta = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表聚焦滚列表,否则页滚动。 */
|
||||
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表/宫格/sdk2 聚焦滚自身,否则页滚动。 */
|
||||
void demo_swipe(int dir, int dist)
|
||||
{
|
||||
if (dir == MEOW_SWIPE_LEFT) { meow_view_page_pop(); return; }
|
||||
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
|
||||
if (g_drag_page || g_drag_list) return; /* 本次手势已实时跟手滚动过 */
|
||||
if (g_drag_page || g_drag_list || sdk2_dragging()) { /* 本次手势已实时跟手滚动过 */
|
||||
demo_flush_page_delta(); /* 抬手:flush 节流窗内遗留页面 delta(视口局部刷)精确落点 */
|
||||
return;
|
||||
}
|
||||
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 手指上移 → 偏移增大 */
|
||||
if (list_view_swipe(del)) return; /* 列表聚焦:滚列表(list.c,子>父消费) */
|
||||
if (sdk2_swipe(del)) return; /* SDK 批次2:宫格/列表聚焦滚自身(sdk2.c,子>父消费) */
|
||||
demo_rebuild_to(g_demo_scroll + del);
|
||||
}
|
||||
|
||||
@@ -441,7 +493,8 @@ static void demo_page_enter(meow_view *v, void *ctx)
|
||||
hello_nav_report(); /* ① 上报 demo 栈深/顶层 */
|
||||
/* 列表区 CONTAINER 子视图随本页进入由 SDK view_enter 递归驱动(其 on_draw 在 demo_rebuild_to
|
||||
* →demo_paint 中经注册表绘制;meow_draw_view_entered 含祖先已进入判定,首批可见)。 */
|
||||
demo_rebuild_to(0);
|
||||
demo_rebuild_to(0); /* 内容建到 scroll=0(视口局部刷) */
|
||||
demo_paint(); /* 进入页整帧首绘(含 chrome 置顶 z=999);仅进入一次,非滚动路径 */
|
||||
meow_log(MEOW_LOG_INFO, "demo enter scroll=0");
|
||||
}
|
||||
|
||||
@@ -475,14 +528,16 @@ int demo_is_active(void)
|
||||
}
|
||||
|
||||
/* demo view:on_create 建 chrome+内容(一次,owner=ACT_DEMO);进入时 rebuild 到顶。
|
||||
* 视图深度化:先挂列表区 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
|
||||
* 列表子视图 id 用非页面值,不占 g_view_by_id(见 list.c)。 */
|
||||
* 视图深度化:先挂列表区 + SDK 批次2 两个 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
|
||||
* 子视图 id 用非页面值,不占 g_view_by_id(见 list.c/sdk2.c)。 */
|
||||
static void demo_page_create(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v;
|
||||
((demo_ctrl *)ctx)->view = v;
|
||||
meow_view *lv = list_view_create();
|
||||
if (lv) meow_view_add_child(v, lv);
|
||||
meow_view *s2 = sdk2_view_create();
|
||||
if (s2) meow_view_add_child(v, s2);
|
||||
demo_init();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
/* ui/demo/sdk2/sdk2.c — 控件库 Demo 页内「SDK 批次2 新控件」CONTAINER 子视图(agent-363)。
|
||||
*
|
||||
* 目标(SDK_VIEW_FRAMEWORK_DESIGN.md §B.4/§E③,同 list.c):把 demo 页底部的三个 SDK 批次2
|
||||
* 新控件演示区块抽成一个 meow_view CONTAINER 子视图,挂到 demo ACTIVITY view 的 children:
|
||||
* ① meow_select (§5.8.9 枚举/预设选择器:下拉模态 + 单选列表 + on_change 回调)
|
||||
* ② meow_lazygrid (§12.4 2 维虚拟化宫格:非 meow_widget,on_draw 直绘 meow_lazygrid_draw)
|
||||
* ③ meow_listview 左滑删除(§5.5 swipe-delete:swipe_begin/move/end + on_delete + 回弹/删除动画)
|
||||
*
|
||||
* 职责边界(诚实,与 list.c 同折中):
|
||||
* - 本子视图承接三节**整体**:章节标题 + select 按钮 + 宫格 + swipe-列表 的创建/释放与视图态
|
||||
* (grid 虚拟源/选中/滚动、swipe 行集/滚动、select 选中值)。控件**创建**在 demo_build_at 链路内
|
||||
* (sdk2_build_at,owner=ACT_DEMO)、**释放**在 sdk2_release(demo_free_content 调)——与 list.c
|
||||
* 相同折中:完整「控件归容器拥有」需 SDK 侧支持(本批冻结 patch_v2/sdk/)。
|
||||
* - meow_lazygrid 是**非 meow_widget**:不注册 z 序/绘制注册表。本子视图 on_draw 直接调
|
||||
* meow_lazygrid_draw(view → 注册表 → invalidate 链路,与 list 区块标题同法)。
|
||||
* - 章节标题(三段)与 select 当前值文本也走 on_draw 直绘(列表区标题同法),不逐标题注册
|
||||
* CUSTOM 绘制物;on_draw 是回调、view 不缓存像素(非保留,§5.8.7)。
|
||||
* - 嵌套生命周期由 SDK view_enter/view_leave 递归驱动(随 demo 页进出,同 list.c);绘制经
|
||||
* meow_draw_view_entered(含祖先已进入判定)在 meow_ui_paint_full/invalidate 触发。
|
||||
*
|
||||
* 事件接线(demo 页把 touch/tap/swipe 分发改派到本子视图,实现「子>父」命中):
|
||||
* - tap → sdk2_hit (select 按钮 / 宫格 tap / 列表行 tap / 行左滑松手 swipe_end)
|
||||
* - down → sdk2_down (宫格聚焦拖拽 / 列表行左滑 begin)
|
||||
* - move → sdk2_move (宫格滚动 / 列表左滑 move 或列表竖向滚动)
|
||||
* - swipe → sdk2_swipe (宫格/列表聚焦时滚自身;否则回退页面滚动)
|
||||
*/
|
||||
#include "hello_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- 本子视图(CONTAINER,非页面:id 用越界值避免占用 g_view_by_id 顶层页槽位)---- */
|
||||
static meow_view *g_sdk2_view = NULL;
|
||||
|
||||
/* ---- demo 内容布局常量(与 demo.c 一致):内容区 x16 宽 448 ---- */
|
||||
#define S2_X 16
|
||||
#define S2_W 448
|
||||
#define S2_DCW 212
|
||||
#define S2_DBH 44
|
||||
#define S2_CH 52
|
||||
|
||||
/* 三节(章节标题几何在 sdk2_build_at 计算后存入,on_draw 直绘用;屏幕坐标)。 */
|
||||
static int s2_t1_y, s2_t2_y, s2_t3_y; /* 三节标题 y(on_draw 直绘) */
|
||||
|
||||
/* ============================================================ */
|
||||
/* ① meow_select — 枚举/预设选择器 */
|
||||
/* ============================================================ */
|
||||
static meow_widget *g_btn_sel = NULL; /* 「打开选择器」按钮(sdk2_release 释放) */
|
||||
static meow_select *g_sel = NULL; /* 选择器实例(sdk2_view_create 建一次,destroy 释放) */
|
||||
static rect_t r_selbtn = { 0, 0, 0, 0 };
|
||||
static rect_t r_selval = { 0, 0, 0, 0 }; /* 当前选择值文本区(on_draw 直绘) */
|
||||
static int g_sel_cur = 0; /* 当前选中项(跨重建保留;on_change 更新) */
|
||||
|
||||
static const char *g_sel_items[] = { /* 枚举演示:音频档位(对齐 M7.2 设置页采样率/码率) */
|
||||
"Auto", "44.1kHz", "48kHz", "96kHz", "192kHz",
|
||||
};
|
||||
#define SEL_N ((int)(sizeof g_sel_items / sizeof g_sel_items[0]))
|
||||
|
||||
/* on_change 回调(meow_select_open 确认后触发:更新选中下标 + 重画值文本区)。 */
|
||||
static void sdk2_sel_change(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx >= 0 && idx < SEL_N) g_sel_cur = idx;
|
||||
if (r_selval.w > 0) meow_ui_invalidate(r_selval.x, r_selval.y, r_selval.w, r_selval.h);
|
||||
}
|
||||
|
||||
/* 打开选择器(阻塞到确认/取消;on_change 已更新选中值)。 */
|
||||
static void sdk2_select_open(void)
|
||||
{
|
||||
if (!g_sel) return;
|
||||
int r = meow_select_open(g_sel);
|
||||
meow_log(MEOW_LOG_INFO, "sdk2 select return=%d (%s)",
|
||||
r, (r >= 0 && r < SEL_N) ? g_sel_items[r] : "cancel");
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* ② meow_lazygrid — 2 维虚拟化宫格(非 widget) */
|
||||
/* ============================================================ */
|
||||
static meow_lazygrid *g_grid = NULL;
|
||||
static rect_t r_grid = { 0, 0, 0, 0 };
|
||||
static int g_grid_scroll = 0; /* 跨重建保留:宫格滚动偏移 */
|
||||
static int g_grid_sel = -1; /* 跨重建保留:选中项 */
|
||||
static int g_drag_grid = 0; /* 触摸拖宫格(down 置位;move 滚动) */
|
||||
static int g_grid_focus = 0; /* 1=宫格聚焦(swipe 滚宫格而非页面) */
|
||||
static int g_grid_last_y = 0;
|
||||
|
||||
#define GRID_N 36 /* 总项数(count > 视口行数 → 可滚动) */
|
||||
#define GRID_COLS 3
|
||||
#define GRID_CW 138
|
||||
#define GRID_CH 86
|
||||
#define GRID_GAP 10
|
||||
#define GRID_H 190
|
||||
|
||||
/* 虚拟源:count 总项数(实时查);bind 项进入视口时调一次(同 listview 池语义)。 */
|
||||
static int grid_count(void *ctx) { (void)ctx; return GRID_N; }
|
||||
static void grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
static char t[24], s[24];
|
||||
snprintf(t, sizeof t, "App %d", idx);
|
||||
snprintf(s, sizeof s, "v1.%d", idx);
|
||||
memset(item, 0, sizeof *item);
|
||||
item->title = t; item->subtitle = s;
|
||||
item->enabled = (idx % 9 != 8); /* 演示禁用变淡(每 9 个禁用 1 个) */
|
||||
}
|
||||
|
||||
/* 完全自绘 cell(set_item_draw):icon+label 风格,对齐主界面用途。 */
|
||||
static void grid_cell(int idx, int x, int y, int w, int h, int st, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int sel = st & MEOW_LAZYGRID_ST_SELECTED;
|
||||
int dis = st & MEOW_LAZYGRID_ST_DISABLED;
|
||||
uint32_t bg = sel ? th->primaryContainer : th->surfaceContainerLow;
|
||||
uint32_t icon = dis ? th->onSurfaceVariant : th->primary;
|
||||
uint32_t txt = sel ? th->onPrimaryContainer
|
||||
: (dis ? th->onSurfaceVariant : th->onSurface);
|
||||
meow_ui_fill_rect(x, y, w, h, bg); /* 格底卡片 */
|
||||
/* icon 块(无图资源:主题 primary 方块代替主界面图标位) */
|
||||
int isz = w / 3; if (isz > h / 2) isz = h / 2;
|
||||
meow_ui_fill_rect(x + (w - isz) / 2, y + 10, isz, isz, icon);
|
||||
/* 名 + 状态(底部两行,中心对齐) */
|
||||
static char t[24], s[24];
|
||||
snprintf(t, sizeof t, "App %d", idx);
|
||||
snprintf(s, sizeof s, "v1.%d", idx);
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
meow_ui_text_center(x + w / 2, y + h - thh - 22, t, th->font_body, txt);
|
||||
meow_ui_text_center(x + w / 2, y + h - thh - 3, s, th->font_body, txt);
|
||||
if (sel) meow_ui_draw_rect_round(x, y, w, h, th->radius_card, th->primary);
|
||||
if (dis) meow_ui_fill_rect_alpha(x, y, w, h, th->onSurface, 90);
|
||||
}
|
||||
|
||||
/* 宫格 tap:置选中 + 局部刷 + 日志。 */
|
||||
static void grid_tap(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
g_grid_sel = idx;
|
||||
meow_log(MEOW_LOG_INFO, "sdk2 grid tap idx=%d", idx);
|
||||
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* ③ meow_listview 行左滑删除(swipe-delete) */
|
||||
/* ============================================================ */
|
||||
#define LV_MAX 9
|
||||
static meow_widget *g_lv = NULL; /* W_LISTVIEW(注册表自动画:行 + 右露删除区) */
|
||||
static rect_t r_lv = { 0, 0, 0, 0 };
|
||||
static int g_lv_deleted[LV_MAX]; /* 1=已删除(行移除;count 实时收缩) */
|
||||
static char g_lv_label[LV_MAX][24]; /* 行标题(bind 填充) */
|
||||
static int g_lv_scroll = 0; /* 跨重建保留:列表滚动 */
|
||||
static int g_lv_focus = 0; /* 1=swipe-list 聚焦(swipe 滚列表而非页面) */
|
||||
static int g_lv_swipe = 0; /* 行左滑在途(swipe_begin 命中;hit 里 swipe_end) */
|
||||
static int g_drag_lv = 0; /* 触摸拖列表(竖向滚动);d. 左滑时同置位 */
|
||||
static int g_lv_last_y = 0;
|
||||
#define LV_H 190
|
||||
|
||||
static int lv_count_remaining(void)
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < LV_MAX; i++) if (!g_lv_deleted[i]) n++;
|
||||
return n;
|
||||
}
|
||||
static int lv_count(void *ctx) { (void)ctx; return lv_count_remaining(); }
|
||||
|
||||
/* 显示下标(0..count-1)→ 源行 id(跳过已删除行)。删除后行前移,身份按源行稳定。 */
|
||||
static int lv_real_index(int display)
|
||||
{
|
||||
if (display < 0) return -1;
|
||||
int c = 0;
|
||||
for (int i = 0; i < LV_MAX; i++) {
|
||||
if (g_lv_deleted[i]) continue;
|
||||
if (c == display) return i;
|
||||
c++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void lv_bind(int idx, meow_lv_item *item, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int real = lv_real_index(idx);
|
||||
static char s[24];
|
||||
memset(item, 0, sizeof *item);
|
||||
snprintf(s, sizeof s, "swipe left");
|
||||
item->title = (real >= 0) ? g_lv_label[real] : "";
|
||||
item->subtitle = s;
|
||||
item->icon_kind = (real >= 0 && real % 3 == 0) ? MEOW_LIST_ICON_FOLDER : MEOW_LIST_ICON_NONE;
|
||||
item->right = MEOW_LIST_RIGHT_NONE;
|
||||
item->enabled = 1;
|
||||
}
|
||||
/* on_delete(swipe 删除动画滑出完成回调):删除该显示下标对应的源行 + 局部刷。
|
||||
* 注:swipe_end 传的是显示下标(含删除前定位的 swipe_idx);映射回源行 id 标记。 */
|
||||
static void lv_delete(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int real = lv_real_index(idx);
|
||||
if (real >= 0) g_lv_deleted[real] = 1;
|
||||
meow_log(MEOW_LOG_INFO, "sdk2 swipe delete display=%d real=%d", idx, real);
|
||||
if (g_lv && r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
}
|
||||
|
||||
/* 建三节时填充行标题(demo_init 一次性;跨重建保留)。 */
|
||||
static void sdk2_init_rows(void)
|
||||
{
|
||||
for (int i = 0; i < LV_MAX; i++) {
|
||||
snprintf(g_lv_label[i], sizeof g_lv_label[i], "notif %d", i);
|
||||
g_lv_deleted[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int s2_built = 0; /* 1=本子视图内容已建(on_draw / 事件分发判空) */
|
||||
|
||||
/* ============================================================ */
|
||||
/* 释放 / 建三节 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 内容释放(demo_free_content 调):清引用(本子视图建的控件在 sdk2_release 释放)。 */
|
||||
void sdk2_release(void)
|
||||
{
|
||||
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
|
||||
if (g_btn_sel) { meow_widget_free(g_btn_sel); g_btn_sel = NULL; }
|
||||
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
|
||||
r_selbtn = r_selval = r_grid = r_lv = RX(0, 0, 0, 0);
|
||||
g_drag_grid = 0; g_grid_focus = 0; g_grid_last_y = 0;
|
||||
g_lv_focus = 0; g_lv_swipe = 0; g_drag_lv = 0; g_lv_last_y = 0;
|
||||
s2_built = 0;
|
||||
if (g_sdk2_view) g_sdk2_view->x = g_sdk2_view->y = g_sdk2_view->w = g_sdk2_view->h = 0;
|
||||
}
|
||||
|
||||
/* 内容重建后建三节(demo_build_at 调)。入参:yb_base=base 空间 y;ct/scroll 同 demo.c
|
||||
* (SY(base)=ct+base-scroll)。返回下一 base y。 */
|
||||
int sdk2_build_at(int yb_base, int ct, int scroll)
|
||||
{
|
||||
#define SY2(b) (ct + (b) - scroll)
|
||||
int yb = yb_base;
|
||||
|
||||
/* ---- ① SELECT ---- */
|
||||
s2_t1_y = SY2(yb);
|
||||
yb += 26;
|
||||
r_selbtn = RX(S2_X, SY2(yb), S2_DCW, S2_DBH);
|
||||
g_btn_sel = meow_btn_create_variant(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h,
|
||||
"CHOOSE", MEOW_BTN_OUTLINED);
|
||||
r_selval = RX(S2_X + S2_DCW + 14, SY2(yb) + 10, S2_W - S2_DCW - 14, 24);
|
||||
yb += S2_CH;
|
||||
|
||||
/* ---- ② LAZYGRID ---- */
|
||||
s2_t2_y = SY2(yb);
|
||||
yb += 30;
|
||||
r_grid = RX(S2_X, SY2(yb), S2_W, GRID_H);
|
||||
g_grid = meow_lazygrid_create(r_grid.x, r_grid.y, r_grid.w, r_grid.h,
|
||||
GRID_COLS, GRID_CW, GRID_CH, GRID_GAP);
|
||||
if (g_grid) {
|
||||
meow_lazygrid_set_vsource(g_grid, grid_count, grid_bind, NULL);
|
||||
meow_lazygrid_set_item_draw(g_grid, grid_cell, NULL);
|
||||
meow_lazygrid_set_empty_text(g_grid, "No apps");
|
||||
meow_lazygrid_set_on_tap(g_grid, grid_tap, NULL);
|
||||
meow_lazygrid_set_scroll(g_grid, g_grid_scroll);
|
||||
meow_lazygrid_set_sel(g_grid, g_grid_sel);
|
||||
}
|
||||
yb += GRID_H + 30;
|
||||
|
||||
/* ---- ③ SWIPE-DELETE ---- */
|
||||
s2_t3_y = SY2(yb);
|
||||
yb += 26;
|
||||
r_lv = RX(S2_X, SY2(yb), S2_W, LV_H);
|
||||
g_lv = meow_listview_create(r_lv.x, r_lv.y, r_lv.w, r_lv.h, 0,
|
||||
NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
if (g_lv) {
|
||||
meow_listview_set_item_h(g_lv, hello_row_h()); /* M17:双行文本块按字体行高派生 */
|
||||
meow_listview_set_vsource(g_lv, lv_count, lv_bind, NULL);
|
||||
meow_listview_set_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
|
||||
meow_listview_set_empty_text(g_lv, "All rows deleted");
|
||||
meow_listview_set_swipe_delete(g_lv, 1, lv_delete, NULL);
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
}
|
||||
yb += LV_H + 10;
|
||||
|
||||
if (g_sdk2_view) {
|
||||
g_sdk2_view->x = S2_X; g_sdk2_view->y = s2_t1_y;
|
||||
g_sdk2_view->w = S2_W; g_sdk2_view->h = (r_lv.y + r_lv.h) - s2_t1_y;
|
||||
}
|
||||
s2_built = 1;
|
||||
return yb;
|
||||
#undef SY2
|
||||
}
|
||||
|
||||
/* ---- on_draw:三节标题 + select 当前值 + 宫格(非保留,invalidate 触发重画)---- */
|
||||
static void sdk2_draw_on(meow_view *v, const meow_theme *th, void *ctx)
|
||||
{
|
||||
(void)v; (void)th; (void)ctx;
|
||||
if (!s2_built) return;
|
||||
const meow_theme *t = meow_theme_get();
|
||||
meow_ui_text(S2_X, s2_t1_y, "SELECT (enum/preset)", 20, t->primary);
|
||||
meow_ui_text(S2_X, s2_t2_y, "LAZYGRID (2D virtualized)", 20, t->primary);
|
||||
meow_ui_text(S2_X, s2_t3_y, "LISTVIEW swipe-delete", 20, t->primary);
|
||||
char buf[40];
|
||||
snprintf(buf, sizeof buf, "sel = %s", g_sel_items[g_sel_cur]);
|
||||
meow_ui_text(r_selval.x, r_selval.y, buf, 20, t->dim);
|
||||
if (g_grid) meow_lazygrid_draw(g_grid, t);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 事件分发(demo 页转发) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* tap:select 按钮 → 打开模态;列表行左滑松手 → swipe_end;宫格 → tap_at;列表行 → tap。 */
|
||||
int sdk2_hit(int x, int y)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
/* 行左滑【松手】最优先:滑动后手指可能已移出列表矩形(只判在途标志)。 */
|
||||
if (g_lv && g_lv_swipe) {
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
int c = meow_listview_swipe_end(g_lv, x, y); /* 判删/回弹;动画由 meow_anim_tick 推进 */
|
||||
g_lv_swipe = 0;
|
||||
if (c) return 1; /* 消费(删/回弹动画中);未滑动=0 落行选中 */
|
||||
}
|
||||
if (g_btn_sel && meow_widget_hit(g_btn_sel, x, y)) {
|
||||
sdk2_select_open();
|
||||
meow_ui_invalidate(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h);
|
||||
return 1;
|
||||
}
|
||||
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
|
||||
meow_lazygrid_tap_at(g_grid, x, y); /* 置选中 + grid_tap 回调 + 局部刷 */
|
||||
return 1;
|
||||
}
|
||||
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
|
||||
meow_listview_tap_at(g_lv, x, y); /* 行点击(演示选中)+ 局部刷 */
|
||||
meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* touch_down:命中区置拖拽/聚焦。宫格>列表。返回 1=本区消费。 */
|
||||
int sdk2_down(int x, int y)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
/* 新手势先复位本子视图拖拽态:demo_touch_down 只复位 list/slider/page 标志,不触及 sdk2
|
||||
* 的 g_drag_grid/g_drag_lv/g_lv_swipe(sdk2.c 内部,demo 页经 extern 看不到);不复位则上一条
|
||||
* sdk2 拖拽残留会在本次「落页内容区」新手势的 move 里继续劫持页面拖动(sdk2_move 守卫误判)。 */
|
||||
g_drag_grid = 0; g_drag_lv = 0; g_lv_swipe = 0;
|
||||
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
|
||||
g_drag_grid = 1; g_grid_focus = 1; g_grid_last_y = y;
|
||||
return 1;
|
||||
}
|
||||
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
g_lv_swipe = meow_listview_swipe_begin(g_lv, x, y); /* 命中可滑行 → 1 */
|
||||
g_lv_focus = 1; g_drag_lv = 1; g_lv_last_y = y;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* touch_move:宫格滚动 / 列表左滑 move(消费→不滚页面)或列表竖向滚动。返回 1=消费。 */
|
||||
int sdk2_move(int x, int y)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
if (g_drag_grid && g_grid) {
|
||||
int dy = y - g_grid_last_y; g_grid_last_y = y;
|
||||
meow_lazygrid_scroll_by(g_grid, -dy);
|
||||
g_grid_scroll = meow_lazygrid_scroll(g_grid);
|
||||
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
|
||||
return 1;
|
||||
}
|
||||
/* 列表滚动须在「本次手势命中列表」才消费(g_drag_lv,由 sdk2_down 命中列表置位)。
|
||||
* 若仅因 g_lv 存在就无条件消费,会劫持 demo 页内容区拖拽(任何 move 都被这里吃掉,
|
||||
* 页面永不滚动)——与 grid(g_drag_grid 守卫)同构;359 教训同源(SDK 控件局部刷对,
|
||||
* 错在调用方控制流把页面滚动让给了控件)。 */
|
||||
if (g_drag_lv && g_lv) {
|
||||
if (g_lv_swipe) { /* 行左滑在途:喂 move;1=横向消费;0=纵向主导→取消本行 swipe */
|
||||
meow_listview_set_scroll(g_lv, g_lv_scroll);
|
||||
if (meow_listview_swipe_move(g_lv, x, y)) return 1;
|
||||
g_lv_swipe = 0;
|
||||
}
|
||||
int dy = y - g_lv_last_y; g_lv_last_y = y; /* 竖向滚动(跟手) */
|
||||
meow_listview_scroll_by(g_lv, -dy);
|
||||
g_lv_scroll = meow_listview_scroll(g_lv);
|
||||
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* swipe UP/DOWN:宫格/列表聚焦时滚自身(del 已按方向加权),1=已消费。 */
|
||||
int sdk2_swipe(int del)
|
||||
{
|
||||
if (!s2_built) return 0;
|
||||
if (g_grid_focus && g_grid) {
|
||||
meow_lazygrid_scroll_by(g_grid, del);
|
||||
g_grid_scroll = meow_lazygrid_scroll(g_grid);
|
||||
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
|
||||
return 1;
|
||||
}
|
||||
if (g_lv_focus && g_lv) {
|
||||
meow_listview_scroll_by(g_lv, del);
|
||||
g_lv_scroll = meow_listview_scroll(g_lv);
|
||||
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 本次手势是否已被本子视图实时滚过(demo_swipe 早退用,防 on_swipe 二次滚动——同 list 的 g_drag_list)。 */
|
||||
int sdk2_dragging(void)
|
||||
{
|
||||
return (g_drag_grid || g_drag_lv) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 子视图生命周期(on_enter/on_leave 由 SDK 递归驱动;on_destroy 清引用) */
|
||||
/* ============================================================ */
|
||||
static void sdk2_page_enter(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
meow_log(MEOW_LOG_INFO, "demo sdk2 view enter");
|
||||
}
|
||||
static void sdk2_page_leave(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
meow_log(MEOW_LOG_INFO, "demo sdk2 view leave");
|
||||
}
|
||||
static void sdk2_page_destroy(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
if (g_sel) { meow_select_free(g_sel); g_sel = NULL; }
|
||||
s2_built = 0;
|
||||
g_sdk2_view = NULL; /* meow_view_free 递归释放前清引用,防悬垂 */
|
||||
}
|
||||
|
||||
meow_view *sdk2_view_create(void)
|
||||
{
|
||||
static const meow_view_hooks h = {
|
||||
.on_enter = sdk2_page_enter,
|
||||
.on_leave = sdk2_page_leave,
|
||||
.on_destroy = sdk2_page_destroy,
|
||||
.on_draw = sdk2_draw_on,
|
||||
};
|
||||
/* id 用非页面值:容器不占 g_view_by_id(那是 page_push/pop 顶层页查找表)。 */
|
||||
g_sdk2_view = meow_view_create(0x2000, MEOW_VIEW_CONTAINER, &h, NULL);
|
||||
/* meow_select 实例一次性创建(控制态随本子视图存活,不受内容重建影响);行标题一次性填充。 */
|
||||
sdk2_init_rows();
|
||||
if (g_sdk2_view && !g_sel) {
|
||||
g_sel = meow_select_create("Sample rate", 0);
|
||||
if (g_sel) {
|
||||
for (int i = 0; i < SEL_N; i++) meow_select_add_item(g_sel, g_sel_items[i]);
|
||||
meow_select_set_on_change(g_sel, sdk2_sel_change, NULL);
|
||||
}
|
||||
}
|
||||
return g_sdk2_view;
|
||||
}
|
||||
meow_view *sdk2_view_get(void)
|
||||
{
|
||||
return g_sdk2_view;
|
||||
}
|
||||
Binary file not shown.
@@ -9,6 +9,7 @@
|
||||
* am history [pkg] → 事件历史(每 App 最近 8 条;无 pkg=全部)
|
||||
* am start <pkg>[/<activity>] → 经 amsd→launcher 拉起
|
||||
* am kill <pkg> → 经 amsd→launcher 终止(SIGTERM 语义)
|
||||
* am rescan → 手动触发注册表重扫(卡刚挂载/热装 App 后立即可见)
|
||||
*
|
||||
* 编译(mipsel 静态):zig-mipsel-cc -Os -Wall -Wextra -static am.c -o am
|
||||
* 宿主:gcc -Os -Wall -Wextra am.c -o am_host
|
||||
@@ -125,10 +126,15 @@ static int cmd_kill(const char *pkg)
|
||||
return am_query(line);
|
||||
}
|
||||
|
||||
static int cmd_rescan(void)
|
||||
{
|
||||
return am_query("rescan\n");
|
||||
}
|
||||
|
||||
static void usage(const char *a0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: %s [--sock <path>] status|list|history [pkg]|start <pkg>[/<act>]|kill <pkg>\n"
|
||||
"usage: %s [--sock <path>] status|list|history [pkg]|start <pkg>[/<act>]|kill <pkg>|rescan\n"
|
||||
" %s resolve <pkg> <activity> | picker <action> | picker-status [reqid]\n"
|
||||
" --sock <path> amsd socket (default %s, or $MEOW_AMS_SOCK)\n",
|
||||
a0, a0, AMS_SOCK_DEFAULT);
|
||||
@@ -181,6 +187,7 @@ int main(int argc, char **argv)
|
||||
if (!strcmp(cmd, "picker-status")) {
|
||||
return cmd_picker_status(arg);
|
||||
}
|
||||
if (!strcmp(cmd, "rescan")) return cmd_rescan();
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
+237
-52
@@ -6,6 +6,8 @@
|
||||
* svcd fork+exec 直接拉起,故本进程【前台常驻】(不 daemonize),与 meow_crash 同款。
|
||||
* - 一期:生命周期监控中心 + 应用注册表。launcher 经 AF_UNIX `/usr/data/meow_ams.sock`(0666)
|
||||
* 上报 launch/exit/crash 事件;`am` CLI 同 socket 查询/下发 start/kill。
|
||||
* - 注册表 = 启动首扫 + 周期合并重扫(卡未挂载扫空 3s 快重试、常态 30s 自愈;
|
||||
* `am rescan` 手动触发)。重扫合并式,绝不清运行时状态(M2.90 真机扫空缺陷修复)。
|
||||
* - 核心目的:从侧面详细抓 App 日志(尤其异常崩溃日志)。launcher 给 App 建 stdout/stderr
|
||||
* 管道,前台循环按行转发(EV OUT);amsd 落盘 `logs/<pkg>/stdout_<ts>_<pid>.log`。
|
||||
* - 崩溃双保险:amsd 检测到异常退出时另以 meow_crash 认识的 `SVC ...` 格式转发
|
||||
@@ -61,10 +63,15 @@
|
||||
#define AMS_MAX_PENDING 8 /* 进行中 picker 请求(结果关联) */
|
||||
#define AMS_TICK_MS 1000
|
||||
#define AMS_CLEANUP_MS 60000
|
||||
#define AMS_RESCAN_MS 30000 /* 常态合并重扫周期(卡热插拔/App 热安装后注册表自愈) */
|
||||
#define AMS_RESCAN_RETRY_MS 3000 /* 扫描源尚未成功打开(开机早于卡挂载扫空)→ 快重试 */
|
||||
#define AMS_PATH_MAX 512
|
||||
#define AMS_SOCK_PATH_MAX 108
|
||||
#define AMS_BOOTTS_LEN 64
|
||||
#define AMS_REQ_MAX 512 /* 命令响应缓冲 */
|
||||
#define AMS_IPC_TIMEOUT_MS 1200 /* launcher IPC 单次收发超时:amsd 绝不为一次转发阻塞无限久 */
|
||||
#define AMS_IPC_RETRY 2 /* launcher 一次性不可达/超时 → 短退避重连一次 */
|
||||
#define AMS_IPC_BACKOFF_MS 250 /* 重连退避基数 */
|
||||
|
||||
/* 每 App 状态(一期:STOPPED/FG;BG 预留二期) */
|
||||
enum { AMS_STOPPED = 0, AMS_FG = 1, AMS_BG = 2 };
|
||||
@@ -84,7 +91,7 @@ struct ams_route {
|
||||
|
||||
struct ams_app {
|
||||
char pkg[MEOW_APP_ID_MAX]; /* app_id=package */
|
||||
char name[17];
|
||||
char name[49]; /* 显示名(UTF-8;截断必须整序列,见 utf8_trim_incomplete) */
|
||||
char exec[129];
|
||||
int known; /* 注册表(app.json 扫描)命中 */
|
||||
int state; /* AMS_* */
|
||||
@@ -367,6 +374,22 @@ static void poll_card(void)
|
||||
}
|
||||
|
||||
/* ---- 注册表扫描(app.json 迷你解析:package/name/exec) ---- */
|
||||
/* 把字符串末尾不完整的 UTF-8 序列砍掉(截断到最后一个完整码点边界)。
|
||||
* 根因记录:name 截断若落在多字节序列中间,下游 JSON 嵌进面板会被浏览器
|
||||
* JSON.parse 以非法 UTF-8 拒收(「非 JSON 响应」事故 2026-09-02)。 */
|
||||
static void utf8_trim_incomplete(char *s)
|
||||
{
|
||||
size_t n = strlen(s), i = 0, last_good = 0;
|
||||
while (i < n) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
int len = (c < 0x80) ? 1 : (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1;
|
||||
if (i + (size_t)len > n) break; /* 不完整序列 */
|
||||
i += (size_t)len;
|
||||
last_good = i;
|
||||
}
|
||||
s[last_good] = '\0';
|
||||
}
|
||||
|
||||
static void appjson_get(const char *content, const char *key, char *out, size_t n)
|
||||
{
|
||||
out[0] = '\0';
|
||||
@@ -387,6 +410,7 @@ static void appjson_get(const char *content, const char *key, char *out, size_t
|
||||
if (l >= n) l = n - 1;
|
||||
memcpy(out, c, l);
|
||||
out[l] = '\0';
|
||||
utf8_trim_incomplete(out);
|
||||
}
|
||||
|
||||
/* §6.2 二期:在 content 二分/扁平解析 route.<id>=<chain>(或 JSON "route.<id>":"<chain>")到 app。
|
||||
@@ -475,14 +499,31 @@ static void ams_parse_pickers(const char *content, const char *app_id)
|
||||
}
|
||||
}
|
||||
|
||||
/* 扫描 --apps 下每个目录(app_id=目录名;name/exec 从 app.json 抽,缺省回退)。 */
|
||||
static void registry_scan(void)
|
||||
/* 扫描源至少成功打开过一次(空表快重试的终止条件;卡未挂载时恒 0)。 */
|
||||
static int g_scan_ok = 0;
|
||||
|
||||
static struct ams_app *app_find(const char *pkg); /* 定义在后(App 表辅助) */
|
||||
|
||||
/* 扫描 --apps 下每个目录(app_id=目录名;name/exec 从 app.json 抽,缺省回退)。
|
||||
* 合并式(merge-preserving):已存在表项只刷新静态字段(known/name/exec/route 表/picker 表),
|
||||
* 运行时状态(state/pid/launches/crashes/history、运行时 DECLARE 并集进来的路由)一律保留;
|
||||
* 新目录追加建项。扫描源不可达(开机早于卡挂载/卡热拔)→ 保留现有表直接返回,绝不清空。
|
||||
* verbose=1(启动/`am rescan`)记全量明细;周期调用仅在结果变化时记一行摘要(防刷屏)。 */
|
||||
static void registry_scan(int verbose)
|
||||
{
|
||||
static int last_found = -1, last_napps = -1, last_npick = -1;
|
||||
DIR *d = opendir(g_apps_dir);
|
||||
if (!d) { emit_log("registry scan: %s not reachable (%s)", g_apps_dir, strerror(errno)); return; }
|
||||
if (!d) {
|
||||
if (verbose || !g_scan_ok)
|
||||
emit_log("registry scan: %s not reachable (%s)", g_apps_dir, strerror(errno));
|
||||
return;
|
||||
}
|
||||
g_scan_ok = 1;
|
||||
for (int i = 0; i < g_napps; i++) g_apps[i].known = 0;
|
||||
for (int i = 0; i < g_npickers; i++) g_pickers[i].used = 0;
|
||||
struct dirent *de;
|
||||
int n = 0;
|
||||
while ((de = readdir(d)) != NULL && n < AMS_MAX_APPS) {
|
||||
int found = 0;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.') continue;
|
||||
char nm[96];
|
||||
scopy(nm, sizeof nm, de->d_name);
|
||||
@@ -503,37 +544,50 @@ static void registry_scan(void)
|
||||
fclose(jf);
|
||||
content[rd] = '\0';
|
||||
}
|
||||
struct ams_app *a = &g_apps[n];
|
||||
memset(a, 0, sizeof *a);
|
||||
/* app_id 权威 = 目录名(APPS.md §2);app.json package 字段作为兜底校验(不一致时用目录名) */
|
||||
scopy(a->pkg, sizeof a->pkg, de->d_name);
|
||||
/* app_id 权威 = 目录名(APPS.md §2);已有表项(含运行时 EV/DECLARE 建的)原位刷新 */
|
||||
struct ams_app *a = app_find(nm);
|
||||
if (!a) {
|
||||
if (g_napps >= AMS_MAX_APPS) continue;
|
||||
a = &g_apps[g_napps++];
|
||||
memset(a, 0, sizeof *a);
|
||||
scopy(a->pkg, sizeof a->pkg, nm);
|
||||
}
|
||||
a->known = 1;
|
||||
char tmp[96];
|
||||
appjson_get(content, "name", tmp, sizeof tmp);
|
||||
if (!tmp[0]) scopy(tmp, sizeof tmp, de->d_name);
|
||||
if (!tmp[0]) scopy(tmp, sizeof tmp, nm);
|
||||
scopy(a->name, sizeof a->name, tmp);
|
||||
utf8_trim_incomplete(a->name); /* scopy 截断同样走整序列边界 */
|
||||
appjson_get(content, "exec", tmp, sizeof tmp);
|
||||
if (tmp[0]) scopy(a->exec, sizeof a->exec, tmp);
|
||||
else {
|
||||
else if (!a->exec[0] || !strcmp(a->exec, "?")) {
|
||||
scopy(a->exec, sizeof a->exec, dir);
|
||||
path_append(a->exec, sizeof a->exec, "/meow");
|
||||
}
|
||||
/* §6.2 二期:路由权威表(静态 app.json route.*)+ picker 表(picker.<action>=<chain>) */
|
||||
ams_parse_routes(content, a);
|
||||
ams_parse_pickers(content, a->pkg);
|
||||
n++;
|
||||
found++;
|
||||
}
|
||||
closedir(d);
|
||||
g_napps = n;
|
||||
emit_log("registry scan: %d app(s) from %s", g_napps, g_apps_dir);
|
||||
for (int i = 0; i < g_napps; i++) {
|
||||
struct ams_app *a = &g_apps[i];
|
||||
emit_log(" app %s name=%s exec=%s routes=%d", a->pkg, a->name, a->exec, a->nroute);
|
||||
for (int r = 0; r < a->nroute; r++)
|
||||
emit_log(" route %s=%s", a->routes[r].act, a->routes[r].chain);
|
||||
if (verbose || found != last_found || g_napps != last_napps || g_npickers != last_npick) {
|
||||
emit_log("registry scan: %d dir(s) from %s, %d app(s) registered",
|
||||
found, g_apps_dir, g_napps);
|
||||
if (verbose) {
|
||||
for (int i = 0; i < g_napps; i++) {
|
||||
struct ams_app *a = &g_apps[i];
|
||||
emit_log(" app %s name=%s exec=%s known=%d routes=%d",
|
||||
a->pkg, a->name, a->exec, a->known, a->nroute);
|
||||
for (int r = 0; r < a->nroute; r++)
|
||||
emit_log(" route %s=%s", a->routes[r].act, a->routes[r].chain);
|
||||
}
|
||||
for (int i = 0; i < g_npickers; i++)
|
||||
if (g_pickers[i].used)
|
||||
emit_log(" picker %s -> %s (chain=%s)",
|
||||
g_pickers[i].action, g_pickers[i].app_id, g_pickers[i].chain);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < g_npickers; i++)
|
||||
emit_log(" picker %s -> %s (chain=%s)", g_pickers[i].action, g_pickers[i].app_id, g_pickers[i].chain);
|
||||
last_found = found; last_napps = g_napps; last_npick = g_npickers;
|
||||
}
|
||||
|
||||
/* ---- App 表辅助 ---- */
|
||||
@@ -730,16 +784,49 @@ static void out_append(const char *pkg, pid_t pid, long epoch, long up, const ch
|
||||
}
|
||||
|
||||
/* ---- launcher IPC 转发(xchg 一帧,读回一帧) ---- */
|
||||
/* 最近一次 ipc_xchg 的失败原因:区分「连不上」与「连上但没及时回」。此前两者都记成
|
||||
* "launcher offline",2026-11-04 真机即误诊——launcher 活着且回了 ACK,只是晚于 1200ms 超时。 */
|
||||
enum { IPC_ERR_NONE = 0, IPC_ERR_CONN, IPC_ERR_IO, IPC_ERR_TIMEOUT, IPC_ERR_PROTO };
|
||||
static int g_ipc_err = IPC_ERR_NONE;
|
||||
|
||||
static const char *ipc_err_str(void)
|
||||
{
|
||||
switch (g_ipc_err) {
|
||||
case IPC_ERR_CONN: return "connect failed";
|
||||
case IPC_ERR_IO: return "io error";
|
||||
case IPC_ERR_TIMEOUT: return "timeout, no reply";
|
||||
case IPC_ERR_PROTO: return "unexpected reply";
|
||||
default: return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
static int ipc_fail(int fd, int err)
|
||||
{
|
||||
if (fd >= 0) close(fd);
|
||||
g_ipc_err = err;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
||||
uint8_t *rbuf, uint32_t rcap)
|
||||
{
|
||||
g_ipc_err = IPC_ERR_NONE;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
if (fd < 0) { g_ipc_err = IPC_ERR_IO; return -1; }
|
||||
/* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞
|
||||
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
|
||||
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
|
||||
* 注意:tv_usec 必须 < 1000000(否则 setsockopt 返回 EINVAL,超时静默失效)。 */
|
||||
struct timeval tv;
|
||||
tv.tv_sec = AMS_IPC_TIMEOUT_MS / 1000;
|
||||
tv.tv_usec = (AMS_IPC_TIMEOUT_MS % 1000) * 1000;
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) return ipc_fail(fd, IPC_ERR_CONN);
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
|
||||
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
|
||||
@@ -750,49 +837,61 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
||||
size_t off = 0;
|
||||
while (off < sizeof hdr) {
|
||||
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
|
||||
if (w <= 0) { close(fd); return -1; }
|
||||
if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
off += (size_t)w;
|
||||
}
|
||||
if (len && payload) {
|
||||
size_t p = 0;
|
||||
while (p < len) {
|
||||
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
|
||||
if (w <= 0) { close(fd); return -1; }
|
||||
if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
p += (size_t)w;
|
||||
}
|
||||
}
|
||||
off = 0;
|
||||
while (off < sizeof hdr) {
|
||||
ssize_t r = read(fd, hdr + off, sizeof hdr - off);
|
||||
if (r <= 0) { close(fd); return -1; }
|
||||
if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
off += (size_t)r;
|
||||
}
|
||||
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
|
||||
memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
|
||||
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) { close(fd); return -1; }
|
||||
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return ipc_fail(fd, IPC_ERR_PROTO);
|
||||
if (rlen > rcap) rlen = rcap;
|
||||
off = 0;
|
||||
while (off < rlen) {
|
||||
ssize_t r = read(fd, rbuf + off, rlen - off);
|
||||
if (r <= 0) { close(fd); return -1; }
|
||||
if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
off += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
return (int)rtype;
|
||||
}
|
||||
|
||||
/* 向 launcher 申请 start/kill。返回 0=launcher 回 code(a_out 输出);-1=launcher 不通。 */
|
||||
/* 向 launcher 申请 start/kill。返回 0=launcher 回 code(a_out 输出);-1=launcher 不通。
|
||||
* launcher 重启窗口(svcd/sysui 重启 launcher、或 amsd 先行启动)内连接会暂不可达/超时:
|
||||
* 这里做「指数式短退避重试」——launcher 数秒内回来即自动恢复转发(无需人工重发)。 */
|
||||
static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uint32_t *code)
|
||||
{
|
||||
uint8_t rbuf[64];
|
||||
int rt = ipc_xchg(type, payload, len, rbuf, sizeof rbuf);
|
||||
if (rt == MSG_ACK) {
|
||||
m_ack_t a;
|
||||
memcpy(&a, rbuf, sizeof a);
|
||||
if (code) *code = a.code;
|
||||
return 0;
|
||||
for (int attempt = 0; attempt < AMS_IPC_RETRY; attempt++) {
|
||||
int rt = ipc_xchg(type, payload, len, rbuf, sizeof rbuf);
|
||||
if (rt == MSG_ACK) {
|
||||
m_ack_t a;
|
||||
memcpy(&a, rbuf, sizeof a);
|
||||
if (code) *code = a.code;
|
||||
return 0;
|
||||
}
|
||||
if (rt == MSG_ERR) { g_ipc_err = IPC_ERR_PROTO; return MEOW_ERR_INTERNAL; }
|
||||
/* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */
|
||||
if (attempt + 1 < AMS_IPC_RETRY)
|
||||
usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000);
|
||||
}
|
||||
if (rt == MSG_ERR) return MEOW_ERR_INTERNAL;
|
||||
if (g_ipc_err == IPC_ERR_NONE) g_ipc_err = IPC_ERR_PROTO; /* 收到非 ACK/ERR 帧:不是「无回执」 */
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -800,10 +899,22 @@ static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uin
|
||||
static void handle_launch(char **t, int nt)
|
||||
{
|
||||
if (nt < 4) return;
|
||||
const char *pkg = t[0], *act = t[1], *chain = t[2];
|
||||
pid_t pid = (pid_t)atoi(t[3]);
|
||||
long epoch = (nt > 4) ? atol(t[4]) : (long)time(NULL);
|
||||
long up = (nt > 5) ? atol(t[5]) : read_uptime_sec();
|
||||
const char *pkg = t[0], *act = t[1], *chain = "-";
|
||||
pid_t pid;
|
||||
long epoch, up;
|
||||
if (nt == 5) {
|
||||
/* 兼容旧格式(M2.90 固件 launcher 实证):EV LAUNCH <pkg> <act> <pid> <epoch> <up>
|
||||
* ——无 chain 字段。不兼容会把 epoch 错当 pid(am status 显示假 pid、
|
||||
* `am kill` 兜底直发 SIGTERM 打错进程),故按 nt==5 识别旧布局。 */
|
||||
pid = (pid_t)atoi(t[2]);
|
||||
epoch = atol(t[3]);
|
||||
up = atol(t[4]);
|
||||
} else {
|
||||
chain = t[2];
|
||||
pid = (pid_t)atoi(t[3]);
|
||||
epoch = (nt > 4) ? atol(t[4]) : (long)time(NULL);
|
||||
up = (nt > 5) ? atol(t[5]) : read_uptime_sec();
|
||||
}
|
||||
struct ams_app *a = app_get(pkg);
|
||||
if (!a) return;
|
||||
a->state = AMS_FG;
|
||||
@@ -874,6 +985,41 @@ static void handle_out(char **t, int nt)
|
||||
out_append(pkg, pid, epoch, up, text);
|
||||
}
|
||||
|
||||
/* 批①后台常驻上报(单向 best-effort,launcher → amsd):
|
||||
* EV BG <app> <pid> → AMS_BG(转后台保活)
|
||||
* EV FG <app> → AMS_FG(回前台)。旧 launcher 不发、本层不识别也不崩。 */
|
||||
static void handle_bg(char **t, int nt)
|
||||
{
|
||||
if (nt < 2) return;
|
||||
const char *pkg = t[0];
|
||||
pid_t pid = (pid_t)atoi(t[1]);
|
||||
long epoch = (nt > 2) ? atol(t[2]) : (long)time(NULL);
|
||||
long up = (nt > 3) ? atol(t[3]) : read_uptime_sec();
|
||||
struct ams_app *a = app_get(pkg);
|
||||
if (!a) return;
|
||||
a->state = AMS_BG;
|
||||
a->pid = pid;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof detail, "pid=%ld", (long)pid);
|
||||
hist_push(a, "bg", detail, epoch, up);
|
||||
emit_log("BG %s pid=%ld", pkg, (long)pid);
|
||||
}
|
||||
|
||||
static void handle_fg(char **t, int nt)
|
||||
{
|
||||
if (nt < 1) return;
|
||||
const char *pkg = t[0];
|
||||
long epoch = (nt > 1) ? atol(t[1]) : (long)time(NULL);
|
||||
long up = (nt > 2) ? atol(t[2]) : read_uptime_sec();
|
||||
struct ams_app *a = app_find(pkg);
|
||||
if (!a) return;
|
||||
a->state = AMS_FG;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof detail, "bg->fg");
|
||||
hist_push(a, "fg", detail, epoch, up);
|
||||
emit_log("FG %s", pkg);
|
||||
}
|
||||
|
||||
/* ---- §6.2 二期:picker 编排(amsd 路由权威) ---- */
|
||||
|
||||
static struct ams_picker *picker_find(const char *action)
|
||||
@@ -996,6 +1142,13 @@ static int cmd_picker_status(const char *reqstr, char *resp, int rn)
|
||||
return l;
|
||||
}
|
||||
|
||||
/* `am rescan`:手动触发注册表重扫(卡刚挂载/热装 App 后立即可见,不等 30s 周期)。 */
|
||||
static int cmd_rescan(char *resp, int rn)
|
||||
{
|
||||
registry_scan(1);
|
||||
return snprintf(resp, rn, "OK rescan %d app(s)\n", g_napps);
|
||||
}
|
||||
|
||||
/* `am resolve <pkg> <act>`:查 amsd 权威路由表。 */
|
||||
static int cmd_resolve(const char *pkg, const char *act, char *resp, int rn)
|
||||
{
|
||||
@@ -1100,12 +1253,18 @@ static int cmd_start(const char *spec, char *resp, int rn)
|
||||
hist_push(a, "ams_start", "intent (forwarded)", (long)time(NULL), read_uptime_sec());
|
||||
snprintf(detail, sizeof detail, "ok forwarded");
|
||||
emit_log("AMS start %s%s forwarded to launcher", pkg, act[0] ? "/" : "");
|
||||
} else if (rt == 0 && code == (uint32_t)MEOW_ERR_BUSY) {
|
||||
} else if (rt == 0 && code == (uint32_t)(-MEOW_ERR_BUSY)) {
|
||||
/* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */
|
||||
snprintf(detail, sizeof detail, "busy (app running)");
|
||||
emit_log("AMS start %s -> BUSY (app running)", pkg);
|
||||
} else if (rt == 0) {
|
||||
snprintf(detail, sizeof detail, "launcher refused code=%d", (int)code);
|
||||
emit_log("AMS start %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
|
||||
} else {
|
||||
snprintf(detail, sizeof detail, "launcher offline");
|
||||
emit_log("AMS start %s -> launcher offline", pkg);
|
||||
/* rt!=0 只代表「这次没拿到回执」,不等于 launcher 死了(同 cmd_kill 注释)。
|
||||
* 响应串保留 "launcher offline"(MeowPanel 据此映射 503),日志补真实原因。 */
|
||||
snprintf(detail, sizeof detail, "launcher offline (%s)", ipc_err_str());
|
||||
emit_log("AMS start %s -> launcher offline (%s)", pkg, ipc_err_str());
|
||||
}
|
||||
snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR",
|
||||
pkg, act[0] ? act : "(default)");
|
||||
@@ -1126,14 +1285,21 @@ static int cmd_kill(const char *pkg, char *resp, int rn)
|
||||
uint32_t code = 0;
|
||||
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
|
||||
if (rt != 0 && a->pid > 0) {
|
||||
/* 兜底:launcher 不在,直接 SIGTERM(与 launcher 的 SIGTERM 语义一致) */
|
||||
/* 兜底:launcher 没给回执 → 直接 SIGTERM(与 launcher 的 SIGTERM 语义一致)。
|
||||
* 注意 rt!=0 **不等于 launcher 死了**:主循环被 IPC 洪泛拖慢时 ACK 会晚于
|
||||
* AMS_IPC_TIMEOUT_MS,故日志写实际原因(connect failed / timeout, no reply / ...),
|
||||
* 而不是笼统的 "launcher offline"(2026-11-04 真机即此误判)。 */
|
||||
kill(a->pid, SIGTERM);
|
||||
emit_log("AMS kill %s -> direct SIGTERM (launcher offline)", pkg);
|
||||
emit_log("AMS kill %s -> direct SIGTERM (launcher %s)", pkg, ipc_err_str());
|
||||
} else if (rt != 0) {
|
||||
return snprintf(resp, rn, "ERR launcher offline: %s\n", pkg);
|
||||
/* 响应串保留 "launcher offline" 字样(MeowPanel api_ams_ctl 依此映射 503 AMS_UNAVAILABLE),
|
||||
* 只在括号里补真实原因,便于区分「连不上」与「连上但超时」。 */
|
||||
return snprintf(resp, rn, "ERR launcher offline (%s): %s\n", ipc_err_str(), pkg);
|
||||
} else if (code != MEOW_OK) {
|
||||
emit_log("AMS kill %s -> launcher refused code=%u", pkg, code);
|
||||
return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code);
|
||||
/* launcher 明确拒绝:不改判为直杀(pid 可能已被回收,盲杀会打错进程),如实回错。
|
||||
* code 是负错误码的 uint32 呈现(-2 → 4294967294),括号里给有符号值便于对照。 */
|
||||
emit_log("AMS kill %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
|
||||
return snprintf(resp, rn, "ERR launcher refused: %s (code=%d)\n", pkg, (int)code);
|
||||
}
|
||||
hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec());
|
||||
int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
|
||||
@@ -1162,6 +1328,8 @@ static int dispatch_line(char *line, char *resp, int rn)
|
||||
if (!strcmp(tok[0], "LAUNCH")) { handle_launch(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "EXIT")) { handle_exit(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "OUT")) { handle_out(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "BG")) { handle_bg(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "FG")) { handle_fg(tok + 1, nt - 1); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1199,13 +1367,14 @@ static int dispatch_line(char *line, char *resp, int rn)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 命令:status/list/history/start/kill/resolve/picker/picker-status */
|
||||
/* 命令:status/list/history/start/kill/resolve/picker/picker-status/rescan */
|
||||
char *save = NULL;
|
||||
char *cmd = strtok_r(line, " ", &save);
|
||||
if (!cmd) return 0;
|
||||
char *arg = strtok_r(NULL, " ", &save);
|
||||
if (!strcmp(cmd, "status")) return cmd_status(resp, rn) > 0;
|
||||
if (!strcmp(cmd, "list")) return cmd_list(resp, rn) > 0;
|
||||
if (!strcmp(cmd, "rescan")) return cmd_rescan(resp, rn) > 0;
|
||||
if (!strcmp(cmd, "history")) {
|
||||
if (arg && arg[0]) cmd_history(arg, resp, rn);
|
||||
else cmd_history(NULL, resp, rn);
|
||||
@@ -1350,7 +1519,7 @@ static int run_daemon(void)
|
||||
boot_ts(g_bootts, sizeof g_bootts);
|
||||
log_open_card();
|
||||
log_cleanup_old();
|
||||
registry_scan();
|
||||
registry_scan(1); /* 启动首扫:卡未挂载扫空也照常常驻,主循环 3s 快重试兜底 */
|
||||
|
||||
fprintf(stderr, "[meow_amsd] up: sock=%s ipc=%s apps=%s card=%s bootts=%s log=%s\n",
|
||||
g_sock, g_ipc_sock, g_apps_dir, g_card, g_bootts,
|
||||
@@ -1362,8 +1531,13 @@ static int run_daemon(void)
|
||||
for (int i = 0; i < AMS_MAX_CONNS; i++) conns[i].fd = -1;
|
||||
int nfds = 2;
|
||||
|
||||
unsigned long long last_cleanup = 0;
|
||||
unsigned long long last_cleanup = 0, last_rescan = 0;
|
||||
while (!g_shutdown) {
|
||||
/* 每圈清零:谨防上一轮已关闭连接残留的 POLLHUP/POLLERR 落在 pfds[2+i] 上。
|
||||
* 若不清,accept 后同一轮就用「陈旧 revents」处理新连接——误判其 HUP/ERR
|
||||
* 而提前 close(am CLI 收到 SIGPIPE、命令被丢弃),launcher 重启后 EV 事件
|
||||
* 连接密集必触发,表现为 `am start` 不再转发到 launcher。 */
|
||||
memset(pfds, 0, sizeof pfds);
|
||||
pfds[0].fd = g_listen_fd; pfds[0].events = POLLIN; pfds[0].revents = 0;
|
||||
pfds[1].fd = g_sigpipe[0]; pfds[1].events = POLLIN; pfds[1].revents = 0;
|
||||
for (int i = 0; i < nfds - 2; i++) {
|
||||
@@ -1424,6 +1598,17 @@ static int run_daemon(void)
|
||||
poll_card();
|
||||
log_cleanup_old();
|
||||
}
|
||||
/* 注册表自愈重扫:扫描源从未成功打开(开机早于卡挂载扫空,M2.90 真机实证
|
||||
* 「registry scan: MeowAPPs not reachable」后注册表永久为空 → am start 报
|
||||
* pkg not registered)→ 3s 快重试直到扫到;常态 30s 合并重扫(卡热插拔 /
|
||||
* App 热安装自愈,对齐 launcher 的应用列表目录热重载)。合并式重扫不清
|
||||
* 运行时状态,见 registry_scan 注释。 */
|
||||
{
|
||||
unsigned long long nm = now_ms();
|
||||
unsigned long long iv = g_scan_ok ? (unsigned long long)AMS_RESCAN_MS
|
||||
: (unsigned long long)AMS_RESCAN_RETRY_MS;
|
||||
if (nm - last_rescan >= iv) { last_rescan = nm; registry_scan(0); }
|
||||
}
|
||||
}
|
||||
|
||||
if (g_listen_fd >= 0) close(g_listen_fd);
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,138 @@
|
||||
/* bt_dev.c — 蓝牙 A2DP 设备自识别(纯逻辑实现,见 bt_dev.h 契约)。
|
||||
*
|
||||
* 纯 libc(string / stdlib),无任何 meow_* 调用、无 popen、无文件 I/O —— meow_audiod 负责
|
||||
* 读取 /usr/data/alsa.conf、执行 `bluealsa-cli list-pcms` 及写回。宿主单测零链路重。
|
||||
*/
|
||||
#include "bt_dev.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
||||
* 端点形如 `a2dpsrc/AA:BB:CC:DD:EE:FF`(MAC 在端点 token 内最后一个 '/' 之后到空白前)。
|
||||
* 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1
|
||||
* "profile/aptx" 与行 3 "a2dp_source/..." 的经典误判)。 */
|
||||
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
||||
{
|
||||
if (!listpcms || !mac || macsz < 2) return -1;
|
||||
mac[0] = 0;
|
||||
const char *ln = listpcms;
|
||||
while (ln && *ln) {
|
||||
const char *nl = strchr(ln, '\n');
|
||||
const char *le = nl ? nl : (ln + strlen(ln));
|
||||
size_t linelen = (size_t)(le - ln);
|
||||
char line[256];
|
||||
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||||
memcpy(line, ln, linelen); line[linelen] = 0;
|
||||
const char *p = line;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
const char *hit = strstr(p, "a2dpsrc");
|
||||
if (!hit) hit = strstr(p, "a2dp_source");
|
||||
if (hit) {
|
||||
const char *te = hit;
|
||||
while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++;
|
||||
const char *slash = NULL;
|
||||
for (const char *q = hit; q < te; q++) if (*q == '/') slash = q;
|
||||
if (slash && (slash + 1) < te) {
|
||||
const char *start = slash + 1;
|
||||
size_t len = (size_t)(te - start);
|
||||
if (len > 0 && len < macsz) {
|
||||
memcpy(mac, start, len);
|
||||
mac[len] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!nl) break;
|
||||
ln = nl + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 在单行内把 anchor("DEV=" / "device")后的 MAC 值替换为 mac,保留引号风格。
|
||||
* 行未含 anchor → 返回 1(out=原样);含 anchor → 写回新行并返回 0。 */
|
||||
static int line_replace_mac(char *out, size_t outsz, const char *line,
|
||||
const char *anchor, const char *mac)
|
||||
{
|
||||
const char *at = strstr(line, anchor);
|
||||
if (!at) { snprintf(out, outsz, "%s", line); return 1; }
|
||||
const char *vp = at + strlen(anchor);
|
||||
while (*vp == ' ' || *vp == '\t') vp++;
|
||||
if (*vp == '"') vp++; /* 引号风格:值可带双引号 */
|
||||
const char *ve = vp;
|
||||
while (*ve && *ve != ' ' && *ve != '\t' && *ve != '\n' &&
|
||||
*ve != '"' && *ve != '{' && *ve != '}') ve++;
|
||||
size_t prefix = (size_t)(vp - line); /* 前段(含开引号,若带) */
|
||||
size_t tail = (size_t)(ve - line); /* 值终点(不含闭引号,若带) */
|
||||
/* 前段 + mac + 尾段(尾段保留闭引号与其后内容) */
|
||||
snprintf(out, outsz, "%.*s%s%s", (int)prefix, line, mac, line + tail);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 幂等改写 alsa.conf:只把 `pcm.bluealsa` 块内 `DEV=` 与 `device` 后的 MAC 换成 mac。
|
||||
* 逐行处理;无 pcm.bluealsa / 无 DEV= / device token → -1;两端已一致 → 1;改了 → 0。 */
|
||||
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
|
||||
char **out, size_t *out_len)
|
||||
{
|
||||
if (!in || !mac || !mac[0] || !out || !out_len || in_len == 0) return -1;
|
||||
if (!strstr(in, "pcm.bluealsa") ||
|
||||
(!strstr(in, "DEV=") && !strstr(in, "device"))) return -1;
|
||||
|
||||
size_t cap = in_len + 128;
|
||||
char *res = malloc(cap ? cap : 1);
|
||||
if (!res) return -1;
|
||||
size_t o = 0, changed = 0;
|
||||
const char *p = in, *end = in + in_len;
|
||||
|
||||
while (p < end) {
|
||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char *le = nl ? nl : end;
|
||||
size_t linelen = (size_t)(le - p);
|
||||
char line[512], newline[512];
|
||||
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||||
memcpy(line, p, linelen); line[linelen] = 0;
|
||||
|
||||
const char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++;
|
||||
int anchored = 0;
|
||||
if (strstr(k, "DEV=")) {
|
||||
line_replace_mac(newline, sizeof newline, line, "DEV=", mac);
|
||||
anchored = 1;
|
||||
} else if (strncmp(k, "device", 6) == 0 &&
|
||||
(k[6] == ' ' || k[6] == '\t' || k[6] == 0)) {
|
||||
line_replace_mac(newline, sizeof newline, line, "device", mac);
|
||||
anchored = 1;
|
||||
}
|
||||
if (anchored) {
|
||||
if (strcmp(line, newline) != 0) changed = 1;
|
||||
size_t n = strlen(newline);
|
||||
if (o + n + 2 > cap) {
|
||||
cap = o + n + 256;
|
||||
char *t = realloc(res, cap);
|
||||
if (!t) { free(res); return -1; }
|
||||
res = t;
|
||||
}
|
||||
memcpy(res + o, newline, n); o += n;
|
||||
if (nl) res[o++] = '\n';
|
||||
} else {
|
||||
size_t n = linelen;
|
||||
if (o + n + 2 > cap) {
|
||||
cap = o + n + 256;
|
||||
char *t = realloc(res, cap);
|
||||
if (!t) { free(res); return -1; }
|
||||
res = t;
|
||||
}
|
||||
memcpy(res + o, p, n); o += n;
|
||||
if (nl) res[o++] = '\n';
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
|
||||
if (!changed) { free(res); return 1; }
|
||||
res[o] = 0;
|
||||
*out = res;
|
||||
*out_len = o;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* bt_dev.h — 蓝牙 A2DP 设备自识别(DEV 自动更新纯逻辑,宿主可单测)。
|
||||
*
|
||||
* 背景:/usr/data/alsa.conf 的 `pcm.bluealsa`(plug→bluealsa)需把 DEV/device 钉在当前
|
||||
* 连接的 A2DP sink 设备 MAC 上才能正确出声。本模块提供两段纯逻辑(无 meow_* / popen / 文件
|
||||
* I/O —— meow_audiod 负责读文件与执行系统命令):
|
||||
* - bt_dev_detect_mac:从 `bluealsa-cli list-pcms` 输出提取第一个 a2dpsrc 端点的设备 MAC;
|
||||
* - bt_dev_rewrite:幂等改写 alsa.conf 文本,把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
|
||||
* `device <mac>` 两处换成目标 MAC(保留引号风格 / codec / 其余 token)。
|
||||
* 纯 libc(string / stdlib),独立编译,宿主零链接重。
|
||||
*/
|
||||
#ifndef BT_DEV_H
|
||||
#define BT_DEV_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
||||
* 返回 0=找到并写入 mac(NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。macsz 建议 ≥18。 */
|
||||
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz);
|
||||
|
||||
/* 幂等改写 alsa.conf 文本(in_len 不含 NUL):把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
|
||||
* `device <mac>` 两处 MAC 换成 mac(保留引号风格、codec/其它 token 原样)。
|
||||
* 返回 0=已改动(*out 与 *out_len 给出新文本,调用方负责 free);1=无需改动(两端已一致);
|
||||
* -1=无 pcm.bluealsa / 无 DEV= 或 device token / 参数非法(不动)。 */
|
||||
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
|
||||
char **out, size_t *out_len);
|
||||
|
||||
#endif /* BT_DEV_H */
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
# build_audiod.sh — meow_audiod 交叉编译(mipsel glibc-dynamic)+ 宿主多目标可选。
|
||||
#
|
||||
# 目标:R3 Pro II / MeowOS(MIPS32 o32,真实 ABI)。audiod 复用 meow_ipc.h(共享协议头,
|
||||
# -I../meow_launcherd)。audiod 为 svcd 托管的独立守护;meow_audio 为用户侧 CLI。
|
||||
#
|
||||
# 【P2 蓝牙链接定案 2026-09-03】设备为 glibc-2.22(ld-2.22.so / libc-2.22.so,非 musl),且
|
||||
# libasound 会在运行时 dlopen 单独的 bluealsa pcm 插件(/usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so)
|
||||
# ——故 audiod 必须以**动态 glibc** 形态链接设备现有 libasound.so.2:
|
||||
# - 用 zig --target=mipsel-linux-gnueabihf.2.22(硬浮点 o32,glibc-2.22 版本引用,设备 libs 齐);
|
||||
# libasound 原型在 meow_audiod.c 内联声明(不依赖 asoundlib.h)。
|
||||
# - 删 -static:musl-static 无 dlopen(实测 "Dynamic loading not supported",无法加载 bluealsa 插件);
|
||||
# musl-dynamic 需设备不存在的 musl ld.so。libasound 链接库来自 libbt/(设备 libasound.so.2 副本)。
|
||||
# - meow_audio CLI 无 libasound 依赖,保持 musl-static(zig-mipsel-cc)。
|
||||
#
|
||||
# 用法:
|
||||
# ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel glibc-dynamic,BT 直链 libasound)
|
||||
# ./build_audiod.sh cli # 交叉编译 meow_audio CLI → meow_audio(mipsel musl-static)
|
||||
# ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
ZIG="$ROOT/.tools/zig-env/bin/zig"
|
||||
INC="-I$ROOT/patch_v2/meow_launcherd"
|
||||
|
||||
if [ "$1" = "host" ]; then
|
||||
HOST_CC="${CC_HOST:-cc}"
|
||||
# BT 目标直链宿主 libasound.so.2(无 asoundlib.h,原型内联于 meow_audiod.c;-l: 精确到版本,
|
||||
# 不依赖 unversioned dev 链接)。删 -ldl:BT 不再用 dlopen。
|
||||
"$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -l:libasound.so.2
|
||||
"$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm
|
||||
"$HOST_CC" $INC -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host -lm
|
||||
"$HOST_CC" -Os -Wall -Wextra test_bt_dev.c bt_dev.c -o test_bt_dev -lm
|
||||
echo "== host builds =="
|
||||
ls -la meow_audiod_host meow_audio_host test_audiod_host test_bt_dev
|
||||
file meow_audiod_host meow_audio_host test_audiod_host test_bt_dev
|
||||
echo "== host regression (test_bt_dev: BT DEV auto-detect) =="
|
||||
./test_bt_dev || { echo "!! test_bt_dev FAILED"; exit 1; }
|
||||
echo "== host regression (test_audiod_host) =="
|
||||
./test_audiod_host || { echo "!! host regression FAILED"; exit 1; }
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" = "cli" ]; then
|
||||
"$CC" $INC -Os -Wall -Wextra -static meow_audio.c -o meow_audio -lm
|
||||
echo "== meow_audio (mipsel static CLI) =="
|
||||
ls -la meow_audio
|
||||
md5sum meow_audio
|
||||
file meow_audio
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# meow_audiod:mipsel glibc-dynamic,BT 直链 libasound(-Llibbt -lasound)。-lpthread 供播放线程。
|
||||
# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件镜像自拷贝(idempotent)。
|
||||
LIBBT="libbt"
|
||||
ASOUND_SRC="$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0"
|
||||
mkdir -p "$LIBBT"
|
||||
if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi
|
||||
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so"
|
||||
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2"
|
||||
"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod \
|
||||
-lpthread -Llibbt -lasound
|
||||
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
|
||||
ls -la meow_audiod
|
||||
md5sum meow_audiod
|
||||
file meow_audiod
|
||||
echo "-- dynamic NEEDED --"
|
||||
readelf -d meow_audiod 2>/dev/null | grep -iE "needed" || true
|
||||
echo "-- glibc version refs (max must be <= 2.22) --"
|
||||
readelf --version-info meow_audiod 2>/dev/null | grep -oE "GLIBC_2\.[0-9]+" | sort -uV | tail -3 || true
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,454 @@
|
||||
#define _GNU_SOURCE
|
||||
|
||||
/* meow_audio.c — meow_audiod 用户侧 CLI(root 免鉴权,ssh 直呼)。
|
||||
*
|
||||
* 命令:
|
||||
* meow_audio status 当前流/参数/帧数/underrun/路由/音量
|
||||
* meow_audio tone [freq] [ms] [rate] 正弦测试音(默认 440Hz 500ms 44100;排查无声不须开 App)
|
||||
* meow_audio route <0|2|3> 手动切路由(0=auto 2=3.5 3=4.4;调试/面板)
|
||||
* meow_audio vol [0-100] 设置音量(无参=查询;v1 透传写 DAC numid=1/2 + settings.conf)
|
||||
* meow_audio output <dac|bt> 输出目标(P2 蓝牙:bt=A2DP source,音量走 AVRCP 不下发 DAC 反转)
|
||||
* meow_audio monitor [秒] 终端实时电平表(rms/peak dBFS + 文本电平条,200ms 刷新)
|
||||
*
|
||||
* 传输:AF_UNIX SOCK_STREAM 到 audiod socket(env MEOW_AUDIO_SOCK / --sock 覆盖,宿主/qemu)。
|
||||
* 帧格式复用 meow_ipc.h 16B 头 + m_audio_* 负载。无 audiod → 连接失败/超时 → 打印友好错误并退出。
|
||||
*
|
||||
* 编译(mipsel 静态):build_audiod.sh cli。宿主编译:build_audiod.sh host。
|
||||
*/
|
||||
#include "meow_ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#define AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock"
|
||||
|
||||
static char g_sock[108] = AUD_SOCK_DEFAULT;
|
||||
static int g_fd = -1;
|
||||
|
||||
static int write_all(int fd, const uint8_t *p, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t w = write(fd, p + off, n - off);
|
||||
if (w < 0) { if (errno == EINTR) continue; return -1; }
|
||||
if (w == 0) return -1;
|
||||
off += (size_t)w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_all(int fd, uint8_t *p, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t r = read(fd, p + off, n - off);
|
||||
if (r < 0) { if (errno == EINTR) continue; return -1; }
|
||||
if (r == 0) return -1;
|
||||
off += (size_t)r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 连接 audiod:connect 非阻塞 + poll 2s 有界(对端 backlog 满/wedge 时绝不永久阻塞);
|
||||
* 建连后 SO_SNDTIMEO 2s(tone 长写在 audiod 停摆时报错退出而非挂死)。 */
|
||||
static int connect_sock(void)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock);
|
||||
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
|
||||
if (r < 0 && errno == EINPROGRESS) {
|
||||
struct pollfd p;
|
||||
p.fd = fd; p.events = POLLOUT; p.revents = 0;
|
||||
if (poll(&p, 1, 2000) > 0) {
|
||||
int err = 0;
|
||||
socklen_t el = sizeof err;
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
|
||||
} else {
|
||||
r = -1;
|
||||
}
|
||||
}
|
||||
if (r < 0) { close(fd); return -1; }
|
||||
if (fl >= 0) fcntl(fd, F_SETFL, fl);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 2; tv.tv_usec = 0;
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC;
|
||||
uint16_t ver = MEOW_IPC_VERSION;
|
||||
memcpy(hdr + 0, &magic, 4);
|
||||
memcpy(hdr + 4, &ver, 2);
|
||||
memcpy(hdr + 6, &type, 2);
|
||||
memcpy(hdr + 8, &req_id, 4);
|
||||
memcpy(hdr + 12, &len, 4);
|
||||
if (write_all(fd, hdr, sizeof hdr) != 0) return -1;
|
||||
if (len && payload && write_all(fd, payload, len) != 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* 读一帧头部+负载;返回负载长(≥0),-1=断/坏帧/超时。 */
|
||||
static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
if (read_all(fd, hdr, sizeof hdr) != 0) return -1;
|
||||
uint32_t magic, len; uint16_t ver;
|
||||
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2);
|
||||
memcpy(&len, hdr + 12, 4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||||
if (len > rcap) len = rcap;
|
||||
if (len > 0 && read_all(fd, rbuf, len) != 0) return -1;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
/* 请求-响应:发一帧、读响应(带 3s 超时防挂)。返回响应负载长;-1=失败。 */
|
||||
static int req_resp(uint16_t type, const void *pl, uint32_t len,
|
||||
uint16_t *otype, uint8_t *obuf, uint32_t ocap)
|
||||
{
|
||||
if (send_frame(g_fd, type, 1, pl, len) != 0) return -1;
|
||||
struct pollfd p;
|
||||
p.fd = g_fd; p.events = POLLIN; p.revents = 0;
|
||||
int r = poll(&p, 1, 3000);
|
||||
if (r <= 0 || !(p.revents & POLLIN)) return -1;
|
||||
return read_frame(g_fd, otype, obuf, ocap);
|
||||
}
|
||||
|
||||
static int fmt_bps(int fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case AUDIO_FMT_S16: return 2;
|
||||
case AUDIO_FMT_S24:
|
||||
case AUDIO_FMT_S32:
|
||||
case AUDIO_FMT_FLOAT:
|
||||
case AUDIO_FMT_DOP: return 4;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
static const char *fmt_name(int fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case AUDIO_FMT_S16: return "S16";
|
||||
case AUDIO_FMT_S24: return "S24";
|
||||
case AUDIO_FMT_S32: return "S32";
|
||||
case AUDIO_FMT_FLOAT: return "FLOAT";
|
||||
case AUDIO_FMT_DOP: return "DoP";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static int cmd_status(void)
|
||||
{
|
||||
uint8_t rbuf[MEOW_IPC_MAX];
|
||||
uint16_t rt;
|
||||
int n = req_resp(MSG_AUDIO_QUERY, NULL, 0, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s), status unavailable\n", g_sock); return 1; }
|
||||
if (rt != MSG_AUDIO_STATE) { fprintf(stderr, "meow_audio: unexpected reply type 0x%04x\n", rt); return 1; }
|
||||
rbuf[n < (int)sizeof rbuf - 1 ? n : (int)sizeof rbuf - 1] = 0;
|
||||
fputs((const char *)rbuf, stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- monitor:终端实时电平表(200ms 刷新,文本电平条;数据源=MSG_AUDIO_STATE 的 rms/peak 键) ---- */
|
||||
static volatile sig_atomic_t g_mon_stop = 0;
|
||||
static void on_mon_sig(int sig) { (void)sig; g_mon_stop = 1; }
|
||||
|
||||
/* 从 STATE 文本取整型字段(key 含 '=',如 "rms=";缺省 def) */
|
||||
static int txt_get_int(const char *t, const char *key, int def)
|
||||
{
|
||||
const char *p = strstr(t, key);
|
||||
return p ? atoi(p + strlen(key)) : def;
|
||||
}
|
||||
static void txt_get_str(const char *t, const char *key, char *out, size_t n)
|
||||
{
|
||||
const char *p = strstr(t, key);
|
||||
if (!p) { snprintf(out, n, "-"); return; }
|
||||
p += strlen(key);
|
||||
size_t k = 0;
|
||||
while (p[k] && p[k] != ' ' && p[k] != '\n' && p[k] != '\t' && k < n - 1) { out[k] = p[k]; k++; }
|
||||
out[k] = 0;
|
||||
}
|
||||
/* deci-dB → "-13.1"(地板显 "-inf") */
|
||||
static void fmt_db10(char *out, size_t n, int v)
|
||||
{
|
||||
if (v <= -990) { snprintf(out, n, " -inf"); return; }
|
||||
int neg = v < 0;
|
||||
int a = neg ? -v : v;
|
||||
snprintf(out, n, "%s%3d.%d", neg ? "-" : " ", a / 10, a % 10);
|
||||
}
|
||||
/* 文本电平条:-60..0 dBFS → 0..30 格 */
|
||||
static void print_bar(int db10)
|
||||
{
|
||||
int cells = 0;
|
||||
if (db10 > -600) cells = (db10 + 600) * 30 / 600;
|
||||
if (cells > 30) cells = 30;
|
||||
putchar('[');
|
||||
for (int i = 0; i < 30; i++) putchar(i < cells ? '#' : '-');
|
||||
putchar(']');
|
||||
}
|
||||
|
||||
static int cmd_monitor(int secs)
|
||||
{
|
||||
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
|
||||
signal(SIGINT, on_mon_sig);
|
||||
signal(SIGTERM, on_mon_sig);
|
||||
printf("monitor: 5Hz 刷新,%s(Ctrl-C 退出)\n", secs > 0 ? "定时自动停" : "持续监视");
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
uint64_t t0 = (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
|
||||
uint8_t rbuf[MEOW_IPC_MAX];
|
||||
uint16_t rt;
|
||||
while (!g_mon_stop) {
|
||||
int n = req_resp(MSG_AUDIO_QUERY, NULL, 0, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0 || rt != MSG_AUDIO_STATE) {
|
||||
fprintf(stderr, "\nmeow_audio: query failed (audiod died?)\n");
|
||||
return 1;
|
||||
}
|
||||
rbuf[n < (int)sizeof rbuf - 1 ? n : (int)sizeof rbuf - 1] = 0;
|
||||
const char *txt = (const char *)rbuf;
|
||||
int active = txt_get_int(txt, "active=", 0);
|
||||
int rms = txt_get_int(txt, "rms=", -990);
|
||||
int pk = txt_get_int(txt, "peak=", -990);
|
||||
int vol = txt_get_int(txt, "vol=", -1);
|
||||
int port = txt_get_int(txt, "port=", -1);
|
||||
int rate = txt_get_int(txt, "rate=", 0);
|
||||
char app[40];
|
||||
txt_get_str(txt, "app=", app, sizeof app);
|
||||
char rmsb[16], pkb[16];
|
||||
fmt_db10(rmsb, sizeof rmsb, rms);
|
||||
fmt_db10(pkb, sizeof pkb, pk);
|
||||
if (active) {
|
||||
printf("\rLIVE rms=%sdBFS peak=%sdBFS ", rmsb, pkb);
|
||||
print_bar(pk);
|
||||
printf(" %s %dHz port=%d vol=%d%% ", app, rate, port, vol);
|
||||
} else {
|
||||
printf("\rIDLE 无活跃流(等待播放…) ");
|
||||
}
|
||||
fflush(stdout);
|
||||
if (secs > 0) {
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
uint64_t now = (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
|
||||
if (now - t0 >= (uint64_t)secs * 1000) break;
|
||||
}
|
||||
usleep(200000); /* 200ms 刷新 */
|
||||
}
|
||||
putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_route(int port)
|
||||
{
|
||||
m_audio_route_t r; r.port = (uint32_t)port;
|
||||
uint8_t rbuf[256]; uint16_t rt;
|
||||
int n = req_resp(MSG_AUDIO_ROUTE, &r, sizeof r, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
|
||||
printf("route %d -> %s\n", port, (port == 0) ? "auto" : (port == 3 ? "4.4" : "3.5"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_vol(int pct)
|
||||
{
|
||||
m_audio_volume_t v; v.pct = (int32_t)pct;
|
||||
uint8_t rbuf[256]; uint16_t rt;
|
||||
int n = req_resp(MSG_AUDIO_VOLUME, &v, sizeof v, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0 || rt != MSG_ACK) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
|
||||
m_ack_t a; memcpy(&a, rbuf, sizeof a);
|
||||
printf("vol %u%%%s\n", (unsigned)a.status, (pct >= 0 ? " (set)" : " (query)"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* P2 蓝牙:切换输出目标(0=DAC 1=BT)。audiod 侧重配 PCM;BT 时音量走 AVRCP,不下发 DAC 反转。 */
|
||||
static int cmd_output(int target)
|
||||
{
|
||||
m_audio_output_t o; o.target = (uint32_t)target;
|
||||
uint8_t rbuf[256]; uint16_t rt;
|
||||
int n = req_resp(MSG_AUDIO_OUTPUT, &o, sizeof o, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0 || rt != MSG_ACK) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
|
||||
m_ack_t a; memcpy(&a, rbuf, sizeof a);
|
||||
printf("output %s\n", a.status == AUDIO_OUT_BT ? "bt" : "dac");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 正弦生成:填 nframes 帧(grant 交织格式)到 buf(上限 bufcap,帧对齐)。
|
||||
* start 为起始帧号;返回实际写入字节数。 */
|
||||
static size_t gen_sine(uint8_t *buf, size_t bufcap, int grant_fmt, int grant_ch,
|
||||
double freq, int rate, size_t start, size_t nframes, double amp)
|
||||
{
|
||||
int bps = fmt_bps(grant_fmt);
|
||||
size_t fs = (size_t)grant_ch * (size_t)bps;
|
||||
if (fs == 0) fs = 1;
|
||||
size_t n = (bufcap / fs) * fs;
|
||||
if (n / fs > nframes) n = nframes * fs;
|
||||
size_t done = 0;
|
||||
for (size_t i = 0; i < n / fs; i++) {
|
||||
double ph = 2.0 * M_PI * freq * (double)(start + i) / (double)rate;
|
||||
double s = sin(ph) * amp;
|
||||
long v = (long)(s * 32767.0);
|
||||
if (v > 32767) v = 32767;
|
||||
if (v < -32768) v = -32768;
|
||||
long vs = v; /* S16 值 */
|
||||
if (grant_fmt == AUDIO_FMT_S32 || grant_fmt == AUDIO_FMT_S24) vs = v << 16;
|
||||
for (int c = 0; c < grant_ch; c++) {
|
||||
if (grant_fmt == AUDIO_FMT_S16) {
|
||||
int16_t xx = (int16_t)v;
|
||||
memcpy(buf + done, &xx, 2); done += 2;
|
||||
} else {
|
||||
int32_t xx = (int32_t)(vs);
|
||||
memcpy(buf + done, &xx, 4); done += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
static int cmd_tone(int freq, int ms, int rate, int fmt)
|
||||
{
|
||||
m_audio_open_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "audio-cli");
|
||||
o.rate = (uint32_t)rate;
|
||||
o.ch = 2;
|
||||
o.fmt = (uint16_t)fmt;
|
||||
uint8_t rbuf[512]; uint16_t rt;
|
||||
int n = req_resp(MSG_AUDIO_OPEN, &o, sizeof o, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
|
||||
if (rt != MSG_AUDIO_OPEN_ACK) { fprintf(stderr, "meow_audio: unexpected reply 0x%04x\n", rt); return 1; }
|
||||
m_audio_open_ack_t a; memcpy(&a, rbuf, sizeof a);
|
||||
if (a.err != MEOW_OK) {
|
||||
fprintf(stderr, "meow_audio: open failed (err=%d): %s\n", a.err,
|
||||
a.err == MEOW_ERR_NODEV ? "no usable PCM device" :
|
||||
a.err == MEOW_ERR_INVAL ? "invalid params" : "error");
|
||||
return 1;
|
||||
}
|
||||
printf("tone: sid=%u grant=%uHz/%uch/%s\n", (unsigned)a.stream_id,
|
||||
(unsigned)a.grant_rate, (unsigned)a.grant_ch, fmt_name((int)a.grant_fmt));
|
||||
|
||||
double amp = 0.3;
|
||||
size_t total_frames = (size_t)((uint64_t)ms * (uint64_t)rate / 1000);
|
||||
uint8_t pcm[MEOW_IPC_MAX];
|
||||
size_t fs = (size_t)a.grant_ch * (size_t)fmt_bps((int)a.grant_fmt);
|
||||
if (fs == 0) fs = 1;
|
||||
size_t sent_frames = 0;
|
||||
while (sent_frames < total_frames) {
|
||||
size_t want = total_frames - sent_frames;
|
||||
if (want > (size_t)(sizeof pcm / fs)) want = sizeof pcm / fs;
|
||||
size_t by = gen_sine(pcm, sizeof pcm, (int)a.grant_fmt, (int)a.grant_ch,
|
||||
(double)freq, rate, sent_frames, want, amp);
|
||||
if (by == 0) break;
|
||||
if (send_frame(g_fd, MSG_AUDIO_WRITE, 0, pcm, (uint32_t)by) != 0) {
|
||||
fprintf(stderr, "meow_audio: write failed (audiod died?)\n");
|
||||
return 1;
|
||||
}
|
||||
sent_frames += by / fs;
|
||||
}
|
||||
|
||||
/* 停止 + 等待排空(WAV 完整落盘 / 真实 DAC 播放完) */
|
||||
m_audio_stream_t s; s.stream_id = a.stream_id;
|
||||
req_resp(MSG_AUDIO_STOP, &s, sizeof s, &rt, rbuf, sizeof rbuf);
|
||||
uint64_t deadline_ms = (uint64_t)ms + 3000;
|
||||
uint64_t t0 = 0;
|
||||
{
|
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
t0 = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
for (;;) {
|
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
uint64_t now = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
if (now - t0 > deadline_ms) break;
|
||||
req_resp(MSG_AUDIO_QUERY, NULL, 0, &rt, rbuf, sizeof rbuf);
|
||||
if (rt == MSG_AUDIO_STATE) {
|
||||
if (strstr((const char *)rbuf, "active=0")) break;
|
||||
}
|
||||
usleep(100000); /* 100ms 轮询 */
|
||||
}
|
||||
printf("tone done: sent %zu frames\n", sent_frames);
|
||||
close(g_fd);
|
||||
g_fd = -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *argv0)
|
||||
{
|
||||
fprintf(stderr, "usage: %s status | tone [freq] [ms] [rate] [S16|S24|S32] | route <0|2|3> | vol [0-100] | output <dac|bt> | monitor [秒]\n"
|
||||
" output: 切换输出目标(dac=本地 hw:0,0 / bt=蓝牙 A2DP source;bt 时音量=AVRCP 侧控)\n"
|
||||
" monitor: 终端实时电平表(200ms 刷新+文本电平条;缺省 10s,0=持续到 Ctrl-C)\n"
|
||||
" env: MEOW_AUDIO_SOCK (socket 缺省 %s)\n", argv0, AUD_SOCK_DEFAULT);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *e = getenv("MEOW_AUDIO_SOCK");
|
||||
if (e && e[0]) snprintf(g_sock, sizeof g_sock, "%s", e);
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--sock") && i + 1 < argc) { snprintf(g_sock, sizeof g_sock, "%s", argv[++i]); }
|
||||
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { usage(argv[0]); return 0; }
|
||||
}
|
||||
if (argc < 2) { usage(argv[0]); return 2; }
|
||||
|
||||
g_fd = connect_sock();
|
||||
if (g_fd < 0 && strcmp(argv[1], "status") != 0 && strcmp(argv[1], "vol") != 0
|
||||
&& strcmp(argv[1], "route") != 0 && strcmp(argv[1], "tone") != 0) {
|
||||
/* 未知命令用 status 兜底说明 */
|
||||
}
|
||||
/* 连接失败:status/vol/route/tone 各自报错;未知命令直接 usage */
|
||||
const char *cmd = argv[1];
|
||||
if (!strcmp(cmd, "status")) {
|
||||
return cmd_status();
|
||||
} else if (!strcmp(cmd, "route")) {
|
||||
if (argc < 3) { usage(argv[0]); return 2; }
|
||||
int port = atoi(argv[2]);
|
||||
if (port != 0 && port != 2 && port != 3) { fprintf(stderr, "route: port 0/2/3\n"); return 2; }
|
||||
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
|
||||
int rc = cmd_route(port); close(g_fd); return rc;
|
||||
} else if (!strcmp(cmd, "vol")) {
|
||||
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
|
||||
int pct = (argc >= 3) ? atoi(argv[2]) : -1;
|
||||
if (pct > 100) pct = 100;
|
||||
int rc = cmd_vol(pct); close(g_fd); return rc;
|
||||
} else if (!strcmp(cmd, "output")) {
|
||||
if (argc < 3) { fprintf(stderr, "output: <dac|bt>\n"); return 2; }
|
||||
int t = (strcmp(argv[2], "bt") == 0) ? AUDIO_OUT_BT : (strcmp(argv[2], "dac") == 0) ? AUDIO_OUT_DAC : -1;
|
||||
if (t < 0) { fprintf(stderr, "output: dac|bt\n"); return 2; }
|
||||
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
|
||||
int rc = cmd_output(t); close(g_fd); return rc;
|
||||
} else if (!strcmp(cmd, "tone")) {
|
||||
int freq = (argc >= 3) ? atoi(argv[2]) : 440;
|
||||
int ms = (argc >= 4) ? atoi(argv[3]) : 500;
|
||||
int rate = (argc >= 5) ? atoi(argv[4]) : 44100;
|
||||
int fmt = AUDIO_FMT_S16;
|
||||
if (argc >= 6) {
|
||||
if (!strcmp(argv[5], "S16")) fmt = AUDIO_FMT_S16;
|
||||
else if (!strcmp(argv[5], "S24")) fmt = AUDIO_FMT_S24;
|
||||
else if (!strcmp(argv[5], "S32")) fmt = AUDIO_FMT_S32;
|
||||
else { fprintf(stderr, "tone: fmt S16|S24|S32(DoP 由 App 侧生成,CLI 不产 DoP)\n"); return 2; }
|
||||
}
|
||||
if (freq <= 0 || ms <= 0 || rate <= 0) { fprintf(stderr, "tone: bad params\n"); return 2; }
|
||||
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
|
||||
return cmd_tone(freq, ms, rate, fmt);
|
||||
} else if (!strcmp(cmd, "monitor")) {
|
||||
int secs = (argc >= 3) ? atoi(argv[2]) : 10;
|
||||
if (secs < 0) secs = 10;
|
||||
return cmd_monitor(secs);
|
||||
}
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
#define _GNU_SOURCE
|
||||
/* probe_alsa.c — 探测 R3 Pro II CS43198 PCM 设备的 hw_params 约束(找可用的 access/format/rate)。
|
||||
* mipsel 交叉编译后推到设备运行,输出 kernel 支持哪些组合,诊断「hw_params ioctl 失败」。 */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sound/asound.h>
|
||||
|
||||
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
|
||||
static void pivset(struct snd_interval *iv, unsigned val)
|
||||
{
|
||||
iv->min = iv->max = val; iv->integer = 1; iv->openmin = iv->openmax = 0; iv->empty = 0;
|
||||
}
|
||||
static const char *fmt_name(unsigned f)
|
||||
{
|
||||
#define N(x) case x: return #x
|
||||
switch (f) {
|
||||
N(SNDRV_PCM_FORMAT_S8); N(SNDRV_PCM_FORMAT_U8); N(SNDRV_PCM_FORMAT_S16_LE);
|
||||
N(SNDRV_PCM_FORMAT_S16_BE); N(SNDRV_PCM_FORMAT_U16_LE); N(SNDRV_PCM_FORMAT_U16_BE);
|
||||
N(SNDRV_PCM_FORMAT_S24_LE); N(SNDRV_PCM_FORMAT_S24_BE); N(SNDRV_PCM_FORMAT_S32_LE);
|
||||
N(SNDRV_PCM_FORMAT_S32_BE); N(SNDRV_PCM_FORMAT_FLOAT_LE); N(SNDRV_PCM_FORMAT_FLOAT_BE);
|
||||
N(SNDRV_PCM_FORMAT_S24_3LE); N(SNDRV_PCM_FORMAT_S24_3BE); N(SNDRV_PCM_FORMAT_U24_LE);
|
||||
N(SNDRV_PCM_FORMAT_S20_3LE); N(SNDRV_PCM_FORMAT_S20_3BE);
|
||||
default: return "?";
|
||||
}
|
||||
#undef N
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setvbuf(stdout, NULL, _IONBF, 0); /* 每行立即刷出,防 kill 丢输出 */
|
||||
const char *dev = "/dev/snd/pcmC0D0p";
|
||||
unsigned access[] = { SNDRV_PCM_ACCESS_MMAP_INTERLEAVED, SNDRV_PCM_ACCESS_RW_INTERLEAVED,
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED, SNDRV_PCM_ACCESS_RW_NONINTERLEAVED, ~0u };
|
||||
unsigned fmts[] = { SNDRV_PCM_FORMAT_S16_LE, SNDRV_PCM_FORMAT_S24_LE, SNDRV_PCM_FORMAT_S32_LE,
|
||||
SNDRV_PCM_FORMAT_S24_3LE, ~0u };
|
||||
unsigned rates[] = { 44100, 48000, 96000, 88200, ~0u };
|
||||
for (unsigned ai = 0; access[ai] != ~0u; ai++) {
|
||||
for (unsigned fi = 0; fmts[fi] != ~0u; fi++) {
|
||||
printf("== access=%u fmt=%s ==\n", access[ai], fmt_name(fmts[fi]));
|
||||
struct snd_pcm_hw_params p;
|
||||
for (unsigned ri = 0; rates[ri] != ~0u; ri++) {
|
||||
int fd = open(dev, O_RDWR | O_NONBLOCK);
|
||||
if (fd < 0) { printf(" open: %s\n", strerror(errno)); break; }
|
||||
/* 「全放行」初始化:memset 0 的 interval =「只能取 0」→ refine 必空 EINVAL
|
||||
*(2026-08-31 真机全灭的根因;libasound snd_pcm_hw_params_any 语义) */
|
||||
memset(&p, 0, sizeof p);
|
||||
unsigned nm = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1;
|
||||
for (unsigned i = 0; i < nm; i++)
|
||||
for (unsigned b = 0; b < sizeof p.masks[i].bits / sizeof p.masks[i].bits[0]; b++)
|
||||
p.masks[i].bits[b] = ~0u;
|
||||
unsigned ni = SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1;
|
||||
for (unsigned i = 0; i < ni; i++) { p.intervals[i].min = 0; p.intervals[i].max = ~0u; }
|
||||
/* mask 先清零再单点:全 1 会被 choose 取最低 bit(access→MMAP_INTERLEAVED) */
|
||||
struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(am, 0, sizeof *am); pmask(am, access[ai]);
|
||||
struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(fm, 0, sizeof *fm); pmask(fm, fmts[fi]);
|
||||
struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD);
|
||||
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], 2);
|
||||
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], rates[ri]);
|
||||
p.rmask = ~0u; p.info = 0; p.msbits = 0; p.rate_num = 0; p.rate_den = 0; p.flags = 0;
|
||||
int rc = ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p);
|
||||
printf(" @%u: %s\n", rates[ri], rc ? strerror(errno) : "OK");
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# meow_audiod 板载服务配置(svcd 托管;由 S89meow_svcd 首启预置到 /usr/data/meow/services.d/)
|
||||
#
|
||||
# trigger=manual:audiod 不在开机自动拉起 —— 由 launcher 在确认 MeowOS 模式后 `meow_svcd start
|
||||
# audiod`(幂等)。audiod 自身还有启动门控(/tmp/meow_boot_mode != meow → exit(0)),双重保证
|
||||
# HibyOS 模式下它物理不存在、绝不碰声卡。
|
||||
# restart=on-fail:运行中崩溃 → svcd 重拉(关流状态由客户端 socket EOF 回收兜底),不级联。
|
||||
exec=/usr/bin/meow_audiod
|
||||
args=
|
||||
enabled=1
|
||||
restart=on-fail
|
||||
backoff_ms=2000
|
||||
max_restart=5
|
||||
trigger=manual
|
||||
cap=
|
||||
Executable
BIN
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,116 @@
|
||||
/* test_bt_dev.c — 蓝牙 DEV 自识别宿主验证(纯逻辑,bt_dev.c)。
|
||||
*
|
||||
* 覆盖矩阵(对齐任务B 宿主测试要求):
|
||||
* D1 bt_dev_detect_mac:a2dpsrc/<MAC> 端点 → 提取 MAC(含 a2dp_source、多空行、多设备取第一个、
|
||||
* 无 a2dpsrc / 空输出 / NULL → -1)。
|
||||
* D2 bt_dev_rewrite:真实 alsa.conf 文本 → DEV=、device 两处 MAC 换成目标,保留 codec/其余 token;
|
||||
* 同值 → 1(无需改动);无 pcm.bluealsa / 无 DEV= → -1;带引号风格保留。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_audiod 下):
|
||||
* cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c
|
||||
* 运行:./test_bt_dev
|
||||
*/
|
||||
#include "bt_dev.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void check(int cond, const char *name)
|
||||
{
|
||||
if (cond) { printf(" PASS %s\n", name); g_pass++; }
|
||||
else { printf(" FAIL %s\n", name); g_fail++; }
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
|
||||
/* ---- D1 设备列表 → 选中 a2dpsrc 连接设备 ---- */
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac(
|
||||
"a2dpsrc/E0:42:6D:79:C5:98\na2dpsnk/E0:42:6D:79:C5:98\n", mac, sizeof mac);
|
||||
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1a a2dpsrc/<MAC> -> extract MAC");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac(
|
||||
"profile/aptx\n\na2dp_source/33:DE:2E:21:AD:30\n", mac, sizeof mac);
|
||||
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1b a2dp_source/<MAC> -> extract MAC");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac(
|
||||
"a2dpsrc/AA:BB:CC:DD:EE:FF 0\na2dpsrc/11:22:33:44:55:66 1\n", mac, sizeof mac);
|
||||
check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac("\n", mac, sizeof mac);
|
||||
check(rc == -1, "D1d empty output -> -1");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac("a2dpsnk/11:22:33:44:55:66\n", mac, sizeof mac);
|
||||
check(rc == -1, "D1e only sink (no a2dpsrc) -> -1");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac(NULL, mac, sizeof mac);
|
||||
check(rc == -1, "D1f NULL input -> -1");
|
||||
}
|
||||
|
||||
/* ---- D2 DEV 串入 / 保留其它 token ---- */
|
||||
{
|
||||
const char *conf =
|
||||
"pcm.bluealsa:DEV=E0:42:6D:79:C5:98 {\n"
|
||||
" type plug\n"
|
||||
" slave {\n"
|
||||
" pcm {\n"
|
||||
" type bluealsa\n"
|
||||
" device E0:42:6D:79:C5:98\n"
|
||||
" profile \"a2dp\"\n"
|
||||
" codec \"LDAC\"\n"
|
||||
" ldac_eqmid \"LDAC_ABR\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == 0 && out != NULL, "D2a rewrite returns changed");
|
||||
check(out && strstr(out, ":DEV=33:DE:2E:21:AD:30 {"), "D2b DEV= updated");
|
||||
check(out && strstr(out, "device 33:DE:2E:21:AD:30\n"), "D2c device updated");
|
||||
check(out && strstr(out, "codec \"LDAC\"") && strstr(out, "ldac_eqmid \"LDAC_ABR\""),
|
||||
"D2d codec/other tokens preserved");
|
||||
/* 旧 MAC 不再出现 */
|
||||
check(out && !strstr(out, "E0:42:6D:79:C5:98"), "D2e old MAC gone");
|
||||
free(out);
|
||||
}
|
||||
{
|
||||
const char *conf =
|
||||
"pcm.bluealsa:DEV=33:DE:2E:21:AD:30 {\n"
|
||||
" type plug\n slave {\n pcm {\n"
|
||||
" type bluealsa\n device 33:DE:2E:21:AD:30\n"
|
||||
" profile \"a2dp\"\n codec \"LDAC\"\n }\n }\n}\n";
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == 1 && out == NULL, "D2f same MAC -> unchanged (idempotent)");
|
||||
}
|
||||
{
|
||||
const char *conf = "pcm.bluealsa {\n type bluealsa\n device \"E0:42:6D:79:C5:98\"\n}\n";
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == 0 && out && strstr(out, "device \"33:DE:2E:21:AD:30\""),
|
||||
"D2g quoted device preserved (no DEV= token)");
|
||||
free(out);
|
||||
}
|
||||
{
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite("pcm.hw {\n type hw\n}\n", 21, "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == -1 && out == NULL, "D2h no pcm.bluealsa -> -1");
|
||||
}
|
||||
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
}
|
||||
Binary file not shown.
@@ -178,7 +178,9 @@ static void log_cleanup_old(void)
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL && n < 128) {
|
||||
const char *nm = de->d_name;
|
||||
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0) continue;
|
||||
/* 收纳 meow_/crash_/perf_/hang_(perf_=RMM 基线、hang_=卡死快照,同 logs/ 1GB 纪律)。 */
|
||||
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0 &&
|
||||
strncmp(nm, "perf_", 5) != 0 && strncmp(nm, "hang_", 5) != 0) continue;
|
||||
char path[CRASH_PATH_MAX];
|
||||
if (snprintf(path, sizeof path, "%s/%s", logs, nm) >= (int)sizeof path) continue;
|
||||
struct stat st;
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -102,9 +102,10 @@ int settings_cache_registered = 0; /* M2.24:监听只注册一次 */
|
||||
* settings_volume(写入 DAC 的映射值)为权威跟踪值。 */
|
||||
void meow_dac_vol_write(int pct)
|
||||
{
|
||||
int dac = pct * 255 / 100;
|
||||
if (dac < 0) dac = 0;
|
||||
if (dac > 255) dac = 255;
|
||||
int t = 100 - pct, dac;
|
||||
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
|
||||
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
||||
if (dac > 255) dac = 255; if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
|
||||
@@ -153,8 +154,65 @@ static unsigned long long vol_ramp_next_at = 0;
|
||||
static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */
|
||||
|
||||
|
||||
/* ---- 插孔消抖(2026-09-01,真机日志实证)----
|
||||
* 4.4/3.5 耳机插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住
|
||||
* (EVIOCGKEY 轮询到持续 down),vol_ramp 一路爬到上限:日志 225391「volume UP <- ramp」
|
||||
* → 227213「ramp end -> 50」,紧接着 audiod 才报「route: jack changed -> 4.4 balance」。
|
||||
* 修法:/sys/class/switch/{headset,balance}/state 任一变化后 JACK_DEBOUNCE_MS 内忽略音量键
|
||||
* 边沿(vol_ramp_begin 不起、vol_ramp_tick 在爬也中止)。插拔耳机时没人按音量,误伤可忽略。
|
||||
* 检测走 1s 节流读缓存——绝不在 10ms 主循环热路径上每圈读文件。 */
|
||||
/* 宿主测试可 -D 覆盖(同 meow_settings.c MEOW_SETTINGS_FILE 惯例) */
|
||||
#ifndef JACK_HS_STATE
|
||||
#define JACK_HS_STATE "/sys/class/switch/headset/state"
|
||||
#endif
|
||||
#ifndef JACK_BAL_STATE
|
||||
#define JACK_BAL_STATE "/sys/class/switch/balance/state"
|
||||
#endif
|
||||
|
||||
static int jack_combo_cached = -1; /* hs*2+bal;-1=未初始化/节点缺失 */
|
||||
|
||||
static unsigned long long jack_poll_at = 0; /* 上次节流读时刻 */
|
||||
|
||||
static unsigned long long jack_changed_at = 0; /* 最近一次检测到插孔变化的时刻(0=无) */
|
||||
|
||||
|
||||
/* 读两个插孔开关节点合成 hs*2+bal;两节点都不可读返回 -1(不更新缓存,不误判变化)。 */
|
||||
static int jack_combo_read(void)
|
||||
{
|
||||
int hs = read_file_int(JACK_HS_STATE);
|
||||
int bal = read_file_int(JACK_BAL_STATE);
|
||||
if (hs < 0 && bal < 0) return -1;
|
||||
return (hs > 0 ? 2 : 0) | (bal > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
/* 返回 1=插孔变化后消抖窗口内(音量键边沿应忽略/在爬 ramp 应中止)。
|
||||
* 自节流:每 JACK_POLL_MS 才真正读一次 sysfs。 */
|
||||
static int jack_debounce_active(void)
|
||||
{
|
||||
unsigned long long now = now_ms();
|
||||
if (now - jack_poll_at >= JACK_POLL_MS) {
|
||||
jack_poll_at = now;
|
||||
int c = jack_combo_read();
|
||||
if (c >= 0) {
|
||||
if (jack_combo_cached >= 0 && c != jack_combo_cached) {
|
||||
jack_changed_at = now;
|
||||
logmsg("jack state changed (%d -> %d), vol-key debounce %dms",
|
||||
jack_combo_cached, c, JACK_DEBOUNCE_MS);
|
||||
}
|
||||
jack_combo_cached = c;
|
||||
}
|
||||
}
|
||||
return jack_changed_at && (now - jack_changed_at < (unsigned long long)JACK_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
|
||||
static void vol_ramp_begin(int delta)
|
||||
{
|
||||
if (jack_debounce_active()) { /* 插孔瞬态期:忽略音量键边沿(幽灵按下) */
|
||||
logmsg("volume adj %+d ignored (jack debounce)", delta);
|
||||
return;
|
||||
}
|
||||
int target = vol_clamp(settings_volume + delta);
|
||||
if (target == settings_volume) return;
|
||||
vol_ramp_dir = (target > settings_volume) ? 1 : -1;
|
||||
@@ -175,6 +233,22 @@ int vol_ramp_tick(int key_held)
|
||||
unsigned long long now = now_ms();
|
||||
if (now < vol_ramp_next_at) return 1;
|
||||
vol_ramp_next_at = now + VOL_RAMP_MS;
|
||||
/* 插孔消抖:插拔瞬态的幽灵按住正在驱动 ramp(真机实证 24%→上限 50)——
|
||||
* 一旦检测到插孔变化(1s 节流轮询),立即中止在爬的 ramp。 */
|
||||
if (jack_debounce_active()) {
|
||||
vol_ramp_dir = 0; vol_ramp_target = -1;
|
||||
meow_settings_save();
|
||||
logmsg("volume ramp aborted (jack debounce) -> %d", settings_volume);
|
||||
return 0;
|
||||
}
|
||||
/* 兜底:ramp 总时长上限 VOL_RAMP_MAX_MS——防任何「持续按住」假象(ADC 噪声卡 key_held=1)
|
||||
* 一次打满/打到底。正常长按 0→上限(通常 ≤2.7s)不受影响;到顶想再调需松手重按。 */
|
||||
if (now - vol_ramp_press_at >= (unsigned long long)VOL_RAMP_MAX_MS) {
|
||||
vol_ramp_dir = 0; vol_ramp_target = -1;
|
||||
meow_settings_save();
|
||||
logmsg("volume ramp force-stop (%dms cap) -> %d", VOL_RAMP_MAX_MS, settings_volume);
|
||||
return 0;
|
||||
}
|
||||
/* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)——
|
||||
* 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */
|
||||
if (settings_volume == vol_ramp_target) {
|
||||
|
||||
@@ -41,7 +41,7 @@ void apply_user_brightness(void)
|
||||
}
|
||||
int v = meow_settings_brightness_clamped();
|
||||
if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */
|
||||
write_brightness(v);
|
||||
write_brightness(v, "apply_user_brightness");
|
||||
g_sys.brightness = v;
|
||||
meow_brightness_pending = 0;
|
||||
logmsg("apply user brightness (MeowOS takeover) -> %d", v);
|
||||
@@ -73,13 +73,16 @@ static void write_file(const char *path, const char *val)
|
||||
}
|
||||
|
||||
|
||||
void write_brightness(int val)
|
||||
void write_brightness(int val, const char *src)
|
||||
{
|
||||
/* 亮度幽灵排查(2026-09-01:用户报告插耳机瞬间亮度自己跳到 raw 上限,写者未知):
|
||||
* 每次写背光都留值+调用源线索,下次「亮度自己跳」可直接定位写者。 */
|
||||
char buf[8];
|
||||
int l = snprintf(buf, sizeof buf, "%d\n", val);
|
||||
int fd = open(BACKLIGHT, O_WRONLY);
|
||||
if (fd < 0) { logmsg("backlight open fail: %s", strerror(errno)); return; }
|
||||
if (write(fd, buf, l) < 0) logmsg("backlight write %d: %s", val, strerror(errno));
|
||||
if (fd < 0) { logmsg("backlight <- %d (src=%s) open fail: %s", val, src, strerror(errno)); return; }
|
||||
if (write(fd, buf, l) < 0) logmsg("backlight <- %d (src=%s): %s", val, src, strerror(errno));
|
||||
else logmsg("backlight <- %d (src=%s)", val, src);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ void ensure_backlight(void)
|
||||
}
|
||||
int target = (saved_brightness > 0) ? saved_brightness : 60;
|
||||
logmsg("backlight off (cur=%d bl_power=%d) -> %d", cur, blp, target);
|
||||
if (cur <= 0) write_brightness(target);
|
||||
if (cur <= 0) write_brightness(target, "ensure_backlight");
|
||||
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@ void restore_backlight(void)
|
||||
{
|
||||
if (menu_opened_blank) {
|
||||
logmsg("restore: panel was blanked at menu open, re-blank (0 + FBIOBLANK 4)");
|
||||
write_brightness(0);
|
||||
write_brightness(0, "restore_backlight(re-blank)");
|
||||
int fbfd = open(FB_DEV, O_RDWR);
|
||||
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 4); close(fbfd); }
|
||||
menu_opened_blank = 0;
|
||||
@@ -138,7 +141,7 @@ void restore_backlight(void)
|
||||
int target = (saved_brightness > 0) ? saved_brightness : 60;
|
||||
logmsg("restore backlight (cur=%d bl_power=%d) -> %d (saved=%d)",
|
||||
cur, blp, target, saved_brightness);
|
||||
if (cur <= 0) write_brightness(target);
|
||||
if (cur <= 0) write_brightness(target, "restore_backlight");
|
||||
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/* bg_page.c — 批③ 后台管理页(桌面顶栏居右按钮进入)。
|
||||
*
|
||||
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
|
||||
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
|
||||
* act=bgm_top_action → bgmgr_kill_all,**不杀受保护/当前前台/非后台**,经 bgmgr_can_kill 门控)。
|
||||
* 列表(live=1 约 2s 重扫,实时增删):
|
||||
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
|
||||
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
|
||||
* - 行=MD3 圆角卡(meow_gfx_fill_round r=12 surfaceContainer;卡在行高内上下对称内缩
|
||||
* BGM_CARD_GAP/2,行中心=卡中心,相邻卡 8px 外距——与主界面列表/ ncm 歌单同手法)。
|
||||
* - 空态「无后台 App」。
|
||||
* 数据源=amsd FG/BG 注册表(后台常驻集=launcher meow_bg 表,EV BG/FG 上报进 amsd)+ 每进程
|
||||
* pcpu/rss(meow_svc_perf 同源 /proc 聚合,本页在 launcher 内同口径取)。
|
||||
*
|
||||
* 按钮化(相对 agent-425 的「点行 kill + 顶置清除动作行」):
|
||||
* - kill:改「点整行」为「行最右独立杀按钮」——行自绘(bgm_row_draw)+ 自定义命中(bgm_row_tap),
|
||||
* 命中几何=bgm_btn_rect(行右 46×34 圆角按钮);行体其它区域 point 回 0(不杀)。
|
||||
* - 清:删顶置「一键清除后台」SUB_ACT 动作行,改为顶栏标题栏右侧 action 按钮(top_action)。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "meow_bgplay.h"
|
||||
#include "meow_bgmgr.h"
|
||||
#include <ctype.h>
|
||||
|
||||
/* 行内杀按钮尺寸(圆心/圆角定位用;命中与绘制共用同一几何,保证所见即所点) */
|
||||
#define BGM_BTN_W 46
|
||||
#define BGM_BTN_H 34
|
||||
|
||||
/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=SUB_ITEM_H)
|
||||
* 不变——卡片在其行高内上下对称内缩 BGM_CARD_GAP/2,故行内内容(按行高垂直居中)仍落卡中心;
|
||||
* 相邻卡顶间距恒=SUB_ITEM_H=卡高+BGM_CARD_GAP。要求偶数,保证上下对称。 */
|
||||
#define BGM_CARD_GAP 8
|
||||
static inline int bgm_row_card_y(int row_top) { return row_top + BGM_CARD_GAP / 2; }
|
||||
static inline int bgm_row_card_h(int h) { return h - BGM_CARD_GAP; }
|
||||
|
||||
typedef struct {
|
||||
struct app *apps;
|
||||
int napps;
|
||||
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
|
||||
int n;
|
||||
/* 行副文本存储:sub_row_t.sub 被 SDK 行池/自绘在整页会话内持续读取,
|
||||
* 不能指向 bgm_rows 的栈帧(曾用函数内 char sub[80] → 悬垂,页面副标题为乱码)。 */
|
||||
char subs[MEOW_BGMR_ROW_MAX][80];
|
||||
} bgm_ctx_t;
|
||||
|
||||
/* 找 struct app(launcher 应用表)by id */
|
||||
static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id)
|
||||
{
|
||||
if (!apps || !app_id) return NULL;
|
||||
for (int i = 0; i < napps; i++)
|
||||
if (apps[i].id[0] && !strcmp(apps[i].id, app_id)) return &apps[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
|
||||
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
|
||||
char *name, int nl, char *icon, int il,
|
||||
char *protected_app, int *is_fg)
|
||||
{
|
||||
(void)pid;
|
||||
bgm_ctx_t *b = ctx;
|
||||
struct app *a = bgm_app_find(b->apps, b->napps, app_id);
|
||||
*protected_app = 0;
|
||||
*is_fg = 0;
|
||||
if (!a) return -1; /* 未注册 → name 回退 app_id */
|
||||
if (a->name[0]) snprintf(name, nl, "%s", a->name);
|
||||
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
|
||||
/* 刻意**不**用 a->framework:app.json 的 framework=1 意为「用 meow_ui.a 的 SDK 框架 App」
|
||||
* (docs/APP_DEV_GUIDE.md §9,推荐且现有 App 全写 1),不是「系统 App」。曾把它当「禁止 kill」
|
||||
* 门控 → 后台页所有行的「杀」按钮被置灰、点了不杀也不留日志(2026-11-04 真机 dev.player.ncm)。
|
||||
* launcher 自身组件(状态栏/设置页/子页…)不在 App 表也不在后台表,无需此位保护。 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bgm_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||||
{
|
||||
(void)ctx;
|
||||
/* 逐槽扫描并**压缩**(g_bg 可能有空洞),跳过已发出终止的条目:
|
||||
* meow_bg_terminate 只置 term_at_ms 发 SIGTERM,真正的 reap/摘表在 meow_bg_tick(launcher
|
||||
* 主循环),它在本页 meow_subpage_run 阻塞期间**不跑**——若不在此过滤,kill/清后旧行会一直
|
||||
* 残留(pid 已死/0 CPU),页面上看就是「点不动」。过滤后 kill/一键清当下即从列表消失(可见反馈)。 */
|
||||
int k = 0;
|
||||
for (int i = 0; i < MEOW_BG_MAX && k < max; i++) {
|
||||
const meow_bg_entry_t *e = meow_bg_at(i);
|
||||
if (!e || !e->used || !e->app_id[0]) continue;
|
||||
if (e->term_at_ms) continue; /* 已进入终止序列(kill/宽限回收/卡拔)→ 行不再显示 */
|
||||
memset(&rows[k], 0, sizeof rows[k]);
|
||||
snprintf(rows[k].app_id, sizeof rows[k].app_id, "%s", e->app_id);
|
||||
rows[k].pid = e->pid;
|
||||
rows[k].keepalive = (e->keepalive ? 1 : 0);
|
||||
rows[k].on_card = (e->on_card ? 1 : 0);
|
||||
rows[k].bg_since_ms = e->bg_since_ms;
|
||||
k++;
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
/* proc_sample:/proc/<pid>/stat(utime+stime 差分) + /proc/<pid>/status(VmRSS)。
|
||||
* 与 meow_svc_perf 897-983 每进程 pcpu/rss 聚合同口径(同 /proc 源)。 */
|
||||
static long g_cpu_pid[MEOW_BG_MAX] = {0};
|
||||
static unsigned long long g_cpu_tick[MEOW_BG_MAX] = {0};
|
||||
|
||||
static int bgm_proc_sample(void *ctx, pid_t pid, double *pcpu, long *rss_kb)
|
||||
{
|
||||
(void)ctx;
|
||||
if (pid <= 0) return -1;
|
||||
char path[64], line[512];
|
||||
long rss = 0;
|
||||
snprintf(path, sizeof path, "/proc/%d/status", pid);
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
ssize_t nr = read(fd, line, sizeof line - 1); close(fd);
|
||||
if (nr > 0) { line[nr] = 0;
|
||||
char *p = strstr(line, "VmRSS:");
|
||||
if (p) p = strchr(p, ' ');
|
||||
if (p) { while (*p == ' ' || *p == '\t') p++; rss = atol(p); }
|
||||
}
|
||||
}
|
||||
double pc = 0.0;
|
||||
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
ssize_t nr = read(fd, line, sizeof line - 1); close(fd);
|
||||
if (nr > 0) {
|
||||
line[nr] = 0;
|
||||
char *rp = strrchr(line, ')');
|
||||
if (rp) {
|
||||
char *p = rp + 2;
|
||||
unsigned long long ut[2] = {0, 0};
|
||||
for (int f = 0; f < 14; f++) {
|
||||
while (*p == ' ') p++;
|
||||
char *end; unsigned long long v = strtoull(p, &end, 10);
|
||||
if (end == p) { p++; continue; }
|
||||
p = end;
|
||||
if (f == 11) ut[0] = v; /* utime */
|
||||
if (f == 12) ut[1] = v; /* stime */
|
||||
}
|
||||
unsigned long long cur = ut[0] + ut[1];
|
||||
static double hz = 0.0;
|
||||
if (hz <= 0) { hz = (double)sysconf(_SC_CLK_TCK); if (hz <= 0) hz = 100.0; }
|
||||
unsigned long long prev = 0; int had = 0, slot = -1;
|
||||
for (int i = 0; i < MEOW_BG_MAX; i++) {
|
||||
if (g_cpu_pid[i] == pid) { prev = g_cpu_tick[i]; had = 1; slot = i; break; }
|
||||
}
|
||||
if (!had) {
|
||||
for (int i = 0; i < MEOW_BG_MAX; i++) if (g_cpu_pid[i] == 0) { slot = i; break; }
|
||||
if (slot < 0) slot = 0;
|
||||
}
|
||||
if (slot >= 0) { g_cpu_pid[slot] = pid; g_cpu_tick[slot] = cur; }
|
||||
if (had) { pc = 100.0 * (double)(cur - prev) / hz; if (pc < 0) pc = 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
*pcpu = pc;
|
||||
*rss_kb = rss;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- rows(填 sub_row_t 列表;每后台 App 一行,空态一行)---- */
|
||||
static int bgm_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||
{
|
||||
bgm_ctx_t *b = ctx;
|
||||
bgmgr_ops_t ops;
|
||||
memset(&ops, 0, sizeof ops);
|
||||
ops.ctx = b;
|
||||
ops.enumerate = bgm_enumerate;
|
||||
ops.app_meta = bgm_app_meta;
|
||||
ops.proc_sample = bgm_proc_sample;
|
||||
b->n = bgmgr_collect(&ops, b->rows, MEOW_BGMR_ROW_MAX);
|
||||
|
||||
int r = 0;
|
||||
if (b->n == 0) {
|
||||
if (r < max) {
|
||||
rbuf[r].kind = SUB_ROW;
|
||||
rbuf[r].label = meow_str_zh("bgmgr.empty", "无后台 App");
|
||||
rbuf[r].sub = NULL;
|
||||
rbuf[r].act = -1;
|
||||
r++;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
for (int i = 0; i < b->n && r < max; i++) {
|
||||
/* 副文本写进 ctx 的持久缓冲(b->subs):sub_row_t.sub 会被 SDK 行池在整页会话内反复读取,
|
||||
* 指向本函数栈帧的 buffer 会在返回后变成乱码/越界(真机实证:副标题全是乱码字节)。 */
|
||||
snprintf(b->subs[i], sizeof b->subs[i], "CPU %.1f%% · %s · %s", b->rows[i].pcpu,
|
||||
bgmgr_mem_str(b->rows[i].rss_kb), bgmgr_status_str(&b->rows[i]));
|
||||
rbuf[r].kind = SUB_ROW;
|
||||
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
|
||||
rbuf[r].sub = b->subs[i];
|
||||
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
|
||||
r++;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/* kill 通道(bgmgr_kill_all 的函数指针签名:void(*)(const char*)) */
|
||||
static void bgm_kill_fn(const char *app_id) { meow_bg_terminate(app_id); }
|
||||
|
||||
/* ---- 行内杀按钮矩形(绘制与命中共用;行视口矩形 rx,ry,rw,rh → 右侧 46×34 圆角钮)----
|
||||
* 卡在行高内上下对称内缩 BGM_CARD_GAP/2(行中心=卡中心);按钮垂直居中于卡内,故其中心
|
||||
* =卡中心=行中心。命中矩形仍为整行距(row_tap 传整行视口),按钮几何不变 → 卡内缩后命中不破。 */
|
||||
static void bgm_btn_rect(int rx, int ry, int rw, int rh, int *bx, int *by, int *bw, int *bh)
|
||||
{
|
||||
*bw = BGM_BTN_W;
|
||||
*bh = BGM_BTN_H;
|
||||
*bx = rx + rw - 12 - BGM_BTN_W;
|
||||
*by = bgm_row_card_y(ry) + (bgm_row_card_h(rh) - BGM_BTN_H) / 2;
|
||||
}
|
||||
|
||||
/* ---- 行完全自绘(sub_page_t.row_draw;覆盖默认 renderer)----
|
||||
* 空态行(act=-1)居中画「无后台 App」;后台 App 行画 名字 + CPU/内存/状态 + 行右「杀」按钮。 */
|
||||
static void bgm_row_draw(void *ctx, int idx, sub_row_t *r,
|
||||
int x, int y, int w, int h, int state)
|
||||
{
|
||||
(void)idx;
|
||||
bgm_ctx_t *b = ctx;
|
||||
const meow_theme *mt = meow_theme_get();
|
||||
int i = r->act;
|
||||
|
||||
/* MD3 圆角卡:行高内上下对称内缩 BGM_CARD_GAP/2(行中心=卡中心),替代扁平行+底部分隔线。 */
|
||||
int cy = bgm_row_card_y(y);
|
||||
int ch = bgm_row_card_h(h);
|
||||
meow_gfx_fill_round(x, cy, w, ch, 12, mt->surfaceContainer);
|
||||
if (state & MEOW_LISTVIEW_ST_PRESSED)
|
||||
meow_gfx_fill_round_alpha(x, cy, w, ch, 12, mt->primary, 26); /* 按压态(实时,限卡内) */
|
||||
|
||||
if (i < 0 || i >= b->n) { /* 空态行 */
|
||||
int th = meow_ui_text_height(18);
|
||||
meow_ui_text_center(x + w / 2, cy + (ch - th) / 2,
|
||||
r->label ? r->label : meow_str_zh("bgmgr.empty", "无后台 App"),
|
||||
18, mt->onSurfaceVariant);
|
||||
return;
|
||||
}
|
||||
int th = meow_ui_text_height(20), tsub = meow_ui_text_height(16);
|
||||
int rx = x + 16; /* 左内边距 */
|
||||
int ry = cy + (ch - (th + tsub + 4)) / 2; /* 内容在卡内垂直居中(行中心=卡中心) */
|
||||
if (r->label) meow_ui_text(rx, ry, r->label, 20, mt->onSurface);
|
||||
if (r->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant);
|
||||
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。
|
||||
* 不可杀行(受保护/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */
|
||||
int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh);
|
||||
uint32_t btn_col = mt->error; /* 可杀:error 红 */
|
||||
if (i >= 0 && i < b->n && !b->rows[i].killable)
|
||||
btn_col = mt->onSurfaceVariant; /* 禁点:置灰(onSurfaceVariant) */
|
||||
meow_ui_draw_rect_round(bx, by, bw, bh, bh / 2, btn_col);
|
||||
int bth = meow_ui_text_height(16);
|
||||
if (bth > bh) bth = bh;
|
||||
meow_ui_text_center(bx + bw / 2, by + (bh - bth) / 2, "杀", 16, btn_col);
|
||||
}
|
||||
|
||||
/* ---- 行内自定义命中(sub_page_t.row_tap;仅按钮区 kill,行体不响应/不误杀)---- */
|
||||
static int bgm_row_tap(void *ctx, int idx, sub_row_t *r, int x, int y,
|
||||
int rx, int ry, int rw, int rh)
|
||||
{
|
||||
(void)idx;
|
||||
bgm_ctx_t *b = ctx;
|
||||
int i = r->act;
|
||||
if (i < 0 || i >= b->n) return 0; /* 空态行无按钮 */
|
||||
int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh);
|
||||
if (x < bx || x >= bx + bw || y < by || y >= by + bh) return 0; /* 行体/按钮外 → 不杀 */
|
||||
if (!bgmgr_can_kill(&b->rows[i])) {
|
||||
/* 禁点:受保护/前台/无进程 → 按钮已置灰,不 kill(返回 1=重绘)。
|
||||
* 留一行日志:「按了没反应」当年因为没有这行日志,只能靠翻 app.json 才查出是门控。 */
|
||||
logmsg("bgpage: kill 被门控 app=%s pid=%d fg=%d protected=%d",
|
||||
b->rows[i].app_id, (int)b->rows[i].pid, b->rows[i].is_fg,
|
||||
b->rows[i].protected_app ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
int rc = meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */
|
||||
if (rc != 0)
|
||||
logmsg("bgpage: kill app=%s 未命中后台表 rc=%d(列表/后台表不同步)", b->rows[i].app_id, rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- 行体点击(sub_page_t.detail):不响应(kill 只认行内按钮)---- */
|
||||
static int bgm_detail(void *ctx, int idx, int act)
|
||||
{
|
||||
(void)ctx; (void)idx; (void)act;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 顶栏右侧「一键清除后台」按钮(sub_page_t.top_action_cb)---- */
|
||||
static int bgm_top_action(void *ctx)
|
||||
{
|
||||
bgm_ctx_t *b = ctx;
|
||||
bgmgr_ops_t ops;
|
||||
memset(&ops, 0, sizeof ops);
|
||||
ops.ctx = b;
|
||||
ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */
|
||||
/* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤
|
||||
* term_at_ms 让清后的行当下消失(可见反馈;不可杀行本就在上/一键不杀)。 */
|
||||
int k = bgmgr_kill_all(&ops, b->rows, b->n, bgm_kill_fn);
|
||||
if (k < b->n)
|
||||
logmsg("bgpage: 一键清除 杀 %d/%d(其余为前台/无进程/受保护)", k, b->n);
|
||||
return 1; /* 重绘:重扫行(已清行消失) */
|
||||
}
|
||||
|
||||
/* ---- 入口:运行后台管理页(阻塞到返回)---- */
|
||||
int run_bg_page(struct app *apps, int napps)
|
||||
{
|
||||
static bgm_ctx_t b;
|
||||
memset(&b, 0, sizeof b);
|
||||
b.apps = apps;
|
||||
b.napps = napps;
|
||||
sub_page_t p;
|
||||
memset(&p, 0, sizeof p);
|
||||
p.title = "后台管理";
|
||||
p.rows = bgm_rows;
|
||||
p.detail = bgm_detail;
|
||||
p.live = 1; /* 后台表/进程变化实时重扫 */
|
||||
p.ctx = &b;
|
||||
p.top_action = "一键清除后台"; /* 顶栏标题栏右侧按钮 */
|
||||
p.top_action_cb = bgm_top_action;
|
||||
p.row_draw = bgm_row_draw; /* 行自绘(名字/CPU/内存/状态 + 行右杀按钮) */
|
||||
p.row_tap = bgm_row_tap; /* 行内自定义命中(仅按钮区 kill) */
|
||||
meow_subpage_run(&p);
|
||||
|
||||
/* 清除 CPU 差分缓存(页面会话结束) */
|
||||
memset(g_cpu_pid, 0, sizeof g_cpu_pid);
|
||||
memset(g_cpu_tick, 0, sizeof g_cpu_tick);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,46 +1,98 @@
|
||||
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76)。
|
||||
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76;P1 权威收口重做)。
|
||||
*
|
||||
* 页面(sub_page_t 脚手架 subpage.c 驱动):
|
||||
* - 顶栏:返回 + 标题「蓝牙」 + 右上开关(network.bluetooth_enabled)
|
||||
* - 已连接设备:tap → bluetoothctl disconnect(角色经 bt_role_refresh 判定)
|
||||
* - 已配对设备:tap → bluetoothctl connect(已连者标「已连接」)
|
||||
* - 附近设备:扫描发现(net_bt_scan,后台有界 bluetoothctl 写 /tmp 缓存,异步不阻塞)——
|
||||
* 未配对者 tap → pair;已配对者 tap → connect。实机配对成功率/超时需真机验证。
|
||||
* 【P1 背景】BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八:连接/断开/配对/取消配对/扫描/A2DP 状态
|
||||
* 查询全部收口进 netmgrd(BT_CTL 消息扩展),本子页**只做客户端**——经 netmgrd socket/IPC 查询
|
||||
* 与下发,不再 bluetoothctl popen 直连(直连仅作 netmgrd 不可达时的回退保留)。
|
||||
*
|
||||
* 与网络线共用 net.c(net_bt_connected/paired/scan/scan_trigger/hci_powered/bt_role)。
|
||||
* 开关写 network.bluetooth_enabled + hciconfig;连接/断开/配对走 bluetoothctl(带超时)。
|
||||
* 【已连接设备显示不全根因修复】旧实现用 `bluetoothctl devices Connected`(慢 + 只反映 agent
|
||||
* 缓存视图,popen 1500ms 易截断)→ 已连接列表不全。P1 改权威 = netmgrd 读**内核 ACL**
|
||||
* (hcitool con,快速/权威/不受 bluetoothctl 过滤影响),名经 paired/devices 缓存兜底,A2DP 详情
|
||||
* 按 MAC join(codec/采样率/acquired/role)。见 netmgrd `bt connected`/`bt a2dp`。
|
||||
*
|
||||
* 【页面结构(sub_page_t,plan §六)】顶栏「蓝牙」+ 右上开关,分组列表:
|
||||
* ① 蓝牙开关(顶栏)② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC)③ 输出目标
|
||||
* (蓝牙/本地,切下发 audio.output_bt,P2 接 audiod)④ 已连接设备(tap 断开/详情)⑤ 已配对
|
||||
* 设备(tap 连接/断开)⑥ 附近设备(扫描节流触发)⑦ 音质/连接管理(codec 选择、P2 参数说明)
|
||||
* ⑧ 功耗档(扫描节流/发现超时/后台保持说明)。
|
||||
*
|
||||
* 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。
|
||||
*
|
||||
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。开关写 network.bluetooth_enabled +
|
||||
* netmgrd on/off;连接/断开/配对经 netmgrd(netmgr_ipc.c nm_bt_* 封装)。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→disconnect) */
|
||||
#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→connect) */
|
||||
#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→pair/connect) */
|
||||
#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→断开) */
|
||||
#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→连接/断开) */
|
||||
#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→配对/连接) */
|
||||
#define BT_ACT_OUTPUT 5000 /* 输出目标(tap→蓝牙/本地切换) */
|
||||
#define BT_ACT_CODEC 5001 /* 编解码器(tap→循环选择) */
|
||||
|
||||
#define BT_CODEC_AUTO 0
|
||||
#define BT_CODEC_N 5 /* auto/SBC/AAC/aptX/LDAC */
|
||||
static const char *bt_codec_names[BT_CODEC_N] = { "auto", "SBC", "AAC", "aptX", "LDAC" };
|
||||
|
||||
typedef struct {
|
||||
meow_net_t conn[S_NET_MAX];
|
||||
meow_net_t paired[S_NET_MAX];
|
||||
meow_net_t near[S_NET_MAX];
|
||||
int conn_n, paired_n, near_n;
|
||||
bt_ep_t a2dp[S_NET_MAX];
|
||||
int conn_n, paired_n, near_n, a2dp_n;
|
||||
int bt_on;
|
||||
int full; /* 1=本次 rows() 全量重扫(conn/paired popen);0=仅附近(file)+角色(节流) */
|
||||
int output_bt; /* 0=DAC/本地,1=蓝牙 */
|
||||
int full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */
|
||||
char mode[48]; /* bt_role 中文描述(次行摘要) */
|
||||
/* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次
|
||||
* rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */
|
||||
char pool[512];
|
||||
int pool_off;
|
||||
} bt_ctx_t;
|
||||
|
||||
|
||||
/* 设备 MAC(net_bt_* 已填 mac) */
|
||||
/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——512B 远超单页行数所需)。 */
|
||||
static char *bt_pool(bt_ctx_t *c, const char *fmt, ...)
|
||||
{
|
||||
va_list ap; char tmp[128];
|
||||
va_start(ap, fmt); vsnprintf(tmp, sizeof tmp, fmt, ap); va_end(ap);
|
||||
int l = (int)strlen(tmp);
|
||||
if (c->pool_off + l + 1 > (int)sizeof c->pool) c->pool_off = 0;
|
||||
char *p = &c->pool[c->pool_off];
|
||||
memcpy(p, tmp, (size_t)l + 1);
|
||||
c->pool_off += l + 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
/* ---- 设备 MAC 相关 ---- */
|
||||
static int bt_is_conn(const bt_ctx_t *c, const char *mac)
|
||||
{
|
||||
if (!mac[0]) return 0;
|
||||
for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bt_is_paired(const bt_ctx_t *c, const char *mac)
|
||||
{
|
||||
if (!mac[0]) return 0;
|
||||
for (int i = 0; i < c->paired_n; i++) if (!strcmp(c->paired[i].mac, mac)) return 1;
|
||||
return 0;
|
||||
}
|
||||
/* 在 a2dp 端点表找 MAC(返回 idx;-1=无) */
|
||||
static int bt_ep_idx(const bt_ctx_t *c, const char *mac)
|
||||
{
|
||||
if (!mac[0]) return -1;
|
||||
for (int i = 0; i < c->a2dp_n; i++) if (!strcmp(c->a2dp[i].mac, mac)) return i;
|
||||
return -1;
|
||||
}
|
||||
/* 从 conn 表找设备名(返回指针到 c->conn[i].name,未命中返回 mac) */
|
||||
static const char *bt_conn_name(const bt_ctx_t *c, const char *mac)
|
||||
{
|
||||
if (mac[0]) for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return c->conn[i].name;
|
||||
return mac;
|
||||
}
|
||||
|
||||
|
||||
/* 顶栏开关当前值 */
|
||||
@@ -53,51 +105,101 @@ static int bt_top_sw(void *ctx, int *on)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 顶栏开关切换:写设置 + hciconfig up/down + 触发扫描 + 同步状态栏 */
|
||||
/* 顶栏开关切换:写设置 + on/off(netmgrd IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */
|
||||
static void bt_top_sw_set(void *ctx, int on)
|
||||
{
|
||||
(void)ctx;
|
||||
meow_settings_set_bool("network.bluetooth_enabled", on);
|
||||
s_net_toggle_bt(on);
|
||||
nm_bt_onoff(on);
|
||||
g_sys.bt_on = on;
|
||||
bt_role_refresh(1);
|
||||
paint_sb_right();
|
||||
meow_settings_save();
|
||||
if (on) net_bt_scan_trigger();
|
||||
if (on) nm_bt_scan_on();
|
||||
}
|
||||
|
||||
|
||||
/* 填行:刷新数据后构建分组列表。
|
||||
* conn/paired 走 bluetoothctl(慢);仅全量(full)时重扫,避开子页 live tick 反复 spawn。 */
|
||||
/* 填行:刷新数据后构建分组列表(权威=netmgrd;仅全量(full)时重扫 conn/paired/a2dp,
|
||||
* 避开子页 live tick 反复 IPC)。 */
|
||||
static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||
{
|
||||
bt_ctx_t *c = ctx;
|
||||
int full = c->full; c->full = 0; /* 消费一次全量标记 */
|
||||
c->bt_on = hci_powered();
|
||||
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired */
|
||||
c->conn_n = net_bt_connected(c->conn, S_NET_MAX);
|
||||
c->paired_n = net_bt_paired(c->paired, S_NET_MAX);
|
||||
c->pool_off = 0; /* 行副文本池绕回(上轮行字符串已被重填替换) */
|
||||
c->bt_on = nm_bt_powered(); /* IPC status;回退直连 hci_powered() */
|
||||
c->output_bt = 0;
|
||||
if (meow_settings_get_bool("audio.output_bt", &c->output_bt) != 0) c->output_bt = 0;
|
||||
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired/a2dp(全权威) */
|
||||
c->conn_n = nm_bt_connected(c->conn, S_NET_MAX);
|
||||
c->paired_n = nm_bt_paired(c->paired, S_NET_MAX);
|
||||
c->a2dp_n = nm_bt_a2dp(c->a2dp, S_NET_MAX);
|
||||
if (c->conn_n < 0) c->conn_n = 0;
|
||||
if (c->paired_n < 0) c->paired_n = 0;
|
||||
if (c->a2dp_n < 0) c->a2dp_n = 0;
|
||||
}
|
||||
c->near_n = net_bt_scan(c->near, S_NET_MAX); /* 附近:读 /tmp 缓存(快,实测无 popen) */
|
||||
c->near_n = nm_bt_devices(c->near, S_NET_MAX); /* 附近:扫描缓存(netmgrd 读 /tmp,快) */
|
||||
if (c->near_n < 0) c->near_n = 0;
|
||||
/* 角色描述:节流(≤5s)重探,读缓存,避免子页驻留期间反复 popen */
|
||||
net_bt_mode_str(c->mode, sizeof c->mode);
|
||||
|
||||
int n = 0;
|
||||
/* 已连接设备 */
|
||||
/* 降级占位:netmgrd 不可达 → 提示(直连回退仍在跑,保功能) */
|
||||
if (!netmgr_ok() && n < max) {
|
||||
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
||||
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
||||
}
|
||||
|
||||
/* ② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC) */
|
||||
{
|
||||
const bt_ep_t *ep = NULL;
|
||||
for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; }
|
||||
if (!ep && c->a2dp_n > 0) ep = &c->a2dp[0];
|
||||
const char *sub;
|
||||
if (c->conn_n > 0 && n < max) {
|
||||
const char *nm = bt_conn_name(c, ep ? ep->mac : c->conn[0].mac);
|
||||
if (ep && (ep->codec[0] || ep->rate[0]))
|
||||
sub = bt_pool(c, "%s%s%s · 输出=%s",
|
||||
ep->codec[0] ? ep->codec : "?", ep->codec[0] && ep->rate[0] ? " " : "",
|
||||
ep->rate[0] ? ep->rate : "", c->output_bt ? "蓝牙" : "DAC");
|
||||
else
|
||||
sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC");
|
||||
rbuf[n].kind = SUB_ROW; rbuf[n].label = nm ? nm : c->conn[0].mac;
|
||||
rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++;
|
||||
} else if (n < max) {
|
||||
sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC");
|
||||
rbuf[n].kind = SUB_ROW; rbuf[n].label = "未连接蓝牙设备";
|
||||
rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++;
|
||||
}
|
||||
}
|
||||
|
||||
/* ③ 输出目标(蓝牙/本地) */
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "输出目标"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
if (n < max) {
|
||||
rbuf[n].kind = SUB_ACT; rbuf[n].label = "输出目标";
|
||||
rbuf[n].sub = c->output_bt ? "蓝牙(耳机/远端)" : "本地(DAC)";
|
||||
rbuf[n].on = c->output_bt; rbuf[n].act = BT_ACT_OUTPUT; n++;
|
||||
}
|
||||
|
||||
/* ④ 已连接设备 */
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已连接设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
if (c->conn_n == 0) {
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已连接设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
} else for (int i = 0; i < c->conn_n && n < max; i++) {
|
||||
const bt_ep_t *ep = bt_ep_idx(c, c->conn[i].mac) >= 0 ? &c->a2dp[bt_ep_idx(c, c->conn[i].mac)] : NULL;
|
||||
const char *sub;
|
||||
if (ep && ep->codec[0])
|
||||
sub = bt_pool(c, "%s%s%s%s",
|
||||
ep->codec[0] ? ep->codec : "", ep->rate[0] ? " " : "", ep->rate[0] ? "· " : "",
|
||||
ep->acquired ? "播放中" : "");
|
||||
else sub = c->conn[i].mac; /* mac 拷在 ctx(稳定) */
|
||||
rbuf[n].kind = SUB_ACT;
|
||||
rbuf[n].label = c->conn[i].name[0] ? c->conn[i].name : c->conn[i].mac;
|
||||
rbuf[n].sub = c->conn[i].mac;
|
||||
rbuf[n].sub = sub[0] ? sub : c->conn[i].mac;
|
||||
rbuf[n].on = 1; rbuf[n].act = BT_ACT_CONN_BASE + i;
|
||||
n++;
|
||||
}
|
||||
/* 已配对设备 */
|
||||
|
||||
/* ⑤ 已配对设备 */
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已配对设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
if (c->paired_n == 0) {
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已配对设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
@@ -110,7 +212,8 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||
rbuf[n].act = BT_ACT_PAIRED_BASE + i;
|
||||
n++;
|
||||
}
|
||||
/* 附近设备(扫描发现,未配对) */
|
||||
|
||||
/* ⑥ 附近设备(扫描发现,未配对;netmgrd 权威扫描缓存) */
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
if (c->near_n == 0) {
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "附近无设备(点顶部开关后自动扫描)"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
@@ -123,30 +226,66 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||
rbuf[n].on = 0; rbuf[n].act = BT_ACT_NEAR_BASE + i;
|
||||
n++;
|
||||
}
|
||||
|
||||
/* ⑦ 音质/连接管理 */
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "音质 / 连接管理"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
{
|
||||
int idx = BT_CODEC_AUTO;
|
||||
char codec[16] = "auto";
|
||||
if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0)
|
||||
for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx = k; break; }
|
||||
rbuf[n].kind = SUB_ACT; rbuf[n].label = "编解码器";
|
||||
rbuf[n].sub = bt_pool(c, "%s%s", bt_codec_names[idx], idx ? "(偏好,P2 生效)" : "");
|
||||
rbuf[n].on = 0;
|
||||
rbuf[n].act = BT_ACT_CODEC; n++;
|
||||
}
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "SBC 质量"; rbuf[n].sub = "P2:bluealsa --sbc-quality 启动参数"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "强制 44.1k"; rbuf[n].sub = "P2:--a2dp-force-audio-cd"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "音量控制"; rbuf[n].sub = "蓝牙音量由耳机/远端(AVRCP)控制"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
|
||||
/* ⑧ 功耗档 */
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "功耗"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "扫描节流"; rbuf[n].sub = "进页自动扫描,退页/7s 停止"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "发现超时"; rbuf[n].sub = "source 无需可发现(有限超时)"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "后台保持"; rbuf[n].sub = "未播放时无流零开销"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
/* 执行 bluetoothctl 命令(带超时)+ 结果弹窗反馈 */
|
||||
static void bt_run(const char *title, const char *cmd)
|
||||
/* 动作反馈(netmgrd 结果 / 直连回退结果)弹窗反馈 */
|
||||
static void bt_feedback(const char *title, int rc)
|
||||
{
|
||||
char *o = popen_read_tmo(8000, cmd);
|
||||
char line[48];
|
||||
const char *txt[4]; txt[0] = line;
|
||||
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
|
||||
strstr(o, "already") || strstr(o, "Paired"));
|
||||
snprintf(line, sizeof line, "%s", ok ? "操作成功" : "操作失败/超时");
|
||||
if (o) free(o);
|
||||
const char *txt[S_NET_MAX]; txt[0] = line;
|
||||
snprintf(line, sizeof line, "%s", rc == MEOW_OK ? "操作成功" : "操作失败/超时");
|
||||
s_dd_modal(title, txt, 1, -1);
|
||||
}
|
||||
|
||||
|
||||
/* 动作行(SUB_ACT)点击:连接/断开/配对 */
|
||||
/* 动作行(SUB_ACT)点击:连接/断开/配对/取消配对/输出目标/编解码器 */
|
||||
static int bt_act(void *ctx, int idx, int act)
|
||||
{
|
||||
(void)idx;
|
||||
bt_ctx_t *c = ctx;
|
||||
char mac[24] = "";
|
||||
/* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */
|
||||
if (act == BT_ACT_OUTPUT) {
|
||||
c->output_bt = !c->output_bt;
|
||||
meow_settings_set_bool("audio.output_bt", c->output_bt);
|
||||
meow_settings_save();
|
||||
logmsg("bt_page: output_target=%s", c->output_bt ? "bt" : "dac");
|
||||
return 1; /* 重绘 + 重填行 */
|
||||
}
|
||||
if (act == BT_ACT_CODEC) {
|
||||
char codec[16] = "auto"; int idx0 = BT_CODEC_AUTO;
|
||||
if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0)
|
||||
for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx0 = k; break; }
|
||||
int ni = (idx0 + 1) % BT_CODEC_N;
|
||||
meow_settings_set_str("bt.codec", bt_codec_names[ni]);
|
||||
meow_settings_save();
|
||||
logmsg("bt_page: codec=%s", bt_codec_names[ni]);
|
||||
return 1;
|
||||
}
|
||||
/* 定位 MAC(按 act 段) */
|
||||
if (act >= BT_ACT_NEAR_BASE) {
|
||||
int i = act - BT_ACT_NEAR_BASE;
|
||||
@@ -161,43 +300,75 @@ static int bt_act(void *ctx, int idx, int act)
|
||||
if (i < 0 || i >= c->conn_n) return 0;
|
||||
snprintf(mac, sizeof mac, "%s", c->conn[i].mac);
|
||||
}
|
||||
|
||||
if (!mac[0]) return 0;
|
||||
|
||||
if (act >= BT_ACT_NEAR_BASE) {
|
||||
/* 附近未配对 → pair;已配对(理论上过滤了)→ connect */
|
||||
char cmd[192];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl pair %s 2>/dev/null", mac);
|
||||
bt_run("配对", cmd);
|
||||
/* 附近:未配对 → pair;已配对(理论上过滤了)→ connect */
|
||||
bt_feedback("配对", nm_bt_pair(mac));
|
||||
} else if (act >= BT_ACT_PAIRED_BASE) {
|
||||
/* 已配对:未连 → connect;已连 → disconnect */
|
||||
if (bt_is_conn(c, mac)) {
|
||||
char cmd[192];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac);
|
||||
bt_run("断开", cmd);
|
||||
} else {
|
||||
char cmd[192];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl connect %s 2>/dev/null", mac);
|
||||
bt_run("连接", cmd);
|
||||
}
|
||||
if (bt_is_conn(c, mac)) bt_feedback("断开", nm_bt_disconnect(mac));
|
||||
else bt_feedback("连接", nm_bt_connect(mac));
|
||||
} else {
|
||||
/* 已连接 → disconnect */
|
||||
char cmd[192];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac);
|
||||
bt_run("断开", cmd);
|
||||
bt_feedback("断开", nm_bt_disconnect(mac));
|
||||
}
|
||||
bt_role_refresh(1);
|
||||
net_bt_scan_trigger();
|
||||
nm_bt_scan_on();
|
||||
c->full = 1; /* 动作后:下一次 rows() 全量重扫 */
|
||||
logmsg("bt_page: act act=%d mac=%s", act, mac);
|
||||
return 1; /* 重绘 + 重填行 */
|
||||
}
|
||||
|
||||
|
||||
/* 点普通行(A2DP 摘要等):弹 A2DP/设备详情。仅 A2DP 摘要(act=-1)响应;其余 info 行(act=-2)无操作。 */
|
||||
static int bt_detail(void *ctx, int idx, int act)
|
||||
{
|
||||
(void)idx;
|
||||
if (act != -1) return 0;
|
||||
bt_ctx_t *c = ctx;
|
||||
if (c->a2dp_n > 0) {
|
||||
const bt_ep_t *ep = &c->a2dp[0];
|
||||
char buf[S_NET_MAX][48];
|
||||
const char *item[S_NET_MAX];
|
||||
int n = 0;
|
||||
for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; }
|
||||
snprintf(buf[n], 48, "设备名: %s", bt_conn_name(c, ep->mac)); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "MAC: %s", ep->mac[0] ? ep->mac : "-"); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "codec: %s", ep->codec[0] ? ep->codec : "-"); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "采样率: %s", ep->rate[0] ? ep->rate : "-"); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "声道/位深: %s/%s", ep->chn[0] ? ep->chn : "-", ep->bits[0] ? ep->bits : "-");
|
||||
item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "传输: %s", ep->acquired ? "播放中" : "空闲"); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "角色: %s", !strcmp(ep->role, "src") ? "设备=source(送流)" :
|
||||
!strcmp(ep->role, "snk") ? "设备=sink(被连)" : "未知");
|
||||
item[n] = buf[n]; n++;
|
||||
s_dd_modal("A2DP 详情", item, n, -1);
|
||||
return 1;
|
||||
}
|
||||
if (c->conn_n > 0) {
|
||||
char buf[S_NET_MAX][48];
|
||||
const char *item[S_NET_MAX];
|
||||
int n = 0;
|
||||
snprintf(buf[n], 48, "设备名: %s", c->conn[0].name[0] ? c->conn[0].name : c->conn[0].mac);
|
||||
item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "MAC: %s", c->conn[0].mac); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "状态: 已连接"); item[n] = buf[n]; n++;
|
||||
snprintf(buf[n], 48, "输出: %s", c->output_bt ? "蓝牙" : "DAC"); item[n] = buf[n]; n++;
|
||||
s_dd_modal("蓝牙详情", item, n, -1);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* 运行蓝牙独立子页面(阻塞到返回) */
|
||||
int run_bt_page(void)
|
||||
{
|
||||
static bt_ctx_t c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.conn_n = -1;
|
||||
sub_page_t p;
|
||||
memset(&p, 0, sizeof p);
|
||||
p.title = "蓝牙";
|
||||
@@ -205,11 +376,13 @@ int run_bt_page(void)
|
||||
p.top_sw_set = bt_top_sw_set;
|
||||
p.rows = bt_rows;
|
||||
p.act = bt_act;
|
||||
p.detail = NULL; /* 蓝牙动作都走 SUB_ACT(act),无 SUB_ROW 详情 */
|
||||
p.detail = bt_detail; /* SUB_ROW(A2DP 摘要等)→ A2DP 详情弹窗 */
|
||||
p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */
|
||||
p.ctx = &c;
|
||||
c.full = 1; /* 首行全量重扫 */
|
||||
net_bt_scan_trigger(); /* 进页立即触发一次附近扫描(异步,不阻塞) */
|
||||
nm_bt_scan_on(); /* 进页:触发一次附近扫描(节流;不进蓝牙不空转 §七) */
|
||||
bt_role_refresh(1);
|
||||
return meow_subpage_run(&p);
|
||||
int rc = meow_subpage_run(&p);
|
||||
nm_bt_scan_off(); /* 退页:扫描收尾(§七 省电) */
|
||||
return rc;
|
||||
}
|
||||
@@ -25,9 +25,12 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||
"$CC" -I"../sdk" -Os -static \
|
||||
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
|
||||
notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
|
||||
subpage.c wifi_page.c bt_page.c now_playing.c \
|
||||
font_theme.c keys.c util.c \
|
||||
meow_touch.c meow_settings.c meow_notif.c meow_media.c \
|
||||
subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \
|
||||
font_theme.c keys.c util.c meow_dep.c key_router.c \
|
||||
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
|
||||
meow_bgmgr.c \
|
||||
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
||||
"../sdk/meow_lyric.c" "../sdk/meow_lyric_view.c" "../sdk/meow_img_color.c" \
|
||||
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
|
||||
echo "== meow_launcherd =="
|
||||
ls -la meow_launcherd
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/poll.h>
|
||||
#include <poll.h>
|
||||
#include <linux/input.h>
|
||||
|
||||
#ifndef EVIOCGRAB
|
||||
@@ -147,18 +147,15 @@ static void loop_forward(const char *dev, int fwd)
|
||||
fflush(stdout);
|
||||
struct pollfd pfd = { fd, POLLIN, 0 };
|
||||
unsigned char ev[16];
|
||||
unsigned long long nread = 0, nwritten = 0;
|
||||
for (;;) {
|
||||
int r = poll(&pfd, 1, -1);
|
||||
if (r < 0) { perror("poll"); break; }
|
||||
if (pfd.revents & POLLIN) {
|
||||
ssize_t n = read(fd, ev, sizeof ev);
|
||||
if (n != 16) { if (n < 0) perror("read"); continue; }
|
||||
nread++;
|
||||
print_ev("G", ev);
|
||||
if (fwd) {
|
||||
ssize_t w = write(fd, ev, sizeof ev);
|
||||
nwritten++;
|
||||
if (w != 16) fprintf(stderr, " write-back failed: %s\n", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
PASS S1 drain required (app was SIGSTOP/frozen)
|
||||
PASS S1 no drain needed (media keep-alive, never stopped)
|
||||
PASS S1 drain window > SDK 200ms throttle
|
||||
PASS S1b keep-alive app needs EVIOCGRAB isolation
|
||||
PASS S1b frozen app -> SIGCONT drain, not grab
|
||||
PASS S1b keep-alive: grab path (drain==0)
|
||||
PASS S1b frozen: drain path (grab==0)
|
||||
PASS S2 force status bar re-sync after unlock
|
||||
PASS S2b lock re-cover ms > SDK suppression window (200ms)
|
||||
PASS S2b lock re-cover ms = 300 (marker-window margin)
|
||||
PASS S1c wake, no sleep -> no lock
|
||||
PASS S1c timeout sleep (auto) -> lock
|
||||
PASS S1c manual power sleep (manual) -> lock (was missing)
|
||||
PASS S1c both -> lock
|
||||
PASS S4 system 0 (never) -> lock caps 30s
|
||||
PASS S4 system absent -> lock caps 30s
|
||||
PASS S4 system 30 -> 30
|
||||
PASS S4 system 60 -> 30 (cap, shorter)
|
||||
PASS S4 system 10 -> 10 (respect shorter)
|
||||
PASS S4 system 1 -> 1 (respect shorter)
|
||||
PASS S4 idle>=off & screen on -> self sleep
|
||||
PASS S4 idle<off -> not yet
|
||||
PASS S4 screen off -> no re-sleep
|
||||
PASS S4 power edge -> wake to lock
|
||||
PASS S4 touch -> wake to lock
|
||||
PASS S4 no wake input -> stay asleep
|
||||
PASS S3 app died -> unified exit path (redraw home)
|
||||
PASS S3 app alive -> normal restore
|
||||
PASS S3 abort=0 (menu lock, no overlay pid)
|
||||
PASS S3 abort=0 (no overlay pid, zombie ignored)
|
||||
PASS S3 abort=0 (overlay pid, app alive)
|
||||
PASS S3 abort=1 (overlay pid + zombie -> abort lock)
|
||||
|
||||
ALL PASS (32 passed, 0 failed)
|
||||
@@ -474,7 +474,7 @@ static void font_tier_marker(const char *charset, int glyph_count, char *out, in
|
||||
* 输出:label=显示名 `<name> <Weight>(common≈4k)`(如 `MiSans Regular(common≈4k)`);
|
||||
* charset/glyph_count=档位信息;wtout=weight slug(缺省 "regular")。 */
|
||||
static int meow_font_probe_weight(const char *dirpath, const char *family, const char *weight,
|
||||
const char *tier_dir,
|
||||
const char *tier_dir, int verify_sha,
|
||||
char *label, int llen,
|
||||
char *charset, int clen, int *glyph_count,
|
||||
char *wtout, int wtlen, char *nameout, int namelen)
|
||||
@@ -506,16 +506,20 @@ static int meow_font_probe_weight(const char *dirpath, const char *family, const
|
||||
snprintf(label, llen, "%s %s(%s)", name[0] ? name : family, wd, marker);
|
||||
if (charset) snprintf(charset, clen, "%s", tier);
|
||||
if (glyph_count) *glyph_count = gc;
|
||||
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */
|
||||
char apng[80] = "", aidx[80] = "";
|
||||
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
|
||||
logmsg("fonts: '%s/%s%s%s' sha256(png) 不可用,跳过校验", family, wt, tsep, tiernm);
|
||||
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
|
||||
logmsg("fonts: '%s/%s%s%s' WARN sha256_png 不匹配(仍可选)", family, wt, tsep, tiernm);
|
||||
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
|
||||
logmsg("fonts: '%s/%s%s%s' sha256(idx) 不可用,跳过校验", family, wt, tsep, tiernm);
|
||||
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
|
||||
logmsg("fonts: '%s/%s%s%s' WARN sha256_idx 不匹配(仍可选)", family, wt, tsep, tiernm);
|
||||
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。
|
||||
* M3:verify_sha=0(进设置页重扫)跳过逐套 sha256sum(gbk/full 套数 MB,逐套双 sha256
|
||||
* 真机耗数秒,入口卡顿主因之一);完整性校验保留给启动全量扫描(verify_sha=1)。 */
|
||||
if (verify_sha) {
|
||||
char apng[80] = "", aidx[80] = "";
|
||||
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
|
||||
logmsg("fonts: '%s/%s%s%s' sha256(png) 不可用,跳过校验", family, wt, tsep, tiernm);
|
||||
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
|
||||
logmsg("fonts: '%s/%s%s%s' WARN sha256_png 不匹配(仍可选)", family, wt, tsep, tiernm);
|
||||
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
|
||||
logmsg("fonts: '%s/%s%s%s' sha256(idx) 不可用,跳过校验", family, wt, tsep, tiernm);
|
||||
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
|
||||
logmsg("fonts: '%s/%s%s%s' WARN sha256_idx 不匹配(仍可选)", family, wt, tsep, tiernm);
|
||||
}
|
||||
logmsg("fonts: add '%s/%s%s%s' (%s) tier=%s glyph_count=%d", family, wt, tsep, tiernm, label, tier, gc);
|
||||
return 1;
|
||||
}
|
||||
@@ -637,7 +641,7 @@ static int font_presets_add(const char *family, const char *weight, const char *
|
||||
* 选库)与 meow_font_reload_multifont 在运行期持续读 font_presets_n,清空会造成大字库
|
||||
* 档位退化(red line「保持档位/多字体行为」)。如需「零驻留」,须把这几个核心路径改为
|
||||
* 直接读 index(较大重构,超出本批边界,留待后续)。 */
|
||||
void meow_font_scan_presets_root(const char *card_root)
|
||||
void meow_font_scan_presets_root(const char *card_root, int verify_sha)
|
||||
{
|
||||
font_presets_n = 0;
|
||||
font_presets[0] = "MeowOS/fonts/default/regular/font.png"; /* 默认字库 = MeowOS/fonts/default/regular */
|
||||
@@ -684,7 +688,7 @@ void meow_font_scan_presets_root(const char *card_root)
|
||||
char label[96] = "", cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = "";
|
||||
if (access(pngcheck, R_OK) == 0) {
|
||||
/* 两级(旧结构向后兼容):<family>/<weight>/font.png,tier 读 font.json charset。 */
|
||||
if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, NULL,
|
||||
if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, NULL, verify_sha,
|
||||
label, sizeof label, cs, sizeof cs, &gc,
|
||||
wt, sizeof wt, nm, sizeof nm) == 0)
|
||||
continue; /* 缺组件/损坏 → 跳过该套 */
|
||||
@@ -702,7 +706,7 @@ void meow_font_scan_presets_root(const char *card_root)
|
||||
strcmp(te->d_name, "full")) continue;
|
||||
char tpath[420];
|
||||
snprintf(tpath, sizeof tpath, "%s/%s", dirpath, te->d_name);
|
||||
if (meow_font_probe_weight(tpath, de->d_name, we->d_name, te->d_name,
|
||||
if (meow_font_probe_weight(tpath, de->d_name, we->d_name, te->d_name, verify_sha,
|
||||
label, sizeof label, cs, sizeof cs, &gc,
|
||||
wt, sizeof wt, nm, sizeof nm) == 0)
|
||||
continue; /* 缺组件/损坏 → 跳过该 tier 套 */
|
||||
@@ -728,13 +732,20 @@ void meow_font_scan_presets_root(const char *card_root)
|
||||
}
|
||||
|
||||
|
||||
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。 */
|
||||
void meow_font_scan_presets(void)
|
||||
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。
|
||||
* verify_sha=1:启动/热插拔全量扫描(含逐套 sha256 完整性校验)。
|
||||
* verify_sha=0:进设置页重扫(只枚举+读 font.json+落 index,跳过 sha256 —— 入口不许卡数秒)。 */
|
||||
void meow_font_scan_presets(int verify_sha)
|
||||
{
|
||||
meow_font_scan_presets_root("/usr/data/mnt/sd_0");
|
||||
meow_font_scan_presets_root("/usr/data/mnt/sd_0", verify_sha);
|
||||
}
|
||||
|
||||
|
||||
#if 0 /* ================================================================
|
||||
* M7.0b 主题退役:launcher 不再自持 4 套静态色板 —— s_ui_theme() 改读 SDK
|
||||
* meow_theme_export()(单一权威:MD3/MIUIx × 明暗 由 SDK meow_theme 解析)。
|
||||
* 本块保留作回退位(删除 #if 0 行即可恢复旧手绘多色板路径)。
|
||||
* ================================================================ */
|
||||
/* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */
|
||||
static const meow_ui_theme_t meow_ui_theme_dark = {
|
||||
0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF,
|
||||
@@ -746,6 +757,18 @@ static const meow_ui_theme_t meow_ui_theme_light = {
|
||||
0x4A4F5A, 0x9AA4C8, 0xB3261E, 0x2A5A8A
|
||||
};
|
||||
|
||||
/* MIUIx 族(§8.1.3 定案):暖色基面 + 小米橙 primary,供 launcher 主界面/设置页/通知中心/
|
||||
* 电源菜单(s_ui_theme())在 ui.style=miuix 时使用(与 SDK meow_theme MIUIx 同色系)。 */
|
||||
static const meow_ui_theme_t meow_ui_theme_miui_dark = {
|
||||
0x181310, 0x2A231C, 0x2E2620, 0xFF8C4A, 0xEDE5DC,
|
||||
0xAB9884, 0x4A3F35, 0xFFB4AB, 0xFF8C4A
|
||||
};
|
||||
|
||||
static const meow_ui_theme_t meow_ui_theme_miui_light = {
|
||||
0xF7F3EE, 0xE9DFD0, 0xF4EDE3, 0xF57200, 0x251A10,
|
||||
0x7D6A5A, 0xCBBFB2, 0xB3261E, 0xF57200
|
||||
};
|
||||
|
||||
/* 获取当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用 */
|
||||
static int s_now_min_of_day(void)
|
||||
{
|
||||
@@ -768,13 +791,18 @@ static int theme_should_dark(int now_min, int start, int end)
|
||||
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
|
||||
}
|
||||
|
||||
/* 当前主题(dark/light 两套;接 display.theme_mode + display.theme)。
|
||||
/* 当前主题(style×明暗 两套;接 ui.style + display.theme_mode + display.theme)。
|
||||
* style(§8.1.1):ui.style 的 md3/miuix → 返回对应体系(MD3 tonal / MIUIx 暖色+橙)。
|
||||
* manual(mode==THEME_MODE_MANUAL):直读 display.theme(dark/light),保持原有行为。
|
||||
* auto(mode==THEME_MODE_AUTO):读深色时段两键 + 当前本地分钟;start==end 未配置 →
|
||||
* 回退 manual(读 display.theme,避免误判全天深色)。
|
||||
* 读取失败一律回退 dark。 */
|
||||
const meow_ui_theme_t *s_ui_theme(void)
|
||||
{
|
||||
int style = 0; /* ui.style:0=md3 / 1=miuix */
|
||||
if (meow_settings_get_enum("ui.style", &style) != 0) style = 0;
|
||||
const meow_ui_theme_t *light = (style == 1) ? &meow_ui_theme_miui_light : &meow_ui_theme_light;
|
||||
const meow_ui_theme_t *dark = (style == 1) ? &meow_ui_theme_miui_dark : &meow_ui_theme_dark;
|
||||
int mode = THEME_MODE_MANUAL;
|
||||
if (meow_settings_get_enum("display.theme_mode", &mode) != 0) mode = THEME_MODE_MANUAL;
|
||||
int v = 0; /* display.theme(manual)当前索引 */
|
||||
@@ -785,13 +813,36 @@ const meow_ui_theme_t *s_ui_theme(void)
|
||||
meow_settings_get_int("display.theme_dark_end", &end);
|
||||
int d = theme_should_dark(s_now_min_of_day(), start, end);
|
||||
if (d < 0) { /* 未配置:回退 manual(display.theme) */
|
||||
if (have_manual && v == 1) return &meow_ui_theme_light;
|
||||
return &meow_ui_theme_dark;
|
||||
if (have_manual && v == 1) return light;
|
||||
return dark;
|
||||
}
|
||||
return d ? &meow_ui_theme_dark : &meow_ui_theme_light;
|
||||
return d ? dark : light;
|
||||
}
|
||||
/* manual:照旧读 display.theme */
|
||||
if (have_manual && v == 1) return &meow_ui_theme_light;
|
||||
return &meow_ui_theme_dark;
|
||||
if (have_manual && v == 1) return light;
|
||||
return dark;
|
||||
}
|
||||
#endif /* M7.0b 退役:launcher 4 套静态色板 → SDK meow_theme 单一权威 */
|
||||
|
||||
/* M7.0b 主题薄壳:launcher 主题 = SDK meow_theme_export()(MD3/MIUIx × 明暗 由 SDK 解析,
|
||||
* launcher 侧不再读 ui.style/display.theme 自行选板)。只做一次拷贝 + 9 字段映射到 launcher
|
||||
* 绘制便利结构 meow_ui_theme_t(§8.1.5)。返回静态缓存(调用方立即使用,不长期持有)。
|
||||
* 注:meow_theme_export() 在 g_loaded==0 时先读 settings.conf,宿主/脱机也可靠。 */
|
||||
const meow_ui_theme_t *s_ui_theme(void)
|
||||
{
|
||||
meow_theme th;
|
||||
static meow_ui_theme_t s;
|
||||
meow_theme_export(&th);
|
||||
/* 映射:SDK meow_theme → launcher meow_ui_theme_t(9 字段,§8.1.5) */
|
||||
s.surface = th.surface;
|
||||
s.surface_variant = th.surfaceVariant;
|
||||
s.surface_container = th.surfaceContainer;
|
||||
s.primary = th.primary;
|
||||
s.on_surface = th.onSurface;
|
||||
s.on_surface_variant = th.onSurfaceVariant;
|
||||
s.outline = th.outline;
|
||||
s.error = th.error;
|
||||
s.surface_tint = th.surfaceTint;
|
||||
return &s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/* key_router.c — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
|
||||
*
|
||||
* 本模块持有一个进程内唯一的音量键防抖/基线:
|
||||
* kp_keys / kp_power(事件设备 poll 态,key_router_init 单开)
|
||||
* kd_vold / kd_volu(40ms 防抖态,KEY_DEBOUNCE_MS)
|
||||
* m_prev_vold / m_prev_volu(音量边沿基线)
|
||||
* 各 launcher-UI 页每圈调 key_router_pump() —— 它只产「音量边沿 + 原始 held + ramp 状态」,
|
||||
* 产出的边沿由「谁在跑循环」天然归属当前页;页据此决定 settings_volume_adj(±step) /
|
||||
* 光标导航 / ramp 推进。这样 run_menu 的 meow_mode 分支(HibyOS overlay 音量=导航)不被破坏,
|
||||
* 其余页(设置/通知中心/Now Playing/子页)用默认「音量=调音量」。
|
||||
*
|
||||
* 边界(§3 P1 / §4):
|
||||
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸;launcher-UI
|
||||
* 下 sink 恒空,此闸是「不改变行为」的保险)。
|
||||
* - 播放键/电源键 P1 不路由(各页现状不动;播放键严禁 cmd=6)。
|
||||
* - 锁屏(EVIOCGRAB)/App 会话层不调用本模块 —— 边界由「调用栈里当前循环是不是 launcher-UI 页」
|
||||
* 自动强加。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
|
||||
static int g_inited = 0;
|
||||
|
||||
/* 进程唯一事件设备 poll 态(key_router_init 单开;各页另自开 kp_* 仅供播放/电源键用)。 */
|
||||
static struct keypoll kp_power, kp_keys;
|
||||
|
||||
/* 进程唯一音量键防抖态 + 边沿基线。 */
|
||||
static struct kd_state kd_vold, kd_volu;
|
||||
static int m_prev_vold = 0, m_prev_volu = 0;
|
||||
|
||||
/* 当前活动页哨兵(key_router_page_enter/exit 登记;与 sdk meow_ui_activity 交叉校验用)。 */
|
||||
static int g_page_sentinel = 0;
|
||||
|
||||
|
||||
void key_router_init(void)
|
||||
{
|
||||
if (g_inited) return;
|
||||
g_inited = 1;
|
||||
memset(&kp_power, 0, sizeof kp_power);
|
||||
memset(&kp_keys, 0, sizeof kp_keys);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
m_prev_vold = 0; m_prev_volu = 0;
|
||||
kp_open(&kp_power, DEV_POWER);
|
||||
kp_open(&kp_keys, DEV_KEYS);
|
||||
/* 基线对齐当前 raw:进入时已按住的音量键不产假边沿。 */
|
||||
key_router_rebase();
|
||||
}
|
||||
|
||||
|
||||
void key_router_rebase(void)
|
||||
{
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
if (!g_inited) return;
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
}
|
||||
|
||||
|
||||
key_evt_t key_router_pump(void)
|
||||
{
|
||||
key_evt_t ev;
|
||||
memset(&ev, 0, sizeof ev);
|
||||
if (!g_inited) key_router_init();
|
||||
|
||||
/* 设备晚就绪兜底:init 时 open 失败则此处每圈重试(open 失败快速返回,无阻塞)。 */
|
||||
if (!kp_keys.ok) kp_open(&kp_keys, DEV_KEYS);
|
||||
if (!kp_power.ok) kp_open(&kp_power, DEV_POWER);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
|
||||
int vold = 0, volu = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
}
|
||||
/* 原始(未防抖)值:长按 ramp held 用(松开立即停止;同 run_menu/now_playing 现状)。 */
|
||||
ev.vol_held_dn = vold;
|
||||
ev.vol_held_up = volu;
|
||||
(void)kd_tick(&kd_vold, vold);
|
||||
(void)kd_tick(&kd_volu, volu);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
ev.vol_st_dn = vold; ev.vol_st_up = volu;
|
||||
|
||||
if (meow_volpage_armed()) {
|
||||
/* 防御闸:App 前台 volume_page 武装时(launcher-UI 下恒空)不放行音量边沿。
|
||||
* 音量键在那态下由 app_launcher 的 meow_volpage_key 消费(翻页),不落系统调音。 */
|
||||
ev.vol_dn_edge = 0;
|
||||
ev.vol_up_edge = 0;
|
||||
} else {
|
||||
ev.vol_dn_edge = (vold && !m_prev_vold);
|
||||
ev.vol_up_edge = (volu && !m_prev_volu);
|
||||
}
|
||||
ev.ramping = (vol_ramp_dir != 0);
|
||||
|
||||
m_prev_vold = vold;
|
||||
m_prev_volu = volu;
|
||||
return ev;
|
||||
}
|
||||
|
||||
|
||||
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input)
|
||||
{
|
||||
if (ev->vol_dn_edge) {
|
||||
settings_volume_adj(-settings_volume_step);
|
||||
if (last_input) *last_input = now_ms();
|
||||
logmsg("key_router: volume DOWN <- step=%d", settings_volume_step);
|
||||
}
|
||||
if (ev->vol_up_edge) {
|
||||
settings_volume_adj(+settings_volume_step);
|
||||
if (last_input) *last_input = now_ms();
|
||||
logmsg("key_router: volume UP <- step=%d", settings_volume_step);
|
||||
}
|
||||
/* 在 settings_volume_adj 之后重查 vol_ramp_dir:同圈「起 ramp」即立刻迈第一步(同现状
|
||||
* 各页「settings_volume_adj 后 if(vol_ramp_dir) vol_ramp_tick」),且长按续爬随每圈推进。 */
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? ev->vol_held_up : ev->vol_held_dn;
|
||||
(void)vol_ramp_tick(held);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void key_router_page_enter(int sentinel)
|
||||
{
|
||||
g_page_sentinel = sentinel;
|
||||
/* 与 sdk 活动栈交叉校验:哨兵非 0 且栈顶不一致 → 记录(不中止,防宿主/真机栈深度差异)。 */
|
||||
if (sentinel) {
|
||||
int act = meow_ui_activity_active();
|
||||
if (act && act != sentinel)
|
||||
logmsg("key_router: page_enter sentinel=%#x but active=%#x", sentinel, act);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void key_router_page_exit(int sentinel)
|
||||
{
|
||||
if (g_page_sentinel == sentinel) g_page_sentinel = 0;
|
||||
/* 页退出即重播基线(§6 嵌套返回风险项:返回瞬间不产假边沿)。 */
|
||||
key_router_rebase();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* key_router.h — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
|
||||
*
|
||||
* 设计(devdocs/plan/KEY_ROUTER_UNIFY.md §2.5 方案 C / §3 P1):
|
||||
* - 进程内**唯一**的 kd_*(防抖态)+ m_prev_*(边沿基线)+ kp_*(事件设备 poll 态),
|
||||
* 把「读边沿 + 防抖 + 长按 held + ramp 推进」这层从 5 个会话(run_menu /
|
||||
* run_settings_page / run_notif_center / run_now_playing / meow_subpage_run)
|
||||
* 收敛到一处。
|
||||
* - 每个 launcher-UI 页在其 for(;;) 每圈调一次 key_router_pump(),得到「防抖后的
|
||||
* 音量边沿 + 原始 held + ramp 状态」;**真正的动作**(音量边沿 →
|
||||
* settings_volume_adj(±step),还是光标导航)由调用页决定 —— 这样 run_menu 的
|
||||
* meow_mode 分支(HibyOS overlay:音量=导航;MeowOS:音量=调音量)得以保留。
|
||||
* - 非导航页可经 key_router_default_volume()(内含 settings_volume_adj + vol_ramp_tick +
|
||||
* last_input 复位)直接用;run_menu 走自己的 meow_mode 分支。
|
||||
*
|
||||
* 范围与边界(§3 P1,勿越界):
|
||||
* - 本模块只收敛**音量键**(KEY_VOLUMEDOWN/KEY_VOLUMEUP)。播放键/电源键 P1 不路由,
|
||||
* 维持各页现状(播放键严禁改成 cmd=6;电源键单击/菜单/8s 状态机不动)。
|
||||
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸)。
|
||||
* - 锁屏层(EVIOCGRAB)/App 会话层从不调用 key_router_pump,天然退出本路由。
|
||||
*/
|
||||
#ifndef MEOW_KEY_ROUTER_H
|
||||
#define MEOW_KEY_ROUTER_H
|
||||
|
||||
/* 一框音量边沿/held/ramp 输出。P1 只填音量;播放/电源字段后续期(P2/P3)再扩展。
|
||||
* 字段语义:
|
||||
* vol_dn_edge / vol_up_edge —— 防抖后**稳定按下边沿**(= stable ∧ ¬上一 stable;
|
||||
* 1=本圈新按下)。这是「音量边沿」的唯一权威判定。
|
||||
* vol_held_dn / vol_held_up —— **原始(未防抖)**按住态,ramp 长按 held 用
|
||||
* (松开立即停止,同 run_menu/now_playing 现状)。
|
||||
* vol_st_dn / vol_st_up —— **防抖稳定**按住态(wake_suppress「全键抬起」判定用)。
|
||||
* ramping —— vol_ramp_dir != 0(线性渐变是否在跑;repaint 用)。
|
||||
*/
|
||||
typedef struct {
|
||||
int vol_dn_edge;
|
||||
int vol_up_edge;
|
||||
int vol_held_dn;
|
||||
int vol_held_up;
|
||||
int vol_st_dn;
|
||||
int vol_st_up;
|
||||
int ramping;
|
||||
} key_evt_t;
|
||||
|
||||
/* 进程级单开 kp_*(事件设备)+ 初始化防抖态/基线(幂等,可安全重复调用)。 */
|
||||
void key_router_init(void);
|
||||
|
||||
/* 每页 for(;;) 每圈调用一次:poll 两设备 → 防抖音量键 → 产边沿/held/ramp。
|
||||
* 不做 settings_volume_adj、不驱动 vol_ramp_tick、不复位 last_input —— 皆由调用页决定
|
||||
* (见 key_router_default_volume 与各页自己的分支)。 */
|
||||
key_evt_t key_router_pump(void);
|
||||
|
||||
/* 模态/菜单/嵌套会话返回/唤醒后重播基线:kd_* 清零 + m_prev_* 对齐当前 raw
|
||||
* (对齐现状各页「memset(kd_*) + kp_poll + m_prev_*=当前 raw」的语义,防假边沿)。
|
||||
* 各页在原本做该重播处调用。 */
|
||||
void key_router_rebase(void);
|
||||
|
||||
/* 页进入/退出登记(记录当前哨兵;与 sdk meow_ui_activity_active 交叉校验)。 */
|
||||
void key_router_page_enter(int sentinel);
|
||||
void key_router_page_exit(int sentinel);
|
||||
|
||||
/* 默认「音量键=调音量」页回调(非导航页直接用):音量边沿 → settings_volume_adj(±step);
|
||||
* 若有 ramp 在爬 → vol_ramp_tick(held);边沿 → 复位 *last_input。
|
||||
* run_menu 不走本函数(自行 meow_mode 分支)。 */
|
||||
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input);
|
||||
|
||||
#endif /* MEOW_KEY_ROUTER_H */
|
||||
@@ -40,7 +40,7 @@ void meow_panel_off(void)
|
||||
{
|
||||
meow_saved_brightness = read_brightness();
|
||||
if (meow_saved_brightness <= 0) meow_saved_brightness = 60;
|
||||
write_brightness(0);
|
||||
write_brightness(0, "meow_panel_off");
|
||||
if (screen_off_mode == 0) {
|
||||
/* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */
|
||||
int f = open(FB_DEV, O_RDWR);
|
||||
@@ -58,7 +58,17 @@ void meow_panel_on(void)
|
||||
{
|
||||
int f = open(FB_DEV, O_RDWR);
|
||||
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
|
||||
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60);
|
||||
/* 亮度恢复以**用户设置**为准,而非 meow_saved_brightness(可能过期)。
|
||||
* watchdog 独立息屏(wd_panel_off 直接 write_brightness(0),见 meow_watchdog.c)**不会**
|
||||
* 更新本进程 meow_saved_brightness——若用其恢复会回错值(真机「PIN 亮屏亮度跳高」,
|
||||
* 2026-09-03)。settings 已从盘上读到→按 display.brightness(clamp)恢复;未读到(早启动)
|
||||
* →回退 meow_saved_brightness 现值(保持旧行为,防开机竞态)。 */
|
||||
if (meow_settings_loaded()) {
|
||||
int b = meow_settings_brightness_clamped();
|
||||
if (b > 0) { write_brightness(b, "meow_panel_on"); meow_saved_brightness = b; }
|
||||
} else {
|
||||
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on");
|
||||
}
|
||||
meow_screen_on = 1;
|
||||
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
|
||||
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
|
||||
@@ -202,3 +212,92 @@ void load_config(void)
|
||||
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* 系统级音量键翻页门(READER_UI_DESIGN §E;用户 2026-09-04 拍板)。
|
||||
*
|
||||
* 语义:音量键是系统资源(默认调音量)。本门在「当前台 App 为其 app.json 声明
|
||||
* volume_page 的应用(MVP:os.meow.reader)」时,把 VOL_UP/VOL_DOWN 转换成系统翻页
|
||||
* 信号(MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该前台 App;其余情况一概
|
||||
* 放行(return 0),音量基本行为/插孔消抖/音量反转等绝不破坏。
|
||||
*
|
||||
* 职责分离(关键,且最小侵入):
|
||||
* - 本模块只提供「判定 + 门」:meow_volpage_app_allows() 读 app.json 声明;
|
||||
* meow_volpage_key() 在「已武装 sink 且按下 VOL」时消费并发下发;
|
||||
* - 武装(sink 注册)= 前台应用进入体积页声明 app 时调用 meow_volpage_sink()
|
||||
* (应用存在连接后其 fd 生效);离开/退出时 meow_volpage_sink_clear()。
|
||||
* 该接线落在「前台会话的按键路径」(app_launcher.c / meow_launcherd.c 主循环),
|
||||
* 属主会话统一收编范围,此处不越界改动。
|
||||
*
|
||||
* 声明字段(app.json):`"volume_page": true`(或 1)。未来其它 App 可声明复用。
|
||||
* ========================================================================= */
|
||||
|
||||
/* 前台翻页 sink(本模块私有;单写者=前台会话上下文,读=按键门)。 */
|
||||
static char g_volpage_app[MEOW_APP_ID_MAX] = "";
|
||||
static int g_volpage_fd = -1;
|
||||
|
||||
void meow_volpage_sink_clear(void); /* 前向声明(sink 内调用) */
|
||||
|
||||
/* 读 <MEOWAPPS>/<app_id>/app.json 是否声明 volume_page=true|1。1=声明;0=否/读不到。 */
|
||||
int meow_volpage_app_allows(const char *app_id)
|
||||
{
|
||||
if (!app_id || !app_id[0]) return 0;
|
||||
char path[320];
|
||||
snprintf(path, sizeof path, "%s/%s/app.json", MEOWAPPS, app_id);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
/* 轻量扫描:找 "volume_page" → 跳过空白 → true/1。app.json 为紧凑 JSON。 */
|
||||
char buf[1024];
|
||||
buf[0] = 0;
|
||||
size_t rd = fread(buf, 1, sizeof buf - 1, f);
|
||||
buf[rd] = 0;
|
||||
fclose(f);
|
||||
const char *p = buf;
|
||||
while ((p = strstr(p, "volume_page")) != NULL) {
|
||||
p += strlen("volume_page");
|
||||
while (*p == ' ' || *p == '\t' || *p == ':' || *p == '"') p++;
|
||||
if (strncmp(p, "true", 4) == 0) return 1;
|
||||
if (*p == '1' && (p[1] < '0' || p[1] > '9')) return 1;
|
||||
/* 未命中则继续找(防字段重复);跳过本段 */
|
||||
while (*p && *p != ',' && *p != '}') p++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 武装/清除 sink。app_id 非空且 fd>=0 → 记录;否则清除。幂等(同 app_id+fd 不重复记日志,
|
||||
* 供前台会话循环每圈安全重调,也兼容断线重连换 fd)。 */
|
||||
void meow_volpage_sink(const char *app_id, int fd)
|
||||
{
|
||||
if (!app_id || !app_id[0] || fd < 0) { meow_volpage_sink_clear(); return; }
|
||||
if (g_volpage_fd == fd && !strcmp(g_volpage_app, app_id)) return; /* 同 app_id+fd:幂等 */
|
||||
snprintf(g_volpage_app, sizeof g_volpage_app, "%s", app_id);
|
||||
g_volpage_fd = fd;
|
||||
logmsg("volpage: sink armed app=%s fd=%d", g_volpage_app, g_volpage_fd);
|
||||
}
|
||||
|
||||
void meow_volpage_sink_clear(void)
|
||||
{
|
||||
if (g_volpage_fd >= 0)
|
||||
logmsg("volpage: sink cleared app=%s", g_volpage_app[0] ? g_volpage_app : "?");
|
||||
g_volpage_app[0] = 0;
|
||||
g_volpage_fd = -1;
|
||||
}
|
||||
|
||||
int meow_volpage_armed(void) { return g_volpage_fd >= 0; }
|
||||
|
||||
/* 按键门:按下 VOL_UP/DOWN 且 sink 已武装 → 下发翻页信号并消费(返回 1)。
|
||||
* 未武装 / 非 VOL / 抬起 → 返回 0(放行给音量路径)。 */
|
||||
int meow_volpage_key(int code, int is_down)
|
||||
{
|
||||
if (!is_down) return 0; /* 只响应按下边沿 */
|
||||
if (g_volpage_fd < 0) return 0; /* 未武装:音量照旧 */
|
||||
if (code == KEY_VOLUMEUP) {
|
||||
(void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEUP, 1, NULL, 0);
|
||||
return 1;
|
||||
}
|
||||
if (code == KEY_VOLUMEDOWN) {
|
||||
(void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEDOWN, 1, NULL, 0);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
/* lock_screen.c — MeowOS 锁屏 4 位 PIN(纯逻辑;宿主可单测 lock_screen.h)。
|
||||
*
|
||||
* 与 meowpanel.c 的访问级 PIN(PANEL_ENHANCE_PLAN)**同源同格式**:`<salt_hex>:<sha256_hex(salt||pin)>`。
|
||||
* 内联 SHA-256 取自 meow_verify/sha256.c(FIPS 180-4,公有领域参考写法),前缀 lck_ 避免符号冲突;
|
||||
* 算法与 meowpanel.c 的 mp_sha256 逐字一致 → 面板生成的 panel.pin 无需重设即可被本模块校验。
|
||||
*
|
||||
* 日志用 fprintf(stderr)(前缀 [lock]),不依赖 launcher 的 logmsg —— 宿主测试可直接 #include 本文件。
|
||||
* 依赖 meow_settings_* API(同一 settings.conf),持久化走 meow_settings_save()(含 boot.default
|
||||
* 同步,幂等)。
|
||||
*/
|
||||
#include "lock_screen.h"
|
||||
#include "meow_settings.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define LCK_LOG(...) fprintf(stderr, "[lock] " __VA_ARGS__)
|
||||
|
||||
/* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 lck_) ---- */
|
||||
static const uint32_t lck_sha256_k[64] = {
|
||||
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
|
||||
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
|
||||
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
|
||||
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
|
||||
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
|
||||
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
|
||||
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
|
||||
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
|
||||
};
|
||||
#define LCK_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||||
#define LCK_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
|
||||
#define LCK_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||||
#define LCK_BSIG0(x) (LCK_ROR(x,2) ^ LCK_ROR(x,13) ^ LCK_ROR(x,22))
|
||||
#define LCK_BSIG1(x) (LCK_ROR(x,6) ^ LCK_ROR(x,11) ^ LCK_ROR(x,25))
|
||||
#define LCK_SSIG0(x) (LCK_ROR(x,7) ^ LCK_ROR(x,18) ^ ((x) >> 3))
|
||||
#define LCK_SSIG1(x) (LCK_ROR(x,17) ^ LCK_ROR(x,19) ^ ((x) >> 10))
|
||||
|
||||
typedef struct lck_sha256_ctx {
|
||||
uint32_t state[8];
|
||||
uint64_t count;
|
||||
uint8_t buf[64];
|
||||
size_t buflen;
|
||||
} lck_sha256_ctx;
|
||||
|
||||
static void lck_sha256_init(lck_sha256_ctx *c)
|
||||
{
|
||||
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
|
||||
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
|
||||
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
|
||||
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
|
||||
c->count = 0; c->buflen = 0;
|
||||
}
|
||||
|
||||
static void lck_sha256_block(lck_sha256_ctx *c, const uint8_t *p)
|
||||
{
|
||||
uint32_t w[64];
|
||||
int i;
|
||||
for (i = 0; i < 16; i++)
|
||||
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
|
||||
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
|
||||
for (i = 16; i < 64; i++)
|
||||
w[i] = LCK_SSIG1(w[i-2]) + w[i-7] + LCK_SSIG0(w[i-15]) + w[i-16];
|
||||
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
|
||||
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
|
||||
for (i = 0; i < 64; i++) {
|
||||
uint32_t t1 = h + LCK_BSIG1(e) + LCK_CH(e,f,g) + lck_sha256_k[i] + w[i];
|
||||
uint32_t t2 = LCK_BSIG0(a) + LCK_MAJ(a,b,cc);
|
||||
h = g; g = f; f = e; e = d + t1;
|
||||
d = cc; cc = b; b = a; a = t1 + t2;
|
||||
}
|
||||
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
|
||||
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
|
||||
}
|
||||
|
||||
static void lck_sha256_update(lck_sha256_ctx *c, const uint8_t *data, size_t len)
|
||||
{
|
||||
c->count += (uint64_t)len;
|
||||
if (c->buflen) {
|
||||
size_t need = 64 - c->buflen;
|
||||
size_t take = len < need ? len : need;
|
||||
memcpy(c->buf + c->buflen, data, take);
|
||||
c->buflen += take; data += take; len -= take;
|
||||
if (c->buflen == 64) { lck_sha256_block(c, c->buf); c->buflen = 0; }
|
||||
}
|
||||
while (len >= 64) { lck_sha256_block(c, data); data += 64; len -= 64; }
|
||||
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
|
||||
}
|
||||
|
||||
static void lck_sha256_final(lck_sha256_ctx *c, uint8_t out[32])
|
||||
{
|
||||
uint64_t bitlen = c->count * 8;
|
||||
uint8_t *b = c->buf;
|
||||
size_t bl = c->buflen;
|
||||
int i;
|
||||
b[bl++] = 0x80;
|
||||
if (bl > 56) {
|
||||
while (bl < 64) b[bl++] = 0;
|
||||
lck_sha256_block(c, b);
|
||||
bl = 0;
|
||||
}
|
||||
while (bl < 56) b[bl++] = 0;
|
||||
for (i = 0; i < 8; i++)
|
||||
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
|
||||
lck_sha256_block(c, b);
|
||||
for (i = 0; i < 8; i++) {
|
||||
out[i*4] = (uint8_t)(c->state[i] >> 24);
|
||||
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
|
||||
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
|
||||
out[i*4+3] = (uint8_t)(c->state[i]);
|
||||
}
|
||||
}
|
||||
#undef LCK_ROR
|
||||
#undef LCK_CH
|
||||
#undef LCK_MAJ
|
||||
#undef LCK_BSIG0
|
||||
#undef LCK_BSIG1
|
||||
#undef LCK_SSIG0
|
||||
#undef LCK_SSIG1
|
||||
|
||||
/* ---- 哈希基元(格式 `<salt_hex>:<sha256_hex(salt||pin)>`,与面板全等)---- */
|
||||
static void lck_hex(const unsigned char *raw, size_t n, char *out)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
out[i*2] = hex[raw[i] >> 4];
|
||||
out[i*2+1] = hex[raw[i] & 0xf];
|
||||
}
|
||||
out[n*2] = 0;
|
||||
}
|
||||
|
||||
static int lck_read_random(unsigned char *buf, size_t n)
|
||||
{
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
size_t got = 0;
|
||||
while (got < n) {
|
||||
ssize_t rd = read(fd, buf + got, n - got);
|
||||
if (rd <= 0) { close(fd); return -1; }
|
||||
got += (size_t)rd;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lck_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
|
||||
{
|
||||
unsigned char raw[16];
|
||||
if (outcap < 33) return -1;
|
||||
if (lck_read_random(raw, sizeof raw) < 0) return -1;
|
||||
lck_hex(raw, sizeof raw, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lck_pass_hash(const char *salt, const char *password, char *out, int outcap)
|
||||
{
|
||||
lck_sha256_ctx c;
|
||||
uint8_t digest[32];
|
||||
if (outcap < LCK_HASH_LEN + 1) return -1;
|
||||
lck_sha256_init(&c);
|
||||
lck_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
|
||||
lck_sha256_update(&c, (const uint8_t *)password, strlen(password));
|
||||
lck_sha256_final(&c, digest);
|
||||
lck_hex(digest, sizeof digest, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lck_make_hash(const char *password, char *out, int outcap) /* salt:hex */
|
||||
{
|
||||
char salt[33], h[LCK_HASH_LEN + 1];
|
||||
if (lck_make_salt(salt, sizeof salt) < 0) return -1;
|
||||
if (lck_pass_hash(salt, password, h, sizeof h) < 0) return -1;
|
||||
snprintf(out, (size_t)outcap, "%s:%s", salt, h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 校验 `salt:hex` 是否匹配明文(恒定比较,不反解)。stored 为 `salt:hex` 完整串。 */
|
||||
static int lck_hash_matches(const char *stored, const char *password)
|
||||
{
|
||||
if (!stored || !stored[0] || !password) return 0;
|
||||
char tmp[160];
|
||||
snprintf(tmp, sizeof tmp, "%s", stored);
|
||||
char *colon = strchr(tmp, ':');
|
||||
if (!colon) return 0;
|
||||
*colon = 0;
|
||||
char want[LCK_HASH_LEN + 1];
|
||||
if (lck_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
|
||||
if (strlen(colon + 1) != LCK_HASH_LEN) return 0;
|
||||
return memcmp(want, colon + 1, LCK_HASH_LEN) == 0;
|
||||
}
|
||||
|
||||
/* ---- 设置读取辅助(NULL 表示键缺失/空)---- */
|
||||
static char *lck_get(const char *key)
|
||||
{
|
||||
char buf[MEOW_SET_MAX_LEN];
|
||||
if (meow_settings_get_str(key, buf, sizeof buf) != 0) return NULL;
|
||||
if (!buf[0]) return NULL; /* 空值=未设 */
|
||||
char *r = strdup(buf);
|
||||
return r;
|
||||
}
|
||||
static int lck_get_int(const char *key)
|
||||
{
|
||||
int v = 0;
|
||||
if (meow_settings_get_int(key, &v) != 0) v = 0;
|
||||
return v;
|
||||
}
|
||||
static int lck_set_int(const char *key, int v)
|
||||
{
|
||||
return meow_settings_set_int(key, v);
|
||||
}
|
||||
static int lck_persist(void)
|
||||
{
|
||||
return meow_settings_save();
|
||||
}
|
||||
|
||||
/* ---- 公开接口 ---- */
|
||||
int lck_pin_valid(const char *pin)
|
||||
{
|
||||
if (!pin) return 0;
|
||||
size_t l = strlen(pin);
|
||||
if (l != LCK_PIN_LEN) return 0;
|
||||
for (size_t i = 0; i < l; i++)
|
||||
if (pin[i] < '0' || pin[i] > '9') return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int lck_pin_configured(void)
|
||||
{
|
||||
char *sp = lck_get(LCK_KEY_PIN);
|
||||
int s = (sp != NULL);
|
||||
free(sp);
|
||||
if (s) return 1;
|
||||
char *pp = lck_get(LCK_KEY_PANEL_PIN);
|
||||
int p = (pp != NULL);
|
||||
free(pp);
|
||||
return p;
|
||||
}
|
||||
|
||||
int lck_lock_required(int allow_disable)
|
||||
{
|
||||
if (!lck_pin_configured()) return 0; /* 未设凭据:锁屏无鉴别物,不开 */
|
||||
if (allow_disable) {
|
||||
int wake = 1;
|
||||
if (meow_settings_get_int(LCK_KEY_WAKE, &wake) != 0) wake = 1;
|
||||
return wake ? 1 : 0;
|
||||
}
|
||||
return 1; /* 手动锁:不看 WAKE 键 */
|
||||
}
|
||||
|
||||
int lck_pin_check(const char *pin)
|
||||
{
|
||||
if (!lck_pin_valid(pin)) return 0;
|
||||
char *sp = lck_get(LCK_KEY_PIN);
|
||||
if (sp) {
|
||||
int ok = lck_hash_matches(sp, pin);
|
||||
free(sp);
|
||||
return ok;
|
||||
}
|
||||
/* system.pin 未设:迁移期回退校验 panel.pin */
|
||||
char *pp = lck_get(LCK_KEY_PANEL_PIN);
|
||||
if (pp) {
|
||||
int ok = lck_hash_matches(pp, pin);
|
||||
free(pp);
|
||||
return ok;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lck_pin_set(const char *pin)
|
||||
{
|
||||
if (!lck_pin_valid(pin)) return MEOW_ERR_INVAL;
|
||||
char hash[160]; /* 32(salt) + ':' + 64(sha256 hex) + NUL = 98 */
|
||||
if (lck_make_hash(pin, hash, sizeof hash) != 0) return MEOW_ERR_IO;
|
||||
if (meow_settings_set_str(LCK_KEY_PIN, hash) != MEOW_OK) return MEOW_ERR_INVAL;
|
||||
lck_set_int(LCK_KEY_FAILS, 0);
|
||||
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
|
||||
lck_persist();
|
||||
LCK_LOG("pin set (%d digits, hash stored salt:sha256)\n", LCK_PIN_LEN);
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
int lck_pin_clear(void)
|
||||
{
|
||||
if (meow_settings_set_str(LCK_KEY_PIN, "") != MEOW_OK) return MEOW_ERR_INVAL;
|
||||
lck_set_int(LCK_KEY_FAILS, 0);
|
||||
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
|
||||
lck_persist();
|
||||
LCK_LOG("pin cleared (unlock)\n");
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
int lck_pin_success(void)
|
||||
{
|
||||
char *sp = lck_get(LCK_KEY_PIN);
|
||||
if (!sp) {
|
||||
/* 首次成功:把 panel.pin 并入 system.pin(一次性迁移),此后 system.pin 为准 */
|
||||
char *pp = lck_get(LCK_KEY_PANEL_PIN);
|
||||
if (pp) {
|
||||
meow_settings_set_str(LCK_KEY_PIN, pp);
|
||||
LCK_LOG("success: migrated panel.pin -> system.pin\n");
|
||||
free(pp);
|
||||
} else {
|
||||
LCK_LOG("success: no system.pin and no panel.pin?!\n");
|
||||
}
|
||||
}
|
||||
free(sp);
|
||||
lck_set_int(LCK_KEY_FAILS, 0);
|
||||
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
|
||||
lck_persist();
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
int lck_pin_fail(void)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
int fails = lck_get_int(LCK_KEY_FAILS);
|
||||
int until = lck_get_int(LCK_KEY_LOCK_UNTIL);
|
||||
/* 已锁定:不再递增(锁屏页本应禁输入;防御) */
|
||||
if (until > 0 && (time_t)until > now)
|
||||
return (int)((time_t)until - now);
|
||||
fails++;
|
||||
if (fails >= LCK_PIN_MAX_FAILS) {
|
||||
until = (int)now + LCK_PIN_LOCK_SEC;
|
||||
fails = 0;
|
||||
LCK_LOG("fail: %d consecutive -> LOCK %ds (until=%d)\n",
|
||||
LCK_PIN_MAX_FAILS, LCK_PIN_LOCK_SEC, until);
|
||||
} else {
|
||||
until = 0;
|
||||
LCK_LOG("fail: attempt %d/%d\n", fails, LCK_PIN_MAX_FAILS);
|
||||
}
|
||||
lck_set_int(LCK_KEY_FAILS, fails);
|
||||
lck_set_int(LCK_KEY_LOCK_UNTIL, until);
|
||||
lck_persist();
|
||||
return (until > 0 && (time_t)until > now) ? (int)((time_t)until - now) : 0;
|
||||
}
|
||||
|
||||
time_t lck_lock_remaining(void)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
int until = lck_get_int(LCK_KEY_LOCK_UNTIL);
|
||||
if (until > 0 && (time_t)until > now)
|
||||
return (time_t)until - now;
|
||||
if (until > 0) {
|
||||
/* 截止已过:清锁定戳+计数(进程内解禁),落盘 */
|
||||
LCK_LOG("lock expired (%d < now=%ld), clearing\n", until, (long)now);
|
||||
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
|
||||
lck_set_int(LCK_KEY_FAILS, 0);
|
||||
lck_persist();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 面板一次性解锁恢复口(tmpfs 标志)---- */
|
||||
int lck_unlock_once_consume(void)
|
||||
{
|
||||
if (access(LCK_UNLOCK_ONCE_FILE, F_OK) != 0) return 0; /* 不存在:不影响 */
|
||||
unlink(LCK_UNLOCK_ONCE_FILE);
|
||||
LCK_LOG("unlock-once: bypass file present -> consume + release once\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int lck_manual_lock_set(void)
|
||||
{
|
||||
int fd = open(LCK_MANUAL_LOCK_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) { LCK_LOG("manual-lock: open %s fail\n", LCK_MANUAL_LOCK_FILE); return MEOW_ERR_IO; }
|
||||
write(fd, "1", 1); close(fd);
|
||||
LCK_LOG("manual-lock: set %s\n", LCK_MANUAL_LOCK_FILE);
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
void lck_manual_lock_clear(void)
|
||||
{
|
||||
if (access(LCK_MANUAL_LOCK_FILE, F_OK) == 0) {
|
||||
unlink(LCK_MANUAL_LOCK_FILE);
|
||||
LCK_LOG("manual-lock: cleared\n");
|
||||
}
|
||||
}
|
||||
|
||||
int lck_wake_should_lock(void)
|
||||
{
|
||||
if (!lck_pin_configured()) return 0;
|
||||
int wake = 1;
|
||||
if (meow_settings_get_int(LCK_KEY_WAKE, &wake) != 0) wake = 1;
|
||||
if (wake) return 1;
|
||||
/* 唤醒锁已关,但存在「主动锁」标志(电源菜单「锁定」)→ 仍必锁屏 */
|
||||
return access(LCK_MANUAL_LOCK_FILE, F_OK) == 0;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/* lock_screen.h — MeowOS 锁屏 4 位 PIN(系统级安全,纯逻辑;宿主可单测)。
|
||||
*
|
||||
* 模型(用户定案,2026-09-02):
|
||||
* - 设置键 `system.pin`:4 位数字 PIN 的 sha256+salt 哈希,存 settings.conf。
|
||||
* 格式与面板 panel.pin **全等**:`<salt_hex>:<sha256_hex(salt||pin)>`,salt 16B
|
||||
* /dev/urandom → 32 hex;绝不存明文、绝不在日志/响应中输出 hash。
|
||||
* - 面板同源迁移:`panel.pin` 已设而 `system.pin` 未设时,锁屏接受 panel.pin,并在
|
||||
* **首次验证成功后并入 system.pin**(lck_pin_success 把 panel.pin 值拷入 system.pin)。
|
||||
* - 锁屏时机:息屏唤醒后(默认开,`system.pin_lock_on_wake=1` 可关)+ 电源菜单「锁定」
|
||||
* 手动锁;**开机不锁**(boot 进主屏不拦,本逻辑不被 boot 路径调用)。
|
||||
* - 错 5 次锁 5 分钟:`system.pin_fails`(连续错计数)与 `system.pin_lock_until`(锁定
|
||||
* 截止 epoch 秒)**持久化到 settings.conf**——launcher 重启不清零(面板的进程内存方案
|
||||
* 在重启后失效,这里要落盘)。
|
||||
* - 兜底:SSH root 删 `system.pin` 键即解锁(无其它后门)。
|
||||
*
|
||||
* 本头只声明纯逻辑接口;渲染/输入循环在 lock_screen_ui.c(run_lock_screen)。逻辑依赖
|
||||
* meow_settings_* API(与 meowpanel 的 settings_get_value 语义同源,走同一 settings.conf)。
|
||||
* 宿主测试:#include "lock_screen.c" + 真实 meow_settings.c(-DMEOW_SETTINGS_FILE 覆盖)。
|
||||
*/
|
||||
#ifndef MEOW_LOCK_SCREEN_H
|
||||
#define MEOW_LOCK_SCREEN_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#define LCK_PIN_LEN 4 /* 恰好 4 位数字 */
|
||||
#define LCK_PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
|
||||
#define LCK_PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
|
||||
#define LCK_HASH_LEN 64 /* sha256 hex 长度 */
|
||||
|
||||
/* ---- 设置键(必须已在 meow_settings.c g_defs 注册)---- */
|
||||
#define LCK_KEY_PIN "system.pin"
|
||||
#define LCK_KEY_WAKE "system.pin_lock_on_wake"
|
||||
#define LCK_KEY_LOCK_UNTIL "system.pin_lock_until"
|
||||
#define LCK_KEY_FAILS "system.pin_fails"
|
||||
#define LCK_KEY_PANEL_PIN "panel.pin" /* 面板 PIN(read-only 兼容源) */
|
||||
|
||||
/* ---- 文件标志(tmpfs,重启自然消失)---- */
|
||||
#define LCK_UNLOCK_ONCE_FILE "/tmp/meow_pin_unlock_once" /* 面板一次性解锁恢复口:存在即消费放行 */
|
||||
#define LCK_MANUAL_LOCK_FILE "/tmp/meow_pin_locked" /* 电源菜单「锁定」主动锁标志:唤醒必锁屏 */
|
||||
|
||||
/* PIN 合法:恰好 4 位数字(0-9)。NULL/长度不符/含非数字 → 0。 */
|
||||
int lck_pin_valid(const char *pin);
|
||||
|
||||
/* 是否已配锁屏凭据:system.pin 设 或 panel.pin 设(锁屏可开)。 */
|
||||
int lck_pin_configured(void);
|
||||
|
||||
/* 唤醒是否应锁屏:system.pin_lock_on_wake(默认 1) 且 已配凭据。
|
||||
* 仅由「息屏唤醒」路径调用——boot 路径不调,天然开机不锁。
|
||||
* allow_disable=0 时忽略 WAKE 键(电源菜单「锁定」手动锁用,锁前需已配凭据)。 */
|
||||
int lck_lock_required(int allow_disable);
|
||||
|
||||
/* 明文校验:system.pin 优先;未设时回退校验 panel.pin(迁移)。恒定比较,不反解。 */
|
||||
int lck_pin_check(const char *pin);
|
||||
|
||||
/* 设置/修改 system.pin(明文→sha256+salt 落盘):校验 4 位数字;清连续错计数+锁定戳;**落盘**。
|
||||
* 返回 MEOW_OK / 负错误。设置页/CLI 设锁屏 PIN 走此。 */
|
||||
int lck_pin_set(const char *pin);
|
||||
/* 清除 system.pin(解锁兜底,SSH root 删键即解锁的同义 API):清计数+锁定戳,**落盘**。 */
|
||||
int lck_pin_clear(void);
|
||||
|
||||
/* 记录一次成功验证:首次成功时把 panel.pin 并入 system.pin(迁移);清连续错计数 + 清
|
||||
* 锁定戳;**落盘**。返回 MEOW_OK。 */
|
||||
int lck_pin_success(void);
|
||||
|
||||
/* 记录一次失败:递增连续错计数(持久化);达 LCK_PIN_MAX_FAILS → 设锁定戳(now+300s)并清
|
||||
* 计数。已处于锁定(now<截止)时不递增。返回**剩余锁定秒**(0=未锁)。
|
||||
* 调用方须先 lck_lock_remaining() 确认未锁才让输入;已锁时本函数不改变截止。 */
|
||||
int lck_pin_fail(void);
|
||||
|
||||
/* 剩余锁定秒(0=未锁定)。若截止已过,自动清锁定戳+计数并落盘(进程内已解禁)。 */
|
||||
time_t lck_lock_remaining(void);
|
||||
|
||||
/* ---- 面板一次性解锁恢复口(tmpfs 标志文件)----
|
||||
* 锁屏页进入时 / 每次重试前调用:LCK_UNLOCK_ONCE_FILE 存在 → **删除并返回 1(放行进入系统一次)**;
|
||||
* 不存在 → 0。不校验 PIN、不清除 system.pin 本身、不重置失败计数(重启标志自然消失)。 */
|
||||
int lck_unlock_once_consume(void);
|
||||
|
||||
/* ---- 电源菜单「锁定」主动锁标志(tmpfs,重启消失)----
|
||||
* 主动锁屏:写 LCK_MANUAL_LOCK_FILE → 之后唤醒 `lck_wake_should_lock()` 无论 system.pin_lock_on_wake
|
||||
* 是否开都必走锁屏;解锁(PIN 或面板绕过)时清除。 */
|
||||
int lck_manual_lock_set(void); /* 写标志;返回 MEOW_OK */
|
||||
void lck_manual_lock_clear(void); /* 删标志(幂等) */
|
||||
/* 唤醒是否应锁屏:已配凭据 且 (system.pin_lock_on_wake=1 或 主动锁标志存在)。
|
||||
* 取代 lck_lock_required(1) 作为唤醒门——主动锁强制锁屏,覆盖「关闭唤醒锁」的手动锁场景。 */
|
||||
int lck_wake_should_lock(void);
|
||||
|
||||
/* ── 锁屏叠加期 App 存活探针(§310 残影修复,纯判定,宿主可单测;header-only inline)----
|
||||
* 锁屏运行在 App 前台叠加会话中(overlay_pid>0,run_app_lock_overlay 置位)且该进程已成僵尸
|
||||
* (调用方 proc_is_zombie 判定后传入 proc_zombie=1)→ 应中止锁屏走统一退出(返回 1)。
|
||||
* 菜单直接锁屏(overlay_pid=0,无 App 可死)恒 0;此谓词不实际读 /proc。 */
|
||||
static inline int lck_overlay_should_abort(int overlay_pid, int proc_zombie)
|
||||
{
|
||||
return (overlay_pid > 0 && proc_zombie) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* run_lock_screen() 返回值:1=已解锁,0=锁屏期间电源键息屏。叠加期 App 崩溃时返回
|
||||
* LCK_OVERLAY_RET_DIED(-1)——调用方 run_app_lock_overlay 据此外走「统一退出路径」
|
||||
* (不再 SIGCONT 死进程/不恢复死帧,交回 waitpid 循环回收,run_menu 重绘主屏)。 */
|
||||
#define LCK_OVERLAY_RET_DIED (-1)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_LOCK_SCREEN_H */
|
||||
@@ -0,0 +1,747 @@
|
||||
/* lock_screen_ui.c — MeowOS 锁屏页(系统级全屏页面:launcher 渲染,App 不可见不可触)。
|
||||
*
|
||||
* 用户定案(2026-09-02):
|
||||
* - 息屏唤醒后(默认开,system.pin_lock_on_wake=1 可关)+ 电源菜单「锁定」手动锁;开机不锁。
|
||||
* - 数字键盘直接用 SDK 按钮画(不调 IME);输入 4 位自动校验;错 5 次锁 5 分钟(持久化)。
|
||||
* - 正确 → 进主屏(return 1);锁屏页期间电源键=息屏(允许再睡,return 0),音量键不动;
|
||||
* 不响应左缘返回 / 通知中心手势(本函数不实现这些手势,触摸只认键盘 tap)。
|
||||
* - 输入不回显数字(● 遮罩)。兜底见 lock_screen.c(SSH root 删 system.pin 即解锁)。
|
||||
*
|
||||
* 渲染/输入循环与电源菜单同构(meow_touch + kd_* + kp_*),阻塞到解锁或电源息屏。
|
||||
* 纯逻辑在 lock_screen.c(可宿主单测);本文件只做 UI 与输入胶水。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "overlay_gate.h" /* OVERLAY_LOCK_COVER_MS(锁屏重铺阈值,与单测同源) */
|
||||
|
||||
/* §310 残影:锁屏处于 App 前台叠加会话时置位当前前台 App pid(run_app_lock_overlay 写入)。
|
||||
* run_lock_screen 每圈据此探测 App 是否已死(组合 App 冻结缓冲 evdev 会在解锁瞬间回放成点击 /
|
||||
* 叠加期 OOM 杀让 fb0 留残帧——探到死即中止锁屏,交回 waitpid 循环回收 + run_menu 重绘主屏)。
|
||||
* 菜单直接锁屏(无 App)时保持 0 = 不探测。 */
|
||||
int g_lock_overlay_pid = 0;
|
||||
|
||||
/* ---- 键盘几何(480×720):3 列×4 行圆角按钮 ---- */
|
||||
#define LK_X0 32 /* 首列左缘 */
|
||||
#define LK_COLW 130 /* 列宽 */
|
||||
#define LK_COLG 8 /* 列间隙 */
|
||||
#define LK_Y0 320 /* 首行顶缘 */
|
||||
#define LK_ROW_H 82 /* 行高 */
|
||||
#define LK_ROWG 10 /* 行间隙 */
|
||||
#define LK_ROWS 4
|
||||
#define LK_COLS 3
|
||||
/* 12 键(行优先):digit=-2 清空 / -1 退格 / >=0 数字 */
|
||||
#define LK_ACT_CLR (-2)
|
||||
#define LK_ACT_BS (-1)
|
||||
static const int lk_keys[LK_ROWS][LK_COLS] = {
|
||||
{ 1, 2, 3 },
|
||||
{ 4, 5, 6 },
|
||||
{ 7, 8, 9 },
|
||||
{ LK_ACT_CLR, 0, LK_ACT_BS },
|
||||
};
|
||||
|
||||
/* ---- M7.1b 锁屏控件化:PIN 数字键盘 / 时钟 / 提示 / PIN 圆点 改用 SDK 控件 ----
|
||||
* 旧自绘锁屏 UI 保留在 #if 0(回退位,见 lk_draw/lk_key_hit),新 SDK 渲染默认启用;
|
||||
* run_lock_screen 会话骨架(EVIOCGRAB / 自睡 / 解锁脏区修复 / 物理键转发)不动,只替换
|
||||
* 它调用的绘制/命中函数体。控件 owner 钉 LK_SENTINEL_ACT(子页 0x52 之外的独立哨兵)。 */
|
||||
#define LK_SENTINEL_ACT 0x51 /* 锁屏控件 owner 哨兵(与子页 SUB_SENTINEL_ACT=0x52 区分) */
|
||||
#define LK_DOWN_NONE (-10) /* 无键按下哨兵(关键值范围 LK_ACT_CLR=-2/-1/0-9) */
|
||||
static meow_widget *lk_key_w[LK_ROWS][LK_COLS]; /* 12 键按钮(meow_widget_custom) */
|
||||
static int lk_widgets_ready = 0;
|
||||
static int g_lk_down_key = LK_DOWN_NONE; /* 当前按下的键(跟手指按压态) */
|
||||
|
||||
/* 键盘按钮 row*3+col 的命中矩形(x,y,w,h) */
|
||||
static void lk_key_rect(int row, int col, int *x, int *y, int *w, int *h)
|
||||
{
|
||||
*x = LK_X0 + col * (LK_COLW + LK_COLG);
|
||||
*y = LK_Y0 + row * (LK_ROW_H + LK_ROWG);
|
||||
*w = LK_COLW;
|
||||
*h = LK_ROW_H;
|
||||
}
|
||||
|
||||
/* 居中文本(真实字体优先;回退 ASCII 点阵) */
|
||||
static void lk_text(int cx, int y, const char *s, int size, uint32_t color)
|
||||
{
|
||||
if (mfont_ready) {
|
||||
int w = meow_font_width_text(s, size);
|
||||
meow_font_drawtext((uint16_t *)fb, cx - w / 2, y, s, rgb565(color), size);
|
||||
} else {
|
||||
draw_text(cx - (int)strlen(s) * 3, y + 1, s, 1, color);
|
||||
}
|
||||
}
|
||||
|
||||
/* 单个 PIN 圆点:filled=1 实心(已输入),0=空心(未输入)。 */
|
||||
static void lk_pin_dot(int cx, int cy, int r, int filled, uint32_t ink, uint32_t bg)
|
||||
{
|
||||
if (filled) {
|
||||
fill_circle(cx, cy, r, ink);
|
||||
} else {
|
||||
fill_circle(cx, cy, r, ink); /* 外圈(描边) */
|
||||
fill_circle(cx, cy, r - 3, bg); /* 内孔(表面底色) */
|
||||
}
|
||||
}
|
||||
|
||||
static void lk_draw_pin(int entered, const meow_ui_theme_t *th)
|
||||
{
|
||||
int cx = fb_xres / 2;
|
||||
int y = 210;
|
||||
int gap = 48, r = 20;
|
||||
int x0 = cx - (3 * gap) / 2; /* 首圆圆心;四圆等距 gap */
|
||||
for (int i = 0; i < LCK_PIN_LEN; i++)
|
||||
lk_pin_dot(x0 + i * gap, y, r, i < entered, th->primary, th->surface);
|
||||
}
|
||||
|
||||
/* ---- M7.1b 控件化:SDK meow_widget 键盘 + meow_font 文字 ---- */
|
||||
|
||||
/* 数字键标签(1-9/0 单字符;static 缓冲各调用立即消费,无跨调用残留) */
|
||||
static const char *lk_key_digit(int key)
|
||||
{
|
||||
static char d[2];
|
||||
d[0] = (char)('0' + key);
|
||||
d[1] = 0;
|
||||
return d;
|
||||
}
|
||||
|
||||
/* 键渲染:surfaceContainer 圆角底(MD3)+ 按压态 primary 26% α + 居中标签(SDK meow_font)。
|
||||
* 颜色全部取 meow_theme_get()(SDK 单一权威:MD3/MIUIx × 明暗自动);读 w->state 的
|
||||
* MEOW_WS_PRESSED 位(跟手指,由 lk_set_down_key 置/清)。 */
|
||||
static void lk_key_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||||
{
|
||||
int key = (int)(intptr_t)ctx;
|
||||
int rc[4]; meow_widget_rect(w, rc); /* 绝对坐标 x,y,w,h(控件不透明,用 SDK 只读几何) */
|
||||
int x = rc[0], y = rc[1], ww = rc[2], hh = rc[3];
|
||||
unsigned st = meow_widget_state(w);
|
||||
int rad = (th->radius > 0) ? th->radius : 10;
|
||||
meow_gfx_fill_round(x, y, ww, hh, rad, th->surfaceContainer);
|
||||
if (st & MEOW_WS_PRESSED)
|
||||
meow_gfx_fill_round_alpha(x, y, ww, hh, rad, th->primary, 66);
|
||||
const char *label;
|
||||
int size;
|
||||
if (key >= 0) { size = 30; label = lk_key_digit(key); }
|
||||
else if (key == LK_ACT_CLR) { size = 22; label = mfont_ready ? meow_str_zh("lock.clear", "清空") : "C"; }
|
||||
else { size = 22; label = mfont_ready ? meow_str_zh("lock.backspace", "退格") : "DEL"; }
|
||||
int cx = x + ww / 2;
|
||||
int cy = y + hh / 2 - meow_ui_text_height(size) / 2;
|
||||
meow_ui_text_center(cx, cy, label, size, th->onSurface);
|
||||
}
|
||||
|
||||
/* 懒创建 12 键(owner=LK_SENTINEL_ACT;仅首次调用建一次)。布局几何沿用 lk_key_rect。 */
|
||||
static void lk_widgets_build(void)
|
||||
{
|
||||
if (lk_widgets_ready) return;
|
||||
meow_ui_activity_push(LK_SENTINEL_ACT);
|
||||
for (int r = 0; r < LK_ROWS; r++) {
|
||||
for (int c = 0; c < LK_COLS; c++) {
|
||||
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
|
||||
lk_key_w[r][c] = meow_widget_custom_create(x, y, w, h, lk_key_render, NULL,
|
||||
(void *)(intptr_t)lk_keys[r][c]);
|
||||
}
|
||||
}
|
||||
meow_ui_activity_pop();
|
||||
lk_widgets_ready = 1;
|
||||
}
|
||||
|
||||
/* 键值 -> 按钮(按压态置/清位用)。 */
|
||||
static meow_widget *lk_widget_for_key(int key)
|
||||
{
|
||||
for (int r = 0; r < LK_ROWS; r++)
|
||||
for (int c = 0; c < LK_COLS; c++)
|
||||
if (lk_keys[r][c] == key) return lk_key_w[r][c];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 坐标 -> 键值(SDK meow_widget_hit;owner==active 已由调用方保证)。未命中返回 LK_DOWN_NONE。 */
|
||||
static int lk_key_value_at(int x, int y)
|
||||
{
|
||||
for (int r = 0; r < LK_ROWS; r++)
|
||||
for (int c = 0; c < LK_COLS; c++)
|
||||
if (lk_key_w[r][c] && meow_widget_hit(lk_key_w[r][c], x, y))
|
||||
return lk_keys[r][c];
|
||||
return LK_DOWN_NONE;
|
||||
}
|
||||
|
||||
/* 更新按压态(跟手指):清旧键状态位、置新键;返回 1=按键变化(调用方据此重绘)。 */
|
||||
static int lk_set_down_key(int key)
|
||||
{
|
||||
if (key == g_lk_down_key) return 0;
|
||||
meow_widget *old = lk_widget_for_key(g_lk_down_key);
|
||||
if (old) meow_widget_set_state(old, MEOW_WS_PRESSED, 0);
|
||||
meow_widget *nw = lk_widget_for_key(key);
|
||||
if (nw) meow_widget_set_state(nw, MEOW_WS_PRESSED, 1);
|
||||
g_lk_down_key = key;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* PIN 输入圆点(● 遮罩:已输入=primary 实心;未输入=primary 环 + surface 孔)。 */
|
||||
static void lk_draw_pin_dots(int entered, const meow_theme *mt)
|
||||
{
|
||||
int cx = fb_xres / 2;
|
||||
int y = 210;
|
||||
int gap = 48, r = 20;
|
||||
int x0 = cx - (3 * gap) / 2;
|
||||
for (int i = 0; i < LCK_PIN_LEN; i++) {
|
||||
int dx = x0 + i * gap;
|
||||
fill_circle(dx, y, r, mt->primary);
|
||||
if (i >= entered) fill_circle(dx, y, r - 3, mt->surface);
|
||||
}
|
||||
}
|
||||
|
||||
/* 键命中:旧自绘(lk_key_rect+mt_in)保留在 #if 0(回退位);M7.1b 走 SDK meow_widget_hit
|
||||
* (owner==active + enabled 双闸门在 SDK 内;命中点=按下原点 mt->down_x/down_y,与原 mt_in 同源)。 */
|
||||
#if 0
|
||||
static int lk_key_hit(const meow_touch *mt, int row, int col)
|
||||
{
|
||||
int x, y, w, h; lk_key_rect(row, col, &x, &y, &w, &h);
|
||||
return mt_in(mt, x, y, w, h);
|
||||
}
|
||||
#else
|
||||
static int lk_key_hit(const meow_touch *mt, int row, int col)
|
||||
{
|
||||
if (!lk_key_w[row][col]) return 0;
|
||||
return meow_widget_hit(lk_key_w[row][col], mt->down_x, mt->down_y);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
/* ============ 旧自绘锁屏整帧(M7.1b 回退位)============
|
||||
* 原 lk_draw:fill_rect 全屏底 + lk_text(时钟/状态)+ lk_draw_pin(●)+ fill_round_rect 键盘。
|
||||
* 一键回退:把下面 #if 0 改成 #if 1(并把新 lk_draw 的 #else 分支由 #else 改走旧轮廓),设备即回旧观感。 */
|
||||
static void lk_draw(int entered, const char *status, time_t locked_until)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
fill_rect(0, 0, fb_xres, fb_yres, th->surface); /* 全屏表面底色 */
|
||||
lk_text(fb_xres / 2, 120, locked_until > 0 ? "已锁定" : "输入 PIN 解锁", 24, th->on_surface);
|
||||
lk_draw_pin(entered, th);
|
||||
lk_text(fb_xres / 2, 260, status, 18, status[0] ? th->error : th->on_surface_variant);
|
||||
for (int r = 0; r < LK_ROWS; r++) {
|
||||
for (int c = 0; c < LK_COLS; c++) {
|
||||
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
|
||||
fill_round_rect(x, y, w, h, 12, th->surface_container);
|
||||
int key = lk_keys[r][c];
|
||||
if (key >= 0) {
|
||||
char d[2] = { (char)('0' + key), 0 };
|
||||
if (mfont_ready) {
|
||||
int fw = meow_font_width_text(d, 30);
|
||||
meow_font_drawtext((uint16_t *)fb, x + w / 2 - fw / 2,
|
||||
y + LK_ROW_H / 2 - meow_font_line_height(30) / 2,
|
||||
d, rgb565(th->on_surface), 30);
|
||||
} else {
|
||||
draw_text(x + w / 2 - 7, y + LK_ROW_H / 2 - 14, d, 2, th->on_surface);
|
||||
}
|
||||
} else if (key == LK_ACT_CLR) {
|
||||
const char *s = "C"; const char *t = s;
|
||||
if (mfont_ready) t = "清空";
|
||||
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t, 22, th->on_surface);
|
||||
} else { /* LK_ACT_BS */
|
||||
const char *t = "DEL";
|
||||
if (mfont_ready) t = "退格";
|
||||
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t, 22, th->on_surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
meow_dirty_full();
|
||||
fb_pan_buf0_force();
|
||||
meow_ui_flush();
|
||||
}
|
||||
#else
|
||||
/* ============ M7.1b 锁屏控件化整帧(默认启用)============
|
||||
* 整帧不透明 surface 底(§310 脏区修复关键:全屏覆盖,无 App 帧透出)+ SDK 字体时钟/提示
|
||||
* + 主题色 PIN 圆点 + meow_widget 键盘(surfaceContainer 圆角 / 按压态 primary 26% α / MD3 主题)。
|
||||
* 颜色全部取 meow_theme_get()(SDK 单一权威:MD3/MIUIx × 明暗自动)。 */
|
||||
static void lk_draw(int entered, const char *status, time_t locked_until)
|
||||
{
|
||||
const meow_theme *mt = meow_theme_get();
|
||||
meow_ui_fill_rect(0, 0, fb_xres, fb_yres, mt->surface); /* 全屏不透明表面底 */
|
||||
lk_widgets_build(); /* 懒创建 12 键 */
|
||||
/* 时钟/提示(居中,SDK meow_font):锁定倒计时标题 */
|
||||
meow_ui_text_center(fb_xres / 2, 120, locked_until > 0 ? meow_str_zh("lock.locked", "已锁定")
|
||||
: meow_str_zh("lock.prompt", "输入 PIN 解锁"),
|
||||
24, mt->onSurface);
|
||||
/* PIN 输入圆点(● 遮罩) */
|
||||
lk_draw_pin_dots(entered, mt);
|
||||
/* 状态行:错误提示(PIN 错误)着 error;倒计时/其它信息用 onSurfaceVariant */
|
||||
int is_err = status[0] && (strstr(status, meow_str_zh("lock.pin_wrong", "PIN 错误")) != NULL ||
|
||||
strstr(status, "WRONG PIN") != NULL);
|
||||
uint32_t status_col = status[0] ? (is_err ? mt->error : mt->onSurfaceVariant)
|
||||
: mt->onSurfaceVariant;
|
||||
meow_ui_text_center(fb_xres / 2, 260, status, 18, status_col);
|
||||
/* 12 键(SDK meow_widget;命中走 lk_key_hit → meow_widget_hit) */
|
||||
for (int r = 0; r < LK_ROWS; r++)
|
||||
for (int c = 0; c < LK_COLS; c++)
|
||||
if (lk_key_w[r][c]) meow_widget_draw(lk_key_w[r][c]);
|
||||
meow_dirty_full();
|
||||
fb_pan_buf0_force();
|
||||
meow_ui_flush();
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 阻塞式锁屏页。返回 1=已解锁,0=锁屏期间电源键息屏。 */
|
||||
int run_lock_screen(void)
|
||||
{
|
||||
int fbfd = open(FB_DEV, O_RDWR);
|
||||
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
|
||||
meow_screen_on = 1;
|
||||
fb_pan_buf0_force();
|
||||
|
||||
meow_touch mt; mt_reset(&mt);
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
/* §310 输入穿透(媒体保活 App 不被 SIGSTOP,实时读 evdev):锁屏页独占触控设备——
|
||||
* EVIOCGRAB 使本 fd 之外的其它进程(保活 App 的 SDK 输入线程 / watchdog 手势)在该设备
|
||||
* 上收不到事件(事件仅投递抓取者),且抓取期事件不会为未抓取 fd 缓冲/回放。由此无论 App
|
||||
* 冻结与否,锁屏期**任何** App 都收不到触摸(真全模态)。失败(驱动不支持)则记日志降级,
|
||||
* 仍靠 keep_running/SIGSTOP+标记隔离。关闭 tfd 时抓取自动解除(仍显式释放一次)。 */
|
||||
if (tfd >= 0) {
|
||||
int grab = 1;
|
||||
if (ioctl(tfd, EVIOCGRAB, &grab) < 0)
|
||||
logmsg("lock: EVIOCGRAB touch fail: %s (graceful degrade, marker+SIGSTOP isola only)",
|
||||
strerror(errno));
|
||||
}
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER);
|
||||
kp_open(&kp_keys, DEV_KEYS);
|
||||
|
||||
int entered = 0;
|
||||
char digits[LCK_PIN_LEN + 1] = {0};
|
||||
char status[64] = "";
|
||||
time_t lock_until = 0;
|
||||
{
|
||||
time_t rem = lck_lock_remaining();
|
||||
if (rem > 0) { lock_until = time(NULL) + rem; snprintf(status, sizeof status, "%s", meow_str_zh("lock.locked", "已锁定")); }
|
||||
}
|
||||
time_t last_countdown = 0;
|
||||
|
||||
/* 面板一次性解锁恢复口:进入时消费(存在即删除+放行一次,不校验 PIN/不清 PIN/不重置失败计数) */
|
||||
if (lck_unlock_once_consume()) {
|
||||
lck_manual_lock_clear(); /* 进入系统即视为解除主动锁 */
|
||||
logmsg("lock: unlock-once bypass consumed at entry -> release");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* M7.1 解锁恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面不会自动重绘
|
||||
* —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧(解锁后整屏脏区根因)。此处
|
||||
* 在锁屏首扑前把底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;解锁(unlocked=1)
|
||||
* 时整帧回写 back_buf + present,强制底层界面完整重绘(app_launcher.c 的 lock-overlay 关闭路径
|
||||
* 随后 pm_capture_app_frame 读 fb0 即得到正确 App 帧,不再读到锁帧)。 */
|
||||
unsigned char *lk_bg_snap = NULL;
|
||||
size_t lk_bg_len = 0;
|
||||
if (back_buf_ok && fb && fb_line > 0 && fb_yres > 0) {
|
||||
lk_bg_len = (size_t)fb_line * (size_t)fb_yres;
|
||||
lk_bg_snap = malloc(lk_bg_len);
|
||||
if (lk_bg_snap) memcpy(lk_bg_snap, fb, lk_bg_len);
|
||||
else { lk_bg_len = 0; logmsg("lock: bg snapshot malloc fail (len=%zu)", lk_bg_len); }
|
||||
}
|
||||
|
||||
/* M7.1b:锁屏控件 owner 闸门打开(meow_widget_hit / 按压态需 active==LK_SENTINEL_ACT)。
|
||||
* 会话骨架(EVIOCGRAB/自睡/解锁脏区修复)不受影响;done/died 处对称 pop。 */
|
||||
meow_ui_activity_push(LK_SENTINEL_ACT);
|
||||
g_lk_down_key = LK_DOWN_NONE;
|
||||
|
||||
lk_draw(entered, status, lock_until); /* 首帧(全屏不透明主题底 + 控件化键盘) */
|
||||
|
||||
int unlocked = 0;
|
||||
unsigned long long entry_ms = now_ms();
|
||||
int covered_once = 0; /* §310 问题2-②:标记节流窗后重铺一次全屏底,盖掉 App 残帧 */
|
||||
/* §310 M6.7 锁屏页自睡(294 遗留):锁屏页不应常亮耗电。
|
||||
* 阈值 = min(系统 screen_off_timeout, 30s):系统未设/设 0(常亮)/超 30 → 锁屏页封顶 30s,
|
||||
* 系统设更短则尊重更短。超阈值 → meow_panel_off()(与息屏路径一致)+ asleep=1(锁态保持,
|
||||
* 仍留在锁屏循环);asleep 后电源键按下边沿/触摸活动 → meow_panel_on() + 重画锁屏页回锁。 */
|
||||
int lk_off_sec = overlay_lock_idle_off_sec(screen_off_timeout);
|
||||
unsigned long long lk_off_ms = (unsigned long long)lk_off_sec * 1000;
|
||||
unsigned long long last_act = now_ms();
|
||||
int asleep = 0;
|
||||
int prev_pwr_down = 0;
|
||||
/* M7.1 锁屏期物理键保留语义(音量/媒体键):边沿跟踪基线(锁屏是 EVIOCGRAB 独占真模态,
|
||||
* 这些键只能在本层显式处理,不会转发到系统)。 */
|
||||
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_prv = 0, prev_nxt = 0;
|
||||
for (;;) {
|
||||
time_t now = time(NULL);
|
||||
int redraw = 0;
|
||||
|
||||
/* 面板一次性解锁恢复口:锁屏页停留期(每次重试前/每轮)也消费 */
|
||||
if (lck_unlock_once_consume()) {
|
||||
lck_manual_lock_clear();
|
||||
logmsg("lock: unlock-once bypass consumed in-loop -> release");
|
||||
unlocked = 1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* §310 问题2-②:媒体保活 App 不冻结、SDK present 抑制有 ≤200ms 标记节流窗——锁屏首扑后
|
||||
* App 仍可能再进 1-2 帧(部分脏区盖到锁屏上)。等标记窗过后(≥300ms)重铺一次全屏不透明底
|
||||
* 盖掉 App 残帧;App 此后被抑制不再上屏,锁屏稳定遮盖(真机「App 控件盖到锁屏上」)。 */
|
||||
if (!covered_once && !asleep && now_ms() - entry_ms >= OVERLAY_LOCK_COVER_MS) {
|
||||
covered_once = 1;
|
||||
logmsg("lock: re-cover full opaque after marker window (app stave suppressed)");
|
||||
lk_draw(entered, status, lock_until);
|
||||
}
|
||||
|
||||
/* §310 残影:锁屏叠加期 App 崩溃(僵尸)→ 中止锁屏走统一退出路径(交回 waitpid 循环
|
||||
* 回收 + run_menu 重绘主屏含状态栏)。仅当 run_app_lock_overlay 置位 g_lock_overlay_pid
|
||||
* 才探(菜单直接锁屏无 App 可死,恒不中止)。 */
|
||||
if (g_lock_overlay_pid > 0 &&
|
||||
lck_overlay_should_abort(g_lock_overlay_pid, proc_is_zombie(g_lock_overlay_pid))) {
|
||||
logmsg("lock: overlay app pid %d died during lock -> abort (unified exit path)",
|
||||
g_lock_overlay_pid);
|
||||
g_lock_overlay_pid = 0;
|
||||
goto died;
|
||||
}
|
||||
|
||||
/* 锁定到期自动解禁(清计数+戳),仅重画状态行 */
|
||||
if (lock_until > 0 && now >= lock_until) {
|
||||
lck_lock_remaining();
|
||||
lock_until = 0;
|
||||
status[0] = 0;
|
||||
redraw = 1;
|
||||
}
|
||||
if (lock_until > 0 && now != last_countdown) {
|
||||
last_countdown = now;
|
||||
time_t rem = lock_until - now;
|
||||
if (mfont_ready) snprintf(status, sizeof status, meow_str_zh("lock.locked_wait", "已锁定,请 %d:%02d 后重试"),
|
||||
(int)(rem / 60), (int)(rem % 60));
|
||||
else snprintf(status, sizeof status, "LOCKED %d:%02d", (int)(rem / 60), (int)(rem % 60));
|
||||
redraw = 1;
|
||||
}
|
||||
|
||||
/* 电源键/音量键轮询 + 输入活动检测(重置自睡计时) + 自睡/唤醒切换。
|
||||
* 活跃态:电源键按下 → 息屏并退出锁屏(回 App/主屏,下次唤醒再锁)——沿用原语义;
|
||||
* 空闲超 lk_off_ms → 自睡(asleep=1,锁态保持,仍在本循环)。
|
||||
* asleep:电源键按下边沿 / 触摸活动 → meow_panel_on + 重画锁屏页(本循环唤醒链回锁)。
|
||||
* 音量键本页不动(仅视为输入活动,重置计时)。 */
|
||||
{
|
||||
int chg = 0;
|
||||
if (kp_power.ok) chg |= kp_poll(&kp_power, NULL, NULL);
|
||||
chg |= kp_poll(&kp_keys, NULL, NULL);
|
||||
if (chg) last_act = now_ms(); /* 任意按键活动重置自睡计时 */
|
||||
int pwr_down = kp_is_down(&kp_power, KEY_POWER);
|
||||
int pwr_edge = pwr_down && !prev_pwr_down; /* 按下边沿 */
|
||||
prev_pwr_down = pwr_down;
|
||||
|
||||
if (asleep) {
|
||||
int touch_ready = (tfd >= 0);
|
||||
if (touch_ready) {
|
||||
struct pollfd tp = { tfd, POLLIN, 0 };
|
||||
if (poll(&tp, 1, 0) <= 0) touch_ready = 0;
|
||||
}
|
||||
if (pwr_edge || touch_ready) {
|
||||
logmsg("lock: wake from self-sleep (power=%d touch=%d)", pwr_edge, touch_ready);
|
||||
meow_panel_on();
|
||||
asleep = 0;
|
||||
last_act = now_ms();
|
||||
g_lk_down_key = LK_DOWN_NONE; /* M7.1b:唤醒复位按压态基线 */
|
||||
lk_draw(entered, status, lock_until); /* 重画锁屏页回锁 */
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
/* 电源键=息屏(允许再睡;下次唤醒按 lck_wake_should_lock 再锁) */
|
||||
if (pwr_down) {
|
||||
logmsg("lock: power key -> screen off (stay locked)");
|
||||
meow_panel_off();
|
||||
break; /* return 0 */
|
||||
}
|
||||
/* M7.1 锁屏期物理键仍执行其语义(用户定案:音量键调音量、媒体键控播放/上下曲)。
|
||||
* 锁屏层 EVIOCGRAB 独占这些输入(真模态),不转发系统,故必须在此显式处理,不能依赖别处。
|
||||
* 音量沿用 launcher 既有递增/递减路径(settings_volume_adj —— 起 1% 线性渐变,
|
||||
* 长按由下方 vol_ramp_tick 每圈推进);播放/上下曲经 meow_media_playctl 下发媒体指令。 */
|
||||
int vold = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
int volu = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
int play = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
int prv = (kp_keys.ok && kp_is_down(&kp_keys, KEY_PREVIOUSSONG)) ||
|
||||
(kp_power.ok && kp_is_down(&kp_power, KEY_PREVIOUSSONG));
|
||||
int nxt = (kp_keys.ok && kp_is_down(&kp_keys, KEY_NEXT)) ||
|
||||
(kp_power.ok && kp_is_down(&kp_power, KEY_NEXT));
|
||||
if (vold && !prev_vold) { settings_volume_adj(-settings_volume_step); last_act = now_ms(); }
|
||||
if (volu && !prev_volu) { settings_volume_adj(+settings_volume_step); last_act = now_ms(); }
|
||||
if (play && !prev_play) {
|
||||
const meow_media_session_t *ms = meow_media_current();
|
||||
if (ms) {
|
||||
uint32_t c = (ms->state == MEOW_MEDIA_PLAYING) ? 2u : 1u; /* 暂停/播放 */
|
||||
int rc = meow_media_playctl(c, 0);
|
||||
logmsg("lock: play key -> media cmd=%u rc=%d (%s)", c, rc, ms->app_id);
|
||||
} else {
|
||||
logmsg("lock: play key -> media-reserved (no media source)");
|
||||
}
|
||||
last_act = now_ms();
|
||||
}
|
||||
if (prv && !prev_prv) {
|
||||
if (meow_media_current()) { int rc = meow_media_playctl(3, 0); logmsg("lock: prev key -> cmd=3 rc=%d", rc); }
|
||||
else logmsg("lock: prev key -> media-reserved (no media source)");
|
||||
last_act = now_ms();
|
||||
}
|
||||
if (nxt && !prev_nxt) {
|
||||
if (meow_media_current()) { int rc = meow_media_playctl(4, 0); logmsg("lock: next key -> cmd=4 rc=%d", rc); }
|
||||
else logmsg("lock: next key -> media-reserved (no media source)");
|
||||
last_act = now_ms();
|
||||
}
|
||||
prev_vold = vold; prev_volu = volu; prev_play = play; prev_prv = prv; prev_nxt = nxt;
|
||||
/* 长按音量键线性渐变(同主菜单 vol_ramp 路径,每圈推进 1%)。锁屏态不刷状态栏
|
||||
* (状态栏被锁屏页覆盖),仅保持真实音量/DAC 同步。 */
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0)
|
||||
? (kp_keys.ok && kp_is_down(&kp_keys, KEY_VOLUMEUP))
|
||||
: (kp_keys.ok && kp_is_down(&kp_keys, KEY_VOLUMEDOWN));
|
||||
int going = vol_ramp_tick(held);
|
||||
if (!going) logmsg("lock: volume ramp end -> %d", settings_volume_get());
|
||||
}
|
||||
/* 空闲超时 → 自睡(锁态保持,仍在锁屏循环,唤醒回锁屏页) */
|
||||
if (meow_screen_on && now_ms() - last_act >= lk_off_ms) {
|
||||
logmsg("lock: idle %ds -> self screen off (stay locked)", lk_off_sec);
|
||||
meow_panel_off();
|
||||
asleep = 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 触摸:只认键盘 tap(滑动手势忽略:不左缘返回、不通知中心下拉);触摸即输入活动,重置自睡计时 */
|
||||
if (tfd >= 0 && !asleep) {
|
||||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||||
if (poll(&pfd, 1, 0) > 0) {
|
||||
last_act = now_ms(); /* §310 M6.7:任意触摸重置自睡计时 */
|
||||
unsigned char ev[16];
|
||||
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||||
int32_t val = *(int32_t *)(ev + 12);
|
||||
if (type == EV_ABS) {
|
||||
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
|
||||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
|
||||
/* M7.1b 跟手指按压态:down 且两轴有效才定键(goodix 时序免疫);
|
||||
* 键变化(滑动换键)即时重绘按压高亮。 */
|
||||
if (mt.down && mt.last_x >= 0 && mt.last_y >= 0) {
|
||||
if (lk_set_down_key(lk_key_value_at(mt.last_x, mt.last_y)))
|
||||
redraw = 1;
|
||||
}
|
||||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||||
if (val && !mt.down) {
|
||||
mt_down(&mt);
|
||||
} else if (!val && mt.down) {
|
||||
lk_set_down_key(LK_DOWN_NONE); /* M7.1b:抬手清按压态 */
|
||||
redraw = 1;
|
||||
int dx, dy;
|
||||
if (mt_up(&mt, &dx, &dy)) {
|
||||
int handled = 0;
|
||||
for (int r = 0; r < LK_ROWS && !handled; r++) {
|
||||
for (int c = 0; c < LK_COLS && !handled; c++) {
|
||||
if (!lk_key_hit(&mt, r, c)) continue; /* M7.1b SDK 控件命中 */
|
||||
handled = 1;
|
||||
int key = lk_keys[r][c];
|
||||
if (lock_until > 0) break; /* 锁定期间键盘不响应 */
|
||||
if (key >= 0) {
|
||||
if (entered < LCK_PIN_LEN) {
|
||||
digits[entered++] = (char)('0' + key);
|
||||
digits[entered] = 0;
|
||||
}
|
||||
} else if (key == LK_ACT_CLR) {
|
||||
entered = 0; digits[0] = 0;
|
||||
} else if (key == LK_ACT_BS) {
|
||||
if (entered > 0) { entered--; digits[entered] = 0; }
|
||||
}
|
||||
redraw = 1;
|
||||
if (entered == LCK_PIN_LEN) {
|
||||
if (lck_pin_check(digits)) {
|
||||
lck_pin_success();
|
||||
lck_manual_lock_clear(); /* 主动锁标志消解 */
|
||||
logmsg("lock: PIN OK -> unlock");
|
||||
unlocked = 1;
|
||||
goto done;
|
||||
} else {
|
||||
int rem = lck_pin_fail();
|
||||
entered = 0; digits[0] = 0;
|
||||
if (rem > 0) {
|
||||
lock_until = time(NULL) + rem;
|
||||
last_countdown = 0;
|
||||
snprintf(status, sizeof status, "%s", meow_str_zh("lock.locked", "已锁定"));
|
||||
} else {
|
||||
if (mfont_ready) snprintf(status, sizeof status, "%s", meow_str_zh("lock.pin_wrong", "PIN 错误"));
|
||||
else snprintf(status, sizeof status, "WRONG PIN");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 整页重绘(锁屏低频交互,简单整页即可);自睡(面板已关)期间不重绘(唤醒时统一重画) */
|
||||
if (redraw && !asleep) lk_draw(entered, status, lock_until);
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
done:
|
||||
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
|
||||
g_lk_down_key = LK_DOWN_NONE;
|
||||
/* M7.1 解锁恢复:把锁屏首扑前快照的底层帧整帧回写 back_buf + present,强制底层 App/主界面
|
||||
* 完整重绘(消除「解锁后整屏 = PIN 最后一帧,仅 App 控件自绘区刷新」的脏区残留)。 */
|
||||
if (unlocked && lk_bg_snap && back_buf_ok && fb && lk_bg_len > 0) {
|
||||
memcpy(fb, lk_bg_snap, lk_bg_len);
|
||||
meow_dirty_full();
|
||||
meow_ui_flush();
|
||||
logmsg("lock: unlock -> restore underlying frame (full repaint)");
|
||||
}
|
||||
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
|
||||
if (tfd >= 0) {
|
||||
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 也会自动解除,双保险) */
|
||||
ioctl(tfd, EVIOCGRAB, &grab);
|
||||
close(tfd);
|
||||
}
|
||||
kp_close(&kp_power);
|
||||
kp_close(&kp_keys);
|
||||
logmsg("lock: exit unlocked=%d screen_on=%d", unlocked, meow_screen_on);
|
||||
return unlocked;
|
||||
died:
|
||||
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
|
||||
g_lk_down_key = LK_DOWN_NONE;
|
||||
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
|
||||
if (tfd >= 0) {
|
||||
int grab = 0;
|
||||
ioctl(tfd, EVIOCGRAB, &grab);
|
||||
close(tfd);
|
||||
}
|
||||
kp_close(&kp_power);
|
||||
kp_close(&kp_keys);
|
||||
logmsg("lock: exit overlay-app-died -> LCK_OVERLAY_RET_DIED");
|
||||
return LCK_OVERLAY_RET_DIED;
|
||||
}
|
||||
|
||||
/* 电源菜单「锁定」:**主动锁屏**(用户定案 2026-09-02 强化)。
|
||||
* 语义 = 立即熄屏 + 进入锁定 + 作废挂起的面板绕过标志:
|
||||
* ① rm /tmp/meow_pin_unlock_once(主动锁屏作废一次性绕过);
|
||||
* ② 写 /tmp/meow_pin_locked(主动锁标志 → 唤醒时无论 system.pin_lock_on_wake 是否开都必锁屏);
|
||||
* ③ meow_panel_off() 立即熄屏(唤醒时经主屏唤醒路径弹锁屏页)。
|
||||
* 不立即弹锁屏页(已熄屏);未配凭据 → no-op。返回 0(关菜单,run_menu 继续)。 */
|
||||
int meow_lock_screen_engage(void)
|
||||
{
|
||||
if (!lck_pin_configured()) {
|
||||
logmsg("lock: no system.pin/panel.pin configured -> skip manual lock (no-op)");
|
||||
return 0;
|
||||
}
|
||||
if (access(LCK_UNLOCK_ONCE_FILE, F_OK) == 0) {
|
||||
unlink(LCK_UNLOCK_ONCE_FILE);
|
||||
logmsg("lock: manual active-lock revoked pending unlock-once bypass");
|
||||
}
|
||||
lck_manual_lock_set();
|
||||
meow_panel_off(); /* 立即熄屏;唤醒时必走锁屏 */
|
||||
logmsg("lock: manual active-lock -> screen off + lock on wake");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 设置页「锁屏 PIN」数字输入模态(system 级 PIN 设置,用 SDK 按钮画数字键盘)----
|
||||
* 全屏遮罩:title + 4 圆点(●)+ 数字键盘(1-9 / C / 0 / 退格)。输入满 4 位即返回 1 并拷贝到 out
|
||||
* (不校验——调用方做双输确认/旧 PIN 校验);电源键=取消返回 0;音量键不动。 */
|
||||
int lck_input_pin_modal(const char *title, char *out, int outcap)
|
||||
{
|
||||
meow_screen_on = 1;
|
||||
fb_pan_buf0_force();
|
||||
meow_touch mt; mt_reset(&mt);
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
/* §310 输入穿透:设置页 PIN 模态同样独占触控设备(后台保活 App 不冻结实时读 evdev,防其
|
||||
* 同坐标控件被 PIN 键盘点击双重触发)。失败降级(仍靠标记+SIGSTOP 隔离)。 */
|
||||
if (tfd >= 0) {
|
||||
int grab = 1;
|
||||
if (ioctl(tfd, EVIOCGRAB, &grab) < 0)
|
||||
logmsg("pin-modal: EVIOCGRAB touch fail: %s (graceful degrade)", strerror(errno));
|
||||
}
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER);
|
||||
kp_open(&kp_keys, DEV_KEYS);
|
||||
|
||||
int entered = 0;
|
||||
char digits[LCK_PIN_LEN + 1] = {0};
|
||||
int result = 0;
|
||||
int dirty = 1; /* 首帧必画 */
|
||||
|
||||
for (;;) {
|
||||
/* 只在首帧/输入变化时整页重绘(消频闪:锁屏低频交互),后续轮次不动 LCDC */
|
||||
if (dirty) {
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
fill_rect(0, 0, fb_xres, fb_yres, th->surface);
|
||||
lk_text(fb_xres / 2, 120, title, 24, th->on_surface);
|
||||
lk_draw_pin(entered, th);
|
||||
for (int r = 0; r < LK_ROWS; r++) {
|
||||
for (int c = 0; c < LK_COLS; c++) {
|
||||
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
|
||||
fill_round_rect(x, y, w, h, 12, th->surface_container);
|
||||
int key = lk_keys[r][c];
|
||||
if (key >= 0) {
|
||||
char d[2] = { (char)('0' + key), 0 };
|
||||
if (mfont_ready) {
|
||||
int fw = meow_font_width_text(d, 30);
|
||||
meow_font_drawtext((uint16_t *)fb, x + w / 2 - fw / 2,
|
||||
y + LK_ROW_H / 2 - meow_font_line_height(30) / 2,
|
||||
d, rgb565(th->on_surface), 30);
|
||||
} else {
|
||||
draw_text(x + w / 2 - 7, y + LK_ROW_H / 2 - 14, d, 2, th->on_surface);
|
||||
}
|
||||
} else if (key == LK_ACT_CLR) {
|
||||
const char *t2 = "C"; if (mfont_ready) t2 = meow_str_zh("lock.clear", "清空");
|
||||
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t2, 22, th->on_surface);
|
||||
} else {
|
||||
const char *t2 = "DEL"; if (mfont_ready) t2 = meow_str_zh("lock.backspace", "退格");
|
||||
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t2, 22, th->on_surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
meow_dirty_full();
|
||||
fb_pan_buf0_force();
|
||||
meow_ui_flush();
|
||||
dirty = 0;
|
||||
}
|
||||
|
||||
if (kp_power.ok) {
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
if (kp_is_down(&kp_power, KEY_POWER)) { result = 0; break; } /* 取消 */
|
||||
}
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
(void)kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
(void)kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
|
||||
if (tfd >= 0) {
|
||||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||||
if (poll(&pfd, 1, 0) > 0) {
|
||||
unsigned char ev[16];
|
||||
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||||
int32_t val = *(int32_t *)(ev + 12);
|
||||
if (type == EV_ABS) {
|
||||
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
|
||||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
|
||||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||||
if (val && !mt.down) { mt_down(&mt); }
|
||||
else if (!val && mt.down) {
|
||||
int dx, dy;
|
||||
if (mt_up(&mt, &dx, &dy)) {
|
||||
for (int r = 0; r < LK_ROWS; r++) {
|
||||
for (int c = 0; c < LK_COLS; c++) {
|
||||
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
|
||||
if (!mt_in(&mt, x, y, w, h)) continue;
|
||||
int key = lk_keys[r][c];
|
||||
if (key >= 0) {
|
||||
if (entered < LCK_PIN_LEN) { digits[entered++] = (char)('0' + key); digits[entered] = 0; }
|
||||
} else if (key == LK_ACT_CLR) { entered = 0; digits[0] = 0; }
|
||||
else if (key == LK_ACT_BS) { if (entered > 0) { entered--; digits[entered] = 0; } }
|
||||
dirty = 1;
|
||||
if (entered == LCK_PIN_LEN) {
|
||||
if (out && outcap > LCK_PIN_LEN) { snprintf(out, (size_t)outcap, "%s", digits); result = 1; }
|
||||
else result = 1;
|
||||
goto done2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
done2:
|
||||
if (tfd >= 0) {
|
||||
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 亦自动解除,双保险) */
|
||||
ioctl(tfd, EVIOCGRAB, &grab);
|
||||
close(tfd);
|
||||
}
|
||||
kp_close(&kp_power);
|
||||
kp_close(&kp_keys);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/* lock_settings.c — MeowOS 系统设置「锁屏 PIN」独立子页面(subpage.c 脚手架,用户定案 2026-09-02)。
|
||||
*
|
||||
* 页面(sub_page_t 驱动,同 wifi_page/bt_page):
|
||||
* - 状态行(SUB_ROW):未设置 / 已设置 / 锁定中(剩余 mm:ss)
|
||||
* - [未设] 设置 PIN:4 位数字双输入确认(lck_input_pin_modal,SDK 数字键)
|
||||
* - [已设] 修改 PIN:需旧 PIN 一次校验 + 新 PIN 双输入确认
|
||||
* - [已设] 清除 PIN:**无需旧密码**(物理持机即授权,仅 s_dd_modal 二次确认)→ lck_pin_clear
|
||||
* - 唤醒后锁屏(system.pin_lock_on_wake)开关行
|
||||
* 全程走 lock_screen.c 纯逻辑 + meow_settings 后端(无旁路)。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
|
||||
#define LS_ACT_STATUS 0
|
||||
#define LS_ACT_SET 1
|
||||
#define LS_ACT_MODIFY 2
|
||||
#define LS_ACT_CLEAR 3
|
||||
#define LS_ACT_WAKE 4
|
||||
|
||||
static char g_ls_status[48]; /* 状态行摘要(rows 填,paint 后不再变) */
|
||||
|
||||
static void ls_status_text(char *buf, int len)
|
||||
{
|
||||
if (!lck_pin_configured()) { snprintf(buf, len, "%s", meow_str_zh("lock.status_unset", "未设置")); return; }
|
||||
time_t rem = lck_lock_remaining();
|
||||
if (rem > 0) snprintf(buf, len, meow_str_zh("lock.status_locked", "锁定中(%d:%02d)"), (int)(rem / 60), (int)(rem % 60));
|
||||
else snprintf(buf, len, "%s", meow_str_zh("lock.status_set", "已设置"));
|
||||
}
|
||||
|
||||
static int ls_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||
{
|
||||
(void)ctx;
|
||||
int n = 0;
|
||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = meow_str_zh("lock.title", "锁屏 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||
ls_status_text(g_ls_status, sizeof g_ls_status);
|
||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = meow_str_zh("lock.status_row", "状态"); rbuf[n].sub = g_ls_status; rbuf[n].on = 0; rbuf[n].act = LS_ACT_STATUS; n++; }
|
||||
int configured = lck_pin_configured();
|
||||
if (!configured) {
|
||||
if (n < max) { rbuf[n].kind = SUB_ACT; rbuf[n].label = meow_str_zh("lock.act_set", "设置 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = LS_ACT_SET; n++; }
|
||||
} else {
|
||||
if (n < max) { rbuf[n].kind = SUB_ACT; rbuf[n].label = meow_str_zh("lock.act_modify", "修改 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = LS_ACT_MODIFY; n++; }
|
||||
if (n < max) { rbuf[n].kind = SUB_ACT; rbuf[n].label = meow_str_zh("lock.act_clear", "清除 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = LS_ACT_CLEAR; n++; }
|
||||
}
|
||||
if (n < max) {
|
||||
int wake = 1;
|
||||
if (meow_settings_get_int("system.pin_lock_on_wake", &wake) != 0) wake = 1;
|
||||
rbuf[n].kind = SUB_SW; rbuf[n].label = meow_str_zh("lock.wake_lock", "唤醒后锁屏"); rbuf[n].sub = NULL;
|
||||
rbuf[n].on = wake; rbuf[n].act = LS_ACT_WAKE; n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void ls_modal(const char *title, const char *line)
|
||||
{
|
||||
const char *it[1]; it[0] = line;
|
||||
s_dd_modal(title, it, 1, -1);
|
||||
}
|
||||
|
||||
static int ls_do_modify(void)
|
||||
{
|
||||
char old[LCK_PIN_LEN + 1];
|
||||
if (!lck_input_pin_modal(meow_str_zh("lock.enter_cur", "输入当前 PIN"), old, sizeof old)) return 0;
|
||||
if (!lck_pin_check(old)) { ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.cur_wrong", "当前 PIN 错误")); return 1; }
|
||||
char a[LCK_PIN_LEN + 1], b[LCK_PIN_LEN + 1];
|
||||
if (!lck_input_pin_modal(meow_str_zh("lock.set_new", "设置新 PIN"), a, sizeof a)) return 0;
|
||||
if (!lck_input_pin_modal(meow_str_zh("lock.confirm_new", "再次确认新 PIN"), b, sizeof b)) return 0;
|
||||
if (strcmp(a, b) != 0) { ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.mismatch", "两次输入不一致")); return 1; }
|
||||
if (lck_pin_set(a) == MEOW_OK) ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.modified", "已修改"));
|
||||
else ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.set_failed", "设置失败"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int ls_do_set(void)
|
||||
{
|
||||
char a[LCK_PIN_LEN + 1], b[LCK_PIN_LEN + 1];
|
||||
if (!lck_input_pin_modal(meow_str_zh("lock.enter_4", "设置 4 位 PIN"), a, sizeof a)) return 0;
|
||||
if (!lck_input_pin_modal(meow_str_zh("lock.confirm_input", "再次输入确认"), b, sizeof b)) return 0;
|
||||
if (strcmp(a, b) != 0) { ls_modal(meow_str_zh("lock.act_set", "设置 PIN"), meow_str_zh("lock.mismatch", "两次输入不一致")); return 1; }
|
||||
if (lck_pin_set(a) == MEOW_OK) ls_modal(meow_str_zh("lock.act_set", "设置 PIN"), meow_str_zh("lock.status_set", "已设置"));
|
||||
else ls_modal(meow_str_zh("lock.act_set", "设置 PIN"), meow_str_zh("lock.set_failed", "设置失败"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int ls_do_clear(void)
|
||||
{
|
||||
const char *items[2]; items[0] = meow_str_zh("power.confirm_yes", "是"); items[1] = meow_str_zh("power.confirm_no", "否");
|
||||
int pick = s_dd_modal(meow_str_zh("lock.clear_confirm", "确定清除锁屏 PIN?"), items, 2, 1); /* 默认「否」(安全) */
|
||||
if (pick != 0) return 1;
|
||||
if (lck_pin_clear() == MEOW_OK) ls_modal(meow_str_zh("lock.act_clear", "清除 PIN"), meow_str_zh("lock.cleared", "已清除"));
|
||||
else ls_modal(meow_str_zh("lock.act_clear", "清除 PIN"), meow_str_zh("lock.clear_failed", "清除失败(settings 不可写)"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int ls_act(void *ctx, int idx, int act)
|
||||
{
|
||||
(void)ctx; (void)idx;
|
||||
switch (act) {
|
||||
case LS_ACT_SET: return ls_do_set();
|
||||
case LS_ACT_MODIFY: return ls_do_modify();
|
||||
case LS_ACT_CLEAR: return ls_do_clear();
|
||||
case LS_ACT_WAKE: {
|
||||
int v = 1;
|
||||
if (meow_settings_get_int("system.pin_lock_on_wake", &v) != 0) v = 1;
|
||||
meow_settings_set_int("system.pin_lock_on_wake", v ? 0 : 1);
|
||||
meow_settings_save();
|
||||
logmsg("lock_settings: wake-lock toggle -> %d", v ? 0 : 1);
|
||||
return 1; /* 重绘 + 重填行(开关态) */
|
||||
}
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int ls_detail(void *ctx, int idx, int act)
|
||||
{
|
||||
(void)ctx; (void)idx;
|
||||
if (act == LS_ACT_STATUS) {
|
||||
char line[80];
|
||||
ls_status_text(line, sizeof line);
|
||||
int rem = (int)lck_lock_remaining();
|
||||
if (lck_pin_configured() && rem > 0) {
|
||||
char buff[96];
|
||||
snprintf(buff, sizeof buff, meow_str_zh("lock.locked_retry", "连续错误已锁定,剩余 %d:%02d 后重试"), rem / 60, rem % 60);
|
||||
ls_modal(meow_str_zh("lock.title", "锁屏 PIN"), buff);
|
||||
} else {
|
||||
ls_modal(meow_str_zh("lock.title", "锁屏 PIN"), line);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 运行「锁屏 PIN」独立子页面(阻塞到返回) */
|
||||
int run_lock_settings_page(void)
|
||||
{
|
||||
sub_page_t p;
|
||||
memset(&p, 0, sizeof p);
|
||||
p.title = meow_str_zh("lock.title", "锁屏 PIN");
|
||||
p.top_sw = NULL;
|
||||
p.top_sw_set = NULL;
|
||||
p.rows = ls_rows;
|
||||
p.act = ls_act;
|
||||
p.detail = ls_detail;
|
||||
p.live = 0;
|
||||
p.ctx = NULL;
|
||||
return meow_subpage_run(&p);
|
||||
}
|
||||
+650
-132
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,129 @@
|
||||
/* meow_ams_start.c — MSG_AMS_START 拉起决策(AMSD_DESIGN §4.3)。
|
||||
*
|
||||
* 独立模块(无 launcher 内部依赖,仅 meow_ipc.h):持有 pending-start 状态 + 拉起决策,
|
||||
* 经 ops 注入真实启动链路(launch_app/launch_activity + resolve_chain + logmsg),
|
||||
* 使「am start」在 run_menu 主循环中真正拉起目标 App。
|
||||
*
|
||||
* 根因修复:历史实现 `meow_ams_pending_start_set` 返回 `-MEOW_ERR_BUSY`(=+8,正)而非
|
||||
* 负错误码,ipc_dispatch 的 `rc<0?-rc:MEOW_OK` 因此把「有前台 App 拒绝」误判为 OK(0),
|
||||
* 且拒绝发生在置 pending 之前 → amsd 报 OK 但 App 永不拉起。本模块统一为负 MEOW_ERR_*。
|
||||
*
|
||||
* 单一写者约定:pending 状态只在主循环上下文写入/消费(ipc_dispatch 写、run_menu 消费),
|
||||
* 与 launcher 「单写者」纪律一致,无需锁。
|
||||
*/
|
||||
#include "meow_ams_start.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
struct meow_ams_start_state {
|
||||
char pkg[MEOW_APP_ID_MAX];
|
||||
char act[MEOW_ACT_MAX];
|
||||
int pending;
|
||||
/* picker 上下文(二期;pick_armed=1 时 next launch 注入 env) */
|
||||
char pick_action[24];
|
||||
char pick_caller[MEOW_APP_ID_MAX];
|
||||
uint32_t pick_req;
|
||||
int pick_armed;
|
||||
};
|
||||
|
||||
static struct meow_ams_start_state g_ams; /* 主循环单写者,无锁 */
|
||||
static struct meow_ams_ops g_ops;
|
||||
static int g_busy; /* =g_app_session_active 镜像(主循环每圈同步) */
|
||||
|
||||
void meow_ams_ops_init(const struct meow_ams_ops *ops)
|
||||
{
|
||||
if (ops) g_ops = *ops;
|
||||
else memset(&g_ops, 0, sizeof g_ops);
|
||||
}
|
||||
|
||||
void meow_ams_set_busy(int busy)
|
||||
{
|
||||
g_busy = busy;
|
||||
}
|
||||
|
||||
int meow_ams_pending_start_set(const char *pkg, const char *act)
|
||||
{
|
||||
if (!pkg || !pkg[0]) return MEOW_ERR_INVAL; /* 负错误码(根因修复) */
|
||||
if (g_busy) return MEOW_ERR_BUSY; /* 有前台 App:拒绝(负错误码) */
|
||||
snprintf(g_ams.pkg, sizeof g_ams.pkg, "%s", pkg);
|
||||
g_ams.act[0] = 0;
|
||||
if (act && act[0]) snprintf(g_ams.act, sizeof g_ams.act, "%s", act);
|
||||
g_ams.pending = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
|
||||
const char *pick_action, uint32_t pick_req,
|
||||
const char *pick_caller)
|
||||
{
|
||||
int rc = meow_ams_pending_start_set(pkg, act);
|
||||
if (rc != 0) return rc; /* 拒绝(有前台 App / 参数非法) */
|
||||
g_ams.pick_action[0] = 0;
|
||||
g_ams.pick_caller[0] = 0;
|
||||
g_ams.pick_req = 0;
|
||||
g_ams.pick_armed = 0;
|
||||
if (pick_action && pick_action[0]) {
|
||||
snprintf(g_ams.pick_action, sizeof g_ams.pick_action, "%s", pick_action);
|
||||
if (pick_caller) snprintf(g_ams.pick_caller, sizeof g_ams.pick_caller, "%s", pick_caller);
|
||||
g_ams.pick_req = pick_req;
|
||||
g_ams.pick_armed = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_ams_take_picker(char *action, int action_len,
|
||||
char *caller, int caller_len, uint32_t *req)
|
||||
{
|
||||
if (!g_ams.pick_armed) return 0;
|
||||
g_ams.pick_armed = 0;
|
||||
if (action && action_len > 0) snprintf(action, action_len, "%s", g_ams.pick_action);
|
||||
if (caller && caller_len > 0) snprintf(caller, caller_len, "%s", g_ams.pick_caller);
|
||||
if (req) *req = g_ams.pick_req;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int meow_ams_start_consume(const char *const *ids, int napps, int state,
|
||||
char *toast, int toast_len, int *app_ran, int *idx_out)
|
||||
{
|
||||
if (app_ran) *app_ran = 0;
|
||||
if (idx_out) *idx_out = -1;
|
||||
if (!g_ams.pending || g_busy) return 0; /* 无待拉起 / 前台 App 会话中 */
|
||||
|
||||
if (state != 0) {
|
||||
/* 应用列表未就绪:保留 pending(不丢「am start」),等 run_menu 2s 重扫后重试。 */
|
||||
if (toast && toast_len > 0) snprintf(toast, toast_len, "AMS START: app list not ready");
|
||||
return 2;
|
||||
}
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < napps; i++)
|
||||
if (ids[i] && !strcmp(ids[i], g_ams.pkg)) { idx = i; break; }
|
||||
if (idx < 0) {
|
||||
g_ams.pending = 0;
|
||||
if (toast && toast_len > 0) snprintf(toast, toast_len, "AMS START: NO APP %s", g_ams.pkg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char chain[MEOW_CHAIN_MAX] = "";
|
||||
if (g_ams.act[0]) {
|
||||
int depth = 0;
|
||||
int rc = g_ops.resolve ? g_ops.resolve(g_ams.pkg, g_ams.act, chain, &depth)
|
||||
: MEOW_ERR_NOENT;
|
||||
if (rc != MEOW_OK) {
|
||||
g_ams.pending = 0;
|
||||
if (toast && toast_len > 0)
|
||||
snprintf(toast, toast_len, "AMS: NO ROUTE %s/%s", g_ams.pkg, g_ams.act);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
g_ams.pending = 0;
|
||||
if (idx_out) *idx_out = idx;
|
||||
/* 复用既有启动链路(launch_app/launch_activity 阻塞至 app 退出或 exec 失败)
|
||||
* chain 空=裸启动;非空=经 resolve_chain 的 activity 链(含参透传)。
|
||||
* 启动失败(lr<0)不在此设 toast:调用方经 idx_out 拿到 apps[idx].name 打 launch_fail_toast。 */
|
||||
int lr = g_ops.launch ? g_ops.launch(idx, chain[0] ? chain : NULL) : -MEOW_ERR_INTERNAL;
|
||||
if (app_ran) *app_ran = (lr == 0);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* meow_ams_start.h — MSG_AMS_START 拉起决策(AMSD_DESIGN §4.3)。
|
||||
*
|
||||
* 独立模块以便宿主单测:pending 状态 + 拉起决策都在此,启动/解析/日志经 ops 注入
|
||||
* (生产=launcher 既有链路 launch_app/launch_activity + resolve_chain + logmsg;
|
||||
* 测试=假件,见 test_ams_start.c)。
|
||||
*
|
||||
* 约定(与 launcher 其余模块一致):本模块函数返回负的 MEOW_ERR_*(-1=INVAL / -8=BUSY)
|
||||
* 表示拒绝,0 表示受理。ipc_dispatch 用 `a.code=(uint32_t)(rc<0?-rc:MEOW_OK)` 把负错误码
|
||||
* 转成正的 ack.code(幅度);amsd 侧按 `(uint32_t)(-MEOW_ERR_BUSY)` 识别 busy。
|
||||
* —— 历史上 set() 误返回 `-MEOW_ERR_BUSY`(=+8,正)导致 ipc_dispatch 把拒绝当 OK,
|
||||
* 表现为「am start 报 OK 但 App 未拉起」;本模块统一为负错误码(根因修复)。
|
||||
*/
|
||||
#ifndef MEOW_AMS_START_H
|
||||
#define MEOW_AMS_START_H
|
||||
|
||||
#include "meow_ipc.h" /* MEOW_APP_ID_MAX / MEOW_ACT_MAX / MEOW_CHAIN_MAX / MEOW_ERR_* */
|
||||
|
||||
/* 启动/解析/日志回调注入(单测=假件;生产在 meow_ipc_init 末尾组装)。 */
|
||||
struct meow_ams_ops {
|
||||
/* 执行启动。idx=调用方应用表下标;chain 空=裸启动。
|
||||
* 复用 launcher 既有链路:launch_app(&apps[idx]) / launch_activity(&apps[idx], chain)。
|
||||
* 返回 0=已受理(app 已运行至退出);<0=启动失败/未找到。 */
|
||||
int (*launch)(int idx, const char *chain);
|
||||
/* 解析 activity 链(resolve_chain)。返回 MEOW_OK / MEOW_ERR_NOENT。 */
|
||||
int (*resolve)(const char *app_id, const char *activity, char *chain, int *depth);
|
||||
/* 日志(可空=静默)。 */
|
||||
void (*log)(const char *fmt, ...);
|
||||
};
|
||||
|
||||
/* 组装 ops(launcher meow_ipc_init 末尾 / 测试 main 调用)。launch/resolve 须非 NULL。 */
|
||||
void meow_ams_ops_init(const struct meow_ams_ops *ops);
|
||||
|
||||
/* 同步「前台 App 会话」占用位(=g_app_session_active)。
|
||||
* busy=1 时 pending_start_set 拒绝(-MEOW_ERR_BUSY)且 consume 不动作。 */
|
||||
void meow_ams_set_busy(int busy);
|
||||
|
||||
/* 置 pending(ipc_dispatch MSG_AMS_START 时调)。
|
||||
* pkg 空 → -MEOW_ERR_INVAL;busy → -MEOW_ERR_BUSY;否则置位并返回 0。 */
|
||||
int meow_ams_pending_start_set(const char *pkg, const char *act);
|
||||
|
||||
/* 二期 picker 扩载荷:置 pending + 武装 picker 上下文(pick_action 非空时,launch_app_chain
|
||||
* 入口经 meow_ams_take_picker 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER)。错误语义同 set。 */
|
||||
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
|
||||
const char *pick_action, uint32_t pick_req,
|
||||
const char *pick_caller);
|
||||
|
||||
/* 消费 pending(各主循环排空 IPC 后 / run_menu 循环调用)。
|
||||
* ids=napps 个应用主键串指针(apps[i].id);state=菜单态(0=就绪;非 0=列表未就绪不拉起,
|
||||
* pending 保留待重扫重试)。toast/toast_len=错误提示槽(可空)。
|
||||
* app_ran=输出:1=app 已运行;0=未运行(拒绝/失败)。idx_out=输出:本次使用的应用表下标
|
||||
* (-1=未命中应用;>=0 且 app_ran=0 表示启动失败,调用方应据 apps[idx].name 打 launch_fail_toast)。
|
||||
* 返回:0=无 pending 或 busy(暂不动作);1=已消费(拉起成功/拒绝并清 pending,toast 已设);
|
||||
* 2=列表未就绪(pending 保留待重扫重试,toast 已设「not ready」,调用方勿红冲)。 */
|
||||
int meow_ams_start_consume(const char *const *ids, int napps, int state,
|
||||
char *toast, int toast_len, int *app_ran, int *idx_out);
|
||||
|
||||
/* 消费拾取 picker 上下文(launch_app_chain 入口调用,一次性清位)。
|
||||
* 返回 1=有 picker 上下文;0=无。 */
|
||||
int meow_ams_take_picker(char *action, int action_len,
|
||||
char *caller, int caller_len, uint32_t *req);
|
||||
|
||||
#endif /* MEOW_AMS_START_H */
|
||||
@@ -0,0 +1,69 @@
|
||||
/* meow_bgmgr.c — 批③ 后台管理页 逻辑层(数据汇聚 + kill 门控)。实现见 meow_bgmgr.h。
|
||||
* 纯 C(无 launcher static/SDK 依赖),宿主 gcc 直编:gcc -Wall -Wextra -std=gnu99 \
|
||||
* test_bgmgr.c meow_bgmgr.c -o test_bgmgr。 */
|
||||
#include "meow_bgmgr.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
|
||||
{
|
||||
if (!ops || !ops->enumerate || !ops->app_meta || !rows || max <= 0) return 0;
|
||||
int n = ops->enumerate(ops->ctx, rows, max);
|
||||
if (n < 0) n = 0;
|
||||
if (n > max) n = max;
|
||||
for (int i = 0; i < n; i++) {
|
||||
bgmgr_row_t *r = &rows[i];
|
||||
r->protected_app = 0;
|
||||
r->is_fg = 0;
|
||||
r->name[0] = 0;
|
||||
r->icon[0] = 0;
|
||||
r->pcpu = 0.0;
|
||||
r->rss_kb = 0;
|
||||
ops->app_meta(ops->ctx, r->app_id, r->pid,
|
||||
r->name, (int)sizeof r->name, r->icon, (int)sizeof r->icon,
|
||||
&r->protected_app, &r->is_fg);
|
||||
if (!r->name[0]) snprintf(r->name, sizeof r->name, "%s", r->app_id);
|
||||
if (ops->proc_sample && ops->proc_sample(ops->ctx, r->pid, &r->pcpu, &r->rss_kb) < 0) {
|
||||
r->pcpu = 0.0; r->rss_kb = 0;
|
||||
}
|
||||
r->killable = bgmgr_can_kill(r);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int bgmgr_can_kill(const bgmgr_row_t *r)
|
||||
{
|
||||
if (!r) return 0;
|
||||
if (r->protected_app) return 0; /* 系统级受保护 App 不 kill(铁律;framework 是 SDK 标志,不在此列) */
|
||||
if (r->is_fg) return 0; /* 当前前台 App 不 kill */
|
||||
if (r->pid <= 0) return 0; /* 非后台/无进程 */
|
||||
return 1;
|
||||
}
|
||||
|
||||
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
|
||||
void (*kill_fn)(const char *app_id))
|
||||
{
|
||||
(void)ops;
|
||||
if (!kill_fn) return 0;
|
||||
int k = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (bgmgr_can_kill(&rows[i])) { kill_fn(rows[i].app_id); k++; }
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
const char *bgmgr_status_str(const bgmgr_row_t *r)
|
||||
{
|
||||
if (!r) return "";
|
||||
if (r->is_fg) return "前台";
|
||||
if (r->keepalive) return "常驻";
|
||||
return "后台";
|
||||
}
|
||||
|
||||
const char *bgmgr_mem_str(long rss_kb)
|
||||
{
|
||||
static char buf[24];
|
||||
if (rss_kb >= 1024) snprintf(buf, sizeof buf, "%.1fMB", (double)rss_kb / 1024.0);
|
||||
else snprintf(buf, sizeof buf, "%ldKB", rss_kb);
|
||||
return buf;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* meow_bgmgr.h — 批③ 后台管理页 逻辑层(数据汇聚 + kill 门控)。自包含、依赖注入、可独立单测。
|
||||
*
|
||||
* 定位:把「后台常驻集 → 展示行(图标/名字/CPU/内存/状态)→ kill/一键清」的**纯逻辑**从 UI 抽出,
|
||||
* 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法:
|
||||
* UI 提供真实 enumerate/app_meta/proc_sample;单测用假件。**单一写者=launcher 主循环**,无锁。
|
||||
*
|
||||
* 铁律遵循:kill/一键清 带 !protected_app 门控(防误杀系统/受保护 App,对齐 app_launcher.c:2088 教训)。
|
||||
* **收口:门控位不是 app.json 的 `framework`**——那是「用 meow_ui.a 的 SDK 框架 App」标志
|
||||
* (docs/APP_DEV_GUIDE.md §9,推荐且现有 App 全写 1),拿它当「禁止 kill」会让所有后台 App 变成
|
||||
* 不可杀(2026-11-04 真机:dev.player.ncm 声明 framework:1 → 后台页杀按钮全被置灰点不动)。
|
||||
* 不新建 daemon;不引 GDBus;不碰 /data/recovery_all。
|
||||
*/
|
||||
#ifndef MEOW_BGMGR_H
|
||||
#define MEOW_BGMGR_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h> /* pid_t */
|
||||
#include "meow_ipc.h" /* MEOW_APP_ID_MAX */
|
||||
|
||||
/* 定义于 meow_bgplay.h,此处仅注释:与 launcher 后台表同槽数(MEOW_BG_MAX=4)。 */
|
||||
#define MEOW_BGMR_ROW_MAX 4
|
||||
|
||||
/* 后台管理页一行。enumerate 填 app_id/pid/keepalive/on_card/bg_since_ms;
|
||||
* bgmgr_collect 再经 app_meta/proc_sample 补元数据/CPU/RSS,并预判 killable。 */
|
||||
typedef struct bgmgr_row {
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
char name[49]; /* display name(回退 app_id) */
|
||||
char icon[129]; /* 图标 path(空=无,页面用首字/占位) */
|
||||
char protected_app; /* '1'=系统级受保护 App(禁止 kill)。**非** app.json framework;
|
||||
* 目前 launcher 自身组件(状态栏/设置页…)不在 App 表/后台表里,
|
||||
* 故实际恒为 0,保留为将来受保护组件的显式标记位。 */
|
||||
pid_t pid; /* >0 后台进程 */
|
||||
int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */
|
||||
int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */
|
||||
int on_card; /* 1=二进制在 TF 卡 */
|
||||
uint64_t bg_since_ms; /* 转后台时刻(monotonic ms) */
|
||||
double pcpu; /* 0-100 */
|
||||
long rss_kb; /* 常驻内存 */
|
||||
int killable; /* 预判:bgmgr_can_kill()==1 */
|
||||
} bgmgr_row_t;
|
||||
|
||||
/* 依赖注入(**必须全非空**;enumerate 返回行数,app_meta/proc_sample 返回 0=成功)。 */
|
||||
typedef struct bgmgr_ops {
|
||||
/* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms,
|
||||
* 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */
|
||||
int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max);
|
||||
/* 按 app_id 补 App 元数据:name/icon/protected_app/is_fg。返回 0=找到;<0=未注册(name 回退 app_id)。 */
|
||||
int (*app_meta)(void *ctx, const char *app_id, pid_t pid,
|
||||
char *name, int nl, char *icon, int il,
|
||||
char *protected_app, int *is_fg);
|
||||
/* 每进程 CPU/RSS 取样:返回 0=ok(写 pcpu 与 rss_kb);<0=无数据(pcpu=0/rss=0)。 */
|
||||
int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb);
|
||||
void (*log)(const char *fmt, ...); /* 可空=静默 */
|
||||
void *ctx;
|
||||
} bgmgr_ops_t;
|
||||
|
||||
/* 枚举后台常驻并补全元数据/CPU/RSS。返回行数(>=0);app_id 空/重复加固在 enumerate 侧。
|
||||
* 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */
|
||||
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max);
|
||||
|
||||
/* !protected_app 门控 + 非前台 + pid>0:返回 1=可 kill;0=禁止(受保护/当前前台/无进程)。 */
|
||||
int bgmgr_can_kill(const bgmgr_row_t *r);
|
||||
|
||||
/* 一键清:遍历 rows,bgmgr_can_kill() 的条目逐一调 kill_fn(app_id)。返回发出 kill 数。
|
||||
* **不杀受保护 App/当前前台/非后台**;调用方负责对返回 0 弹「无后台 App 可清」之类提示。 */
|
||||
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
|
||||
void (*kill_fn)(const char *app_id));
|
||||
|
||||
/* 状态文案(用于行内「状态」列):「常驻」keepalive=1;「后台」宽限期类;「前台」is_fg=1;空=非后台。 */
|
||||
const char *bgmgr_status_str(const bgmgr_row_t *r);
|
||||
|
||||
/* 内存文案(rss_kb → "x.xMB"/"xxKB")。返回静态缓冲区;勿复用变长调用。 */
|
||||
const char *bgmgr_mem_str(long rss_kb);
|
||||
|
||||
#endif /* MEOW_BGMGR_H */
|
||||
@@ -0,0 +1,622 @@
|
||||
/* meow_bgplay.c — M4 后台播放保活 + 无头后台服务运行时表(见 meow_bgplay.h 头注释)。 */
|
||||
#include "meow_bgplay.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
static meow_bgplay_ops g_ops;
|
||||
static meow_bg_entry_t g_bg[MEOW_BG_MAX];
|
||||
static meow_svc_rt_t g_svc[MEOW_SVC_RT_MAX];
|
||||
static uint32_t g_change_seq;
|
||||
|
||||
static const uint32_t k_backoff[] = {
|
||||
MEOW_SVC_BACKOFF_1, MEOW_SVC_BACKOFF_2, MEOW_SVC_BACKOFF_4, MEOW_SVC_BACKOFF_10
|
||||
};
|
||||
#define BACKOFF_STEPS ((int)(sizeof(k_backoff) / sizeof(k_backoff[0])))
|
||||
|
||||
/* SIGTERM 后 SIGKILL 兜底宽限(ms) */
|
||||
#define TERM_GRACE_MS 500u
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void bg_log(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
char buf[384];
|
||||
|
||||
if (!g_ops.log) return;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
g_ops.log("%s", buf);
|
||||
}
|
||||
|
||||
static uint64_t bg_now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
if (g_ops.now_ms) return g_ops.now_ms();
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
static int bg_card_present(void)
|
||||
{
|
||||
return g_ops.card_present ? g_ops.card_present() : 1;
|
||||
}
|
||||
|
||||
void meow_bgplay_init(const meow_bgplay_ops *ops)
|
||||
{
|
||||
memset(&g_ops, 0, sizeof(g_ops));
|
||||
memset(g_bg, 0, sizeof(g_bg));
|
||||
memset(g_svc, 0, sizeof(g_svc));
|
||||
g_change_seq = 0;
|
||||
if (ops) g_ops = *ops;
|
||||
}
|
||||
|
||||
/* ================= 后台播放保活 ================= */
|
||||
|
||||
int meow_bg_should_keep(int bg_audio_declared, int media_resident, int keepalive_declared)
|
||||
{
|
||||
if (keepalive_declared) return 1; /* 通用常驻:进后台即保活 */
|
||||
return bg_audio_declared && media_resident; /* 播放类:播放/暂停/缓冲都保活 */
|
||||
}
|
||||
|
||||
static meow_bg_entry_t *bg_find(const char *app_id)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (g_bg[i].used && strcmp(g_bg[i].app_id, app_id) == 0)
|
||||
return &g_bg[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 填一槽(add 与槽满 LRU 复用)。 */
|
||||
static void bg_fill(int i, const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive)
|
||||
{
|
||||
meow_bg_entry_t *e = &g_bg[i];
|
||||
|
||||
memset(e, 0, sizeof(*e));
|
||||
e->used = 1;
|
||||
snprintf(e->app_id, sizeof(e->app_id), "%s", app_id);
|
||||
e->pid = pid;
|
||||
e->out_fd = out_fd;
|
||||
e->on_card = on_card;
|
||||
e->bg_since_ms = bg_now_ms(); /* 批①:转后台时刻(宽限/槽满 LRU 掐最久用的锚点) */
|
||||
e->keepalive = keepalive ? 1 : 0; /* 批①+宽限:1=声明常驻(无限期);0=宽限期类 */
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] 转后台保活: %s pid=%d on_card=%d keepalive=%d",
|
||||
app_id, (int)pid, on_card, e->keepalive);
|
||||
}
|
||||
|
||||
/* 真实单调毫秒(不随 ops.now_ms 注入的假时钟走)。槽满 LRU 回收需要真实的有界等超时——
|
||||
* 注入假时钟的宿主测试若用它算 deadline 则永不超时(假时钟不随 usleep 推进),故独立取真时钟。 */
|
||||
static uint64_t bg_real_now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
/* 槽满 LRU 回收:找 bg_since_ms 最旧的**未声明且媒体不活跃**槽,走真退序列 SIGTERM→有界等→
|
||||
* SIGKILL→reap→on_bg_dead(通知 amsd + 清媒体会话)→ 摘表。返回被回收槽下标;-1=无可回收
|
||||
* (全为声明常驻/媒体活跃,按保安全拒绝新入)。 */
|
||||
static int bg_evict_lru(void)
|
||||
{
|
||||
uint64_t oldest = UINT64_MAX;
|
||||
int cand = -1;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
if (!g_bg[i].used) continue;
|
||||
if (g_bg[i].keepalive) continue; /* 声明常驻不回收 */
|
||||
if (g_ops.media_resident && g_ops.media_resident(g_bg[i].app_id))
|
||||
continue; /* 媒体活跃不回收 */
|
||||
if (g_bg[i].bg_since_ms < oldest) { oldest = g_bg[i].bg_since_ms; cand = i; }
|
||||
}
|
||||
if (cand < 0) return -1;
|
||||
|
||||
{
|
||||
meow_bg_entry_t *e = &g_bg[cand];
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
pid_t pid = e->pid;
|
||||
int ofd = e->out_fd;
|
||||
int st = 0;
|
||||
uint64_t deadline;
|
||||
|
||||
snprintf(id, sizeof(id), "%s", e->app_id);
|
||||
bg_log("[bgplay] 后台表满 LRU 回收: %s pid=%d(最久未激活·未声明)", id, (int)pid);
|
||||
kill(pid, SIGTERM);
|
||||
deadline = bg_real_now_ms() + TERM_GRACE_MS;
|
||||
for (;;) {
|
||||
pid_t r = waitpid(pid, &st, WNOHANG);
|
||||
if (r == pid) break;
|
||||
if (r < 0 && errno != EINTR) break; /* 已回收/非子进程:不再等 */
|
||||
if (bg_real_now_ms() >= deadline) {
|
||||
kill(pid, SIGKILL);
|
||||
deadline = bg_real_now_ms() + 200u; /* SIGKILL 后非阻塞兜底,防 D-state 挂死 */
|
||||
while (bg_real_now_ms() < deadline) {
|
||||
if (waitpid(pid, &st, WNOHANG) == pid) break;
|
||||
usleep(10000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
usleep(5000);
|
||||
}
|
||||
if (ofd >= 0) close(ofd); /* 被回收 App 已死,其 stdout 读端一并关 */
|
||||
if (g_ops.on_bg_dead) g_ops.on_bg_dead(id, pid, st); /* 通知 amsd + 清媒体会话 */
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] LRU 回收完成: %s 槽释放", id);
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
return cand;
|
||||
}
|
||||
|
||||
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (!app_id || !app_id[0] || pid <= 0) return MEOW_ERR_INVAL;
|
||||
if (bg_find(app_id)) return MEOW_ERR_EXISTS;
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (!g_bg[i].used) { bg_fill(i, app_id, pid, out_fd, on_card, keepalive); return 0; }
|
||||
/* 槽满:LRU 回收最久未激活的未声明槽;全为声明常驻/媒体活跃 → NOMEM(调用方回落真退)。 */
|
||||
if (bg_evict_lru() >= 0)
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (!g_bg[i].used) { bg_fill(i, app_id, pid, out_fd, on_card, keepalive); return 0; }
|
||||
return MEOW_ERR_NOMEM;
|
||||
}
|
||||
|
||||
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid)
|
||||
{
|
||||
meow_bg_entry_t *e;
|
||||
|
||||
if (!app_id || !app_id[0]) return -1;
|
||||
e = bg_find(app_id);
|
||||
if (!e) return -1;
|
||||
if (out_fd) *out_fd = e->out_fd;
|
||||
if (pid) *pid = e->pid;
|
||||
bg_log("[bgplay] 回前台: %s pid=%d", app_id, (int)e->pid);
|
||||
memset(e, 0, sizeof(*e));
|
||||
g_change_seq++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_bg_is_backgrounded(const char *app_id)
|
||||
{
|
||||
return app_id && app_id[0] && bg_find(app_id) != NULL;
|
||||
}
|
||||
|
||||
int meow_bg_count(void)
|
||||
{
|
||||
int i, n = 0;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (g_bg[i].used) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
const meow_bg_entry_t *meow_bg_at(int i)
|
||||
{
|
||||
if (i < 0 || i >= MEOW_BG_MAX || !g_bg[i].used) return NULL;
|
||||
return &g_bg[i];
|
||||
}
|
||||
|
||||
uint32_t meow_bgplay_change_seq(void)
|
||||
{
|
||||
return g_change_seq;
|
||||
}
|
||||
|
||||
void meow_bg_tick(void)
|
||||
{
|
||||
uint64_t now = bg_now_ms();
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
meow_bg_entry_t *e = &g_bg[i];
|
||||
int st;
|
||||
pid_t r;
|
||||
|
||||
if (!e->used) continue;
|
||||
|
||||
/* 触发终止(SIGTERM→SIGKILL 真退序列)的两类条件,仅当尚未进入终止序列(term_at_ms==0):
|
||||
* a) 卡拔例外:on_card 且卡不在(防 SIGBUS);
|
||||
* b) 批①+宽限 超时回收:未声明(keepalive=0)且媒体不活跃(media_resident=0),转后台
|
||||
* 超 MEOW_BG_GRACE_MS 未再启动 → 回收。声明常驻(keepalive=1)/媒体活跃**不清**。 */
|
||||
if (e->term_at_ms == 0) {
|
||||
int grace = (e->keepalive == 0 &&
|
||||
!(g_ops.media_resident ? g_ops.media_resident(e->app_id) : 0) &&
|
||||
now - e->bg_since_ms > (uint64_t)MEOW_BG_GRACE_MS);
|
||||
if ((e->on_card && !bg_card_present()) || grace) {
|
||||
if (grace)
|
||||
bg_log("[bgplay] 宽限期超时回收后台App: %s pid=%d(超 %ums 未再启动)",
|
||||
e->app_id, (int)e->pid, (unsigned)MEOW_BG_GRACE_MS);
|
||||
else
|
||||
bg_log("[bgplay] 卡拔出,终止卡上后台 App: %s pid=%d",
|
||||
e->app_id, (int)e->pid);
|
||||
kill(e->pid, SIGTERM);
|
||||
e->term_at_ms = now;
|
||||
}
|
||||
}
|
||||
/* SIGTERM 后 SIGKILL 兜底(TERM_GRACE_MS 节流重复发直至 reap;卡拔/宽限/槽满回收共用) */
|
||||
if (e->term_at_ms && now - e->term_at_ms >= TERM_GRACE_MS) {
|
||||
kill(e->pid, SIGKILL);
|
||||
e->term_at_ms = now;
|
||||
}
|
||||
|
||||
r = waitpid(e->pid, &st, WNOHANG);
|
||||
if (r == e->pid) {
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
pid_t pid = e->pid;
|
||||
int ofd = e->out_fd;
|
||||
|
||||
snprintf(id, sizeof(id), "%s", e->app_id);
|
||||
if (ofd >= 0) close(ofd); /* 回收:后台 App 已死,其 stdout 管道读端一并关(防 fd 泄漏) */
|
||||
memset(e, 0, sizeof(*e));
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] 后台 App 退出: %s pid=%d status=0x%x", id, (int)pid, st);
|
||||
if (g_ops.on_bg_dead) g_ops.on_bg_dead(id, pid, st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 批③后台管理页 / `am kill`:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。
|
||||
* 只置 term_at_ms(tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表),
|
||||
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。
|
||||
* 调用者:bg_page.c 行内「杀」/「一键清除」+ util.c MSG_AMS_KILL(`am kill`,无前台会话时)
|
||||
* ——两个入口同一通道,故页面按钮与面板/CLI kill 行为对称。 */
|
||||
int meow_bg_terminate(const char *app_id)
|
||||
{
|
||||
if (!app_id || !app_id[0]) return MEOW_ERR_INVAL;
|
||||
int i;
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
meow_bg_entry_t *e = &g_bg[i];
|
||||
if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) {
|
||||
if (e->term_at_ms == 0) {
|
||||
bg_log("[bgplay] 用户终止后台App: %s pid=%d(后台管理页/am kill 同通道)",
|
||||
app_id, (int)e->pid);
|
||||
kill(e->pid, SIGTERM);
|
||||
e->term_at_ms = bg_now_ms();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return MEOW_ERR_NOENT;
|
||||
}
|
||||
|
||||
/* ================= 无头后台服务 ================= */
|
||||
|
||||
static meow_svc_rt_t *svc_find_mut(const char *app_id, const char *svc)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
|
||||
if (g_svc[i].used && strcmp(g_svc[i].app_id, app_id) == 0 &&
|
||||
strcmp(g_svc[i].svc, svc) == 0)
|
||||
return &g_svc[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const meow_svc_rt_t *meow_svc_find(const char *app_id, const char *svc)
|
||||
{
|
||||
if (!app_id || !svc) return NULL;
|
||||
return svc_find_mut(app_id, svc);
|
||||
}
|
||||
|
||||
/* spawn 服务子进程:stdio→/dev/null,env={PATH,HOME,MEOW_SERVICE[,MEOW_LOG_DIR/MEOW_LOG_APP]}。
|
||||
* 父进程返回 pid;子进程不返回(execve 或 _exit(127))。 */
|
||||
static pid_t svc_spawn(meow_svc_rt_t *s)
|
||||
{
|
||||
char env_service[32];
|
||||
char env_logdir[160];
|
||||
char env_logapp[64];
|
||||
char *envp[6];
|
||||
int n = 0;
|
||||
pid_t pid;
|
||||
|
||||
envp[n++] = (char *)"PATH=/usr/bin:/bin:/usr/sbin:/sbin";
|
||||
envp[n++] = (char *)"HOME=/root";
|
||||
snprintf(env_service, sizeof(env_service), "MEOW_SERVICE=%s", s->svc);
|
||||
envp[n++] = env_service;
|
||||
if (g_ops.log_dir && g_ops.log_dir[0]) {
|
||||
snprintf(env_logdir, sizeof(env_logdir), "MEOW_LOG_DIR=%s", g_ops.log_dir);
|
||||
envp[n++] = env_logdir;
|
||||
snprintf(env_logapp, sizeof(env_logapp), "MEOW_LOG_APP=%s", s->app_id);
|
||||
envp[n++] = env_logapp;
|
||||
}
|
||||
envp[n] = NULL;
|
||||
|
||||
pid = fork();
|
||||
if (pid != 0) return pid; /* 父:pid 或 -1 */
|
||||
|
||||
/* 子进程:脱离终端会话,stdio 全部重定向 /dev/null(无头服务) */
|
||||
setsid();
|
||||
{
|
||||
int dn = open("/dev/null", O_RDWR);
|
||||
if (dn >= 0) {
|
||||
dup2(dn, STDIN_FILENO);
|
||||
dup2(dn, STDOUT_FILENO);
|
||||
dup2(dn, STDERR_FILENO);
|
||||
if (dn > STDERR_FILENO) close(dn);
|
||||
}
|
||||
}
|
||||
{
|
||||
char *argv[2];
|
||||
|
||||
argv[0] = s->exec;
|
||||
argv[1] = NULL;
|
||||
execve(s->exec, argv, envp);
|
||||
}
|
||||
_exit(127); /* exec 失败:父进程 tick 回收后按退避/熔断处理 */
|
||||
}
|
||||
|
||||
int meow_svc_register(const char *app_id, const char *svc, const char *exec,
|
||||
int restart, int max_restart, int window_ms,
|
||||
int on_card, int reg_fd)
|
||||
{
|
||||
meow_svc_rt_t *s;
|
||||
pid_t pid;
|
||||
int i;
|
||||
|
||||
if (!app_id || !app_id[0] || !svc || !svc[0] || !exec || !exec[0])
|
||||
return MEOW_ERR_INVAL;
|
||||
if (access(exec, X_OK) != 0) return MEOW_ERR_NOENT;
|
||||
|
||||
s = svc_find_mut(app_id, svc);
|
||||
if (s) {
|
||||
if (s->pid > 0) return MEOW_ERR_EXISTS;
|
||||
if (!s->circuit) return MEOW_ERR_EXISTS; /* 退避排程中也视为在册 */
|
||||
/* 熔断槽重注册:复位熔断,重新武装 */
|
||||
bg_log("[bgplay] 服务熔断复位重注册: %s/%s", app_id, svc);
|
||||
s->circuit = 0;
|
||||
s->stopping = 0;
|
||||
s->restart_count = 0;
|
||||
s->window_start_ms = bg_now_ms();
|
||||
s->next_retry_ms = 0;
|
||||
s->reg_fd = reg_fd;
|
||||
s->on_card = on_card;
|
||||
} else {
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
|
||||
if (!g_svc[i].used) break;
|
||||
if (i >= MEOW_SVC_RT_MAX) return MEOW_ERR_NOMEM;
|
||||
s = &g_svc[i];
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->used = 1;
|
||||
snprintf(s->app_id, sizeof(s->app_id), "%s", app_id);
|
||||
snprintf(s->svc, sizeof(s->svc), "%s", svc);
|
||||
snprintf(s->exec, sizeof(s->exec), "%s", exec);
|
||||
s->restart = restart;
|
||||
s->max_restart = max_restart;
|
||||
s->window_ms = window_ms;
|
||||
s->on_card = on_card;
|
||||
s->reg_fd = reg_fd;
|
||||
s->window_start_ms = bg_now_ms();
|
||||
}
|
||||
|
||||
pid = svc_spawn(s);
|
||||
if (pid < 0) {
|
||||
if (s->pid == 0 && s->restart_count == 0 && !s->circuit)
|
||||
memset(s, 0, sizeof(*s)); /* 新槽首次 fork 即败:不留尸槽 */
|
||||
return MEOW_ERR_IO;
|
||||
}
|
||||
s->pid = pid;
|
||||
s->term_at_ms = 0;
|
||||
bg_log("[bgplay] 服务拉起: %s/%s pid=%d restart=%d max=%d",
|
||||
app_id, svc, (int)pid, restart, max_restart);
|
||||
return (int)pid;
|
||||
}
|
||||
|
||||
int meow_svc_stop(const char *app_id, const char *svc)
|
||||
{
|
||||
meow_svc_rt_t *s;
|
||||
|
||||
if (!app_id || !svc) return MEOW_ERR_INVAL;
|
||||
s = svc_find_mut(app_id, svc);
|
||||
if (!s) return MEOW_ERR_NOENT;
|
||||
bg_log("[bgplay] 服务停止: %s/%s pid=%d", app_id, svc, (int)s->pid);
|
||||
if (s->pid > 0) {
|
||||
s->stopping = 1;
|
||||
kill(s->pid, SIGTERM);
|
||||
s->term_at_ms = bg_now_ms();
|
||||
} else {
|
||||
/* 未在跑(退避中/熔断):直接清槽 */
|
||||
if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc);
|
||||
memset(s, 0, sizeof(*s));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 服务死亡后的退避/熔断判定。返回 1=已排程重启;0=槽已清或熔断。 */
|
||||
static int svc_on_exit(meow_svc_rt_t *s, int st)
|
||||
{
|
||||
uint64_t now = bg_now_ms();
|
||||
|
||||
bg_log("[bgplay] 服务退出: %s/%s pid=%d status=0x%x stopping=%d",
|
||||
s->app_id, s->svc, (int)s->pid, st, s->stopping);
|
||||
s->pid = 0;
|
||||
|
||||
if (s->stopping || !s->restart) {
|
||||
if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc);
|
||||
memset(s, 0, sizeof(*s));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 滑窗:超窗复位计数 */
|
||||
if (s->window_ms > 0 && now - s->window_start_ms >= (uint64_t)s->window_ms) {
|
||||
s->restart_count = 0;
|
||||
s->window_start_ms = now;
|
||||
}
|
||||
s->restart_count++;
|
||||
|
||||
if (s->max_restart > 0 && s->restart_count > s->max_restart) {
|
||||
/* 熔断:清通知 + 广播 MSG_SYS{SVC_DEAD},槽保留待重注册复位 */
|
||||
m_sys_t m;
|
||||
|
||||
s->circuit = 1;
|
||||
s->next_retry_ms = 0;
|
||||
bg_log("[bgplay] 服务熔断: %s/%s 窗口内重启 %d 次超 max=%d",
|
||||
s->app_id, s->svc, s->restart_count, s->max_restart);
|
||||
if (g_ops.notif_clear_app)
|
||||
g_ops.notif_clear_app(s->app_id);
|
||||
if (g_ops.send_frame && s->reg_fd >= 0) {
|
||||
memset(&m, 0, sizeof(m));
|
||||
m.event = MEOW_SYS_SVC_DEAD;
|
||||
snprintf(m.app_id, sizeof(m.app_id), "%s", s->app_id);
|
||||
snprintf(m.svc, sizeof(m.svc), "%s", s->svc);
|
||||
g_ops.send_frame(s->reg_fd, MSG_SYS, 0, &m, sizeof(m));
|
||||
}
|
||||
if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
{
|
||||
int step = s->restart_count - 1;
|
||||
uint32_t dly;
|
||||
|
||||
if (step >= BACKOFF_STEPS) step = BACKOFF_STEPS - 1;
|
||||
dly = k_backoff[step];
|
||||
s->next_retry_ms = now + dly;
|
||||
bg_log("[bgplay] 服务退避重启: %s/%s 第 %d 次,%ums 后拉起",
|
||||
s->app_id, s->svc, s->restart_count, (unsigned)dly);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void meow_svc_tick(void)
|
||||
{
|
||||
uint64_t now = bg_now_ms();
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++) {
|
||||
meow_svc_rt_t *s = &g_svc[i];
|
||||
|
||||
if (!s->used) continue;
|
||||
|
||||
/* 卡拔例外:exec 在卡上 → SIGTERM + stopping=1(不重启防空转死循环) */
|
||||
if (s->on_card && s->pid > 0 && !bg_card_present() && !s->stopping) {
|
||||
bg_log("[bgplay] 卡拔出,终止卡上服务: %s/%s pid=%d",
|
||||
s->app_id, s->svc, (int)s->pid);
|
||||
s->stopping = 1;
|
||||
kill(s->pid, SIGTERM);
|
||||
s->term_at_ms = now;
|
||||
}
|
||||
/* SIGTERM 宽限超时 → SIGKILL */
|
||||
if (s->stopping && s->pid > 0 && s->term_at_ms &&
|
||||
now - s->term_at_ms >= TERM_GRACE_MS) {
|
||||
kill(s->pid, SIGKILL);
|
||||
s->term_at_ms = now;
|
||||
}
|
||||
|
||||
if (s->pid > 0) {
|
||||
int st;
|
||||
pid_t r = waitpid(s->pid, &st, WNOHANG);
|
||||
|
||||
if (r == s->pid) svc_on_exit(s, st);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 退避到点 → 重新拉起 */
|
||||
if (!s->circuit && !s->stopping && s->restart &&
|
||||
s->next_retry_ms && now >= s->next_retry_ms) {
|
||||
pid_t pid = svc_spawn(s);
|
||||
|
||||
s->next_retry_ms = 0;
|
||||
if (pid > 0) {
|
||||
s->pid = pid;
|
||||
bg_log("[bgplay] 服务重启: %s/%s pid=%d(第 %d 次)",
|
||||
s->app_id, s->svc, (int)pid, s->restart_count);
|
||||
} else {
|
||||
/* fork 失败:压到下一档重试 */
|
||||
s->next_retry_ms = now + k_backoff[BACKOFF_STEPS - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 关机前回收 ================= */
|
||||
|
||||
void meow_bgplay_kill_all(void)
|
||||
{
|
||||
uint64_t deadline;
|
||||
int i;
|
||||
|
||||
/* best-effort 广播 SHUTDOWN */
|
||||
if (g_ops.send_frame) {
|
||||
m_sys_t m;
|
||||
|
||||
memset(&m, 0, sizeof(m));
|
||||
m.event = MEOW_SYS_SHUTDOWN;
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
|
||||
if (g_svc[i].used && g_svc[i].reg_fd >= 0)
|
||||
g_ops.send_frame(g_svc[i].reg_fd, MSG_SYS, 0, &m, sizeof(m));
|
||||
}
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (g_bg[i].used && g_bg[i].pid > 0) kill(g_bg[i].pid, SIGTERM);
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
|
||||
if (g_svc[i].used && g_svc[i].pid > 0) kill(g_svc[i].pid, SIGTERM);
|
||||
|
||||
/* 有界等 1s(轮询 reap,期间 tick 语义不在——直接 waitpid) */
|
||||
deadline = bg_now_ms() + 1000u;
|
||||
while (bg_now_ms() < deadline) {
|
||||
int alive = 0;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
if (!g_bg[i].used) continue;
|
||||
if (waitpid(g_bg[i].pid, NULL, WNOHANG) == g_bg[i].pid)
|
||||
memset(&g_bg[i], 0, sizeof(g_bg[i]));
|
||||
else
|
||||
alive++;
|
||||
}
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++) {
|
||||
if (!g_svc[i].used) continue;
|
||||
if (waitpid(g_svc[i].pid, NULL, WNOHANG) == g_svc[i].pid)
|
||||
memset(&g_svc[i], 0, sizeof(g_svc[i]));
|
||||
else
|
||||
alive++;
|
||||
}
|
||||
if (!alive) break;
|
||||
usleep(50 * 1000);
|
||||
}
|
||||
|
||||
/* 兜底 SIGKILL + 有界 reap(waitpid 阻塞上限由 SIGKILL 必死保证) */
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (g_bg[i].used) {
|
||||
kill(g_bg[i].pid, SIGKILL);
|
||||
waitpid(g_bg[i].pid, NULL, 0);
|
||||
memset(&g_bg[i], 0, sizeof(g_bg[i]));
|
||||
}
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
|
||||
if (g_svc[i].used) {
|
||||
kill(g_svc[i].pid, SIGKILL);
|
||||
waitpid(g_svc[i].pid, NULL, 0);
|
||||
memset(&g_svc[i], 0, sizeof(g_svc[i]));
|
||||
}
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] kill_all 完成(关机/退出回收)");
|
||||
}
|
||||
|
||||
void meow_bgplay_dump(const char *prefix)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (g_bg[i].used)
|
||||
bg_log("%s bg[%d] %s pid=%d on_card=%d", prefix, i,
|
||||
g_bg[i].app_id, (int)g_bg[i].pid, g_bg[i].on_card);
|
||||
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
|
||||
if (g_svc[i].used)
|
||||
bg_log("%s svc[%d] %s/%s pid=%d restart=%d count=%d circuit=%d",
|
||||
prefix, i, g_svc[i].app_id, g_svc[i].svc, (int)g_svc[i].pid,
|
||||
g_svc[i].restart, g_svc[i].restart_count, g_svc[i].circuit);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/* meow_bgplay.h — MeowOS M4:后台播放保活 + 无头后台服务运行时表(FRAMEWORK_DESIGN §9)。
|
||||
*
|
||||
* 定位:
|
||||
* ① 后台播放保活——退出播放中(声明 background_audio=true 且媒体会话 PLAYING)的 App
|
||||
* 不 SIGTERM:进程保留(音频经 audiod socket 天然续播),UI 冻结走 §10.5 的
|
||||
* /tmp/meow_notif_active SDK 自抑机制(present 画进 back_buf 不上屏 + 输入排空)。
|
||||
* **不做字面 SIGSTOP**:SIGSTOP 会冻结同进程播放线程、断供 PCM(§10.5 真机实证的坑)。
|
||||
* 点图标/媒体卡片回前台=take 恢复原进程;停播后再退出=真退(走原 SIGTERM 序列)。
|
||||
* ② 无头后台服务——MSG_REGISTER 注册 app.json `service.<svc>` 声明的服务:launcher
|
||||
* spawn 子进程(无 UI,stdio→/dev/null),死亡按 1/2/4/10s 退避重启,超 max_restart
|
||||
* 熔断(清该 App 通知 + 经注册连接广播 MSG_SYS{SVC_DEAD}),重注册可复位熔断。
|
||||
*
|
||||
* 边界:
|
||||
* - 卡拔出例外:二进制在 TF 卡上的后台 App/服务仍 SIGTERM(防 SIGBUS),不适用保活。
|
||||
* - 崩溃照收:waitpid WNOHANG 回收,异常退出经 on_bg_dead 回调走 crash 上报。
|
||||
* - 关机前回收:meow_bgplay_kill_all() 有界 SIGTERM→SIGKILL→reap。
|
||||
*
|
||||
* 自包含:不依赖 launcher static 符号;日志/时钟/卡检测/通知清理/发帧全经
|
||||
* meow_bgplay_ops 注入(launcher 用 logmsg/sd_device_present/ipc_send;宿主单测用假件)。
|
||||
* 单一写者=launcher 主循环(meow_ipc_process 排空点挂 tick),无锁。可独立单测(test_bgplay.c)。
|
||||
*/
|
||||
#ifndef MEOW_BGPLAY_H
|
||||
#define MEOW_BGPLAY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
#include "meow_ipc.h"
|
||||
|
||||
/* 后台保活 App 上限(同时驻留后台的播放中 App) */
|
||||
#define MEOW_BG_MAX 4
|
||||
/* 批①+宽限:无声明/媒体不活跃 App 入后台的宽限期(默认 30 分钟)。
|
||||
* 语义:转后台(bg_since_ms)后超 MEOW_BG_GRACE_MS 未再启动 → meow_bg_tick 走真退序列
|
||||
* SIGTERM→SIGKILL 回收(摘表 + 通知 amsd)。**声明常驻(keepalive=1)或媒体活跃
|
||||
* (meow_media_app_resident=1)不清**。本批为编译期常量(30*60*1000 = 1800000ms);
|
||||
* 后续可进设置/按 App 可配(待拍板)。 */
|
||||
#define MEOW_BG_GRACE_MS 1800000u
|
||||
/* 无头后台服务运行时槽上限(与 meow_acct.svcs 的 MEOW_MAX_SVCS 对齐) */
|
||||
#define MEOW_SVC_RT_MAX 8
|
||||
|
||||
/* 退避重启档位(ms):1s/2s/4s/10s 封顶 */
|
||||
#define MEOW_SVC_BACKOFF_1 1000u
|
||||
#define MEOW_SVC_BACKOFF_2 2000u
|
||||
#define MEOW_SVC_BACKOFF_4 4000u
|
||||
#define MEOW_SVC_BACKOFF_10 10000u
|
||||
|
||||
/* 后台保活 App 槽。out_fd=其 stdout 管道读端(保活期继续 drain,防 App 写满管道阻塞)。 */
|
||||
typedef struct meow_bg_entry {
|
||||
int used;
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
pid_t pid;
|
||||
int out_fd;
|
||||
int on_card; /* 二进制在 TF 卡上(卡拔需 SIGTERM 防 SIGBUS) */
|
||||
uint64_t term_at_ms; /* 卡拔/宽限超时/槽满回收 SIGTERM 发出时刻(0=未发;SIGKILL 兜底计时) */
|
||||
uint64_t bg_since_ms; /* 批①:转后台时刻(monotonic ms;宽限超时回收/槽满 LRU 掐最久用) */
|
||||
char keepalive; /* 批①+宽限:1=声明常驻(background_keepalive,无限期不清);0=宽限期类
|
||||
* (未声明/background_audio,媒体不活跃时超 bg_grace_ms 回收) */
|
||||
} meow_bg_entry_t;
|
||||
|
||||
/* 无头后台服务运行时槽(静态声明在 meow_acct.svcs,此处为运行态)。 */
|
||||
typedef struct meow_svc_rt {
|
||||
int used;
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
char svc[16];
|
||||
char exec[129];
|
||||
pid_t pid; /* >0 在跑;0=等待退避重启/已熔断 */
|
||||
int reg_fd; /* 注册方 IPC 连接 fd(熔断广播投递用;-1=静态自动拉起) */
|
||||
int restart; /* 1=死亡后退避重启 */
|
||||
int max_restart; /* 熔断阈值(窗口内重启次数上限;0=不熔断) */
|
||||
int window_ms; /* 重启窗口(0=累计不滑窗) */
|
||||
int restart_count; /* 当前窗口内已重启次数 */
|
||||
int circuit; /* 1=已熔断(清通知+广播过,不再拉起;重注册复位) */
|
||||
int stopping; /* 1=主动停止/卡拔终止中(SIGTERM 已发,不重启) */
|
||||
int on_card; /* exec 在 TF 卡上 */
|
||||
uint64_t window_start_ms; /* 滑窗起点 */
|
||||
uint64_t next_retry_ms; /* 退避下次拉起时刻(0=无排程) */
|
||||
uint64_t term_at_ms; /* SIGTERM 发出时刻(0=未发;500ms 未死 SIGKILL) */
|
||||
} meow_svc_rt_t;
|
||||
|
||||
/* 依赖注入。全 NULL 可用(log 静默、now_ms 内部 CLOCK_MONOTONIC、card_present 恒 1、
|
||||
* 通知清理/发帧跳过)。
|
||||
* on_bg_dead:后台保活 App 进程退出(status=waitpid 原始状态;正常/异常由回调方判);
|
||||
* media_resident:该 App 媒体会话是否「活跃可驻留」(launcher 注入 meow_media_app_resident)。
|
||||
* 批①+宽限用:宽限超时回收 / 槽满 LRU 判「媒体活跃不清不回收」;NULL=恒 0(视为不活跃)。
|
||||
* on_svc_dead:服务槽最终消亡(stopping 走完/不重启死亡;熔断不算消亡,槽保留待复位);
|
||||
* notif_clear_app:熔断时清该 App 全部通知,返回清除条数(<0=失败);
|
||||
* send_frame:熔断广播 MSG_SYS(launcher 侧须先查连接存活再发);
|
||||
* log_dir:svc 子进程 MEOW_LOG_DIR 注入(NULL 不注入)。 */
|
||||
typedef struct meow_bgplay_ops {
|
||||
void (*log)(const char *fmt, ...);
|
||||
uint64_t (*now_ms)(void);
|
||||
int (*card_present)(void);
|
||||
void (*on_bg_dead)(const char *app_id, pid_t pid, int status);
|
||||
int (*media_resident)(const char *app_id);
|
||||
void (*on_svc_dead)(const char *app_id, const char *svc);
|
||||
int (*notif_clear_app)(const char *app_id);
|
||||
size_t (*send_frame)(int fd, uint16_t type, uint32_t req_id,
|
||||
const void *payload, uint32_t len);
|
||||
const char *log_dir;
|
||||
} meow_bgplay_ops;
|
||||
|
||||
/* 注入 ops(拷贝);NULL=全清空复位(含两张表)。launcher 在 meow_ipc_init 末尾调一次。 */
|
||||
void meow_bgplay_init(const meow_bgplay_ops *ops);
|
||||
|
||||
/* ---- 后台播放保活判定(纯函数,无副作用)---- */
|
||||
/* 退出时是否转后台保活(批①三参化):
|
||||
* keepalive_declared=1(声明 background_keepalive)→ 恒保活(通用常驻,不依赖媒体会话);
|
||||
* 否则 bg_audio_declared(声明 background_audio)且 media_resident(该 App 媒体会话活跃
|
||||
* =PLAYING/PAUSED/BUFFER,见 meow_media_app_resident)→ 保活。
|
||||
* 未声明/无活跃会话 → 0(走原真退序列)。 */
|
||||
int meow_bg_should_keep(int bg_audio_declared, int media_resident, int keepalive_declared);
|
||||
|
||||
/* ---- 后台保活 App 表 ---- */
|
||||
/* 转后台:登记 app_id/pid/out_fd,keepalive 标记常驻类型(1=background_keepalive 类=无限期;
|
||||
* 0=宽限期类=未声明/background_audio,媒体不活跃时超 bg_grace_ms 回收)。批①+宽限:**全 App**
|
||||
* nav-exit 都经此入表;槽满(MEOW_BG_MAX=4)时 LRU 回收最久未激活(bg_since_ms 最旧)的**未声明**
|
||||
* 槽(不回收声明常驻/媒体活跃);全为声明常驻/媒体活跃 → MEOW_ERR_NOMEM(调用方回落真退,保安全)。
|
||||
* 返回 0=成功;MEOW_ERR_EXISTS=已在后台;MEOW_ERR_NOMEM=满且无可回收。 */
|
||||
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive);
|
||||
/* 回前台:按 app_id 摘除槽(进程归还前台会话)。成功返回 0 且 *out_fd(可 NULL)带回
|
||||
* 管道读端;未在后台返回 -1。 */
|
||||
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid);
|
||||
/* 该 App 是否在后台保活。 */
|
||||
int meow_bg_is_backgrounded(const char *app_id);
|
||||
/* 用户终止后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。调用者:后台管理页行内「杀」/
|
||||
* 「一键清除」+ launcher IPC MSG_AMS_KILL(`am kill`)——两入口同通道,页面与面板/CLI 行为对称。
|
||||
* 置 e->term_at_ms=now 由 meow_bg_tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表。
|
||||
* 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */
|
||||
int meow_bg_terminate(const char *app_id);
|
||||
/* 当前后台保活数。 */
|
||||
int meow_bg_count(void);
|
||||
/* 取第 i 槽(0..MEOW_BG_MAX-1)只读指针;空槽/越界返回 NULL。供 drain out_fd。 */
|
||||
const meow_bg_entry_t *meow_bg_at(int i);
|
||||
/* 表变化计数(add/take/回收 递增);菜单 1s tick 比对刷新「·后台」标注用。 */
|
||||
uint32_t meow_bgplay_change_seq(void);
|
||||
|
||||
/* 周期调(主循环 ~10ms 排空点):waitpid WNOHANG 回收死亡后台 App(→on_bg_dead 清槽)。
|
||||
* 终止触发(SIGTERM,TERM_GRACE_MS 后 SIGKILL):
|
||||
* - 卡拔例外:on_card 槽在卡不在时终止(防 SIGBUS);
|
||||
* - 批①+宽限 超时回收:未声明(keepalive=0)且媒体不活跃(media_resident=0)的槽,转后台超
|
||||
* MEOW_BG_GRACE_MS 未再启动 → 回收(走真退序列 + 摘表 + 通知 amsd)。声明常驻/媒体活跃不清。 */
|
||||
void meow_bg_tick(void);
|
||||
|
||||
/* ---- 无头后台服务 ---- */
|
||||
/* 注册/拉起服务。exec 须可执行(先 access(X_OK),拒绝返回 MEOW_ERR_NOENT 不污染槽)。
|
||||
* 返回 pid(>0)=已拉起;MEOW_ERR_EXISTS=同名服务在跑;MEOW_ERR_NOENT=exec 不可执行;
|
||||
* MEOW_ERR_NOMEM=槽满;MEOW_ERR_IO=fork 失败。同 svc 已熔断槽重注册=复位熔断重新拉起。 */
|
||||
int meow_svc_register(const char *app_id, const char *svc, const char *exec,
|
||||
int restart, int max_restart, int window_ms,
|
||||
int on_card, int reg_fd);
|
||||
/* 请求停止服务:SIGTERM,标记 stopping(不触发重启)。返回 0=已受理;MEOW_ERR_NOENT=无此服务。 */
|
||||
int meow_svc_stop(const char *app_id, const char *svc);
|
||||
/* 查服务槽(只读);无返回 NULL。 */
|
||||
const meow_svc_rt_t *meow_svc_find(const char *app_id, const char *svc);
|
||||
/* 周期调:回收死亡子进程;按 restart/退避档 1/2/4/10s 重启;滑窗复位计数;超
|
||||
* max_restart 熔断(notif_clear_app + send_frame MSG_SYS{SVC_DEAD} + circuit=1);
|
||||
* 卡拔:on_card 槽 SIGTERM + stopping=1(exec 随卡消失,不重启防空转)。 */
|
||||
void meow_svc_tick(void);
|
||||
|
||||
/* 关机前回收:向各服务注册连接广播 MSG_SYS{SHUTDOWN}(best-effort),随后全部
|
||||
* bg/svc 子进程 SIGTERM→有界等 1s→SIGKILL→有界 reap。调用方负责 unlink NOTIF_ACTIVE。 */
|
||||
void meow_bgplay_kill_all(void);
|
||||
|
||||
/* 调试打印(经 ops->log,每槽一行)。 */
|
||||
void meow_bgplay_dump(const char *prefix);
|
||||
|
||||
#endif /* MEOW_BGPLAY_H */
|
||||
@@ -0,0 +1,117 @@
|
||||
/* meow_dep.c — app.json 二进制依赖声明(depends=<name>[:<mod>][,...])解析与启动前检查。
|
||||
*
|
||||
* 背景(用户定案):App 某些复杂功能若依赖现成 Linux 二进制程序(板载 /usr/bin 或卡上
|
||||
* MeowOS/opt/bin)能效更好,故在 app.json 声明二进制依赖;launcher 在启动 App 前检查
|
||||
* required 依赖是否都存在,缺 required 则不启动(toast「缺依赖: <name>」+ 日志);
|
||||
* optional 缺失仅 WARN 放行。检查只在启动时做(不做周期复查)。
|
||||
*
|
||||
* 与 meow_parse_app_json 的 line 解析同风格:取值串后逐项切分(不引 JSON 解析器)。
|
||||
* 本文件独立成 TU,由 build_launcher.sh 编入;宿主测试用「假根(env 覆写搜索目录)」验证。 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include "meow_launcher_internal.h"
|
||||
|
||||
|
||||
/* 最近一次因缺 required 依赖而阻断启动的首个缺失名(供 menu toast「缺依赖: <name>」)。
|
||||
* 每次 launch_app_chain 入口清空(meow_dep_clear_reason);无缺失则 ""。 */
|
||||
static char g_dep_block_reason[32] = "";
|
||||
|
||||
void meow_dep_clear_reason(void)
|
||||
{
|
||||
g_dep_block_reason[0] = 0;
|
||||
}
|
||||
|
||||
const char *meow_dep_block_reason(void)
|
||||
{
|
||||
return g_dep_block_reason;
|
||||
}
|
||||
|
||||
|
||||
/* 二进制搜索目录(真机默认):板载 /usr/bin ← 卡上 <卡根>/MeowOS/opt/bin。
|
||||
* 宿主测试经 MEOW_DEP_BIN / MEOW_DEP_ROOT 覆写为假根(与 nyai MEOWROOT 同思路)。 */
|
||||
static const char *dep_bin_root(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_DEP_BIN");
|
||||
return (e && e[0]) ? e : "/usr/bin";
|
||||
}
|
||||
static const char *dep_card_root(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_DEP_ROOT");
|
||||
return (e && e[0]) ? e : "/usr/data/mnt/sd_0";
|
||||
}
|
||||
|
||||
/* 解析 depends 值串 → 逐个填充 deps[](最多 max 项,超量丢弃;空项/纯空白丢弃)。
|
||||
* <name>[:<mod>];<mod> 缺省 required;:opt/:req 等价 optional/required;
|
||||
* 未知修饰符按 required 处理并 logmsg WARN。返回写入项数。 */
|
||||
int meow_parse_depends(const char *val, struct meow_dep *deps, int max)
|
||||
{
|
||||
if (!val || !deps || max <= 0) return 0;
|
||||
int n = 0;
|
||||
const char *p = val;
|
||||
while (*p && n < max) {
|
||||
while (*p && (*p == ' ' || *p == '\t' || *p == ',')) p++;
|
||||
if (!*p || *p == '"') break; /* 结束(含 JSON 值尾引号) */
|
||||
const char *start = p; /* 名称起点 */
|
||||
while (*p && *p != ':' && *p != ',' && *p != '"' && *p != ' ' && *p != '\t') p++;
|
||||
int nl = (int)(p - start);
|
||||
if (nl > 0) {
|
||||
if (nl > 31) nl = 31;
|
||||
char nm[32];
|
||||
memcpy(nm, start, (size_t)nl);
|
||||
nm[nl] = 0;
|
||||
int req = 1; /* 默认 required */
|
||||
if (*p == ':') { /* 修饰符 */
|
||||
const char *ms = p + 1;
|
||||
while (*ms == ' ' || *ms == '\t') ms++;
|
||||
const char *m = ms;
|
||||
while (*m && *m != ',' && *m != '"' && *m != ' ' && *m != '\t') m++;
|
||||
int ml = (int)(m - ms);
|
||||
if (ml == 8 && !strncmp(ms, "optional", 8)) req = 0;
|
||||
else if (ml == 3 && !strncmp(ms, "opt", 3)) req = 0;
|
||||
else if (ml == 8 && !strncmp(ms, "required", 8)) req = 1;
|
||||
else if (ml == 3 && !strncmp(ms, "req", 3)) req = 1;
|
||||
else logmsg("WARN: depends '%s' unknown mod '%.*s' (treated required)", nm, ml, ms);
|
||||
p = m;
|
||||
}
|
||||
snprintf(deps[n].name, sizeof deps[n].name, "%s", nm);
|
||||
deps[n].req = (char)(req ? '1' : '0');
|
||||
n++;
|
||||
}
|
||||
if (*p == ',') p++; /* 消费分隔符 */
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
/* 依赖二进制存在性:按序 <board>/<name> → <card>/MeowOS/opt/bin/<name>(X_OK)。返回 1=存在。 */
|
||||
static int dep_bin_exists(const char *name)
|
||||
{
|
||||
if (!name || !name[0]) return 0;
|
||||
char p[320];
|
||||
snprintf(p, sizeof p, "%s/%s", dep_bin_root(), name);
|
||||
if (access(p, X_OK) == 0) return 1;
|
||||
snprintf(p, sizeof p, "%s/MeowOS/opt/bin/%s", dep_card_root(), name);
|
||||
if (access(p, X_OK) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* 启动前检查依赖。returns: 0=全部满足 / 1=缺 required(missing 回填首个缺失名,可 NULL)。 */
|
||||
int meow_check_app_depends(const struct app *a, char *missing, int mlen)
|
||||
{
|
||||
if (!a) return 0;
|
||||
for (int i = 0; i < a->ndep; i++) {
|
||||
if (dep_bin_exists(a->deps[i].name)) continue;
|
||||
if (a->deps[i].req == '1') {
|
||||
logmsg("launch blocked: missing required dep '%s'", a->deps[i].name);
|
||||
if (missing && mlen > 0) snprintf(missing, (size_t)mlen, "%s", a->deps[i].name);
|
||||
snprintf(g_dep_block_reason, sizeof g_dep_block_reason, "%s", a->deps[i].name);
|
||||
return 1;
|
||||
}
|
||||
logmsg("WARN: optional dep '%s' missing, continuing", a->deps[i].name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -13,10 +13,11 @@
|
||||
* uint32_t len; = payload 字节数
|
||||
* };
|
||||
*
|
||||
* 本阶段(D · M0+M1+M2+M3+M4 前置)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT +
|
||||
* 本阶段(D · M0+M1+M2+M3+M4)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT +
|
||||
* MSG_ACT_FOCUS/BACK + MSG_NOTIFY/PROGRESS/QUERY/DISMISS* + MSG_NAV(导航栈上报/退出)
|
||||
* + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)。
|
||||
* picker(0x0070/0x0071)仅预留协议位,本批不实现。
|
||||
* + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)
|
||||
* + MSG_REGISTER/SERVICE_OK/SERVICE_STOP/MSG_SYS(后台服务注册/停止/熔断事件,M4)。
|
||||
* picker(0x0070/0x0071)已落地(二期)。
|
||||
*/
|
||||
#ifndef MEOW_IPC_H
|
||||
#define MEOW_IPC_H
|
||||
@@ -34,6 +35,12 @@
|
||||
/* 列表编码上限(count 个 NUL 字符串时,distinct 工作缓冲上限) */
|
||||
#define MEOW_IPC_CARGO 1024u /* 守护进程入队拷贝上限(可小于 MEOW_IPC_MAX) */
|
||||
|
||||
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE.md,③混合)----
|
||||
* meow_svc_perf(C:/proc/meminfo Cached 监控降载)把「是否暂停下载/预取」写入此共享状态文件
|
||||
* (一行式 key=value;App/SDK meow_cachegov 只读,系统级单点,不重复起线程)。
|
||||
* 路径可被 env MEOW_CACHEGOV_C_FILE 覆盖(宿主测试隔离)。 */
|
||||
#define MEOW_CACHEGOV_C_FILE "/tmp/meow_cachegov_c.state"
|
||||
|
||||
/* ---- 消息类型(FRAMEWORK_DESIGN §1 表)---- */
|
||||
enum {
|
||||
MSG_NONE = 0x0000,
|
||||
@@ -74,10 +81,47 @@ enum {
|
||||
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
|
||||
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
||||
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
||||
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
|
||||
/* ---- 系统翻页信号(READER_UI_DESIGN §E,用户 2026-09-04 拍板):0x0080-0x0081 ----
|
||||
* 语义:launcher 按键层(meow_launcherd/keys.c)在「当前台 App 为其 app.json 声明
|
||||
* volume_page 的前台应用」时,把物理 VOL_UP/VOL_DOWN 转换成系统翻页信号下发(否则照旧
|
||||
* 调音量——音量基本行为/插孔消抖/音量反转绝不破坏)。App 侧(如 reader_app/app/reader_ipc.c)
|
||||
* 持持久连接收本消息 → 上一页/下一页。方向由消息号本身决定(UP=上一页,DOWN=下一页);
|
||||
* 无负载(len=0)。属系统级能力,未来其它 App 可声明复用。 */
|
||||
MSG_KEY_PAGEUP = 0x0080, /* ↓ 系统级:上一页(VOL_UP 语义,仅前台声明 app 收) */
|
||||
MSG_KEY_PAGEDOWN = 0x0081, /* ↓ 系统级:下一页(VOL_DOWN 语义,仅前台声明 app 收) */
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
||||
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
||||
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义) */
|
||||
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义):后台保活表命中→就地真退序列;
|
||||
* 否则有前台会话→置 pending 交会话消费;都没有→ACK -2(MEOW_ERR_NOENT) */
|
||||
/* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ----
|
||||
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
|
||||
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
|
||||
MSG_AUDIO_OPEN = 0x00C0, /* ↑ 开流(m_audio_open;client 原生 rate/ch/fmt/flags) */
|
||||
MSG_AUDIO_OPEN_ACK = 0x00C1, /* ↓ 授权(stream_id + grant_rate/ch/fmt + err;grant=客户端应产出的格式) */
|
||||
MSG_AUDIO_WRITE = 0x00C2, /* ↑ PCM 字节流(按 grant 格式交织;单帧 ≤4096B,多帧连续发,无回执) */
|
||||
MSG_AUDIO_PAUSE = 0x00C3, /* ↑ 暂停(u32 stream_id) */
|
||||
MSG_AUDIO_RESUME = 0x00C4, /* ↑ 恢复(u32 stream_id) */
|
||||
MSG_AUDIO_STOP = 0x00C5, /* ↑ 停止并关流(u32 stream_id) */
|
||||
MSG_AUDIO_PREEMPTED= 0x00C6, /* ↓ 被抢占通知(m_audio_preempted;旧流被后开流赶走,App 收 UI 提示) */
|
||||
MSG_AUDIO_QUERY = 0x00C7, /* ↑ 状态快照查询(空负载) */
|
||||
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
|
||||
MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */
|
||||
MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */
|
||||
/* P2 蓝牙(BLUETOOTH_FULL_OVERHAUL_PLAN §五):输出目标切换。audiod 读 settings.conf
|
||||
* `audio.output_bt`(0/1) 或收本消息改写目标(0=OUT_DAC 1=OUT_BT),重配 PCM。
|
||||
* 与物理路由 numid=9 解耦:蓝牙非物理端口,不写 numid=9、不触发 FPGA 烧录。 */
|
||||
MSG_AUDIO_OUTPUT = 0x00CB, /* ↑ 输出目标(u32 target:0=DAC 1=BT;ui/CLI 下发) */
|
||||
/* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ----
|
||||
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
|
||||
* 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power:
|
||||
* QUERY/HISTORY/EVENTS=读级任意调用者;POLICY_SET=动作级(期 3 落地时接 SO_PEERCRED 同源代码)。 */
|
||||
MSG_PM_QUERY = 0x00D0, /* ↑ 功耗聚合查询(m_pm_query_t{subset}:PM_SUBSET_* 位;0=全部) */
|
||||
MSG_PM_RESULT = 0x00D1, /* ↓ 聚合 JSON(电池深加工/功耗估算/分解/策略,形状对齐 §3/§4.1) */
|
||||
/* 0x00D2 MSG_PM_POLICY_GET / 0x00D3 MSG_PM_POLICY_SET —— 期 3(profile/低电阈值读改,SET 动作级鉴权)
|
||||
* 0x00D4 MSG_PM_HISTORY(u32 hours)/ 0x00D5 MSG_PM_HISTORY_RESULT(统计 JSON)—— 期 2
|
||||
* 0x00D6 MSG_PM_EVENTS / 0x00D7 MSG_PM_EVENTS_RESULT(异常耗电事件清单)—— 期 2 */
|
||||
MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */
|
||||
};
|
||||
|
||||
@@ -134,6 +178,38 @@ typedef struct m_err {
|
||||
char msg[64];
|
||||
} m_err_t;
|
||||
|
||||
/* ================= 后台服务注册(M4,FRAMEWORK_DESIGN §9) = */
|
||||
|
||||
/* MSG_REGISTER ↑:无头后台服务注册。调用方(App 前台进程)须:
|
||||
* ① app.json 声明 `background=true` 或 HELLO flags 含 MF_BACKGROUND(否则 ERR_PERM);
|
||||
* ② app.json `service.<svc>=mode:restart:max:window_ms` 静态声明该服务(否则 ERR_NOENT);
|
||||
* ③ 注册后保持 IPC 连接不断(launcher 以连接存活做熔断广播的投递判定)。
|
||||
* svc=NUL 结尾服务名(≤15 字符);flags 按位:MF_BACKGROUND(申请后台常驻)。 */
|
||||
typedef struct m_register {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
char svc[16]; /* 服务名(app.json service.<svc>) */
|
||||
uint32_t flags; /* MF_BACKGROUND 等 */
|
||||
} m_register_t; /* 52 B */
|
||||
|
||||
/* MSG_SERVICE_OK ↓:用 m_ack_t(code=0;status=服务子进程 pid)。 */
|
||||
|
||||
/* MSG_SERVICE_STOP ↑:请求停止自己已注册的服务(SIGTERM,不触发重启)。 */
|
||||
typedef struct m_service_stop {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
char svc[16];
|
||||
} m_service_stop_t; /* 48 B */
|
||||
|
||||
/* MSG_SYS ↓:系统事件广播(守护关闭/服务死亡/播放器状态)。
|
||||
* event:MEOW_SYS_*;app_id/svc 为事件主体(SHUTDOWN 时可空)。 */
|
||||
enum { MEOW_SYS_SHUTDOWN = 1, /* 守护进程即将关闭(关机/重启前回收) */
|
||||
MEOW_SYS_SVC_DEAD = 2, /* 后台服务熔断(超 max_restart 不再拉起) */
|
||||
MEOW_SYS_PLAYER_STATE = 3 }; /* 播放器状态变更(预留) */
|
||||
typedef struct m_sys {
|
||||
uint32_t event; /* MEOW_SYS_* */
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
char svc[16]; /* SVC_DEAD 时为熔断服务名 */
|
||||
} m_sys_t; /* 52 B */
|
||||
|
||||
/* MSG_ACT_QUERY ↑:解析某 app 的 activity 链路 */
|
||||
typedef struct m_act_query {
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
@@ -201,15 +277,40 @@ typedef struct m_meta {
|
||||
} m_meta_t; /* 128+128+128+16+192+192+32 = 828 B */
|
||||
|
||||
/* MSG_PLAYCTL ↓(launcher→App)/↑(控制器→launcher 转发到最近活跃媒体会话):播放控制。
|
||||
* cmd:1=播放 2=暂停 3=上一首 4=下一首 5=seek;其余保留。
|
||||
* cmd:1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停切换(toggle,源侧按当前态翻转);其余保留。
|
||||
* seek_ms:cmd=5 时目标位置(ms);其余置 0。
|
||||
* 语义:通知中心卡片/播放页按钮、物理媒体键 → meow_media_playctl(cmd,seek_ms) 经
|
||||
* 「最近活跃媒体会话」的连接 fd 下发;宿主验证经 meow_ipc playctl 发到 launcher 转发。 */
|
||||
* 「最近活跃媒体会话」的连接 fd 下发;宿主验证经 meow_ipc playctl 发到 launcher 转发。
|
||||
* 物理播放/暂停键(164)在 SDK 侧恒映射 cmd=6(不再本地判 state:走自有 IPC 线程的源
|
||||
* 不经 SDK meta 上报,SDK 侧 state 可能恒 0);launcher 透传 cmd 不改写。 */
|
||||
typedef struct m_playctl {
|
||||
uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek */
|
||||
uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停toggle */
|
||||
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
|
||||
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
|
||||
|
||||
/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)----
|
||||
* App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页
|
||||
* 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限,
|
||||
* 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。
|
||||
* - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用)
|
||||
* - kind = 0 原文 lrc / 1 翻译 tlrc
|
||||
* - total= 该 kind 总字节数(不含 NUL;0=清空该 kind)
|
||||
* - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送)
|
||||
* 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲,
|
||||
* 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */
|
||||
#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */
|
||||
#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */
|
||||
#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */
|
||||
typedef struct m_lyric {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */
|
||||
uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */
|
||||
uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */
|
||||
uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */
|
||||
uint16_t off; /* 本片起始偏移(≤ total;首片=0) */
|
||||
uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */
|
||||
char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */
|
||||
} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */
|
||||
|
||||
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
|
||||
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
|
||||
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
|
||||
@@ -325,6 +426,28 @@ typedef struct m_power_status {
|
||||
char msg[96]; /* 结果/状态文本 */
|
||||
} m_power_status_t;
|
||||
|
||||
/* ---- meow_powermgr 功耗管理(POWER_MANAGER_DESIGN §6.1;服务端 meow_powermgr)---- */
|
||||
|
||||
/* MSG_PM_QUERY 子集位(0=全部) */
|
||||
#define PM_SUBSET_BATTERY (1u << 0) /* 电池深加工:state/pct/mv/health/续航估算 */
|
||||
#define PM_SUBSET_ESTIMATE (1u << 1) /* 当前功耗估算:est_mw + confidence(estimated/derived) */
|
||||
#define PM_SUBSET_BREAKDOWN (1u << 2) /* 功耗分解:各子系统分量(§3.2 经验模型,系数待标定) */
|
||||
#define PM_SUBSET_POLICY (1u << 3) /* 策略档位快照(期 3;期 1 恒 null) */
|
||||
|
||||
/* MSG_PM_QUERY ↑:功耗聚合查询 */
|
||||
typedef struct m_pm_query {
|
||||
uint32_t subset; /* 0=全部;PM_SUBSET_* 组合 */
|
||||
} m_pm_query_t; /* 4 B */
|
||||
|
||||
/* MSG_PM_RESULT ↓:聚合 JSON(payload 为 NUL 结尾 JSON 文本,同 m_perf_result_t 口径) */
|
||||
typedef struct m_pm_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
|
||||
uint32_t subset; /* 回显请求 subset */
|
||||
uint16_t json_len; /* json 有效字节(不含 NUL) */
|
||||
uint16_t pad;
|
||||
char json[MEOW_IPC_MAX - 12]; /* 聚合 JSON(§3/§4.1 形状) */
|
||||
} m_pm_result_t;
|
||||
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2)---- */
|
||||
|
||||
/* MSG_AMS_START →:请求 launcher 拉起某 App。app_id=包名(权威);activity 空=裸启动
|
||||
@@ -341,7 +464,9 @@ typedef struct m_ams_start {
|
||||
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
|
||||
} m_ams_start_t; /* 160 B */
|
||||
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。 */
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。
|
||||
* 后台保活 App:launcher 就地走 meow_bg_terminate 真退序列(与后台管理页「杀」同一通道);
|
||||
* 前台 App:置 pending 交前台会话消费;两者皆非 → ACK code=(uint32_t)MEOW_ERR_NOENT(-2)。 */
|
||||
typedef struct m_ams_kill {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
} m_ams_kill_t; /* 32 B */
|
||||
@@ -369,4 +494,239 @@ typedef struct m_picker_result {
|
||||
char value[256]; /* 返回项(路径/文本);空=cancel */
|
||||
} m_picker_result_t; /* 320 B */
|
||||
|
||||
/* ================= audiod 独占音频服务(AUDIOD_DESIGN.md) = */
|
||||
|
||||
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE
|
||||
* 5=AUDIO_FMT_DOP(DoP DSD 透明容器格式)。v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32
|
||||
* 处理;FLOAT v1 拒绝。AUDIO_FMT_DOP 为 **独立格式值**(A3+DSD 定案:避免 S24 名义(3B)/实际(4B)歧义),
|
||||
* 仅在 MSG_AUDIO_OPEN.flags 带 AUDIO_FLAG_DSD 时合法;audiod 只做 DoP 容器透传(不引 dsd2pcm)。 */
|
||||
enum {
|
||||
AUDIO_FMT_S16 = 1,
|
||||
AUDIO_FMT_S24 = 2,
|
||||
AUDIO_FMT_S32 = 3,
|
||||
AUDIO_FMT_FLOAT = 4,
|
||||
AUDIO_FMT_DOP = 5 /* DoP DSD(32-bit 容器:bits[31:16]=16bit DSD 数据,bits[15:8]=标记 0xFA/0x05,bits[7:0]=0),
|
||||
容器率 = DSD 位率/16(DSD64→176400 / DSD128→352800 / DSD256→705600) */
|
||||
};
|
||||
|
||||
/* MSG_AUDIO_OPEN.flags 位(预留曾恒 0;A3+DSD 起使用) */
|
||||
#define AUDIO_FLAG_DSD 0x00000001u /* fmt==AUDIO_FMT_DOP:本流为 DoP DSD(App 侧已解码),audiod 只透传 */
|
||||
|
||||
/* MSG_AUDIO_OPEN ↑:请求开流(App 原生声源参数)。grant_*(ACK)回 audiod 实际授权给客户端的
|
||||
* 产出格式(率/声道/位深)——客户端必须按 grant 格式产出 PCM(SDK meow_ui_audio 负责归一:
|
||||
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。fmt==AUDIO_FMT_DOP
|
||||
* 须带 AUDIO_FLAG_DSD(否则 ERR_INVAL);DSD 经 DoP 透传,audiod 不引 dsd2pcm。 */
|
||||
typedef struct m_audio_open {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
uint32_t rate; /* 原生采样率(应用源,如 44100/48000;DSD 容器率) */
|
||||
uint16_t ch; /* 原生声道数(1=mono 2=stereo) */
|
||||
uint16_t fmt; /* 原生格式(AUDIO_FMT_*;DSD 用 AUDIO_FMT_DOP) */
|
||||
uint32_t flags; /* AUDIO_FLAG_DSD 等 */
|
||||
} m_audio_open_t; /* 32+4+2+2+4 = 44 B */
|
||||
|
||||
/* MSG_AUDIO_OPEN_ACK ↓:授权结果。err=MEOW_OK 才有效(stream_id>0 且 grant_* 为实际授权值);
|
||||
* err<0 时 stream_id=0、grant_* 视为无效(客户端 open 返回负错误码)。 */
|
||||
typedef struct m_audio_open_ack {
|
||||
uint32_t stream_id; /* 授权流 id(>0) */
|
||||
uint32_t grant_rate; /* 客户端应产出的采样率(=客户端 rate,无 SRC) */
|
||||
uint16_t grant_ch; /* 客户端应产出的声道数(DAC 双声道 → 通常 2) */
|
||||
uint16_t grant_fmt; /* 客户端应产出的格式(AUDIO_FMT_*,=硬件最优) */
|
||||
int32_t err; /* MEOW_OK / MEOW_ERR_INVAL / MEOW_ERR_NODEV / MEOW_ERR_BUSY 等 */
|
||||
} m_audio_open_ack_t; /* 4+4+2+2+4 = 16 B */
|
||||
|
||||
/* MSG_AUDIO_PAUSE/RESUME/STOP ↑:流控制(按 stream_id 定位)。 */
|
||||
typedef struct m_audio_stream {
|
||||
uint32_t stream_id;
|
||||
} m_audio_stream_t; /* 4 B */
|
||||
|
||||
/* MSG_AUDIO_PREEMPTED ↓:被抢占通知(后开流赢;旧流收此消息,App 应暂停 UI/提示)。
|
||||
* 旧流仍可读但 PCM 被丢弃(draining),直到客户端 CLOSE/断开。 */
|
||||
typedef struct m_audio_preempted {
|
||||
uint32_t stream_id; /* 被抢占的流 id */
|
||||
char by_app[MEOW_APP_ID_MAX]; /* 32:抢占者 app_id */
|
||||
} m_audio_preempted_t; /* 4+32 = 36 B */
|
||||
|
||||
/* MSG_AUDIO_ROUTE ↑:手动切换输出路由(调试/面板用)。port:0=auto(跟随插孔) 2=3.5 3=4.4。 */
|
||||
typedef struct m_audio_route {
|
||||
uint32_t port; /* 0=auto 2=3.5 3=4.4 */
|
||||
} m_audio_route_t; /* 4 B */
|
||||
|
||||
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
|
||||
* + **DSD_VOLUME(音量收编 v2 泛化双组写,保证 DSD↔PCM 切换连续/防爆音)** + 持久化 settings.conf
|
||||
* `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时**不下发 DAC 音量反转**
|
||||
* (音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
|
||||
typedef struct m_audio_volume {
|
||||
int32_t pct; /* -1=查询;0-100=设置 */
|
||||
} m_audio_volume_t; /* 4 B */
|
||||
|
||||
/* 输出目标枚举(msg AUDIO_OUTPUT / settings.conf `audio.output_bt` 共用;亦用于 CLI output) */
|
||||
enum {
|
||||
AUDIO_OUT_DAC = 0, /* 本地 DAC(hw:0,0,独占/period=1024×4/FPGA numid=9/音量反转) */
|
||||
AUDIO_OUT_BT = 1 /* 蓝牙 A2DP source(pcm.bluealsa 播放向,透传+格式归一,AVRCP 控音量) */
|
||||
};
|
||||
|
||||
/* MSG_AUDIO_OUTPUT ↑:切换输出目标(ui/CLI 下发)。0=DAC 1=BT。audiod 侧重配 PCM。 */
|
||||
typedef struct m_audio_output {
|
||||
uint32_t target; /* AUDIO_OUT_DAC / AUDIO_OUT_BT */
|
||||
} m_audio_output_t; /* 4 B */
|
||||
|
||||
/* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)=====
|
||||
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
|
||||
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
|
||||
* svcd 托管 trigger=boot + 单一二进制双形态(/usr/bin/meow_netmgrd 守护 + /usr/bin/meow_net CLI)。
|
||||
* 鉴权分级同 svc_power(§10/期1):读级(STATUS/IFACE、WIFI_CTL 内 status/networks、BT_CTL 内
|
||||
* status/paired)任意调用者;写级(WIFI_CTL 内 scan/connect/disconnect/on/off/rfkill_set/udhcpc、
|
||||
* BT_CTL 内 on/off)仅系统身份(SO_PEERCRED → /usr/bin/meow_* 或 os.meow.* 包)。
|
||||
* HibyOS 模式(/tmp/meow_boot_mode=hiby):读级可查,写级拒 + 记日志。
|
||||
* 期 1 实装 0x00E0-0x00E7;0x00E8-0x00EF 保留(期2:连接普查/DNS 代理;期3:管控策略/事件推送)。 */
|
||||
enum { /* 与主 enum 分列,追加式不改既有行 */
|
||||
MSG_NET_STATUS_REQ = 0x00E0, /* ↑ 概览查询(空负载=全部) */
|
||||
MSG_NET_STATUS_RESULT = 0x00E1, /* ↓ 概览 JSON */
|
||||
MSG_NET_IFACE_REQ = 0x00E2, /* ↑ 接口列表/详情(name 空=全部) */
|
||||
MSG_NET_IFACE_RESULT = 0x00E3, /* ↓ 接口 JSON */
|
||||
MSG_NET_WIFI_CTL = 0x00E4, /* ↑ 子命令字串(scan/connect/... + rfkill/udhcpc) */
|
||||
MSG_NET_WIFI_RESULT = 0x00E5, /* ↓ 结果/文本 */
|
||||
MSG_NET_BT_CTL = 0x00E6, /* ↑ 子命令字串(status/on/off/paired) */
|
||||
MSG_NET_BT_RESULT = 0x00E7 /* ↓ 结果/文本 */
|
||||
/* 0x00E8-0x00EF 保留:0xE8 连接表 / 0xEA 策略读写 / 0xEC DNS 控制 / 0xEE 事件推送(期 2/3) */
|
||||
};
|
||||
|
||||
/* MSG_NET_STATUS_REQ ↑(空负载=全部;预留 subset 位,期 1 恒 0)→ MSG_NET_STATUS_RESULT ↓:
|
||||
* 概览 JSON(mode/ifaces[]/wifi/bt/rfkill[],见 NETMGR_DESIGN §3 统一模型)。*/
|
||||
typedef struct m_net_status_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
|
||||
uint16_t json_len; /* json 有效字节(不含 NUL) */
|
||||
uint16_t pad;
|
||||
char json[MEOW_IPC_MAX - 8]; /* 概览 JSON(.data 形状) */
|
||||
} m_net_status_result_t;
|
||||
|
||||
#define NET_IFACE_NAME_MAX 32 /* 接口名上限(lo/wlan0/usb-eth) */
|
||||
|
||||
/* MSG_NET_IFACE_REQ ↑:name 空=全部接口;否则单接口详情。→ MSG_NET_IFACE_RESULT ↓。 */
|
||||
typedef struct m_net_iface_req {
|
||||
char name[NET_IFACE_NAME_MAX];
|
||||
} m_net_iface_req_t; /* 32 B */
|
||||
|
||||
typedef struct m_net_iface_result {
|
||||
uint32_t code;
|
||||
uint16_t json_len;
|
||||
uint16_t pad;
|
||||
char json[MEOW_IPC_MAX - 8];
|
||||
} m_net_iface_result_t;
|
||||
|
||||
/* MSG_NET_WIFI_CTL / MSG_NET_BT_CTL ↑:子命令字串 + 变长 arg(connect 的 ssid/psk)。→ *_RESULT ↓。
|
||||
* cmd(WIFI_CTL): status|scan|networks|connect|disconnect|on|off + rfkill/udhcpc 命名空间
|
||||
* (rfkill arg="list"|"<type> <on|off>";udhcpc arg="renew <if>"|"release <if>")。
|
||||
* cmd(BT_CTL) : status|on|off|paired。 */
|
||||
typedef struct m_net_cmd {
|
||||
char cmd[24]; /* 子命令字串(大写不敏感;接收侧截断) */
|
||||
char arg[132]; /* connect: "<ssid>[,<psk>]";rfkill: "<type> <on|off>";udhcpc: "renew|release <if>" */
|
||||
} m_net_cmd_t; /* 156 B */
|
||||
|
||||
typedef struct m_net_cmd_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / MEOW_ERR_NODEV */
|
||||
uint16_t text_len; /* text 有效字节(不含 NUL) */
|
||||
uint16_t pad;
|
||||
char text[MEOW_IPC_MAX - 8];
|
||||
} m_net_cmd_result_t;
|
||||
|
||||
/* ---- 期 2:观测层(连接普查)+ DNS 代理(NETMGR_DESIGN §4/§5)----
|
||||
* 0x00E8/0x00EC 实现取代上方「0x00E8-0x00EF 保留」注释中的连接表/DNS 控制两项;
|
||||
* 0x00EA(策略读写,期3)/0x00EE(事件推送,期2 订阅制预留)仍保留未实装。 */
|
||||
enum { /* 与主 enum 分列,追加式不改既有行 */
|
||||
MSG_NET_CONNS_REQ = 0x00E8, /* ↑ 连接表查询(entity 空=全部) */
|
||||
MSG_NET_CONNS_RESULT = 0x00E9, /* ↓ 连接表文本(定长行;超帧截断标记) */
|
||||
MSG_NET_DNS_CTL = 0x00EC, /* ↑ dns status|stats|flush */
|
||||
MSG_NET_DNS_RESULT = 0x00ED /* ↓ 结果/统计文本 */
|
||||
/* 0x00EA 策略读写(期3)/ 0x00EE 事件推送(预留,期2 订阅制)/ 0xEB、0xEF 余量 */
|
||||
};
|
||||
|
||||
/* MSG_NET_CONNS_REQ ↑:entity 前缀过滤(空=全部)。entity 形如 `app:<pkg>`/`svc:<svc>`/`proc:<comm>`。 */
|
||||
typedef struct m_net_conns_req {
|
||||
char entity[48];
|
||||
} m_net_conns_req_t; /* 48 B */
|
||||
|
||||
/* MSG_NET_CONNS_RESULT ↓:连接表文本(每行 `entity|lip:lport|rip:rport|state|age_s|domain`)。
|
||||
* 超单帧上限时截断并置 truncated=1(app/top 由客户端按行聚合,容纳全部)。 */
|
||||
typedef struct m_net_conns_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
|
||||
uint16_t text_len;
|
||||
uint16_t truncated; /* 1=被截断 */
|
||||
char text[MEOW_IPC_MAX - 8];
|
||||
} m_net_conns_result_t;
|
||||
|
||||
/* MSG_NET_DNS_CTL ↑:cmd=status|stats|flush。status/stats 读级任意;flush(清缓存)为动作级系统身份。 */
|
||||
typedef struct m_net_dns_ctl {
|
||||
char cmd[16];
|
||||
} m_net_dns_ctl_t; /* 16 B */
|
||||
|
||||
typedef struct m_net_dns_result {
|
||||
uint32_t code;
|
||||
uint16_t text_len;
|
||||
uint16_t pad;
|
||||
char text[MEOW_IPC_MAX - 8];
|
||||
} m_net_dns_result_t;
|
||||
|
||||
/* ---- 期 3:管控层(NETMGR_DESIGN §6;默认 allow,按实体手动 block + 域名白/黑)----
|
||||
* 0x00EA MSG_NET_POLICY(同时承载 GET/SET,op 区分)。GET=读级任意;SET=写级仅系统身份
|
||||
* (SO_PEERCRED → /usr/bin/meow_* / os.meow.* / MeowPanel)。防自锁实体(dropbear/wpa_supplicant/
|
||||
* udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init)拒绝置 block(服务端硬校验)。
|
||||
* 策略持久化:`/data/meow/netmgrd/net_policy.conf`(/data=/usr/data 符号链接),读改写单键原子写。 */
|
||||
enum { /* 与主 enum 分列,追加式不改既有行 */
|
||||
MSG_NET_POLICY = 0x00EA, /* ↑ op=0 查询 / op=1 写(set net / set domain / del) */
|
||||
MSG_NET_POLICY_RESULT = 0x00EB /* ↓ 策略文本/结果 */
|
||||
/* 0x00EE 事件推送(预留,期2 订阅制)/ 0xEF 余量(均为预留) */
|
||||
};
|
||||
|
||||
/* MSG_NET_POLICY 操作码 */
|
||||
enum {
|
||||
NET_POLICY_SHOW = 0, /* show [entity]:entity 空=全部 */
|
||||
NET_POLICY_SET_NET = 1, /* set <entity> allow|block */
|
||||
NET_POLICY_SET_DOMAIN = 2, /* domain <entity> +host|-host */
|
||||
NET_POLICY_DEL = 3 /* del <entity> */
|
||||
};
|
||||
|
||||
typedef struct m_net_policy {
|
||||
uint32_t op; /* NET_POLICY_* */
|
||||
char entity[48]; /* app:<pkg> | svc:<svc> | proc:<comm> | global;空=适用 SHOW 的全量 */
|
||||
char arg[64]; /* SET_NET:allow|block;SET_DOMAIN:+host|-host;其余空 */
|
||||
} m_net_policy_t; /* 120 B */
|
||||
|
||||
typedef struct m_net_policy_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / (防自锁)MEOW_ERR_PERM */
|
||||
uint16_t text_len;
|
||||
uint16_t pad;
|
||||
char text[MEOW_IPC_MAX - 8];
|
||||
} m_net_policy_result_t;
|
||||
|
||||
/* ---- 期 4:SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;0xEE/0xEF)----
|
||||
* 一、meow_net_policy_check(host):框架 App 联网前自查 host 是否被 netmgrd 策略放行——
|
||||
* 服务端以调用方 app_id(msg.app_id)→ 实体 app:<pkg>,评估 policy_net_blocked(entity)
|
||||
* || dns_query_domain_denied(entity, host),放行=allow 1 / 拒绝=block 0;netmgrd 不在 → SDK 侧放行。
|
||||
* 二、meow_net_report(rx,tx):App 自愿上报本进程收发字节数(内核无 per-socket/per-uid 计数,
|
||||
* §4 诚实声明的「字节级记账走 SDK 自报」);netmgrd 记入 per-实体统计(status JSON entity_bytes)。
|
||||
* 读级/上报任意调用者(自愿自报,无副作用;block 判定由服务端策略引擎权威执行)。
|
||||
* (0x00EE 曾留作「事件推送 订阅制」,期 4 起改作 SDK 通道;事件推送期 2 从未实装。) */
|
||||
enum { /* 与主 enum 分列,追加式不改既有行 */
|
||||
MSG_NET_SDK = 0x00EE, /* ↑ op=0 策略检查 / op=1 字节上报 */
|
||||
MSG_NET_SDK_RESULT = 0x00EF /* ↓ code + (CHECK)allow + 状态文本 */
|
||||
};
|
||||
enum { NET_SDK_CHECK = 0, NET_SDK_REPORT = 1 };
|
||||
|
||||
typedef struct m_net_sdk {
|
||||
uint32_t op; /* NET_SDK_CHECK / NET_SDK_REPORT */
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 调用方 application(SDK 用 env MEOW_LOG_APP;CLI/测试可任意) */
|
||||
char host[64]; /* CHECK:待查域名 */
|
||||
uint32_t rx; /* REPORT:本进程累计 RX 字节 */
|
||||
uint32_t tx; /* REPORT:本进程累计 TX 字节 */
|
||||
} m_net_sdk_t; /* 4+32+64+4+4 = 108 B */
|
||||
|
||||
typedef struct m_net_sdk_result {
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
|
||||
int32_t allow; /* CHECK:1=allow 0=block(block 由服务端策略引擎判定) */
|
||||
uint16_t text_len;
|
||||
uint16_t pad;
|
||||
char text[MEOW_IPC_MAX - 12];
|
||||
} m_net_sdk_result_t;
|
||||
|
||||
#endif /* MEOW_IPC_H */
|
||||
@@ -28,6 +28,7 @@ extern "C" {
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
||||
#include <pthread.h> /* M0:IPC 线程 */
|
||||
#include "key_router.h" /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1) */
|
||||
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
|
||||
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
||||
#include <sys/un.h> /* M0:sockaddr_un */
|
||||
@@ -37,9 +38,21 @@ extern "C" {
|
||||
#include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */
|
||||
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
|
||||
#include "meow_media.h" /* 阶段D附录B:媒体会话注册表(MSG_META/MSG_PLAYCTL/最近活跃源) */
|
||||
#include "meow_bgplay.h" /* M4:后台播放保活 + 无头后台服务运行时表(§9) */
|
||||
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
|
||||
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
|
||||
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
|
||||
#include "lock_screen.h" /* M2.x:锁屏 4 位 PIN 纯逻辑(设置/校验/错5锁定持久化/panel.pin 迁移/唤醒判定) */
|
||||
|
||||
/* M7.0b 文案抽取(SDK meow_str i18n 雏形):`meow_str(id)` 在 id 未进内嵌表(且无 TF 覆盖表)
|
||||
* 时原样返回含'.'的 id —— 系统 UI 硬编码中文案经本辅助抽取,未知 id 回退 `zh` 兜底,保证默认
|
||||
* 中文不被裸 id 顶替;其 en/自定义文案可由 TF `strings/<locale>.txt` 覆盖表或后续并入 SDK 内嵌表补齐。
|
||||
* 注:static inline 不跨 TU 产生未用告警(GCC 忽略 `unused static inline`)。 */
|
||||
static inline const char *meow_str_zh(const char *id, const char *zh)
|
||||
{
|
||||
const char *s = meow_str(id);
|
||||
return (s && s[0] && !strchr(s, '.')) ? s : zh;
|
||||
}
|
||||
|
||||
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
|
||||
#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL, PLAY, NEXT */
|
||||
@@ -140,6 +153,9 @@ extern "C" {
|
||||
#define MEOW_VOL_MAX_DEFAULT 100 /* 默认最大音量上限(尊重上限;可配 audio.volume_max) */
|
||||
#define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */
|
||||
#define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */
|
||||
#define VOL_RAMP_MAX_MS 3000 /* ramp 总时长上限(防 ADC 噪声假按住一次打满;正常长按到上限 <3s) */
|
||||
#define JACK_POLL_MS 1000 /* 插孔状态节流读取间隔(绝不在 10ms 热路径每圈读文件) */
|
||||
#define JACK_DEBOUNCE_MS 800 /* 插孔变化后忽略音量键边沿的窗口(插拔时没人按音量,误伤可忽略) */
|
||||
#define BT_ROLE_OFF 0 /* 关 */
|
||||
#define BT_ROLE_ON_NO_CONN 1 /* 开未连接 */
|
||||
#define BT_ROLE_MASTER 2 /* 主模式:连了外围(如耳机) */
|
||||
@@ -163,7 +179,7 @@ extern "C" {
|
||||
#define NCP_DRAG_ENTER_SLOP 16
|
||||
#define NCP_DRAG_THR_PCT 40
|
||||
#define TAP_ARM_DELAY_MS 300
|
||||
#define PM_NUM 5
|
||||
#define PM_NUM 6
|
||||
#define PM_R 50 /* 圆钮半径(直径 100) */
|
||||
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
|
||||
#define PM_GAPY 140 /* 相邻行圆心 y 间距 */
|
||||
@@ -185,6 +201,7 @@ extern "C" {
|
||||
#define NET_LIST_ABOUT 6 /* 系统设置「关于本机」:只读设备信息弹窗(复用 s_dd_modal) */
|
||||
#define NET_LIST_WIFI_PAGE 7 /* M2.76:WiFi 独立子页面入口(tap → run_wifi_page) */
|
||||
#define NET_LIST_BT_PAGE 8 /* M2.76:蓝牙独立子页面入口(tap → run_bt_page) */
|
||||
#define NET_LIST_PIN_PAGE 9 /* M6.x:锁屏 PIN 独立子页面入口(tap → run_lock_settings_page) */
|
||||
#define S_NET_MAX 8 /* 网络下拉一次最多展示项(屏幕高度限制) */
|
||||
#define BG_PRESETS_N 3
|
||||
#define HK_PRESETS_N 2
|
||||
@@ -291,10 +308,19 @@ struct meow_svc {
|
||||
};
|
||||
|
||||
|
||||
/* 二进制依赖行:depends=<name>[:<mod>][,...]。<name>=二进制名;<mod>=required|req(默认)/optional|opt。
|
||||
* launcher 启动前检查(required 缺失阻断,optional 缺失放行),nyai 安装时提示。 */
|
||||
#define MEOW_MAX_DEPS 8
|
||||
struct meow_dep {
|
||||
char name[32]; /* 二进制名(/usr/bin/<name> 或 <卡>/MeowOS/opt/bin/<name>) */
|
||||
char req; /* '1'=required(缺失阻断启动) '0'=optional(缺失仅 WARN 放行) */
|
||||
};
|
||||
|
||||
|
||||
struct app {
|
||||
char id[MEOW_APP_ID_MAX]; /* M2.52:app_id=app.json 的 package(权威,缺省回退卡上目录名 de->d_name),供 MEOW_LOG_APP 注入 */
|
||||
char package[MEOW_APP_ID_MAX]; /* 安卓化:app.json 的 package 字段(反域名权威标识;目录名=package),scan_apps 据此定 app_id */
|
||||
char name[17]; /* ASCII display name */
|
||||
char name[49]; /* display name(UTF-8,截断走 json_str 整序列保护) */
|
||||
char exec[129]; /* absolute path */
|
||||
int ok; /* exec exists and X_OK */
|
||||
/* ---- 阶段 D · M1:app.json 路由/框架/服务/图标(scan_apps 解析)---- */
|
||||
@@ -302,11 +328,16 @@ struct app {
|
||||
char immersive; /* '1'=immersive=true(隐藏虚拟返回键,返回手势归 SDK) */
|
||||
char fullscreen; /* '1'=app.json fullscreen=true(注入 MEOW_FULLSCREEN=1,SDK 预先设全屏) */
|
||||
char fonts; /* 字库档:0=默认4K常用 1=大字库GBK 2=Full(app.json "fonts";注入 MEOW_FONT_TIER) */
|
||||
char background; /* M4:'1'=app.json background=true(可注册无头后台服务) */
|
||||
char background_audio; /* M4:'1'=background_audio=true(播放中退出转后台保活,§9) */
|
||||
char background_keepalive; /* 批①:'1'=background_keepalive=true(进入后台即保活,不依赖媒体会话=通用常驻) */
|
||||
char icon[129]; /* icon= 路径 */
|
||||
int nroute; /* route.<id> 行数 */
|
||||
struct meow_route routes[MEOW_MAX_ROUTES];
|
||||
int nsvc;
|
||||
struct meow_svc svcs[MEOW_MAX_SVCS];
|
||||
int ndep; /* app.json depends 二进制依赖项数(meow_dep.c) */
|
||||
struct meow_dep deps[MEOW_MAX_DEPS];
|
||||
};
|
||||
|
||||
|
||||
@@ -314,7 +345,7 @@ struct app {
|
||||
* 与 MSG_ACT_DECLARE/HELLO(运行时)并集填充;单一写者=主循环,无需锁。 */
|
||||
struct meow_acct {
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
char name[17];
|
||||
char name[49];
|
||||
char exec[129];
|
||||
int ok;
|
||||
char framework;
|
||||
@@ -324,6 +355,9 @@ struct meow_acct {
|
||||
int nsvc;
|
||||
struct meow_svc svcs[MEOW_MAX_SVCS];
|
||||
int declared; /* 是否经 HELLO/ACT_DECLARE 运行时声明过 */
|
||||
char background; /* M4:'1'=app.json background=true(MSG_REGISTER 权限位0) */
|
||||
char background_audio; /* M4:'1'=background_audio=true(MSG_REGISTER MF_PLAYBACK 权限位1) */
|
||||
char background_keepalive; /* 批①:'1'=background_keepalive=true(进入后台即保活,不依赖媒体会话) */
|
||||
};
|
||||
|
||||
|
||||
@@ -425,8 +459,10 @@ typedef struct {
|
||||
|
||||
|
||||
/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作)。
|
||||
* M2.76:追加 mac[24](AP BSSID / 蓝牙设备 MAC),供连接/配对/详情展示;无则空串。 */
|
||||
typedef struct { int id; char name[48]; char mac[24]; } meow_net_t;
|
||||
* M2.76:追加 mac[24](AP BSSID / 蓝牙设备 MAC),供连接/配对/详情展示;无则空串。
|
||||
* M6.0:追加 sec[16](WiFi 扫描结果安全类型:Open/WPA-PSK/...;无则空=未知),
|
||||
* 供「WiFi 新网络录入」判断是否需要弹 IME 输密码。 */
|
||||
typedef struct { int id; char name[48]; char mac[24]; char sec[16]; } meow_net_t;
|
||||
|
||||
|
||||
/* ================================================================== */
|
||||
@@ -458,12 +494,26 @@ typedef struct sub_page {
|
||||
int (*detail)(void *ctx, int idx, int act);/* 点 SUB_ROW 行(如连接/设备详情);返回 1=重绘 */
|
||||
int live; /* 1=驻留时周期性(约2s)重扫行(蓝牙/附近扫描刷新用) */
|
||||
void *ctx; /* 页面私有上下文 */
|
||||
|
||||
/* ---- 批③ 后台管理页按钮化(可选;NULL/0=按旧路径,未填页/WiFi/蓝牙/锁屏等不受影响)----
|
||||
* 提供下方任一回调即启用对应渲染/命中定制;全 NULL 时行为与旧版完全一致。 */
|
||||
const char *top_action; /* 顶栏标题栏右侧动作按钮文案(NULL=无按钮) */
|
||||
int (*top_action_cb)(void *ctx); /* 点顶栏动作按钮;返回 1=重绘 */
|
||||
/* 行完全自绘(覆盖默认 renderer;页内由 r->kind 区分行型)。state=MEOW_LISTVIEW_ST_*(选中/按压/禁用)。 */
|
||||
void (*row_draw)(void *ctx, int idx, sub_row_t *r,
|
||||
int x, int y, int w, int h, int state);
|
||||
/* 点 SUB_ROW 行(自定义命中:页内自行区分「行内按钮区 vs 行体」;x,y=tap,rx..rh=行视口矩形);
|
||||
* 返回 1=重绘。仅 kind==SUB_ROW 且本回调非空时走此路径,否则走 detail。 */
|
||||
int (*row_tap)(void *ctx, int idx, sub_row_t *r, int x, int y,
|
||||
int rx, int ry, int rw, int rh);
|
||||
} sub_page_t;
|
||||
|
||||
/* 运行子页(阻塞到返回;返回 0)。由 wifi_page.c / bt_page.c 各自实现。 */
|
||||
int meow_subpage_run(sub_page_t *p);
|
||||
int run_wifi_page(void);
|
||||
int run_bt_page(void);
|
||||
/* 批③:后台管理页(bg_page.c;桌面顶栏居右按钮进入;阻塞到返回)。 */
|
||||
int run_bg_page(struct app *apps, int napps);
|
||||
|
||||
|
||||
/* chip 索引(⑥:第 5 个 = 电源菜单,点击唤起既有系统电源菜单 show_power_menu) */
|
||||
@@ -626,6 +676,15 @@ int kp_open(struct keypoll *kp, const char *dev);
|
||||
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
|
||||
void *ctx);
|
||||
int launch_app(struct app *a);
|
||||
/* M6.40:最近一次启动失败的具体原因(fork OOM / exec 失败 / 缺依赖 / 路径不存在等;
|
||||
* launch 成功或未失败则 "")。launch_app/launch_activity 返回 -1 前由 app_launcher.c 填好,
|
||||
* main_menu.c 的 launch_fail_toast 据以显示「CANNOT START: %s(%s)」。 */
|
||||
const char *launch_fail_reason(void);
|
||||
/* ---- app.json 二进制依赖(depends)解析与启动前检查(meow_dep.c)---- */
|
||||
int meow_parse_depends(const char *val, struct meow_dep *deps, int max);
|
||||
int meow_check_app_depends(const struct app *a, char *missing, int mlen);
|
||||
void meow_dep_clear_reason(void);
|
||||
const char *meow_dep_block_reason(void);
|
||||
/* M3:以指定 activity 链路冷启动某 app(注入 MEOW_ACTIVITY/MEOW_CHAIN + 设 nav_stack 栈深)。
|
||||
* chain=NULL 或空 = 裸启动(进 App 主屏/首页,不注入 activity env)。 */
|
||||
int launch_activity(struct app *a, const char *chain);
|
||||
@@ -646,20 +705,40 @@ void meow_nav_exit(void); /* 清栈(应用退出/EXIT) */
|
||||
void meow_nav_request_exit(void); /* MSG_NAV{EXIT}:置位让前台 waitpid 循环终止 app */
|
||||
int meow_nav_exit_pending(void); /* 读取并清除退出请求(前台 launch_* 循环轮询) */
|
||||
/* ---- amsd 控制挂点(AMSD_DESIGN §4.3 / §6.2 二期):主循环单写者,跨模块声明 ----
|
||||
* pending-start/kill 仅在主循环上下文写入与消费(ipc_dispatch 写、run_menu 前台循环/菜单循环读)。 */
|
||||
int meow_ams_pending_start_set(const char *pkg, const char *act); /* 有前台 App 时拒绝(返回 -MEOW_ERR_BUSY) */
|
||||
int meow_ams_take_start(char *pkg, int pn, char *act, int an); /* 1=取到待拉起 App(清位);0=无 */
|
||||
* pending-start 状态/拉起决策在 meow_ams_start.c(自包含、可单测,见 meow_ams_start.h);
|
||||
* pending-kill 在 app_launcher.c。仅在主循环上下文写入与消费(ipc_dispatch 写、
|
||||
* run_menu 前台循环/菜单循环读)。 */
|
||||
void meow_ams_pending_kill_set(const char *pkg); /* 请求终止某 pkg */
|
||||
int meow_ams_kill_pending(const char *pkg); /* 匹配则清除并返回 1(前台循环轮询) */
|
||||
/* §6.2 二期:带 picker 上下文的 pending-start(amsd `am picker` 经 MSG_AMS_START 扩载荷)。
|
||||
* pick_action 非空 → launch_app_chain 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER 到子进程。 */
|
||||
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
|
||||
const char *pick_action, uint32_t pick_req,
|
||||
const char *pick_caller);
|
||||
/* run_menu 消费 pending-start:复用 launch_app/launch_activity 链真正拉起目标 App。
|
||||
* 返回 0=无 pending/busy;1=已消费(拉起成功/拒绝);2=列表未就绪(pending 保留待重扫)。
|
||||
* app_ran=1=app 已运行(据以设 touch 冷却);idx_out>=0 且 app_ran=0 → 启动失败,打 launch_fail_toast。 */
|
||||
int meow_ams_start_consume(const char *const *ids, int napps, int state,
|
||||
char *toast, int toast_len, int *app_ran, int *idx_out);
|
||||
/* launch_app_chain 入口拾取 picker 上下文(一次性清位)。 */
|
||||
int meow_ams_take_picker(char *action, int action_len,
|
||||
char *caller, int caller_len, uint32_t *req);
|
||||
/* 组装 amsd-start ops(生产=launch_app/launch_activity+resolve_chain+logmsg;meow_ipc_init 末尾调)。 */
|
||||
void meow_ams_launcher_init(void);
|
||||
/* 供 run_menu/idle 挂当前 apps[] 上下文(ops.launch 的 idx→app 映射用)。 */
|
||||
void meow_ams_ctx_set(struct app *apps, int napps);
|
||||
/* §6.2 二期:amsd 上报通道(best-effort,app_launcher.c 的 ams_send 公开包装)。
|
||||
* util.c 转发运行时 declare 与 picker call/result 给 amsd 用。 */
|
||||
void meow_ams_send(const char *payload, int len);
|
||||
|
||||
/* ---- M4 后台保活/后台服务胶水(app_launcher.c 实现;util.c/main_menu.c 消费)---- */
|
||||
/* launcher 侧 meow_bgplay ops 组装(meow_ipc_init 末尾调一次)。 */
|
||||
void meow_bgplay_launcher_init(void);
|
||||
/* 后台表周期维护:drain 各 bg App 的 stdout 管道 + meow_bg_tick + meow_svc_tick。
|
||||
* 挂载点=meow_ipc_process 末尾(主循环单写者排空点,四处循环都经它)。 */
|
||||
void meow_bg_housekeep(void);
|
||||
/* 查静态声明的 service.<svc>;无返回 NULL。 */
|
||||
const struct meow_svc *meow_acct_svc_find(const struct meow_acct *ac, const char *svc);
|
||||
/* 后台权限位:bit0=background(可注册服务)bit1=background_audio(后台音频)。 */
|
||||
int meow_acct_background_perm(const struct meow_acct *ac);
|
||||
/* M4:服务 exec 路径约定 = App 二进制同目录下的 <svc> 名文件。 */
|
||||
void meow_svc_exec_path(const char *app_exec, const char *svc, char *out, int olen);
|
||||
|
||||
void meow_dac_vol_write(int pct);
|
||||
void meow_dac_vol_write_throttled(void);
|
||||
void meow_dirty_add(int x, int y, int w, int h);
|
||||
@@ -673,10 +752,24 @@ int meow_font_effective_tier(int app_tier);
|
||||
int meow_font_ensure(void);
|
||||
void meow_font_reload_multifont(void);
|
||||
int meow_font_resolve_path(const char *ref, char *out, int olen);
|
||||
void meow_font_scan_presets(void);
|
||||
void meow_font_scan_presets_root(const char *card_root);
|
||||
void meow_font_scan_presets(int verify_sha);
|
||||
void meow_font_scan_presets_root(const char *card_root, int verify_sha);
|
||||
int meow_ipc_init(void);
|
||||
void meow_ipc_process(void);
|
||||
/* M4:IPC 连接存活查询(熔断广播投递前判连接未断;util.c ipc_conns 表查 fd)。 */
|
||||
int meow_ipc_conn_alive(int fd);
|
||||
/* M4:ipc_send 公开包装(meow_bgplay send_frame 回调用;同 meow_media 的注入口径)。 */
|
||||
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
|
||||
const void *payload, uint32_t len);
|
||||
/* ---- E:系统级音量翻页(READER_UI_DESIGN §E,用户 2026-09-04 拍板)----
|
||||
* 前台 App 声明 app.json `volume_page` 时,launcher 把物理 VOL_UP/DOWN 转成系统翻页信号
|
||||
* (MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该 App;否则照旧调音量。 */
|
||||
int meow_volpage_app_allows(const char *app_id); /* keys.c:读 app.json 声明 */
|
||||
void meow_volpage_sink(const char *app_id, int fd); /* keys.c:武装前台翻页 sink */
|
||||
void meow_volpage_sink_clear(void); /* keys.c:撤武装 */
|
||||
int meow_volpage_armed(void); /* keys.c:查询 */
|
||||
int meow_volpage_key(int code, int is_down); /* keys.c:门(1=已消费/已下发) */
|
||||
int meow_ipc_app_fd(const char *app_id); /* util.c:前台 App→持久 IPC fd(-1=无) */
|
||||
/* §10.5:读并清除待处理的 MSG_NOTIF_OPEN(App 前台顶部下滑请求展开通知中心叠加会话)。
|
||||
* 调用方=launch_app waitpid 循环;返回 1 并填 *out(待处理),0=无。 */
|
||||
int meow_notif_open_take(m_notif_open_t *out);
|
||||
@@ -717,7 +810,7 @@ uint16_t rgb565(uint32_t c);
|
||||
void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
int live_open, int lox, int loy);
|
||||
/* 阶段D附录B:Now Playing 大页(主界面横划进入;左缘右滑返回;阻塞到返回)。 */
|
||||
void run_now_playing(void);
|
||||
void run_now_playing(struct app *apps, int napps);
|
||||
void run_settings_page(void);
|
||||
void s_about_open(void);
|
||||
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur);
|
||||
@@ -730,8 +823,63 @@ int s_net_get(const srow_t *r, char *buf, int len);
|
||||
void s_net_open(const srow_t *r);
|
||||
void s_net_refresh(void);
|
||||
void s_net_sync_from_sys(void);
|
||||
/* M3:设置页异步网络刷新 —— kick 重置步骤机并触发扫描(非阻塞);pump 每步一个
|
||||
* 有界 popen,返回 1=数据可能更新(调用方重绘)。run_settings_page 主循环逐步骤驱动。 */
|
||||
void s_net_refresh_kick(void);
|
||||
int s_net_refresh_pump(void);
|
||||
void s_net_toggle_bt(int on);
|
||||
void s_net_toggle_wifi(int on);
|
||||
|
||||
/* ================= meow_netmgrd IPC 客户端(netmgr_ipc.c)=================
|
||||
* M6.0 设置页迁移(NETMGR_DESIGN §12):wifi_page/bt_page 的查询/操作统一走 netmgrd
|
||||
* IPC(socket /usr/data/meow_net.sock,env MEOW_NET_SOCK 覆盖)。netmgrd 是 WiFi/BT
|
||||
* 操作权威;其不可达(NETMGR_UNREACH)时回退 net.c 直连 wpa_cli/hciconfig(双轨保功能)。
|
||||
* 迁移注释:此封装是「设置页客户端→权威 IPC」的唯一入口,netmgrd 不可达才走直连回退。 */
|
||||
#define NETMGR_UNREACH (-1000) /* 传输/握手不可达(区别于 netmgrd MEOW_ERR_* 业务码) */
|
||||
int netmgr_ok(void); /* 1=netmgrd 可达(结果约 1s 缓存,节流) */
|
||||
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen); /* 返回 code / NETMGR_UNREACH */
|
||||
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen);
|
||||
|
||||
/* WiFi(IPC 优先;网卡链路态 wlan_connected()/hci_powered() 仍读内核,属只读观测) */
|
||||
int nm_wifi_connected(void);
|
||||
void nm_wifi_ssid(char *buf, int len);
|
||||
int nm_wifi_info(char (*info)[48], int max);
|
||||
int nm_wifi_saved(meow_net_t *out, int max);
|
||||
int nm_wifi_scan(meow_net_t *out, int max);
|
||||
void nm_wifi_scan_trigger(void);
|
||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
|
||||
/* WiFi 设置页 bug2(2026-09-15):hidden=1 走 netmgrd connecth(scan_ssid=1 + 更长 COMPLETED 等待),
|
||||
* 回退直连同步对齐;err 回填 netmgrd/直连的诚实结果文本(成功含 ip / 失败含原因)。 */
|
||||
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen);
|
||||
int nm_wifi_disconnect(char *err, int elen);
|
||||
int nm_wifi_toggle(int on);
|
||||
|
||||
/* BT(IPC 优先:全量子命令权威收口 P1;netmgrd 不可达回退 net.c 直连)。
|
||||
* P1 收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八):connect/disconnect/pair/unpair/scan 写级
|
||||
* 迁 netmgrd;connected/devices/a2dp 读级也迁 netmgrd(launcher 只做客户端)。 */
|
||||
int nm_bt_powered(void);
|
||||
int nm_bt_onoff(int on);
|
||||
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen);
|
||||
int nm_bt_paired(meow_net_t *out, int max);
|
||||
int nm_bt_connected(meow_net_t *out, int max);
|
||||
int nm_bt_devices(meow_net_t *out, int max);
|
||||
/* A2DP 单端点详情(codec/采样率/声道/位深/acquired/role;netmgrd bt a2dp 解析) */
|
||||
typedef struct {
|
||||
char mac[24]; /* 端点设备 MAC(与 connected/paired 列表可 join) */
|
||||
char codec[16]; /* SBC/AAC/aptX/LDAC...;空=未知 */
|
||||
char rate[16]; /* 采样率(如 "44100");空=未知 */
|
||||
char chn[8]; /* 声道数 */
|
||||
char bits[8]; /* 位深 */
|
||||
int acquired; /* 1=传输已获取(streaming 中) */
|
||||
char role[8]; /* "src"/"snk"/"-"(设备侧 A2DP 角色) */
|
||||
} bt_ep_t;
|
||||
int nm_bt_a2dp(bt_ep_t *eps, int max); /* 返回端点数 */
|
||||
int nm_bt_connect(const char *mac);
|
||||
int nm_bt_disconnect(const char *mac);
|
||||
int nm_bt_pair(const char *mac);
|
||||
int nm_bt_unpair(const char *mac);
|
||||
void nm_bt_scan_on(void);
|
||||
void nm_bt_scan_off(void);
|
||||
const meow_ui_theme_t *s_ui_theme(void);
|
||||
void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h);
|
||||
int scan_apps(struct app apps[MAX_APPS]);
|
||||
@@ -743,6 +891,18 @@ void settings_sync_cache_listener(const char *key);
|
||||
void settings_volume_adj(int delta);
|
||||
int settings_volume_get(void);
|
||||
int show_power_menu(void);
|
||||
/* M2.x 锁屏:阻塞式锁屏页(渲染+输入);返回 1=已解锁(回主屏),0=锁屏期间电源键息屏(屏已关),
|
||||
* -1(LCK_OVERLAY_RET_DIED)=锁屏叠加期 App 崩溃(run_app_lock_overlay 走统一退出路径)。
|
||||
* meow_lock_screen_engage() 供电源菜单「锁定」与唤醒路径用;未配凭据时 no-op。 */
|
||||
int run_lock_screen(void);
|
||||
int meow_lock_screen_engage(void);
|
||||
/* §310 残影:锁屏处于 App 前台叠加会话时置位当前 App pid(run_app_lock_overlay 写入,
|
||||
* run_lock_screen 每圈据此 proc_is_zombie 探测;菜单直接锁屏时保持 0=不探)。 */
|
||||
extern int g_lock_overlay_pid;
|
||||
/* 设置页「锁屏 PIN」数字输入模态(系统级 PIN 设置/修改输入):满 4 位返回 1 并拷贝到 out,电源=取消返回 0。 */
|
||||
int lck_input_pin_modal(const char *title, char *out, int outcap);
|
||||
/* 设置页「锁屏 PIN」独立子页面(subpage.c 脚手架;阻塞到返回)。 */
|
||||
int run_lock_settings_page(void);
|
||||
int stop_player(void);
|
||||
/* 电源管理服务 IPC(SYSTEM_FRAMEWORK_SERVICES §3.2:launcher 电源动作经 meow_svc_power 执行) */
|
||||
int pm_power_ipc(uint32_t cmd, uint32_t flag, const char *arg, char *res, int reslen);
|
||||
@@ -759,7 +919,7 @@ void vol_restore(struct vol_snap s);
|
||||
struct vol_snap vol_snapshot(void);
|
||||
int wlan_connected(void);
|
||||
int wlan_radio_up(void);
|
||||
void write_brightness(int val);
|
||||
void write_brightness(int val, const char *src); /* src=调用源线索,随值一并 logmsg(亮度幽灵排查) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -120,6 +120,19 @@ int meow_media_has_source(void)
|
||||
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
|
||||
}
|
||||
|
||||
int meow_media_app_playing(const char *app_id)
|
||||
{
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
if (!s || !app_id || !app_id[0]) return 0;
|
||||
return (s->state == MEOW_MEDIA_PLAYING && strcmp(s->app_id, app_id) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int meow_media_app_resident(const char *app_id)
|
||||
{
|
||||
const meow_media_session_t *s = sess_find(app_id);
|
||||
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
|
||||
}
|
||||
|
||||
static int send_playctl(const meow_media_session_t *s, uint32_t cmd, int64_t seek_ms)
|
||||
{
|
||||
if (!s || !s->used || !g_ops || !g_ops->send) return -1;
|
||||
@@ -190,13 +203,74 @@ uint32_t meow_media_display_pos(const meow_media_session_t *s)
|
||||
return p;
|
||||
}
|
||||
|
||||
/* 把一帧歌词分片写入会话缓冲(kind=0 lrc / kind=1 tlrc)。含分片边界截断(不越界)。
|
||||
* 返回 0=写入;-1=参数非法。 */
|
||||
static int lyric_put(meow_media_session_t *s, const m_lyric_t *l)
|
||||
{
|
||||
if (!s || !l) return -1;
|
||||
char *buf;
|
||||
uint32_t cap;
|
||||
uint32_t *lenp;
|
||||
if (l->kind == 0) { buf = s->lyric_lrc; cap = MEOW_MEDIA_LRC_MAX; lenp = &s->lyric_lrc_len; }
|
||||
else if (l->kind == 1) { buf = s->lyric_tlrc; cap = MEOW_MEDIA_TLRC_MAX; lenp = &s->lyric_tlrc_len; }
|
||||
else return -1;
|
||||
uint32_t off = l->off, n = l->len;
|
||||
if (off >= cap) { off = cap; n = 0; } /* 起始越界:整片丢弃 */
|
||||
if (n > cap - off) n = cap - off; /* 末段越界:截断到缓冲上限 */
|
||||
if (n) memcpy(buf + off, l->data, n);
|
||||
buf[off + n] = 0; /* 恒 NUL 结尾(供字符串读取) */
|
||||
uint32_t end = off + n;
|
||||
if (end > *lenp) *lenp = end;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_media_lyric(const m_lyric_t *l)
|
||||
{
|
||||
if (!l || !l->app_id[0]) return -1;
|
||||
meow_media_session_t *s = sess_find(l->app_id);
|
||||
if (!s) {
|
||||
/* 会话须先经 META 注册(否则 fd=0,PLAYCTL 会误发到 0 号描述符);丢弃。 */
|
||||
media_log("media: lyric drop app=%s (no session; report META first)", l->app_id);
|
||||
return -1;
|
||||
}
|
||||
/* 新代际(换歌/换词):清旧词缓冲,重开分片写入。 */
|
||||
if (l->seq != s->lyric_seq) {
|
||||
s->lyric_seq = l->seq;
|
||||
s->lyric_lrc_len = 0;
|
||||
s->lyric_tlrc_len = 0;
|
||||
s->lyric_lrc[0] = 0;
|
||||
s->lyric_tlrc[0] = 0;
|
||||
}
|
||||
lyric_put(s, l);
|
||||
media_log("media: LYRIC app=%s seq=%u kind=%u total=%u off=%u len=%u lrc=%u tlrc=%u",
|
||||
s->app_id, l->seq, l->kind, l->total, l->off, l->len,
|
||||
s->lyric_lrc_len, s->lyric_tlrc_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_media_lyric_valid(const meow_media_session_t *s)
|
||||
{
|
||||
return (s && s->used && s->lyric_lrc_len > 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
const char *meow_media_lyric_lrc(const meow_media_session_t *s)
|
||||
{
|
||||
return (s && s->used) ? s->lyric_lrc : "";
|
||||
}
|
||||
|
||||
const char *meow_media_lyric_tlrc(const meow_media_session_t *s)
|
||||
{
|
||||
return (s && s->used) ? s->lyric_tlrc : "";
|
||||
}
|
||||
|
||||
void meow_media_dump(const char *prefix)
|
||||
{
|
||||
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
|
||||
if (!g_sess[i].used) continue;
|
||||
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\"",
|
||||
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\" lyric=%u",
|
||||
prefix ? prefix : "media", i, g_sess[i].app_id, g_sess[i].state,
|
||||
g_sess[i].fd, (unsigned long long)g_sess[i].last_active_ms,
|
||||
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title);
|
||||
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title,
|
||||
(unsigned)(g_sess[i].lyric_lrc_len > 0 ? 1 : 0));
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,15 @@ typedef struct meow_media_session {
|
||||
char artist[128];
|
||||
char album[128];
|
||||
char artwork[192];
|
||||
char lyric[192];
|
||||
char lyric[192]; /* 歌词源路径/槽位(沿用 META.lyric) */
|
||||
/* 批3:当前曲 live 歌词(App 经 MSG_META_LYRIC 上报;有界缓冲,新 seq 重置)。
|
||||
* lyric_lrc/lyric_tlrc 为当前曲歌词原文/翻译文本(桌面歌词页 meow_lyric_parse 用);
|
||||
* lyric_seq=歌曲代际(App 新歌/新词自增,launcher 见新 seq 清旧词)。 */
|
||||
uint32_t lyric_seq;
|
||||
uint32_t lyric_lrc_len;
|
||||
uint32_t lyric_tlrc_len;
|
||||
char lyric_lrc[MEOW_MEDIA_LRC_MAX];
|
||||
char lyric_tlrc[MEOW_MEDIA_TLRC_MAX];
|
||||
} meow_media_session_t;
|
||||
|
||||
/* 依赖注入:send=发一帧(launcher 用 ipc_send 包装;宿主单测用 socket 写);
|
||||
@@ -72,6 +80,15 @@ const meow_media_session_t *meow_media_current(void);
|
||||
/* 是否有活动媒体源:当前源存在且 state != 0(停止)。通知中心卡片/播放页据此显示。 */
|
||||
int meow_media_has_source(void);
|
||||
|
||||
/* §10.5 叠加会话保活判定:当前源(最近活跃会话)存在、state==PLAYING 且 app_id 匹配。
|
||||
* launcher 据此决定通知中心叠加不 SIGSTOP 该 App(保 PCM 供流),SDK present 层自抑上屏。 */
|
||||
int meow_media_app_playing(const char *app_id);
|
||||
|
||||
/* 批①后台常驻判定:该 app 的媒体会话「活跃可驻留」= 会话存在且 state != STOPPED
|
||||
* (PLAYING/PAUSED/BUFFER 皆常驻)。按 app 自身会话判(sess_find),不读最近活跃源——
|
||||
* 多源时不误判「当前源≠本 App」。launcher 转后台保活依据。 */
|
||||
int meow_media_app_resident(const char *app_id);
|
||||
|
||||
/* 向当前源下发 PLAYCTL(卡片按钮/播放页/媒体键/控制器)。返回 0=已发,-1=无源/发送失败。 */
|
||||
int meow_media_playctl(uint32_t cmd, int64_t seek_ms);
|
||||
|
||||
@@ -87,6 +104,17 @@ void meow_media_tick(void);
|
||||
/* 显示用虚拟位置:播放中按墙钟推进 pos_ms(clamp 到 dur);暂停/缓冲/停止原样。 */
|
||||
uint32_t meow_media_display_pos(const meow_media_session_t *s);
|
||||
|
||||
/* ---- 歌词(批3 · live lyric 数据源)----
|
||||
* App 上报当前曲歌词(MSG_META_LYRIC 分片):按 app_id 归并到会话,seq 为歌曲代际、
|
||||
* 新代际清旧词;分片按 kind(0=原文/1=翻译)/off 写入会话内有序缓冲(超 MEOW_MEDIA_LRC_MAX/
|
||||
* MEOW_MEDIA_TLRC_MAX 截断)。会话不存在(未先报 META 注册)→ 丢弃返回 -1(防 fd=0 槽)。
|
||||
* meow_media_lyric_valid/lrc/tlrc:桌面歌词页从当前会话取歌词文本,交由批1 meow_lyric_parse
|
||||
* 解析 + meow_lyric_line_at(meow_media_display_pos(s)) 驱动批2 meow_lyric_view 实时滚动。 */
|
||||
int meow_media_lyric(const m_lyric_t *l); /* 0=已写入;-1=会话不存在/参数非法 */
|
||||
int meow_media_lyric_valid(const meow_media_session_t *s); /* 会话携带有效歌词(lrc 非空) */
|
||||
const char *meow_media_lyric_lrc(const meow_media_session_t *s); /* 原文;无则 "" */
|
||||
const char *meow_media_lyric_tlrc(const meow_media_session_t *s); /* 翻译;无则 "" */
|
||||
|
||||
/* 调试打印(经 ops->log,前缀+每会话一行)。 */
|
||||
void meow_media_dump(const char *prefix);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ static int g_no_file_logged = 0; /* M2.86:本次「未读到」连续段是
|
||||
/* ---- 默认值表:与 SYSTEM_SETTINGS.md §1 逐键一致(新增设置照附录扩展)---- */
|
||||
static const meow_setting_def g_defs[] = {
|
||||
/* 显示 */
|
||||
{ "display.brightness", SET_INT, "101", 0, 255, NULL, NULL },
|
||||
{ "display.brightness", SET_INT, "50", 0, 100, NULL, NULL },
|
||||
{ "display.brightness_min", SET_INT, "1", 0, 255, NULL, NULL },
|
||||
{ "display.screen_off_mode", SET_ENUM, "panel",0, 0, "panel,black", "面板下电,背光全黑" },
|
||||
{ "display.screen_off_timeout", SET_INT, "30", 0, 86400, NULL, NULL },
|
||||
@@ -118,7 +118,7 @@ static const meow_setting_def g_defs[] = {
|
||||
{ "font.load_full", SET_INT, "0", 0, 1, NULL, NULL },
|
||||
{ "font.size.title", SET_INT, "28", 0, 0, NULL, NULL },
|
||||
{ "font.size.body", SET_INT, "20", 0, 0, NULL, NULL },
|
||||
{ "ui.style", SET_ENUM, "md3", 0, 0, "md3,miui", "Material You,MIUI" },
|
||||
{ "ui.style", SET_ENUM, "md3", 0, 0, "md3,miuix", "Material You,MIUIx" },
|
||||
/* 通知 */
|
||||
{ "notify.center_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
|
||||
{ "notify.app_quota", SET_INT, "16", 0, 0, NULL, NULL },
|
||||
@@ -140,6 +140,12 @@ static const meow_setting_def g_defs[] = {
|
||||
{ "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL },
|
||||
{ "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL },
|
||||
{ "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL },
|
||||
/* P1 蓝牙 P1:输出目标偏好(0=DAC/本地,1=蓝牙 OUT_BT)。audiod 侧 target 切换在 P2
|
||||
* 实装;P1 仅作为「输出目标」行/摘要的持久化键(蓝牙子页写,DAC 侧不受影响)。 */
|
||||
{ "audio.output_bt", SET_BOOL, "0", 0, 0, NULL, NULL },
|
||||
/* P1 蓝牙:codec 偏好(auto/SBC/AAC/aptX/LDAC;P2 接 alsa.conf pcm.bt/bluealsa 生效)。
|
||||
* 当前实际协商 codec 由 a2dp 状态实时读,本键只是页面「编解码器」选项的持久化。 */
|
||||
{ "bt.codec", SET_STR, "auto", 0, 0, NULL, NULL },
|
||||
/* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网,
|
||||
* 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */
|
||||
{ "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
|
||||
@@ -147,6 +153,22 @@ static const meow_setting_def g_defs[] = {
|
||||
/* 系统(M2.54:主机名,bootmenu S89meow_svcd 开机读 system.hostname sethostname;
|
||||
* 缺省 meowos;RFC1035 首标签,供 meow_mdns 公布 <host>.local) */
|
||||
{ "system.hostname", SET_STR, "meowos", 0, 0, NULL, NULL },
|
||||
/* 锁屏 4 位 PIN(系统级安全;格式与面板 panel.pin 全等:
|
||||
* `<salt_hex>:<sha256_hex(salt||pin)>`,salt 16B /dev/urandom → 32 hex,绝不存明文。
|
||||
* 空 = 未设锁屏 PIN(锁屏功能关闭)。与面板同源:panel.pin 已设而本键未设时,锁屏
|
||||
* 接受 panel.pin 并在首次成功后并入本键(lck_pin_success,见 lock_screen.c)。 */
|
||||
{ "system.pin", SET_STR, "", 0, 0, NULL, NULL },
|
||||
/* 息屏唤醒后锁屏(int 0/1,默认 1=开)。0=关(唤醒直进主屏;电源菜单「锁定」手动锁仍可用)。 */
|
||||
{ "system.pin_lock_on_wake", SET_INT, "1", 0, 1, NULL, NULL },
|
||||
/* 错 PIN 锁定截止戳(int,epoch 秒;0=未锁定)。**持久化到 settings**(防 launcher 重启
|
||||
* 清零——面板进程内存方案在重启后失效,此处落盘)。截止前锁屏页禁输入并倒计。 */
|
||||
{ "system.pin_lock_until", SET_INT, "0", 0, 0, NULL, NULL },
|
||||
/* 连续错 PIN 计数(int 0-4;达 5 触发锁定并清零)。持久化防「关机重启重试」。 */
|
||||
{ "system.pin_fails", SET_INT, "0", 0, 0, NULL, NULL },
|
||||
/* 面板访问级 PIN(meowpanel 写入,格式与本表 system.pin 全等)。launcher 注册它仅为:
|
||||
* ①锁屏迁移期读取(system.pin 未设时接受 panel.pin);②本表 save 重写全文件时**保留**
|
||||
* 该行(write_full_conf 只写已注册键,不注册会把面板已设的 panel.pin 抹掉)。launcher 不写它。 */
|
||||
{ "panel.pin", SET_STR, "", 0, 0, NULL, NULL },
|
||||
};
|
||||
|
||||
/* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "settings:")---- */
|
||||
@@ -255,6 +277,14 @@ int meow_settings_loaded(void)
|
||||
return g_loaded_from_file;
|
||||
}
|
||||
|
||||
/* 存储文件路径(= 编译期 MEOW_SETTINGS_FILE,默认 /usr/data/meow/settings.conf)。
|
||||
* 供跨进程变更轮询(launcher App 前台会话 stat mtime,见 app_launcher.c)等场景取权威路径,
|
||||
* 避免调用方硬编码与编译期覆盖分叉。 */
|
||||
const char *meow_settings_file_path(void)
|
||||
{
|
||||
return MEOW_SETTINGS_FILE;
|
||||
}
|
||||
|
||||
/* ---- 按类型把字符串值 apply 到 cur(读文件/导入时用;含 clamp/校验)---- */
|
||||
static void apply_value(meow_setting *s, const char *v)
|
||||
{
|
||||
@@ -280,7 +310,13 @@ static void apply_value(meow_setting *s, const char *v)
|
||||
}
|
||||
case SET_ENUM: {
|
||||
int idx = enum_index_of_name_str(s->enum_names, v);
|
||||
if (idx < 0) idx = atoi(v); /* 向后兼容:数值索引 */
|
||||
if (idx < 0) {
|
||||
/* §8.1.1 注:旧版 ui.style 值域为 "md3,miui",本版对齐为 "md3,miuix"——存量
|
||||
* settings.conf / 备份里的 "miui" 应兼容映射为 "miuix"(而非 atoi 落到 md3=0)。 */
|
||||
if (!strcmp(s->key, "ui.style") && strcasecmp(v, "miui") == 0)
|
||||
idx = enum_index_of_name_str(s->enum_names, "miuix");
|
||||
if (idx < 0) idx = atoi(v); /* 向后兼容:数值索引 */
|
||||
}
|
||||
if (idx >= 0 && idx < enum_count_str(s->enum_names)) {
|
||||
const char *name = enum_at_str(s->enum_names, idx);
|
||||
if (name) strncpy(s->cur, name, sizeof s->cur - 1);
|
||||
|
||||
@@ -82,6 +82,10 @@ int meow_settings_brightness_clamped(void);
|
||||
* 1 = 文件已读到:键缺失走各自默认(属「已知」态),用户设定可信。 */
|
||||
int meow_settings_loaded(void);
|
||||
|
||||
/* 存储文件绝对路径(编译期 MEOW_SETTINGS_FILE,默认 /usr/data/meow/settings.conf)。
|
||||
* 供调用方做 mtime 轮询等(App 前台会话状态栏刷新,见 app_launcher.c)。 */
|
||||
const char *meow_settings_file_path(void);
|
||||
|
||||
int meow_settings_set_int (const char *key, int v); /* clamp 到取值域 */
|
||||
int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW_SET_MAX_LEN */
|
||||
int meow_settings_set_enum(const char *key, int idx); /* 校验枚举合法,越界拒绝 */
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* meow_utf8.h — UTF-8 整序列有界拷贝/字符步进纯决策(宿主可单测;无 ui_width/mfont 依赖)。
|
||||
*
|
||||
* 背景(问题2,M6.3 真机):主界面应用名「流媒体客户端」被截成 5 字 + 半个字。两处根因:
|
||||
* ① app_launcher.c `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节(UTF-8 下=5 汉字+ 半个
|
||||
* 「端」)——M6.3 只扩了 name[49]+json_str 整序列,漏改此处;
|
||||
* ② main_menu.c `menu_name_fit` 按字节回退、边界判定错(do..while 读的是刚清零的字节,恒停续字节),
|
||||
* 停在半截多字节字符;再加 "..." 未为省略号留宽 → 溢出 maxw 被裁切。
|
||||
* 本头把 UTF-8 整序列处理抽成纯函数(meow_utf8_char_len / meow_utf8_copy_bounded / 截断决策),
|
||||
* 两处复用并可宿主单测(test_utf8_name.c)。
|
||||
*/
|
||||
#ifndef MEOW_UTF8_H
|
||||
#define MEOW_UTF8_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h> /* snprintf(有界拷贝用) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 由首字节 c0 求整字符 UTF-8 字节长(1..4)。非法首字节(0x80..0xBF 续字节)返回 1(按异常字节跳过,
|
||||
* 绝不把孤立续字节与后字并成大字符)。纯按前导位,不看后续字节是否齐全(那由 copy_bounded 处理)。 */
|
||||
static inline int meow_utf8_char_len(unsigned char c0)
|
||||
{
|
||||
if (c0 < 0x80) return 1;
|
||||
if (c0 < 0xC0) return 1; /* 孤立续字节:兜底 1 */
|
||||
if (c0 < 0xE0) return 2;
|
||||
if (c0 < 0xF0) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
/* 有界拷贝:把 src 拷入 dst(容量 cap,含 NUL),**绝不劈开多字节字符**。snprintf 截断若落在字符
|
||||
* 中间(或 src 本身尾部是半截字符),回退到整字符边界:删去最末不完整字符。返回拷贝后字节长(不含
|
||||
* NUL)。源可为 NULL。持久化/展示名均用——保证 UTF-8 整序列,杜绝「5 汉字+半个字」式残字。 */
|
||||
static inline int meow_utf8_copy_bounded(char *dst, int cap, const char *src)
|
||||
{
|
||||
if (!dst || cap <= 0) return 0;
|
||||
if (cap == 1) { dst[0] = 0; return 0; }
|
||||
if (!src) src = "";
|
||||
snprintf(dst, (size_t)cap, "%s", src);
|
||||
int n = (int)strlen(dst);
|
||||
if (n <= 0) return 0;
|
||||
/* 定位最末一个「起始字节」(非续字节):从尾部回退续字节。 */
|
||||
int last_start = n - 1;
|
||||
while (last_start > 0 && ((unsigned char)dst[last_start] & 0xC0) == 0x80)
|
||||
last_start--;
|
||||
/* 末字符:起始字节=last_start,实际字节数=have;字符应需 need。have<need → 不完整 → 删。 */
|
||||
int need = meow_utf8_char_len((unsigned char)dst[last_start]);
|
||||
int have = n - last_start;
|
||||
if (have < need) { dst[last_start] = 0; n = last_start; }
|
||||
return n;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UTF8_H */
|
||||
@@ -312,6 +312,22 @@ int net_wifi_scan(meow_net_t *out, int max)
|
||||
while (*se && *se != '\n' && *se != '\r') se++;
|
||||
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
|
||||
int has = (se > ssid && *ssid);
|
||||
/* M6.0:flags 首个方括号内容 = 安全类型([WPA-PSK-CCMP] 等);纯 [ESS]/[IBSS] = 开放。
|
||||
* 供「WiFi 新网络录入」判断是否需要弹 IME 输密码。 */
|
||||
char sec[16] = "Open";
|
||||
if (br) {
|
||||
char *be = strchr(br + 1, ']');
|
||||
if (be) {
|
||||
int sl = (int)(be - (br + 1));
|
||||
if (sl > 15) sl = 15;
|
||||
if (sl > 0) {
|
||||
memcpy(sec, br + 1, (size_t)sl); sec[sl] = 0;
|
||||
if (!strstr(sec, "WPA") && !strstr(sec, "RSN") && !strstr(sec, "WEP") &&
|
||||
!strstr(sec, "SAE") && !strstr(sec, "EAP"))
|
||||
snprintf(sec, sizeof sec, "Open");
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 去重:同一 BSSID 只留一条,优先具名 SSID */
|
||||
int idx = -1;
|
||||
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
|
||||
@@ -323,6 +339,7 @@ int net_wifi_scan(meow_net_t *out, int max)
|
||||
named[idx] = has;
|
||||
out[idx].id = -1;
|
||||
snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid); /* 保留 BSSID 供详情展示 */
|
||||
snprintf(out[idx].sec, sizeof out[idx].sec, "%s", sec);
|
||||
if (has) {
|
||||
/* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */
|
||||
char raw[96];
|
||||
@@ -645,23 +662,50 @@ static int g_net_scan_n = 0;
|
||||
static int g_net_bt_n = 0;
|
||||
|
||||
|
||||
/* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 WiFi 扫描触发) */
|
||||
/* ---- 设置页异步网络刷新(进设置不再在入口同步跑子进程查询)----
|
||||
* 背景:进设置页此前在入口同步跑 s_net_refresh()——wpa_cli/bluetoothctl/hcitool 逐条
|
||||
* popen(各自带 1.2~2.5s 有界超时),5 条串行叠加达数秒,真机表现为「点设置卡 ≥5s」。
|
||||
* 修法:入口只 draw + 触发扫描(快),wifi/bt 状态与列表改由 run_settings_page 主循环
|
||||
* 逐步骤 pump(每步一个带超时 popen,数据变化即 need_paint)。s_net_refresh() 保留同步
|
||||
* 语义(kick + drain)供需要立即结果的调用方(通知中心等)使用。
|
||||
* 步骤:1=SSID 2=已保存 3=附近WiFi 4=蓝牙角色 5=附近蓝牙(各写 g_net_* 与 g_bt_mode)。 */
|
||||
static int g_net_rstep = 0; /* 0=idle;1..N=步骤机位置 */
|
||||
|
||||
void s_net_refresh_kick(void)
|
||||
{
|
||||
net_wifi_scan_trigger(); /* 异步发起扫描(wpa_cli scan 快返回) */
|
||||
net_bt_scan_trigger(); /* 异步发起 BT 附近扫描(& 立即返回) */
|
||||
g_net_rstep = 1;
|
||||
}
|
||||
|
||||
int s_net_refresh_pump(void)
|
||||
{
|
||||
meow_net_t tmp[S_NET_MAX];
|
||||
switch (g_net_rstep) {
|
||||
case 1:
|
||||
g_net_rstep = 2;
|
||||
if (wlan_connected()) {
|
||||
char ss[64]; net_wifi_ssid(ss, sizeof ss);
|
||||
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
|
||||
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
|
||||
} else {
|
||||
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
|
||||
}
|
||||
return 1;
|
||||
case 2: g_net_rstep = 3; g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX); return 1;
|
||||
case 3: g_net_rstep = 4; g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX); return 1;
|
||||
case 4: g_net_rstep = 5; bt_role_refresh(1); return 1; /* 蓝牙角色 → g_bt_mode */
|
||||
case 5: g_net_rstep = 0; g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); return 1;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 刷新网络信息缓存(同步:kick + 排空全部异步步骤;需要立即结果的调用方用)。
|
||||
* 进设置页走 s_net_sync_from_sys()(非阻塞:toggle 同步 + kick,由主循环 pump)。 */
|
||||
void s_net_refresh(void)
|
||||
{
|
||||
if (wlan_connected()) {
|
||||
char ss[64]; net_wifi_ssid(ss, sizeof ss);
|
||||
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
|
||||
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
|
||||
} else {
|
||||
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
|
||||
}
|
||||
net_wifi_scan_trigger(); /* 异步发起扫描,稍后附近/计数更接近真实 */
|
||||
net_bt_scan_trigger(); /* M2.40:异步发起蓝牙附近扫描(写 /tmp 缓存,不阻塞) */
|
||||
bt_role_refresh(1); /* M2.48:进入设置/网络动作时立即重探蓝牙角色,本行/状态栏最新 */
|
||||
meow_net_t tmp[S_NET_MAX];
|
||||
g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX);
|
||||
g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX);
|
||||
g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); /* 附近(已配对+扫描发现) */
|
||||
s_net_refresh_kick();
|
||||
while (s_net_refresh_pump()) { /* drain: 同步排空所有异步步骤 */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -677,6 +721,11 @@ int s_net_get(const srow_t *r, char *buf, int len)
|
||||
case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0;
|
||||
case NET_LIST_WIFI_PAGE: snprintf(buf, len, "%s", g_net_ssid); return 0; /* M2.76:子页入口摘要 */
|
||||
case NET_LIST_BT_PAGE: snprintf(buf, len, "%s", g_bt_mode); return 0; /* M2.76 */
|
||||
case NET_LIST_PIN_PAGE: { /* M6.x:锁屏 PIN 入口摘要 */
|
||||
if (!lck_pin_configured()) { snprintf(buf, len, "未设置"); }
|
||||
else if (lck_lock_remaining() > 0) { snprintf(buf, len, "锁定中"); }
|
||||
else { snprintf(buf, len, "已设置"); }
|
||||
return 0; }
|
||||
}
|
||||
snprintf(buf, len, "-");
|
||||
return 0;
|
||||
@@ -712,7 +761,8 @@ void s_net_sync_from_sys(void)
|
||||
int real = hci_powered() ? 1 : 0;
|
||||
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
|
||||
}
|
||||
s_net_refresh();
|
||||
/* 只同步开关(快)+ 异步 kick;状态/列表由 run_settings_page 主循环 pump,绝不阻塞入口。 */
|
||||
s_net_refresh_kick();
|
||||
}
|
||||
|
||||
|
||||
@@ -776,6 +826,6 @@ void s_net_open(const srow_t *r)
|
||||
default:
|
||||
return;
|
||||
}
|
||||
s_net_refresh();
|
||||
s_net_refresh_kick(); /* 异步刷新;run_settings_page 主循环 pump 逐步骤填充 */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
/* netmgr_ipc.c — MeowOS launcher → meow_netmgrd IPC 客户端(M6.0 设置页迁移,NETMGR_DESIGN §12)。
|
||||
*
|
||||
* 【迁移注释】wifi_page/bt_page 的 WiFi/BT 查询与操作统一走 netmgrd IPC(socket
|
||||
* /usr/data/meow_net.sock,env MEOW_NET_SOCK 覆盖)。netmgrd 是 WiFi/BT 操作权威
|
||||
* (IPC 0xE4 WIFI_CTL / 0xE6 BT_CTL);本文件是「设置页客户端 → 权威 IPC」的唯一入口。
|
||||
* 【双轨过渡】netmgrd 不可达(NETMGR_UNREACH)时,本文件回退到 net.c 既有的直连
|
||||
* wpa_cli/hciconfig/busybox 读取(保功能);状态栏物理键语义、sdk/amsd/netmgrd 本体不动。
|
||||
*
|
||||
* 鉴权:跟 pm_power_ipc 同款 —— 先 MSG_HELLO(app_id=os.meow.launcher,netmgrd 按
|
||||
* SO_PEERCRED 认出 /usr/bin/meow_launcherd = 系统身份,写级放行),再发 CTL,收 RESULT。
|
||||
* 只读观测(wlan_connected()/hci_powered() 读 /sys 链路态)仍在 net.c;此处只接权威操作。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
|
||||
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
|
||||
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
|
||||
|
||||
/* IPC 读超时(ms)。netmgrd 单线程 poll,connect/connecth 会阻塞 ~10s/~15s 有界等待
|
||||
* wpa_state=COMPLETED(bug1),scan 内含 1.5s 触发等待(P3)——客户端 per-command 超时需
|
||||
* 覆盖这些同步等待,其余读级子命令维持 1.5s 不被卡死。 */
|
||||
#define NM_TMO_DFL 1500
|
||||
#define NM_TMO_SCAN 5000
|
||||
#define NM_TMO_CONNECT 12000
|
||||
#define NM_TMO_CONNECTH 17000
|
||||
|
||||
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
|
||||
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
|
||||
static time_t g_nm_ok_ts = 0;
|
||||
|
||||
static void nm_cache(int ok) { g_nm_ok = ok; g_nm_ok_ts = time(NULL); }
|
||||
|
||||
/* ---- socket 连接 ---- */
|
||||
static int nm_sock(void)
|
||||
{
|
||||
const char *sock = getenv("MEOW_NET_SOCK");
|
||||
if (!sock || !sock[0]) sock = NETMGR_SOCK_DEFAULT;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, sock, sizeof a.sun_path - 1);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* ---- 帧收发(复刻 power_menu.c pw_xchg:读超时 1.5s,绝不让子页主循环卡死)---- */
|
||||
static int nm_w_all(int fd, const uint8_t *b, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) { ssize_t w = write(fd, b + off, n - off); if (w <= 0) return -1; off += (size_t)w; }
|
||||
return 0;
|
||||
}
|
||||
static int nm_r_all(int fd, uint8_t *b, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) { ssize_t r = read(fd, b + off, n - off); if (r <= 0) return -1; off += (size_t)r; }
|
||||
return 0;
|
||||
}
|
||||
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
|
||||
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
||||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||||
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
|
||||
memcpy(hdr + 8, &req, 4); memcpy(hdr + 12, &rlen, 4);
|
||||
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
|
||||
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
|
||||
struct pollfd pf = { fd, POLLIN, 0 };
|
||||
int pr = poll(&pf, 1, tmo_ms);
|
||||
if (pr <= 0) return -1;
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
|
||||
uint32_t rmagic, rlen2; uint16_t rver, rt;
|
||||
memcpy(&rmagic, rhdr, 4); memcpy(&rver, rhdr + 4, 2); memcpy(&rt, rhdr + 6, 2);
|
||||
memcpy(&rlen2, rhdr + 12, 4);
|
||||
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return -1;
|
||||
if (rlen2 > rcap) rlen2 = rcap;
|
||||
if (rlen2 && nm_r_all(fd, rbuf, rlen2) < 0) return -1;
|
||||
if (rtype) *rtype = rt;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int nm_hello(int fd)
|
||||
{
|
||||
m_hello_t h;
|
||||
memset(&h, 0, sizeof h);
|
||||
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
|
||||
h.version = 1;
|
||||
uint8_t rbuf[256]; uint16_t rtype = 0;
|
||||
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype, NM_TMO_DFL) < 0 || rtype != MSG_ACK)
|
||||
return -1;
|
||||
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
|
||||
return (ak.code == MEOW_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
|
||||
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。 */
|
||||
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
|
||||
char *text, int tlen, int tmo_ms)
|
||||
{
|
||||
m_net_cmd_t q;
|
||||
memset(&q, 0, sizeof q);
|
||||
snprintf(q.cmd, sizeof q.cmd, "%s", cmd ? cmd : "");
|
||||
if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg);
|
||||
int fd = nm_sock();
|
||||
if (fd < 0) { nm_cache(0); return NETMGR_UNREACH; }
|
||||
if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; }
|
||||
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
|
||||
memset(rbuf, 0, sizeof rbuf);
|
||||
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype, tmo_ms) < 0 || rtype != res_type) {
|
||||
close(fd); nm_cache(0); return NETMGR_UNREACH;
|
||||
}
|
||||
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
|
||||
close(fd);
|
||||
nm_cache(1);
|
||||
uint32_t tl = res.text_len;
|
||||
if (tl >= (uint32_t)tlen) tl = (uint32_t)(tlen - 1);
|
||||
if (text && tlen > 0) { memcpy(text, res.text, tl); text[tl] = 0; }
|
||||
return (int)res.code;
|
||||
}
|
||||
|
||||
int netmgr_wifi_tmo(const char *cmd, const char *arg, char *text, int tlen, int tmo_ms)
|
||||
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen, tmo_ms); }
|
||||
|
||||
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
|
||||
{ return netmgr_wifi_tmo(cmd, arg, text, tlen, NM_TMO_DFL); }
|
||||
|
||||
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen)
|
||||
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen, NM_TMO_DFL); }
|
||||
|
||||
int netmgr_ok(void)
|
||||
{
|
||||
if (g_nm_ok >= 0 && time(NULL) - g_nm_ok_ts < 1) return g_nm_ok;
|
||||
int fd = nm_sock();
|
||||
if (fd < 0) { nm_cache(0); return 0; }
|
||||
close(fd); nm_cache(1); return 1;
|
||||
}
|
||||
|
||||
|
||||
/* ================= WiFi(IPC 优先,net.c 直连回退)================= */
|
||||
|
||||
/* —— net.c 的 wpa_decode_ssid 是 static,这里复制最小实现(本模块自含,避免改 net.c 可见性)—— */
|
||||
static int nm_hex(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
static void nm_decode_ssid(const char *src, char *dst, int dlen)
|
||||
{
|
||||
int o = 0;
|
||||
while (*src && o < dlen - 1) {
|
||||
if (src[0] == '\\' && src[1] == 'x' && nm_hex(src[2]) >= 0 && nm_hex(src[3]) >= 0) {
|
||||
dst[o++] = (char)((nm_hex(src[2]) << 4) | nm_hex(src[3]));
|
||||
src += 4;
|
||||
} else {
|
||||
dst[o++] = *src++;
|
||||
}
|
||||
}
|
||||
dst[o] = 0;
|
||||
}
|
||||
|
||||
/* P1:拷一个「空格分隔的值 token」到 dst(a2dp 字段 extraction 用)。 */
|
||||
static void nm_val_field(char **p, char *dst, int dlen)
|
||||
{
|
||||
while (**p == ' ' || **p == '\t') (*p)++;
|
||||
int o = 0;
|
||||
while (**p && **p != ' ' && **p != '\t' && **p != '\n' && o < dlen - 1) dst[o++] = *(*p)++;
|
||||
dst[o] = 0;
|
||||
}
|
||||
|
||||
/* —— netmgrd `wifi status` 单行摘要解析:`wifi state=X connected=N ssid=S ip=I rssi=R`。
|
||||
* 已知字段以「空格+key=」分隔;ssid 可能含空格,需读到下一个已知字段为止。 —— */
|
||||
static int nm_is_status_key(const char *p)
|
||||
{
|
||||
if (!strncmp(p, " ip=", 4) || !strncmp(p, " rssi=", 6) ||
|
||||
!strncmp(p, " connected=", 11) || !strncmp(p, " state=", 7)) return 1;
|
||||
return 0;
|
||||
}
|
||||
static const char *nm_val(const char *text, const char *key, char *dst, int dlen, int space_ok)
|
||||
{
|
||||
const char *p = strstr(text, key);
|
||||
if (!p) { dst[0] = 0; return p; }
|
||||
p += strlen(key);
|
||||
int o = 0;
|
||||
while (*p && o < dlen - 1) {
|
||||
if (*p == ' ') {
|
||||
if (!space_ok) break;
|
||||
if (nm_is_status_key(p)) break; /* ssid 含空格:停在下一个已知字段 */
|
||||
}
|
||||
dst[o++] = *p++;
|
||||
}
|
||||
dst[o] = 0;
|
||||
return p;
|
||||
}
|
||||
static int nm_parse_connected(const char *text)
|
||||
{
|
||||
char v[8];
|
||||
nm_val(text, "connected=", v, sizeof v, 0);
|
||||
return atoi(v);
|
||||
}
|
||||
|
||||
int nm_wifi_connected(void)
|
||||
{
|
||||
char text[256];
|
||||
int rc = netmgr_wifi("status", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return wlan_connected();
|
||||
if (rc != MEOW_OK) return 0;
|
||||
return nm_parse_connected(text);
|
||||
}
|
||||
|
||||
void nm_wifi_ssid(char *buf, int len)
|
||||
{
|
||||
buf[0] = 0;
|
||||
char text[256];
|
||||
int rc = netmgr_wifi("status", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) { net_wifi_ssid(buf, len); return; }
|
||||
if (rc != MEOW_OK) return;
|
||||
if (!nm_parse_connected(text)) return;
|
||||
char raw[128];
|
||||
nm_val(text, "ssid=", raw, sizeof raw, 1);
|
||||
nm_decode_ssid(raw, buf, len);
|
||||
}
|
||||
|
||||
int nm_wifi_info(char (*info)[48], int max)
|
||||
{
|
||||
char text[256];
|
||||
int rc = netmgr_wifi("status", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return net_wifi_info(info, max); /* 回退:完整详情(直连 wpa_cli+ip route) */
|
||||
if (rc != MEOW_OK || !nm_parse_connected(text)) return 0;
|
||||
char ssid[40] = "", ip[32] = "", rssi[16] = "";
|
||||
char raw[128];
|
||||
nm_val(text, "ssid=", raw, sizeof raw, 1);
|
||||
nm_decode_ssid(raw, ssid, sizeof ssid);
|
||||
nm_val(text, "ip=", ip, sizeof ip, 0);
|
||||
nm_val(text, "rssi=", rssi, sizeof rssi, 0);
|
||||
int n = 0;
|
||||
if (ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid);
|
||||
if (ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip);
|
||||
if (rssi[0]) snprintf(info[n++], 48, "信号: %s dBm", rssi);
|
||||
if (n == 0) snprintf(info[n++], 48, "已连接(无详情)");
|
||||
return n;
|
||||
}
|
||||
|
||||
/* —— 解析 netmgrd `wifi networks`(wpa_cli list_networks 原样文本)→ meow_net_t[];
|
||||
* 直接复用 net.c 既有表头跳行逻辑。返回项数。 —— */
|
||||
static int nm_parse_saved(const char *txt, meow_net_t *out, int max)
|
||||
{
|
||||
int n = 0;
|
||||
if (!txt || max <= 0) return 0;
|
||||
if (max > S_NET_MAX) max = S_NET_MAX;
|
||||
const char *line = strchr(txt, '\n');
|
||||
if (line) line++;
|
||||
while (line && *line && n < max) {
|
||||
const char *eol = strchr(line, '\n');
|
||||
const char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p && *p != '#') {
|
||||
int id = atoi(p);
|
||||
const char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++;
|
||||
while (*sp == ' ' || *sp == '\t') sp++;
|
||||
const char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++;
|
||||
const char *bp = se; while (*bp == ' ' || *bp == '\t') bp++;
|
||||
const char *be = bp; while (*be && *be != ' ' && *be != '\t' && *be != '\n' && *be != '\r') be++;
|
||||
if (sp != se) {
|
||||
char raw[160];
|
||||
int l = (int)(se - sp);
|
||||
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||||
memcpy(raw, sp, (size_t)l); raw[l] = 0;
|
||||
out[n].id = id;
|
||||
nm_decode_ssid(raw, out[n].name, sizeof out[n].name);
|
||||
if (be > bp && (be - bp) < (int)sizeof out[n].mac) {
|
||||
memcpy(out[n].mac, bp, (size_t)(be - bp)); out[n].mac[be - bp] = 0;
|
||||
}
|
||||
out[n].sec[0] = 0;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
line = eol ? eol + 1 : NULL;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* —— 解析 netmgrd `wifi scan`(wpa_cli scan_results 原样文本)→ meow_net_t[](按 BSSID 去重、
|
||||
* 优先具名;补充 sec 安全类型供「新网络录入」判断是否弹 IME)。返回项数。 —— */
|
||||
static int nm_parse_scan(const char *txt, meow_net_t *out, int max)
|
||||
{
|
||||
int n = 0;
|
||||
if (!txt || max <= 0) return 0;
|
||||
if (max > S_NET_MAX) max = S_NET_MAX;
|
||||
char seen_bssid[S_NET_MAX][24];
|
||||
int named[S_NET_MAX] = {0};
|
||||
const char *line = strchr(txt, '\n');
|
||||
if (line) line++;
|
||||
while (line && *line && n < max) {
|
||||
const char *eol = strchr(line, '\n');
|
||||
const char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p) {
|
||||
char bssid[24] = "";
|
||||
{ const char *b = p; while (*p && *p != ' ' && *p != '\t') p++;
|
||||
int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; }
|
||||
const char *ssid = "";
|
||||
char sec[16] = "Open";
|
||||
const char *br = strchr(p, '[');
|
||||
if (br) {
|
||||
/* 安全类型 = 首个方括号内容([WPA-PSK-CCMP] 等);纯 [ESS]/[IBSS] = 开放 */
|
||||
const char *be = strchr(br + 1, ']');
|
||||
if (be) {
|
||||
int sl = (int)(be - (br + 1));
|
||||
if (sl > 15) sl = 15;
|
||||
if (sl > 0) {
|
||||
memcpy(sec, br + 1, (size_t)sl); sec[sl] = 0;
|
||||
if (!strstr(sec, "WPA") && !strstr(sec, "RSN") &&
|
||||
!strstr(sec, "WEP") && !strstr(sec, "SAE") && !strstr(sec, "EAP"))
|
||||
snprintf(sec, sizeof sec, "Open");
|
||||
}
|
||||
}
|
||||
const char *q = br;
|
||||
while (*q) {
|
||||
while (*q == ' ' || *q == '\t') q++;
|
||||
if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; }
|
||||
else break;
|
||||
}
|
||||
ssid = q; while (*ssid == ' ' || *ssid == '\t') ssid++;
|
||||
}
|
||||
const char *se = ssid;
|
||||
while (*se && *se != '\n' && *se != '\r') se++;
|
||||
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
|
||||
int has = (se > ssid && *ssid);
|
||||
int idx = -1;
|
||||
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
|
||||
if (idx < 0) {
|
||||
if (n >= max) { line = eol ? eol + 1 : NULL; continue; }
|
||||
idx = n++;
|
||||
strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1);
|
||||
seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0;
|
||||
named[idx] = has;
|
||||
out[idx].id = -1;
|
||||
snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid);
|
||||
snprintf(out[idx].sec, sizeof out[idx].sec, "%s", sec);
|
||||
if (has) {
|
||||
char raw[96];
|
||||
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||||
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
|
||||
nm_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
|
||||
} else {
|
||||
snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)");
|
||||
}
|
||||
} else if (has && !named[idx]) {
|
||||
char raw[96];
|
||||
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||||
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
|
||||
nm_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
|
||||
named[idx] = 1;
|
||||
}
|
||||
}
|
||||
line = eol ? eol + 1 : NULL;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int nm_wifi_saved(meow_net_t *out, int max)
|
||||
{
|
||||
char text[MEOW_IPC_MAX - 32];
|
||||
int rc = netmgr_wifi("networks", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return net_wifi_saved(out, max); /* 直连回退 */
|
||||
if (rc != MEOW_OK) return 0;
|
||||
return nm_parse_saved(text, out, max);
|
||||
}
|
||||
|
||||
int nm_wifi_scan(meow_net_t *out, int max)
|
||||
{
|
||||
char text[MEOW_IPC_MAX - 32];
|
||||
int rc = netmgr_wifi_tmo("scan", NULL, text, sizeof text, NM_TMO_SCAN); /* netmgrd 内含触发+有界等待+读结果 */
|
||||
if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max);
|
||||
if (rc != MEOW_OK) return 0;
|
||||
return nm_parse_scan(text, out, max);
|
||||
}
|
||||
|
||||
void nm_wifi_scan_trigger(void)
|
||||
{
|
||||
/* IPC:scan 子命令在 netmgrd 内已一次性「触发+等待+读结果」,此处无需单独触发;
|
||||
* 直连回退:异步 wpa_cli scan(结果稍后 net_wifi_scan 读)。 */
|
||||
if (!netmgr_ok()) net_wifi_scan_trigger();
|
||||
}
|
||||
|
||||
/* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
|
||||
* 匹配既有(\xNN 还原同表示)→ 新建或(命中且带新 psk)更新凭据 → hidden 补 scan_ssid=1 →
|
||||
* enable+select → 有界等待 wpa_state=COMPLETED → 成功 enable_network all + save_config(校验重试);
|
||||
* 失败时本次新建的网络回收,报文明示原因。仅在 netmgrd 不可达时走此路径。 */
|
||||
static const char NM_WPA_PREFIX[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
|
||||
|
||||
static int nm_direct_wpa(char *out, int cap, const char *args, int tmo)
|
||||
{
|
||||
char cmd[384];
|
||||
snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
|
||||
char *o = popen_read_tmo(tmo, cmd);
|
||||
if (!o) { out[0] = 0; return -1; }
|
||||
int l = (int)strlen(o);
|
||||
if (l >= cap) l = cap - 1;
|
||||
memcpy(out, o, (size_t)l); out[l] = 0;
|
||||
free(o);
|
||||
return l;
|
||||
}
|
||||
|
||||
static int nm_direct_save_config(void)
|
||||
{
|
||||
char out[128];
|
||||
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||
nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
|
||||
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int nm_direct_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
|
||||
{
|
||||
char out[1024]; char args[256];
|
||||
int id = -1;
|
||||
if (nm_direct_wpa(out, sizeof out, "list_networks", 1500) > 0) {
|
||||
char *ln = strtok(out, "\n");
|
||||
while (ln) {
|
||||
char *tab0 = strchr(ln, '\t');
|
||||
if (tab0) {
|
||||
int this_id = atoi(ln);
|
||||
char *ss = tab0 + 1;
|
||||
char *tab1 = strchr(ss, '\t');
|
||||
if (tab1) *tab1 = 0;
|
||||
char clean[128], unesc[160], norm[128];
|
||||
size_t cl = strlen(ss);
|
||||
if (cl >= 2 && ss[0] == '"' && ss[cl - 1] == '"') {
|
||||
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
|
||||
} else {
|
||||
snprintf(clean, sizeof clean, "%s", ss);
|
||||
}
|
||||
nm_decode_ssid(clean, unesc, sizeof unesc);
|
||||
char *p = unesc; while (*p == ' ' || *p == '\t') p++;
|
||||
snprintf(norm, sizeof norm, "%s", p);
|
||||
size_t nl = strlen(norm);
|
||||
while (nl > 0 && (norm[nl - 1] == ' ' || norm[nl - 1] == '\t')) norm[--nl] = 0;
|
||||
if (strcmp(norm, ssid) == 0) { id = this_id; break; }
|
||||
}
|
||||
ln = strtok(NULL, "\n");
|
||||
}
|
||||
}
|
||||
int created = 0;
|
||||
if (id < 0) {
|
||||
if (nm_direct_wpa(out, sizeof out, "add_network", 1500) <= 0) {
|
||||
if (err) snprintf(err, elen, "add_network 无 id");
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
id = atoi(out);
|
||||
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
|
||||
created = 1;
|
||||
snprintf(args, sizeof args, "set_network %d ssid '\"%s\"'", id, ssid);
|
||||
nm_direct_wpa(out, sizeof out, args, 1500);
|
||||
if (psk && psk[0]) { snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||
else { snprintf(args, sizeof args, "set_network %d key_mgmt NONE", id); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||
} else if (psk && psk[0]) {
|
||||
snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk);
|
||||
nm_direct_wpa(out, sizeof out, args, 1500);
|
||||
}
|
||||
if (hidden) { snprintf(args, sizeof args, "set_network %d scan_ssid 1", id); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||
snprintf(args, sizeof args, "enable_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
|
||||
snprintf(args, sizeof args, "select_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
|
||||
|
||||
int wait_ms = hidden ? 15000 : 10000;
|
||||
int waited = 0, ok = 0; char ip[40] = "";
|
||||
for (;;) {
|
||||
char st[512];
|
||||
if (nm_direct_wpa(st, sizeof st, "status", 1500) > 0) {
|
||||
char *ws = strstr(st, "wpa_state=");
|
||||
if (ws && !strncmp(ws + 10, "COMPLETED", 9)) {
|
||||
ok = 1;
|
||||
char *ib = strstr(st, "ip_address=");
|
||||
if (ib) { ib += 11; char *ie = ib; while (*ie && *ie != '\n' && *ie != '\r') ie++;
|
||||
int il = (int)(ie - ib); if (il >= (int)sizeof ip) il = (int)sizeof ip - 1;
|
||||
memcpy(ip, ib, (size_t)il); ip[il] = 0; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (waited >= wait_ms) break;
|
||||
usleep(200000);
|
||||
waited += 200;
|
||||
}
|
||||
if (!ok) {
|
||||
if (created) { snprintf(args, sizeof args, "remove_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500); nm_direct_save_config(); }
|
||||
if (err) snprintf(err, elen, "连接失败:密码错误或信号不佳(超时未关联)");
|
||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d timeout", ssid, hidden, id);
|
||||
return MEOW_ERR_NODEV;
|
||||
}
|
||||
nm_direct_wpa(out, sizeof out, "enable_network all", 1500);
|
||||
int persisted = nm_direct_save_config();
|
||||
if (err) snprintf(err, elen, "已连接:%s ip=%s%s", ssid, ip[0] ? ip : "-", persisted ? "" : "(记录未持久化)");
|
||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d ip=%s", ssid, hidden, id, ip[0] ? ip : "-");
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
|
||||
{
|
||||
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
|
||||
char arg[132];
|
||||
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* 带空 psk → netmgrd 复用既有网络 */
|
||||
char text[192];
|
||||
int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
|
||||
hidden ? NM_TMO_CONNECTH : NM_TMO_CONNECT);
|
||||
if (rc == NETMGR_UNREACH) return nm_direct_connect_ex(ssid, psk, hidden, err, elen);
|
||||
if (err && elen > 0) snprintf(err, elen, "%s", text);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
|
||||
{ return nm_wifi_connect_ex(ssid, psk, 0, err, elen); }
|
||||
|
||||
int nm_wifi_disconnect(char *err, int elen)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_wifi("disconnect", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) {
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 disconnect 2>/dev/null");
|
||||
popen_read_tmo(1200, cmd);
|
||||
if (err) snprintf(err, elen, "已下发");
|
||||
return MEOW_OK;
|
||||
}
|
||||
if (err && elen > 0) snprintf(err, elen, "%s", text);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nm_wifi_toggle(int on)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_wifi(on ? "on" : "off", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) { s_net_toggle_wifi(on); return MEOW_OK; } /* 直连回退 */
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/* ================= BT(IPC 优先:全量子命令权威收口 P1;不可达回退直连)================= */
|
||||
|
||||
static int nm_parse_devices(const char *txt, meow_net_t *out, int max); /* 前向声明 */
|
||||
|
||||
int nm_bt_powered(void)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_bt("status", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return hci_powered();
|
||||
if (rc != MEOW_OK) return 0;
|
||||
char v[8];
|
||||
nm_val(text, "powered=", v, sizeof v, 0);
|
||||
return atoi(v);
|
||||
}
|
||||
|
||||
int nm_bt_onoff(int on)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_bt(on ? "on" : "off", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) { s_net_toggle_bt(on); return MEOW_OK; }
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen)
|
||||
{
|
||||
char text[192];
|
||||
int rc = netmgr_bt("status", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) { /* 直连回退:读内核/tool */
|
||||
if (powered) *powered = hci_powered();
|
||||
if (paired_n) *paired_n = -1;
|
||||
if (addr && alen > 0) addr[0] = 0;
|
||||
return rc;
|
||||
}
|
||||
if (rc == MEOW_OK) {
|
||||
char v[8];
|
||||
if (powered) { nm_val(text, "powered=", v, sizeof v, 0); *powered = atoi(v); }
|
||||
if (paired_n) { nm_val(text, "paired=", v, sizeof v, 0); *paired_n = atoi(v); }
|
||||
if (addr && alen > 0) nm_val(text, "address=", addr, alen, 0);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nm_bt_paired(meow_net_t *out, int max)
|
||||
{
|
||||
char text[MEOW_IPC_MAX - 32];
|
||||
int rc = netmgr_bt("paired", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return net_bt_paired(out, max);
|
||||
if (rc != MEOW_OK) return 0;
|
||||
return nm_parse_devices(text, out, max);
|
||||
}
|
||||
|
||||
/* ---- P1 收口:连接/断开/配对/扫描/A2DP 全部迁 netmgrd(权威),不可达回退直连 ---- */
|
||||
|
||||
/* 通用 `Device <MAC> <Name>` 行解析(netmgrd bt {connected,paired,devices} 文本;launcher 只做客户端)。 */
|
||||
static int nm_parse_devices(const char *txt, meow_net_t *out, int max)
|
||||
{
|
||||
int n = 0;
|
||||
if (!txt || max <= 0) return 0;
|
||||
if (max > S_NET_MAX) max = S_NET_MAX;
|
||||
char *line = (char *)txt;
|
||||
while (line && *line && n < max) {
|
||||
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||||
char *p = strstr(line, "Device ");
|
||||
if (p) {
|
||||
p += 7;
|
||||
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
|
||||
char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++;
|
||||
int ml = (int)(mac - p); if (ml > 47) ml = 47;
|
||||
if (ml > 0 && ml < (int)sizeof out[n].mac) {
|
||||
memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0;
|
||||
}
|
||||
snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p);
|
||||
out[n].id = -1; out[n].sec[0] = 0;
|
||||
n++;
|
||||
}
|
||||
line = eol ? eol + 1 : (char *)0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int nm_bt_connected(meow_net_t *out, int max)
|
||||
{
|
||||
char text[MEOW_IPC_MAX - 32];
|
||||
int rc = netmgr_bt("connected", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return net_bt_connected(out, max); /* 直连回退 */
|
||||
if (rc != MEOW_OK) return 0;
|
||||
return nm_parse_devices(text, out, max);
|
||||
}
|
||||
|
||||
int nm_bt_devices(meow_net_t *out, int max)
|
||||
{
|
||||
char text[MEOW_IPC_MAX - 32];
|
||||
int rc = netmgr_bt("devices", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return net_bt_scan(out, max); /* 直连回退(读扫描缓存) */
|
||||
if (rc != MEOW_OK) return 0;
|
||||
return nm_parse_devices(text, out, max);
|
||||
}
|
||||
|
||||
/* A2DP 端点解析:"A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=.. role=.." */
|
||||
int nm_bt_a2dp(bt_ep_t *eps, int max)
|
||||
{
|
||||
if (max <= 0) return 0;
|
||||
char text[MEOW_IPC_MAX - 32];
|
||||
int rc = netmgr_bt("a2dp", NULL, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH || rc != MEOW_OK) return 0; /* A2DP 详情无直连回退(P2 bluealsa 才接) */
|
||||
int n = 0;
|
||||
char *line = text;
|
||||
while (line && *line && n < max) {
|
||||
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||||
if (!strncmp(line, "A2DP ", 5)) {
|
||||
char *p = line + 5;
|
||||
bt_ep_t *e = &eps[n];
|
||||
memset(e, 0, sizeof *e);
|
||||
snprintf(e->role, sizeof e->role, "%s", "-");
|
||||
e->acquired = 0;
|
||||
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n') mac++;
|
||||
int ml = (int)(mac - p); if (ml > 23) ml = 23;
|
||||
if (ml > 0) { memcpy(e->mac, p, (size_t)ml); e->mac[ml] = 0; }
|
||||
char *t = mac;
|
||||
while (*t) {
|
||||
while (*t == ' ' || *t == '\t') t++;
|
||||
if (!strncmp(t, "codec=", 6)) { t += 6; nm_val_field(&t, e->codec, sizeof e->codec); }
|
||||
else if (!strncmp(t, "rate=", 5)) { t += 5; nm_val_field(&t, e->rate, sizeof e->rate); }
|
||||
else if (!strncmp(t, "channels=", 9)) { t += 9; nm_val_field(&t, e->chn, sizeof e->chn); }
|
||||
else if (!strncmp(t, "bits=", 5)) { t += 5; nm_val_field(&t, e->bits, sizeof e->bits); }
|
||||
else if (!strncmp(t, "acquired=", 9)) { t += 9; e->acquired = atoi(t); }
|
||||
else if (!strncmp(t, "role=", 5)) { t += 5; nm_val_field(&t, e->role, sizeof e->role); }
|
||||
while (*t && *t != ' ' && *t != '\t' && *t != '\n') t++;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
line = eol ? eol + 1 : NULL;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 直连回退:bluetoothctl <sub> <mac>(带超时)。返回 MEOW_OK/MEOW_ERR_*。 */
|
||||
static int bt_direct_cmd(const char *sub, const char *mac)
|
||||
{
|
||||
if (!mac || !mac[0]) return MEOW_ERR_INVAL;
|
||||
char cmd[192];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl %s %s 2>/dev/null", sub, mac);
|
||||
char *o = popen_read_tmo(8000, cmd);
|
||||
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
|
||||
strstr(o, "already") || strstr(o, "Paired"));
|
||||
if (o) free(o);
|
||||
return ok ? MEOW_OK : MEOW_ERR_NODEV;
|
||||
}
|
||||
|
||||
int nm_bt_connect(const char *mac)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_bt("connect", mac, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return bt_direct_cmd("connect", mac);
|
||||
return rc;
|
||||
}
|
||||
int nm_bt_disconnect(const char *mac)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_bt("disconnect", mac, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return bt_direct_cmd("disconnect", mac);
|
||||
return rc;
|
||||
}
|
||||
int nm_bt_pair(const char *mac)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_bt("pair", mac, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return bt_direct_cmd("pair", mac);
|
||||
return rc;
|
||||
}
|
||||
int nm_bt_unpair(const char *mac)
|
||||
{
|
||||
char text[128];
|
||||
int rc = netmgr_bt("unpair", mac, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return bt_direct_cmd("remove", mac);
|
||||
return rc;
|
||||
}
|
||||
void nm_bt_scan_on(void)
|
||||
{
|
||||
if (!netmgr_ok()) { net_bt_scan_trigger(); return; } /* 直连回退 */
|
||||
netmgr_bt("scan", "on", NULL, 0);
|
||||
}
|
||||
void nm_bt_scan_off(void)
|
||||
{
|
||||
if (!netmgr_ok()) return;
|
||||
netmgr_bt("scan", "off", NULL, 0);
|
||||
}
|
||||
@@ -23,13 +23,14 @@ int g_notif_ctrl_died = 0;
|
||||
static int g_ncp_dnd = 0;
|
||||
|
||||
|
||||
/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。
|
||||
* g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。
|
||||
* run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint)
|
||||
* 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */
|
||||
/* ⑦ 行左滑删除的绘制态(供旧手绘 ncp_draw_list / ncp_repaint_row 画该行位移)——M7.3 已迁 SDK
|
||||
* meow_listview 左滑删除(SDK 内部持有 swipe_idx/swipe_dx,不再需要本组全局)。
|
||||
* 保留在 #if 0 回退区内(随旧手绘内层一起重出)。 */
|
||||
#if 0
|
||||
static int g_ncp_swipe_row = -1;
|
||||
|
||||
static int g_ncp_swipe_off = 0;
|
||||
#endif
|
||||
|
||||
|
||||
/* ---- 阶段D附录B:通知中心顶部媒体卡片(固定于通知列表上方,活动媒体会话时显示)---- */
|
||||
@@ -59,6 +60,58 @@ static int ncp_list_view(void) { int v = NCP_LIST_BOT - ncp_list_top(); return v
|
||||
#define NCP_MEDIA_BTN_R 22 /* play 半径;prev/next 略小 */
|
||||
#define NCP_MEDIA_PROG_Y 74 /* 相对卡片顶的进度条 top */
|
||||
|
||||
/* M7.0b:媒体卡片三控制按钮改用 SDK meow_iconbtn + sb_icons 矢量图标(与 Now Playing 同源),
|
||||
* 取代原先手绘三角/竖条(ncp_media_tri/ncp_media_btn,用户反馈手绘有 bug)。up: 圆钮尺寸由
|
||||
* meow_iconbtn 的 size 决定(ink=0.6*size),此处 prev/next 用 NCP_MEDIA_BTN_R-4、play 用
|
||||
* NCP_MEDIA_BTN_R(与旧几何一致,命中仍按三圆心——ncp_media_card_tap 不变)。控件 lazy 建一次,
|
||||
* owner=哨兵 0x52(不与电源菜单 0x50 / Now Playing 0x51 冲突);绘制经 meow_widget_draw。 */
|
||||
#define NCP_MEDIA_ACT 0x52
|
||||
#define NCP_MEDIA_PREV_R (NCP_MEDIA_BTN_R - 4) /* 18 */
|
||||
#define NCP_MEDIA_NEXT_R (NCP_MEDIA_BTN_R - 4) /* 18 */
|
||||
static meow_widget *g_ncp_btn_prev = NULL, *g_ncp_btn_play = NULL;
|
||||
static meow_widget *g_ncp_btn_pause = NULL, *g_ncp_btn_next = NULL;
|
||||
static int g_ncp_media_btn_init = 0;
|
||||
|
||||
/* 创建三控制按钮(owner 钉哨兵;play/pause 同位叠两钮,按状态只画其一——同 now_playing)。 */
|
||||
static void ncp_media_btns_create(void)
|
||||
{
|
||||
meow_ui_activity_push(NCP_MEDIA_ACT);
|
||||
int by = NCP_LIST_TOP + NCP_MEDIA_BTN_Y; /* 三钮圆心 y(与 ncp_draw_media_card/ncp_media_card_tap 同源) */
|
||||
g_ncp_btn_prev = meow_iconbtn_create(NCP_MEDIA_PREV_CX - NCP_MEDIA_PREV_R, by - NCP_MEDIA_PREV_R,
|
||||
NCP_MEDIA_PREV_R * 2, MEOW_ICON_SKIP_PREV);
|
||||
g_ncp_btn_play = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
|
||||
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PLAY_ARROW);
|
||||
g_ncp_btn_pause = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
|
||||
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PAUSE);
|
||||
g_ncp_btn_next = meow_iconbtn_create(NCP_MEDIA_NEXT_CX - NCP_MEDIA_NEXT_R, by - NCP_MEDIA_NEXT_R,
|
||||
NCP_MEDIA_NEXT_R * 2, MEOW_ICON_SKIP_NEXT);
|
||||
/* 主播放钮保持「强调动作」观感(与 Now Playing 一致):底色=primary、图标=onPrimary;其余默认。 */
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_widget_opts op; memset(&op, 0, sizeof op);
|
||||
op.bg = th->primary; op.fg = th->onPrimary;
|
||||
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
|
||||
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
|
||||
meow_ui_activity_pop();
|
||||
g_ncp_media_btn_init = 1;
|
||||
}
|
||||
|
||||
/* 绘制三控制按钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时新圆底盖掉旧)。
|
||||
* 每次刷新播放钮底色/图标(同 now_playing np_theme_widgets:跨主题切换跟手)。 */
|
||||
static void ncp_draw_media_btns(const meow_media_session_t *s)
|
||||
{
|
||||
if (!g_ncp_media_btn_init) ncp_media_btns_create();
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_widget_opts op; memset(&op, 0, sizeof op);
|
||||
op.bg = th->primary; op.fg = th->onPrimary;
|
||||
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
|
||||
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
|
||||
if (g_ncp_btn_prev) { meow_widget_set_selected(g_ncp_btn_prev, 0); meow_widget_draw(g_ncp_btn_prev); }
|
||||
if (g_ncp_btn_next) { meow_widget_set_selected(g_ncp_btn_next, 0); meow_widget_draw(g_ncp_btn_next); }
|
||||
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
|
||||
meow_widget *pb = playing ? g_ncp_btn_pause : g_ncp_btn_play;
|
||||
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
|
||||
}
|
||||
|
||||
|
||||
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
|
||||
static void ncp_strncpy(char *dst, const char *src, size_t len)
|
||||
@@ -340,7 +393,8 @@ static void ncp_chip_state(int chip, int *on, char *val, int vlen)
|
||||
} else if (chip == NCP_CHIP_THEME) {
|
||||
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
|
||||
*on = (v == 1);
|
||||
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
|
||||
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? meow_str_zh("ncp.theme_light", "浅色")
|
||||
: meow_str_zh("ncp.theme_dark", "深色"));
|
||||
} else if (chip == NCP_CHIP_BRIGHT) {
|
||||
int v = 0; meow_settings_get_int("display.brightness", &v);
|
||||
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
|
||||
@@ -386,11 +440,11 @@ static int ncp_chip_apply(int chip)
|
||||
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
|
||||
if (mnp_brightness_clamp(cur, vmin) < 100) {
|
||||
meow_settings_set_int("display.brightness", 100);
|
||||
write_brightness(100);
|
||||
write_brightness(100, "notif_center bright_chip");
|
||||
g_sys.brightness = 100;
|
||||
} else {
|
||||
meow_settings_set_int("display.brightness", vmin);
|
||||
write_brightness(vmin);
|
||||
write_brightness(vmin, "notif_center bright_chip");
|
||||
g_sys.brightness = vmin;
|
||||
}
|
||||
paint_sb_right();
|
||||
@@ -406,8 +460,199 @@ static int ncp_chip_apply(int chip)
|
||||
}
|
||||
|
||||
|
||||
/* ==================================================================== */
|
||||
/* M7.3:通知中心内层迁 SDK 控件(芯片/双滑条/勿扰开关/通知列表/左滑删除) */
|
||||
/* --------------------------------------------------------------------
|
||||
* 定位:只迁「内层绘制/命中」。叠加会话骨架(SIGSTOP / 输入移交 / 恢复帧 / 顶条热区——见
|
||||
* app_launcher.c run_app_notif_overlay)绝对不动,此处仅把 run_notif_center / draw_notif_center
|
||||
* 中面板体的绘制与行命中交给 SDK meow_widget。SDK meow_ui.a 已定稿(c3d6371b),勿重编直接用。
|
||||
*
|
||||
* owner 钉哨兵 NCP_WIDGET_ACT(不冲突:0x50 电源菜单 / 0x51 锁屏·NowPlaying / 0x52 子页·媒体卡 /
|
||||
* 0x53 设置页)。绘制经 meow_widget_draw / meow_listview_vs_draw;命中:通知列表走
|
||||
* meow_listview_hit / meow_listview_swipe_begin·move·end(SDK 手势判定+删除/回弹动画),
|
||||
* 芯片/滑条/勿扰开关沿用既有人工几何命中(与媒体卡 M7.0b 一致——叠加会话为手绘循环,不跑 SDK
|
||||
* 输入线程,故不依赖 meow_widget_hit 的 owner==active 闸门)。 */
|
||||
#define NCP_WIDGET_ACT 0x54
|
||||
static meow_widget *g_ncp_lv = NULL; /* 通知列表:listview 虚拟化 + 行自绘 + 空态 + 左滑删除 */
|
||||
static meow_widget *g_ncp_sl_bri = NULL; /* 亮度滑条 */
|
||||
static meow_widget *g_ncp_sl_vol = NULL; /* 音量滑条 */
|
||||
static meow_widget *g_ncp_sw_dnd = NULL; /* 勿扰开关 */
|
||||
static meow_widget *g_ncp_chip[NCP_CHIP_COUNT]; /* 5 个快捷 chip(自定义画布:圆角 + 图标 + 标签) */
|
||||
static int g_ncp_widget_init = 0;
|
||||
static int g_ncp_need_paint = 0; /* M7.3:行左滑删除回调置位 → 主循环整面板重画 */
|
||||
|
||||
/* ---- 通知列表虚拟化源:count=当前行数(动态,逐帧实时查);bind 仅填 enabled 供
|
||||
* meow_listview_hit 判定(行内容经 set_item_draw 自绘直读 g_rows[],无池缓存过期问题)。 ---- */
|
||||
static int ncp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
|
||||
static void ncp_lv_bind(int idx, meow_lv_item *it, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
memset(it, 0, sizeof *it);
|
||||
it->enabled = (idx >= 0 && idx < g_nrows) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 文本截断(SDK meow_ui_text_width/meow_ui_text;UTF-8 安全,与旧 ncp_text 同构)。 */
|
||||
static void ncp_mt_text(int x, int y, const char *s, int size, uint32_t col, int maxw)
|
||||
{
|
||||
if (!s) s = "";
|
||||
if (meow_ui_text_width(s, size) <= maxw) { meow_ui_text(x, y, s, size, col); return; }
|
||||
int ell = meow_ui_text_width("...", size);
|
||||
int limit = maxw - ell;
|
||||
char buf[128]; int len = 0;
|
||||
while (s[len] && len < (int)sizeof buf - 8) {
|
||||
unsigned char c0 = (unsigned char)s[len];
|
||||
int cp = 1;
|
||||
if ((c0 & 0xE0) == 0xC0) cp = 2;
|
||||
else if ((c0 & 0xF0) == 0xE0) cp = 3;
|
||||
else if ((c0 & 0xF8) == 0xF0) cp = 4;
|
||||
if (len + cp >= (int)sizeof buf - 1) break;
|
||||
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
|
||||
buf[len + cp] = 0;
|
||||
if (meow_ui_text_width(buf, size) > limit) { buf[len] = 0; break; }
|
||||
len += cp;
|
||||
}
|
||||
if (len > 0) {
|
||||
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
|
||||
meow_ui_text(x, y, buf, size, col);
|
||||
} else {
|
||||
meow_ui_text(x, y, "...", size, col);
|
||||
}
|
||||
}
|
||||
|
||||
/* 通知行自绘(set_item_draw 覆盖默认 renderer)。x 已含左滑位移(SDK 先画右露删除区并把行 x 左移),
|
||||
* 逐元素对齐旧 ncp_draw_row;颜色全取 meow_theme 语义色(M7 定案 #2 单一权威)。 */
|
||||
static void ncp_lv_row_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
||||
{
|
||||
(void)w; (void)state; (void)ctx; /* w=SDK 视口宽;行卡宽用 NCP_ROW_W(含 NCP_ROW_X 两侧留白) */
|
||||
if (idx < 0 || idx >= g_nrows) return;
|
||||
const ncp_row_t *r = &g_rows[idx];
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_gfx_fill_round(x + NCP_ROW_X, y, NCP_ROW_W, h - 6, 12, th->surfaceContainer);
|
||||
/* 图标 */
|
||||
int icx = x + NCP_ICON_X, icy = y + (h - 32) / 2;
|
||||
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
|
||||
else {
|
||||
const char *nm = ncp_app_name(r->app_id);
|
||||
ncp_dot(icx + 16, icy + 16, 16, th->primary);
|
||||
char ch = nm[0] ? nm[0] : '?';
|
||||
char cbuf[4] = { ch, 0, 0, 0 };
|
||||
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
|
||||
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
|
||||
int cw = meow_ui_text_width(cbuf, th->font_body);
|
||||
meow_ui_text(icx + 16 - cw / 2, icy + 16 - meow_ui_text_height(th->font_body) / 2,
|
||||
cbuf, th->font_body, th->onPrimary);
|
||||
}
|
||||
/* app 名 + 时间 */
|
||||
meow_ui_text(x + NCP_TEXT_X, y + 10, ncp_app_name(r->app_id), th->font_body, th->onSurfaceVariant);
|
||||
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
|
||||
meow_ui_text(x + NCP_ROW_X + NCP_ROW_W - 8 - meow_ui_text_width(tbuf, th->font_body),
|
||||
y + 12, tbuf, th->font_body, th->onSurfaceVariant);
|
||||
/* 标题 + 内容(SDK 截断) */
|
||||
ncp_mt_text(x + NCP_TEXT_X, y + 30, r->summary, th->font_body, th->onSurface, NCP_TEXT_W);
|
||||
ncp_mt_text(x + NCP_TEXT_X, y + 56, r->body, th->font_body, th->onSurfaceVariant, NCP_TEXT_W);
|
||||
/* 进度(progress>=0):细条 */
|
||||
if (r->progress >= 0) {
|
||||
int px = x + NCP_TEXT_X, pw = NCP_TEXT_W, py = y + h - 14;
|
||||
meow_ui_fill_rect(px, py, pw, 4, th->surfaceVariant);
|
||||
int fw = r->progress * pw / 100;
|
||||
if (fw > 0) meow_ui_fill_rect(px, py, fw, 4, th->primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* 左滑删除完成回调(SDK 删除/回弹动画结束后触发):撤下该通知 + 重采集 + 重排。 */
|
||||
static void ncp_lv_on_delete(int idx, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
logmsg("notif-center: swipe delete row %d", idx);
|
||||
if (idx >= 0 && idx < g_nrows) meow_notif_dismiss(g_rows[idx].id);
|
||||
ncp_refresh_rows();
|
||||
/* 行数变化 → 列表重排:置位让主循环整面板重画(含媒体卡出现/消失重排 + 列表滚动重钳)。 */
|
||||
g_ncp_need_paint = 1;
|
||||
}
|
||||
|
||||
/* 芯片自绘(自定义画布 render_cb:TONAL 观感圆角 + 位图图标 + 标签,与旧 draw_notif_center chip 循环一致)。
|
||||
* launcher 侧 meow_widget 不透明 → 用 meow_widget_rect 取绝对几何(同 power_menu pm_render_cb)。 */
|
||||
static void ncp_chip_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||||
{
|
||||
int c = (int)(intptr_t)ctx;
|
||||
if (c < 0 || c >= NCP_CHIP_COUNT) return;
|
||||
int rc[4]; meow_widget_rect(w, rc);
|
||||
int x = rc[0], y = rc[1], wd = rc[2], ht = rc[3];
|
||||
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
|
||||
uint32_t bg = on ? th->primary : th->surfaceContainer;
|
||||
uint32_t fg = on ? th->onPrimary : th->onSurfaceVariant;
|
||||
meow_gfx_fill_round(x, y, wd, ht, 12, bg);
|
||||
int icon_cx = x + (wd - SB_ICON_SIZE) / 2;
|
||||
int icon_cy = y + 14;
|
||||
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_THEME) {
|
||||
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
|
||||
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
|
||||
}
|
||||
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_POWER) {
|
||||
int pc = icon_cx + SB_ICON_SIZE / 2;
|
||||
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
|
||||
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
|
||||
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
|
||||
}
|
||||
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
|
||||
static const char *chip_id[NCP_CHIP_COUNT] = { "ncp.chip_wifi", "ncp.chip_bt",
|
||||
"ncp.chip_theme", "ncp.chip_bright",
|
||||
"ncp.chip_power" };
|
||||
char label[24]; snprintf(label, sizeof label, "%s", meow_str_zh(chip_id[c], chip_lbl[c]));
|
||||
if (val[0] && c == NCP_CHIP_BRIGHT) snprintf(label, sizeof label, "%.9s", val);
|
||||
ui_draw(x + (wd - ui_width(label, 14)) / 2, y + ht - 18, label, fg, 14);
|
||||
}
|
||||
|
||||
/* 芯片自定义画布命中(矩形;未被主循环使用——命中仍走 ncp_chip_hit 人工几何,此处仅满足 W_CUSTOM 契约)。 */
|
||||
static int ncp_chip_widget_hit(meow_widget *w, int x, int y, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int rc[4]; meow_widget_rect(w, rc);
|
||||
return (x >= rc[0] && x < rc[0] + rc[2] && y >= rc[1] && y < rc[1] + rc[3]);
|
||||
}
|
||||
|
||||
/* 创建 M7.3 内层控件集(owner 钉哨兵;仅首进创建一次,跨次复用——同媒体卡 M7.0b 惯用法)。 */
|
||||
static void ncp_widgets_create(void)
|
||||
{
|
||||
meow_ui_activity_push(NCP_WIDGET_ACT);
|
||||
/* 通知列表(视口 = 列表区;行高 NCP_ROW_H;空态 + 左滑删除) */
|
||||
int lvy = ncp_list_top(), lvh = ncp_list_view();
|
||||
g_ncp_lv = meow_listview_create(0, lvy, fb_xres, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
if (g_ncp_lv) {
|
||||
meow_listview_set_vsource(g_ncp_lv, ncp_lv_count, ncp_lv_bind, NULL);
|
||||
meow_listview_set_item_draw(g_ncp_lv, ncp_lv_row_draw, NULL);
|
||||
meow_listview_set_item_h(g_ncp_lv, NCP_ROW_H);
|
||||
meow_listview_set_empty_text(g_ncp_lv, meow_str_zh("ncp.empty", "暂无通知"));
|
||||
meow_listview_set_swipe_delete(g_ncp_lv, 1, ncp_lv_on_delete, NULL);
|
||||
}
|
||||
/* 亮度/音量双滑条(track 中心对齐旧 s_draw_slider:NCP_SL*_Y+3) */
|
||||
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
|
||||
int bval = 0; meow_settings_get_int("display.brightness", &bval);
|
||||
bval = mnp_brightness_clamp(bval, bmin);
|
||||
g_ncp_sl_bri = meow_slider_create(NCP_SLIDER_X, NCP_SL1_Y - 14, NCP_SLIDER_W, bmin, 100, bval);
|
||||
int vmax = vol_clamp(settings_volume_max);
|
||||
int vval = vol_clamp(settings_volume_get());
|
||||
g_ncp_sl_vol = meow_slider_create(NCP_SLIDER_X, NCP_SL2_Y - 14, NCP_SLIDER_W, 0, vmax, vval);
|
||||
/* 勿扰开关 */
|
||||
g_ncp_sw_dnd = meow_switch_create(0, 0, g_ncp_dnd ? 1 : 0);
|
||||
if (g_ncp_sw_dnd) meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48,
|
||||
NCP_DND_Y + NCP_DND_H / 2 - 14, 48, 28);
|
||||
/* 5 个快捷 chip(自定义画布;坐标与旧 ncp_chip_x / NCP_CHIP_Y 一致) */
|
||||
for (int c = 0; c < NCP_CHIP_COUNT; c++)
|
||||
g_ncp_chip[c] = meow_widget_custom_create(ncp_chip_x(c), NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H,
|
||||
ncp_chip_render, ncp_chip_widget_hit,
|
||||
(void *)(intptr_t)c);
|
||||
meow_ui_activity_pop();
|
||||
g_ncp_widget_init = 1;
|
||||
}
|
||||
|
||||
|
||||
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
|
||||
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
|
||||
#if 0 /* M7.3 回退位:旧手绘行删除底(SDK 左滑删除已接管此绘制,见 meow_listview swipe 渲染)。 */
|
||||
static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th)
|
||||
{
|
||||
if (off >= 0) return;
|
||||
@@ -520,6 +765,35 @@ static void ncp_repaint_row(int scroll, int row, int off)
|
||||
meow_dirty_add(0, ry, fb_xres, NCP_ROW_H);
|
||||
meow_ui_flush();
|
||||
}
|
||||
#endif /* M7.3 回退位结束(上述旧手绘 ncp_draw_delete_bg/ncp_draw_row/ncp_draw_list/
|
||||
* ncp_repaint_list/ncp_repaint_row 重开本区即回退;新路径由下方 SDK 版接管)。 */
|
||||
|
||||
|
||||
/* ---- M7.3:通知列表绘制(SDK listview 版,替换上方 #if 0 的手绘版)---- */
|
||||
|
||||
/* 绘制通知列表可见区(scroll=像素偏移)。同步 listview 视口矩形(媒体卡出现/消失时列表顶缘
|
||||
* 变化)+ 滚动偏移,再画整个列表控件(虚拟化 + set_item_draw 行自绘 + 空态 + 左滑删除渲染
|
||||
* 全由 SDK 完成)。 */
|
||||
static void ncp_draw_list(int scroll)
|
||||
{
|
||||
if (!g_ncp_widget_init || !g_ncp_lv) return;
|
||||
int lt = ncp_list_top(), lv = ncp_list_view();
|
||||
meow_widget_set_rect(g_ncp_lv, 0, lt, fb_xres, lv);
|
||||
meow_listview_set_scroll(g_ncp_lv, mnp_scroll_clamp(scroll, g_nrows * NCP_ROW_H, lv));
|
||||
meow_widget_draw(g_ncp_lv);
|
||||
}
|
||||
|
||||
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底,再画 SDK listview;
|
||||
* 只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT))——「滚动只重绘列表可视区,绝不整屏」。 */
|
||||
static void ncp_repaint_list(int scroll)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int lt = ncp_list_top(), lv = ncp_list_view();
|
||||
fill_rect(0, lt, fb_xres, lv, th->surface_variant);
|
||||
ncp_draw_list(scroll);
|
||||
meow_dirty_add(0, lt, fb_xres, lv);
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
|
||||
/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段):
|
||||
@@ -555,14 +829,20 @@ static void ncp_draw_dnd(const meow_ui_theme_t *th)
|
||||
meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */
|
||||
ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */
|
||||
/* label + 状态 */
|
||||
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18);
|
||||
const char *dnd_lbl = meow_str_zh("ncp.dnd", "勿扰");
|
||||
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, dnd_lbl, th->on_surface, 18);
|
||||
char st[16];
|
||||
snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关");
|
||||
int lw = ui_width("勿扰", 18);
|
||||
snprintf(st, sizeof st, "%s", dnd_on ? meow_str_zh("common.on", "开") : meow_str_zh("common.off", "关"));
|
||||
int lw = ui_width(dnd_lbl, 18);
|
||||
ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st,
|
||||
dnd_on ? th->primary : th->on_surface_variant, 18);
|
||||
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */
|
||||
s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th);
|
||||
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中)。
|
||||
* M7.3:迁 SDK meow_switch(绘制经 meow_widget_draw;命中仍走 ncp_dnd_hit 整带)。 */
|
||||
if (g_ncp_widget_init && g_ncp_sw_dnd) {
|
||||
meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48, cy - 14, 48, 28);
|
||||
meow_switch_set(g_ncp_sw_dnd, dnd_on ? 1 : 0);
|
||||
meow_widget_draw(g_ncp_sw_dnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -576,7 +856,7 @@ static void ncp_draw_clear(const meow_ui_theme_t *th)
|
||||
if (cw <= 0) return;
|
||||
meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container);
|
||||
/* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */
|
||||
const char *lbl = "清除";
|
||||
const char *lbl = meow_str_zh("ncp.clear", "清除");
|
||||
int lw = ui_width(lbl, 18);
|
||||
int unit = 22; /* 图标占位宽 + 与文字的间距 */
|
||||
int total = unit + lw;
|
||||
@@ -620,6 +900,7 @@ static void ncp_media_cover_load(const meow_media_session_t *s)
|
||||
{
|
||||
if (!s) return;
|
||||
if (strcmp(g_ncp_media_cover_path, s->artwork) == 0) return;
|
||||
if (!s->artwork[0]) return; /* 用户 2026-09-04:空 artwork 保留上一张媒体卡封面(勿清,防暂停/开歌单后封面闪没) */
|
||||
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
|
||||
ncp_strncpy(g_ncp_media_cover_path, s->artwork, sizeof g_ncp_media_cover_path);
|
||||
if (!s->artwork[0]) return;
|
||||
@@ -629,38 +910,6 @@ static void ncp_media_cover_load(const meow_media_session_t *s)
|
||||
g_ncp_media_cover = meow_img_load_file(path);
|
||||
}
|
||||
|
||||
/* 实心三角(dir=+1 右向 / -1 左向);size=最大宽度 px。按扫描线 fill_rect 近似 */
|
||||
static void ncp_media_tri(int cx, int cy, int size, int dir, uint32_t col)
|
||||
{
|
||||
int half = size / 2;
|
||||
for (int dy = -half; dy <= half; dy++) {
|
||||
int w = half + 1 - (dy < 0 ? -dy : dy); /* 顶宽 1 → 底宽 half+1 */
|
||||
if (w <= 0) continue;
|
||||
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
|
||||
fill_rect(x0, cy + dy, w, 1, col);
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆形按钮底 + 字形(kind:0=prev 1=play 2=next 3=pause 4=暂停态play)。 */
|
||||
static void ncp_media_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
|
||||
{
|
||||
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
|
||||
uint32_t fg = 0xFFFFFF;
|
||||
if (kind == 0) { /* prev:左三角 + 左竖条 */
|
||||
ncp_media_tri(cx + 4, cy, r - 4, -1, fg);
|
||||
fill_rect(cx - 3, cy - (r - 5), 3, (r - 5) * 2, fg);
|
||||
} else if (kind == 1) { /* play:右三角 */
|
||||
ncp_media_tri(cx + 2, cy, r - 2, 1, fg);
|
||||
} else if (kind == 2) { /* next:右三角 + 右竖条 */
|
||||
ncp_media_tri(cx - 4, cy, r - 4, 1, fg);
|
||||
fill_rect(cx + 1, cy - (r - 5), 3, (r - 5) * 2, fg);
|
||||
} else if (kind == 3) { /* pause:双竖条 */
|
||||
int bw = (r - 6) / 2; if (bw < 2) bw = 2;
|
||||
fill_rect(cx - 7, cy - (r - 5), bw, (r - 5) * 2, fg);
|
||||
fill_rect(cx + 7 - bw, cy - (r - 5), bw, (r - 5) * 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
|
||||
static void ncp_draw_media_card(const meow_ui_theme_t *th)
|
||||
{
|
||||
@@ -678,19 +927,15 @@ static void ncp_draw_media_card(const meow_ui_theme_t *th)
|
||||
const char *t = s->title[0] ? s->title : s->app_id;
|
||||
int cw = ui_width(t, 20);
|
||||
ui_draw(cover_x + (cover_s - cw) / 2, cover_y + (cover_s - meow_font_line_height(20)) / 2,
|
||||
t, 0xFFFFFF, 20);
|
||||
t, meow_theme_get()->onPrimary, 20);
|
||||
}
|
||||
int tx = cover_x + cover_s + 14;
|
||||
int tw = (NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R - 8) - tx; /* 文本宽限到上一首按钮左缘前 8px */
|
||||
if (tw < 0) tw = 0;
|
||||
ncp_text(tx, cy + 14, s->title, 18, th->on_surface, tw);
|
||||
ncp_text(tx, cy + 40, s->artist[0] ? s->artist : s->album, 16, th->on_surface_variant, tw);
|
||||
/* 控制按钮 */
|
||||
int by = cy + NCP_MEDIA_BTN_Y;
|
||||
ncp_media_btn(NCP_MEDIA_PREV_CX, by, NCP_MEDIA_BTN_R - 4, 0, th);
|
||||
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 /* pause */ : 1 /* play */;
|
||||
ncp_media_btn(NCP_MEDIA_PLAY_CX, by, NCP_MEDIA_BTN_R, pk, th);
|
||||
ncp_media_btn(NCP_MEDIA_NEXT_CX, by, NCP_MEDIA_BTN_R - 4, 2, th);
|
||||
/* 控制按钮(SDK meow_iconbtn + sb_icons,与 Now Playing 一致;命中仍走 ncp_media_card_tap 三圆心) */
|
||||
ncp_draw_media_btns(s);
|
||||
/* 进度条 + 时间 */
|
||||
uint32_t pos = meow_media_display_pos(s);
|
||||
int px = tx, pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px, py = cy + NCP_MEDIA_PROG_Y;
|
||||
@@ -702,7 +947,8 @@ static void ncp_draw_media_card(const meow_ui_theme_t *th)
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片命中:按钮/进度(含 seek)。返回 1=已处理(内部已下发 PLAYCTL);0=未命中。 */
|
||||
/* 卡片命中:按钮/进度(含 seek)/卡片体。返回 2=卡片体命中已发 FOCUS(7)(调用方收起面板露出
|
||||
* 播放页);1=按钮/进度已处理(内部已下发 PLAYCTL,面板保持开启);0=未命中。 */
|
||||
static int ncp_media_card_tap(int tx, int ty)
|
||||
{
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
@@ -728,7 +974,14 @@ static int ncp_media_card_tap(int tx, int ty)
|
||||
cmd = 5;
|
||||
}
|
||||
}
|
||||
if (!cmd) return 0;
|
||||
if (!cmd) {
|
||||
/* 卡片体命中(非按钮/进度):向活动媒体源下发 FOCUS(7)(=NCM_CTL_FOCUS,见 ncm
|
||||
* ipc_media.c case 7 → g_req_focus_player=1 → UI 线程 push ACT_PLAYER),请求打开
|
||||
* App 播放页,并消费该次点击——返回 2 让调用方收起面板露出播放页。 */
|
||||
meow_media_playctl(7, 0);
|
||||
logmsg("notif-card: media tap card body -> FOCUS(7) open player");
|
||||
return 2;
|
||||
}
|
||||
int rc = meow_media_playctl(cmd, seek);
|
||||
logmsg("notif-card: media tap cmd=%u seek=%lld rc=%d", cmd, (long long)seek, rc);
|
||||
return 1;
|
||||
@@ -762,61 +1015,34 @@ static void draw_notif_center(int scroll, int redraw_sb)
|
||||
int grab_w = 44, grab_x = (W - grab_w) / 2;
|
||||
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
|
||||
/* 标题 + 「上滑/点空白关闭」 提示 */
|
||||
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
|
||||
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
|
||||
ui_draw(24, NCP_TITLE_Y, meow_str_zh("ncp.title", "通知中心"), th->on_surface, 22);
|
||||
ui_draw(W - 24 - ui_width(meow_str_zh("ncp.tap_close", "点空白关闭"), 16), NCP_TITLE_Y + 2,
|
||||
meow_str_zh("ncp.tap_close", "点空白关闭"), th->on_surface_variant, 16);
|
||||
|
||||
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */
|
||||
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
|
||||
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
|
||||
int chip_x0 = (W - total_chip) / 2;
|
||||
for (int c = 0; c < NCP_CHIP_N; c++) {
|
||||
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
|
||||
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
|
||||
uint32_t bg = on ? th->primary : th->surface_container;
|
||||
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
|
||||
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
|
||||
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
|
||||
int icon_cy = NCP_CHIP_Y + 14;
|
||||
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_THEME) {
|
||||
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
|
||||
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
|
||||
}
|
||||
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_POWER) {
|
||||
/* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */
|
||||
int pc = icon_cx + SB_ICON_SIZE / 2;
|
||||
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
|
||||
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
|
||||
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
|
||||
}
|
||||
char label[24];
|
||||
snprintf(label, sizeof label, "%s", chip_lbl[c]);
|
||||
if (val[0] && c == NCP_CHIP_BRIGHT)
|
||||
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
|
||||
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
|
||||
label, fg, 14);
|
||||
}
|
||||
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单)。M7.3:迁 SDK 自定义画布 chip
|
||||
*(TONAL 观感圆角 + 位图图标 + 标签,见 ncp_chip_render;绘制经 meow_widget_draw)。 */
|
||||
if (g_ncp_widget_init)
|
||||
for (int c = 0; c < NCP_CHIP_N; c++)
|
||||
if (g_ncp_chip[c]) meow_widget_draw(g_ncp_chip[c]);
|
||||
|
||||
/* 亮度滑条 */
|
||||
/* 亮度滑条(M7.3:迁 SDK meow_slider;track 中心 = NCP_SL1_Y+3,与旧 s_draw_slider 对齐) */
|
||||
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
|
||||
int bval = 0; meow_settings_get_int("display.brightness", &bval);
|
||||
bval = mnp_brightness_clamp(bval, bmin);
|
||||
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
|
||||
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, meow_str_zh("ncp.brightness", "亮度"), th->on_surface, 18);
|
||||
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
|
||||
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
|
||||
th->on_surface_variant, 18);
|
||||
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
|
||||
if (g_ncp_widget_init && g_ncp_sl_bri) { meow_slider_set(g_ncp_sl_bri, bval); meow_widget_draw(g_ncp_sl_bri); }
|
||||
|
||||
/* 音量滑条 */
|
||||
int vval = 0; meow_settings_get_int("audio.volume", &vval);
|
||||
vval = vol_clamp(vval);
|
||||
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
|
||||
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, meow_str_zh("ncp.volume", "音量"), th->on_surface, 18);
|
||||
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
|
||||
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
|
||||
th->on_surface_variant, 18);
|
||||
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
|
||||
if (g_ncp_widget_init && g_ncp_sl_vol) { meow_slider_set(g_ncp_sl_vol, vval); meow_widget_draw(g_ncp_sl_vol); }
|
||||
|
||||
/* 分隔线 */
|
||||
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
|
||||
@@ -833,8 +1059,8 @@ static void draw_notif_center(int scroll, int redraw_sb)
|
||||
ncp_draw_list(scroll);
|
||||
|
||||
/* 底部提示 */
|
||||
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
|
||||
th->on_surface_variant, 16);
|
||||
ui_draw((W - ui_width(meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), 16)) / 2, NCP_FOOT_Y,
|
||||
meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), th->on_surface_variant, 16);
|
||||
|
||||
/* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh
|
||||
* 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过
|
||||
@@ -865,7 +1091,7 @@ static int ncp_slider_apply(int which, int tx, int save_now)
|
||||
int cur = 0; meow_settings_get_int("display.brightness", &cur);
|
||||
if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */
|
||||
meow_settings_set_int("display.brightness", v);
|
||||
write_brightness(v);
|
||||
write_brightness(v, "notif_center bright_slider");
|
||||
g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */
|
||||
paint_sb_right();
|
||||
if (save_now) meow_settings_save();
|
||||
@@ -942,16 +1168,15 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, kd_pwr;
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||||
int m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
key_router_rebase(); /* 进入通知中心:音量基线对齐当前 raw(防残留边沿) */
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
@@ -968,16 +1193,10 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */
|
||||
int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */
|
||||
int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
|
||||
/* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off):
|
||||
* row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手);
|
||||
* row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。
|
||||
* row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */
|
||||
int row_swipe_row = -1;
|
||||
int row_off = 0;
|
||||
int row_settle = 0;
|
||||
int row_off_target = 0;
|
||||
unsigned long long row_anim_at = 0;
|
||||
int row_paint = 0; /* 仅重绘该行带(行左滑专用) */
|
||||
/* ⑦ 单条通知行左滑删除(M7.3:迁 SDK meow_listview_set_swipe_delete + swipe_begin/move/end)。
|
||||
* sdk_swipe = 本触摸已交给 SDK 行左滑手势(begin 命中可滑行);SDK 负责方向仲裁(横向=消费 /
|
||||
* 纵向=让位滚动)、删除/回弹动画(meow_anim_tick 推进)与 on_delete 回调。 */
|
||||
int sdk_swipe = 0;
|
||||
/* ⑨ 面板跟手下拉、半程可回:
|
||||
* panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停);
|
||||
* drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变);
|
||||
@@ -1018,6 +1237,9 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
g_ncp_dnd = d;
|
||||
}
|
||||
|
||||
/* M7.3:建内层 SDK 控件集(listview/双滑条/勿扰开关/芯片;owner 钉哨兵,仅首进一次)。 */
|
||||
ncp_widgets_create();
|
||||
|
||||
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
|
||||
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
|
||||
ncp_refresh_rows();
|
||||
@@ -1081,20 +1303,17 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
if (wak) {
|
||||
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
||||
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
|
||||
scroll_invert = 0; scroll_rebound = 0; list_paint = 0;
|
||||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||||
sdk_swipe = 0; /* 唤醒:清行左滑手势(SDK swipe 内部状态亦重置) */
|
||||
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
|
||||
/* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */
|
||||
anim_p = anim_target = anim_last_target = NCP_ANIM_FIX;
|
||||
@@ -1110,23 +1329,20 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
|
||||
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
||||
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||||
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||
} else {
|
||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (play && !m_prev_play) {
|
||||
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
||||
last_act = now;
|
||||
@@ -1145,12 +1361,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
show_power_menu();
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1; need_paint = 1;
|
||||
goto ncp_keys_seeded;
|
||||
@@ -1169,14 +1384,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
}
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||||
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
need_paint = 1;
|
||||
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||||
m_prev_play = play; m_prev_pwr = pwr;
|
||||
ncp_keys_seeded:
|
||||
;
|
||||
|
||||
@@ -1201,42 +1416,27 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
|
||||
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
|
||||
}
|
||||
/* M7.3:按下原点落在通知列表行 → 交 SDK 行左滑手势(begin 命中可滑行;
|
||||
* 后续 move 由 SDK 做方向仲裁:横向=消费/纵向=让位滚动)。先同步 listview
|
||||
* 滚动偏移(meow_listview_hit 用其内部 scroll)。 */
|
||||
if (open_settled && !drag_slider && !panel_drag && g_ncp_widget_init && g_ncp_lv) {
|
||||
meow_listview_set_scroll(g_ncp_lv, list_scroll);
|
||||
sdk_swipe = meow_listview_swipe_begin(g_ncp_lv, tlx, tly);
|
||||
}
|
||||
}
|
||||
if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
|
||||
else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
|
||||
/* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop,
|
||||
* 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。
|
||||
* 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含
|
||||
* row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大:
|
||||
* 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */
|
||||
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
|
||||
int ddx = tlx - tdx, ddy = tly - tdy;
|
||||
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
|
||||
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
|
||||
int eff = tdy - ncp_list_top() + list_scroll;
|
||||
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
|
||||
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
|
||||
if (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
|
||||
row >= 0 && row < g_nrows) {
|
||||
row_swipe_row = row;
|
||||
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
|
||||
row_settle = 0; row_off_target = 0;
|
||||
row_anim_at = now_ms();
|
||||
g_ncp_swipe_row = row; g_ncp_swipe_off = row_off;
|
||||
row_paint = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */
|
||||
if (row_swipe_row >= 0 && td_ok && !row_settle) {
|
||||
row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W);
|
||||
g_ncp_swipe_off = row_off;
|
||||
row_paint = 1;
|
||||
/* ⑦ 行左滑(SDK swipe-delete):已交给 SDK 手势 → 喂 move。SDK 方向仲裁:
|
||||
* 横向小位移超容差 → 消费(返回 1,行左移/删除区由 SDK 画,本触摸不滚/不拖面板);
|
||||
* 纵向主导 → 取消本行 swipe(返回 0,sdk_swipe=0),回退到下方滚动/面板拖动。 */
|
||||
if (sdk_swipe && td_ok && drag_slider == 0) {
|
||||
meow_listview_set_scroll(g_ncp_lv, list_scroll);
|
||||
if (!meow_listview_swipe_move(g_ncp_lv, tlx, tly)) sdk_swipe = 0;
|
||||
}
|
||||
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
|
||||
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
|
||||
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
|
||||
if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) {
|
||||
if (td_ok && !drag_slider && !scroll_drag && !sdk_swipe && !panel_drag) {
|
||||
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
|
||||
if (ady2 >= S_SCROLL_SLOP) {
|
||||
int av = g_nrows * NCP_ROW_H;
|
||||
@@ -1260,7 +1460,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
/* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 +
|
||||
* 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式);
|
||||
* 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */
|
||||
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
|
||||
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && !sdk_swipe) {
|
||||
int ddy = tly - scroll_y0;
|
||||
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
|
||||
int total = g_nrows * NCP_ROW_H;
|
||||
@@ -1290,10 +1490,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0;
|
||||
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
|
||||
scroll_invert = 0; scroll_rebound = 0;
|
||||
/* ⑦ 新触摸:清行左滑(防上一手势残留) */
|
||||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||||
/* ⑦ 新触摸:清行左滑手势(防上一手势残留) */
|
||||
sdk_swipe = 0;
|
||||
/* ⑨ 新触摸:清面板跟手(防上一拖动残留) */
|
||||
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
|
||||
}
|
||||
@@ -1311,19 +1509,18 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
if (anim_ok) anim_target = 0; else closed = 1;
|
||||
break;
|
||||
}
|
||||
if (row_swipe_row >= 0) {
|
||||
/* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。
|
||||
* 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */
|
||||
/* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */
|
||||
row_off = mnp_swipe_clamp(dx, NCP_ROW_W);
|
||||
g_ncp_swipe_off = row_off;
|
||||
if (mnp_swipe_delete(row_off, NCP_ROW_W,
|
||||
NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) {
|
||||
row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */
|
||||
} else {
|
||||
row_off_target = 0; /* 回弹复原 */
|
||||
if (sdk_swipe && g_ncp_widget_init && g_ncp_lv) {
|
||||
/* ⑦ 行左滑松手(SDK swipe-delete):消费 → 删除/回弹动画交 SDK
|
||||
* (meow_anim_tick 推进;删除完成后 ncp_lv_on_delete 置 g_ncp_need_paint)。
|
||||
* 本手势不关面板、不走 tap/上滑/点空白。未滑动(=点击某行)→ 未消费,
|
||||
* 清 sdk_swipe 后走下方 tap 分支(ncp_row_tap 命中该行 → dismiss+关面板)。 */
|
||||
meow_listview_set_scroll(g_ncp_lv, list_scroll);
|
||||
if (meow_listview_swipe_end(g_ncp_lv, tlx, tly)) {
|
||||
sdk_swipe = 0;
|
||||
td_ok = 0;
|
||||
goto ncp_release_done;
|
||||
}
|
||||
row_settle = 1; row_anim_at = now_ms();
|
||||
sdk_swipe = 0;
|
||||
} else if (scroll_drag) {
|
||||
/* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值);
|
||||
* 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。
|
||||
@@ -1359,15 +1556,13 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
||||
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
||||
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
||||
/* 菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
||||
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
||||
@@ -1395,20 +1590,28 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
if (!hit && ncp_clear_hit(tx, ty)) {
|
||||
int n = ncp_clear_all();
|
||||
logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n);
|
||||
/* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */
|
||||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||||
/* 清除后取消行左滑手势(下标会变),再重采集 + 重钳 + 整面板重画。 */
|
||||
sdk_swipe = 0;
|
||||
ncp_refresh_rows();
|
||||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||||
ncp_list_view());
|
||||
hit = 1; need_paint = 1;
|
||||
}
|
||||
/* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL。
|
||||
/* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL;
|
||||
* 卡片体 → 下发 FOCUS(7) 请求打开 App 播放页 + 收起面板(返回 2)。
|
||||
* 卡片命中固定于列表上方,先于列表行命中处理。 */
|
||||
if (!hit && meow_media_has_source() && ncp_media_card_tap(tx, ty)) {
|
||||
hit = 1; need_paint = 1;
|
||||
} else if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
|
||||
if (!hit && meow_media_has_source()) {
|
||||
int mh = ncp_media_card_tap(tx, ty);
|
||||
if (mh) {
|
||||
hit = 1;
|
||||
if (mh == 2) { /* 卡片体 FOCUS → 收起面板露出播放页 */
|
||||
if (anim_ok) anim_target = 0; else closed = 1;
|
||||
break;
|
||||
}
|
||||
need_paint = 1; /* 按钮/进度:仅重绘卡片(面板保持开启) */
|
||||
}
|
||||
}
|
||||
if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
|
||||
if (ncp_row_tap(tx, ty, list_scroll, click)) {
|
||||
hit = 1;
|
||||
if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */
|
||||
@@ -1430,6 +1633,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
/* 其余横滑/下滑忽略(不关闭不动作) */
|
||||
}
|
||||
td_ok = 0;
|
||||
ncp_release_done:
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1453,10 +1658,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
media_paint = 1; /* 已有源:进度/封面随 META 变化 → 仅重绘卡片带 */
|
||||
}
|
||||
if (ncp_rows_changed()) {
|
||||
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
|
||||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||||
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑手势,避免行下标错位。⑨ 同时取消面板跟手。 */
|
||||
sdk_swipe = 0;
|
||||
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
|
||||
ncp_refresh_rows();
|
||||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||||
@@ -1490,41 +1693,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */
|
||||
|
||||
/* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。
|
||||
* 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。
|
||||
* 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */
|
||||
if (row_swipe_row >= 0 && row_settle) {
|
||||
unsigned long long rnow = now_ms();
|
||||
int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0);
|
||||
row_anim_at = rnow;
|
||||
int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE);
|
||||
if (noff != row_off) {
|
||||
row_off = noff;
|
||||
g_ncp_swipe_off = noff;
|
||||
row_paint = 1;
|
||||
}
|
||||
if (noff == row_off_target) {
|
||||
if (row_off_target == 0) {
|
||||
/* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */
|
||||
ncp_repaint_row(list_scroll, row_swipe_row, 0);
|
||||
row_swipe_row = -1; row_settle = 0; row_off = 0;
|
||||
row_off_target = 0; row_paint = 0;
|
||||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||||
if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */
|
||||
} else {
|
||||
/* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */
|
||||
if (row_swipe_row >= 0 && row_swipe_row < g_nrows)
|
||||
meow_notif_dismiss(g_rows[row_swipe_row].id);
|
||||
row_swipe_row = -1; row_settle = 0; row_off = 0;
|
||||
row_off_target = 0; row_paint = 0;
|
||||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||||
ncp_refresh_rows();
|
||||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||||
ncp_list_view());
|
||||
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
|
||||
}
|
||||
}
|
||||
}
|
||||
/* M7.3:行左滑落位动画已由 SDK 接管(meow_listview_swipe_end → meow_anim → swipe_anim_done
|
||||
* → on_delete(idx) → ncp_lv_on_delete 置 g_ncp_need_paint)。此处不再需要手绘行位移/回弹/滑出。 */
|
||||
|
||||
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
|
||||
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
|
||||
@@ -1561,14 +1731,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok);
|
||||
drag_paint = 0;
|
||||
}
|
||||
/* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。
|
||||
* row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */
|
||||
if (row_paint) {
|
||||
last_act = now;
|
||||
if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target))
|
||||
ncp_repaint_row(list_scroll, row_swipe_row, row_off);
|
||||
row_paint = 0;
|
||||
}
|
||||
/* M7.3:行左滑跟手/回弹/滑出重绘已由 SDK 接管(swipe_anim_update 经 meow_widget_draw(lv)
|
||||
* 重绘列表 + meow_anim_set_region 标该行带脏)。此处不再有 row_paint 手绘路径。 */
|
||||
/* ~1s 状态栏时钟 */
|
||||
if (now - last_tick >= 1000) {
|
||||
last_tick = now;
|
||||
@@ -1576,12 +1740,27 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
fb_norm_buf0();
|
||||
meow_ui_flush();
|
||||
}
|
||||
/* M7.3:推进 SDK 动画(行左滑删除/回弹、meow_scroll fling 等)。左滑删除完成后 on_delete
|
||||
* 置 g_ncp_need_paint → 下一帧整面板重画。swipe 动画推进时 SDK 已重绘列表(swipe_anim_update
|
||||
* 调 meow_widget_draw(lv)),此处只把列表带标脏上屏(覆盖 SDK 经 meow_gfx 标脏的列表矩形)。 */
|
||||
meow_anim_tick();
|
||||
if (g_ncp_need_paint) { g_ncp_need_paint = 0; need_paint = 1; }
|
||||
if (g_ncp_widget_init && g_ncp_lv && meow_listview_swipe_active(g_ncp_lv)) {
|
||||
meow_dirty_add(0, ncp_list_top(), fb_xres, ncp_list_view());
|
||||
meow_ui_flush();
|
||||
}
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_power); kp_close(&kp_keys);
|
||||
ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */
|
||||
ncp_rows_free();
|
||||
/* 封面缓存随面板关闭回收(对齐 Now Playing np_cleanup:每次下拉重新加载当前封面)。
|
||||
* 旧码 g_ncp_media_cover_path 是文件级 static、从不重置——若首次加载失败/或 artwork 初
|
||||
* 为空的瞬间进面板,其后即便封面落盘并因路径不变而被「按文本缓存」跳过重载,卡片永久
|
||||
* 停留在「显示歌名」回退/旧封面。关面板即清缓存,保证下次下拉能拿到最新封面。 */
|
||||
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
|
||||
g_ncp_media_cover_path[0] = 0;
|
||||
logmsg("notification center closed");
|
||||
}
|
||||
|
||||
@@ -10,43 +10,142 @@
|
||||
* 元数据/源变化立即整页重画;交互只重绘按钮/进度带。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "np_gesture.h" /* 批4:双页方向感知手势决策(header-only,宿主可单测) */
|
||||
#include "meow_lyric_view.h" /* 批2控件:歌词页渲染(批4接入,数据批3 meow_media_lyric_*) */
|
||||
#include "meow_img_color.h" /* 批5:封面双色取色原语(独立源码,不并入 meow_ui.a) */
|
||||
|
||||
|
||||
#define NP_COVER 300 /* 封面边长(方,居中) */
|
||||
#define NP_COVER_X ((fb_xres - NP_COVER) / 2)
|
||||
#define NP_COVER_Y 60
|
||||
#define NP_TITLE_Y 412
|
||||
#define NP_ARTIST_Y 450
|
||||
#define NP_ALBUM_Y 480
|
||||
#define NP_COVER_Y 84 /* 用户 2026-09-04:控件组整体下移,消除底部大留白 */
|
||||
#define NP_TITLE_Y 436
|
||||
#define NP_ARTIST_Y 476
|
||||
#define NP_ALBUM_Y 508
|
||||
#define NP_PROG_X 40
|
||||
#define NP_PROG_W (fb_xres - 80)
|
||||
#define NP_PROG_Y 518
|
||||
#define NP_TIME_Y 492
|
||||
#define NP_BTN_Y 568 /* 控制按钮中心 y */
|
||||
#define NP_PROG_Y 548
|
||||
#define NP_TIME_Y 526
|
||||
#define NP_BTN_Y 612 /* 控制按钮中心 y */
|
||||
#define NP_PLAY_CX (fb_xres / 2)
|
||||
#define NP_PREV_CX (NP_PLAY_CX - 78)
|
||||
#define NP_NEXT_CX (NP_PLAY_CX + 78)
|
||||
#define NP_PLAY_R 30
|
||||
#define NP_PREV_R 22
|
||||
#define NP_LYRIC_Y 640
|
||||
/* 批② home 指示条(底部上滑回桌面):位于屏底左缘、避开左缘返回区(NP_EDGE_W=24)。
|
||||
* 与 np_gesture.h np_home_gesture 的起滑区一致(down_x>=NP_EDGE_W 才认 home),保证
|
||||
* 「指示条可触发手势」自洽。y 距屏底 16px,高 6px(镜像 SDK meow_ui_swipe_hint 提示线高一档)。
|
||||
*/
|
||||
#define NP_HOME_BAR_X (NP_EDGE_W + 8) /* 左缘 32:紧贴左缘返回区右侧 */
|
||||
#define NP_HOME_BAR_W 48
|
||||
#define NP_HOME_BAR_H 6
|
||||
#define NP_HOME_BAR_Y (fb_yres - 16)
|
||||
|
||||
static meow_img *g_np_cover = NULL;
|
||||
static char g_np_cover_path[200] = "";
|
||||
|
||||
/* 封面解码缓存(artwork 路径变化才重载)。 */
|
||||
/* ---- 批4:Now Playing 双页状态 + 歌词页控件 ----
|
||||
* 同一容器两页:NP_PAGE_PLAY(播放页,原有内容)+ NP_PAGE_LYRIC(歌词页,批2 meow_lyric_view)。
|
||||
* 页面切换靠方向感知手势(np_gesture.h);歌词数据批3(meow_media_lyric_*),主题先走 meow_theme
|
||||
* 默认;meow_lyric_view_set_colors 为批5 封面取色动态主题预留(本批不取色,见 np_lyric_refresh)。 */
|
||||
static int g_np_page = NP_PAGE_PLAY;
|
||||
static meow_lyric_view *g_np_lyric = NULL; /* 歌词视图(lazy 建一次,跨进入复用) */
|
||||
static uint32_t g_np_lyric_seq = 0xFFFFFFFFu; /* 上次解析的歌曲代际(新 seq 才重解析) */
|
||||
static char g_np_lyric_app[MEOW_APP_ID_MAX] = ""; /* 上次解析的源 app_id */
|
||||
static int g_np_lyric_last_valid = -1; /* -1=未初始化 0=无词 1=有词(防重复 clear) */
|
||||
|
||||
/* 批4:歌词页宽度测量(launcher ui_width 包装成 meow_lyric_width_fn;meow_lyric_view 需要)。 */
|
||||
static int np_lyric_width(const char *u, int size, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return ui_width(u, size);
|
||||
}
|
||||
|
||||
/* 封面解码缓存(artwork 路径变化才重载;路径未变但上次加载失败→允许重试)。
|
||||
* M6.33 修「artwork 空→复原」封面不重载:原「路径未变即跳过」在「空 artwork 清空封面后再
|
||||
* 以同路径复原」或「首载失败同路径重报」时封面永不恢复(g_np_cover==NULL 时即使路径未变也重载)。 */
|
||||
static void np_cover_load(const meow_media_session_t *s)
|
||||
{
|
||||
if (!s) return;
|
||||
if (strcmp(g_np_cover_path, s->artwork) == 0) return;
|
||||
if (g_np_cover && strcmp(g_np_cover_path, s->artwork) == 0) return; /* 已载且未变 → 跳过 */
|
||||
if (!s->artwork[0]) return; /* 用户 2026-09-04:空 artwork(如打开歌单清 g_cover_path 致 META 报空)
|
||||
* 时保留上一张已解码封面,勿据此清封面 → 播放页/歌词页封面不闪没 */
|
||||
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
|
||||
snprintf(g_np_cover_path, sizeof g_np_cover_path, "%s", s->artwork);
|
||||
if (!s->artwork[0]) return;
|
||||
char path[200];
|
||||
char path[240]; /* 相对 artwork 加前缀最坏 191+19=210 < 240,防 -Wformat-truncation 实际截断 */
|
||||
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
|
||||
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
|
||||
g_np_cover = meow_img_load_file(path);
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* 批5:封面双色取色(主色+次色)→ 播放页/歌词页动态主题。
|
||||
* - 何时取:进入 Now Playing 页 / 封面路径变化时(np_cover_theme_update 幂等,按路径缓存)。
|
||||
* - 取色成本:meow_img_duocolor 内部自适副采样(等效缩略图,≤~3.6k 样本),不额外解码/分配。
|
||||
* - 缓存:按 artwork 路径;路径未变不重取(g_np_cc_valid + g_np_cc_path)。
|
||||
* - 回退:无封面/取色失败(g_np_cc_valid=0)→ 歌词页 set_colors(NULL) 用 meow_theme 默认;
|
||||
* 播放页背景/文字走 s_ui_theme() 主题面。
|
||||
* ================================================================== */
|
||||
static meow_img_color_theme g_np_cc;
|
||||
static int g_np_cc_valid = 0;
|
||||
static char g_np_cc_path[200] = "";
|
||||
|
||||
/* 校验/刷新取色缓存(封面路径变化才重取;幂等)。 */
|
||||
static void np_cover_theme_update(void)
|
||||
{
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
const char *art = s ? s->artwork : "";
|
||||
if (g_np_cc_valid && strcmp(g_np_cc_path, art) == 0) return; /* 缓存命中 */
|
||||
snprintf(g_np_cc_path, sizeof g_np_cc_path, "%s", art);
|
||||
g_np_cc_valid = 0;
|
||||
if (!g_np_cover || !art[0]) return; /* 无封面 → 回退主题 */
|
||||
uint32_t mainc = 0, secondc = 0;
|
||||
if (meow_img_duocolor(g_np_cover, &mainc, &secondc) != 0) return; /* 取色失败 → 回退主题 */
|
||||
meow_img_color_derive(mainc, secondc, meow_theme_get(), &g_np_cc);
|
||||
g_np_cc_valid = 1;
|
||||
}
|
||||
|
||||
/* 把封面取色注入歌词页(无取色 → NULL 回退 meow_theme 默认)。 */
|
||||
static void np_apply_lyric_colors(void)
|
||||
{
|
||||
if (!g_np_lyric) return;
|
||||
meow_lyric_view_set_colors(g_np_lyric, g_np_cc_valid ? &g_np_cc.lyr : NULL);
|
||||
}
|
||||
|
||||
/* 渐变插值(无 GPU,逐行整数;封面主题背景用)。返回 [y0, y0+h) 带内屏幕行 y 的颜色。 */
|
||||
static uint32_t np_grad_row(uint32_t top, uint32_t bottom, int y0, int h, int y)
|
||||
{
|
||||
int t = (h <= 1) ? 0 : (y - y0) * 255 / (h - 1);
|
||||
if (t < 0) t = 0; else if (t > 255) t = 255;
|
||||
int tr = (top >> 16) & 255, tg = (top >> 8) & 255, tb = top & 255;
|
||||
int br = (bottom >> 16) & 255, bg = (bottom >> 8) & 255, bb = bottom & 255;
|
||||
int r = tr + ((br - tr) * t + 128) / 255;
|
||||
int g = tg + ((bg - tg) * t + 128) / 255;
|
||||
int b = tb + ((bb - tb) * t + 128) / 255;
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
|
||||
}
|
||||
|
||||
/* 竖向双色渐变填充(封面主题背景用;整页/局部共用 np_grad_row,保证局部重绘与整页渐变无缝)。 */
|
||||
static void np_fill_gradient(int x, int y, int w, int h, uint32_t top, uint32_t bottom)
|
||||
{
|
||||
if (w <= 0 || h <= 0) return;
|
||||
for (int yy = 0; yy < h; yy++)
|
||||
fill_rect(x, y + yy, w, 1, np_grad_row(top, bottom, y, h, y + yy));
|
||||
}
|
||||
|
||||
/* 播放页当前主题 · 页背景行色:内容区(状态栏下)屏幕行 y 的背景色。
|
||||
* 取色有效 → 取色渐变(按整页 span 插值:局部重绘进度带逐行取色与整页渐变无缝衔接);
|
||||
* 取色无效 → 返回 0(调用方回退 meow_theme 面)。 */
|
||||
static uint32_t np_page_bg_row(int y)
|
||||
{
|
||||
if (!g_np_cc_valid) return 0;
|
||||
return np_grad_row(g_np_cc.bg_top, g_np_cc.bg_bottom, SB_BAR_H, fb_yres - SB_BAR_H, y);
|
||||
}
|
||||
|
||||
#if 0 /* ================================================================
|
||||
* M7.0b 退役:Now Playing 手绘控制钮/进度条改走 SDK 控件(meow_iconbtn +
|
||||
* meow_progress),命中/状态/进度全由 SDK 接管;保留作回退位(删除 #if 0 行恢复旧手绘)。
|
||||
* ================================================================ */
|
||||
/* 实心三角(dir=+1 右向 / -1 左向)。 */
|
||||
static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
|
||||
{
|
||||
@@ -59,25 +158,6 @@ static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
|
||||
}
|
||||
}
|
||||
|
||||
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。 */
|
||||
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
|
||||
{
|
||||
if (!s) s = "";
|
||||
int W = fb_xres;
|
||||
if (ui_width(s, size) <= maxw) {
|
||||
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
|
||||
return;
|
||||
}
|
||||
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
|
||||
char buf[160];
|
||||
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
|
||||
if (keep < 0) keep = 0;
|
||||
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
|
||||
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
|
||||
strcat(buf, "...");
|
||||
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
|
||||
}
|
||||
|
||||
/* 圆形控制按钮(kind:0=prev 1=play 2=next 3=pause)。 */
|
||||
static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
|
||||
{
|
||||
@@ -97,22 +177,268 @@ static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
|
||||
fill_rect(cx + 8 - bw, cy - (r - 7), bw, (r - 7) * 2, fg);
|
||||
}
|
||||
}
|
||||
#endif /* M7.0b 退役:Now Playing 旧手绘控制钮 */
|
||||
|
||||
/* 进度带(进度条 + 时间)绘制;pts = 当前显示位置。 */
|
||||
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。(M7.0b 保留:封面/歌名/艺术家仍 SD) */
|
||||
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
|
||||
{
|
||||
if (!s) s = "";
|
||||
int W = fb_xres;
|
||||
if (ui_width(s, size) <= maxw) {
|
||||
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
|
||||
return;
|
||||
}
|
||||
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
|
||||
char buf[160];
|
||||
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
|
||||
if (keep < 0) keep = 0;
|
||||
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
|
||||
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
|
||||
strcat(buf, "...");
|
||||
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
|
||||
}
|
||||
|
||||
/* 左对齐 + 超宽截断(UTF-8 安全,近似按字节比例切;歌词页顶部元信息用)。 */
|
||||
static void np_left_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
|
||||
{
|
||||
if (!s) s = "";
|
||||
if (ui_width(s, size) <= maxw) {
|
||||
ui_draw(x, y, s, color, size);
|
||||
return;
|
||||
}
|
||||
char buf[160];
|
||||
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
|
||||
if (keep < 0) keep = 0;
|
||||
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
|
||||
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
|
||||
strcat(buf, "...");
|
||||
ui_draw(x, y, buf, color, size);
|
||||
}
|
||||
|
||||
/* 歌词页顶部元信息(两行居左,复刻 ncm 歌词页):第一行歌名(大字白)、第二行歌手(小字灰)。
|
||||
* 多歌手已由源侧拼串(ncm art_name 即「A / B」),此处原样显示 + 超宽截断。
|
||||
* 左边距与歌词正文一致(LYR_PAD=24);离状态栏留边距(SB_BAR_H + 20);右缘同距截断。 */
|
||||
static void np_draw_lyric_title(const meow_media_session_t *s)
|
||||
{
|
||||
if (!s) return;
|
||||
if (!s->title[0] && !s->artist[0]) return;
|
||||
int pad = 24;
|
||||
int y = SB_BAR_H + 20;
|
||||
int maxw = fb_xres - 2 * pad;
|
||||
if (s->title[0])
|
||||
np_left_text(pad, y, s->title, 28, 0xFFFFFF, maxw);
|
||||
if (s->artist[0])
|
||||
np_left_text(pad, y + meow_font_line_height(28) + 6, s->artist, 20, 0xBBBBBB, maxw);
|
||||
}
|
||||
|
||||
/* 歌词页强制 ncm 配色:活动行恒纯白 + 上下逐级灰阶(对齐 ncm lyr_gray_tbl
|
||||
* {FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}——gray[0]=0 按 cur=0xFFFFFF 自动递减即为该表)。
|
||||
* 背景恒为 np_draw_lyric_page 铺的黑色半透明叠加层,故关掉 lyric_view 自带 scrim
|
||||
* (scrim_alpha=0)避免双重压暗;当前行/灰阶不随主题/封面取色(保黑底对比度,夜间可读)。 */
|
||||
static void np_set_lyric_ncm_colors(void)
|
||||
{
|
||||
if (!g_np_lyric) return;
|
||||
meow_lyric_view_colors c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.scrim = 0x000000;
|
||||
c.cur = 0xFFFFFF;
|
||||
c.scrim_alpha = 0; /* 不画 scrim:由 launcher 黑底铺(单一叠加层) */
|
||||
meow_lyric_view_set_colors(g_np_lyric, &c);
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
|
||||
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
|
||||
* - 播放控制三钮 → meow_iconbtn(PREV / PLAY_ARROW|PAUSE / NEXT;按下=PAUSE、暂停=PLAY_ARROW,
|
||||
* 同位置叠两钮、按当前状态只画其一);
|
||||
* - 命中走 meow_widget_hit(圆形,owner==哨兵 + enabled 闸门)→ meow_media_playctl 分发;
|
||||
* - 封面(meow_img contain)与歌名/艺术家(meow_font)不动(本就 SD),仅文案走 meow_str。
|
||||
* 控件 lazy 创建一次(owner=NP_SENTINEL_ACT),跨多次进入复用;run_now_playing() 进入 push
|
||||
* 哨兵、退出 pop(owner 闸门使控件命中生效)。
|
||||
* ================================================================== */
|
||||
#define NP_SENTINEL_ACT 0x51 /* Now Playing 哨兵 activity */
|
||||
|
||||
static meow_widget *g_np_prog = NULL;
|
||||
static meow_widget *g_np_prev = NULL, *g_np_play = NULL, *g_np_pause = NULL, *g_np_next = NULL;
|
||||
static int g_np_init = 0;
|
||||
|
||||
static void np_widgets_create(void)
|
||||
{
|
||||
meow_ui_activity_push(NP_SENTINEL_ACT);
|
||||
g_np_prog = meow_progress_create(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, 0);
|
||||
g_np_prev = meow_iconbtn_create(NP_PREV_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
|
||||
NP_PREV_R * 2, MEOW_ICON_SKIP_PREV);
|
||||
g_np_play = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
|
||||
NP_PLAY_R * 2, MEOW_ICON_PLAY_ARROW);
|
||||
g_np_pause = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
|
||||
NP_PLAY_R * 2, MEOW_ICON_PAUSE);
|
||||
g_np_next = meow_iconbtn_create(NP_NEXT_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
|
||||
NP_PREV_R * 2, MEOW_ICON_SKIP_NEXT);
|
||||
/* 主播放钮保持「强调动作」观感:底色=primary、图标=onPrimary;其余两钮默认 surfaceContainerLow。 */
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_widget_opts op; memset(&op, 0, sizeof op);
|
||||
op.bg = th->primary; op.fg = th->onPrimary;
|
||||
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
|
||||
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
|
||||
/* 批4:歌词页视图(覆盖状态栏下整页;宽测量走 launcher ui_width 包装)。
|
||||
* set_colors 留作批5 封面取色覆盖入口;本批用 meow_theme 默认(不调用)。 */
|
||||
g_np_lyric = meow_lyric_view_create(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H,
|
||||
np_lyric_width, NULL);
|
||||
if (g_np_lyric)
|
||||
meow_lyric_view_set_empty(g_np_lyric, meow_str_zh("np.no_lyric", "暂无歌词"));
|
||||
meow_ui_activity_pop();
|
||||
g_np_init = 1;
|
||||
}
|
||||
|
||||
/* 播放页当前主题 → 控件修饰(整页与局部重绘共用同一次派生,避免两处取色逻辑漂移)。
|
||||
* 进度条 meow_progress:opts.bg=轨道、opts.fg=填充(批5b W_PROGRESS 新增支持);
|
||||
* 控制钮 meow_iconbtn:opts.bg=圆底、opts.fg=图标墨色。
|
||||
* 取色有效(g_np_cc_valid):进度条轨道=fg_dim/填充=accent,三钮圆底=accent、图标=accent_fg;
|
||||
* 取色无效:全部清 opts 回主题默认(进度条=surfaceVariant/primary、play/pause=primary/onPrimary、
|
||||
* prev/next=surfaceContainerLow/onSurface——与创建时一致,不回归)。
|
||||
* 每次绘制前调用(np_draw_page 播放分支 / np_repaint_progress),保证主题跟手。 */
|
||||
static void np_theme_widgets(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_widget_opts op; memset(&op, 0, sizeof op);
|
||||
if (g_np_cc_valid) {
|
||||
op.bg = g_np_cc.fg_dim; op.fg = g_np_cc.accent; /* 进度条:track=fg_dim / fill=accent */
|
||||
if (g_np_prog) meow_widget_set_opts(g_np_prog, &op);
|
||||
op.bg = g_np_cc.accent; op.fg = g_np_cc.accent_fg; /* 三钮:圆底=accent / 图标=accent_fg */
|
||||
if (g_np_prev) meow_widget_set_opts(g_np_prev, &op);
|
||||
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
|
||||
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
|
||||
if (g_np_next) meow_widget_set_opts(g_np_next, &op);
|
||||
return;
|
||||
}
|
||||
/* 取色无效 → 默认主题(回退):清 prog/prev/next 的 opts,play/pause 回主题 primary。 */
|
||||
memset(&op, 0, sizeof op);
|
||||
if (g_np_prog) meow_widget_set_opts(g_np_prog, &op);
|
||||
if (g_np_prev) meow_widget_set_opts(g_np_prev, &op);
|
||||
if (g_np_next) meow_widget_set_opts(g_np_next, &op);
|
||||
if (g_np_play || g_np_pause) {
|
||||
op.bg = th->primary; op.fg = th->onPrimary; /* 主播放钮:强调动作 */
|
||||
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
|
||||
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
|
||||
}
|
||||
}
|
||||
|
||||
/* 绘制播放控制三钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时
|
||||
* 上一帧残留被新画圆底盖掉)。控件配色在调用前由 np_theme_widgets 统一挂载取色主题。 */
|
||||
static void np_draw_ctl(const meow_media_session_t *s)
|
||||
{
|
||||
if (g_np_prev) { meow_widget_set_selected(g_np_prev, 0); meow_widget_draw(g_np_prev); }
|
||||
if (g_np_next) { meow_widget_set_selected(g_np_next, 0); meow_widget_draw(g_np_next); }
|
||||
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
|
||||
meow_widget *pb = playing ? g_np_pause : g_np_play;
|
||||
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
|
||||
}
|
||||
|
||||
/* 进度带(进度条 + 时间)绘制;pos = 当前显示位置。进度条走 SDK meow_progress。 */
|
||||
static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const meow_ui_theme_t *th)
|
||||
{
|
||||
fill_rect(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, th->outline);
|
||||
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * NP_PROG_W / s->dur_ms) : 0;
|
||||
if (fw > NP_PROG_W) fw = NP_PROG_W;
|
||||
if (fw > 0) fill_rect(NP_PROG_X, NP_PROG_Y, fw, 6, th->primary);
|
||||
int pct = (s->dur_ms > 0) ? (int)((uint64_t)pos * 100 / s->dur_ms) : 0;
|
||||
if (pct > 100) pct = 100;
|
||||
if (g_np_prog) { meow_progress_set(g_np_prog, pct); meow_widget_draw(g_np_prog); }
|
||||
/* 时间(左=当前,右=总长;MM:SS) */
|
||||
char l[16], rbuf[16];
|
||||
unsigned lm = pos / 60000u, ls = (pos % 60000u) / 1000u;
|
||||
unsigned dm = (s->dur_ms ? s->dur_ms : 0) / 60000u, ds = (s->dur_ms ? s->dur_ms : 0) % 60000u / 1000u;
|
||||
snprintf(l, sizeof l, "%u:%02u", lm, ls);
|
||||
snprintf(rbuf, sizeof rbuf, "%u:%02u", dm, ds);
|
||||
ui_draw(NP_PROG_X, NP_TIME_Y, l, th->on_surface_variant, 16);
|
||||
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, th->on_surface_variant, 16);
|
||||
uint32_t tcol = g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant;
|
||||
ui_draw(NP_PROG_X, NP_TIME_Y, l, tcol, 16);
|
||||
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, tcol, 16);
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* 批4:歌词页渲染(批2 meow_lyric_view + 数据批3 meow_media_lyric_*)。
|
||||
* - 有词:meow_lyric_parse(lrc,tlrc)→meow_lyric_view_set_pos(display_pos) 实时滚动,
|
||||
* 当前行由 meow_lyric_view 内部 meow_lyric_line_at(pos_ms) 计算(与绘制/点跳同源)。
|
||||
* - 无词/无源:清空并置 pos=0,视图内建空态文案(np.no_lyric→"暂无歌词")。
|
||||
* 主题:先走 meow_theme 默认(视图自绘 scrim/活动行纯白/灰阶递减);批5 封面取色动态主题的可
|
||||
* 接入点为 meow_lyric_view_set_colors(本批不取色,故不调用;接入只需在下方填 meow_lyric_view_colors
|
||||
* 后调用一次,字段 0=回主题默认)。
|
||||
* ================================================================== */
|
||||
static void np_lyric_refresh(const meow_media_session_t *s)
|
||||
{
|
||||
if (!g_np_lyric) return;
|
||||
int valid = meow_media_lyric_valid(s);
|
||||
if (valid) {
|
||||
/* 换歌/换源(新 lyric_seq/app_id)才重解析;否则沿用,只推进位置。 */
|
||||
if (g_np_lyric_seq != s->lyric_seq || strcmp(g_np_lyric_app, s->app_id) != 0) {
|
||||
meow_lyric_parse(meow_media_lyric_lrc(s), meow_media_lyric_tlrc(s));
|
||||
g_np_lyric_seq = s->lyric_seq;
|
||||
snprintf(g_np_lyric_app, sizeof g_np_lyric_app, "%s", s->app_id);
|
||||
}
|
||||
meow_lyric_view_set_pos(g_np_lyric, meow_media_display_pos(s));
|
||||
} else {
|
||||
/* 无词/无源:仅当上次仍有词才清空(省冗余 memset),置 pos=0。 */
|
||||
if (g_np_lyric_last_valid != 0) {
|
||||
meow_lyric_clear();
|
||||
g_np_lyric_seq = 0xFFFFFFFFu;
|
||||
g_np_lyric_app[0] = 0;
|
||||
}
|
||||
meow_lyric_view_set_pos(g_np_lyric, 0);
|
||||
}
|
||||
g_np_lyric_last_valid = valid;
|
||||
}
|
||||
|
||||
/* 批② home 指示条:底部上滑回桌面的可见反馈(镜向左缘返回提示条;唯位置=屏底左缘)。
|
||||
* 主题用 on_surface_variant(淡灰指示,iOS home 指示同款语义);draw 在整页/空态/歌词页
|
||||
* 均调用一次(np_draw_page 播放分支/空态分支 + np_draw_lyric_page)。 */
|
||||
static void np_draw_home_bar(void)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
meow_gfx_fill_round(NP_HOME_BAR_X, NP_HOME_BAR_Y, NP_HOME_BAR_W, NP_HOME_BAR_H,
|
||||
NP_HOME_BAR_H / 2, th->on_surface_variant);
|
||||
}
|
||||
|
||||
/* 网易云式歌词页封面背景(用户 2026-09-05):有封面 → 等比居中放大铺满内容区(cover-fit,
|
||||
* 超出部分由画布裁剪,镜像 ncm plr_draw_lyrics 的 meow_ui_img_scaled 算法);无封面 → 纯黑底
|
||||
* (现状回退)。本函数只铺底;压暗由调用方随后叠黑色半透明叠加层(alpha176)完成。 */
|
||||
static void np_draw_lyric_cover_bg(int W, int H)
|
||||
{
|
||||
int x = 0, y = SB_BAR_H, cw = W, ch = H - SB_BAR_H; /* 只盖内容区(状态栏下),与播放页一致 */
|
||||
if (!g_np_cover || meow_img_w(g_np_cover) <= 0 || meow_img_h(g_np_cover) <= 0) {
|
||||
fill_rect(x, y, cw, ch, 0x000000); /* 无封面 → 纯黑底(回退现状) */
|
||||
return;
|
||||
}
|
||||
int iw = meow_img_w(g_np_cover), ih = meow_img_h(g_np_cover);
|
||||
long dw = cw, dh = (long)ih * cw / iw; /* cover-fit:铺满宽度,高按等比 */
|
||||
if (dh < ch) { dh = ch; dw = (long)iw * ch / ih; } /* 宽度不足再铺满高度(超出裁剪) */
|
||||
meow_ui_img_scaled(x + (cw - (int)dw) / 2, y + (ch - (int)dh) / 2, (int)dw, (int)dh, g_np_cover);
|
||||
}
|
||||
|
||||
/* 歌词页整页绘制:网易云式封面背景(封面图当底 + 黑色半透明叠加层压暗,保证歌词对比度)→
|
||||
* lyric_view(自绘 scrim 关闭,仅歌词/空态)→ 顶部元信息(歌名/歌手)。 */
|
||||
static void np_draw_lyric_page(int redraw_sb)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int W = fb_xres, H = fb_yres;
|
||||
if (!g_np_init) np_widgets_create();
|
||||
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||
/* 批5:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
|
||||
* 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
|
||||
* 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
|
||||
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
|
||||
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
|
||||
np_cover_load(meow_media_current());
|
||||
np_cover_theme_update();
|
||||
/* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
|
||||
* 随后统一叠黑色半透明叠加层(alpha176)压暗,保证歌词对比度。歌词配色仍恒 ncm 白/灰
|
||||
* (不随封面取色——取色仅服务播放页文字/按钮)。歌词行布局/滚动逻辑不变,只换背景。 */
|
||||
np_draw_lyric_cover_bg(W, H);
|
||||
np_set_lyric_ncm_colors();
|
||||
/* 用户 2026-09-04:黑色半透明叠加层(alpha176)——压暗封面底,保证歌词可读;无封面时叠于
|
||||
* 纯黑底之上(仍纯黑,即现状回退)。 */
|
||||
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
|
||||
np_lyric_refresh(meow_media_current());
|
||||
if (g_np_lyric)
|
||||
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||||
np_draw_lyric_title(meow_media_current()); /* 顶部元信息(歌名/歌手,居左,避状态栏) */
|
||||
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
|
||||
if (redraw_sb) draw_statusbar();
|
||||
}
|
||||
|
||||
/* 整页绘制(空态占位)。redraw_sb=1 时喂/标脏状态栏(打开/主题变化),否则保留上次顶条。 */
|
||||
@@ -120,21 +446,36 @@ static void np_draw_page(int redraw_sb)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int W = fb_xres, H = fb_yres;
|
||||
if (!g_np_init) np_widgets_create(); /* M7.0b:lazy 建控件(跨进入复用) */
|
||||
/* 批4:歌词页整页走 lyric_view(无源/无词空态由视图内建)。 */
|
||||
if (g_np_page == NP_PAGE_LYRIC) {
|
||||
np_draw_lyric_page(redraw_sb);
|
||||
return;
|
||||
}
|
||||
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
if (!s || s->state == MEOW_MEDIA_STOPPED) {
|
||||
/* 空态占位 */
|
||||
const char *t1 = "未在播放";
|
||||
/* 空态占位(M7.0b 文案走 meow_str,未知 id 回退中文) */
|
||||
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
|
||||
const char *t1 = meow_str_zh("np.not_playing", "未在播放");
|
||||
ui_draw((W - ui_width(t1, 26)) / 2, 300, t1, th->on_surface_variant, 26);
|
||||
const char *t2 = "打开播放 App 以显示正在播放";
|
||||
const char *t2 = meow_str_zh("np.open_app_hint", "打开播放 App 以显示正在播放");
|
||||
ui_draw((W - ui_width(t2, 18)) / 2, 348, t2, th->on_surface_variant, 18);
|
||||
const char *t3 = "左缘右滑返回";
|
||||
const char *t3 = meow_str_zh("np.edge_back", "左缘右滑返回");
|
||||
ui_draw((W - ui_width(t3, 16)) / 2, 396, t3, th->on_surface_variant, 16);
|
||||
np_draw_home_bar(); /* 批②:空态也示 home 指示(底部上滑可回桌面) */
|
||||
if (redraw_sb) draw_statusbar();
|
||||
return;
|
||||
}
|
||||
np_cover_load(s);
|
||||
/* 批5:封面取色(主/次色)+ 背景渐变或主题面;并注入歌词页 set_colors。
|
||||
* np_cover_theme_update 幂等(封面路径不变不重取);无封面/取色失败回退 meow_theme。 */
|
||||
np_cover_theme_update();
|
||||
if (g_np_cc_valid)
|
||||
np_fill_gradient(0, SB_BAR_H, W, H - SB_BAR_H, g_np_cc.bg_top, g_np_cc.bg_bottom);
|
||||
else
|
||||
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
|
||||
np_apply_lyric_colors();
|
||||
/* 封面:解码缓存 + 圆角裁方;无封面回退主题色占位 + 首字 */
|
||||
if (g_np_cover) {
|
||||
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
|
||||
@@ -145,56 +486,59 @@ static void np_draw_page(int redraw_sb)
|
||||
const char *t = s->title[0] ? s->title : s->app_id;
|
||||
int cw = ui_width(t, 22);
|
||||
ui_draw(NP_COVER_X + (NP_COVER - cw) / 2, NP_COVER_Y + (NP_COVER - meow_font_line_height(22)) / 2,
|
||||
t, 0xFFFFFF, 22);
|
||||
t, meow_theme_get()->onPrimary, 22);
|
||||
}
|
||||
/* 元数据 */
|
||||
np_center_text(NP_TITLE_Y, s->title, 26, th->on_surface, W - 40);
|
||||
np_center_text(NP_ARTIST_Y, s->artist, 18, th->on_surface_variant, W - 60);
|
||||
/* 元数据(批5:封面取色时文字随 fg/fg_dim,否则主题面) */
|
||||
np_center_text(NP_TITLE_Y, s->title, 26, g_np_cc_valid ? g_np_cc.fg : th->on_surface, W - 40);
|
||||
np_center_text(NP_ARTIST_Y, s->artist, 18, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 60);
|
||||
if (s->album[0])
|
||||
np_center_text(NP_ALBUM_Y, s->album, 16, th->on_surface_variant, W - 80);
|
||||
/* 进度 */
|
||||
np_center_text(NP_ALBUM_Y, s->album, 16, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 80);
|
||||
/* 批5b:控件随播放页当前主题(进度条轨道/填充 + 三钮)——整页与局部重绘共用同一次派生。 */
|
||||
np_theme_widgets();
|
||||
/* 进度(SDK meow_progress) */
|
||||
uint32_t pos = meow_media_display_pos(s);
|
||||
np_draw_progress(s, pos, th);
|
||||
/* 控制按钮 */
|
||||
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
|
||||
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1;
|
||||
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, pk, th);
|
||||
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
|
||||
/* 歌词占位 */
|
||||
const char *ly = "歌词(占位)";
|
||||
ui_draw((W - ui_width(ly, 16)) / 2, NP_LYRIC_Y, ly, th->on_surface_variant, 16);
|
||||
/* 控制按钮(SDK meow_iconbtn,命中/状态走 SDK) */
|
||||
np_draw_ctl(s);
|
||||
/* 播放页底部(M6.33:歌词占位已移除——已有独立歌词页,占位多余;批②:底部仅示
|
||||
* home 指示条,非歌词占位)。 */
|
||||
np_draw_home_bar(); /* 批②:播放页示 home 指示(底部上滑可回桌面) */
|
||||
if (redraw_sb) draw_statusbar();
|
||||
}
|
||||
|
||||
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。 */
|
||||
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。
|
||||
* 批5b:局部重绘也走播放页当前主题——进度带/按钮带底色用取色渐变(np_page_bg_row 与整页同 span
|
||||
* 无缝衔接),进度条/三钮经 np_theme_widgets 挂取色色;无取色回退 meow_theme 面/默认控件色。 */
|
||||
static void np_repaint_progress(void)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
|
||||
np_cover_theme_update(); /* 取色当前主题(幂等,封面路径未变不重取) */
|
||||
uint32_t pos = meow_media_display_pos(s);
|
||||
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏)。 */
|
||||
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
|
||||
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏);底色随取色渐变或主题面。 */
|
||||
if (g_np_cc_valid) {
|
||||
for (int yy = NP_TIME_Y; yy < NP_BTN_Y + NP_PLAY_R; yy++)
|
||||
fill_rect(0, yy, fb_xres, 1, np_page_bg_row(yy));
|
||||
} else {
|
||||
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
|
||||
}
|
||||
np_theme_widgets(); /* 进度条/按钮随取色主题(或回退默认) */
|
||||
np_draw_progress(s, pos, th);
|
||||
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
|
||||
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1, th);
|
||||
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
|
||||
np_draw_ctl(s);
|
||||
meow_dirty_add(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y);
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
/* 控制按钮命中:返回 cmd(1-4),0=未命中。 */
|
||||
/* 控制按钮命中:返回 cmd(1-4),0=未命中。走 SDK 圆形命中(owner==哨兵 + enabled 闸门)。
|
||||
* 播放/暂停两钮同位:命中任一(同矩形)→ 按会话状态分派 play(1)/pause(2)。 */
|
||||
static int np_btn_tap(int tx, int ty)
|
||||
{
|
||||
if (tx >= NP_PREV_CX - NP_PREV_R && tx <= NP_PREV_CX + NP_PREV_R &&
|
||||
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 3;
|
||||
if (tx >= NP_PLAY_CX - NP_PLAY_R && tx <= NP_PLAY_CX + NP_PLAY_R &&
|
||||
ty >= NP_BTN_Y - NP_PLAY_R && ty <= NP_BTN_Y + NP_PLAY_R) {
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
if (g_np_prev && meow_widget_hit(g_np_prev, tx, ty)) return 3;
|
||||
if (g_np_play && meow_widget_hit(g_np_play, tx, ty))
|
||||
return (s && s->state == MEOW_MEDIA_PLAYING) ? 2 : 1;
|
||||
}
|
||||
if (tx >= NP_NEXT_CX - NP_PREV_R && tx <= NP_NEXT_CX + NP_PREV_R &&
|
||||
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 4;
|
||||
if (g_np_next && meow_widget_hit(g_np_next, tx, ty)) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -211,32 +555,79 @@ static int64_t np_progress_seek(int tx, int ty)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void run_now_playing(void)
|
||||
/* 播放页按键动作应用(音量 + 播放切换)。语义与主界面/子页一致:
|
||||
* - 音量键 = 系统音量(settings_volume_adj 内含插孔消抖 + 线性渐变);
|
||||
* - 播放键 = 媒体播放/暂停切换(路由最近活跃媒体会话,与主界面同判);
|
||||
* 边沿由调用方按防抖后稳定值计算(vold_edge = 稳定按下 ∧ 上一稳定为抬起)。
|
||||
* 抽成独立小函数供宿主单测(np_apply_key_actions 仅依赖 settings_volume_adj/
|
||||
* meow_media_playctl/now_ms/logmsg,可 -ffunction-sections 链接隔离)。 */
|
||||
static void np_apply_key_actions(int vold_edge, int volu_edge, int play_edge,
|
||||
const meow_media_session_t *s,
|
||||
unsigned long long *last_input)
|
||||
{
|
||||
if (vold_edge) {
|
||||
settings_volume_adj(-settings_volume_step);
|
||||
if (last_input) *last_input = now_ms();
|
||||
logmsg("np: volume DOWN <- ramp step=%d", settings_volume_step);
|
||||
} else if (volu_edge) {
|
||||
settings_volume_adj(+settings_volume_step);
|
||||
if (last_input) *last_input = now_ms();
|
||||
logmsg("np: volume UP <- ramp step=%d", settings_volume_step);
|
||||
}
|
||||
if (play_edge) {
|
||||
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
|
||||
if (last_input) *last_input = now_ms();
|
||||
}
|
||||
}
|
||||
|
||||
void run_now_playing(struct app *apps, int napps)
|
||||
{
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
struct keypoll kp_keys;
|
||||
kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_play;
|
||||
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
|
||||
kp_open(&kp_power, DEV_POWER);
|
||||
struct kd_state kd_play; /* 音量键防抖/基线上收 key_router(进程唯一) */
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 读播放键按住态(用于边沿) */
|
||||
int m_prev_play = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
}
|
||||
int np_prev_power = 0; /* 电源键边沿跟踪 */
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
|
||||
key_router_rebase(); /* 进入 Now Playing:音量基线对齐当前 raw(防残留边沿) */
|
||||
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
|
||||
|
||||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||
unsigned long long last_prog = now_ms();
|
||||
unsigned long long np_last_status_tick = now_ms(); /* M6.20:播放页状态栏时钟 1s 刷新(主界面同语义) */
|
||||
int need_redraw = 1;
|
||||
int np_exit = 0; /* M6.22:返回手势命中 → 置位退出外层主循环。触摸内的 break 只跳出内层
|
||||
* 读事件 while,不退出外层 while(!sigterm_flag),故必须靠标志跳出(根因:返回「没反应」)。 */
|
||||
char last_title[128] = "";
|
||||
uint32_t last_state = 0xFFFFFFFFu;
|
||||
char last_app[MEOW_APP_ID_MAX] = "";
|
||||
char last_art[200] = ""; /* M6.33:上次已刷新封面 artwork 路径(artwork 变化也触发整页重绘) */
|
||||
|
||||
meow_ui_activity_push(NP_SENTINEL_ACT); /* M7.0b:进入 push 哨兵(与函数尾 pop 配对)。
|
||||
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
||||
* owner==active 闸门——漏 push 则 active()!=0x51,
|
||||
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
||||
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
||||
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
||||
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
||||
g_np_lyric_app[0] = 0;
|
||||
np_draw_page(1);
|
||||
meow_dirty_full();
|
||||
meow_ui_flush();
|
||||
|
||||
while (!sigterm_flag) {
|
||||
if (np_exit) break; /* M6.22:返回手势已命中 → 退出本页,走尾部收尾回主界面 */
|
||||
meow_ipc_process();
|
||||
meow_media_tick();
|
||||
unsigned long long now = now_ms();
|
||||
@@ -256,22 +647,79 @@ void run_now_playing(void)
|
||||
if (td && !td_ok && tlx >= 0 && tly >= 0) { tdx = tlx; tdy = tly; td_ok = 1; }
|
||||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||||
if (val && !td) {
|
||||
if (now_ms() >= touch_cooldown_until) { td = 1; td_ok = 0; tlx = tly = -1; }
|
||||
if (now_ms() >= touch_cooldown_until) { td = 1; td_ok = 0; tlx = tly = -1;
|
||||
np_last_input = now_ms(); }
|
||||
} else if (!val && td) {
|
||||
td = 0;
|
||||
if (td_ok) {
|
||||
int dx = tlx - tdx, dy = tly - tdy;
|
||||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||||
/* 左缘右滑返回(全局手势语义):回到主界面(不清理媒体会话) */
|
||||
if (meow_edge_back(tdx, dx, dy)) break;
|
||||
if (adx < 60 && ady < 60) {
|
||||
int cmd = np_btn_tap(tlx, tly);
|
||||
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
|
||||
else {
|
||||
int64_t seek = np_progress_seek(tlx, tly);
|
||||
if (seek > 0) meow_media_playctl(5, seek);
|
||||
/* M6.21 诊断 + 批4 页感知:打印该次抬手手势的完整输入与按当前页
|
||||
* 的方向感知判定(真机定位「返回滑不出」根因曾依赖此日志;
|
||||
* 保留,键改为 ges=np_gesture_decide 返回值)。 */
|
||||
np_gest_t gnp = np_gesture_decide(g_np_page, tdx, tdy, dx, dy);
|
||||
/* 批②:底部上滑(home)优先——该手势竖向主导、起点在底部起滑区,
|
||||
* 与左缘返回(横向+左缘起点)/顶部下滑(顶部热区)互斥,故先判 home
|
||||
* 覆盖(np_gesture_decide 对纵滑返回 NONE,不冲突)。 */
|
||||
if (np_home_gesture(tdx, tdy, dx, dy, fb_yres))
|
||||
gnp = NP_GEST_HOME;
|
||||
logmsg("np up: page=%d down=(%d,%d) d=(%d,%d) adx=%d ady=%d ges=%d",
|
||||
g_np_page, tdx, tdy, dx, dy, adx, ady, (int)gnp);
|
||||
switch (gnp) {
|
||||
case NP_GEST_TOP:
|
||||
/* 顶部热区下滑 → 打开通知中心(与主界面同语义;mnp_top_swipe_down
|
||||
* 内部已排除左缘 down_x<24,与下方返回手势互斥)。run_notif_center
|
||||
* 为阻塞模态,返回后整页重画。 */
|
||||
{
|
||||
notif_click_t click;
|
||||
memset(&click, 0, sizeof click);
|
||||
click.app_idx = -1;
|
||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||
run_notif_center(apps, napps, &click, 0, 0, 0);
|
||||
need_redraw = 1;
|
||||
meow_dirty_full();
|
||||
meow_ui_flush();
|
||||
}
|
||||
break;
|
||||
case NP_GEST_EXIT:
|
||||
/* 播放页左缘右滑 → 回桌面(保留 M6.26 全局手势语义)。 */
|
||||
logmsg("np: BACK break (exit, page=%d)", g_np_page);
|
||||
np_exit = 1;
|
||||
break;
|
||||
case NP_GEST_HOME:
|
||||
/* 批②:底部上滑 → 回桌面(后台)。媒体会话续播/进程保活归批①
|
||||
* keepalive(本页退出不改保活核心),此处仅走回桌面的既有路径。 */
|
||||
logmsg("np: HOME bottom-swipe-up -> back to desktop (bg)");
|
||||
np_exit = 1;
|
||||
break;
|
||||
case NP_GEST_NEXT:
|
||||
/* 播放页 右→左滑 → 进歌词页。 */
|
||||
g_np_page = NP_PAGE_LYRIC;
|
||||
need_redraw = 1;
|
||||
logmsg("np: -> lyric page");
|
||||
break;
|
||||
case NP_GEST_PREV:
|
||||
/* 歌词页 左→右滑 → 回播放页(含左缘;不接桌面,避免误退)。 */
|
||||
g_np_page = NP_PAGE_PLAY;
|
||||
need_redraw = 1;
|
||||
logmsg("np: -> play page");
|
||||
break;
|
||||
case NP_GEST_TAP:
|
||||
/* 近点按(按页分派):播放页=三钮/进度看点;歌词页预留点跳
|
||||
* (本批不启用,见 batch5/后续:借 meow_lyric_view_hit_line + on_seek)。 */
|
||||
if (g_np_page == NP_PAGE_PLAY) {
|
||||
int cmd = np_btn_tap(tlx, tly);
|
||||
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
|
||||
else {
|
||||
int64_t seek = np_progress_seek(tlx, tly);
|
||||
if (seek > 0) meow_media_playctl(5, seek);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break; /* NP_GEST_NONE:反向/纵向主导,忽略不误切 */
|
||||
}
|
||||
if (np_exit) break; /* M6.22:返回手势已命中 → 跳出内层读事件 while */
|
||||
tdx = tdy = -1;
|
||||
} else { tdx = tdy = -1; }
|
||||
}
|
||||
@@ -280,31 +728,73 @@ void run_now_playing(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* 播放键边沿 → 播放/暂停切换 */
|
||||
/* 播放键/音量键边沿 → 媒体播放/暂停切换 + 系统音量(与主界面/子页一致)。
|
||||
* 修复:run_now_playing 原先只接播放/电源键,未接音量键 → 播放页/歌词页(同一循环,
|
||||
* 与页无关)音量键无反应。 */
|
||||
key_evt_t kev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
int play = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (play && !m_prev_play) {
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
|
||||
}
|
||||
(void)kd_tick(&kd_play, play);
|
||||
play = kd_play.stable;
|
||||
/* 音量键/播放键边沿 → 动作(与主界面/子页同语义;见 np_apply_key_actions) */
|
||||
np_apply_key_actions(kev.vol_dn_edge, kev.vol_up_edge,
|
||||
play && !m_prev_play, meow_media_current(), &np_last_input);
|
||||
m_prev_play = play;
|
||||
/* 音量线性渐变(按住长爬;与主界面/子页同语义,状态栏即时刷新)。
|
||||
* Now Playing 用 raw held(kev.vol_held_*,同现状 rvold/rvolu)。 */
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
if (!going) logmsg("np: volume ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
|
||||
/* 媒体变化检测(元数据/源/状态):立即整页重画 */
|
||||
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
|
||||
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
|
||||
* NP 退出后生效(可接受偏差,记日志)。 */
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
int pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
if (pwr && !np_prev_power) {
|
||||
np_last_input = now_ms();
|
||||
if (meow_screen_on) { meow_panel_off(); logmsg("nowplaying: power tap -> panel off"); }
|
||||
else { meow_panel_on(); need_redraw = 1; logmsg("nowplaying: power tap -> panel on"); }
|
||||
}
|
||||
np_prev_power = pwr;
|
||||
|
||||
/* M2.92:NP 页自动息屏(与主界面同超时;页面无触摸/按键计时器不走主循环) */
|
||||
if (screen_off_timeout > 0 && meow_screen_on &&
|
||||
now_ms() - np_last_input >= (unsigned long long)screen_off_timeout * 1000) {
|
||||
meow_panel_off();
|
||||
meow_auto_slept = 1;
|
||||
logmsg("nowplaying: idle %ds -> auto panel off", screen_off_timeout);
|
||||
}
|
||||
/* 息屏期只低频轮询按键(唤醒用),不重画不读触摸(FBIOBLANK 已挂起 goodix) */
|
||||
if (!meow_screen_on) { usleep(100 * 1000); continue; }
|
||||
|
||||
/* 媒体变化检测(元数据/源/状态/artwork):立即整页重画。
|
||||
* M6.33 补 artwork:ncm 状态切换可能先报空 artwork 再复原(标题/源/状态未变),
|
||||
* 原检测漏 artwork → 封面清空后不再触发整页重绘 → 播放页/歌词页封面永不恢复。 */
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
int meta_changed = !s || strcmp(last_app, s->app_id) ||
|
||||
strcmp(last_title, s->title) || last_state != s->state;
|
||||
strcmp(last_title, s->title) || strcmp(last_art, s->artwork) ||
|
||||
last_state != s->state;
|
||||
if (meta_changed) {
|
||||
if (s) {
|
||||
snprintf(last_app, sizeof last_app, "%s", s->app_id);
|
||||
snprintf(last_title, sizeof last_title, "%s", s->title);
|
||||
snprintf(last_art, sizeof last_art, "%s", s->artwork);
|
||||
last_state = s->state;
|
||||
} else { last_app[0] = 0; last_title[0] = 0; last_state = 0xFFFFFFFFu; }
|
||||
} else { last_app[0] = 0; last_title[0] = 0; last_art[0] = 0; last_state = 0xFFFFFFFFu; }
|
||||
need_redraw = 1;
|
||||
}
|
||||
/* 空态/无源:立即重画(清占位/回空态) */
|
||||
if (!s || s->state == MEOW_MEDIA_STOPPED) need_redraw = 1;
|
||||
/* M2.92 修死循环:空态/停止态只在「变化」时画一次(meta_changed 已覆盖
|
||||
* NULL↔会话切换),不再每 10ms 整页重画(旧行为 = 100% CPU 忙刷 → 卡死观感)。 */
|
||||
|
||||
if (need_redraw) {
|
||||
np_draw_page(0);
|
||||
@@ -313,14 +803,33 @@ void run_now_playing(void)
|
||||
need_redraw = 0;
|
||||
last_prog = now;
|
||||
} else if (s && (now - last_prog >= 1000)) {
|
||||
/* 进度推进:仅重绘进度带 + 按钮 */
|
||||
/* 进度推进(按页分流):播放页=进度带+按钮;歌词页=歌词实时滚动重绘(当前行随 pos)。 */
|
||||
last_prog = now;
|
||||
np_repaint_progress();
|
||||
if (g_np_page == NP_PAGE_LYRIC) {
|
||||
/* 批6:整页重绘(封面背景+scrim+歌词),避免 lyric_view 半透明 scrim 每 1s
|
||||
* 叠涂把封面/背景逐秒压黑(批4 既有「只重绘控件不重铺背景」结构修正)。 */
|
||||
np_draw_lyric_page(0);
|
||||
meow_dirty_add(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H);
|
||||
meow_ui_flush();
|
||||
} else {
|
||||
np_repaint_progress();
|
||||
}
|
||||
}
|
||||
/* M6.20:播放页状态栏时钟 1s 刷新(与主界面/子页同语义;draw_clock 喂 SDK 全局
|
||||
* 状态栏 + 标脏顶条,SDK present() 重绘——否则时间冻结在进入播放页那一刻)。 */
|
||||
if (now - np_last_status_tick >= 1000) {
|
||||
np_last_status_tick = now;
|
||||
draw_clock();
|
||||
}
|
||||
usleep(10000);
|
||||
}
|
||||
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_keys);
|
||||
kp_close(&kp_power);
|
||||
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
|
||||
g_np_cover_path[0] = 0;
|
||||
g_np_cc_valid = 0; /* 批5:退出清取色缓存,下次进入重新取 */
|
||||
g_np_cc_path[0] = 0;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/* np_gesture.h — Now Playing 双页手势方向感知(header-only 纯逻辑,宿主可单测)。
|
||||
*
|
||||
* 定案(用户,批4):Now Playing 同一容器两页——播放页 + 歌词页,手势按当前页方向感知:
|
||||
* - 播放页:右→左滑 → 进歌词页;任意右滑 → 回桌面(用户拍板保留任意右滑)。
|
||||
* - 歌词页:左→右滑 → 回播放页(不是回桌面);左缘右滑不接桌面(避免误退)。
|
||||
* 本头为 now_playing.c 与宿主单测(test_now_playing_widgets.c)共用的唯一手势决策,避免两端漂移。
|
||||
*
|
||||
* 依赖:仅 <stdint.h> + meow_notif_panel.h(mnp_top_swipe_down,纯函数)。左缘返回逻辑自
|
||||
* (re)实现(镜像 meow_touch.c meow_edge_back 同口径,宿主编译无需链 meow_touch.c)。
|
||||
*/
|
||||
#ifndef NP_GESTURE_H
|
||||
#define NP_GESTURE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "meow_notif_panel.h" /* mnp_top_swipe_down(顶部下滑→通知中心) */
|
||||
|
||||
/* ---- 页面 ---- */
|
||||
#define NP_PAGE_PLAY 0
|
||||
#define NP_PAGE_LYRIC 1
|
||||
|
||||
/* ---- 手势动作 ---- */
|
||||
typedef enum {
|
||||
NP_GEST_NONE = 0, /* 无(横向反向滑/纵向主导不匹配;调用方忽略,不误切页) */
|
||||
NP_GEST_TOP, /* 顶部热区下滑 → 打开通知中心 */
|
||||
NP_GEST_NEXT, /* 播放页 右→左滑 → 进入歌词页 */
|
||||
NP_GEST_PREV, /* 歌词页 左→右滑 → 回到播放页 */
|
||||
NP_GEST_EXIT, /* 播放页 任意右滑 → 返回桌面 */
|
||||
NP_GEST_HOME, /* 批②:底部上滑 → 返回桌面(后台保留) */
|
||||
NP_GEST_TAP, /* 近点按(按当前页分派:播放页=按钮/进度看点) */
|
||||
} np_gest_t;
|
||||
|
||||
/* ---- 常量(对齐 launcher 同口径;#ifndef 防与 meow_touch.h 等重复定义冲突)---- */
|
||||
#ifndef NP_SWIPE_THRESH
|
||||
#define NP_SWIPE_THRESH 65 /* 顶部下滑触发位移阈值(对齐 launcher MENU_SWIPE_THRESH) */
|
||||
#endif
|
||||
#ifndef NP_PAGE_SWIPE
|
||||
#define NP_PAGE_SWIPE 40 /* 横向切页最小位移(对齐 M6.26 np_anyright 的 dx>=40) */
|
||||
#endif
|
||||
#ifndef NP_TAP_MAX
|
||||
#define NP_TAP_MAX 60 /* 点按位移上限(对齐现有 adx<60 && ady<60) */
|
||||
#endif
|
||||
#ifndef NP_TOP_HOTZONE
|
||||
#define NP_TOP_HOTZONE 28 /* 顶部热区高(= launcher SB_BAR_H;通知中心/播放页同) */
|
||||
#endif
|
||||
#ifndef NP_EDGE_W
|
||||
#define NP_EDGE_W 24 /* 左缘起点宽度(镜像 meow_touch.h MEOW_EDGE_W) */
|
||||
#endif
|
||||
#ifndef NP_EDGE_SWIPE
|
||||
#define NP_EDGE_SWIPE 65 /* 左缘右滑位移阈值(镜像 meow_touch.h MEOW_EDGE_SWIPE) */
|
||||
#endif
|
||||
#ifndef NP_HOME_EDGE_H
|
||||
#define NP_HOME_EDGE_H 28 /* 底部起滑区高(自屏底向上;镜像顶部热区 NP_TOP_HOTZONE 同为 28) */
|
||||
#endif
|
||||
#ifndef NP_HOME_SWIPE
|
||||
#define NP_HOME_SWIPE 65 /* 底部上滑位移阈值(镜像 NP_SWIPE_THRESH / meow_edge_back 同口径) */
|
||||
#endif
|
||||
|
||||
/* 左缘返回(镜像 meow_touch.c meow_edge_back:down_x∈[0,NP_EDGE_W) 且 dx>=NP_EDGE_SWIPE
|
||||
* 且横向主导)。为宿主测试免链 meow_touch.c,此处 static inline 复刻同口径。 */
|
||||
static inline int np_edge_back(int down_x, int dx, int dy)
|
||||
{
|
||||
int ady = dy < 0 ? -dy : dy;
|
||||
if (down_x >= 0 && down_x < NP_EDGE_W && dx >= NP_EDGE_SWIPE && ady < dx)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 批② 底部上滑返回桌面(home,与左缘返回 np_edge_back 对称):原点在底部起滑区
|
||||
* (down_y∈[fb_h-NP_HOME_EDGE_H, fb_h))且**不在左缘**(down_x>=NP_EDGE_W,与 np_edge_back 按
|
||||
* 原点分界互斥——左缘 24px 列留给左缘返回)+ 向上位移≥NP_HOME_SWIPE 且竖向主导(adx<-dy)。
|
||||
* fb_h=屏高(调用方喂 fb_yres,宿主单测亦可喂固定值)。其余(顶部/中部起滑、横向主导、
|
||||
* 向下滑、未达阈值)一律 0,不与顶部下滑/横向切页/点按冲突。 */
|
||||
static inline int np_home_gesture(int down_x, int down_y, int dx, int dy, int fb_h)
|
||||
{
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
if (down_x >= NP_EDGE_W && down_y >= fb_h - NP_HOME_EDGE_H && down_y >= 0 &&
|
||||
dy <= -NP_HOME_SWIPE && adx < -dy)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 按当前页把手势几何 → 动作。down_x/down_y=抬手原点;dx/dy=抬手位移(释放-原点,即 tlx-tdx)。
|
||||
* 优先级:顶部下滑 > 横向主导切页/返回 > 点按。横向反向或无主导 → NONE(调用方忽略,不误切)。 */
|
||||
static inline np_gest_t np_gesture_decide(int page, int down_x, int down_y, int dx, int dy)
|
||||
{
|
||||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||||
/* 顶部热区下滑 → 通知中心(与主界面同语义;mnp_top_swipe_down 内部排除左缘起点) */
|
||||
if (mnp_top_swipe_down(down_x, down_y, dx, dy, NP_TOP_HOTZONE, NP_SWIPE_THRESH))
|
||||
return NP_GEST_TOP;
|
||||
/* 横向主导(adx>=NP_PAGE_SWIPE 且横向位移>竖向)→ 按当前页分流切页/返回 */
|
||||
if (adx >= NP_PAGE_SWIPE && adx > ady) {
|
||||
if (page == NP_PAGE_PLAY) {
|
||||
if (dx >= NP_PAGE_SWIPE) return NP_GEST_EXIT; /* 任意右滑 → 回桌面(含左缘/非左缘;用户拍板保留任意右滑) */
|
||||
if (dx <= -NP_PAGE_SWIPE) return NP_GEST_NEXT; /* 右→左滑 → 进歌词页 */
|
||||
return NP_GEST_NONE; /* 其余忽略 */
|
||||
} else { /* NP_PAGE_LYRIC */
|
||||
if (dx >= NP_PAGE_SWIPE) return NP_GEST_PREV; /* 左→右滑 → 回播放页(含左缘,不接桌面) */
|
||||
return NP_GEST_NONE; /* 右→左滑=歌词页是末页,忽略 */
|
||||
}
|
||||
}
|
||||
/* 非横向主导:近点按 */
|
||||
if (adx < NP_TAP_MAX && ady < NP_TAP_MAX) return NP_GEST_TAP;
|
||||
return NP_GEST_NONE;
|
||||
}
|
||||
|
||||
#endif /* NP_GESTURE_H */
|
||||
@@ -0,0 +1,94 @@
|
||||
/* overlay_gate.h — SIGSTOP 叠加会话(通知中心 §10.5 / 锁屏 M2.x)关闭与死亡回退的**纯决策**。
|
||||
*
|
||||
* 宿主可单测(无信号/无设备/无文件系统依赖,全部 static inline);app_launcher.c 的两处叠加
|
||||
* 关闭路径(run_app_notif_overlay / run_app_lock_overlay)调用,避免把「排空窗口」「中止条件」
|
||||
* 这类时序/判定散落内联难验证。三个 bug 的定案语义都收敛到这里:
|
||||
*
|
||||
* ① 输入穿透(S1 根因):App 被 SIGSTOP 冻结过 → 关闭时必须在 /tmp/meow_notif_active 仍存在
|
||||
* 时先 SIGCONT,让 SDK 输入线程(meow_ui_input.c overlay_input_suppressed)排空冻结期缓冲的
|
||||
* evdev 事件副本 + 对齐 EVIOCGKEY,再等 OVERLAY_DRAIN_WINDOW_MS 后把标记内容更新为前台归属
|
||||
* (notif_active_set(a->id),批①内容化标记取代旧 unlink;清标记/置空会连后台常驻 App 一起放行)。
|
||||
* 若反序(清标记->SIGCONT,改动前写法),App 在无隔离态恢复 → 解冻瞬间把积累的触摸回放成
|
||||
* App 内点击(锁屏穿透 /「启动奇怪界面」/ 解锁后状态栏消失的触发源)。
|
||||
* ② 状态栏恢复(S2):解锁后强制 g_sb_mode_last=-1 让 app 循环立即重叠加,不等 1s 慢刷。
|
||||
* ③ 死亡重绘(S3):锁屏叠加中 App 崩溃(/proc/<pid>/stat==Z)→ 中止锁屏走统一退出路径,
|
||||
* 由 waitpid 循环回收 + run_menu dirty=1 重绘主屏(含状态栏)。
|
||||
*/
|
||||
#ifndef MEOW_OVERLAY_GATE_H
|
||||
#define MEOW_OVERLAY_GATE_H
|
||||
|
||||
/* SDK overlay_input_suppressed() 的标记存在性节流窗是 200ms;排空窗须 > 该值(留 100ms 余量),
|
||||
* 保证 App 恢复后 SDK 输入线程确实重新 stat 到标记再整批丢弃缓冲事件。 */
|
||||
#define OVERLAY_DRAIN_WINDOW_MS 300
|
||||
|
||||
/* ① 排空是否必要:App 被 SIGSTOP 冻结过(非保活)才需要「SIGCONT 保持隔离 → 排窗 → unlink」;
|
||||
* 媒体保活(keep_running,从未 STOP)直接「恢复帧上屏 → unlink」(自抑随标记消失自然解除)。 */
|
||||
static inline int overlay_drain_required(int keep_running)
|
||||
{
|
||||
return !keep_running ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ③ 锁屏叠加中 App 崩溃是否触发「统一退出路径」(= 中止锁屏、交回 waitpid 循环回收、run_menu 重绘)。
|
||||
* proc_zombie=1(/proc/<pid>/stat==Z)→ 走退出(返回 1);否则正常恢复(返回 0)。 */
|
||||
static inline int lock_overlay_app_died_exit(int proc_zombie)
|
||||
{
|
||||
return proc_zombie ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ② 解锁后是否需要强制状态栏重同步(返回 1):始终需要——把 g_sb_mode_last 拨回 -1,
|
||||
* 使 run_menu / app 循环的「模式变化→立即重叠加」分支命中(-1 != m 恒真除非 m==-1,
|
||||
* 但 m==-1 表示无模式文件=展开态,同样被 app_overlay_statusbar 兜底到展开整条)。 */
|
||||
static inline int overlay_force_sb_resync(void)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 未冻结(媒体保活,keep_running)App 是否**需要 EVIOCGRAB 独占隔离**(返回 1)。
|
||||
* §310 问题1真根:媒体保活 App 从不 SIGSTOP(保 PCM),SDK 输入线程**实时读 evdev**(不走缓冲),
|
||||
* 标记(/tmp/meow_notif_active)让 SDK 排空只在有该新 SDK 的 App 上生效;launcher 侧可**无条件**
|
||||
* 用 EVIOCGRAB 独占触控设备,使包括保活 App 在内的**任何**前台进程在锁屏期收不到触摸(且抓取期
|
||||
* 事件不为未抓取 fd 缓冲/回放)。keep_alive=1 → 隔离靠 grab(overlay_drain_required==0,无
|
||||
* SIGCONT 排空语义);冻结 App(keep_running=0)→ 隔离靠 SIGSTOP+SIGCONT 排空(drain_required==1)。 */
|
||||
static inline int overlay_keepalive_needs_grab(int keep_running)
|
||||
{
|
||||
return keep_running ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 锁屏进场后「重铺全屏不透明底」的阈值(ms)。SDK present 抑制标记节流窗(MEOW_OVERLAY_CHK_MS)
|
||||
* 是 200ms——媒体保活 App 不冻结、锁定瞬间还会进 1-2 帧(部分脏区盖到锁屏上)。阈值取 300ms
|
||||
* (> 200ms),保证覆盖到抑制窗内 App 的残帧;App 此后被抑制不再上屏,锁屏稳定遮盖。
|
||||
* 由 lock_screen_ui.c 的 run_lock_screen 重铺逻辑使用(单测同源断言)。 */
|
||||
#define OVERLAY_LOCK_COVER_MS 300
|
||||
|
||||
/* 唤醒是否应触发锁屏(§310 M6.5 问题1):超时息屏(meow_auto_slept)与电源键手动息屏(watchdog
|
||||
* 直接 blank 面板、背光 0 → launcher 检测得 app_panel_manual_off)两类唤醒都须走 lck_wake_should_lock
|
||||
* 语义——真机实测「App 内电源键手动息屏→亮屏无锁屏」(手动路径丢了锁屏触发、超时路径接了)。 */
|
||||
static inline int overlay_wake_should_lock(int auto_slept, int manual_panel_off)
|
||||
{
|
||||
return (auto_slept || manual_panel_off) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 锁屏页自睡阈值(秒,§310 M6.7,294 遗留):min(系统 screen_off_timeout, 30s)。
|
||||
* 系统未设/设 0(常亮)/超 30 → 锁屏页封顶 30s(锁屏页不应常亮耗电);系统设更短 → 尊重更短。
|
||||
* 返回正数秒。lock_screen_ui.c 的 run_lock_screen 空闲超时用它(单测同源断言)。 */
|
||||
static inline int overlay_lock_idle_off_sec(int screen_off_timeout)
|
||||
{
|
||||
return (screen_off_timeout > 0 && screen_off_timeout < 30)
|
||||
? screen_off_timeout : 30;
|
||||
}
|
||||
|
||||
/* 锁屏页是否应自睡(§310 M6.7):屏幕亮(meow_screen_on)且在本循环 && 空闲(now-last_act) ≥ 阈值。
|
||||
* 返回 1=超时灭屏(锁态保持)。 */
|
||||
static inline int overlay_lock_should_sleep(int screen_on, unsigned long long idle_ms,
|
||||
unsigned long long off_ms)
|
||||
{
|
||||
return (screen_on && idle_ms >= off_ms) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 锁屏自睡后应否唤醒回锁屏页:电源键按下边沿 或 触摸活动。返回 1=唤醒(meow_panel_on+重画锁屏)。 */
|
||||
static inline int overlay_lock_wake_now(int pwr_edge, int touch_ready)
|
||||
{
|
||||
return (pwr_edge || touch_ready) ? 1 : 0;
|
||||
}
|
||||
|
||||
#endif /* MEOW_OVERLAY_GATE_H */
|
||||
@@ -59,11 +59,71 @@ static const unsigned char PM_ICON_CANCEL[3136]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
};
|
||||
|
||||
static const pm_icon_t pm_icons[5] = {
|
||||
static const unsigned char PM_ICON_LOCK[3136]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
};
|
||||
|
||||
static const pm_icon_t pm_icons[6] = {
|
||||
{ PM_ICON_POWER },
|
||||
{ PM_ICON_RESTART },
|
||||
{ PM_ICON_BACK },
|
||||
{ PM_ICON_UPGRADE },
|
||||
{ PM_ICON_LOCK },
|
||||
{ PM_ICON_CANCEL },
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||||
#include "meow_launcher_internal.h"
|
||||
|
||||
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530 }; /* 圆心 y(行距 140) */
|
||||
/* M2.x 锁屏:电源菜单增「锁定」手动锁。网格 2 列×3 行:0=关机 1=重启 2=回HiBy 3=升级
|
||||
* 4=锁定 5=取消;取消仍是安全默认项(PM_NUM-1),电源键确认/导航不会误触发关机。 */
|
||||
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530, 530 }; /* 圆心 y(行距 140) */
|
||||
|
||||
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "取消" };
|
||||
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "锁定", "取消" };
|
||||
|
||||
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "CANCEL" };
|
||||
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "LOCK", "CANCEL" };
|
||||
|
||||
|
||||
/* 电源菜单圆钮图标:从 pm_icons.h 的 56×56 alpha 位图 blit 到 fb(以圆钮中心 cx,cy 为锚居中)。
|
||||
@@ -39,6 +41,10 @@ static void blit_pm_icon(int idx, int cx, int cy, uint16_t col)
|
||||
}
|
||||
|
||||
|
||||
#if 0 /* ================================================================
|
||||
* M7.0b 退役:电源菜单圆钮改走 SDK 控件(meow_card/meow_iconbtn/自定义画布),
|
||||
* 命中/选中/按压态全由 SDK 接管;本块保留作回退位(删除 #if 0 行恢复旧手绘)。
|
||||
* ================================================================ */
|
||||
/* 绘制电源菜单圆钮 i(MD3 圆形:外描边+内填充+圆内 Material 图标;选中高亮)。
|
||||
* 圆内放 Material Symbols 位图,文字标签放圆下(Android 12 式:icon 上、label 下)。 */
|
||||
static void draw_pm_button(int i, int selected)
|
||||
@@ -69,6 +75,186 @@ static void draw_pm_button(int i, int selected)
|
||||
draw_text(cx - lw / 2, ly + 1, lab, 1, col);
|
||||
}
|
||||
}
|
||||
#endif /* M7.0b 退役:电源菜单旧手绘圆钮 */
|
||||
|
||||
/* ==================================================================
|
||||
* M7.0b 电源菜单控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
|
||||
* - 圆角卡 → meow_card(SDK 主题 surfaceContainerLow);
|
||||
* - 六个圆钮 → meow_widget_custom_create(pm_icons 专有位图:关机/重启/回HiBy/升级/锁定/取消,
|
||||
* 均 56×56 1:1 blit,无放大)。meow_iconbtn 分支保留(回HiBy/取消曾改走 sb_icons,因放大锯齿
|
||||
* 定案回退 pm_icons,见 pm_sdk_icon[] 注释;后续 SDK 出更高清图标可再启用);
|
||||
* - 命中/选中/按压态全走 SDK(meow_widget_hit/set_state/draw);
|
||||
* - 主题 = 读 SDK meow_theme(单一权威),后台遮罩仍 dim_screen(会话骨架不动)。
|
||||
* 控件 lazy 创建一次(owner=PM_SENTINEL_ACT 哨兵 activity),跨多次进入复用;
|
||||
* show_power_menu() 进入时 push 哨兵、退出时 pop(owner 闸门使控件命中生效)。
|
||||
* ================================================================== */
|
||||
#define PM_SENTINEL_ACT 0x50 /* 电源菜单哨兵 activity(不与 launcher 其它页冲突) */
|
||||
#define PM_BTN_SIZE (PM_R * 2) /* 圆钮 = 外接正方形边长(直径) */
|
||||
|
||||
/* 哪些 pm 图标走 SDK sb_icons 表(可用 meow_iconbtn 直接出图);-1=不在 → 自定义画布 blit pm_icons。
|
||||
* M7.0b 曾把 2=回HiBy / 5=取消 走 meow_iconbtn(SB_ARROW_BACK / SB_CLOSE),但 meow_iconbtn 按
|
||||
* 0.6*size 把 32×32 sb_icons 位图放大到 ~60px(>位图原生墨高),最近邻放大导致明显锯齿;且此尺寸
|
||||
* 大于 M6.8 及更早电源菜单圆钮图标(pm_icons 56×56,1:1 无放大)。定案:这两钮也回退 pm_icons 专有
|
||||
* 位图(PM_ICON_BACK/PM_ICON_CANCEL,同为 Material Symbols 56×56),统一其余四钮画法,恢复旧尺寸。
|
||||
* pm_icons[] 全部专用 → meow_iconbtn 分支保留(后续 SDK 出更高清图标可再启用)。 */
|
||||
static const int pm_sdk_icon[PM_NUM] = {
|
||||
-1, /* 0 关机:pm_icons 专有(SDK 无 power 位图) */
|
||||
-1, /* 1 重启:pm_icons 专有 */
|
||||
-1, /* 2 回HiBy:pm_icons 专有(恢复 M6.8 56×56,避免 meow_iconbtn 放大锯齿) */
|
||||
-1, /* 3 升级:pm_icons 专有 */
|
||||
-1, /* 4 锁定:pm_icons 专有(SDK 无 lock 位图) */
|
||||
-1, /* 5 取消:pm_icons 专有(同 2,恢复旧尺寸) */
|
||||
};
|
||||
|
||||
/* 圆钮文案 i18n id(meow_str)。common.cancel 内嵌已有;power.* 需 TF 覆盖表/后续并入 SDK 内嵌。 */
|
||||
static const char *pm_str_id[PM_NUM] = {
|
||||
"power.off", "power.restart", "power.back_hiby",
|
||||
"power.upgrade", "power.lock", "common.cancel",
|
||||
};
|
||||
|
||||
static meow_widget *g_pm_card = NULL;
|
||||
static meow_widget *g_pm_btns[PM_NUM] = {NULL};
|
||||
static int g_pm_init = 0;
|
||||
|
||||
/* 按主题混合(pct 0..100:base 向 top 混 pct%,24-bit 每通道;近似 SDK state_tint/blend)。 */
|
||||
static uint32_t pm_blend(uint32_t base, uint32_t top, int pct)
|
||||
{
|
||||
int br = (base >> 16) & 0xFF, bg = (base >> 8) & 0xFF, bb = base & 0xFF;
|
||||
int tr = (top >> 16) & 0xFF, tg = (top >> 8) & 0xFF, tb = top & 0xFF;
|
||||
int r = (br * (100 - pct) + tr * pct) / 100;
|
||||
int g = (bg * (100 - pct) + tg * pct) / 100;
|
||||
int b = (bb * (100 - pct) + tb * pct) / 100;
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
|
||||
}
|
||||
|
||||
/* 自定义画布:绘制 pm_icons 专有圆钮(图标不在 SDK sb_icons 表时用)。
|
||||
* SDK 已把绘制 clip 设为控件矩形;圆形底色按 selected 取主题容器色,图标墨色随之;
|
||||
* 禁用/按压态由本回调自叠(SDK 不替 W_CUSTOM 自动叠,meow_widget_draw_state_layer 铺方矩形,
|
||||
* 圆形钮此处手工按圆域混合,更贴合)。 */
|
||||
static void pm_render_cb(meow_widget *w, const meow_theme *th, void *ctx)
|
||||
{
|
||||
int i = (int)(intptr_t)ctx;
|
||||
int rc[4]; meow_widget_rect(w, rc); /* 绝对坐标 x,y,w,h(launcher 侧 meow_widget 不透明) */
|
||||
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
|
||||
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
|
||||
unsigned st = meow_widget_state(w);
|
||||
int sel = (st & MEOW_WS_SELECTED) != 0;
|
||||
int disabled = (st & MEOW_WS_ENABLED) == 0;
|
||||
int pressed = (st & MEOW_WS_PRESSED) != 0;
|
||||
uint32_t base = sel ? th->primaryContainer : th->surfaceContainer;
|
||||
uint32_t fg = sel ? th->onPrimaryContainer : th->onSurface;
|
||||
if (disabled) base = pm_blend(base, th->onSurface, 38); /* 禁用变暗(向灰) */
|
||||
else if (pressed) base = pm_blend(base, th->onSurface, sel ? 24 : 12); /* 按压状态层 */
|
||||
meow_gfx_fill_circle(cx, cy, r, base);
|
||||
blit_pm_icon(i, cx, cy, rgb565(fg));
|
||||
}
|
||||
|
||||
/* 自定义画布命中:圆形(圆心=控件中心,半径=size/2)。 */
|
||||
static int pm_hit_cb(meow_widget *w, int x, int y, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int rc[4]; meow_widget_rect(w, rc);
|
||||
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
|
||||
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
|
||||
int dx = x - cx, dy = y - cy;
|
||||
return dx * dx + dy * dy <= r * r;
|
||||
}
|
||||
|
||||
/* 创建卡片 + 六钮(owner 钉 = PM_SENTINEL_ACT,仅首进创建一次,复用)。 */
|
||||
static void pm_widgets_create(void)
|
||||
{
|
||||
meow_ui_activity_push(PM_SENTINEL_ACT);
|
||||
g_pm_card = meow_card_create(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H);
|
||||
for (int i = 0; i < PM_NUM; i++) {
|
||||
int sz = PM_BTN_SIZE;
|
||||
int x = pm_cx[i] - sz / 2, y = pm_cy[i] - sz / 2;
|
||||
if (pm_sdk_icon[i] >= 0)
|
||||
g_pm_btns[i] = meow_iconbtn_create(x, y, sz, pm_sdk_icon[i]);
|
||||
else
|
||||
g_pm_btns[i] = meow_widget_custom_create(x, y, sz, sz,
|
||||
pm_render_cb, pm_hit_cb,
|
||||
(void *)(intptr_t)i);
|
||||
}
|
||||
meow_ui_activity_pop(); /* 回主 activity(控件 owner 已钉为哨兵) */
|
||||
g_pm_init = 1;
|
||||
}
|
||||
|
||||
/* 主题刷新:iconbtn 底色=surfaceContainer(与卡片 surfaceContainerLow 拉开对比)。
|
||||
* custom 钮由 render_cb 读 th 自带;此处只补 iconbtn 的 opts.bg(跨开放切主题也跟手)。 */
|
||||
static void pm_refresh_theme(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_widget_opts op; memset(&op, 0, sizeof op);
|
||||
op.bg = th->surfaceContainer;
|
||||
for (int i = 0; i < PM_NUM; i++)
|
||||
if (g_pm_btns[i] && pm_sdk_icon[i] >= 0)
|
||||
meow_widget_set_opts(g_pm_btns[i], &op);
|
||||
}
|
||||
|
||||
/* i18n 文案(meow_str_zh:未知 id → 中文兜底)。TF 覆盖表/后续 SDK 内嵌并入后自动换语。 */
|
||||
static const char *pm_label(int i)
|
||||
{
|
||||
return meow_str_zh(pm_str_id[i], pm_labels_cn[i]);
|
||||
}
|
||||
|
||||
/* 绘制单个圆钮(SDK 控件)+ 圆下标签(主题色,选中亮/未选次色)。 */
|
||||
static void pm_draw_button(int i, int selected)
|
||||
{
|
||||
meow_widget *w = g_pm_btns[i];
|
||||
if (!w) return;
|
||||
meow_widget_set_selected(w, selected);
|
||||
meow_widget_draw(w);
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int cx = pm_cx[i], cy = pm_cy[i];
|
||||
int ly = cy + PM_R + 8;
|
||||
uint32_t col = selected ? th->on_surface : th->on_surface_variant;
|
||||
if (mfont_ready) {
|
||||
const char *lab = pm_label(i);
|
||||
int lw = meow_font_width_text(lab, 18);
|
||||
meow_font_drawtext((uint16_t *)fb, cx - lw / 2, ly, lab, rgb565(col), 18);
|
||||
} else {
|
||||
const char *lab = pm_labels_en[i];
|
||||
draw_text(cx - (int)strlen(lab) * 5 / 2, ly + 1, lab, 1, col);
|
||||
}
|
||||
}
|
||||
|
||||
/* 标脏一个圆钮的整块区域(圆外接框 + 圆下标签),供局部 flush。 */
|
||||
static void pm_button_dirty(int i)
|
||||
{
|
||||
int sz = PM_BTN_SIZE, cy = pm_cy[i];
|
||||
meow_dirty_add(pm_cx[i] - sz / 2 - 4, cy - sz / 2 - 4, sz + 8, sz + PM_R + 44);
|
||||
}
|
||||
|
||||
/* 命中检测:返回命中的钮下标(0..PM_NUM-1),否则 -1。 */
|
||||
static int pm_hit_button(int x, int y)
|
||||
{
|
||||
for (int i = 0; i < PM_NUM; i++)
|
||||
if (g_pm_btns[i] && meow_widget_hit(g_pm_btns[i], x, y)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 绘制卡片 + 标题 + 底部提示 + 全部圆钮(进入/全刷时调用一次)。 */
|
||||
static void pm_draw_card(void)
|
||||
{
|
||||
pm_refresh_theme();
|
||||
if (g_pm_card) meow_widget_draw(g_pm_card);
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
const char *title = "POWER MENU";
|
||||
int title_y = PM_CARD_Y0 + 26;
|
||||
if (mfont_ready)
|
||||
meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2,
|
||||
title_y, title, rgb565(th->on_surface), 20);
|
||||
else
|
||||
draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, th->on_surface);
|
||||
const char *hint = meow_str_zh("power.hint_nav", "音量导航 电源确认");
|
||||
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
|
||||
if (mfont_ready)
|
||||
meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(hint, 18)) / 2,
|
||||
hint_y, hint, rgb565(th->on_surface_variant), 18);
|
||||
else
|
||||
draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2,
|
||||
hint_y + 1, "VOLUME NAV POWER OK", 1, th->on_surface_variant);
|
||||
}
|
||||
|
||||
/* ---- 电源管理服务 IPC 客户端(SYSTEM_FRAMEWORK_SERVICES §3.2:launcher 经服务触发电源动作)----
|
||||
* launcher 不再直连 reboot(2)syscall/写 mtd5/写 boot 默认 —— 统一经 meow_svc_power 的
|
||||
@@ -189,19 +375,12 @@ int show_power_menu(void)
|
||||
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
|
||||
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
|
||||
int sel = PM_NUM - 1; /* 默认取消(安全) */
|
||||
/* M2.30 MD3:居中圆角卡片背景 + 标题居中 + 圆钮网格。卡片白色柔和描边/深色底。 */
|
||||
fill_round_rect(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H, PM_CARD_R, 0x141824);
|
||||
fill_round_rect(PM_CARD_X0 + 2, PM_CARD_Y0 + 2, PM_CARD_W - 4, PM_CARD_H - 4, PM_CARD_R - 2, 0x1C2438);
|
||||
/* 卡片顶部标题,水平居中 */
|
||||
char title[32];
|
||||
snprintf(title, sizeof title, "POWER MENU");
|
||||
int title_y = PM_CARD_Y0 + 26; /* 标题在卡片内顶部(卡片顶 115,标题基线 ~141) */
|
||||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2, title_y, title, rgb565(0xFFFFFF), 20);
|
||||
else draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, 0xFFFFFF);
|
||||
/* 底部提示行(卡片下方,非圆钮所需) */
|
||||
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
|
||||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text("音量导航 电源确认", 18)) / 2, hint_y, "音量导航 电源确认", rgb565(0x777799), 18);
|
||||
else draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2, hint_y + 1, "VOLUME NAV POWER OK", 1, 0x777799);
|
||||
/* M7.0b 控件化:进入 push 哨兵(控件 owner 闸门使 meow_widget_hit 生效)+ lazy 建卡/钮。
|
||||
* 卡片=meow_card(SDK 主题 surfaceContainerLow),圆钮=meow_iconbtn/自定义画布,命中/选中/
|
||||
* 按压态全走 SDK;背景遮罩仍 dim_screen(会话骨架不动)。 */
|
||||
meow_ui_activity_push(PM_SENTINEL_ACT);
|
||||
if (!g_pm_init) pm_widgets_create();
|
||||
pm_draw_card(); /* 卡片 + 标题 POWER MENU + 底部提示(SDK 主题色,i18n 文案) */
|
||||
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
|
||||
|
||||
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
|
||||
@@ -228,19 +407,18 @@ int show_power_menu(void)
|
||||
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
|
||||
* 8s=强关」。*/
|
||||
int pm_interacted = 0;
|
||||
int pm_press_idx = -1; /* M7.0b:当前按下钮(按压态走 SDK 状态机位) */
|
||||
for (;;) {
|
||||
/* dirty 绘制:首次全画;此后仅选中变化才重画受影响两钮(消频闪)→ 标脏 + present。 */
|
||||
if (!pm_init) {
|
||||
for (int i = 0; i < PM_NUM; i++) draw_pm_button(i, i == sel);
|
||||
for (int i = 0; i < PM_NUM; i++) pm_draw_button(i, i == sel);
|
||||
pm_init = 1; last_sel = sel;
|
||||
meow_ui_flush(); /* A-1/A-2:SDK present(vsync+帧率门控+blit 脏=全帧,含压暗列表+标题+按钮) */
|
||||
} else if (sel != last_sel) {
|
||||
draw_pm_button(last_sel, 0);
|
||||
draw_pm_button(sel, 1);
|
||||
/* M2.30 圆形:dirty 区 = 圆(圆心±半径)+ 圆下文字(半径+偏移+行高),四周留 10px */
|
||||
int dr = (PM_R + 6) + 10, dtxt = PM_R + 26 + 10;
|
||||
meow_dirty_add(pm_cx[last_sel] - dr, pm_cy[last_sel] - dr, dr * 2, dr + dtxt);
|
||||
meow_dirty_add(pm_cx[sel] - dr, pm_cy[sel] - dr, dr * 2, dr + dtxt);
|
||||
pm_draw_button(last_sel, 0);
|
||||
pm_draw_button(sel, 1);
|
||||
pm_button_dirty(last_sel);
|
||||
pm_button_dirty(sel);
|
||||
last_sel = sel;
|
||||
meow_ui_flush(); /* A-1/A-2:SDK present,blit 两钮区 */
|
||||
}
|
||||
@@ -278,19 +456,31 @@ int show_power_menu(void)
|
||||
mt_down(&mt);
|
||||
pm_interacted = 1;
|
||||
pm_last_interact = now_ms();
|
||||
/* M7.0b 按压态:按下命中钮 → 置 PRESSED 并重绘(SDK 状态层,实时) */
|
||||
pm_press_idx = pm_hit_button(mt.down_x, mt.down_y);
|
||||
if (pm_press_idx >= 0) {
|
||||
meow_widget_set_state(g_pm_btns[pm_press_idx], MEOW_WS_PRESSED, 1);
|
||||
meow_widget_draw(g_pm_btns[pm_press_idx]);
|
||||
pm_button_dirty(pm_press_idx);
|
||||
meow_ui_flush();
|
||||
}
|
||||
} else if (!val && mt.down) {
|
||||
pm_interacted = 1;
|
||||
pm_last_interact = now_ms();
|
||||
if (pm_press_idx >= 0) { /* 清按压态(SDK 状态层随 PRESSED 位清除) */
|
||||
meow_widget_set_state(g_pm_btns[pm_press_idx], MEOW_WS_PRESSED, 0);
|
||||
meow_widget_draw(g_pm_btns[pm_press_idx]);
|
||||
pm_button_dirty(pm_press_idx);
|
||||
pm_press_idx = -1;
|
||||
meow_ui_flush();
|
||||
}
|
||||
int dx, dy;
|
||||
/* M2.25:抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动(返回0),
|
||||
* 不做确认;否则有效 tap,按下原点命中的按钮即确认。 */
|
||||
if (mt_up(&mt, &dx, &dy)) {
|
||||
/* M2.30 圆形命中:按下原点距圆心 <= PM_R+6 即命中 */
|
||||
for (int i = 0; i < PM_NUM; i++) {
|
||||
int ddx = mt.down_x - pm_cx[i], ddy = mt.down_y - pm_cy[i];
|
||||
int rr = (PM_R + 6) * (PM_R + 6);
|
||||
if (ddx * ddx + ddy * ddy <= rr) { sel = i; act = i; break; }
|
||||
}
|
||||
/* M7.0b 命中走 SDK(owner==哨兵 + enabled + 圆形命中) */
|
||||
int hit = pm_hit_button(mt.down_x, mt.down_y);
|
||||
if (hit >= 0) { sel = hit; act = hit; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,12 +491,13 @@ int show_power_menu(void)
|
||||
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
|
||||
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
|
||||
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
|
||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); return 1; }
|
||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); meow_ui_activity_pop(); return 1; }
|
||||
logmsg("pm: POWER HOLD 8s -> force poweroff");
|
||||
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
|
||||
logmsg("pm: power service unavailable, abort force-off (menu closed, no action)");
|
||||
return 0;
|
||||
meow_ui_activity_pop(); return 0;
|
||||
}
|
||||
meow_ui_activity_pop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -318,7 +509,7 @@ int show_power_menu(void)
|
||||
logmsg("pm: idle %ds -> close menu + auto screen-off (act=cancel)", screen_off_timeout);
|
||||
meow_panel_off();
|
||||
meow_auto_slept = 1;
|
||||
act = 4; /* 取消(关菜单) */
|
||||
act = PM_NUM - 1; /* 取消(关菜单) */
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -326,14 +517,22 @@ int show_power_menu(void)
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
if (tfd >= 0) close(tfd);
|
||||
meow_ui_activity_pop(); /* M7.0b:退出电源菜单,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||
|
||||
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与电源动作逐项核对:
|
||||
* act==4(取消) -> 只关菜单,绝不关机(return 0,永不到电源动作);
|
||||
* act==PM_NUM-1(取消) -> 只关菜单,绝不关机(return 0,永不到电源动作);
|
||||
* act==4(锁定) -> 手动锁(meow_lock_screen_engage,未配 PIN 则 no-op),关菜单;
|
||||
* act==0(关机)/act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才经服务 IPC 执行电源动作。
|
||||
* 只有 act 为 0/1/2/3 才可能发电源动作;power 键本身绝不触发关机。 */
|
||||
logmsg("pm: act=%d -> %s", act,
|
||||
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式" : "取消");
|
||||
if (act == 4) { logmsg("pm: cancel (close menu only)"); return 0; }
|
||||
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式"
|
||||
: act == 4 ? "锁定" : "取消");
|
||||
if (act == PM_NUM - 1) { logmsg("pm: cancel (close menu only)"); return 0; }
|
||||
if (act == 4) {
|
||||
logmsg("pm: LOCK SCREEN");
|
||||
meow_lock_screen_engage();
|
||||
return 0; /* 关菜单;run_menu 继续(锁屏页已阻塞到解锁或电源息屏) */
|
||||
}
|
||||
/* 动作统一经电源管理服务 IPC(meow_svc_power);launcher 不再直连 poweroff/reboot(2)/写 mtd5/
|
||||
* 写 boot 默认——关机/重启/升级/回HiBy 的动作原语都在服务内实现(§3.2 单一动作链路)。 */
|
||||
/* 公共服务:执行动作;服务不可达 → 按「取消」返回(launcher 存活,绝不误报已动作卡死)。
|
||||
@@ -380,15 +579,20 @@ int show_power_menu(void)
|
||||
* 不随主循环每 10ms 全刷(原实现每帧重画同一内容,与 LCD 刷新竞争产生频闪)。 */
|
||||
static void draw_cr_button(int sel, int i)
|
||||
{
|
||||
const meow_theme *mt = meow_theme_get(); /* M7.5:主题语义色(深/浅×MD3/MIUIx 单一权威) */
|
||||
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
|
||||
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
|
||||
const char *lbl = (i == 0) ? "是" : "否";
|
||||
uint32_t bg = (i == sel) ? mt->primary : mt->surfaceContainer;
|
||||
fill_round_rect(x0, y0, w, h, 12, bg);
|
||||
/* M7.0b i18n:是/否 经 meow_str 抽取(未知 id 回退中文) */
|
||||
const char *lbl = (i == 0) ? meow_str_zh("power.confirm_yes", "是")
|
||||
: meow_str_zh("power.confirm_no", "否");
|
||||
uint32_t fg = (i == sel) ? mt->onPrimary : mt->onSurface;
|
||||
if (mfont_ready) {
|
||||
int lw = meow_font_width_text(lbl, 20);
|
||||
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
|
||||
lbl, rgb565((i == sel) ? 0xFFFFFF : 0xC8D0E8), 20);
|
||||
lbl, rgb565(fg), 20);
|
||||
} else {
|
||||
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, (i == sel) ? 0xFFFFFF : 0xC8D0E8);
|
||||
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, fg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,8 +603,10 @@ int confirm_reboot_player(void)
|
||||
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
|
||||
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
|
||||
fb_pan_buf0_force();
|
||||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320, "返回播放器需重启,现在重启?", rgb565(0xFFFFFF), 20);
|
||||
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
|
||||
const meow_theme *mt0 = meow_theme_get(); /* M7.5:标题颜色走主题语义色 */
|
||||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320,
|
||||
meow_str_zh("power.reboot_confirm", "返回播放器需重启,现在重启?"), rgb565(mt0->onSurface), 20);
|
||||
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, mt0->onSurface);
|
||||
|
||||
meow_touch mt;
|
||||
mt_reset(&mt);
|
||||
@@ -419,8 +625,9 @@ int confirm_reboot_player(void)
|
||||
int cr_init = 0, cr_last_sel = -1; /* M2.9 Bug 4:dirty——仅 sel 变化才重画两钮 */
|
||||
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
|
||||
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
|
||||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560, "音量切换 电源取消", rgb565(0x666688), 20);
|
||||
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
|
||||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560,
|
||||
meow_str_zh("power.confirm_hint", "音量切换 电源取消"), rgb565(mt0->onSurfaceVariant), 20);
|
||||
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, mt0->onSurfaceVariant);
|
||||
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
|
||||
for (;;) {
|
||||
/* dirty 绘制:首次全画两钮;此后仅 sel 变化才重画(消频闪)→ 标脏 + present。 */
|
||||
|
||||
@@ -23,6 +23,26 @@ static const char *const out_presets[] = { "hw:0,0" };
|
||||
|
||||
static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" };
|
||||
|
||||
/* i18n:SC_PRESET 选项中文标签键(meow_str_zh:未知 id → 中文兜底,行为/显示不变)。
|
||||
* 字体行(font.file/font.class_*)用运行时扫描表 font_presets/font_preset_labels,无键表,原样。 */
|
||||
static const char *const bg_preset_i18n[] = { "settings.bg_black", "settings.bg_blue", "settings.bg_green" };
|
||||
static const char *const hk_preset_i18n[] = { "settings.hk_powervol_pr", "settings.hk_sequence" };
|
||||
static const char *const seq_preset_i18n[] = { "settings.seq_volup3", "settings.seq_voldown3" };
|
||||
static const char *const out_preset_i18n[] = { "settings.out_cs43198" };
|
||||
|
||||
/* SC_PRESET 行选项标签本地化:按该行 presets 表匹配对应键表(字体行用运行时动态表,无键 → 原样)。 */
|
||||
static const char *s_preset_label(const srow_t *r, int i)
|
||||
{
|
||||
const char *const *keys = NULL;
|
||||
if (r->presets == bg_presets) keys = bg_preset_i18n;
|
||||
else if (r->presets == hk_presets) keys = hk_preset_i18n;
|
||||
else if (r->presets == seq_presets) keys = seq_preset_i18n;
|
||||
else if (r->presets == out_presets) keys = out_preset_i18n;
|
||||
const char *lbl = (r->preset_labels && r->preset_labels[i]) ? r->preset_labels[i] : "";
|
||||
if (keys && keys[i]) return meow_str_zh(keys[i], lbl);
|
||||
return lbl;
|
||||
}
|
||||
|
||||
|
||||
/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s,
|
||||
* 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按
|
||||
@@ -115,17 +135,94 @@ static const srow_t SMENU[] = {
|
||||
{ "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0,0 },
|
||||
{ "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0,0 },
|
||||
{ "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N,0 },
|
||||
/* M6.x 锁屏:安全组「锁屏 PIN」独立子页面(状态/设置/修改/清除/唤醒锁开关,见 lock_settings.c) */
|
||||
{ "安全", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
|
||||
{ "锁屏 PIN", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_PIN_PAGE },
|
||||
/* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */
|
||||
{ "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
|
||||
{ "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT },
|
||||
};
|
||||
|
||||
/* 设置行标题 i18n 键:与 SMENU 一一对应(含分组标题 / SC_LIST 导航行),settings.* 域。
|
||||
* 默认值取 SMENU 行 label 原中文,行为/显示不变;TF 覆盖表/后续 SDK 内嵌并入后自动换语。 */
|
||||
static const char *const s_row_i18n[SMENU_N] = {
|
||||
"settings.sec_display", /* 显示 */
|
||||
"settings.brightness", /* 亮度 */
|
||||
"settings.screen_off_mode", /* 息屏模式 */
|
||||
"settings.screen_off_timeout",/* 息屏时间 */
|
||||
"settings.refresh_rate", /* 刷新率 */
|
||||
"settings.refresh_always_max",/* 最高刷新率 */
|
||||
"settings.theme_mode", /* 主题 */
|
||||
"settings.theme", /* 主题深浅 */
|
||||
"settings.theme_dark_start", /* 深色时段开始 */
|
||||
"settings.theme_dark_end", /* 深色时段结束 */
|
||||
"settings.background", /* 背景 */
|
||||
"settings.bg_overlay_alpha", /* 背景叠加强度 */
|
||||
"settings.home_layout", /* 主界面布局 */
|
||||
"settings.sec_network", /* 网络 */
|
||||
"settings.wifi", /* WiFi */
|
||||
"settings.bt", /* 蓝牙 */
|
||||
"settings.sec_power", /* 电源 */
|
||||
"settings.longpress_menu", /* 长按弹菜单 */
|
||||
"settings.forceoff", /* 长按强关 */
|
||||
"settings.defeat_hotkey", /* 退出热键 */
|
||||
"settings.sec_boot", /* 开机 */
|
||||
"settings.boot_default", /* 默认系统 */
|
||||
"settings.boot_menu", /* 显示选单 */
|
||||
"settings.boot_timeout", /* 选单超时 */
|
||||
"settings.sec_input", /* 输入 */
|
||||
"settings.hotkey_seq", /* 热键序列 */
|
||||
"settings.hotkey_window", /* 热键窗口 */
|
||||
"settings.touch_arm", /* 触摸布防 */
|
||||
"settings.swipe_invert", /* 滑动方向 */
|
||||
"settings.sec_font", /* 字体主题 */
|
||||
"settings.font_file", /* 字体文件 */
|
||||
"settings.font_load_full", /* 加载大字库 */
|
||||
"settings.multi_font", /* 多字体 */
|
||||
"settings.font_cjk", /* 中文(CJK) */
|
||||
"settings.font_cjk_sym", /* 中文标点 */
|
||||
"settings.font_num", /* 数字 */
|
||||
"settings.font_latin", /* 拉丁字母 */
|
||||
"settings.font_latin_sym", /* 半角标点 */
|
||||
"settings.font_other", /* 其他字符 */
|
||||
"settings.font_title_size", /* 标题字号 */
|
||||
"settings.font_body_size", /* 正文字号 */
|
||||
"settings.ui_style", /* UI风格 */
|
||||
"settings.sec_notify", /* 通知 */
|
||||
"settings.notify_center", /* 通知中心 */
|
||||
"settings.notify_app_quota", /* 应用配额 */
|
||||
"settings.sec_log", /* 日志 */
|
||||
"settings.log_level", /* 日志级别 */
|
||||
"settings.log_to_card", /* 写入TF卡 */
|
||||
"settings.sec_audio", /* 音频 */
|
||||
"settings.volume", /* 音量 */
|
||||
"settings.volume_max", /* 音量上限 */
|
||||
"settings.volume_step", /* 音量跳步 */
|
||||
"settings.audio_output", /* 输出设备 */
|
||||
"settings.sec_security", /* 安全 */
|
||||
"settings.pin", /* 锁屏 PIN */
|
||||
"settings.sec_about", /* 关于 */
|
||||
"settings.about", /* 关于本机 */
|
||||
};
|
||||
|
||||
/* SMENU 行标题本地化:能定位到 SMENU 行 → 按对应键取文案(默认=原中文);否则原样返回。 */
|
||||
static const char *s_row_label(const srow_t *r)
|
||||
{
|
||||
for (int i = 0; i < SMENU_N; i++) {
|
||||
if (&SMENU[i] == r) return meow_str_zh(s_row_i18n[i], r->label);
|
||||
}
|
||||
return meow_str_zh("settings.unknown", r->label);
|
||||
}
|
||||
|
||||
/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由
|
||||
* 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */
|
||||
|
||||
#if 0 /* M7.2 回退位:旧「当前选中滑条行 track y」随旧手绘 draw_settings_page 退役
|
||||
* (新路径在触摸按下时由 meow_listview hit 直接算 track 中心,无需全局记录)。 */
|
||||
/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量,
|
||||
* 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */
|
||||
static int s_sel_track_y = -1;
|
||||
#endif
|
||||
|
||||
|
||||
/* ---- 行高/位置/导航辅助 ---- */
|
||||
@@ -203,6 +300,9 @@ static const char *const *s_row_presets(const srow_t *r, int *n)
|
||||
}
|
||||
|
||||
|
||||
#if 0 /* M7.2 回退位:旧「纯范围/选中可见滚动钳制 + 内容 y→行号」随旧手绘 draw_settings_page 退役
|
||||
* (新路径滚动走 meow_scroll 包裹 meow_listview,钳制/命中/可见性由 SDK 接管)。
|
||||
* 删除 #if 0 行并切回旧 draw_settings_page 路径即可恢复。 */
|
||||
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
|
||||
static int s_clamp_scroll_range(int scroll)
|
||||
{
|
||||
@@ -237,15 +337,16 @@ static int s_row_at_content_y(int cy)
|
||||
if (pos >= s_vis_count()) return -1;
|
||||
return s_vis_at(pos);
|
||||
}
|
||||
#endif /* M7.2 回退位:旧滚动钳制 + 内容 y→行号 */
|
||||
|
||||
|
||||
/* ---- 值格式化/展示 ---- */
|
||||
static void s_fmt_time(int sec, char *buf, int len)
|
||||
{
|
||||
if (sec <= 0) { snprintf(buf, len, "常亮"); return; }
|
||||
if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; }
|
||||
if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; }
|
||||
snprintf(buf, len, "%d 时", sec / 3600);
|
||||
if (sec <= 0) { snprintf(buf, len, "%s", meow_str_zh("settings.always_on", "常亮")); return; }
|
||||
if (sec < 60) { snprintf(buf, len, meow_str_zh("settings.time_sec", "%d 秒"), sec); return; }
|
||||
if (sec < 3600) { snprintf(buf, len, meow_str_zh("settings.time_min", "%d 分"), sec / 60); return; }
|
||||
snprintf(buf, len, meow_str_zh("settings.time_hr", "%d 时"), sec / 3600);
|
||||
}
|
||||
|
||||
/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */
|
||||
@@ -288,14 +389,14 @@ static void s_fmt_value(const srow_t *r, char *buf, int len)
|
||||
int s2 = 0, e2 = 0;
|
||||
meow_settings_get_int("display.theme_dark_start", &s2);
|
||||
meow_settings_get_int("display.theme_dark_end", &e2);
|
||||
if (s2 == e2) { snprintf(buf, len, "未设置"); return; }
|
||||
if (s2 == e2) { snprintf(buf, len, "%s", meow_str_zh("settings.unset", "未设置")); return; }
|
||||
s_fmt_minutes(v, buf, len);
|
||||
return;
|
||||
}
|
||||
/* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */
|
||||
if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) {
|
||||
if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000);
|
||||
else snprintf(buf, len, "%.1f 秒", v / 1000.0);
|
||||
if (v % 1000 == 0) snprintf(buf, len, meow_str_zh("settings.sec_val", "%d 秒"), v / 1000);
|
||||
else snprintf(buf, len, meow_str_zh("settings.sec_val_f", "%.1f 秒"), v / 1000.0);
|
||||
return;
|
||||
}
|
||||
snprintf(buf, len, "%d", v);
|
||||
@@ -314,14 +415,14 @@ static void s_display_value(const srow_t *r, char *buf, int len)
|
||||
if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; }
|
||||
/* M2.5x 多字体:class 行空值 = 跟随全局 font.file */
|
||||
if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) {
|
||||
snprintf(buf, len, "跟随全局");
|
||||
snprintf(buf, len, "%s", meow_str_zh("settings.follow_global", "跟随全局"));
|
||||
return;
|
||||
}
|
||||
int pn = 0;
|
||||
const char *const *pps = s_row_presets(r, &pn);
|
||||
for (int i = 0; i < pn; i++) {
|
||||
if (!strcmp(pps[i], cur)) {
|
||||
snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur);
|
||||
snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? s_preset_label(r, i) : cur);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -373,7 +474,7 @@ static void s_commit(const srow_t *r, int save_now)
|
||||
* 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。
|
||||
* 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */
|
||||
int v; if (meow_settings_get_int("display.brightness", &v) == 0) {
|
||||
write_brightness(v);
|
||||
write_brightness(v, "settings_page bright_slider");
|
||||
g_sys.brightness = v;
|
||||
paint_sb_right();
|
||||
}
|
||||
@@ -396,16 +497,35 @@ static void s_commit(const srow_t *r, int save_now)
|
||||
* 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */
|
||||
else if (!strcmp(r->key, "display.background")) {
|
||||
char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg);
|
||||
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
|
||||
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
|
||||
else menu_bg = 0x000000; /* black(默认) */
|
||||
/* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */
|
||||
int mstyle = 0; if (meow_settings_get_enum("ui.style", &mstyle) != 0) mstyle = 0;
|
||||
if (mstyle == MEOW_STYLE_MIUIX) { /* MIUIx 暖色族(与 main_menu menu_bg_prepare 同色) */
|
||||
if (!strcmp(bg, "blue")) menu_bg = 0x101A2E;
|
||||
else if (!strcmp(bg, "green")) menu_bg = 0x14251A;
|
||||
else menu_bg = 0x181310;
|
||||
} else { /* MD3(既有) */
|
||||
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
|
||||
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
|
||||
else menu_bg = 0x000000; /* black(默认) */
|
||||
}
|
||||
/* menu_bg 仅主界面背景用(状态栏走 SDK meow_theme bg);退出设置页回主界面时经
|
||||
* menu_bg_sig(含 ui.style)重build 缓存。 */
|
||||
}
|
||||
/* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */
|
||||
else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") ||
|
||||
!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
|
||||
paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */
|
||||
}
|
||||
/* §8.1.2 一键切换闭环:ui.style(md3/miuix)顶级风格族切换 → 全系统即时换肤,无需重启。
|
||||
* meow_ui_set_style() 设 SDK g_style(launcher 同一进程内立即生效)+ 广播(App/状态栏下一帧换肤);
|
||||
* paint_sb_right() → sb_sdk_refresh() 逼 SDK 状态栏重读 settings(含 display.background 派生)跟手换肤;
|
||||
* meow_dirty_full() 让 launcher 主界面/设置页整帧重绘(s_ui_theme() 已接 ui.style → 换族配色)。
|
||||
* 主界面背景缓存经 menu_bg_sig(含 ui.style,见 main_menu.c)在回主界面时自动重建。 */
|
||||
else if (!strcmp(r->key, "ui.style")) {
|
||||
int st = 0; meow_settings_get_enum("ui.style", &st);
|
||||
meow_ui_set_style(st);
|
||||
paint_sb_right();
|
||||
meow_dirty_full();
|
||||
}
|
||||
/* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。
|
||||
* 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走
|
||||
* font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */
|
||||
@@ -475,6 +595,9 @@ static int s_slider_val_from_x(const srow_t *r, int px)
|
||||
}
|
||||
|
||||
|
||||
#if 0 /* M7.2 回退位:旧手绘设置页主列表(手绘分组列表 + s_draw_switch/s_draw_slider + 自算滚动/命中)。
|
||||
* 新路径走 meow_listview + meow_switch + meow_scroll + meow_select_show;删除 #if 0 行并把
|
||||
* 下方「M7.2 SDK 路径」的 set_paint_page 调用切回 draw_settings_page 即可一键回退。 */
|
||||
/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */
|
||||
static void draw_settings_page(int sel, int scroll_off)
|
||||
{
|
||||
@@ -574,6 +697,7 @@ static void draw_settings_page(int sel, int scroll_off)
|
||||
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
|
||||
}
|
||||
}
|
||||
#endif /* M7.2 回退位:旧手绘设置页主列表 */
|
||||
|
||||
|
||||
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
|
||||
@@ -622,21 +746,23 @@ static int s_dropdown(const srow_t *r)
|
||||
n = base + cnt;
|
||||
if (is_cls) {
|
||||
names[0] = "";
|
||||
snprintf(labels[0], sizeof labels[0], "跟随全局字体");
|
||||
snprintf(labels[0], sizeof labels[0], "%s", meow_str_zh("settings.follow_global_font", "跟随全局字体"));
|
||||
item[0] = labels[0];
|
||||
if (!cstr[0]) cur = 0;
|
||||
}
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
names[base + i] = pps[i];
|
||||
snprintf(labels[base + i], sizeof labels[base + i], "%s",
|
||||
(r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]);
|
||||
(r->preset_labels && r->preset_labels[i][0]) ? s_preset_label(r, i) : pps[i]);
|
||||
item[base + i] = labels[base + i];
|
||||
if (!strcmp(pps[i], cstr)) cur = base + i;
|
||||
}
|
||||
}
|
||||
if (n <= 0) return -1;
|
||||
if (cur < 0 || cur >= n) cur = 0;
|
||||
return s_dd_modal(r->label, item, n, cur);
|
||||
/* M7.2:枚举/预设选择器迁 SDK meow_select_show(BottomSheet+单选列表,批次2 新控件),
|
||||
* 替换自绘 s_dd_modal(后者仍供 about/网络列表/字体两级子菜单用,勿删)。 */
|
||||
return meow_select_show(s_row_label(r), item, n, cur);
|
||||
}
|
||||
|
||||
|
||||
@@ -654,8 +780,8 @@ static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int
|
||||
int content_top, int content_bot, int dd_scroll)
|
||||
{
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
|
||||
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
|
||||
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
|
||||
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 28, th->surface_container); /* MD3 extraLarge 28(原 16 偏小) */
|
||||
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 28, th->outline);
|
||||
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
|
||||
else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface);
|
||||
for (int i = 0; i < n; i++) {
|
||||
@@ -664,7 +790,7 @@ static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int
|
||||
int cur_sel = (i == sel);
|
||||
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
|
||||
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
|
||||
cur_sel ? 0xFFFFFF : th->on_surface, 20);
|
||||
cur_sel ? meow_theme_get()->onPrimary : th->on_surface, 20);
|
||||
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
|
||||
}
|
||||
}
|
||||
@@ -871,8 +997,9 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
|
||||
char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64];
|
||||
int famn = 0;
|
||||
if (is_cls) {
|
||||
if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], "");
|
||||
snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; }
|
||||
if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", "");
|
||||
snprintf(fam_disp[famn], sizeof fam_disp[0], "%s",
|
||||
meow_str_zh("settings.follow_global_font", "跟随全局字体")); famn++; }
|
||||
}
|
||||
for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) {
|
||||
const char *f = font_preset_fams[i] ? font_preset_fams[i] : "";
|
||||
@@ -891,9 +1018,9 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
|
||||
int cur_fam = is_cls ? 0 : -1;
|
||||
for (int i = is_cls ? 1 : 0; i < famn; i++)
|
||||
if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i;
|
||||
int pick = s_dd_modal(r->label, fitem, famn, cur_fam);
|
||||
int pick = s_dd_modal(s_row_label(r), fitem, famn, cur_fam);
|
||||
if (pick < 0) return 0;
|
||||
if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */
|
||||
if (is_cls && pick == 0) { snprintf(out_val, vlen, "%s", ""); return 1; } /* 跟随全局 */
|
||||
const char *fam = fam_slug[pick];
|
||||
|
||||
/* ---- 二级:该 family 的字重列表(去重:同 (family,weight) 只显一行)----
|
||||
@@ -937,49 +1064,280 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
|
||||
for (int i = 0; i < wn; i++)
|
||||
if (cwt2[0] && !strcmp(wt_pick[i], cwt2)) { cur_wt = i; break; }
|
||||
}
|
||||
int wp = s_dd_modal(r->label, witem, wn, cur_wt);
|
||||
int wp = s_dd_modal(s_row_label(r), witem, wn, cur_wt);
|
||||
if (wp < 0) return 0;
|
||||
snprintf(out_val, vlen, "%s", wt_val[wp]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* ==================================================================
|
||||
* M7.2 设置页渲染层迁移(主列表迁 SDK 控件):
|
||||
* - 分组列表 → meow_listview(虚拟化 set_vsource 固定 item 池 + section 段头 + set_item_draw 自绘);
|
||||
* - 开关行 → 行内真 meow_switch(池化,SDK 绘制 + 按压态);
|
||||
* - 滑条行 → 行内嵌 meow_slider(自绘 track/thumb,保留既有 s_draw_slider 语义色;
|
||||
* 「行内嵌自绘」,M7 定案允许)——拖动实时写 settings.conf(读改写单键,
|
||||
* 经 meow_settings_set_*,勿整文件覆盖——音量存盘教训);
|
||||
* - 枚举/预设 → SDK meow_select_show(BottomSheet+单选列表,替换自绘 s_dd_modal);
|
||||
* - 信息/列表行 → 行 right 值 + chevron(SDK 主题语义色)。
|
||||
* 滚动 → meow_scroll(跟手 track + 松手 fling,同 subpage.c M7.1a 语义;刻意不 set_snap)。
|
||||
* 控件 lazy 创建一次(owner=SET_SENTINEL_ACT 哨兵),跨多次进入复用;数据/动作(s_commit、
|
||||
* s_set_int_value、s_net_*、s_font_dropdown、s_dropdown)逻辑层不动,仅换渲染层 + 命中;
|
||||
* run_settings_page 会话骨架(触摸/滚动/返回/IPC/电源键语义)不改。旧手绘 draw_settings_page
|
||||
* + s_clamp_scroll + s_row_at_content_y 保留 #if 0 作回退位(M7 红线:一键回退)。
|
||||
* ================================================================== */
|
||||
#define SET_SENTINEL_ACT 0x53 /* 设置页哨兵 activity(不与 launcher 其它页冲突) */
|
||||
#define SET_LIST_Y SET_CONTENT_Y /* 100:列表顶缘(顶栏/标题下) */
|
||||
#define SET_FOOT_H 40 /* 底部返回/版本带高 */
|
||||
#define SET_ITEM_H 68 /* MD3 行高(滑条行给 track 留位) */
|
||||
#define SET_SLIDER_YOFF 45 /* 滑条行内 track 顶缘相对行顶(÷s_draw_slider h=6,中心=顶+3) */
|
||||
#define SET_TRACK_CY 48 /* 滑条 track 中心相对行顶 y(=SET_SLIDER_YOFF+3,与绘制对齐) */
|
||||
#define SET_SLIDER_LY 12 /* 滑条行 label 顶相对行顶 y(上移给 track 留位) */
|
||||
#define SET_SWITCH_POOL 12
|
||||
/* MD3 容器化(P0):设置页行=surface 上 surfaceContainer 圆角卡 + 行间空隙(去分隔线);
|
||||
* 卡横向外边距=SET_CARD_X(8),卡在行高内上下对称内缩(行中心=卡中心,同主界面/ncm 歌单手法)。 */
|
||||
#define SET_ROW_CARD_X SET_CARD_X /* 8:卡左缘 = 内容区左缘 */
|
||||
#define SET_ROW_CARD_G 8 /* 卡纵向间隙(px) */
|
||||
#define SET_ROW_CARD_R 12 /* 卡圆角(12dp) */
|
||||
|
||||
static meow_widget *g_set_lv = NULL; /* meow_listview(虚拟化) */
|
||||
static meow_scroll *g_set_sc = NULL; /* 滚动物理 */
|
||||
static meow_widget *g_set_sw[SET_SWITCH_POOL]; /* 行内开关池(真 meow_switch) */
|
||||
static int g_set_sw_n = 0, g_set_sw_use = 0; /* 池创建数 / 本轮已用(每次整绘复位) */
|
||||
static int g_set_lv_h = 0; /* 列表视口高(选中可见性 clamp 用) */
|
||||
static int g_set_init = 0;
|
||||
|
||||
static void set_lv_rect(int *x, int *y, int *w, int *h)
|
||||
{
|
||||
if (x) *x = 0;
|
||||
if (y) *y = SET_LIST_Y;
|
||||
if (w) *w = fb_xres;
|
||||
if (h) { int t = fb_yres - SET_LIST_Y - SET_FOOT_H; if (t < 0) t = 0; *h = t; }
|
||||
}
|
||||
|
||||
static int set_lv_count(void *ctx) { (void)ctx; return s_vis_count(); }
|
||||
|
||||
/* 行填充(虚拟化 pool bind;段头行 section=1、enabled=1 供 SDK 命中/段头绘制) */
|
||||
static void set_lv_bind(int idx, meow_lv_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->icon_kind = MEOW_LIST_ICON_NONE;
|
||||
out->enabled = 1;
|
||||
if (idx < 0 || idx >= s_vis_count()) { out->title = ""; return; }
|
||||
const srow_t *r = &SMENU[s_vis_at(idx)];
|
||||
out->title = r->label ? s_row_label(r) : "";
|
||||
out->section = (r->ctrl == SC_HEADER) ? 1 : 0;
|
||||
if (r->ctrl == SC_SWITCH) {
|
||||
int on = 0; meow_settings_get_switch(r->key, &on);
|
||||
out->right = MEOW_LIST_RIGHT_SWITCH; out->on = on;
|
||||
}
|
||||
}
|
||||
|
||||
/* 行内开关池:取下一个可用槽(每帧 set_lv_draw 顺序取用,整绘前复位 g_set_sw_use) */
|
||||
static meow_widget *set_sw_take(void)
|
||||
{
|
||||
if (g_set_sw_use < g_set_sw_n) return g_set_sw[g_set_sw_use++];
|
||||
if (g_set_sw_n >= SET_SWITCH_POOL) return NULL;
|
||||
meow_widget *w = meow_switch_create(0, 0, 0);
|
||||
if (w) { g_set_sw[g_set_sw_n++] = w; return g_set_sw[g_set_sw_use++]; }
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 整行自绘(set_item_draw 覆盖默认 renderer;x=列表左缘、y=行顶、w/h=行全宽高,
|
||||
* state=SELECTED/PRESSED/DISABLED 组合;颜色全取 meow_theme 语义色,无散落硬编码) */
|
||||
static void set_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
||||
{
|
||||
(void)ctx; (void)h;
|
||||
const meow_theme *mt = meow_theme_get();
|
||||
if (idx < 0) return;
|
||||
int si = s_vis_at(idx);
|
||||
const srow_t *r = &SMENU[si];
|
||||
int sel = (state & MEOW_LISTVIEW_ST_SELECTED);
|
||||
int press = (state & MEOW_LISTVIEW_ST_PRESSED);
|
||||
int dis = (state & MEOW_LISTVIEW_ST_DISABLED);
|
||||
int thh = meow_ui_text_height(mt->font_body);
|
||||
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
|
||||
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
|
||||
|
||||
/* MD3 容器化(P0):行=surface 上 surfaceContainer 圆角卡;段头=surface 上的 primary 段标签(无卡)。 */
|
||||
int cx = x + SET_ROW_CARD_X; /* 卡左缘 */
|
||||
int cw = w - 2 * SET_ROW_CARD_X; /* 卡宽 */
|
||||
int cy = y + SET_ROW_CARD_G / 2; /* 卡顶(行内对称内缩) */
|
||||
int ch = SET_ITEM_H - SET_ROW_CARD_G; /* 卡高 */
|
||||
|
||||
if (r->ctrl == SC_HEADER) {
|
||||
int ty = y + (SET_ITEM_H - thh) / 2;
|
||||
if (ty >= 0) meow_ui_text(cx + 16, ty, s_row_label(r), mt->font_body, mt->primary);
|
||||
return;
|
||||
}
|
||||
meow_gfx_fill_round(cx, cy, cw, ch, SET_ROW_CARD_R, mt->surfaceContainer);
|
||||
if (sel) meow_gfx_fill_round(cx, cy, cw, ch, SET_ROW_CARD_R, mt->primaryContainer);
|
||||
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, ch, SET_ROW_CARD_R, mt->primary, 26);
|
||||
|
||||
int ly = y + (SET_ITEM_H - thh) / 2; /* label 单行垂直居中 */
|
||||
int rgt = cx + cw - 16; /* 卡内右缘(内容右对齐锚点) */
|
||||
switch (r->ctrl) {
|
||||
case SC_SWITCH: {
|
||||
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
|
||||
meow_widget *sw = set_sw_take();
|
||||
if (sw) {
|
||||
int on = 0; meow_settings_get_switch(r->key, &on);
|
||||
meow_widget_set_rect(sw, rgt - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
|
||||
meow_switch_set(sw, on);
|
||||
meow_widget_draw(sw);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SC_SLIDER: {
|
||||
/* label 上移一行,track 放行下方(中心=SET_SLIDER_YOFF+3=48),与拖动命中带对齐 */
|
||||
meow_ui_text(cx + 16, y + SET_SLIDER_LY, s_row_label(r), mt->font_body, textcol);
|
||||
char vb[32]; s_fmt_value(r, vb, sizeof vb);
|
||||
meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), y + SET_SLIDER_LY, vb, mt->font_body, subcol);
|
||||
int v = 0; meow_settings_get_int(r->key, &v);
|
||||
/* 息屏时间为档位索引型(秒→索引 等距画);其余 clamp 到 [min,max] */
|
||||
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
|
||||
else { if (v < r->min) v = r->min; if (v > r->max) v = r->max; }
|
||||
s_draw_slider(S_SLIDER_X, y + SET_SLIDER_YOFF, S_SLIDER_W, v, r->min, r->max, s_ui_theme());
|
||||
break;
|
||||
}
|
||||
case SC_INFO: {
|
||||
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
|
||||
char vb[48]; s_net_get(r, vb, sizeof vb);
|
||||
meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
|
||||
break;
|
||||
}
|
||||
case SC_LIST: {
|
||||
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
|
||||
char vb[48]; s_net_get(r, vb, sizeof vb);
|
||||
int vx = rgt - 12 - meow_ui_text_width(vb, mt->font_body);
|
||||
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
|
||||
s_draw_chevron(rgt, y + SET_ITEM_H / 2, subcol);
|
||||
break;
|
||||
}
|
||||
default: { /* SC_ENUM / SC_PRESET */
|
||||
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
|
||||
char vb[40]; s_display_value(r, vb, sizeof vb);
|
||||
int vx = rgt - meow_ui_text_width(vb, mt->font_body);
|
||||
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
|
||||
s_draw_chevron(vx - 8, y + SET_ITEM_H / 2, subcol);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dis) meow_gfx_fill_round_alpha(cx, cy, cw, ch, SET_ROW_CARD_R, mt->onSurface, 90);
|
||||
}
|
||||
|
||||
/* ---- meow_scroll 回调(包 listview)---- */
|
||||
static void set_sc_set(void *ctx, int off) { (void)ctx; if (g_set_lv) meow_listview_set_scroll(g_set_lv, off); }
|
||||
static int set_sc_max(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_max_scroll(g_set_lv) : 0; }
|
||||
static int set_sc_get(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_scroll(g_set_lv) : 0; }
|
||||
|
||||
/* 列表区重绘(含内容底 + listview),供 fling/滚动局部刷。 */
|
||||
static void set_redraw_region(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
||||
if (lvh <= 0) return;
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
meow_ui_fill_rect(lvx, lvy, lvw, lvh, th->surface); /* P0:内容底改 surface(卡在其上抬升) */
|
||||
if (g_set_lv) { g_set_sw_use = 0; meow_widget_draw(g_set_lv); }
|
||||
meow_dirty_add(lvx, lvy, lvw, lvh);
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
/* 选中行可见性:滚动到选中行不越界(仅靠 tap 触发,选中行即在视野内,防御性兜底)。 */
|
||||
static void set_ensure_sel_visible(int sel)
|
||||
{
|
||||
if (!g_set_lv) return;
|
||||
int p = s_vis_of(sel); if (p < 0) return;
|
||||
int scroll = meow_listview_scroll(g_set_lv);
|
||||
int top = p * SET_ITEM_H, bot = top + SET_ITEM_H;
|
||||
if (top < scroll) meow_listview_set_scroll(g_set_lv, top);
|
||||
else if (bot > scroll + g_set_lv_h) meow_listview_set_scroll(g_set_lv, bot - g_set_lv_h);
|
||||
}
|
||||
|
||||
/* 创建设置页控件集(owner=SET_SENTINEL_ACT;仅首进创建一次,跨次进入复用)。 */
|
||||
static void set_widgets_create(void)
|
||||
{
|
||||
meow_ui_activity_push(SET_SENTINEL_ACT);
|
||||
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
||||
g_set_lv_h = lvh;
|
||||
g_set_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
|
||||
if (g_set_lv) {
|
||||
meow_listview_set_vsource(g_set_lv, set_lv_count, set_lv_bind, NULL);
|
||||
meow_listview_set_item_draw(g_set_lv, set_lv_draw, NULL);
|
||||
meow_listview_set_item_h(g_set_lv, SET_ITEM_H);
|
||||
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标会往回拉 */
|
||||
}
|
||||
g_set_sc = meow_scroll_create(set_sc_set, set_sc_max, set_sc_get, set_redraw_region, NULL);
|
||||
meow_ui_activity_pop();
|
||||
g_set_init = 1;
|
||||
}
|
||||
|
||||
/* 整页绘制(进 back_buf;调用方 meow_dirty_full/meow_dirty_add + flush)。sel=当前选中(SMENU 下标)。 */
|
||||
static void set_paint_page(int sel)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int W = fb_xres, H = fb_yres;
|
||||
fill_rect(0, 0, W, H, th->surface);
|
||||
draw_statusbar();
|
||||
if (mfont_ready) ui_draw(24, 50, meow_str_zh("menu.settings", "系统设置"), th->on_surface, 28);
|
||||
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
|
||||
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
|
||||
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface); /* P0:内容底 surface,行用圆角卡抬升 */
|
||||
if (g_set_lv) {
|
||||
g_set_sw_use = 0;
|
||||
int p = s_vis_of(sel); if (p >= 0) meow_listview_set_sel(g_set_lv, p);
|
||||
meow_widget_draw(g_set_lv);
|
||||
}
|
||||
/* 底部:返回(左)+ 版本(右),复用应用列表底部对齐逻辑 */
|
||||
char ver[32];
|
||||
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
|
||||
int foot_y = fb_yres - meow_font_line_height(20) - 8;
|
||||
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
|
||||
ui_draw(SET_CARD_X + 24, foot_y, meow_str_zh("settings.back", "返回"), th->on_surface_variant, 20);
|
||||
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
|
||||
}
|
||||
|
||||
/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */
|
||||
void run_settings_page(void)
|
||||
{
|
||||
int sel = s_first_setting();
|
||||
int scroll_off = 0;
|
||||
int H = fb_yres;
|
||||
unsigned long long t_sets = now_ms(); /* M3 取证:进设置页耗时分段(真机核对用) */
|
||||
|
||||
/* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,刷新「字体文件」下拉。
|
||||
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */
|
||||
meow_font_scan_presets();
|
||||
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。
|
||||
* M3:verify=0 —— 跳过逐套 sha256 校验(gbk 套 6.8MB 级,逐套双 sha256 真机耗数秒),
|
||||
* 只枚举+读 font.json+落 index,入口不再被 sha256sum 拖住;完整性校验留给启动全量扫描。 */
|
||||
meow_font_scan_presets(0);
|
||||
logmsg("settings entry: font rescan %llums", now_ms() - t_sets);
|
||||
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, kd_pwr;
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||||
int m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
key_router_rebase(); /* 进入设置页:音量基线对齐当前 raw(防残留边沿) */
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
unsigned long long last_tick = now_ms();
|
||||
unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */
|
||||
int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||||
unsigned long long net_refresh_pump_at = 0; /* M3:异步网络刷新下一推进时刻(0=立即) */
|
||||
|
||||
/* 触摸拖动滑条状态 */
|
||||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||
int drag = -1; /* 被拖动的行索引;-1=无 */
|
||||
/* M2.37 列表拖动滚动状态 */
|
||||
int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */
|
||||
int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */
|
||||
int drag_last_y = 0; /* M7.2:滚动跟手增量基准(meow_scroll_track 速度采样) */
|
||||
int scroll_y0 = 0; /* 按下原点纵向坐标 */
|
||||
int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */
|
||||
int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */
|
||||
@@ -992,10 +1350,17 @@ void run_settings_page(void)
|
||||
int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */
|
||||
|
||||
/* 首次全帧绘制 */
|
||||
s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */
|
||||
scroll_off = s_clamp_scroll(sel, scroll_off);
|
||||
draw_settings_page(sel, scroll_off);
|
||||
/* M2.39/M3:开关行同步真实硬件态(快)+ 异步 kick 网络刷新 —— 不再在入口同步跑
|
||||
* wpa_cli/bluetoothctl 串行 popen(真机 ≥5s 卡顿根因);状态/列表由下方主循环逐步骤
|
||||
* s_net_refresh_pump() 填充,draw 用缓存(g_net_*),数据变化即 need_paint。 */
|
||||
s_net_sync_from_sys();
|
||||
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
|
||||
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
|
||||
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
|
||||
key_router_page_enter(SET_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||
set_paint_page(sel);
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
|
||||
|
||||
for (;;) {
|
||||
unsigned long long now = now_ms();
|
||||
@@ -1031,48 +1396,51 @@ void run_settings_page(void)
|
||||
meow_auto_slept = 0;
|
||||
wake_suppress = 1;
|
||||
last_act = now_ms();
|
||||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸。
|
||||
* 音量防抖/基线上收 key_router:唤醒时重播音量基线;播放/电源仍本页自理。 */
|
||||
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
||||
logmsg("settings: auto screen-off -> input wake, stayed in settings page");
|
||||
scroll_off = s_clamp_scroll_range(scroll_off);
|
||||
draw_settings_page(sel, scroll_off);
|
||||
set_paint_page(sel);
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
}
|
||||
usleep(POLL_MS * 1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ---- M3:异步网络刷新(每 ~150ms 推进一步,每步一个有界 popen)----
|
||||
* 入口已同步 draw(用 g_net_* 缓存),此处逐步填充 SSID/已保存/附近/蓝牙角色,
|
||||
* 数据变化则 need_paint=1 让行值即时刷新,绝不在某一同步调用里连续跑数条 popen。 */
|
||||
if (now >= net_refresh_pump_at) {
|
||||
net_refresh_pump_at = now + 150;
|
||||
if (s_net_refresh_pump()) need_paint = 1;
|
||||
}
|
||||
|
||||
/* ---- 按键 ---- */
|
||||
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
|
||||
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
|
||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||||
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||
} else {
|
||||
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
||||
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
||||
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
||||
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
||||
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
||||
@@ -1108,14 +1476,11 @@ void run_settings_page(void)
|
||||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
||||
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
||||
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||||
need_paint = 1;
|
||||
@@ -1142,14 +1507,14 @@ void run_settings_page(void)
|
||||
}
|
||||
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||||
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
need_paint = 1;
|
||||
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||||
m_prev_play = play; m_prev_pwr = pwr;
|
||||
settings_keys_seeded:
|
||||
;
|
||||
|
||||
@@ -1166,34 +1531,47 @@ void run_settings_page(void)
|
||||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
|
||||
if (td && !td_ok && tlx >= 0 && tly >= 0) {
|
||||
tdx = tlx; tdy = tly; td_ok = 1;
|
||||
scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */
|
||||
/* 按下原点落在选中滑条 track 带内 → 开始拖动 */
|
||||
if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 &&
|
||||
tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
|
||||
tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) {
|
||||
drag = sel;
|
||||
const srow_t *r = &SMENU[sel];
|
||||
int v = s_slider_val_from_x(r, tlx);
|
||||
s_slider_set_from_track(r, v, 0);
|
||||
need_paint = 1;
|
||||
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(行按压态渲染) */
|
||||
scroll_y0 = tdy; /* 列表拖动基准:按下原点 */
|
||||
/* 按下原点落在滑条行 track 带内 → 开始拖动(meow_listview hit 判行) */
|
||||
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tlx, tly) : -1;
|
||||
if (vp >= 0 && drag < 0) {
|
||||
const srow_t *rr = &SMENU[s_vis_at(vp)];
|
||||
if (rr->ctrl == SC_SLIDER) {
|
||||
int track_cy = SET_LIST_Y + vp * SET_ITEM_H -
|
||||
meow_listview_scroll(g_set_lv) + SET_TRACK_CY;
|
||||
if (tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
|
||||
tly >= track_cy - 14 && tly <= track_cy + 14) {
|
||||
drag = s_vis_at(vp);
|
||||
int v = s_slider_val_from_x(rr, tlx);
|
||||
s_slider_set_from_track(rr, v, 0);
|
||||
need_paint = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
|
||||
* scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。
|
||||
* M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项);
|
||||
* 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */
|
||||
/* M7.2 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
|
||||
* 走 meow_scroll(跟手 track + 松手 fling),同 subpage.c M7.1a 语义;
|
||||
* 方向经 input.swipe_invert 切换(默认手指上滑 → 内容下翻)。 */
|
||||
if (td_ok && drag < 0 && !scroll_drag) {
|
||||
int ddy = tly - scroll_y0;
|
||||
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
|
||||
scroll_drag = 1;
|
||||
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
|
||||
scroll_base = scroll_off + (scroll_invert ? -ddy : ddy);
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
|
||||
drag_last_y = tly;
|
||||
}
|
||||
}
|
||||
if (scroll_drag && td_ok) {
|
||||
int dydt = tly - scroll_y0;
|
||||
scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt));
|
||||
need_paint = 1;
|
||||
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
|
||||
drag_last_y = tly;
|
||||
int scdy = scroll_invert ? dydt : -dydt;
|
||||
if (g_set_sc && scdy) meow_scroll_track(g_set_sc, scdy, now);
|
||||
/* 列表滚动不在此即时重绘:整批事件末尾由下方「if (scroll_drag && td_ok)
|
||||
* set_redraw_region」合并为一次视口局部重绘。此处若 set_redraw_region 且再设
|
||||
* need_paint=1,底部 set_paint_page 会再走一遍整页重绘 + dirty_add(0,42,...)
|
||||
* 近整屏 flush —— 快滑每帧整页重绘 → 卡顿,违背「局部脏区不整屏重绘」铁律;
|
||||
* 且一个 poll 多个移动事件各自 flush 会多次 FBIO_WAITFORVSYNC 阻塞放大延迟。 */
|
||||
}
|
||||
if (drag >= 0 && td_ok) {
|
||||
const srow_t *r = &SMENU[drag];
|
||||
@@ -1210,6 +1588,7 @@ void run_settings_page(void)
|
||||
}
|
||||
} else if (!val && td) {
|
||||
td = 0;
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
|
||||
if (drag >= 0) { /* 滑条拖动结束 → 落盘 */
|
||||
const srow_t *r = &SMENU[drag];
|
||||
s_commit(r, 1);
|
||||
@@ -1217,7 +1596,8 @@ void run_settings_page(void)
|
||||
need_paint = 1;
|
||||
} else if (scroll_drag) {
|
||||
scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */
|
||||
scroll_base = 0; scroll_y0 = 0;
|
||||
if (g_set_sc) meow_scroll_fling(g_set_sc); /* 松手 fling(惯性 ease-out) */
|
||||
scroll_y0 = 0;
|
||||
need_paint = 1;
|
||||
} else if (td_ok) {
|
||||
int dx = tlx - tdx, dy = tly - tdy;
|
||||
@@ -1236,8 +1616,10 @@ void run_settings_page(void)
|
||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||||
goto done;
|
||||
}
|
||||
int cy = ty - SET_CONTENT_Y + scroll_off;
|
||||
int idx = s_row_at_content_y(cy);
|
||||
/* M7.2:行命中走 meow_listview(含滚动换算 + 禁用/越界行不响应);
|
||||
* 段头行 enabled=1 故 hit 亦命中 → 下方按 ctrl==SC_HEADER 分流。 */
|
||||
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tx, ty) : -1;
|
||||
int idx = (vp >= 0) ? s_vis_at(vp) : -1;
|
||||
if (idx >= 0 && !s_setting_idx(idx)) {
|
||||
/* 点分组标题行 → 仅选中下一设置行 */
|
||||
int ns = s_next_setting(idx);
|
||||
@@ -1255,8 +1637,9 @@ void run_settings_page(void)
|
||||
* M2.76:WiFi/蓝牙升级为独立子页面 → 点开进 run_wifi_page/
|
||||
* run_bt_page(阻塞,内部自开 fds;返回后仍须排空本页 tfd)。 */
|
||||
after_modal = 1;
|
||||
if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh(); }
|
||||
else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh(); }
|
||||
if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh_kick(); }
|
||||
else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh_kick(); }
|
||||
else if (r->net_list == NET_LIST_PIN_PAGE) { run_lock_settings_page(); s_net_refresh_kick(); }
|
||||
else s_net_open(r);
|
||||
if (tfd >= 0) {
|
||||
unsigned char e[16];
|
||||
@@ -1265,17 +1648,13 @@ void run_settings_page(void)
|
||||
while (read(tfd, e, sizeof e) == 16) {}
|
||||
}
|
||||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
|
||||
scroll_base = 0; scroll_y0 = 0;
|
||||
scroll_y0 = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
key_router_rebase(); /* 音量基线重播 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
need_paint = 1;
|
||||
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
||||
@@ -1309,18 +1688,14 @@ void run_settings_page(void)
|
||||
while (read(tfd, e, sizeof e) == 16) {}
|
||||
}
|
||||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
|
||||
scroll_base = 0; scroll_y0 = 0;
|
||||
scroll_y0 = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
/* 重置设置页按键防抖基线,防残边 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
key_router_rebase(); /* 音量基线重播 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
need_paint = 1;
|
||||
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
|
||||
@@ -1335,14 +1710,18 @@ void run_settings_page(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 列表拖动滚动:本批连续触摸事件合并为一次视口局部重绘(只刷列表区,
|
||||
* 最多一次 present),不再触发 need_paint 整页重绘。scroll_drag/td_ok 在
|
||||
* 抬手处置 0(抬手那轮已走 meow_scroll_fling + need_paint 整页),故此处只兜活动滚动。 */
|
||||
if (scroll_drag && td_ok) set_redraw_region(NULL);
|
||||
}
|
||||
|
||||
/* ---- 滚动 + 重绘 ---- */
|
||||
if (need_paint) {
|
||||
last_act = now;
|
||||
/* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */
|
||||
if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; }
|
||||
draw_settings_page(sel, scroll_off);
|
||||
if (sel_changed) { set_ensure_sel_visible(sel); sel_changed = 0; }
|
||||
set_paint_page(sel);
|
||||
/* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化),
|
||||
* 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。
|
||||
* 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */
|
||||
@@ -1367,9 +1746,14 @@ void run_settings_page(void)
|
||||
fb_norm_buf0();
|
||||
meow_ui_flush();
|
||||
}
|
||||
/* M7.2: 推进 fling/回弹动画(meow_scroll 惯性停;无动画零开销) */
|
||||
meow_anim_tick();
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
done:
|
||||
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
|
||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||
key_router_page_exit(SET_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_power); kp_close(&kp_keys);
|
||||
logmsg("settings page closed");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙)统一脚手架(M2.76)。
|
||||
/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙/锁屏 PIN)统一脚手架(M2.76)。
|
||||
*
|
||||
* 安卓风子页骨架:顶栏(标题 + 返回 + 右上开关)+ 分组列表(组标题/行/开关/动作)
|
||||
* + 滚动(跟手,方向沿用设置页 input.swipe_invert 语义)+ 左缘左滑返回 + 底部返回。
|
||||
* + 滚动 + 左缘左滑返回。
|
||||
*
|
||||
* 由页面(wifi_page.c / bt_page.c)通过 sub_page_t 提供:
|
||||
* 由页面(wifi_page.c / bt_page.c / lock_settings.c)通过 sub_page_t 提供:
|
||||
* - title 顶栏标题
|
||||
* - top_sw 顶栏开关(0=不显示;1=显示并回写当前值)
|
||||
* - top_sw_set 开关切换(写设置 + 生效 + 触发重扫)
|
||||
@@ -12,7 +12,18 @@
|
||||
* - detail 点普通行(如当前连接详情、网络/设备详情)
|
||||
* - live 1=驻留时周期性(约2s)重扫行(蓝牙附近扫描等动态数据)
|
||||
*
|
||||
* 与设置页同主题(s_ui_theme,MD3 语义色,明暗自动);局部刷新不整屏重绘;
|
||||
* ================================================================
|
||||
* M7.1a 渲染层迁移:脚手架手绘(顶栏 + 分组列表 + 跟手滚动 + 手绘 switch/chevron)
|
||||
* 改走 SDK 控件(单一 meow_theme 权威):
|
||||
* - 顶栏 → meow_topappbar(标题 + 返回箭头 + 底部分隔线);
|
||||
* - 顶部开关 → meow_switch(叠顶栏右缘,保留开/关视觉与命中);
|
||||
* - 分组列表 → meow_listview(virtualized set_vsource 固定 item 池 + section 段头
|
||||
* + right=SWITCH/ARROW 尾件 + 选中/按压态);
|
||||
* - 滚动 → meow_scroll(跟手 track 采样松手 fling + snap 到行边界)。
|
||||
* wifi/bt/lock_settings 的「数据 + 动作」逻辑(netmgr IPC / bluetoothctl / 设置契约)
|
||||
* 全部保留在 launcher 逻辑层,仅换渲染。旧手绘路径整体保留 #if 0 作回退位(M7 红线)。
|
||||
*
|
||||
* 与设置页同主题(s_ui_theme → SDK meow_theme 单一权威,MD3/MIUIx×明暗自动);
|
||||
* 模态弹窗交给 launcher 既有 s_dd_modal(点外关闭不穿透),本脚手架在回调返回后
|
||||
* 彻底排空本页 tfd + 重置触摸基线(复刻 run_settings_page 后置排空,防穿透)。
|
||||
*
|
||||
@@ -22,16 +33,28 @@
|
||||
#include "meow_launcher_internal.h"
|
||||
|
||||
|
||||
/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface_variant 整屏底)---- */
|
||||
/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface 整屏底)----
|
||||
* note:SUB_CONT_TOP 与旧版一致(96),顶栏底缘分隔线由 SDK topappbar 绘制(y=95)。 */
|
||||
#define SUB_TB_Y SB_BAR_H /* 28:顶栏顶缘(状态栏下) */
|
||||
#define SUB_TB_H 68 /* 顶栏高 28..96 */
|
||||
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 / 分隔线 */
|
||||
#define SUB_CONT_TOP (SUB_TB_BOT + 6) /* 102:内容区顶缘 */
|
||||
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 */
|
||||
#define SUB_CONT_TOP (SUB_TB_BOT) /* 96:内容区顶缘(topappbar 分隔线下) */
|
||||
#define SUB_FOOT_H 44 /* 底部版本信息带(右对齐) */
|
||||
#define SUB_ITEM_H 56 /* MD3 行高(双行富 item:主/副标题) */
|
||||
#define SUB_PAD_X 16 /* 水平内边距(顶开关右缘留白用) */
|
||||
#define SUB_SW_H 32 /* 顶开关高(meow_switch 固定 56×32) */
|
||||
|
||||
|
||||
#if 0 /* ================================================================
|
||||
* M7.1a 退役:子页旧手绘脚手架(顶栏 + 分组列表 + 跟手无惯性滚动 + 手绘
|
||||
* s_draw_switch/s_draw_chevron + 自算行前缀和 + 自算命中)。改走 SDK 控件
|
||||
* (meow_topappbar + meow_listview + meow_scroll),命中/布局/状态/滚动/主题
|
||||
* 全由 SDK 接管。本块保留作回退位(删除 #if 0 行并删下方「M7.1a SDK 路径」段
|
||||
* 即可恢复旧手绘)。以下全部内容与迁移前 subpage.c 逐字一致。
|
||||
* ================================================================ */
|
||||
#define SUB_ROW_H 58 /* 行高(对齐 SROW_H,便于触摸) */
|
||||
#define SUB_HDR_H 40 /* 组标题行高 */
|
||||
#define SUB_PAD_X 16 /* 水平内边距 */
|
||||
#define SUB_SW_W 48 /* 开关宽(s_draw_switch) */
|
||||
#define SUB_SW_H 28
|
||||
#define SUB_BTN_X 8 /* 底部返回按钮左缘 */
|
||||
#define SUB_BTN_W 120 /* 底部返回按钮宽 */
|
||||
|
||||
@@ -224,12 +247,11 @@ int meow_subpage_run(sub_page_t *p)
|
||||
meow_auto_slept = 0;
|
||||
wake_suppress = 1;
|
||||
last_act = now;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
|
||||
@@ -279,14 +301,12 @@ int meow_subpage_run(sub_page_t *p)
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||
pwr_suppress_click = 1;
|
||||
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
@@ -433,14 +453,12 @@ int meow_subpage_run(sub_page_t *p)
|
||||
scroll_base = 0; scroll_y0 = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
@@ -502,3 +520,566 @@ subdone:
|
||||
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
||||
return 0;
|
||||
}
|
||||
#endif /* M7.1a 退役:旧手绘子页脚手架回退位 */
|
||||
|
||||
|
||||
/* ==================================================================
|
||||
* M7.1a 子页渲染层迁移(SDK 控件化):
|
||||
* - 顶栏 → meow_topappbar(标题 + 返回箭头 + 底部分隔线);
|
||||
* - 顶部开关 → meow_switch(叠顶栏右缘,保留开/关视觉 + 命中);
|
||||
* - 分组列表 → meow_listview(virtualized set_vsource 固定 item 池 + section 段头
|
||||
* + right=SWITCH/ARROW 尾件 + 选中/按压态,SDK 语义色);
|
||||
* - 滚动 → meow_scroll(跟手 meow_scroll_track 采样 + 松手 fling ease-out 惯性停;
|
||||
* 刻意不 set_snap——SDK snap 以 fling 起点为吸附目标会往回拉,见 sp_widgets_create 注)。
|
||||
* 控件 lazy 创建一次(owner=SUB_SENTINEL_ACT 哨兵),跨多次进入复用;meow_subpage_run()
|
||||
* 进入 push 哨兵、退出 pop(owner 闸门使控件命中生效)。数据/动作回调(rows/act/detail/
|
||||
* top_sw_*)仍由各子页提供,逻辑层不动,仅渲染层替换。
|
||||
* ================================================================== */
|
||||
#define SUB_SENTINEL_ACT 0x52 /* 子页哨兵 activity(不与 launcher 其它页冲突) */
|
||||
|
||||
static sub_page_t *g_sp; /* 当前运行子页(rows/act/detail/top_sw 回调入口) */
|
||||
static sub_row_t g_rows[SUB_ROWS_MAX]; /* 行缓冲:rows() 一次性填满,listview bind 只读 */
|
||||
static int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
|
||||
static int g_top_sw_en = 0; /* 本页是否显示顶开关 */
|
||||
static int g_top_on = 0; /* 顶开关当前值 */
|
||||
|
||||
static meow_widget *g_tb = NULL; /* topappbar */
|
||||
static meow_widget *g_sw = NULL; /* 顶开关(仅在有顶开关的页创建/使用) */
|
||||
static meow_widget *g_lv = NULL; /* listview */
|
||||
static meow_scroll *g_sc = NULL; /* 滚动物理 */
|
||||
static int g_sp_init = 0;
|
||||
|
||||
/* 列表视口矩形(内容区,去掉底部版本带) */
|
||||
static void sp_lv_rect(int *x, int *y, int *w, int *h)
|
||||
{
|
||||
if (x) *x = 0;
|
||||
if (y) *y = SUB_CONT_TOP;
|
||||
if (w) *w = fb_xres;
|
||||
if (h) { *h = fb_yres - SUB_CONT_TOP - SUB_FOOT_H; if (*h < 0) *h = 0; }
|
||||
}
|
||||
|
||||
/* ---- 行缓冲 → meow_lv_item(虚拟化 bind;每行轻量填充,不做 IO §5.5)---- */
|
||||
static int sp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
|
||||
static void sp_lv_bind(int idx, meow_lv_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->icon_kind = MEOW_LIST_ICON_NONE;
|
||||
out->enabled = 1;
|
||||
if (idx < 0 || idx >= g_nrows) { out->title = ""; return; }
|
||||
const sub_row_t *r = &g_rows[idx];
|
||||
switch (r->kind) {
|
||||
case SUB_HDR: /* 段头占位行(非粘性):default renderer 特殊绘制 */
|
||||
out->section = 1; out->title = r->label ? r->label : "";
|
||||
out->right = MEOW_LIST_RIGHT_NONE; out->enabled = 0; break;
|
||||
case SUB_ROW: /* 普通信息行 */
|
||||
out->title = r->label ? r->label : "";
|
||||
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_NONE; break;
|
||||
case SUB_SW: /* 开关行:右尾件 SWITCH */
|
||||
out->title = r->label ? r->label : "";
|
||||
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_SWITCH; out->on = r->on; break;
|
||||
case SUB_ACT: /* 动作行:右尾件 ARROW */
|
||||
out->title = r->label ? r->label : "";
|
||||
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_ARROW; break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 行完全自绘(批③:页面经 sub_page_t.row_draw 定制整行;默认 renderer 被覆盖)---- */
|
||||
static void sp_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!g_sp || !g_sp->row_draw) return;
|
||||
if (idx < 0 || idx >= g_nrows) return;
|
||||
g_sp->row_draw(g_sp->ctx, idx, &g_rows[idx], x, y, w, h, state);
|
||||
}
|
||||
|
||||
/* ---- 顶栏标题栏右侧动作按钮矩形(批③:sub_page_t.top_action)---- */
|
||||
static void sp_top_action_rect(int *x, int *y, int *w, int *h)
|
||||
{
|
||||
if (x) *x = 0;
|
||||
if (y) *y = 0;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!g_sp || !g_sp->top_action) return;
|
||||
int tw = ui_width(g_sp->top_action, 20);
|
||||
if (tw <= 0) return;
|
||||
int bw = tw + 24, bh = 40; /* 含内边距的圆角按钮 */
|
||||
*x = fb_xres - 12 - bw;
|
||||
*y = SUB_TB_Y + (SUB_TB_H - bh) / 2;
|
||||
*w = bw; *h = bh;
|
||||
}
|
||||
|
||||
/* 绘制顶栏动作按钮(sp_paint_page 内调用)。 */
|
||||
static void sp_paint_top_action(void)
|
||||
{
|
||||
if (!g_sp || !g_sp->top_action) return;
|
||||
int x, y, w, h; sp_top_action_rect(&x, &y, &w, &h);
|
||||
if (w <= 0 || h <= 0) return;
|
||||
const meow_theme *mt = meow_theme_get();
|
||||
uint32_t col = mt->primary;
|
||||
meow_ui_draw_rect_round(x, y, w, h, h / 2, col); /* 全形=胶囊描边按钮(P1) */
|
||||
int th = meow_ui_text_height(20);
|
||||
if (th > h) th = h;
|
||||
meow_ui_text_center(x + w / 2, y + (h - th) / 2, g_sp->top_action, 20, col);
|
||||
}
|
||||
|
||||
/* ---- meow_scroll 回调(包 listview)---- */
|
||||
static void sp_sc_set(void *ctx, int off) { (void)ctx; if (g_lv) meow_listview_set_scroll(g_lv, off); }
|
||||
static int sp_sc_max(void *ctx) { (void)ctx; return g_lv ? meow_listview_max_scroll(g_lv) : 0; }
|
||||
static int sp_sc_get(void *ctx) { (void)ctx; return g_lv ? meow_listview_scroll(g_lv) : 0; }
|
||||
|
||||
/* 列表区重绘(含背景 + listview),供 fling 每帧/滚动局部刷。ctx 为 meow_scroll 回调用。 */
|
||||
static void sp_redraw_region(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
int lvx, lvy, lvw, lvh;
|
||||
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
||||
if (lvh <= 0) return;
|
||||
const meow_theme *mt = meow_theme_get();
|
||||
meow_ui_fill_rect(lvx, lvy, lvw, lvh, mt->surface); /* 内容底 surface */
|
||||
if (g_lv) meow_widget_draw(g_lv);
|
||||
meow_dirty_add(lvx, lvy, lvw, lvh);
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
/* 创建子页控件集(owner=SUB_SENTINEL_ACT;仅首进创建一次,跨页复用)。 */
|
||||
static void sp_widgets_create(void)
|
||||
{
|
||||
meow_ui_activity_push(SUB_SENTINEL_ACT);
|
||||
g_tb = meow_topappbar_create(0, SUB_TB_Y, fb_xres, SUB_TB_H, "");
|
||||
if (g_tb) meow_topappbar_set_back(g_tb, 1);
|
||||
int lvx, lvy, lvw, lvh;
|
||||
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
||||
g_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
if (g_lv) {
|
||||
meow_listview_set_vsource(g_lv, sp_lv_count, sp_lv_bind, NULL);
|
||||
/* 双行富 item + 无左图标 + 右尾件(SWITCH/ARROW);行高 56 */
|
||||
meow_listview_set_item_renderer(g_lv, 2, 0, 1);
|
||||
meow_listview_set_item_h(g_lv, SUB_ITEM_H);
|
||||
/* MD3(P0):默认 renderer 容器模式——行=surfaceContainer 圆角卡+行间空隙,
|
||||
* 选中/按压 tonal(primaryContainer/primary α26)、去冗余分隔线;段头=surface 上的 primary 段标签。 */
|
||||
meow_listview_set_container(g_lv, 1);
|
||||
}
|
||||
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标,快速滑动时会
|
||||
* 从自然停点往回拉一整行(实测 697→336),UX 不佳;默认 ease-out 惯性停(无 snap)更顺滑。
|
||||
* 若后续 SDK 修正 snap 到「自然停点」,可恢复 meow_scroll_set_snap(g_sc, SUB_ITEM_H)。 */
|
||||
g_sc = meow_scroll_create(sp_sc_set, sp_sc_max, sp_sc_get, sp_redraw_region, NULL);
|
||||
meow_ui_activity_pop();
|
||||
g_sp_init = 1;
|
||||
}
|
||||
|
||||
/* 逐页配置:topappbar 标题 + 顶开关(按需创建/置态)+ listview 行数。 */
|
||||
static void sp_configure_page(const sub_page_t *p, int top_sw_en, int top_on)
|
||||
{
|
||||
if (p->title && g_tb) meow_topappbar_set_title(g_tb, p->title);
|
||||
g_top_sw_en = top_sw_en;
|
||||
g_top_on = top_on;
|
||||
if (top_sw_en) {
|
||||
if (!g_sw) {
|
||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
|
||||
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
|
||||
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
|
||||
g_sw = meow_switch_create(swx, swy, top_on);
|
||||
meow_ui_activity_pop();
|
||||
} else if (g_sw) {
|
||||
meow_switch_set(g_sw, top_on);
|
||||
}
|
||||
}
|
||||
/* 批③:行完全自绘 on/off —— 页提供 row_draw 用自绘;否则清 NULL 回默认 renderer。 */
|
||||
if (g_lv) meow_listview_set_item_draw(g_lv, p->row_draw ? sp_lv_draw : NULL, NULL);
|
||||
}
|
||||
|
||||
/* 整页绘制(进 back_buf;调用方 meow_dirty_full + flush 上屏)。 */
|
||||
static void sp_paint_page(void)
|
||||
{
|
||||
const meow_theme *mt = meow_theme_get();
|
||||
int W = fb_xres, H = fb_yres;
|
||||
meow_ui_fill_rect(0, 0, W, H, mt->surface); /* 全屏底 surface */
|
||||
draw_statusbar();
|
||||
if (g_tb) meow_widget_draw(g_tb); /* topappbar(含底部分隔线) */
|
||||
if (g_sw && g_top_sw_en) meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
|
||||
sp_paint_top_action(); /* 批③:顶栏右侧动作按钮 */
|
||||
int lvx, lvy, lvw, lvh;
|
||||
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
||||
meow_ui_fill_rect(lvx, lvy, lvw, lvh, mt->surface); /* 内容底 surface */
|
||||
if (g_lv) meow_widget_draw(g_lv); /* 分组列表 */
|
||||
/* 底部版本信息(右对齐 dim) */
|
||||
char ver[32];
|
||||
snprintf(ver, sizeof ver, "MeowOS v%s", g_meowos_version);
|
||||
ui_draw(W - 8 - ui_width(ver, 18), H - SUB_FOOT_H + 10, ver,
|
||||
s_ui_theme()->outline, 18);
|
||||
}
|
||||
|
||||
/* 顶部开关切换:写设置 + 生效 + 触发重扫 + 重填行。 */
|
||||
static void sp_top_sw_toggle(void)
|
||||
{
|
||||
if (!g_sp || !g_sp->top_sw_set) return;
|
||||
g_sp->top_sw_set(g_sp->ctx, !g_top_on);
|
||||
g_top_on = !g_top_on;
|
||||
if (g_sw) meow_switch_set(g_sw, g_top_on);
|
||||
if (g_sp->rows) {
|
||||
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
|
||||
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动作后重扫行(act/detail 已变更数据):重填行并 clamp 滚动。 */
|
||||
static void sp_refresh_rows(void)
|
||||
{
|
||||
if (g_sp && g_sp->rows) {
|
||||
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
|
||||
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
|
||||
}
|
||||
if (g_lv) {
|
||||
int ms = meow_listview_max_scroll(g_lv);
|
||||
if (meow_listview_scroll(g_lv) > ms) meow_listview_set_scroll(g_lv, ms);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 运行子页(阻塞到返回;返回 0)---- */
|
||||
int meow_subpage_run(sub_page_t *p)
|
||||
{
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, kd_pwr;
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
key_router_rebase(); /* 进入子页:音量基线对齐当前 raw(防残留边沿) */
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
unsigned long long last_tick = now_ms();
|
||||
unsigned long long last_act = now_ms();
|
||||
unsigned long long last_live = now_ms();
|
||||
int after_modal = 0; /* 子页内模态关闭后:覆盖过状态栏,需整帧重绘还原 */
|
||||
|
||||
/* 触摸状态 */
|
||||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||
int scroll_drag = 0, scroll_invert = 0, drag_last_y = 0;
|
||||
int wake_suppress = 0;
|
||||
unsigned long long pwr_hold_at = 0;
|
||||
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
|
||||
|
||||
/* 顶栏开关初始值 */
|
||||
int top_on = 0, top_sw_en = 0;
|
||||
if (p->top_sw) top_sw_en = p->top_sw(p->ctx, &top_on);
|
||||
|
||||
/* 首填行 + lazy 建控件 + 配置 + 首绘 */
|
||||
g_sp = p;
|
||||
g_nrows = p->rows ? p->rows(p->ctx, g_rows, SUB_ROWS_MAX) : 0;
|
||||
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
|
||||
if (!g_sp_init) sp_widgets_create();
|
||||
sp_configure_page(p, top_sw_en, top_on);
|
||||
|
||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
|
||||
key_router_page_enter(SUB_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||
sp_paint_page();
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
|
||||
for (;;) {
|
||||
unsigned long long now = now_ms();
|
||||
if (sigterm_flag) break;
|
||||
int need_paint = 0;
|
||||
|
||||
/* ---- 自动息屏态:留在子页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- */
|
||||
if (meow_auto_slept && !meow_screen_on) {
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int wake = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1;
|
||||
}
|
||||
if (!wake && screen_off_mode == 1 && tfd >= 0) {
|
||||
struct pollfd spfd = { tfd, POLLIN, 0 };
|
||||
if (poll(&spfd, 1, 0) > 0) {
|
||||
unsigned char ev[16];
|
||||
while (read(tfd, ev, sizeof ev) == 16) {
|
||||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||||
int32_t val = *(int32_t *)(ev + 12);
|
||||
if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wake) {
|
||||
meow_panel_on();
|
||||
meow_auto_slept = 0;
|
||||
wake_suppress = 1;
|
||||
last_act = now;
|
||||
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
|
||||
after_modal = 1;
|
||||
sp_paint_page();
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
}
|
||||
usleep(POLL_MS * 1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
|
||||
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||
} else {
|
||||
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
|
||||
if (pwr && !m_prev_pwr) {
|
||||
pwr_hold_at = now; pwr_menu_state = 0;
|
||||
pwr_suppress_click = 0;
|
||||
last_act = now;
|
||||
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
|
||||
}
|
||||
if (pwr && pwr_hold_at) {
|
||||
if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) {
|
||||
if (sim_no_exec) { logmsg("subpage pm: [sim-no-exec] POWER HOLD -> force poweroff"); }
|
||||
else { logmsg("subpage pm: POWER HOLD -> force poweroff"); meow_power_action(POWER_SHUTDOWN); }
|
||||
} else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) {
|
||||
pwr_menu_state = 1;
|
||||
logmsg("subpage pm: POWER HOLD -> show power menu");
|
||||
show_power_menu();
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||
pwr_suppress_click = 1;
|
||||
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
goto sub_keys_seeded;
|
||||
}
|
||||
}
|
||||
if (!pwr && m_prev_pwr) {
|
||||
if (pwr_suppress_click) {
|
||||
logmsg("subpage pm: power release after menu -> no toggle");
|
||||
} else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) &&
|
||||
!pwr_menu_state && !pwr_woke) {
|
||||
logmsg("subpage pm: power single-click -> toggle panel");
|
||||
meow_toggle_panel();
|
||||
meow_auto_slept = 0;
|
||||
last_act = now_ms();
|
||||
need_paint = 1;
|
||||
} else if (pwr_woke) {
|
||||
logmsg("subpage pm: power release after auto wake -> no toggle");
|
||||
}
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||
pwr_suppress_click = 0;
|
||||
}
|
||||
}
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||
(void)vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
need_paint = 1;
|
||||
}
|
||||
m_prev_play = play; m_prev_pwr = pwr;
|
||||
sub_keys_seeded:
|
||||
;
|
||||
|
||||
/* ---- 触摸(SDK 控件命中:topappbar/顶开关/listview + meow_scroll 滚动)---- */
|
||||
if (tfd >= 0 && meow_screen_on) {
|
||||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||||
if (poll(&pfd, 1, 0) > 0) {
|
||||
unsigned char ev[16];
|
||||
while (read(tfd, ev, sizeof ev) == 16) {
|
||||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||||
int32_t val = *(int32_t *)(ev + 12);
|
||||
if (type == EV_ABS) {
|
||||
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
|
||||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
|
||||
if (td && !td_ok && tlx >= 0 && tly >= 0) {
|
||||
tdx = tlx; tdy = tly; td_ok = 1;
|
||||
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(按压态渲染) */
|
||||
drag_last_y = tdy;
|
||||
if (g_lv && meow_widget_hit(g_lv, tlx, tly)) /* 按压态即时反馈 */
|
||||
sp_redraw_region(NULL);
|
||||
}
|
||||
if (td_ok && !scroll_drag) {
|
||||
int ddy = tly - drag_last_y;
|
||||
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
|
||||
scroll_drag = 1;
|
||||
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
|
||||
drag_last_y = tly; /* 从此增量(跨 slop 无跳变,对齐旧版补偿) */
|
||||
}
|
||||
}
|
||||
if (scroll_drag && td_ok) {
|
||||
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
|
||||
drag_last_y = tly;
|
||||
int scdy = scroll_invert ? dydt : -dydt;
|
||||
if (g_sc && scdy) meow_scroll_track(g_sc, scdy, now);
|
||||
sp_redraw_region(NULL); /* 列表区重绘(新 offset) */
|
||||
need_paint = 1;
|
||||
}
|
||||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||||
if (val && !td) {
|
||||
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
|
||||
else { td = 1; td_ok = 0; tlx = -1; tly = -1; scroll_drag = 0;
|
||||
meow_input_touch_set(0, 0, 0, 0); }
|
||||
} else if (!val && td) {
|
||||
td = 0;
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
|
||||
if (scroll_drag) {
|
||||
scroll_drag = 0;
|
||||
if (g_sc) meow_scroll_fling(g_sc); /* 松手 fling(速度采样) */
|
||||
} else if (td_ok) {
|
||||
int dx = tlx - tdx, dyy = tly - tdy;
|
||||
/* 左缘左滑返回(全局手势语义) */
|
||||
if (meow_edge_back(tdx, dx, dyy)) {
|
||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||||
goto subdone;
|
||||
}
|
||||
int adx = dx < 0 ? -dx : dx, ady = dyy < 0 ? -dyy : dyy;
|
||||
if (adx < 60 && ady < 60) {
|
||||
int tx = tlx, ty = tly;
|
||||
/* 顶栏:返回箭头(topappbar hit)/ 顶开关(meow_switch hit) */
|
||||
int tb_hit = g_tb ? meow_topappbar_hit(g_tb, tx, ty) : -1;
|
||||
if (tb_hit == MEOW_TOPBAR_BACK) {
|
||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||||
goto subdone;
|
||||
}
|
||||
/* 批③:顶栏标题栏右侧动作按钮(一键清除;先于顶开关判定,二者页间互斥) */
|
||||
if (g_sp && g_sp->top_action) {
|
||||
int ax, ay, aw, ah; sp_top_action_rect(&ax, &ay, &aw, &ah);
|
||||
if (aw > 0 && tx >= ax && tx < ax + aw && ty >= ay && ty < ay + ah) {
|
||||
if (g_sp->top_action_cb) g_sp->top_action_cb(g_sp->ctx);
|
||||
last_act = now;
|
||||
sp_refresh_rows(); /* 清后立即重扫行 */
|
||||
need_paint = 1;
|
||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (g_top_sw_en && g_sw && meow_widget_hit(g_sw, tx, ty)) {
|
||||
sp_top_sw_toggle();
|
||||
last_act = now;
|
||||
need_paint = 1;
|
||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||||
continue;
|
||||
}
|
||||
/* 列表行命中(SDK 命中:含滚动换算 + 越界/禁用行不响应) */
|
||||
int row_hit = g_lv ? meow_listview_hit(g_lv, tx, ty) : -1;
|
||||
if (row_hit >= 0) {
|
||||
sub_kind_t kind = g_rows[row_hit].kind;
|
||||
if (kind == SUB_ROW && g_sp->row_tap) {
|
||||
/* 批③:行内自定义命中(行内杀按钮 vs 行体;页自判,返回 1=重绘) */
|
||||
int rry = SUB_CONT_TOP + row_hit * SUB_ITEM_H -
|
||||
(g_lv ? meow_listview_scroll(g_lv) : 0);
|
||||
g_sp->row_tap(g_sp->ctx, row_hit, &g_rows[row_hit],
|
||||
tx, ty, 0, rry, fb_xres, SUB_ITEM_H);
|
||||
} else if (kind == SUB_SW || kind == SUB_ACT) {
|
||||
if (g_sp->act) g_sp->act(g_sp->ctx, row_hit, g_rows[row_hit].act);
|
||||
} else if (kind == SUB_ROW) {
|
||||
if (g_sp->detail) g_sp->detail(g_sp->ctx, row_hit, g_rows[row_hit].act);
|
||||
}
|
||||
/* 动作后重扫行 + clamp 滚动 */
|
||||
sp_refresh_rows();
|
||||
}
|
||||
/* 仅命中行(回调可能开模态)才做后置排空/触摸基线重置/整帧重绘
|
||||
* (对齐旧版 idx>=0 分支;空白区点按不重绘不排空,置 td_ok=0 即可)。 */
|
||||
if (row_hit >= 0) {
|
||||
if (tfd >= 0) {
|
||||
unsigned char e[16];
|
||||
while (read(tfd, e, sizeof e) == 16) {}
|
||||
usleep(40000);
|
||||
while (read(tfd, e, sizeof e) == 16) {}
|
||||
}
|
||||
td = 0; td_ok = 0; scroll_drag = 0;
|
||||
drag_last_y = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
td_ok = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
|
||||
if (g_sp && g_sp->live && now - last_live >= 2000) {
|
||||
last_live = now;
|
||||
int new_n = g_sp->rows ? g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX) : 0;
|
||||
if (new_n > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX;
|
||||
if (new_n != g_nrows) {
|
||||
g_nrows = new_n;
|
||||
int ms = g_lv ? meow_listview_max_scroll(g_lv) : 0;
|
||||
if (g_lv && meow_listview_scroll(g_lv) > ms) meow_listview_set_scroll(g_lv, ms);
|
||||
last_act = now;
|
||||
need_paint = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 重绘(整帧/内容区局部;fling 由 meow_scroll redraw 回调自刷)---- */
|
||||
if (need_paint) {
|
||||
last_act = now;
|
||||
sp_paint_page();
|
||||
if (after_modal) { meow_dirty_full(); after_modal = 0; }
|
||||
else meow_dirty_add(0, SUB_TB_Y, fb_xres, fb_yres - SUB_TB_Y); /* 含顶栏(开关/返回/标题) */
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
/* ---- 自动息屏(空闲超时关屏,留在子页,等输入唤醒)---- */
|
||||
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept &&
|
||||
(unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) {
|
||||
meow_panel_off();
|
||||
meow_auto_slept = 1;
|
||||
wake_suppress = 1;
|
||||
logmsg("subpage: idle %ds -> auto screen-off (stay)", screen_off_timeout);
|
||||
}
|
||||
/* ~1s 状态栏时钟刷新 */
|
||||
if (now - last_tick >= 1000) {
|
||||
last_tick = now;
|
||||
draw_clock();
|
||||
fb_norm_buf0();
|
||||
meow_ui_flush();
|
||||
}
|
||||
/* M7.1a: 推进 fling/回弹/snap 动画(无动画零开销) */
|
||||
meow_anim_tick();
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
subdone:
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
|
||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||
key_router_page_exit(SUB_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_power); kp_close(&kp_keys);
|
||||
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/* test_ams_start.c — 宿主单测:MSG_AMS_START 拉起决策(meow_ams_start.c)。
|
||||
*
|
||||
* 覆盖(AMSD_DESIGN §4.3 缺口回归):
|
||||
* 1) 合法 MSG_AMS_START(裸启动 act 空)→ 置 pending → consume → ops.launch(idx, NULL);
|
||||
* 2) 合法 MSG_AMS_START + activity → resolve → consume → ops.launch(idx, chain);
|
||||
* 3) 参数非法(pkg 空)→ meow_ams_pending_start_set 返回 MEOW_ERR_INVAL,不置位、不拉起、不崩;
|
||||
* 4) 有前台 App(busy)→ set 返回 MEOW_ERR_BUSY(负错误码,根因修复),不拉起;
|
||||
* 5) 列表未就绪(state!=0)→ consume 返回 2(pending 保留),不拉起;后就绪再 consume → 拉起;
|
||||
* 6) pkg 不在应用表 → consume 返回 1,toast「NO APP」,不拉起;
|
||||
* 7) activity 无路由 → consume 返回 1,toast「NO ROUTE」,不拉起;
|
||||
* 8) 启动失败(ops.launch 返 <0)→ consume 返回 1,app_ran=0,idx_out 命中(供 launch_fail_toast);
|
||||
* 9) picker 扩载荷:set_ex 武装 → take_picker 取回并清位。
|
||||
* 只依赖 meow_ams_start.c + meow_ipc.h(纯 C),宿主 gcc 直编:
|
||||
* gcc -Wall -Wextra -std=gnu99 test_ams_start.c meow_ams_start.c -o test_ams_start
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include "meow_ams_start.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
static int g_fails = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) printf("PASS: %s\n", msg); \
|
||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||
|
||||
/* ---- 假件 ---- */
|
||||
static int g_launch_calls = 0; /* ops.launch 被调次数 */
|
||||
static int g_launch_last_idx = -1;
|
||||
static char g_launch_last_chain[MEOW_CHAIN_MAX];
|
||||
static int g_launch_ret = 0; /* 可注入:0=成功 / <0=启动失败 */
|
||||
static int g_resolve_calls = 0;
|
||||
static int g_resolve_rc = MEOW_OK; /* 可注入:MEOW_OK / MEOW_ERR_NOENT */
|
||||
static int g_log_calls = 0;
|
||||
|
||||
static int fake_launch(int idx, const char *chain)
|
||||
{
|
||||
g_launch_calls++;
|
||||
g_launch_last_idx = idx;
|
||||
if (chain) snprintf(g_launch_last_chain, sizeof g_launch_last_chain, "%s", chain);
|
||||
else g_launch_last_chain[0] = 0;
|
||||
return g_launch_ret;
|
||||
}
|
||||
|
||||
static int fake_resolve(const char *app_id, const char *activity,
|
||||
char *chain, int *depth)
|
||||
{
|
||||
(void)app_id;
|
||||
g_resolve_calls++;
|
||||
if (g_resolve_rc != MEOW_OK) return g_resolve_rc;
|
||||
snprintf(chain, MEOW_CHAIN_MAX, "%s", activity); /* 简化:chain=activity */
|
||||
if (depth) *depth = 1;
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
static void fake_log(const char *fmt, ...) { (void)fmt; g_log_calls++; }
|
||||
|
||||
static void reset_fakes(void)
|
||||
{
|
||||
g_launch_calls = 0; g_launch_last_idx = -1;
|
||||
g_launch_last_chain[0] = 0; g_launch_ret = 0;
|
||||
g_resolve_calls = 0; g_resolve_rc = MEOW_OK;
|
||||
g_log_calls = 0;
|
||||
meow_ams_set_busy(0);
|
||||
}
|
||||
|
||||
/* 应用表:id 主键串 + 用于 idx 断言 */
|
||||
static const char *g_ids[3] = { "dev.player.ncm", "os.meow.files", "os.meow.hello" };
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static struct meow_ams_ops ops;
|
||||
ops.launch = fake_launch;
|
||||
ops.resolve = fake_resolve;
|
||||
ops.log = fake_log;
|
||||
meow_ams_ops_init(&ops);
|
||||
|
||||
char toast[64];
|
||||
int app_ran = 0, idx = -1;
|
||||
|
||||
/* ---- 1) 合法裸启动(act 空)→ consume → ops.launch ---- */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set(bare): 受理");
|
||||
int r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 1 && g_launch_calls == 1, "consume(bare): 拉起链路被调");
|
||||
CHECK(g_launch_last_idx == 0, "consume(bare): idx 命中 dev.player.ncm");
|
||||
CHECK(g_launch_last_chain[0] == 0, "consume(bare): chain 空(裸启动)");
|
||||
CHECK(app_ran == 1, "consume(bare): app_ran=1");
|
||||
|
||||
/* ---- 2) activity 链 → resolve → consume → ops.launch(chain) ---- */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set("os.meow.files", "pick") == 0, "set(act): 受理");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 1 && g_resolve_calls == 1, "consume(act): resolve 被调");
|
||||
CHECK(g_launch_calls == 1 && g_launch_last_idx == 1, "consume(act): ops.launch(idx=1)");
|
||||
CHECK(g_launch_last_chain[0] && strcmp(g_launch_last_chain, "pick") == 0,
|
||||
"consume(act): chain 透传 pick");
|
||||
|
||||
/* ---- 3) 参数非法(pkg 空)→ INVAL,不置位、不拉起 ---- */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set("", "") == MEOW_ERR_INVAL, "set(空 pkg): 返回负错误码 MEOW_ERR_INVAL");
|
||||
CHECK(meow_ams_pending_start_set(NULL, NULL) == MEOW_ERR_INVAL, "set(NULL pkg): MEOW_ERR_INVAL");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 0 && g_launch_calls == 0, "consume: 空 pkg 未置位 → 不拉起");
|
||||
|
||||
/* ---- 4) busy(有前台 App)→ BUSY,不拉起 ---- */
|
||||
reset_fakes();
|
||||
meow_ams_set_busy(1);
|
||||
CHECK(meow_ams_pending_start_set("os.meow.hello", "") == MEOW_ERR_BUSY,
|
||||
"set(busy): 返回负错误码 MEOW_ERR_BUSY(根因修复:旧实现返回 +8 被误判 OK)");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 0 && g_launch_calls == 0, "consume(busy): 不拉起");
|
||||
meow_ams_set_busy(0);
|
||||
|
||||
/* ---- 5) 列表未就绪(state!=0)→ 保留 pending,后就绪拉起 ---- */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set: 受理");
|
||||
r = meow_ams_start_consume(g_ids, 3, 2, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 2 && g_launch_calls == 0, "consume(state!=0): 返回 2 保留 pending,不拉起");
|
||||
CHECK(toast[0] && strstr(toast, "not ready"), "consume(state!=0): toast=列表未就绪");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 1 && g_launch_calls == 1 && g_launch_last_idx == 0,
|
||||
"consume(后就绪): pending 保留后拉起成功");
|
||||
|
||||
/* ---- 6) pkg 不在应用表 → NO APP,不拉起 ---- */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set("no.such.app", "") == 0, "set(未知app): 受理");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 1 && g_launch_calls == 0, "consume(未知app): 不拉起");
|
||||
CHECK(toast[0] && strstr(toast, "NO APP"), "consume(未知app): toast=NO APP");
|
||||
|
||||
/* ---- 7) activity 无路由 → NO ROUTE,不拉起 ---- */
|
||||
reset_fakes();
|
||||
g_resolve_rc = MEOW_ERR_NOENT;
|
||||
CHECK(meow_ams_pending_start_set("os.meow.files", "nope") == 0, "set(nope): 受理");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 1 && g_launch_calls == 0, "consume(无路由): 不拉起");
|
||||
CHECK(toast[0] && strstr(toast, "NO ROUTE"), "consume(无路由): toast=NO ROUTE");
|
||||
g_resolve_rc = MEOW_OK;
|
||||
|
||||
/* ---- 8) 启动失败(ops.launch<0)→ app_ran=0,idx_out 命中 ---- */
|
||||
reset_fakes();
|
||||
g_launch_ret = -2; /* MEOW_ERR_NOENT */
|
||||
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set: 受理");
|
||||
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
|
||||
CHECK(r == 1 && g_launch_calls == 1, "consume(启动失败): 拉起被调(失败)");
|
||||
CHECK(app_ran == 0 && idx == 0, "consume(启动失败): app_ran=0 且 idx_out=0(供 launch_fail_toast)");
|
||||
g_launch_ret = 0;
|
||||
|
||||
/* ---- 9) picker 扩载荷:set_ex 武装 → take_picker 取回并清位 ---- */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set_ex("os.meow.files", "pick", "pick_file", 7, "dev.player.ncm") == 0,
|
||||
"set_ex(picker): 受理");
|
||||
char act[24] = "", caller[MEOW_APP_ID_MAX] = "";
|
||||
uint32_t req = 0;
|
||||
CHECK(meow_ams_take_picker(act, sizeof act, caller, sizeof caller, &req) == 1,
|
||||
"take_picker: 有上下文");
|
||||
CHECK(strcmp(act, "pick_file") == 0 && req == 7 && strcmp(caller, "dev.player.ncm") == 0,
|
||||
"take_picker: action/req/caller 正确");
|
||||
CHECK(meow_ams_take_picker(act, sizeof act, caller, sizeof caller, &req) == 0,
|
||||
"take_picker: 二次取回=0(一次性清位)");
|
||||
|
||||
/* 非 picker 启动:set(非 set_ex)不武装 picker */
|
||||
reset_fakes();
|
||||
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set(bare): 受理");
|
||||
CHECK(meow_ams_take_picker(act, sizeof act, caller, sizeof caller, &req) == 0,
|
||||
"take_picker: 普通启动无 picker 上下文");
|
||||
|
||||
printf("\n%s (%d fails)\n", g_fails ? "FAILED" : "ALL PASS", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/* test_bg_keep.c — 批①`background_keepalive` 声明层宿主单测。
|
||||
*
|
||||
* 验证 meow_parse_app_json 对 app.json 后台三位(background / background_audio /
|
||||
* background_keepalive)的解析与 meow_acct_sync 透传,重点覆盖**前缀不串扰**:
|
||||
* - "background_keepalive":true → a->background_keepalive=='1';且 background/background_audio
|
||||
* 在该行不被误命中(分别是 0/0);
|
||||
* - background:true → a->background=='1'(独立键匹配不受影响);
|
||||
* - background_audio:true → a->background_audio=='1';
|
||||
* - 一行三键并存:三者各归其位;
|
||||
* - meow_acct_sync 透传 background_keepalive 到注册表。
|
||||
* 实现手法同 test_fg_act.c:真实 include app_launcher.c,-ffunction-sections -Wl,--gc-sections
|
||||
* 只保留 parseInt/acct_sync/upsert/find 触及函数;json_str/meow_parse_depends/logmsg 由本文件提供
|
||||
* 忠实桩(json_str 复刻 util.c 语义,含键前缀独立 ident_ch + 整体 UTF-8 截断)。
|
||||
*
|
||||
* 运行(patch_v2/meow_launcherd 下):
|
||||
* gcc -D_GNU_SOURCE -Wall -Wextra -ffunction-sections -Wl,--gc-sections \
|
||||
* -I. -o test_bg_keep test_bg_keep.c && ./test_bg_keep
|
||||
* 全 PASS -> exit 0。
|
||||
*/
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ---- logmsg:app_launcher.c 依赖,minimal 桩 ---- */
|
||||
void logmsg(const char *fmt, ...)
|
||||
{
|
||||
(void)fmt;
|
||||
}
|
||||
|
||||
/* ---- json_str:复刻 util.c 语义(键前缀独立 + 值提取 + UTF-8 截断)---- */
|
||||
static int ident_ch(char c)
|
||||
{
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '.';
|
||||
}
|
||||
static void utf8_trim(char *s)
|
||||
{
|
||||
size_t n = strlen(s), i = 0, last_good = 0;
|
||||
while (i < n) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
int len = (c < 0x80) ? 1 : (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1;
|
||||
if (i + (size_t)len > n) break;
|
||||
i += (size_t)len;
|
||||
last_good = i;
|
||||
}
|
||||
s[last_good] = '\0';
|
||||
}
|
||||
int json_str(const char *buf, const char *key, char *out, int olen)
|
||||
{
|
||||
out[0] = 0;
|
||||
if (!buf || !key || olen <= 0) return 0;
|
||||
const char *p = strstr(buf, key);
|
||||
if (!p) return 0;
|
||||
if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */
|
||||
p += strlen(key);
|
||||
if (*p == '"') p++; /* JSON 键自身右引号 */
|
||||
while (*p && (*p == ':' || *p == '=')) p++;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p == '"') p++; /* 值左引号 */
|
||||
int i = 0;
|
||||
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1)
|
||||
out[i++] = *p++;
|
||||
out[i] = 0;
|
||||
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
|
||||
utf8_trim(out);
|
||||
return (int)strlen(out);
|
||||
}
|
||||
|
||||
#include "app_launcher.c" /* 真实实现:meow_parse_app_json / meow_acct_sync / meow_acct_upsert */
|
||||
|
||||
/* meow_parse_depends:本测试 app.json 无 depends 行,桩返回 0(struct meow_dep 此时已可见)。 */
|
||||
int meow_parse_depends(const char *val, struct meow_dep *deps, int max)
|
||||
{
|
||||
(void)val; (void)deps; (void)max;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \
|
||||
else fprintf(stderr, "ok: %s\n", msg); \
|
||||
} while (0)
|
||||
|
||||
/* 用一段 app.json 内容解析出 struct app。返回 0=成功。 */
|
||||
static int parse_json(const char *content, struct app *a)
|
||||
{
|
||||
FILE *f = tmpfile();
|
||||
if (!f) return -1;
|
||||
fputs(content, f);
|
||||
rewind(f);
|
||||
memset(a, 0, sizeof *a);
|
||||
meow_parse_app_json(f, a);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* ---- 1) 单独 background_keepalive:true ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_keepalive\":true\n}", &a);
|
||||
CHECK(a.background_keepalive == '1', "background_keepalive:true -> '1'");
|
||||
CHECK(a.background == 0, "keepalive 行不误命中 background (==0)");
|
||||
CHECK(a.background_audio == 0, "keepalive 行不误命中 background_audio (==0)");
|
||||
}
|
||||
/* ---- 2) 单独 background:true ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background\":true\n}", &a);
|
||||
CHECK(a.background == '1', "background:true -> '1'(独立键匹配不受影响)");
|
||||
CHECK(a.background_keepalive == 0, "background 行不串 to keepalive (==0)");
|
||||
CHECK(a.background_audio == 0, "background 行不串 to background_audio (==0)");
|
||||
}
|
||||
/* ---- 3) 单独 background_audio:true ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_audio\":true\n}", &a);
|
||||
CHECK(a.background_audio == '1', "background_audio:true -> '1'");
|
||||
CHECK(a.background == 0, "background_audio 行不误命中 background (==0)");
|
||||
CHECK(a.background_keepalive == 0, "background_audio 行不串 to keepalive (==0)");
|
||||
}
|
||||
/* ---- 4) 三键并存:各归其位(逐行格式,值后跟 , 或换行,同真实 app.json) ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background\":true,\n"
|
||||
"\"background_audio\":true,\n\"background_keepalive\":true\n}", &a);
|
||||
CHECK(a.background == '1', "三键并存 background=='1'");
|
||||
CHECK(a.background_audio == '1', "三键并存 background_audio=='1'");
|
||||
CHECK(a.background_keepalive == '1', "三键并存 background_keepalive=='1'");
|
||||
}
|
||||
/* ---- 5) 假值 / 前导噪声不应置位 ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_keepalive\":false,\n"
|
||||
"\"background_audio\":0\n}", &a);
|
||||
CHECK(a.background_keepalive == 0, "background_keepalive:false -> 0");
|
||||
CHECK(a.background_audio == 0, "background_audio:0 -> 0");
|
||||
CHECK(a.background == 0, "无 background 键 -> 0");
|
||||
}
|
||||
/* ---- 6) meow_acct_sync 透传 ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n"
|
||||
"\"background_keepalive\":true,\n\"background_audio\":true\n}", &a);
|
||||
meow_acct_sync("dev.test.keep", &a); /* 以 app_id 建/更注册表项 */
|
||||
struct meow_acct *ac = meow_acct_find("dev.test.keep");
|
||||
CHECK(ac != NULL, "meow_acct_sync 建注册表项");
|
||||
CHECK(ac && ac->background_keepalive == '1', "透传 background_keepalive=='1'");
|
||||
CHECK(ac && ac->background_audio == '1', "透传 background_audio=='1'");
|
||||
CHECK(ac && ac->background == 0, "透传 background==0");
|
||||
}
|
||||
|
||||
if (g_fail) { fprintf(stderr, "\n== %d assertion(s) FAILED ==\n", g_fail); return 1; }
|
||||
fprintf(stderr, "\n== test_bg_keep ALL PASS ==\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/* meow_launcherd/test_bg_row_card.c — 宿主验证:后台管理页(bg_page)MD3 圆角卡行 + 行内杀按钮
|
||||
* 的几何与命中(纯整数运算,不链 SDK / 不开设备):
|
||||
* gcc -Wall -Wextra test_bg_row_card.c -o test_bg_row_card && ./test_bg_row_card
|
||||
*
|
||||
* 断言集(对应「相邻 item 顶之间 = item 高 + gap」的可见形式):
|
||||
* ① 卡高 = 行高 - BGM_CARD_GAP;卡在其行高内上下对称内缩 gap/2(行中心=卡中心);
|
||||
* ② 相邻卡顶间距 = SUB_ITEM_H(= 卡高 + gap;SDK 行距不变),卡片间可见外距 = gap;
|
||||
* ③ 杀按钮垂直居中于卡(中心=卡中心=行中心),完整落卡内,右缘距行右 12px;
|
||||
* ④ 命中不破:卡内缩后按钮几何不变(相对整行视口)——按钮区内点按命中(1),
|
||||
* 行体/卡内空白点按不命中(0);命中矩形仍为整行距;
|
||||
* ⑤ 滚动偏移只整体平移,不改变上述间距/对称/命中。
|
||||
*
|
||||
* 几何公式与 patch_v2/meow_launcherd/bg_page.c 的 bgm_row_card_y/bgm_row_card_h/bgm_btn_rect 同源。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <assert.h>
|
||||
|
||||
#define SUB_ITEM_H 56 /* 行距(SDK listview item_h,subpage.c,不变) */
|
||||
#define BGM_CARD_GAP 8
|
||||
#define BGM_BTN_W 46
|
||||
#define BGM_BTN_H 34
|
||||
|
||||
/* 与 bg_page.c bgm_row_card_y / bgm_row_card_h 同源(上下对称内缩)。 */
|
||||
static int bgm_row_card_y(int row_top) { return row_top + BGM_CARD_GAP / 2; }
|
||||
static int bgm_row_card_h(int h) { return h - BGM_CARD_GAP; }
|
||||
|
||||
/* 与 bg_page.c bgm_btn_rect 同源(按钮垂直居中于卡;卡中心=行中心 → 命中不破)。 */
|
||||
static void bgm_btn_rect(int rx, int ry, int rw, int rh, int *bx, int *by, int *bw, int *bh)
|
||||
{
|
||||
*bw = BGM_BTN_W;
|
||||
*bh = BGM_BTN_H;
|
||||
*bx = rx + rw - 12 - BGM_BTN_W;
|
||||
*by = bgm_row_card_y(ry) + (bgm_row_card_h(rh) - BGM_BTN_H) / 2;
|
||||
}
|
||||
|
||||
/* 行顶 = list_y + idx*item - off(SDK listview 布局:行距=item)。 */
|
||||
static int row_top(int idx, int item, int list_y, int off) { return list_y + idx * item - off; }
|
||||
|
||||
/* 复刻 bgm_row_tap 的按钮命中判定(仅几何):命中按钮区=1,行体/卡内空白=0。 */
|
||||
static int btn_hit(int tx, int ty, int rx, int ry, int rw, int rh)
|
||||
{
|
||||
int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh);
|
||||
if (tx < bx || tx >= bx + bw || ty < by || ty >= by + bh) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const int item = SUB_ITEM_H;
|
||||
const int gap = BGM_CARD_GAP;
|
||||
const int list_y = 100; /* 内容区顶缘(SUB_CONT_TOP 等值,任意值皆可) */
|
||||
const int rw = 480; /* 行视口宽(fb_xres,480×720) */
|
||||
const int rx = 0; /* 行视口左缘 */
|
||||
|
||||
int y0 = bgm_row_card_y(row_top(0, item, list_y, 0));
|
||||
int y1 = bgm_row_card_y(row_top(1, item, list_y, 0));
|
||||
int h = bgm_row_card_h(item);
|
||||
|
||||
/* ① 卡相对行顶对称内缩 gap/2;卡高=item-gap;卡高+gap=item */
|
||||
assert(y0 == list_y + gap / 2);
|
||||
assert(h == item - gap);
|
||||
assert(h + gap == item);
|
||||
|
||||
/* ① 相邻卡顶间距 = item(顶到顶 = 卡高 + gap) */
|
||||
assert(y1 - y0 == item);
|
||||
|
||||
/* ② 卡片间可见外距 = gap(卡 0 底 到 卡 1 顶) */
|
||||
assert(y1 - (y0 + h) == gap);
|
||||
|
||||
/* ③ 卡中心 == 行中心(内容按行高居中 → 落卡中心) */
|
||||
assert(y0 - gap / 2 + item / 2 == y0 + h / 2);
|
||||
/* 下行底到卡底 = gap/2(下对称) */
|
||||
assert((list_y + item) - (y0 + h) == gap / 2);
|
||||
|
||||
/* ③④ 杀按钮:相对整行视口定位,垂直居中于卡(中心=卡中心=行中心),完整落卡内,
|
||||
* 右缘距行右 12px;滚动偏移只平移,不改变以上(③④⑤合并循环)。 */
|
||||
for (int off = 0; off <= item; off += item / 2) {
|
||||
int r0 = row_top(0, item, list_y, off);
|
||||
int bx, by, bw, bh; bgm_btn_rect(rx, r0, rw, item, &bx, &by, &bw, &bh);
|
||||
int card_top = bgm_row_card_y(r0);
|
||||
int card_bot = card_top + bgm_row_card_h(item);
|
||||
int row_cy = r0 + item / 2;
|
||||
int card_cy = card_top + bgm_row_card_h(item) / 2;
|
||||
|
||||
/* 按钮完整落卡内(含圆角内边距;上下由卡限,左右由行限) */
|
||||
assert(bx >= rx && bx + bw <= rx + rw);
|
||||
assert(by >= card_top && by + bh <= card_bot);
|
||||
/* 按钮中心 = 卡中心 = 行中心(垂直) */
|
||||
assert(by + bh / 2 == card_cy);
|
||||
assert(by + bh / 2 == row_cy);
|
||||
/* 按钮右缘距行右 12px(与绘制一致) */
|
||||
assert(rx + rw - (bx + bw) == 12);
|
||||
|
||||
/* ④ 命中:按钮中心 → 命中(1);卡内空白(左上圆角内) / 行体左缘 → 不命中(0) */
|
||||
assert(btn_hit(bx + bw / 2, by + bh / 2, rx, r0, rw, item) == 1);
|
||||
assert(btn_hit(card_top + 2, card_top + 2, rx, r0, rw, item) == 0);
|
||||
assert(btn_hit(rx + 16, row_cy, rx, r0, rw, item) == 0);
|
||||
/* 卡上/下外侧(gap 带内)点按 → 不命中(命中仍按整行距,含 gap 带) */
|
||||
assert(btn_hit(rx + rw / 2, r0 + gap / 2 - 1, rx, r0, rw, item) == 0);
|
||||
}
|
||||
|
||||
printf("PASS bg_row_card_gap=%d card_h=%d top2top=%d visible_gap=%d "
|
||||
"btn_in_card=1 btn_center==row_center=1 hit_unbroken=1\n",
|
||||
gap, h, item, gap);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/* test_bgmgr.c — 批③ 后台管理页 逻辑层宿主单测(meow_bgmgr)。
|
||||
*
|
||||
* 验证:
|
||||
* 1) bgmgr_collect:enumerate 后台表 → app_meta 补 name/icon/protected_app/is_fg → proc_sample 补
|
||||
* pcpu/rss_kb;行 killable 预判正确;name 回退 app_id。
|
||||
* 2) bgmgr_can_kill 门控矩阵:protected_app=1 → 0(**受保护 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
|
||||
* pid<=0 → 0(非后台/无进程);普通后台(非受保护/非前台/pid>0)→ 1。
|
||||
* 3) bgmgr_kill_all 一键清:受保护 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
|
||||
* 3b) 回归:app.json framework=1(SDK 框架 App 标志,现有 App 全写 1)**不影响**可杀性。
|
||||
* 4) bgmgr_status_str / bgmgr_mem_str 文案。
|
||||
* 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编:
|
||||
* gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include "meow_bgmgr.h"
|
||||
|
||||
static int g_fails = 0;
|
||||
static int g_checks = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
g_checks++; \
|
||||
if (cond) printf("PASS: %s\n", msg); \
|
||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||
|
||||
/* ---- 假件(enumerate/app_meta/proc_sample 注入)---- */
|
||||
static const char g_meta_app[][24] = {
|
||||
"os.meow.hello", "os.meow.reader", "os.meow.sysguard", "os.meow.fgapp",
|
||||
};
|
||||
static int g_killed_n = 0;
|
||||
static char g_killed_app[8][40];
|
||||
static int g_meta_use_unknown = 0; /* 第 4 行 meta 返回 -1(未注册),测 name 回退 */
|
||||
|
||||
static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||||
{
|
||||
(void)ctx;
|
||||
if (max < 4) return 0;
|
||||
/* 4 个后台行:hello=普通、reader=常驻、sysguard=受保护、fgapp=当前前台 */
|
||||
memset(&rows[0], 0, sizeof rows[0]);
|
||||
snprintf(rows[0].app_id, sizeof rows[0].app_id, "%s", g_meta_app[0]);
|
||||
rows[0].pid = 1001; rows[0].keepalive = 0; rows[0].on_card = 0; rows[0].bg_since_ms = 1000;
|
||||
|
||||
memset(&rows[1], 0, sizeof rows[1]);
|
||||
snprintf(rows[1].app_id, sizeof rows[1].app_id, "%s", g_meta_app[1]);
|
||||
rows[1].pid = 1002; rows[1].keepalive = 1; rows[1].on_card = 0; rows[1].bg_since_ms = 2000;
|
||||
|
||||
memset(&rows[2], 0, sizeof rows[2]);
|
||||
snprintf(rows[2].app_id, sizeof rows[2].app_id, "%s", g_meta_app[2]);
|
||||
rows[2].pid = 1003; rows[2].keepalive = 0; rows[2].on_card = 0; rows[2].bg_since_ms = 3000;
|
||||
|
||||
memset(&rows[3], 0, sizeof rows[3]);
|
||||
snprintf(rows[3].app_id, sizeof rows[3].app_id, "%s", g_meta_app[3]);
|
||||
rows[3].pid = 1004; rows[3].keepalive = 0; rows[3].on_card = 0; rows[3].bg_since_ms = 4000;
|
||||
return 4;
|
||||
}
|
||||
|
||||
/* meta:hello/reader/sysguard/fgapp 各给名字;sysguard 标受保护;pid==1004 标前台。 */
|
||||
static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
|
||||
char *name, int nl, char *icon, int il,
|
||||
char *protected_app, int *is_fg)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放");
|
||||
else if (!strcmp(app_id, "os.meow.reader")) snprintf(name, nl, "阅读");
|
||||
else if (!strcmp(app_id, "os.meow.sysguard")) snprintf(name, nl, "系统守护");
|
||||
else if (!strcmp(app_id, "os.meow.fgapp")) snprintf(name, nl, "前台应用");
|
||||
else return -1; /* 未注册 → name 回退 app_id */
|
||||
snprintf(icon, il, "/tmp/icon_%s.png", app_id);
|
||||
*protected_app = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0;
|
||||
*is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fake_proc_sample(void *ctx, pid_t pid, double *pcpu, long *rss_kb)
|
||||
{
|
||||
(void)ctx;
|
||||
if (pid <= 0) return -1;
|
||||
*pcpu = (double)(pid % 100) * 0.5; /* 1001->0.5% ... 1004->2.0% */
|
||||
*rss_kb = (long)(pid % 10) * 2048; /* 1001->2048 ... 1004->8192 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void fake_log(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
static void fake_kill(const char *app_id)
|
||||
{
|
||||
if (g_killed_n < 8) snprintf(g_killed_app[g_killed_n++], 40, "%s", app_id);
|
||||
}
|
||||
|
||||
/* enumerate 只回一行:未注册 app_id,测 name 回退。 */
|
||||
static int fake_enumerate_unknown(void *ctx, bgmgr_row_t *rows, int max)
|
||||
{
|
||||
(void)ctx;
|
||||
if (max < 1) return 0;
|
||||
(void)g_meta_use_unknown;
|
||||
memset(&rows[0], 0, sizeof rows[0]);
|
||||
snprintf(rows[0].app_id, sizeof rows[0].app_id, "os.meow.noreg");
|
||||
rows[0].pid = 2001; rows[0].keepalive = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
bgmgr_ops_t ops;
|
||||
memset(&ops, 0, sizeof ops);
|
||||
ops.enumerate = fake_enumerate;
|
||||
ops.app_meta = fake_app_meta;
|
||||
ops.proc_sample = fake_proc_sample;
|
||||
ops.log = fake_log;
|
||||
|
||||
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
|
||||
int n = bgmgr_collect(&ops, rows, MEOW_BGMR_ROW_MAX);
|
||||
|
||||
printf("=== 1) collect ===\n");
|
||||
CHECK(n == 4, "collect returns 4 rows");
|
||||
CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid");
|
||||
CHECK(rows[0].protected_app == 0, "row0 hello not protected");
|
||||
CHECK(!strcmp(rows[1].name, "阅读") && rows[1].keepalive == 1, "row1 reader keepalive");
|
||||
CHECK(rows[2].protected_app == '1', "row2 sysguard protected");
|
||||
CHECK(rows[3].is_fg == 1, "row3 fgapp is_fg");
|
||||
CHECK(rows[0].pcpu > 0 && rows[0].rss_kb > 0, "row0 pcpu/rss populated");
|
||||
CHECK(rows[0].killable == 1 && rows[2].killable == 0, "killable prefilled by can_kill");
|
||||
CHECK(!strcmp(rows[2].icon, "/tmp/icon_os.meow.sysguard.png"), "icon populated");
|
||||
|
||||
/* name 回退:未注册 app_id(app_meta 返回 -1)→ name==app_id */
|
||||
printf("=== 1b) name fallback ===\n");
|
||||
{
|
||||
bgmgr_ops_t o2 = ops; o2.enumerate = fake_enumerate_unknown;
|
||||
bgmgr_row_t r1[1]; memset(r1, 0, sizeof r1);
|
||||
int n2 = bgmgr_collect(&o2, r1, 1);
|
||||
CHECK(n2 == 1, "unknown row collected");
|
||||
CHECK(!strcmp(r1[0].name, "os.meow.noreg"), "name falls back to app_id");
|
||||
CHECK(bgmgr_can_kill(&r1[0]) == 1, "unknown bg app still killable");
|
||||
}
|
||||
|
||||
printf("=== 2) can_kill gating ===\n");
|
||||
CHECK(bgmgr_can_kill(&rows[2]) == 0, "protected app NOT killable");
|
||||
CHECK(bgmgr_can_kill(&rows[3]) == 0, "fg app NOT killable (is_fg)");
|
||||
CHECK(bgmgr_can_kill(&rows[0]) == 1, "normal bg app killable");
|
||||
CHECK(bgmgr_can_kill(&rows[1]) == 1, "keepalive bg app killable");
|
||||
{
|
||||
bgmgr_row_t dead; memset(&dead, 0, sizeof dead);
|
||||
snprintf(dead.app_id, sizeof dead.app_id, "x");
|
||||
dead.pid = 0;
|
||||
CHECK(bgmgr_can_kill(&dead) == 0, "pid<=0 NOT killable");
|
||||
CHECK(bgmgr_can_kill(NULL) == 0, "NULL NOT killable");
|
||||
}
|
||||
|
||||
printf("=== 3) kill_all (one-key clear) ===\n");
|
||||
g_killed_n = 0;
|
||||
int k = bgmgr_kill_all(&ops, rows, n, fake_kill);
|
||||
CHECK(k == 2, "kill_all kills 2 (hello+reader)");
|
||||
CHECK(g_killed_n == 2, "kill_all actually invoked kill_fn 2x");
|
||||
int killed_fw = 0, killed_fg = 0, killed_bg = 0;
|
||||
for (int i = 0; i < g_killed_n; i++) {
|
||||
if (!strcmp(g_killed_app[i], "os.meow.sysguard")) killed_fw = 1;
|
||||
if (!strcmp(g_killed_app[i], "os.meow.fgapp")) killed_fg = 1;
|
||||
if (!strcmp(g_killed_app[i], "os.meow.hello")) killed_bg = 1;
|
||||
}
|
||||
CHECK(killed_fw == 0, "kill_all did NOT kill protected app");
|
||||
CHECK(killed_fg == 0, "kill_all did NOT kill fg app");
|
||||
CHECK(killed_bg == 1, "kill_all killed a real bg app");
|
||||
CHECK(bgmgr_kill_all(&ops, rows, n, NULL) == 0, "kill_all NULL kill_fn -> 0");
|
||||
|
||||
/* 回归(2026-11-04 真机):app.json framework=1 是「SDK 框架 App」标志(现有 App 全写 1),
|
||||
* **不是**受保护位。旧实现把它当门控 → dev.player.ncm 等所有后台 App 都不可杀、按钮置灰。 */
|
||||
printf("=== 3b) framework(SDK) ≠ 受保护 ===\n");
|
||||
{
|
||||
bgmgr_row_t sdk; memset(&sdk, 0, sizeof sdk);
|
||||
snprintf(sdk.app_id, sizeof sdk.app_id, "dev.player.ncm");
|
||||
sdk.pid = 7520; sdk.protected_app = 0;
|
||||
CHECK(bgmgr_can_kill(&sdk) == 1, "SDK 框架 App(framework=1)后台仍可杀");
|
||||
}
|
||||
|
||||
printf("=== 4) status/mem str ===\n");
|
||||
CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻");
|
||||
CHECK(!strcmp(bgmgr_status_str(&rows[0]), "后台"), "status_str normal=后台");
|
||||
CHECK(!strcmp(bgmgr_status_str(&rows[3]), "前台"), "status_str is_fg=前台");
|
||||
CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB");
|
||||
CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB");
|
||||
|
||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
|
||||
else printf("\n%d FAILED\n", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。
|
||||
*
|
||||
* 通过 #include bg_page.c 把其 FILE-STATIC 的 bgm_rows/bgm_btn_rect/bgm_row_tap/bgm_top_action
|
||||
* 平移进本 TU;桩掉 launcher 后台表(meow_bg_count/at/terminate)+ 时钟 + meow_str/logmsg,
|
||||
* 直接验证:
|
||||
* P1 bgm_rows 行骨架:后台 App 行(act=i)+ 空态行(act=-1);name 经 app_meta 正确;
|
||||
* **行副文本指向 ctx 持久缓冲且扛得住栈抖动**(曾指向 bgm_rows 栈帧 → 真机副标题乱码);
|
||||
* P2 bgm_btn_rect 按钮几何:行右 46×34 圆角钮(rx+rw-12-46 定位);
|
||||
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;
|
||||
* **app.json framework=1(SDK 框架 App)照样可杀**(2026-11-04 真机回归);
|
||||
* 受保护/前台行按钮点击 → 不 terminate(门控,留诊断日志);空态行不响应;
|
||||
* P4 bgm_top_action 顶栏清除:kill_all 不杀受保护/前台、杀普通后台(含 framework=1 的);
|
||||
* P5 门控矩阵:bgmgr_can_kill(protected_app/is_fg/pid<=0/普通)。
|
||||
* 不调用行自绘 bgm_row_draw(纯逻辑),故无需桩 SDK 绘制;行自绘段经 -ffunction-sections/--gc-sections 剔除。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_launcherd 下):
|
||||
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined \
|
||||
* -ffunction-sections -fdata-sections test_bgpage.c meow_bgmgr.c \
|
||||
* -Wl,--gc-sections -o test_bgpage
|
||||
* 运行:./test_bgpage
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "bg_page.c" /* FILE-STATIC 函数平移进本 TU */
|
||||
|
||||
/* ---------- 桩:launcher 后台表(meow_bgplay 视角) ---------- */
|
||||
static meow_bg_entry_t g_bg[MEOW_BG_MAX];
|
||||
static int g_bg_n;
|
||||
static int g_term_calls;
|
||||
static char g_terminated[16][64];
|
||||
|
||||
int meow_bg_count(void) { return g_bg_n; }
|
||||
const meow_bg_entry_t *meow_bg_at(int i)
|
||||
{
|
||||
if (i < 0 || i >= g_bg_n) return NULL;
|
||||
return &g_bg[i];
|
||||
}
|
||||
int meow_bg_terminate(const char *app_id)
|
||||
{
|
||||
if (g_term_calls < 16) snprintf(g_terminated[g_term_calls++], 64, "%s", app_id);
|
||||
return 0;
|
||||
}
|
||||
unsigned long long now_ms(void) { return 0; }
|
||||
const char *meow_str(const char *id) { (void)id; return NULL; } /* 未知 id → meow_str_zh 回退 zh */
|
||||
static int g_log_n;
|
||||
void logmsg(const char *fmt, ...) { (void)fmt; g_log_n++; } /* 只计数:门控/未命中应留诊断日志 */
|
||||
|
||||
/* 栈抖动:bgm_rows 返回后调用,用来暴露「sub 指向栈帧」的悬垂指针。 */
|
||||
static void stack_clobber(void)
|
||||
{
|
||||
volatile char buf[256];
|
||||
memset((void *)buf, 'X', sizeof buf);
|
||||
buf[0] = buf[0]; /* 防优化掉 */
|
||||
}
|
||||
|
||||
/* ---------- 测试态:launcher 应用表(app_meta 用) ---------- */
|
||||
static struct app g_apps[4];
|
||||
|
||||
static void set_app(int i, const char *id, const char *name, char fw)
|
||||
{
|
||||
memset(&g_apps[i], 0, sizeof g_apps[i]);
|
||||
snprintf(g_apps[i].id, sizeof g_apps[i].id, "%s", id);
|
||||
snprintf(g_apps[i].name, sizeof g_apps[i].name, "%s", name);
|
||||
g_apps[i].framework = fw;
|
||||
}
|
||||
|
||||
static void bg_reset(int n, const char *id0, pid_t pid0, const char *id1, pid_t pid1)
|
||||
{
|
||||
g_bg_n = n; g_term_calls = 0;
|
||||
memset(g_bg, 0, sizeof g_bg);
|
||||
if (n >= 1 && id0) {
|
||||
g_bg[0].used = 1; snprintf(g_bg[0].app_id, sizeof g_bg[0].app_id, "%s", id0);
|
||||
g_bg[0].pid = pid0; g_bg[0].keepalive = 0;
|
||||
}
|
||||
if (n >= 2 && id1) {
|
||||
g_bg[1].used = 1; snprintf(g_bg[1].app_id, sizeof g_bg[1].app_id, "%s", id1);
|
||||
g_bg[1].pid = pid1; g_bg[1].keepalive = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int g_fails = 0;
|
||||
static int g_checks = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
g_checks++; \
|
||||
if (cond) printf("PASS: %s\n", msg); \
|
||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||
|
||||
static int term_count_app(const char *app_id)
|
||||
{
|
||||
int c = 0;
|
||||
for (int i = 0; i < g_term_calls; i++) if (!strcmp(g_terminated[i], app_id)) c++;
|
||||
return c;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static bgm_ctx_t b;
|
||||
memset(&b, 0, sizeof b);
|
||||
b.apps = g_apps; b.napps = 3;
|
||||
set_app(0, "os.meow.hello", "播放", 0);
|
||||
set_app(1, "dev.player.ncm", "流媒体客户端", '1'); /* SDK 框架 App(真机 app.json framework:1) */
|
||||
set_app(2, "os.meow.sysguard", "系统守护", 0);
|
||||
static sub_row_t rows[SUB_ROWS_MAX];
|
||||
|
||||
printf("=== P1) bgm_rows 行骨架 + 副文本持久化 ===\n");
|
||||
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||
int rn = bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)");
|
||||
CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0");
|
||||
CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)");
|
||||
CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1");
|
||||
CHECK(!strcmp(rows[1].label, "流媒体客户端"), "row1 label=流媒体客户端");
|
||||
CHECK(rows[1].sub != NULL && !strncmp(rows[1].sub, "CPU ", 4), "row1 sub=CPU/内存/状态");
|
||||
CHECK(b.n == 2, "bgm_ctx.n=2");
|
||||
/* 悬垂指针回归:sub 必须落在 ctx 缓冲里,且栈被改写后内容不变 */
|
||||
uintptr_t subp = (uintptr_t)rows[1].sub;
|
||||
uintptr_t lob = (uintptr_t)(char *)&b;
|
||||
uintptr_t hib = lob + sizeof b;
|
||||
CHECK(subp >= lob && subp < hib, "sub 指向 ctx 持久缓冲(不是 bgm_rows 栈帧)");
|
||||
char sub_before[80];
|
||||
snprintf(sub_before, sizeof sub_before, "%s", rows[1].sub);
|
||||
stack_clobber();
|
||||
CHECK(!strcmp(rows[1].sub, sub_before), "栈抖动后 sub 内容不变(无悬垂)");
|
||||
|
||||
printf("=== P2) bgm_btn_rect 按钮几何(行右 46×34) ===\n");
|
||||
{
|
||||
int bx, by, bw, bh; bgm_btn_rect(0, 0, 480, 56, &bx, &by, &bw, &bh);
|
||||
CHECK(bw == 46 && bh == 34, "按钮 46×34");
|
||||
CHECK(bx == 480 - 12 - 46, "按钮左缘=行宽-12-46");
|
||||
CHECK(by == (56 - 34) / 2, "按钮垂直居中");
|
||||
}
|
||||
|
||||
printf("=== P3) bgm_row_tap 行内杀按钮命中(仅按钮区 kill,行区不误杀) ===\n");
|
||||
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
/* 按钮区命中:行(0,0,480,56),按钮 x 422-468, y 11-45 */
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 1, "按钮点击→返回1(重绘)");
|
||||
CHECK(term_count_app("os.meow.hello") == 1, "按钮点击→terminate hello 一次");
|
||||
/* 行体(按钮左/其下区)命中 → 不 kill */
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 200, 28, 0, 0, 480, 56) == 0, "行体点击→返回0(不杀)");
|
||||
CHECK(term_count_app("os.meow.hello") == 1, "行体点击→不误杀(仍1次)");
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 410, 28, 0, 0, 480, 56) == 0, "按钮左侧缘(410)→不杀");
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀");
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀");
|
||||
/* 回归(2026-11-04 真机「杀后台按钮按下去没反应」):dev.player.ncm 的 app.json 写 framework:1
|
||||
* (= 用 SDK 框架,现有 App 全写 1),**必须照杀**——旧实现把它当受保护位,按钮置灰点不动。 */
|
||||
CHECK(b.rows[1].killable == 1, "framework=1(SDK 框架)App 仍判可杀");
|
||||
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "framework=1 App 按钮→返回1");
|
||||
CHECK(term_count_app("dev.player.ncm") == 1, "framework=1 App 按钮→真 terminate(无反应 bug 回归)");
|
||||
/* 真·受保护行(未来系统组件/前台):按钮命中但不 terminate,且留诊断日志 */
|
||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
b.rows[1].protected_app = '1';
|
||||
g_log_n = 0;
|
||||
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "受保护行按钮→返回1(重绘)");
|
||||
CHECK(term_count_app("os.meow.sysguard") == 0, "受保护行按钮→不 terminate");
|
||||
CHECK(g_log_n >= 1, "受保护行按钮→留一行诊断日志(可查「点了没反应」)");
|
||||
|
||||
printf("=== P3b) 空态行不响应 ===\n");
|
||||
bg_reset(0, NULL, 0, NULL, 0);
|
||||
int rn0 = bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
CHECK(rn0 == 1 && rows[0].act == -1, "0 后台 App → 1 空态行(act=-1)");
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)");
|
||||
g_term_calls = 0;
|
||||
|
||||
printf("=== P4) 顶栏清除(一键清除后台)不杀受保护/前台 ===\n");
|
||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
b.rows[1].protected_app = '1';
|
||||
CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)");
|
||||
CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello");
|
||||
CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀受保护 sysguard");
|
||||
/* 回归:framework=1 的 SDK 框架 App 也在「一键清」范围内 */
|
||||
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
g_term_calls = 0;
|
||||
CHECK(bgm_top_action(&b) == 1, "framework=1 App:一键清返回 1");
|
||||
CHECK(term_count_app("dev.player.ncm") == 1, "一键清→terminate framework=1 App(回归)");
|
||||
/* 前台 App(is_fg)不杀:给 hello 标前台再清 */
|
||||
bg_reset(1, "os.meow.hello", 1001, NULL, 0);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
for (int i = 0; i < b.n; i++) if (!strcmp(b.rows[i].app_id, "os.meow.hello")) b.rows[i].is_fg = 1;
|
||||
g_term_calls = 0;
|
||||
CHECK(bgm_top_action(&b) == 1, "前台 App 仍在:顶栏清除返回 1(重绘)");
|
||||
CHECK(g_term_calls == 0, "前台 App → kill_all 不杀(0 terminate)");
|
||||
CHECK(bgm_top_action(&b) == 1, "二次清除(仍前台/不可杀):返回 1");
|
||||
CHECK(g_term_calls == 0, "二次清除 → 仍不 terminate");
|
||||
|
||||
printf("=== P5) 门控矩阵(bgmgr_can_kill) ===\n");
|
||||
{
|
||||
bgmgr_row_t r; memset(&r, 0, sizeof r);
|
||||
r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀");
|
||||
r.protected_app = '1'; CHECK(bgmgr_can_kill(&r) == 0, "protected_app 不可杀");
|
||||
r.protected_app = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀");
|
||||
r.is_fg = 0; r.pid = 0; CHECK(bgmgr_can_kill(&r) == 0, "pid<=0 不可杀");
|
||||
}
|
||||
|
||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
|
||||
else printf("\n%d FAILED\n", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
/* test_bgplay.c — 宿主单测:meow_bgplay 后台播放保活 + 无头后台服务状态机(M4)。
|
||||
*
|
||||
* 验证:
|
||||
* 1) meow_bg_should_keep 判定矩阵(background_audio × media_playing);
|
||||
* 2) 后台播放保活全链路(真 fork 子进程):META PLAYING → should_keep=1 → bg_add →
|
||||
* 进程存活 + PLAYCTL 经会话 fd 往返(后台化不断连)→ take 恢复 → META STOPPED →
|
||||
* should_keep=0(停播后再退出=真退);
|
||||
* 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽;
|
||||
* 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS);
|
||||
* 4b) meow_bg_terminate(后台管理页「杀」按钮 / `am kill` 共用通道):受理置 term_at_ms →
|
||||
* tick SIGTERM 回收 + on_bg_dead;不在表/空 id 分别 NOENT/INVAL;
|
||||
* 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
|
||||
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
|
||||
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
|
||||
* (不回收声明常驻);pid<=0 → MEOW_ERR_INVAL;重复 add → MEOW_ERR_EXISTS;
|
||||
* 6) 服务注册:exec 不可执行 → NOENT;同名在跑 → EXISTS;
|
||||
* 7) 退避重启 1s/2s + max_restart=2 熔断(假时钟注入):清通知回调 + 注册连接收到
|
||||
* MSG_SYS{SVC_DEAD=2} + circuit 后不再拉起;重注册复位熔断重新拉起;
|
||||
* 8) MSG_SERVICE_STOP:SIGTERM + stopping,不触发重启,槽释放;
|
||||
* 9) kill_all:bg+svc 全回收 + 注册连接收到 MSG_SYS{SHUTDOWN=1}。
|
||||
* 只依赖 meow_bgplay.c + meow_media.c + meow_ipc.h(纯 C),宿主 gcc 直编:
|
||||
* gcc -Wall -Wextra -std=gnu99 test_bgplay.c meow_bgplay.c meow_media.c -o test_bgplay
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include "meow_bgplay.h"
|
||||
#include "meow_media.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
static int g_fails = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) printf("PASS: %s\n", msg); \
|
||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||
|
||||
/* ---- 假件(ops 注入)---- */
|
||||
static uint64_t g_ms = 100000; /* 假时钟(退避断言用,手动推进) */
|
||||
static int g_card = 1; /* 卡在位 */
|
||||
static int g_bg_dead = 0; /* on_bg_dead 计数 */
|
||||
static int g_bg_dead_status = 0;
|
||||
static char g_bg_dead_app[MEOW_APP_ID_MAX];
|
||||
static int g_notif_cleared = 0; /* notif_clear_app 计数 */
|
||||
static char g_notif_cleared_app[MEOW_APP_ID_MAX];
|
||||
|
||||
static uint64_t fake_now_ms(void) { return g_ms; }
|
||||
static int fake_card(void) { return g_card; }
|
||||
static void fake_bg_dead(const char *app_id, pid_t pid, int status)
|
||||
{
|
||||
(void)pid;
|
||||
g_bg_dead++;
|
||||
g_bg_dead_status = status;
|
||||
snprintf(g_bg_dead_app, sizeof g_bg_dead_app, "%s", app_id);
|
||||
}
|
||||
static int fake_notif_clear(const char *app_id)
|
||||
{
|
||||
g_notif_cleared++;
|
||||
snprintf(g_notif_cleared_app, sizeof g_notif_cleared_app, "%s", app_id);
|
||||
return 1;
|
||||
}
|
||||
/* send_frame:整帧写到 fd(socketpair 一端),对端读回校验。 */
|
||||
static size_t fake_send_frame(int fd, uint16_t type, uint32_t req_id,
|
||||
const void *payload, uint32_t len)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, rid = req_id, l = len;
|
||||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||||
size_t off = 0;
|
||||
|
||||
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
|
||||
memcpy(hdr + 8, &rid, 4); memcpy(hdr + 12, &l, 4);
|
||||
if (write(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return 0;
|
||||
off = sizeof hdr;
|
||||
if (len && payload && write(fd, payload, len) > 0) off += len;
|
||||
return off;
|
||||
}
|
||||
/* media ops.send 同 fake_send_frame(PLAYCTL 下发校验用) */
|
||||
|
||||
/* 从 socketpair 读一帧(poll 500ms 防挂)。返回负载字节数,<0=超时/失败。 */
|
||||
static int read_frame(int fd, uint16_t *type, void *pay, int paycap)
|
||||
{
|
||||
struct pollfd pf = { .fd = fd, .events = POLLIN, .revents = 0 };
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t len;
|
||||
|
||||
if (poll(&pf, 1, 500) <= 0) return -1;
|
||||
if (read(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return -1;
|
||||
memcpy(type, hdr + 6, 2);
|
||||
memcpy(&len, hdr + 12, 4);
|
||||
if (len > (uint32_t)paycap) return -1;
|
||||
if (len && read(fd, pay, len) != (ssize_t)len) return -1;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
/* fork 一个长眠子进程(300s),测试末统一 SIGKILL 清理。 */
|
||||
static pid_t spawn_sleeper(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) { sleep(300); _exit(0); }
|
||||
return pid;
|
||||
}
|
||||
|
||||
/* 泵 tick 直到 cond 或超时(~2s):子进程死亡到 WNOHANG 可见有调度延迟。 */
|
||||
static int pump_bg_until(int (*cond)(void *), void *ctx)
|
||||
{
|
||||
for (int i = 0; i < 100; i++) {
|
||||
meow_bg_tick();
|
||||
if (cond(ctx)) return 1;
|
||||
usleep(20000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int pump_svc_until(int (*cond)(void *), void *ctx)
|
||||
{
|
||||
for (int i = 0; i < 100; i++) {
|
||||
meow_svc_tick();
|
||||
if (cond(ctx)) return 1;
|
||||
usleep(20000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int cond_bg_dead(void *ctx) { (void)ctx; return g_bg_dead > 0; }
|
||||
static int cond_svc_reaped(void *ctx)
|
||||
{
|
||||
const meow_svc_rt_t *s;
|
||||
(void)ctx;
|
||||
s = meow_svc_find("dev.test.svc", "daemon");
|
||||
return s && s->pid == 0;
|
||||
}
|
||||
static int cond_svc_gone(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return meow_svc_find("dev.test.svc", "daemon") == NULL;
|
||||
}
|
||||
|
||||
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。
|
||||
* 路径取 $TMPDIR(Termux/Android 主机没有 /tmp),缺省回退 /tmp。 */
|
||||
static char g_svc_path[256];
|
||||
#define TEST_SVC_PATH g_svc_path
|
||||
static void write_test_svc(void)
|
||||
{
|
||||
const char *tmp = getenv("TMPDIR");
|
||||
if (!tmp || !tmp[0]) tmp = "/tmp";
|
||||
snprintf(g_svc_path, sizeof g_svc_path, "%s/meow_bgplay_test_svc.sh", tmp);
|
||||
FILE *f = fopen(TEST_SVC_PATH, "w");
|
||||
if (!f) { printf("FAIL: 无法创建测试脚本 %s\n", TEST_SVC_PATH); exit(1); }
|
||||
fputs("#!/bin/sh\nexec sleep 300\n", f);
|
||||
fclose(f);
|
||||
chmod(TEST_SVC_PATH, 0755);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_bgplay_ops ops;
|
||||
meow_media_ops mops;
|
||||
|
||||
memset(&ops, 0, sizeof ops);
|
||||
ops.now_ms = fake_now_ms;
|
||||
ops.card_present = fake_card;
|
||||
ops.on_bg_dead = fake_bg_dead;
|
||||
ops.media_resident = meow_media_app_resident; /* 批①+宽限:媒体活跃判定(宽限回收/槽满LRU) */
|
||||
ops.notif_clear_app = fake_notif_clear;
|
||||
ops.send_frame = fake_send_frame;
|
||||
ops.log_dir = NULL;
|
||||
meow_bgplay_init(&ops);
|
||||
|
||||
memset(&mops, 0, sizeof mops);
|
||||
mops.send = fake_send_frame;
|
||||
meow_media_init(&mops);
|
||||
|
||||
/* ---- 1) should_keep 判定矩阵(批①三参:bg_audio, media_resident, keepalive) ---- */
|
||||
CHECK(meow_bg_should_keep(0, 0, 0) == 0, "should_keep(未声明,未活跃,非常驻)=0");
|
||||
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(声明bg_audio,无会话)=0");
|
||||
CHECK(meow_bg_should_keep(0, 1, 0) == 0, "should_keep(未声明bg_audio,活跃)=0");
|
||||
CHECK(meow_bg_should_keep(1, 1, 0) == 1, "should_keep(bg_audio+活跃)=1");
|
||||
CHECK(meow_bg_should_keep(0, 0, 1) == 1, "should_keep(keepalive恒保)=1");
|
||||
CHECK(meow_bg_should_keep(0, 1, 1) == 1, "should_keep(keepalive恒保,忽略media)=1");
|
||||
CHECK(meow_bg_should_keep(1, 0, 1) == 1, "should_keep(keepalive恒保,无会话)=1");
|
||||
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(bg_audio+resident=0 真退)=0");
|
||||
|
||||
/* ---- 2) 后台播放保活全链路 ---- */
|
||||
int sp[2];
|
||||
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0, "socketpair 建立");
|
||||
{
|
||||
m_meta_t meta;
|
||||
memset(&meta, 0, sizeof meta);
|
||||
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1");
|
||||
meta.state = MEOW_MEDIA_PLAYING;
|
||||
meta.dur_ms = 200000;
|
||||
CHECK(meow_media_meta(&meta, sp[0]) == 0, "META 上报 PLAYING 注册媒体会话");
|
||||
}
|
||||
CHECK(meow_media_app_playing("dev.test.bg1") == 1, "app_playing(bg1)=1");
|
||||
CHECK(meow_media_app_resident("dev.test.bg1") == 1, "app_resident(bg1)=1");
|
||||
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 1,
|
||||
"播放中退出 → 应保活");
|
||||
|
||||
pid_t kid = spawn_sleeper();
|
||||
CHECK(kid > 0, "fork 假 App 子进程");
|
||||
CHECK(meow_bg_add("dev.test.bg1", kid, sp[0], 0, 0) == 0, "bg_add 转后台保活");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 1, "is_backgrounded=1");
|
||||
CHECK(meow_bg_count() == 1, "bg count=1");
|
||||
{
|
||||
const meow_bg_entry_t *be = meow_bg_at(0);
|
||||
CHECK(be && be->bg_since_ms > 0, "bg 槽记 bg_since_ms(转后台时刻)");
|
||||
CHECK(be && be->keepalive == 0, "bg_audio 类 keepalive=0");
|
||||
}
|
||||
CHECK(kill(kid, 0) == 0, "后台化后进程仍存活(未 SIGTERM/SIGSTOP)");
|
||||
/* PLAYCTL 经会话 fd 往返(后台化不断连:进程/fd 均保留) */
|
||||
CHECK(meow_media_playctl_to("dev.test.bg1", 6, 0) == 0, "PLAYCTL 下发到后台 App 会话");
|
||||
{
|
||||
uint16_t t = 0;
|
||||
m_playctl_t pc;
|
||||
int n = read_frame(sp[1], &t, &pc, sizeof pc);
|
||||
CHECK(n == (int)sizeof pc && t == MSG_PLAYCTL && pc.cmd == 6,
|
||||
"对端收到 MSG_PLAYCTL cmd=6(控制通道后台不断连)");
|
||||
}
|
||||
{
|
||||
int rfd = -1;
|
||||
pid_t rpid = 0;
|
||||
CHECK(meow_bg_take("dev.test.bg1", &rfd, &rpid) == 0 && rpid == kid && rfd == sp[0],
|
||||
"bg_take 回前台(pid/fd 原样归还)");
|
||||
}
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 0 && meow_bg_count() == 0,
|
||||
"take 后清槽");
|
||||
meow_media_clear("dev.test.bg1");
|
||||
{
|
||||
m_meta_t meta;
|
||||
memset(&meta, 0, sizeof meta);
|
||||
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1");
|
||||
meta.state = MEOW_MEDIA_STOPPED;
|
||||
meow_media_meta(&meta, sp[0]);
|
||||
}
|
||||
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 0,
|
||||
"停播后再退出 → should_keep=0(真退 SIGTERM)");
|
||||
kill(kid, SIGKILL); waitpid(kid, NULL, 0);
|
||||
close(sp[0]); close(sp[1]);
|
||||
meow_media_clear("dev.test.bg1");
|
||||
|
||||
/* ---- 2b) keepalive 类(background_keepalive)入槽标记 ---- */
|
||||
{
|
||||
pid_t kk = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.keep", kk, -1, 0, 1) == 0, "bg_add(keepalive=1)");
|
||||
const meow_bg_entry_t *bk = meow_bg_at(0);
|
||||
CHECK(bk && bk->keepalive == 1, "keepalive 类槽 keepalive=1");
|
||||
CHECK(bk && bk->bg_since_ms > 0, "keepalive 类槽 bg_since_ms>0");
|
||||
meow_bg_take("dev.test.keep", NULL, NULL);
|
||||
kill(kk, SIGKILL); waitpid(kk, NULL, 0);
|
||||
CHECK(meow_bg_count() == 0, "keepalive 槽 take 后清槽");
|
||||
}
|
||||
|
||||
/* ---- 3) 后台 App 崩溃照收 ---- */
|
||||
g_bg_dead = 0;
|
||||
kid = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.crash", kid, -1, 0, 0) == 0, "bg_add(crash)");
|
||||
kill(kid, SIGKILL);
|
||||
{
|
||||
int ok = pump_bg_until(cond_bg_dead, NULL);
|
||||
(void)ok;
|
||||
}
|
||||
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.crash") == 0 &&
|
||||
WIFSIGNALED(g_bg_dead_status) && WTERMSIG(g_bg_dead_status) == SIGKILL,
|
||||
"崩溃子进程被 tick 回收 + on_bg_dead(SIGKILL) 回调");
|
||||
CHECK(meow_bg_count() == 0, "崩溃回收后清槽");
|
||||
|
||||
/* ---- 4) 卡拔例外:on_card=1 + 卡不在 → SIGTERM 回收 ---- */
|
||||
g_bg_dead = 0;
|
||||
kid = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.card", kid, -1, 1, 0) == 0, "bg_add(on_card=1)");
|
||||
g_card = 0;
|
||||
pump_bg_until(cond_bg_dead, NULL);
|
||||
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.card") == 0,
|
||||
"卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)");
|
||||
g_card = 1;
|
||||
|
||||
/* ---- 4b) meow_bg_terminate:后台管理页「杀」按钮 / `am kill` 共用的终止通道 ---- */
|
||||
{
|
||||
g_bg_dead = 0;
|
||||
pid_t tk = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.term", tk, -1, 0, 0) == 0, "bg_add(term)");
|
||||
CHECK(meow_bg_terminate("") == MEOW_ERR_INVAL, "terminate 空 app_id → INVAL");
|
||||
CHECK(meow_bg_terminate("dev.test.nothere") == MEOW_ERR_NOENT, "terminate 不在后台表 → NOENT");
|
||||
CHECK(meow_bg_terminate("dev.test.term") == 0, "terminate 受理(在后台表且 pid>0)");
|
||||
{
|
||||
int found = 0;
|
||||
for (int i = 0; i < MEOW_BG_MAX; i++) {
|
||||
const meow_bg_entry_t *be = meow_bg_at(i);
|
||||
if (be && !strcmp(be->app_id, "dev.test.term")) {
|
||||
CHECK(be->term_at_ms > 0, "terminate 置 term_at_ms(bgm_enumerate 据此隐藏行)");
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
CHECK(found == 1, "terminate 后槽仍在(等 tick 回收)");
|
||||
}
|
||||
pump_bg_until(cond_bg_dead, NULL);
|
||||
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.term") == 0,
|
||||
"terminate → tick SIGTERM 回收 + on_bg_dead 上报 amsd");
|
||||
CHECK(meow_bg_count() == 0, "terminate 回收后清槽");
|
||||
}
|
||||
|
||||
/* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
|
||||
{
|
||||
pid_t k[MEOW_BG_MAX];
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
int i;
|
||||
pid_t gextra = 0;
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
k[i] = spawn_sleeper();
|
||||
snprintf(id, sizeof id, "dev.test.lru%d", i);
|
||||
CHECK(meow_bg_add(id, k[i], -1, 0, 0) == 0, "lru 填表(未声明 keepalive=0)");
|
||||
}
|
||||
/* 5.1 表满 + 全未声明 → 新增 LRU 回收最旧(lru0,bg_since_ms 最小),新 App 入表成功 */
|
||||
{
|
||||
pid_t e2 = spawn_sleeper();
|
||||
gextra = e2;
|
||||
CHECK(meow_bg_add("dev.test.lrunew", e2, -1, 0, 0) == 0,
|
||||
"表满全未声明 → LRU 回收最旧槽并成功新增");
|
||||
CHECK(meow_bg_count() == MEOW_BG_MAX, "回收后仍满 4 槽");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.lrunew") == 1, "新增 App 在后台表");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.lru0") == 0, "最久未激活未声明 lru0 被回收摘表");
|
||||
CHECK(kill(k[0], 0) < 0, "lru0 进程已终止(SIGTERM 真退回收)");
|
||||
}
|
||||
/* 5.2 重复 add → EXISTS;pid<=0 → INVAL(lru0 已回收,表内为 lrunew/lru1/lru2/lru3) */
|
||||
CHECK(meow_bg_add("dev.test.lrunew", 999999, -1, 0, 0) == MEOW_ERR_EXISTS, "重复 app_id → EXISTS");
|
||||
CHECK(meow_bg_add("dev.test.lrunew", 0, -1, 0, 0) == MEOW_ERR_INVAL, "pid<=0 → INVAL");
|
||||
/* 5.3 清空剩余(take + 杀进程防泄漏) */
|
||||
for (i = 1; i < MEOW_BG_MAX; i++) {
|
||||
snprintf(id, sizeof id, "dev.test.lru%d", i);
|
||||
meow_bg_take(id, NULL, NULL);
|
||||
kill(k[i], SIGKILL); waitpid(k[i], NULL, 0);
|
||||
}
|
||||
meow_bg_take("dev.test.lrunew", NULL, NULL);
|
||||
kill(gextra, SIGKILL); waitpid(gextra, NULL, 0);
|
||||
CHECK(meow_bg_count() == 0, "清空后 bg 表空(未声明)");
|
||||
}
|
||||
|
||||
/* ---- 5b) 表满 + 全为声明常驻(keepalive=1)→ 拒绝新入 NOMEM(不回收声明常驻,保安全) ---- */
|
||||
{
|
||||
pid_t d[MEOW_BG_MAX];
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
int i;
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
d[i] = spawn_sleeper();
|
||||
snprintf(id, sizeof id, "dev.test.dec%d", i);
|
||||
CHECK(meow_bg_add(id, d[i], -1, 0, 1) == 0, "声明常驻填表(keepalive=1)");
|
||||
}
|
||||
{
|
||||
pid_t e2 = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.decnew", e2, -1, 0, 0) == MEOW_ERR_NOMEM,
|
||||
"全声明常驻满 → 新增 NOMEM(不回退真退,保安全)");
|
||||
kill(e2, SIGKILL); waitpid(e2, NULL, 0); /* 未入表,就地清理 */
|
||||
}
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
snprintf(id, sizeof id, "dev.test.dec%d", i);
|
||||
meow_bg_take(id, NULL, NULL);
|
||||
kill(d[i], SIGKILL); waitpid(d[i], NULL, 0);
|
||||
}
|
||||
CHECK(meow_bg_count() == 0, "声明常驻清空后 bg 表空");
|
||||
}
|
||||
|
||||
/* ================= 批①+宽限:宽限超时回收 / 声明常驻与媒体活跃不清 ================= */
|
||||
g_ms = 100000; /* 本组从干净时间基准起(后续 svc 段用相对 g_ms,不受影响) */
|
||||
|
||||
/* 5c) 未声明 + 媒体不活跃:grace 内保活;恰等 MEOW_BG_GRACE_MS 不回收(严格 >);超界回收 */
|
||||
{
|
||||
pid_t s1 = spawn_sleeper();
|
||||
g_bg_dead = 0;
|
||||
CHECK(meow_bg_add("dev.test.grace1", s1, -1, 0, 0) == 0, "grace1 入表(未声明 keepalive=0)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS; /* 恰好等于宽限(非严格大于) */
|
||||
meow_bg_tick();
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 1 && g_bg_dead == 0,
|
||||
"grace 边界:恰等 MEOW_BG_GRACE_MS 不回收(严格 > 才回收)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS + 1; /* 超界 1ms */
|
||||
pump_bg_until(cond_bg_dead, NULL);
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 0 && meow_bg_count() == 0,
|
||||
"超 grace 未再启动 → 回收摘表");
|
||||
CHECK(g_bg_dead == 1, "宽限回收触发 on_bg_dead(通知 amsd)+ 进程经 SIGTERM 退出");
|
||||
}
|
||||
|
||||
/* 5d) 声明常驻(keepalive=1)超 grace 不清(无限期) */
|
||||
{
|
||||
pid_t k1 = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.gracekeep", k1, -1, 0, 1) == 0, "gracekeep 入表(keepalive=1)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
|
||||
meow_bg_tick();
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.gracekeep") == 1,
|
||||
"声明常驻 keepalive=1 超 grace 不清(无限期)");
|
||||
meow_bg_take("dev.test.gracekeep", NULL, NULL);
|
||||
kill(k1, SIGKILL); waitpid(k1, NULL, 0);
|
||||
}
|
||||
|
||||
/* 5e) 媒体活跃(META PLAYING,keepalive=0)超 grace 不清(resident 保护) */
|
||||
{
|
||||
int sp2[2];
|
||||
pid_t m1;
|
||||
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp2) == 0, "grace media socketpair");
|
||||
{
|
||||
m_meta_t meta;
|
||||
memset(&meta, 0, sizeof meta);
|
||||
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.gracemedia");
|
||||
meta.state = MEOW_MEDIA_PLAYING;
|
||||
meow_media_meta(&meta, sp2[0]);
|
||||
}
|
||||
m1 = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.gracemedia", m1, -1, 0, 0) == 0,
|
||||
"gracemedia 入表(keepalive=0 但媒体活跃)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
|
||||
meow_bg_tick();
|
||||
CHECK(meow_media_app_resident("dev.test.gracemedia") == 1, "gracemedia media_resident=1");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.gracemedia") == 1,
|
||||
"媒体活跃超 grace 不清(resident 保护)");
|
||||
meow_bg_take("dev.test.gracemedia", NULL, NULL);
|
||||
kill(m1, SIGKILL); waitpid(m1, NULL, 0);
|
||||
meow_media_clear("dev.test.gracemedia");
|
||||
close(sp2[0]); close(sp2[1]);
|
||||
}
|
||||
|
||||
/* ================= 无头后台服务 ================= */
|
||||
write_test_svc();
|
||||
|
||||
/* ---- 6) 注册校验 ---- */
|
||||
CHECK(meow_svc_register("dev.test.svc", "daemon", "/nonexistent/x", 1, 2, 60000, 0, -1)
|
||||
== MEOW_ERR_NOENT, "exec 不可执行 → NOENT(不留尸槽)");
|
||||
CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "NOENT 后无槽残留");
|
||||
|
||||
int sp_svc[2];
|
||||
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp_svc) == 0, "svc socketpair 建立");
|
||||
int pid1 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH,
|
||||
1, 2, 60000, 0, sp_svc[0]);
|
||||
CHECK(pid1 > 0, "register 拉起服务");
|
||||
CHECK(meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 2, 60000, 0, sp_svc[0])
|
||||
== MEOW_ERR_EXISTS, "同名服务在跑 → EXISTS");
|
||||
|
||||
/* ---- 7) 退避 1s/2s + max=2 熔断 ---- */
|
||||
const meow_svc_rt_t *sl;
|
||||
kill(pid1, SIGKILL);
|
||||
pump_svc_until(cond_svc_reaped, NULL);
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->pid == 0 && sl->restart_count == 1 &&
|
||||
sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_1,
|
||||
"第 1 次死 → 1s 退避排程(restart_count=1)");
|
||||
g_ms += MEOW_SVC_BACKOFF_1;
|
||||
meow_svc_tick();
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->pid > 0 && sl->pid != pid1, "到点重启:新 pid");
|
||||
|
||||
pid_t pid2 = sl->pid;
|
||||
kill(pid2, SIGKILL);
|
||||
pump_svc_until(cond_svc_reaped, NULL);
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->pid == 0 && sl->restart_count == 2 &&
|
||||
sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_2,
|
||||
"第 2 次死 → 2s 退避排程(restart_count=2)");
|
||||
g_ms += MEOW_SVC_BACKOFF_2;
|
||||
meow_svc_tick();
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->pid > 0, "第 2 次重启拉起");
|
||||
|
||||
g_notif_cleared = 0;
|
||||
kill(sl->pid, SIGKILL);
|
||||
pump_svc_until(cond_svc_reaped, NULL);
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->pid == 0 && sl->circuit == 1 && sl->restart_count == 3,
|
||||
"第 3 次死超 max_restart=2 → 熔断 circuit=1");
|
||||
CHECK(g_notif_cleared == 1 && strcmp(g_notif_cleared_app, "dev.test.svc") == 0,
|
||||
"熔断清该 App 通知(notif_clear_app 回调)");
|
||||
{
|
||||
uint16_t t = 0;
|
||||
m_sys_t m;
|
||||
int n = read_frame(sp_svc[1], &t, &m, sizeof m);
|
||||
CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SVC_DEAD &&
|
||||
strcmp(m.svc, "daemon") == 0,
|
||||
"注册连接收到 MSG_SYS{SVC_DEAD=2} 熔断广播");
|
||||
}
|
||||
g_ms += 60000;
|
||||
meow_svc_tick();
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->pid == 0, "熔断后不再拉起");
|
||||
|
||||
/* 重注册复位熔断 */
|
||||
int pid4 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH,
|
||||
1, 2, 60000, 0, sp_svc[0]);
|
||||
CHECK(pid4 > 0, "熔断槽重注册 → 复位重新拉起");
|
||||
sl = meow_svc_find("dev.test.svc", "daemon");
|
||||
CHECK(sl && sl->circuit == 0 && sl->restart_count == 0, "熔断复位(circuit/count 清零)");
|
||||
|
||||
/* ---- 8) SERVICE_STOP:不重启 ---- */
|
||||
CHECK(meow_svc_stop("dev.test.svc", "daemon") == 0, "svc_stop 受理");
|
||||
pump_svc_until(cond_svc_gone, NULL);
|
||||
CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "stop 后槽释放(不重启)");
|
||||
CHECK(meow_svc_stop("dev.test.svc", "daemon") == MEOW_ERR_NOENT, "重复 stop → NOENT");
|
||||
close(sp_svc[0]); close(sp_svc[1]);
|
||||
|
||||
/* ---- 9) kill_all:全回收 + SHUTDOWN 广播 ---- */
|
||||
{
|
||||
int sp2[2];
|
||||
socketpair(AF_UNIX, SOCK_STREAM, 0, sp2);
|
||||
int spid = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH,
|
||||
1, 3, 60000, 0, sp2[0]);
|
||||
pid_t bgk = spawn_sleeper();
|
||||
meow_bg_add("dev.test.bg9", bgk, -1, 0, 0);
|
||||
CHECK(spid > 0 && meow_bg_count() == 1, "kill_all 前置:1 svc + 1 bg");
|
||||
meow_bgplay_kill_all();
|
||||
CHECK(meow_bg_count() == 0 && meow_svc_find("dev.test.svc", "daemon") == NULL,
|
||||
"kill_all 后 bg/svc 全回收");
|
||||
CHECK(kill(spid, 0) < 0 && kill(bgk, 0) < 0, "子进程确已终止");
|
||||
uint16_t t = 0;
|
||||
m_sys_t m;
|
||||
int n = read_frame(sp2[1], &t, &m, sizeof m);
|
||||
CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SHUTDOWN,
|
||||
"注册连接收到 MSG_SYS{SHUTDOWN=1}(关机前回收广播)");
|
||||
close(sp2[0]); close(sp2[1]);
|
||||
}
|
||||
|
||||
unlink(TEST_SVC_PATH);
|
||||
printf(g_fails ? "== FAILURES: %d ==\n" : "== ALL PASS ==\n", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
Loaded 100 of 576 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user