diff --git a/patch_v2/meow_watchdog/meow_watchdog b/patch_v2/meow_watchdog/meow_watchdog index abba889..49f5d80 100755 Binary files a/patch_v2/meow_watchdog/meow_watchdog and b/patch_v2/meow_watchdog/meow_watchdog differ diff --git a/patch_v2/meow_watchdog/meow_watchdog.c b/patch_v2/meow_watchdog/meow_watchdog.c index 144cd50..c70b403 100644 --- a/patch_v2/meow_watchdog/meow_watchdog.c +++ b/patch_v2/meow_watchdog/meow_watchdog.c @@ -22,6 +22,10 @@ * 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。 * 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。 * (8s 强关的「防误触 / pm_interacted」逻辑在 launcher 的 show_power_menu 内,此进程只做兜底。) + * - M2.86(226) SDK 框架 App 让位:启动时经 /proc//exe 读同目录 app.json 的 framework + * (true|1|yes)。判定为 SDK 框架 App -> 音量/媒体键让位 SDK 输入层(meow_ui_input.c 已消化, + * 防同一次按键双步进);电源仍由本进程兜底(SDK 对电源是 no-op)。裸自绘 App(framework=0/ + * 无 framework)或 app.json 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。 * Exits when the app process disappears (kill(pid,0) -> ESRCH) or on * SIGTERM from the parent. * @@ -116,27 +120,6 @@ static unsigned char *fb; static int xres, yres, line_len, bpp; -static inline void put_px(int x, int y, uint32_t color) -{ - if (x < 0 || x >= xres || y < 0 || y >= yres) return; - unsigned char *p = fb + y * line_len + x * (bpp / 8); - if (bpp == 16) { - uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) | - ((color >> 3) & 0x001F); - *(uint16_t *)p = c; - } else { - *(uint32_t *)p = (color << 8) | 0xFF; - } -} - -/* 5x7 "<" arrow + border */ -static void fill_rect(int x0, int y0, int w, int h, uint32_t color) -{ - for (int y = y0; y < y0 + h; y++) - for (int x = x0; x < x0 + w; x++) - put_px(x, y, color); -} - static int read_state_file(void) { /* returns 1 if immersive */ @@ -194,6 +177,10 @@ static int down_valid = 0; /* a press is currently being tracked */ static int down_x = 0, down_y = 0; static unsigned long long down_ms = 0; +/* M2.86(226):1=当前前台 App 为 SDK 框架 App(音量/媒体键让位 SDK;M2.87 起左缘返回 + * 手势 SIGUSR1 也照样让位 SDK——框架 App 经 SDK on_swipe 处理返回)。在 handle_touch 前声明。 */ +static int g_fw_app = 0; + /* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */ static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */ static int wd_saved_bri = 60; /* 熄屏前亮度快照 */ @@ -427,15 +414,96 @@ static void handle_touch(int immersive, int app_pid, down_valid = 0; if (now_ms() - down_ms > SHORT_PRESS_MS) return; /* held too long */ /* M2.21:返回键彻底移除。watchdog 不再有「按钮命中->SIGTERM」路径(那是 - * 虚拟返回键的年代)。返回手势统一为「左缘起点 + 向右滑 SWIPE_DX」→ SIGUSR1, - * 不论 immersive 与否(返回键没了,所有 app 都靠左滑返回)。app 卡死仍可由 - * 外层强杀序列(音量减x3)兜底。 */ + * 虚拟返回键的年代)。返回手势统一为「左缘起点 + 向右滑 SWIPE_DX」→ SIGUSR1。 + * M2.87:SIGUSR1 只发给「裸沉浸 App」(framework=0/无 framework 的自绘 App, + * 其自己处理 SIGUSR1 当返回事件)。SDK 框架 App(g_fw_app=1,如 hello/files/ncm) + * 自己经 SDK on_swipe 处理左缘返回/顶部下拉(meow_ui_input.c),若 watchdog 也发 + * SIGUSR1 → 默认动作杀死 App(ncm `signal=16` 崩溃根因)→ 本进程不发、避免双处理/误杀。 + * 裸沉浸判定 = 非框架 且 /tmp/meowapp_state=1(app.json immersive 声明,launcher 写)。 */ if (down_x < EDGE_W && (last_x - down_x) > SWIPE_DX) { - wd_log("swipe -> SIGUSR1"); - kill(app_pid, SIGUSR1); + if (!g_fw_app && immersive) { + wd_log("swipe -> SIGUSR1 (bare immersive, framework=%d)", g_fw_app); + kill(app_pid, SIGUSR1); + } else { + wd_log("swipe ignored (framework=%d immersive=%d) -> SDK owns back", g_fw_app, immersive); + } } } +/* ---- M2.86(226) SDK 框架 App 判定:app.json framework=1 -> 音量/媒体键让位 SDK ---- + * 背景:App 前台时 SDK 输入层(meow_ui_input.c)已消化音量/媒体键;若 watchdog 也消化, + * 同一次物理按键会步进两次(冗余双处理)。判定「SDK 框架 App 在跑」用 app.json 的 + * framework=true|1|yes —— 这是同类候选中更稳的一个:launcher 启动应用前已读同一 app.json + * 并成功 exec(app.json 必然可读、结果确定);SDK 侧无稳定「框架注册」标记 + * (/tmp/meow_app_activity 交互节流、/tmp/meowapp_sbar 仅状态栏模式变化时写),不宜作判定源。 + * 判定为框架 App -> watchdog 对音量/媒体键让位(交给 SDK),电源仍由本进程兜底(SDK 对电源 + * 是 no-op,见 meow_ui_input.c);裸自绘 App(framework=0/无 framework)或 app.json 不可读 -> + * watchdog 保持兜底,绝不造成功能真空。 */ +/* 极简 JSON/扁平值提取:与 launcher util.c json_str 同语义(键前缀须独立 + ':'/'=' + 可选引号)。 */ +static int wd_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) { /* 键前缀须独立:防 "x_framework":1 被误命中为 framework */ + unsigned char pc = (unsigned char)p[-1]; + if ((pc >= 'a' && pc <= 'z') || (pc >= 'A' && pc <= 'Z') || + (pc >= '0' && pc <= '9') || pc == '_' || pc == '.') + return 0; + } + p += strlen(key); + if (*p == '"') p++; /* JSON 键右引号 */ + while (*p == ' ' || *p == '\t') p++; /* 键与 ':'/'=' 间空白(宽松容错) */ + while (*p == ':' || *p == '=') p++; + while (*p == ' ' || *p == '\t') p++; + if (*p == '"') p++; /* 值左引号 */ + int i = 0; + while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && + *p != '}' && *p != ']' && *p != ':' && *p != ' ' && *p != '\t' && i < olen - 1) + out[i++] = *p++; + out[i] = 0; + while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0; + return i; +} + +/* 读 /app.json 的 framework 字段(true|1|yes = 1)。返回 1=SDK 框架 App。 */ +static int wd_app_is_framework(const char *dir) +{ + char path[576]; + int n = snprintf(path, sizeof path, "%s/app.json", dir); + if (n < 0 || n >= (int)sizeof path) return 0; + FILE *f = fopen(path, "r"); + if (!f) return 0; + int fw = 0; + char buf[256]; + while (fgets(buf, sizeof buf, f)) { + if (!strstr(buf, "framework")) continue; + char v[16] = ""; + if (wd_json_str(buf, "framework", v, sizeof v) > 0 && + (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))) + fw = 1; + break; /* framework 每 app.json 至多一次,读到即定 */ + } + fclose(f); + return fw; +} + +/* 经 /proc//exe 反查应用安装目录(exec 与 app.json 同目录),再判 framework。 */ +static int wd_framework_from_pid(int pid) +{ + char exe[576], link[64]; + snprintf(link, sizeof link, "/proc/%d/exe", pid); + ssize_t r = readlink(link, exe, sizeof exe - 1); + if (r < 0 || r >= (ssize_t)sizeof exe) return 0; + exe[r] = 0; + char *slash = strrchr(exe, '/'); + if (slash) *slash = 0; /* 目录 = exec 所在目录(app.json 同目录) */ + if (!exe[0]) return 0; + return wd_app_is_framework(exe); +} + +#ifndef WD_NO_MAIN int main(int argc, char **argv) { if (argc < 2) { @@ -452,7 +520,11 @@ int main(int argc, char **argv) setvbuf(stderr, NULL, _IONBF, 0); wd_log_init(); wd_vol_load(); /* M2.24:音量初始值读自与 launcher 同源 settings.conf */ - wd_log("app_pid=%d sim_tap=%d sim_swipe=%d", app_pid, sim_tap, sim_swipe); + /* M2.86(226):SDK 框架 App 判定(app.json framework=1)-> 音量/媒体键让位 SDK。 */ + g_fw_app = wd_framework_from_pid(app_pid); + wd_log("app_pid=%d sim_tap=%d sim_swipe=%d framework=%d%s", + app_pid, sim_tap, sim_swipe, g_fw_app, + g_fw_app ? " -> yield vol/media to SDK" : " -> watchdog owns vol/media"); /* cooldown anchor: ~the moment this watchdog started (== app launch). */ unsigned long long start_ms = now_ms(); @@ -540,12 +612,15 @@ int main(int argc, char **argv) } else if (type == EV_KEY && code == KEY_POWER) { handle_power(val); /* M2.22(A)/M2.24:电源单击=息屏/亮屏,长按 tick 里判菜单 */ } else if (type == EV_KEY && code == KEY_VOLUMEUP) { - if (val == 1) wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */ + /* M2.86(226):SDK 框架 App 前台 -> 音量键让位 SDK(防双步进)。 */ + if (val == 1 && !g_fw_app) wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */ } else if (type == EV_KEY && code == KEY_VOLUMEDOWN) { - if (val == 1) wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */ + if (val == 1 && !g_fw_app) wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */ } else if (type == EV_KEY && (code == KEY_PLAYPAUSE || code == KEY_PREVIOUSSONG || code == KEY_NEXTSONG || code == KEY_PREVIOUS)) { - if (val == 1) wd_log("media key captured (code %d) -> consumed, no route", code); + /* M2.86(226):框架 App 前台 -> 媒体键让位 SDK(接媒体会话路由)。 */ + if (val == 1 && !g_fw_app) + wd_log("media key captured (code %d) -> consumed, no route", code); } } } @@ -555,3 +630,4 @@ int main(int argc, char **argv) } return 0; } +#endif /* !WD_NO_MAIN */ diff --git a/patch_v2/meow_watchdog/test_wd_fw b/patch_v2/meow_watchdog/test_wd_fw new file mode 100755 index 0000000..da55ff7 Binary files /dev/null and b/patch_v2/meow_watchdog/test_wd_fw differ diff --git a/patch_v2/meow_watchdog/test_wd_fw.c b/patch_v2/meow_watchdog/test_wd_fw.c new file mode 100644 index 0000000..d1222f6 --- /dev/null +++ b/patch_v2/meow_watchdog/test_wd_fw.c @@ -0,0 +1,101 @@ +/* test_wd_fw.c — 宿主单测:meow_watchdog 的「SDK 框架 App 判定」逻辑(M2.86, issue 226)。 + * + * 编译:#include meow_watchdog.c(去 main 拿 wd_app_is_framework/wd_json_str/wd_framework_from_pid)。 + * gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \ + * test_wd_fw.c -o test_wd_fw + * (meow_watchdog.c 含仅 main 使用的静态函数,宿主去 main 后须 -Wno-unused-function 静音; + * mips 目标构建不定义 WD_NO_MAIN,不受影响。) + * 运行:./test_wd_fw;全 PASS -> exit 0。 + */ +#define WD_NO_MAIN +#include "meow_watchdog.c" + +#include +#include +#include +#include +#include + +static int g_pass = 0, g_fail = 0; + +static void check(const char *name, int got, int want) +{ + if (got == want) { g_pass++; printf("PASS %s\n", name); } + else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); } +} + +/* 在 / 建目录并写 app.json;写失败返回 -1。 */ +static int fixture(const char *base, const char *name, const char *appjson) +{ + char dir[512], path[560]; + snprintf(dir, sizeof dir, "%s/%s", base, name); + if (mkdir(dir, 0755) != 0 && access(dir, F_OK) != 0) return -1; + snprintf(path, sizeof path, "%s/app.json", dir); + FILE *f = fopen(path, "w"); + if (!f) return -1; + fputs(appjson, f); + fclose(f); + return 0; +} + +static int expect_fw(const char *base, const char *name, const char *appjson, int want) +{ + char dir[512]; + snprintf(dir, sizeof dir, "%s/%s", base, name); + if (fixture(base, name, appjson) != 0) { g_fail++; printf("FAIL %s: cannot write fixture\n", name); return 0; } + char label[256]; + snprintf(label, sizeof label, "is_fw(%s)", name); + check(label, wd_app_is_framework(dir), want); + return 1; +} + +int main(void) +{ + char dir[] = "/tmp/wdfwXXXXXX"; + if (!mkdtemp(dir)) { perror("mkdtemp"); return 2; } + + /* 框架 App:各值形态 */ + expect_fw(dir, "v_num", "{\"name\":\"x\",\"framework\":1,\"exec\":\"/a/b\"}", 1); /* JSON 数值 1 */ + expect_fw(dir, "v_true", "{\"name\":\"x\",\"framework\":true}", 1); /* true */ + expect_fw(dir, "v_yes", "framework=yes", 1); /* 扁平 yes */ + expect_fw(dir, "v_str", "{\"framework\":\"true\"}", 1); /* 字符串 "true" */ + expect_fw(dir, "v_space","{\"name\":\"x\", \"framework\" : 1 }", 1); /* 空格容错 */ + + /* 非框架 App */ + expect_fw(dir, "v_zero", "{\"name\":\"x\",\"framework\":0}", 0); /* 数值 0 */ + expect_fw(dir, "v_false","{\"framework\":false}", 0); /* false */ + expect_fw(dir, "v_none", "{\"name\":\"x\",\"exec\":\"/a/b\"}", 0); /* 无 framework 键 */ + expect_fw(dir, "v_sub", "{\"x_framework\":1}", 0); /* 键前缀歧义:不误判 */ + + /* 多行 JSON(framework 单独一行,值独立) */ + expect_fw(dir, "multiline", + "{\n \"name\": \"x\",\n \"framework\": 1,\n \"exec\": \"/a/b\"\n}\n", 1); + + /* wd_json_str 直接断言(防 "myframework" / 前缀干扰) */ + { + char out[16] = ""; + check("json_str exact", wd_json_str("{\"framework\":1}", "framework", out, sizeof out) > 0 && + !strcmp(out, "1"), 1); + check("json_str prefix-reject", + wd_json_str("{\"x_framework\":1}", "framework", out, sizeof out), 0); + check("json_str no-hit", wd_json_str("{\"name\":\"x\"}", "framework", out, sizeof out), 0); + } + + /* wd_framework_from_pid:自进程 /proc/self/exe 目录无 app.json -> 0,不崩 */ + check("from_pid(no app.json)", wd_framework_from_pid(getpid()), 0); + + /* 清理 */ + { + char path[560]; + const char *names[] = { "v_num","v_true","v_yes","v_str","v_space","v_zero", + "v_false","v_none","v_sub","multiline" }; + for (unsigned i = 0; i < sizeof names / sizeof names[0]; i++) { + snprintf(path, sizeof path, "%s/%s/app.json", dir, names[i]); unlink(path); + snprintf(path, sizeof path, "%s/%s", dir, names[i]); rmdir(path); + } + rmdir(dir); + } + + printf("\n%d pass, %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +}