From 5999c2dfb9bfba89b4204782303f6055fb1eb8ef Mon Sep 17 00:00:00 2001 From: MeowOS Date: Mon, 31 Aug 2026 15:42:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(sdk+launcher):=20=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E4=B8=8D=E6=B8=B2=E6=9F=93=E7=9C=9F=E6=A0=B9=E5=9B=A0(agent-21?= =?UTF-8?q?9)=20=E2=80=94=20meow=5Fui=5Fimg.c=20scale=5Fbuf=20cover=20?= =?UTF-8?q?=E9=87=87=E6=A0=B7=E6=8A=8A=E4=B9=98=E5=86=99=E6=88=90=E9=99=A4?= =?UTF-8?q?(=C3=B7=E7=BC=A9=E6=94=BE=E6=AF=94=E2=86=92=E9=87=87=E5=88=B0?= =?UTF-8?q?=E6=BA=90=E5=9B=BE=E5=B7=A6=E4=B8=8A=E8=A7=92=E9=80=8F=E6=98=8E?= =?UTF-8?q?=E5=8C=BA,=E5=85=A8=E4=B8=A2);=20=E6=94=B9=E4=B9=98+=E5=B1=85?= =?UTF-8?q?=E4=B8=AD=E8=A3=81=E5=89=AA=E5=81=8F=E7=A7=BB=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?;=20=E5=AE=BF=E4=B8=BB=E6=97=A7=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E7=BA=AF=E8=89=B2=E5=9B=BE=E6=8E=A9=E7=9B=96=E4=BA=86=E8=AF=A5?= =?UTF-8?q?bug=E3=80=82+=20main=5Fmenu=20App=E9=80=80=E5=87=BA/=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8F=98=E5=8C=96=E9=87=8D=E7=BB=98=E6=97=B6=20fb=5Fp?= =?UTF-8?q?an=5Fbuf0=5Fforce=20=E6=97=A0=E6=9D=A1=E4=BB=B6pan=E5=9B=9Ebuff?= =?UTF-8?q?er0(=E9=98=B2App=E7=BF=BB=E9=A1=B5=E7=95=99buffer1=E8=87=B4?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=AE=8B=E5=B8=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meow_ui.a→3e0ee8f7 launcher→9d0efc2f hello→9609ea6e files→90f5cf07 ncm→be8be221(均已上卡); 真机截图证据 .tmp/meowbug/fix1.png+fix_b2_after.png --- meow_ui_img.c | 20 ++++--- meow_ui_input.c | 78 +++++++++++++++++++++++---- meow_ui_media.c | 64 ++++++++++++++++++++++ meow_ui_media.h | 7 +++ meow_ui_vol.c | 69 ++++++++++++++++++++++++ meow_ui_vol.h | 18 +++++++ test_sdk_input_keys | Bin 0 -> 73704 bytes test_sdk_input_keys.c | 123 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 361 insertions(+), 18 deletions(-) create mode 100755 test_sdk_input_keys create mode 100644 test_sdk_input_keys.c diff --git a/meow_ui_img.c b/meow_ui_img.c index 0e6b2e8..c2bd08b 100644 --- a/meow_ui_img.c +++ b/meow_ui_img.c @@ -59,19 +59,23 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover) unsigned char *out = (unsigned char *)malloc((size_t)dw * dh * 4); if (!out) return NULL; float s = 1.0f; - float ox = 0.0f, oy = 0.0f; if (cover) { + /* 真正铺满(cover):每目标像素对应 s 个源像素,s = min(W/dw, H/dh)——缩放后图像 + * 宽/高两维都不小于目标(可能外溢),居中裁方。居中裁剪偏移(源像素)= (W-dw*s)/2 + * 恒 ≥ 0。曾误作 `s = max(sx,sy)` 且采样 `(dx+ox)/s`(除以缩放比):把整块输出映射到 + * 源左上角 ~dw/s 像素的透明角——图标(透明角 logo)整块透明、不绘制,即 + * 「icon: ... OK 但渲染不出(只显首字母占位)」根因。正确:源索引 = dx*s + 裁剪偏移 + * (与下方非 cover 分支 `dx*im->w/dw` 同一乘向)。 */ float sx = (float)im->w / dw, sy = (float)im->h / dh; - s = sx > sy ? sx : sy; /* 铺满 → 可能外溢,居中裁 */ - float sw = im->w / s, sh = im->h / s; /* 目标显示寸(sw×sh) */ - ox = (float)(dw - sw) * 0.5f; - oy = (float)(dh - sh) * 0.5f; - if (ox < 0) ox = 0; if (oy < 0) oy = 0; + s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */ + float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */ + float oys = ((float)im->h - (float)dh * s) * 0.5f; + if (oxs < 0) oxs = 0; if (oys < 0) oys = 0; for (int dy = 0; dy < dh; dy++) { - int syi = (int)((dy + oy) / s); + int syi = (int)((float)dy * s + oys); if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1; for (int dx = 0; dx < dw; dx++) { - int sxi = (int)((dx + ox) / s); + int sxi = (int)((float)dx * s + oxs); if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1; memcpy(out + ((size_t)dy * dw + dx) * 4, im->rgba + ((size_t)syi * im->w + sxi) * 4, 4); diff --git a/meow_ui_input.c b/meow_ui_input.c index a931b65..93e5a51 100644 --- a/meow_ui_input.c +++ b/meow_ui_input.c @@ -6,12 +6,20 @@ * meow_ui_run / IME 循环弹出分发。应用不再自读 event*。 * * 触摸:BTN_TOUCH 按下记录原点,抬起时位移 < 阈值 → TAP,否则按主导轴 → SWIPE。 - * 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.24 全面按键接管:电源/音量/播放/ - * 上下曲全部由系统(watchdog/launcher)消费,SDK 吞噬不发给 APP(应用只收 tap/swipe/tick)。 + * 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.55 起系统按键语义在 SDK 输入层消化: + * 电源 116 → 维持现状(息屏/亮屏/长按菜单由内核/sys_server/watchdog),SDK 不动、不下发 APP; + * 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,volume_step/volume_max + * 约束,与 launcher 设置页/watchdog 同源); + * 播放/暂停 164、上一曲 165/19、下一曲 163 → 转发到当前媒体会话(App 是媒体源→本地回调, + * 否则经 IPC 发 launcher 转发)。 + * 「App 收到的是语义事件」:上述音量/媒体键在 SDK 消化后**仍下发 MEOW_EV_KEY**(on_key),但 + * App 不得把它们映射成自定义行为(APPS.md §3.4.1 语义不变)。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "meow_ui_sys.h" +#include "meow_ui_vol.h" +#include "meow_ui_media.h" #include #include @@ -81,6 +89,45 @@ static uint64_t now_ms(void) return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; } +/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */ +static const char *meow_settings_path(void) +{ + const char *e = getenv("MEOW_SETTINGS_FILE"); + return (e && e[0]) ? e : "/usr/data/meow/settings.conf"; +} + +/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)---- + * 物理键是系统资源(APPS.md §3.4.1),语义全局统一。本函数在输入线程执行系统动作: + * 电源 116 → 息屏/亮屏/长按菜单(内核/sys_server/watchdog),SDK 不动(维持现状); + * 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,与 launcher + * 设置页/watchdog 同源:volume_step/volume_max 约束),仅按下边沿(1)一步; + * 播放/上次曲 164/165/19/163 → 转发到当前媒体会话(App 是媒体源→本地回调;否则 IPC→launcher)。 + * 仅处理按下边沿(val==1)触发系统动作;重复(2)/抬起(0)不重复执行。on_key 的下发由输入线程 + * 在调用本函数后另行 push(音量/媒体键仍通知 App,App 不得映射)。 */ +static void meow_input_handle_sys_key(int code, int val) +{ + switch (code) { + case MEOW_KEY_POWER: /* 电源:维持现状,SDK 不动 */ + break; + case MEOW_KEY_VOLUMEDOWN: + case MEOW_KEY_VOLUMEUP: + if (val == 1) { + int dir = (code == MEOW_KEY_VOLUMEUP) ? +1 : -1; + int nv = meow_ui_vol_step(meow_settings_path(), dir); /* settings 语义+持久化 */ + meow_ui_vol_dac_write(nv); /* 写真实 CS43198 DAC */ + } + break; + case MEOW_KEY_PLAYPAUSE: + case MEOW_KEY_PREVIOUS: + case MEOW_KEY_NEXT: + case 19: /* KEY_PREVIOUSSONG 旧码 */ + if (val == 1) meow_ui_media_sys_key(code); + break; + default: + break; + } +} + void meow_input_drain(void) { pthread_mutex_lock(&g_qm); @@ -335,14 +382,25 @@ static void *input_thread(void *arg) if (code == BTN_TOUCH) { if (val && !t_down) touch_press(t_cx, t_cy); else if (!val && t_down) touch_release(); - } else if (code == MEOW_KEY_POWER || code == MEOW_KEY_VOLUMEDOWN || - code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE || - code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 19) { - /* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 / - * 上一曲 165(MEOW_KEY_PREVIOUS)/19(旧码) / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费, - * 绝不下发 APP。应用不碰物理键、不把任何物理键映射成确认/exit/返回/翻页 - * (KEY_CONTROL_DESIGN §1.3 / APPS.md §3.4.1);APP 只接收 tap/swipe/tick。 - * 忽略该键,不发 MEOW_EV_KEY。 */ + } else if (code == MEOW_KEY_POWER) { + /* M2.55(接 M2.24 纪律):电源=息屏/亮屏/长按菜单,由内核/sys_server/ + * watchdog 统一处理,SDK 不动、也不下发 APP。维持现状。 */ + } else if (code == MEOW_KEY_VOLUMEDOWN || code == MEOW_KEY_VOLUMEUP) { + /* 音量键=系统音量(M2.55:SDK 输入层消化,App 前台也生效)。 + * 语义动作在 meow_input_handle_sys_key(仅按下边沿一步);随后仍下发 + * MEOW_EV_KEY(App on_key 收到语义事件,但不得映射成自定义行为)。 */ + if (val == 1 || val == 0) { + meow_input_handle_sys_key(code, val); + push_ev(MEOW_EV_KEY, code, (val == 1) ? 1 : 0); + } + } else if (code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_PREVIOUS || + code == MEOW_KEY_NEXT || code == 19) { + /* 媒体键=转发到当前媒体会话(M2.55:SDK 输入层消化)。语义动作在 + * meow_input_handle_sys_key;随后仍下发 MEOW_EV_KEY(App 不得映射)。 */ + if (val == 1 || val == 0) { + meow_input_handle_sys_key(code, val); + push_ev(MEOW_EV_KEY, code, (val == 1) ? 1 : 0); + } } else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */ push_ev(MEOW_EV_KEY, code, 1); } else if (val == 0) { /* 抬起 */ diff --git a/meow_ui_media.c b/meow_ui_media.c index 6063642..3632302 100644 --- a/meow_ui_media.c +++ b/meow_ui_media.c @@ -30,6 +30,11 @@ static pthread_t g_thr; static int g_thr_on = 0; static volatile int g_quit = 0; static meow_media_playctl_fn g_cb = NULL; +/* 本进程最后一次经 meow_ui_media_meta 上报的 state(0=停 1=播放 2=暂停 3=缓冲)。 + * 供「物理播放/暂停键」做切换判据:播放中→暂停(2),否则→播放(1)。与 launcher + * now_playing.c 的同款判定(state==PLAYING ? 2 : 1)对齐。 */ +static int g_last_state = 0; +#define MEOW_UI_MEDIA_STATE_PLAYING 1 /* 后台线程主体(定义在文件后段;on_playctl 懒起时引用)。 */ static void *media_thread_main(void *arg); @@ -177,6 +182,7 @@ void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms, memset(&mm, 0, sizeof mm); pthread_mutex_lock(&g_mu); media_app_id(); + g_last_state = (int)state; /* 记录最近上报 state:供物理播放/暂停键切换判据(见 sys_key) */ snprintf(mm.app_id, sizeof mm.app_id, "%s", g_app); mm.state = state; mm.pos_ms = pos_ms; @@ -207,6 +213,64 @@ void meow_ui_media_stop(void) pthread_mutex_unlock(&g_mu); } +/* ---- 物理媒体键 → 系统语义(M2.xx:SDK 输入层接管,App 前台也生效)---- + * 物理键是系统资源(APPS.md §3.4.1):播放/暂停/上一曲/下一曲 → 转发到当前媒体会话。 + * 本进程注册过 playctl 回调(= 本 App 是媒体源)→ 直接本地回调(最简,无 IPC 往返); + * 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到「最近活跃媒体会话」。best-effort 绝不崩。 */ + +/* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek)。 + * 播放/暂停(164)按最近上报 state 切换(播放中→暂停,否则→播放);上一曲 165/19→3,下一曲 163→4。 + * 非媒体键返回 -1。last_state 由调用方在持锁下传入(读 g_last_state 快照)。 */ +static int media_key_cmd(int code, int last_state) +{ + switch (code) { + case 164: /* KEY_PLAYPAUSE:切换(依最近上报 state) */ + return (last_state == MEOW_UI_MEDIA_STATE_PLAYING) ? 2 : 1; + case 165: case 19: /* KEY_PREVIOUSSONG / 旧码 */ + return 3; + case 163: /* KEY_NEXTSONG */ + return 4; + default: + return -1; + } +} + +/* 控制器路径:发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话(launcher ipc_dispatch + * MSG_PLAYCTL → meow_media_playctl → 下发到当前源 fd)。 + * 专门开一条临时连接(不复用媒体源持久连接 g_fd,避免与后台线程 poll/读竞争); + * 无 launcher/断线 → 静默降级(绝不崩)。 */ +static void send_playctl_upstream(int cmd, int64_t seek_ms) +{ + int fd = connect_path(sock_path()); + if (fd < 0) return; + m_playctl_t c; + memset(&c, 0, sizeof c); + c.cmd = (uint32_t)cmd; + c.seek_ms = seek_ms; + if (send_frame(fd, MSG_PLAYCTL, 1, &c, sizeof c) != 0) { close(fd); return; } + /* 读 ACK(best-effort):带 200ms poll,防 launcher 挂死导致输入线程阻塞。 */ + struct pollfd p; + p.fd = fd; p.events = POLLIN; p.revents = 0; + if (poll(&p, 1, 200) > 0 && (p.revents & POLLIN)) { + uint8_t rbuf[128]; + uint16_t rt; + (void)read_frame(fd, &rt, rbuf, sizeof rbuf); + } + close(fd); +} + +/* 物理媒体键按下边沿(val==1 时由输入线程调用;重复/抬起不重复触发)。 */ +void meow_ui_media_sys_key(int code) +{ + pthread_mutex_lock(&g_mu); + meow_media_playctl_fn cb = g_cb; + int cmd = media_key_cmd(code, g_last_state); + pthread_mutex_unlock(&g_mu); + if (cmd <= 0) return; + if (cb) { cb(cmd, 0); return; } /* 本 App 是媒体源(注册过回调)→ 本地回调 */ + send_playctl_upstream(cmd, 0); /* 否则经 IPC 发 launcher 转发到最近活跃会话 */ +} + /* 后台线程主体:懒连 + poll 读 launcher 下发 MSG_PLAYCTL → 调 cb。 */ static void *media_thread_main(void *arg) { diff --git a/meow_ui_media.h b/meow_ui_media.h index 8bdcfae..a5ef67d 100644 --- a/meow_ui_media.h +++ b/meow_ui_media.h @@ -39,6 +39,13 @@ void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms, /* 停止后台接收线程并断开连接(App 退出时调;幂等)。 */ void meow_ui_media_stop(void); +/* 物理媒体键 → 系统语义(SDK 输入层调用;App 前台也生效)。 + * code:164=播放/暂停、165/19=上一曲、163=下一曲。 + * 本 App 注册过 playctl 回调(媒体源)→ 本地直接回调 cb(cmd,seek_ms); + * 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话。非媒体键/无源静默。 + * best-effort:无 launcher/socket → 优雅降级,绝不崩。 */ +void meow_ui_media_sys_key(int code); + #ifdef __cplusplus } #endif diff --git a/meow_ui_vol.c b/meow_ui_vol.c index 2f3cb4d..fdcff49 100644 --- a/meow_ui_vol.c +++ b/meow_ui_vol.c @@ -50,3 +50,72 @@ int meow_ui_vol_read(const char *path, const char *key, int def) while (*found == ' ' || *found == '\t') found++; /* value 左去空白;atoi 天然停于非数字 */ return atoi(found); } + +/* 改 settings.conf 一个 int 键:流式拷贝原文件到 .tmp,命中键行改写为 + * "key = value"(launcher 落盘格式,带空格),键缺失则末尾追加;rename 原子替换。 + * 保留其余行(注释/其它设置)——绝不 O_TRUNC 整文件。 */ +int meow_ui_vol_set(const char *path, const char *key, int val) +{ + if (!path || !path[0] || !key || !key[0]) return -1; + FILE *in = fopen(path, "r"); + char tmp[512]; + int kl = (int)strlen(key); + snprintf(tmp, sizeof tmp, "%s.tmp", path); + FILE *out = fopen(tmp, "w"); + if (!out) { if (in) fclose(in); return -1; } + + int found = 0; + char line[256]; + while (in && fgets(line, sizeof line, in)) { + char *eq = strchr(line, '='); + char *k = line; + while (*k == ' ' || *k == '\t') k++; + int n = eq ? (int)(eq - k) : 0; + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; + if (eq && n == kl && strncmp(k, key, (size_t)kl) == 0) { + fprintf(out, "%s = %d\n", key, val); /* 命中:改写为规范格式 */ + found = 1; + } else { + fputs(line, out); /* 其它行原样保留 */ + } + } + if (in) fclose(in); + if (!found) fprintf(out, "%s = %d\n", key, val); + fclose(out); + + if (rename(tmp, path) != 0) { remove(tmp); return -1; } + return 0; +} + +/* 音量步进(系统语义,与 launcher 设置页/watchdog 同源:volume_step/volume_max 约束)。 + * 只做 settings 逻辑 + 持久化;DAC 写入由调用方(meow_ui_vol_dac_write)负责。 */ +int meow_ui_vol_step(const char *path, int dir) +{ + int vol = meow_ui_vol_read(path, "audio.volume", MEOW_UI_VOL_DEFAULT); + int vol_max= meow_ui_vol_read(path, "audio.volume_max", MEOW_UI_VOL_MAX_DEF); + int step = meow_ui_vol_read(path, "audio.volume_step", 1); + if (step < 1) step = 1; + if (step > 5) step = 5; /* 与 watchdog 读取口径一致(1..5) */ + if (dir < 0) dir = -1; + else if (dir > 0) dir = 1; + else return vol; + + int v = meow_ui_vol_clamp(vol + dir * step, vol_max); + if (v == vol) return vol; /* 已在上下限:无进展,不写 */ + meow_ui_vol_set(path, "audio.volume", v); + return v; +} + +/* 真实 DAC 音量(CS43198, numid 1/2, 0-255),pct→255 映射直写两声道。 + * 与 launcher meow_dac_vol_write / watchdog wd_vol_write 完全同路径(语义同源)。 */ +void meow_ui_vol_dac_write(int pct) +{ + int dac = pct * 255 / 100; + if (dac < 0) dac = 0; + if (dac > 255) dac = 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", + dac, dac); + system(cmd); +} diff --git a/meow_ui_vol.h b/meow_ui_vol.h index fa28c42..d0c71c1 100644 --- a/meow_ui_vol.h +++ b/meow_ui_vol.h @@ -29,4 +29,22 @@ int meow_ui_vol_clamp(int vol, int vol_max); * path 由调用方传入(设备传导 MEOW_SETTINGS_FILE;宿主单测指向临时文件)。 */ int meow_ui_vol_read(const char *path, const char *key, int def); +/* 改 settings.conf 里一个 int 键(地址更新/新增;**保留其余行**——与 launcher 落盘格式 + * "key = value"(带空格)一致;键缺失则追加)。用临时文件 + rename 原子替换,失败返回 -1。 + * 用途:SDK 输入层调系统音量时写 audio.volume(绝不像 watchdog wd_vol_save 那样整文件 + * O_TRUNC,避免删掉 display.* 等其它设置)。 */ +int meow_ui_vol_set(const char *path, const char *key, int val); + +/* 读 audio.volume + audio.volume_max + audio.volume_step,按 dir(±1)迈一步(绝对跨度 + * = volume_step),clamp 到 [0, MEOW_UI_VOL_MAX] 再 clamp 到 volume_max(同 meow_ui_vol_clamp + * 语义,与 launcher 设置页/watchdog 一致),经 meow_ui_vol_set 持久化到 audio.volume。 + * 返回写入 DAC 的映射值(= 夹后新卷值);已在上下限未变则返回当前值(不写)。 + * 仅做 settings 逻辑(不改 DAC);DAC 写入由调用方用 meow_ui_vol_dac_write 做。 */ +int meow_ui_vol_step(const char *path, int dir); + +/* 写真实 DAC(CS43198, numid 1/2, pct→0-255),与 launcher meow_dac_vol_write / watchdog + * wd_vol_write 同路径(amixer cset = SNDRV_CTL_IOCTL_ELEM_WRITE)。无副作用前提:宿主上 + * amixer 不存在 → system() 静默失败(拼音量的宿主单测只测 meow_ui_vol_step,不测写 DAC)。 */ +void meow_ui_vol_dac_write(int pct); + #endif /* MEOW_UI_VOL_H */ diff --git a/test_sdk_input_keys b/test_sdk_input_keys new file mode 100755 index 0000000000000000000000000000000000000000..15f72da6c1bacbff8d8ea059b6c53047e12f6d3b GIT binary patch literal 73704 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%0AeX-M246 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 Y^GC(4bm?7CG5g1+2+c7L1Bb}|7rz`4O8@`> literal 0 HcmV?d00001 diff --git a/test_sdk_input_keys.c b/test_sdk_input_keys.c new file mode 100644 index 0000000..bf93ee0 --- /dev/null +++ b/test_sdk_input_keys.c @@ -0,0 +1,123 @@ +/* + * test_sdk_input_keys.c — M2.55 宿主验证:SDK 输入层接管系统按键语义(App 前台也生效)。 + * + * 覆盖(直接调输入层语义函数,等效"模拟按键事件"): + * K1 音量 +/step:audio.volume 迈一步(volume_step 约束),settings.conf 落到值改变; + * K2 音量 ± clamp:volume_max 软上限、0 下限、硬上限 100;到限不再写; + * K3 保留其它键:改 audio.volume 不整文件 O_TRUNC(display.brightness/注释保留); + * K4 媒体键本地回调:注册 playctl 回调后,sys_key(上一曲/下一曲/播放暂停) → 回调 cmd 正确 + * (播放/暂停按最近上报 state 切换:播放中→暂停(2),暂停/停止→播放(1)); + * K5 媒体键 IPC 兜底:未注册回调(g_cb==NULL)→ 走 IPC 发 launcher(无 socket → 静默不崩)。 + * + * 编译(宿主 gcc,仅链 vol/media 两个 TU;无需 meow_ui 其它子系统): + * gcc -Wall -Wextra -pthread -o test_sdk_input_keys test_sdk_input_keys.c \ + * meow_ui_vol.c meow_ui_media.c -I. -I../meow_launcherd + */ +#include "meow_ui_vol.h" +#include "meow_ui_media.h" + +#include +#include +#include +#include +#include + +static int failures = 0; +static void check(const char *name, int cond) +{ + printf("%s %s\n", cond ? "PASS" : "FAIL", name); + if (!cond) failures++; +} + +static char g_path[64]; +static void wfile(const char *content) +{ + FILE *f = fopen(g_path, "w"); + if (!f) { perror("fopen"); exit(2); } + fputs(content, f); + fclose(f); +} +static int vol_in_file(void) +{ + return meow_ui_vol_read(g_path, "audio.volume", -1); +} + +/* —— 媒体回调捕获 —— */ +static int cb_calls = 0, cb_cmd = 0; +static int64_t cb_seek = 0; +static void playctl_cb(int cmd, int64_t seek_ms) +{ cb_calls++; cb_cmd = cmd; cb_seek = seek_ms; } + +int main(void) +{ + snprintf(g_path, sizeof g_path, "/tmp/meow_kb_%d.conf", (int)getpid()); + char sock[64]; /* 指向不存在路径:让 IPC 兜底路径确定性静默失败 */ + snprintf(sock, sizeof sock, "/tmp/meow_kb_%d.sock", (int)getpid()); + setenv("MEOW_IPC_SOCK", sock, 1); + + /* ---- K1 音量迈一步(volume_step=1 默认)---- */ + wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 1\n"); + check("K1 vol 60 +1 -> 61", meow_ui_vol_step(g_path, +1) == 61); + check("K1 settings.conf 落到 61", vol_in_file() == 61); + check("K1 vol 61 -1 -> 60", meow_ui_vol_step(g_path, -1) == 60); + check("K1 settings.conf 回到 60", vol_in_file() == 60); + + /* ---- K2 步进 volume_step=4 + clamp ---- */ + wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 4\n"); + check("K2 step=4: 60 +1 -> 64", meow_ui_vol_step(g_path, +1) == 64); + wfile("audio.volume = 99\naudio.volume_max = 100\naudio.volume_step = 4\n"); + check("K2 step=4: 99 +1 -> 100 (硬上限)", meow_ui_vol_step(g_path, +1) == 100); + + /* volume_max 软上限 */ + wfile("audio.volume = 60\naudio.volume_max = 55\naudio.volume_step = 1\n"); + meow_ui_vol_step(g_path, +1); + check("K2 volume_max=55: vol -> 55 (clamp 上限)", vol_in_file() == 55); + + /* 到上限不再写 */ + wfile("audio.volume = 100\naudio.volume_max = 100\naudio.volume_step = 1\n"); + check("K2 at hard max: +1 -> 100 (no write)", meow_ui_vol_step(g_path, +1) == 100); + wfile("audio.volume = 0\naudio.volume_max = 100\naudio.volume_step = 1\n"); + check("K2 at min: -1 -> 0 (no write)", meow_ui_vol_step(g_path, -1) == 0); + check("K2 at min settings 保持 0", vol_in_file() == 0); + + /* ---- K3 保留其它键(不整文件 O_TRUNC)---- */ + wfile("# meow-settings 1\ndisplay.brightness = 100\naudio.volume = 60\n"); + meow_ui_vol_step(g_path, +1); + check("K3 vol 60 -> 61", vol_in_file() == 61); + check("K3 保留其它键 display.brightness=100", + meow_ui_vol_read(g_path, "display.brightness", -1) == 100); + + /* ---- K5 媒体键 IPC 兜底:未注册回调 → 走 IPC(无 socket 静默不崩)---- */ + cb_calls = 0; + meow_ui_media_sys_key(165); /* 上一曲:g_cb==NULL 走 IPC 兜底,不崩 */ + check("K5 未注册回调不崩", cb_calls == 0); + + /* ---- K4 媒体键本地回调:注册后按 cmd 分发 ---- */ + meow_ui_media_on_playctl(playctl_cb); + meow_ui_media_sys_key(165); /* 上一曲 */ + check("K4 上一曲(165) -> cb(3)", cb_calls == 1 && cb_cmd == 3 && cb_seek == 0); + meow_ui_media_sys_key(163); /* 下一曲 */ + check("K4 下一曲(163) -> cb(4)", cb_calls == 2 && cb_cmd == 4); + meow_ui_media_sys_key(19); /* 旧码上一曲 */ + check("K4 旧码(19) -> cb(3)", cb_calls == 3 && cb_cmd == 3); + + /* 播放/暂停按最近上报 state 切换 */ + meow_ui_media_meta(1, 0, 0, "t", "a", "al", NULL, NULL); /* state=PLAYING */ + meow_ui_media_sys_key(164); + check("K4 播放中按播放/暂停 -> cb(2) 暂停", cb_calls == 4 && cb_cmd == 2); + meow_ui_media_meta(2, 0, 0, "t", "a", "al", NULL, NULL); /* state=PAUSED */ + meow_ui_media_sys_key(164); + check("K4 暂停中按播放/暂停 -> cb(1) 播放", cb_calls == 5 && cb_cmd == 1); + meow_ui_media_meta(0, 0, 0, "t", "a", "al", NULL, NULL); /* state=STOPPED */ + meow_ui_media_sys_key(164); + check("K4 停止中按播放/暂停 -> cb(1) 播放", cb_calls == 6 && cb_cmd == 1); + + meow_ui_media_stop(); + + unlink(g_path); + unlink(sock); + + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +}