From 318e6ec7c1a9b43acf59b0cbc9d24fd54795445b Mon Sep 17 00:00:00 2001 From: MeowOS Dev Date: Sun, 30 Aug 2026 03:27:24 +0800 Subject: [PATCH] =?UTF-8?q?WIP=20checkpoint=20(M2.53=E2=86=92M2.54=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=BA):=20=E5=AD=97=E4=BD=93=E5=BD=92Meow?= =?UTF-8?q?OS/fonts+SDK=E8=B7=9F=E9=9A=8F+=E5=A4=9A=E5=AD=97=E4=BD=93(?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=B8=AD)+boot=E6=9D=BF=E8=BD=BD+=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A0=8F28+=E4=BA=AE=E5=BA=A6/=E9=9F=B3=E9=87=8F?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+=E6=96=87=E6=A1=A3=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_sdk.sh | 6 +- meow_notify_ipc.c | 243 ++++++++++++++++++++++++++++++++ meow_notify_ipc.h | 44 ++++++ meow_ui.h | 2 +- meow_ui_gfx.c | 51 ++++++- meow_ui_input.c | 4 +- meow_ui_notify.c | 78 ++++++++--- meow_ui_sys.c | 33 ++--- meow_ui_sys.h | 12 +- meow_ui_vol.c | 52 +++++++ meow_ui_vol.h | 32 +++++ test_meow_ui_vol.c | 145 ++++++++++++++++++++ test_notify_integration.c | 202 +++++++++++++++++++++++++++ test_notify_ipc.c | 281 ++++++++++++++++++++++++++++++++++++++ test_notify_post.c | 150 ++++++++++++++++++++ 15 files changed, 1289 insertions(+), 46 deletions(-) create mode 100644 meow_notify_ipc.c create mode 100644 meow_notify_ipc.h create mode 100644 meow_ui_vol.c create mode 100644 meow_ui_vol.h create mode 100644 test_meow_ui_vol.c create mode 100644 test_notify_integration.c create mode 100644 test_notify_ipc.c create mode 100644 test_notify_post.c diff --git a/build_sdk.sh b/build_sdk.sh index 7d502e4..6589f06 100755 --- a/build_sdk.sh +++ b/build_sdk.sh @@ -9,10 +9,12 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架 rm -f meow_ui.a *.o -OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys stb_image" +# -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移) +OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys meow_ui_vol stb_image" +CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd" for f in $OBJS; do echo "== cc $f ==" - "$CC" -Os -static -c "$f.c" -o "$f.o" + "$CC" $CFLAGS -c "$f.c" -o "$f.o" done # meow_font.o(复用 launcher 字库,打进 meow_ui.a → 应用只链一个库) diff --git a/meow_notify_ipc.c b/meow_notify_ipc.c new file mode 100644 index 0000000..a4c768b --- /dev/null +++ b/meow_notify_ipc.c @@ -0,0 +1,243 @@ +/* + * meow_notify_ipc.c — MeowOS SDK 通知 → launcher IPC 桥(任务 #5,SDK↔IPC 关键断点)。 + * + * 让 meow_notify_send() 的通知真正经 AF_UNIX IPC 送到 launcher 中央存储(meow_notif_post) + * 并回读 MSG_NOTIFY_ACK 拿 launcher 分配的通知 id(nid)。协议帧与负载结构复用 launcher 的 + * meow_ipc.h(同源一份、杜绝两端漂移): + * - 帧头 16B:magic=0x4D4F5701 / version=1 / type / req_id / len(全小端 LE,mipsel 原生); + * - MSG_NOTIFY(0x0020) ↑ 负载 = m_notify_t;MSG_NOTIFY_ACK(0x0021) ↓ 负载 = m_ack_t(status=nid); + * - MSG_NOTIFY_DISMISS(0x0022) ↑ 负载 = uint32 nid。 + * + * 设计要点: + * - 只做「一次性 connect + send + read ACK」,不维护连接/订阅循环(与 meow_ipc.c 客户端同型); + * - 映射层(SDK 枚举 ↔ IPC 字段)集中在本文件:分类枚举 CAT_* ↔ 字符串 + * "general"/"message"/"email"/"event"/"progress"/"media";timeout_ms → expire_timeout: + * >0 该毫秒、0=不自动过期、-1(ONGOING)=0xFFFFFFFF(并置 ongoing=1);媒体类型置 media=1; + * - 优雅降级:无 socket(connect 失败)/写失败/ACK 超时(SO_RCVTIMEO)/非 ACK 应答 → + * 返回 0(调用方 meow_ui_notify.c 保留本地 id 兜底),绝不崩; + * - 线程安全:app_id 经 pthread_once 缓存;socket 路径每次调用读 env MEOW_IPC_SOCK 覆盖 + * (宿主/test 用),缺省 meow_ipc.h 的 MEOW_IPC_PATH(/usr/data/meow_ipc.sock)。 + * + * 约定:与 meow_ui_notify.c 同风格(M2.x 中文注释);本文件为纯传输层,不调 meow_log, + * 失败诊断由调用方 meow_ui_notify.c 的 notify_deliver() 统一打点。 + */ +#include "meow_notify_ipc.h" +#include "meow_ipc.h" /* launcher 端共享协议:帧头/m_notify_t/m_ack_t/MSG_* —— 防漂移 */ + +#include /* snprintf */ +#include /* getenv */ +#include +#include /* EINTR */ +#include +#include +#include +#include +#include + +/* ACK 读超时(ms):launcher 主循环为 IPC 单写者(eventfd 唤醒,正常 <10ms 响应), + * 放宽容错 500ms;超时视为降级(返回 0,不崩)。 */ +#define MEOW_NOTIFY_IPC_TIMEOUT_MS 500 + +/* app_id(env MEOW_LOG_APP,缺省 "app";与 meow_ui_log.c 的取值口径一致)。 */ +static pthread_once_t g_once = PTHREAD_ONCE_INIT; +static char g_app_id[MEOW_APP_ID_MAX] = "app"; + +static void ipc_init(void) +{ + const char *s = getenv("MEOW_LOG_APP"); + if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s); +} + +/* socket 路径:优先 env MEOW_IPC_SOCK(宿主/qemu sim 覆盖),缺省 MEOW_IPC_PATH。 */ +static const char *ipc_sock_path(void) +{ + const char *s = getenv("MEOW_IPC_SOCK"); + return (s && s[0]) ? s : MEOW_IPC_PATH; +} + +const char *meow_notify_ipc_app_id(void) +{ + pthread_once(&g_once, ipc_init); + return g_app_id; +} + +/* ============================================================ */ +/* 映射层: SDK meow_notify 枚举/字段 ↔ IPC m_notify_t 字段 */ +/* ============================================================ */ + +/* SDK 分类枚举 → IPC 字符串(与 meow_ui.h §9.1 的 MEOW_NOTIFY_CAT_* 序号对齐; + * 越界钳位为 "general"——同 meow_notify_category_str 的防御语义)。 */ +static const char *cat_to_str(meow_notify_category cat) +{ + static const char *t[] = { "general", "message", "email", "event", "progress", "media" }; + if ((int)cat >= 0 && (int)cat < (int)MEOW_NOTIFY_CAT_COUNT) return t[cat]; + return t[MEOW_NOTIFY_CAT_GENERAL]; +} + +/* SDK timeout_ms → IPC expire_timeout(存储/显示端语义): + * >0 → 该毫秒数后过期;0 → 不自动过期(NEVER);<0 → 常驻(ONGOING)= 0xFFFFFFFF。 */ +static uint32_t timeout_to_expire(int timeout_ms) +{ + if (timeout_ms < 0) return 0xFFFFFFFFu; /* ONGOING:常驻,不自动过期 */ + return (uint32_t)timeout_ms; /* 0(NEVER)/ >0(毫秒) */ +} + +/* 构建 IPC 负载 m_notify_t(app_icon/actions/hints v1 留空,M2b D-Bus 桥再填)。 + * 仅在持有调用者互斥之外、发送前调用(纯 CPU 拷贝,不阻塞)。 */ +static void build_m_notify(const meow_notify *n, uint32_t replaces_id, m_notify_t *mn) +{ + memset(mn, 0, sizeof *mn); + snprintf(mn->app_id, sizeof mn->app_id, "%s", meow_notify_ipc_app_id()); + snprintf(mn->category, sizeof mn->category, "%s", cat_to_str(n->cat)); + snprintf(mn->summary, sizeof mn->summary, "%s", n->title ? n->title : ""); + snprintf(mn->body, sizeof mn->body, "%s", n->body ? n->body : ""); + snprintf(mn->target_activity, sizeof mn->target_activity, "%s", + n->target_activity ? n->target_activity : ""); + mn->replaces_id = replaces_id; /* 0=新建;>0=更新(不存在则落为新建,见 launcher) */ + mn->expire_timeout = timeout_to_expire(n->timeout_ms); + mn->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress); + mn->media = (n->cat == MEOW_NOTIFY_CAT_MEDIA) ? 1 : 0; + mn->ongoing = (n->timeout_ms == MEOW_NOTIFY_TIMEOUT_ONGOING) ? 1 : 0; +} + +/* ============================================================ */ +/* 传输层(AF_UNIX SOCK_STREAM;一次性 connect + send + read) */ +/* ============================================================ */ + +/* 连接 socket。失败返回 -1(不崩;调用方降级)。 */ +static int conn_path(const char *path) +{ + 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, path, sizeof a.sun_path - 1); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { + close(fd); + return -1; + } + return fd; +} + +/* 写满 n 字节(EINTR 重试);任一次写失败返回 0。 */ +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 0; } + if (w == 0) return 0; + off += (size_t)w; + } + return 1; +} + +/* 读满 n 字节(超时/断开返回 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 0; } + if (r == 0) return 0; + off += (size_t)r; + } + return 1; +} + +/* 组装并写出 16B(LE)帧头 + 负载。 */ +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)) return 0; + if (len && payload && !write_all(fd, payload, len)) return 0; + return 1; +} + +/* 给 conn 设 ACK 读超时(失败忽略——超时降级由 read_all 兜底)。 */ +static void set_ack_timeout(int fd) +{ + struct timeval tv; + tv.tv_sec = MEOW_NOTIFY_IPC_TIMEOUT_MS / 1000; + tv.tv_usec = (MEOW_NOTIFY_IPC_TIMEOUT_MS % 1000) * 1000; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); +} + +uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id) +{ + m_notify_t mn; + int fd; + if (!n) return 0; + build_m_notify(n, replaces_id, &mn); + + fd = conn_path(ipc_sock_path()); + if (fd < 0) return 0; /* 无 socket:优雅降级 */ + set_ack_timeout(fd); + + if (!send_frame(fd, MSG_NOTIFY, 0x4E4F5449u /* "NOTI" */, &mn, (uint32_t)sizeof mn)) { + close(fd); /* 写失败:降级 */ + return 0; + } + + /* 读回 16B 应答头 + m_ack_t,取 launcher 分配的通知 id(a.status)。 */ + uint8_t rhdr[MEOW_IPC_HDR_LEN]; + uint32_t nid = 0; + if (read_all(fd, rhdr, sizeof rhdr)) { + uint32_t rmagic, rlen; + uint16_t rver, rtype; + memcpy(&rmagic, rhdr + 0, 4); + memcpy(&rver, rhdr + 4, 2); + memcpy(&rtype, rhdr + 6, 2); + memcpy(&rlen, rhdr + 12, 4); + if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION && + rtype == MSG_NOTIFY_ACK && rlen >= sizeof(m_ack_t)) { + uint8_t pay[sizeof(m_ack_t)]; + if (read_all(fd, pay, sizeof(m_ack_t))) { + m_ack_t a; + memcpy(&a, pay, sizeof a); + if (a.code == MEOW_OK) nid = a.status; /* 落库成功 → nid */ + } + } + } + close(fd); + return nid; /* 非 ACK/超时 → 0(降级) */ +} + +void meow_notify_ipc_dismiss(uint32_t nid) +{ + int fd; + if (nid == 0) return; + fd = conn_path(ipc_sock_path()); + if (fd < 0) return; /* 无 socket:静默 */ + set_ack_timeout(fd); + if (!send_frame(fd, MSG_NOTIFY_DISMISS, 0x4E4F5444u /* "NOTD" */, &nid, (uint32_t)sizeof nid)) { + close(fd); /* 写失败:静默 */ + return; + } + /* 读回应答(launcher 对 DISMISS 回 MSG_ACK,见 meow_ipc.c cmd_dismiss)。虽为 + * best-effort,但读回确保 launcher 已处理完撤下再关连接(避免写后即关的丢帧竞态)。 */ + uint8_t rhdr[MEOW_IPC_HDR_LEN]; + if (read_all(fd, rhdr, sizeof rhdr)) { + uint32_t rmagic, rlen; + uint16_t rver, rtype; + memcpy(&rmagic, rhdr + 0, 4); + memcpy(&rver, rhdr + 4, 2); + memcpy(&rtype, rhdr + 6, 2); + memcpy(&rlen, rhdr + 12, 4); + if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION && + rtype == MSG_ACK && rlen >= sizeof(m_ack_t)) { + uint8_t pay[sizeof(m_ack_t)]; + (void)read_all(fd, pay, sizeof(m_ack_t)); /* 读到即确认;内容不关心 */ + } + } + close(fd); +} diff --git a/meow_notify_ipc.h b/meow_notify_ipc.h new file mode 100644 index 0000000..c1da243 --- /dev/null +++ b/meow_notify_ipc.h @@ -0,0 +1,44 @@ +/* + * meow_notify_ipc.h — MeowOS SDK 通知 → launcher IPC 桥(内部头,不随 meow_ui.h 发布给应用)。 + * + * 定位(任务 #5 SDK↔IPC 桥关键断点):让框架 app 调 meow_notify_send() 的通知真正经 + * AF_UNIX IPC 送到 launcher(meow_launcherd)中央存储(meow_notif.c 的 meow_notif_post), + * 通知中心(阶段 B · M5)才能显示。消息通道(FRAMEWORK_DESIGN §1 / meow_ipc.h): + * - MSG_NOTIFY 0x0020 ↑:推送/更新通知(负载 m_notify_t); + * - MSG_NOTIFY_ACK 0x0021 ↓:落库结果(m_ack_t,status=launcher 分配的通知 id nid); + * - MSG_NOTIFY_DISMISS 0x0022 ↑:撤下通知(负载 uint32 nid)。 + * + * 约定:只做「一次性发送 + 读 ACK」,不造 IPC 订阅循环;SDK 本地活动表(meow_ui_notify.c) + * 保留(app 进程内即时查询/配额自检),真正的通知中心数据在 launcher 中央存储,二者以 + * 本接口经 IPC 同步。全部失败路径优雅降级(无 socket/写失败/超时 → 返回 0,不崩)。 + * + * 本接口只暴露「SDK 类型出/进 + 状态码」,不把 m_notify_t / 帧格式泄漏给 meow_ui_notify.c; + * 映射层(SDK 枚举 ↔ IPC 字符串/uint32)实现在 meow_notify_ipc.c 内部。 + */ +#ifndef MEOW_NOTIFY_IPC_H +#define MEOW_NOTIFY_IPC_H + +#include +#include "meow_ui.h" /* meow_notify */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* 当前 app 的 app_id(env MEOW_LOG_APP,缺省 "app";pthread_once 缓存,线程安全)。 */ +const char *meow_notify_ipc_app_id(void); + +/* 把一条 SDK meow_notify 映射为 m_notify_t 并一次性发送 MSG_NOTIFY,阻塞读 + * MSG_NOTIFY_ACK 取 launcher 分配的通知 id(nid)。replaces_id:0=新建;>0=更新已存在 + * 通知(与 freedesktop 语义一致)。成功返回 nid(>0);无 socket/写失败/超时/非 ACK → + * 返回 0(调用方保留本地 id 兜底,不崩)。 */ +uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id); + +/* 一次性发送 MSG_NOTIFY_DISMISS 撤下 launcher 侧通知。best-effort:写失败/无 socket + * 静默(不崩、不回读 ACK)。 */ +void meow_notify_ipc_dismiss(uint32_t nid); + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_NOTIFY_IPC_H */ diff --git a/meow_ui.h b/meow_ui.h index b4f0552..bca5b25 100644 --- a/meow_ui.h +++ b/meow_ui.h @@ -32,7 +32,7 @@ extern "C" { #define MEOW_KEY_VOLUMEDOWN 114 #define MEOW_KEY_VOLUMEUP 115 #define MEOW_KEY_PLAYPAUSE 164 -#define MEOW_KEY_PREVIOUS 19 /* KEY_PREVIOUSSONG */ +#define MEOW_KEY_PREVIOUS 165 /* KEY_PREVIOUSSONG=165(上一曲;19 是另一码,勿混用) */ #define MEOW_KEY_NEXT 163 /* KEY_NEXTSONG */ /* 滑动方向(on_swipe 的 dir 参数) */ diff --git a/meow_ui_gfx.c b/meow_ui_gfx.c index 6397fef..83a6abe 100644 --- a/meow_ui_gfx.c +++ b/meow_ui_gfx.c @@ -29,10 +29,14 @@ #define FBIO_WAITFORVSYNC 0x4620 #define FB_DEV "/dev/fb0" -#define MEOW_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowAPPs/fonts/font.png" -#define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowAPPs/fonts/font.idx" +#define MEOW_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/font.png" +#define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/font.idx" #define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png" #define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx" +/* 默认字库(卡 MeowOS/fonts/default)与固件内置之外,launcher 会把「当前生效字体」 + * 经环境变量下发给 app(见 meow_launcherd.c launch_app / meow_font_effective_paths): + * MEOW_FONT_PNG / MEOW_FONT_IDX = 绝对路径;MEOW_FONT_APP = 或 "default"。 + * SDK load_font 优先读 env(匹配系统 font.file),无则回退卡默认 → 固件。 */ #define MUI_DEFAULT_W 480 #define MUI_DEFAULT_H 720 @@ -572,13 +576,55 @@ int meow_gfx_is_device(void) { return g_fb_mem != NULL; } int meow_gfx_mfont_ready(void) { return g_mfont_ready; } /* ================= 初始化/销毁 ================= */ +/* 字库加载优先级(M2.x): + * 1) 环境变量 MEOW_FONT_PNG/MEOW_FONT_IDX(launcher 注入的「当前生效字体」= 系统 font.file) + * —— app 未自带字库(§5.7 load_handle 未实现)时跟随系统 font.file; + * 2) 卡默认 MeowOS/fonts/default/font.png(不再硬编码 MeowAPPs/fonts); + * 3) 固件 /usr/share/meowos/font/font.png。 + * 全失败 → g_mfont_ready=0,text_draw 回退 5x7 点阵(系统不死)。 */ static int load_font(void) { + const char *ep = getenv("MEOW_FONT_PNG"); + const char *ei = getenv("MEOW_FONT_IDX"); + if (ep && ep[0] && ei && ei[0] && meow_font_load(ep, ei) == 0) return 1; if (meow_font_load(MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) return 1; if (meow_font_load(MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) return 1; return 0; } +/* M2.5x 多字体:读 launcher 注入的环境变量,构建 6 字符类 → 字库的映射(同 family 去重、 + * ≤4 套上限由 meow_font_config_multifont 统一处理)。launcher 注入(见 meow_launcherd.c + * launch_app):MEOW_MULTI_FONT=0/1;MEOW_FONT_PNG/IDX=全局(保留);每类 + * MEOW_FONT_CLS__PNG/IDX(命中则覆盖全局;未注入 = 该类跟随全局)。 + * 仅当全局字库已装载(g_mfont_ready)才配置;关/无 env → 单字体,零额外开销。 */ +static void meow_gfx_load_multifont(void) +{ + static const char *cls_env[MEOW_FCLS_COUNT][2] = { + { "MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_IDX" }, + { "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_CJK_SYM_IDX" }, + { "MEOW_FONT_CLS_NUM_PNG", "MEOW_FONT_CLS_NUM_IDX" }, + { "MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_IDX" }, + { "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_LATIN_SYM_IDX" }, + { "MEOW_FONT_CLS_OTHER_PNG", "MEOW_FONT_CLS_OTHER_IDX" }, + }; + const char *emf = getenv("MEOW_MULTI_FONT"); + int on = emf && emf[0] == '1'; + const char *gp = getenv("MEOW_FONT_PNG"); + const char *gi = getenv("MEOW_FONT_IDX"); + meow_font_class_cfg cfg[MEOW_FCLS_COUNT]; + memset(cfg, 0, sizeof cfg); + for (int c = 0; c < MEOW_FCLS_COUNT; c++) { + const char *p = getenv(cls_env[c][0]); + const char *idx = getenv(cls_env[c][1]); + if (p && p[0] && idx && idx[0]) { cfg[c].png = p; cfg[c].idx = idx; } + } + int distinct = 0, truncated = 0; + if (meow_font_config_multifont(on, gp, gi, cfg, &distinct, &truncated) != 0) + meow_log(MEOW_LOG_WARN, "multi_font: config failed, single-font fallback"); + else if (truncated) + meow_log(MEOW_LOG_WARN, "multi_font: >%d distinct fonts -> capped (more memory!)", distinct); +} + int meow_ui_init(const meow_ui_opts *opts) { if (g_init_ok) return 0; @@ -623,6 +669,7 @@ int meow_ui_init(const meow_ui_opts *opts) meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); g_mfont_ready = load_font(); + if (g_mfont_ready) meow_gfx_load_multifont(); /* M2.5x 多字体(读 env;关/无 env = 单字体) */ const char *dp = getenv("MEOW_UI_DUMP"); if (dp && *dp) g_dump_path = strdup(dp); diff --git a/meow_ui_input.c b/meow_ui_input.c index aa55289..fed0fc6 100644 --- a/meow_ui_input.c +++ b/meow_ui_input.c @@ -219,9 +219,9 @@ static void *input_thread(void *arg) 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 == 165) { + code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 19) { /* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 / - * 上一曲 19,165 / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费, + * 上一曲 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。 */ diff --git a/meow_ui_notify.c b/meow_ui_notify.c index 2a02957..dbad728 100644 --- a/meow_ui_notify.c +++ b/meow_ui_notify.c @@ -12,15 +12,20 @@ * (替换);媒体类型单例(每 app 仅保留最近 1 条,重发替换——可多媒体 App)。 * - 超时语义 [实现定案]:timeout_ms > 0 为该毫秒数后过期;0 = 不自动过期(直到 * meow_notify_cancel);-1 = 常驻(ONGOING),同样不自动过期。 - * - 落点:本阶段内部维护活动通知表 + 用既有 meow_log() 打日志作为落点;真实投递到 - * 通知中心 UI / D-Bus 是后续任务——meow_notify 结构已对齐 freedesktop 字段 - * (summary/body/…)为此预留,但不写死后端。 + * - 落点(任务 #5 SDK↔IPC 桥):本表登记后,经 IPC 把 MSG_NOTIFY(0x0020) 送到 + * launcher 中央存储(meow_notif.c 的 meow_notif_post),回读 MSG_NOTIFY_ACK(0x0021) + * 取 launcher 分配的通知 id(nid,存进 slot->ipc_id,供后续 replaces_id/dismiss 用)。 + * 通知中心 UI / D-Bus / TF 转储由 launcher 侧处理;SDK 本地活动表保留(app 进程内 + * 即时查询/配额自检)。无 socket/写失败/超时 → 优雅降级(保留本地 id 兜底,不崩)。 + * meow_notify 结构已对齐 freedesktop 字段(summary/body/…)为此预留。 * * 线程安全:pthread_mutex(同 meow_ui_log.c,launcher 前后台进程都会在已链 -lpthread - * 的应用里调用)。不依赖 /dev/fb0、不依赖板子,可宿主编译。内部只复用 - * meow_ui_time_ms()(单调毫秒)与 meow_log()(统一日志)两个既有 SDK 能力。 + * 的应用里调用),且 IPC 发送/读 ACK 在持锁之外进行(避免 socket I/O 阻塞其它查询)。 + * 不依赖 /dev/fb0、不依赖板子,可宿主编译。内部只复用 meow_ui_time_ms()(单调毫秒)、 + * meow_log()(统一日志)与 meow_notify_ipc_*(IPC 桥,纯传输层)三个既有/新增 SDK 能力。 */ #include "meow_ui.h" +#include "meow_notify_ipc.h" /* 任务 #5:通知 → launcher 中央存储的 IPC 桥(映射+传输) */ #include #include @@ -32,7 +37,8 @@ /* 活动通知槽(内部;标题/正文等复制副本,不持 app 外部指针,避免栈字符串悬垂) */ typedef struct { - uint32_t id; /* 唯一 id;0=空闲槽 */ + uint32_t id; /* 本地唯一 id(app 侧句柄);0=空闲槽 */ + uint32_t ipc_id; /* launcher 中央存储分配的通知 id(nid);0=未同步/降级 */ int active; /* 1=活动(未取消未过期),0=空闲 */ meow_notify_category cat; meow_notify_urgency urg; @@ -85,6 +91,7 @@ static void notify_purge_expired(void) if (s->timeout_ms > 0 && (now - s->created_ms) >= (uint64_t)s->timeout_ms) { s->active = 0; s->id = 0; + s->ipc_id = 0; } } } @@ -93,6 +100,7 @@ static void notify_purge_expired(void) static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uint64_t now) { s->id = id; + s->ipc_id = 0; /* 初值=未同步;发送后由 notify_deliver 回填 launcher 分配的 nid */ s->active = 1; s->cat = n->cat; s->urg = ((int)n->urgency >= 0 && (int)n->urgency < (int)MEOW_NOTIFY_URG_COUNT) @@ -106,12 +114,23 @@ static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uin s->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress); } -/* 落点:本阶段用统一日志标出一条投递;真实后端(通知中心 UI / D-Bus)后续替换此处。 */ -static void notify_deliver(const notify_slot_t *s, const char *ev) +/* 落点(任务 #5):把通知经 IPC MSG_NOTIFY(0x0020) 送到 launcher 中央存储 + * (meow_notif.c 的 meow_notif_post),回读 MSG_NOTIFY_ACK(0x0021) 取 launcher 分配的 + * 通知 id(nid)。返回 nid(>0=成功);无 socket/写失败/超时 → 返回 0(调用方保留本地 id + * 兜底,不崩)。调用方须在持锁外调用(socket I/O 不占 g_mu,避免阻塞其它查询)。 + * replaces_id:0=新建;>0=更新 launcher 侧已存在通知(freedesktop 语义)。 */ +static uint32_t notify_deliver(const meow_notify *n, uint32_t replaces_id) { - meow_log(MEOW_LOG_INFO, "[notify] %s id=%u type=%s urgency=%s title=%s", - ev, (unsigned)s->id, meow_notify_category_str(s->cat), - meow_notify_urgency_str(s->urg), s->title); + uint32_t nid = meow_notify_ipc_send(n, replaces_id); + if (nid) { + meow_log(MEOW_LOG_INFO, "[notify] delivered nid=%u replaces=%u cat=%s title=%s", + (unsigned)nid, (unsigned)replaces_id, meow_notify_category_str(n->cat), + n->title ? n->title : ""); + } else { + meow_log(MEOW_LOG_INFO, "[notify] degraded (no launcher) cat=%s title=%s", + meow_notify_category_str(n->cat), n->title ? n->title : ""); + } + return nid; } /* ============================================================ */ @@ -132,7 +151,7 @@ int meow_notify_register_type(meow_notify_category cat) uint32_t meow_notify_send(const meow_notify *n) { - uint32_t id; + uint32_t id, nid, replaces_id = 0; uint64_t now, oldest; int i, slot_idx, count, oldest_idx; @@ -155,11 +174,16 @@ uint32_t meow_notify_send(const meow_notify *n) if (n->cat == MEOW_NOTIFY_CAT_MEDIA) { /* 媒体单例:复用已存在的媒体槽(重发替换同 app 上一条媒体通知) */ for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) - if (g_slots[i].active && g_slots[i].cat == MEOW_NOTIFY_CAT_MEDIA) { slot_idx = i; break; } + if (g_slots[i].active && g_slots[i].cat == MEOW_NOTIFY_CAT_MEDIA) { + slot_idx = i; + replaces_id = g_slots[i].ipc_id; /* 更新该媒体通知(0=降级/未同步) */ + break; + } } else { count = notify_count_cat(n->cat); if (count >= MEOW_NOTIFY_MAX_PER_TYPE) { - /* 满则丢该类型最旧一条(替换) */ + /* 满则丢该类型最旧一条(更新):先取被丢最旧的 launcher nid 作 replaces_id, + * 再释放槽(顺序不可反,否则 ipc_id 已被清 0)。 */ oldest_idx = -1; oldest = UINT64_MAX; for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) @@ -167,7 +191,13 @@ uint32_t meow_notify_send(const meow_notify *n) oldest = g_slots[i].created_ms; oldest_idx = i; } - if (oldest_idx >= 0) { g_slots[oldest_idx].active = 0; g_slots[oldest_idx].id = 0; } + if (oldest_idx >= 0) { + replaces_id = g_slots[oldest_idx].ipc_id; /* 复用被丢最旧的 launcher nid */ + if (replaces_id == 0) replaces_id = 0; /* 未同步时按新建处理 */ + g_slots[oldest_idx].active = 0; + g_slots[oldest_idx].id = 0; + g_slots[oldest_idx].ipc_id = 0; + } } } if (slot_idx < 0) { @@ -184,26 +214,42 @@ uint32_t meow_notify_send(const meow_notify *n) id = g_next_id++; if (g_next_id == 0) g_next_id = 1; /* 防回绕 */ notify_fill(&g_slots[slot_idx], n, id, now); - notify_deliver(&g_slots[slot_idx], "send"); pthread_mutex_unlock(&g_mu); + + /* 落点(持锁外):IPC 送 launcher 中央存储,回读 nid;无 launcher 则降级(nid=0)。 */ + nid = notify_deliver(n, replaces_id); + + /* 回填 launcher 分配的 nid(槽可能已被并发 cancel/过期清掉——仅在时回填)。 */ + if (nid) { + pthread_mutex_lock(&g_mu); + for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) + if (g_slots[i].active && g_slots[i].id == id) { g_slots[i].ipc_id = nid; break; } + pthread_mutex_unlock(&g_mu); + } return id; } int meow_notify_cancel(uint32_t id) { int i, rc = 0; + uint32_t ipc_id = 0; if (id == 0) return 0; pthread_mutex_lock(&g_mu); notify_purge_expired(); for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) { if (g_slots[i].active && g_slots[i].id == id) { + ipc_id = g_slots[i].ipc_id; /* launcher 侧 nid;0=未同步/降级 */ g_slots[i].active = 0; g_slots[i].id = 0; + g_slots[i].ipc_id = 0; rc = 1; break; } } pthread_mutex_unlock(&g_mu); + /* 落点:经 IPC MSG_NOTIFY_DISMISS(0x0022) 撤下 launcher 中央存储对应通知 + * (best-effort;无 launcher/未同步则静默降级)。 */ + if (rc && ipc_id) meow_notify_ipc_dismiss(ipc_id); return rc; } diff --git a/meow_ui_sys.c b/meow_ui_sys.c index 07c5ef8..c271f5e 100644 --- a/meow_ui_sys.c +++ b/meow_ui_sys.c @@ -14,6 +14,7 @@ */ #include "meow_ui_sys.h" #include "meow_ui_internal.h" +#include "meow_ui_vol.h" #include #include @@ -23,7 +24,6 @@ #include #define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" -#define VOL_DEFAULT 60 /* 蓝牙 powered 缓存刷新间隔(hciconfig 较贵) */ #define BT_REFRESH_MS 30000 @@ -80,22 +80,15 @@ static int hci_powered(void) return up; } -/* 音量:读 settings.conf 的 audio.volume(launcher 同源,watchdog 写同文件)。 */ +/* 音量:读 settings.conf 的 audio.volume(launcher 同源,watchdog 写同文件)。 + * 与 launcher 口径一致:0-100,且尊重 audio.volume_max 上限 clamp(vol_clamp 同义)。 + * 注意 settings 落盘为 "key = value"(带空格),必须按 '=' 切分解析,不能朴素前缀匹配—— + * 否则永远读不到(回退默认 60,即「状态栏音量数字不对、卡在 60」的根因)。 */ static int settings_vol(void) { - FILE *f = fopen(MEOW_SETTINGS_FILE, "r"); - if (!f) return VOL_DEFAULT; - int vol = VOL_DEFAULT; - char buf[128]; - while (fgets(buf, sizeof buf, f)) { - if (!strncmp(buf, "audio.volume=", 13)) { - int v = atoi(buf + 13); - if (v >= 0 && v <= 100) vol = v; - break; - } - } - fclose(f); - return vol; + int vol = meow_ui_vol_read(MEOW_SETTINGS_FILE, "audio.volume", MEOW_UI_VOL_DEFAULT); + int vol_max = meow_ui_vol_read(MEOW_SETTINGS_FILE, "audio.volume_max", MEOW_UI_VOL_MAX_DEF); + return meow_ui_vol_clamp(vol, vol_max); } int meow_ui_sys_init(void) @@ -210,21 +203,25 @@ void meow_ui_sys_statusbar_render(void) /* 整条清底:覆盖 app 可能画进顶行的内容(z 最顶) */ meow_ui_fill_rect(0, 0, w, h, th->bg); + /* 文字垂直锚 y:(栏高-行高)/2 居中;下限 1 防极端行高 > 栏高时负 y 溢出顶端(不越界优先)。 */ + int ty = (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2; + if (ty < 1) ty = 1; + /* 时钟(左):每秒现取 localtime */ char timebuf[16]; time_t t = time(NULL); struct tm tm; localtime_r(&t, &tm); snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); - meow_ui_text(8, (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2, timebuf, MUI_SBAR_FONT, th->fg); + meow_ui_text(8, ty, timebuf, MUI_SBAR_FONT, th->fg); /* 右侧信息串:batt% WIFI 蓝牙 VOL nn% */ char right[96]; statusbar_right_str(right, sizeof right); if (right[0]) - meow_ui_text_right(w - 8, (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2, right, MUI_SBAR_FONT, th->fg); + meow_ui_text_right(w - 8, ty, right, MUI_SBAR_FONT, th->fg); - /* 分隔线(栏内最底 1px,主题次要色);内容 inset=statusbar_height()=40 从 y=40 起即干净 */ + /* 分隔线(栏内最底 1px,主题次要色);内容 inset=statusbar_height()=28 从 y=28 起即干净 */ meow_ui_fill_rect(0, h - 1, w, 1, th->dim); g_statusbar_force = 0; diff --git a/meow_ui_sys.h b/meow_ui_sys.h index 61561b3..8caeb68 100644 --- a/meow_ui_sys.h +++ b/meow_ui_sys.h @@ -10,7 +10,8 @@ * - 电量 /sys/class/power_supply/battery/{capacity,status} * - WiFi /sys/class/net/wlan0/operstate * - 蓝牙 实际 powered(hciconfig -a hci0 的 UP;spawn 贵 → 缓存 30s 刷一次) - * - 音量 /usr/data/meow/settings.conf 的 audio.volume(launcher 同文件) + * - 音量 /usr/data/meow/settings.conf 的 audio.volume(launcher 同文件; + * 0-100 并 clamp 到 audio.volume_max 上限,与 launcher vol_clamp 口径一致) * - 时钟 localtime_r(每秒在绘制时现取) */ #ifndef MEOW_UI_SYS_H @@ -28,9 +29,10 @@ typedef struct meow_sys_state { int vol; /* 音量 0-100 */ } meow_sys_state; -/* 顶部状态栏几何(与 launcher SB_BAR_H=40 / SB_FONT=28 对齐,视觉统一)。 */ -#define MUI_SBAR_H 40 -#define MUI_SBAR_FONT 28 +/* 顶部状态栏几何(与 launcher SB_BAR_H=28 / SB_FONT=20 对齐,视觉统一)。 + * 高度 40→28(用户定),字号 28→20(28px 栏放不下 28 号;20 号按 (高-行高)/2 垂直居中)。 */ +#define MUI_SBAR_H 28 +#define MUI_SBAR_FONT 20 /* 首次全量读取;失败仍返回 0(字段置 -1/0)。 */ int meow_ui_sys_init(void); @@ -44,7 +46,7 @@ const meow_sys_state *meow_ui_sys_get(void); /* 状态栏是否应绘制(= set_statusbar(1) 且 非 fullscreen)。 */ int meow_ui_sys_statusbar_active(void); -/* 状态栏高度(px):active 时 40,否则 0(供 meow_ui_statusbar_height 用)。 */ +/* 状态栏高度(px):active 时 28,否则 0(供 meow_ui_statusbar_height 用)。 */ int meow_ui_sys_statusbar_height(void); /* 自上次渲染后状态栏数据是否已变(1s 轻刷/显隐切换置位);读取即清除。 diff --git a/meow_ui_vol.c b/meow_ui_vol.c new file mode 100644 index 0000000..b428653 --- /dev/null +++ b/meow_ui_vol.c @@ -0,0 +1,52 @@ +/* + * meow_ui_vol.c — SDK 状态栏音量换算纯函数 + settings.conf 读取(A-0 核心数源)。 + * + * 单独成 TU 的目的: + * 1. 纯函数(clamp / 键读取)不依赖 meow_ui 其它子系统,宿主 gcc 直接可测 + * (test_meow_ui_vol.c),无需链整库/stub。 + * 2. 把「音量从 settings 读数并 clamp 到 volume_max」这一语义钉死在单点, + * 与 launcher 的 vol_clamp(meow_launcherd.c)口径对齐,杜绝两端音量漂移。 + * + * 数据源:/usr/data/meow/settings.conf 扁平 key=value。launcher meow_settings.c + * 落盘为 "key = value"(带空格),本解析兼容无空格/多空格,与 launcher 读循环 + * 及 sdk meow_ui_theme.c 的 get_setting_str 同款解析。 + */ +#include "meow_ui_vol.h" + +#include +#include +#include + +int meow_ui_vol_clamp(int vol, int vol_max) +{ + if (vol < 0) vol = 0; + if (vol > MEOW_UI_VOL_MAX) vol = MEOW_UI_VOL_MAX; + if (vol_max < 0) vol_max = 0; /* 异常上限兜底:不进负值 */ + if (vol > vol_max) vol = vol_max; + return vol; +} + +int meow_ui_vol_read(const char *path, const char *key, int def) +{ + if (!path || !key) return def; + FILE *f = fopen(path, "r"); + if (!f) return def; + int kl = (int)strlen(key); + char line[160]; + char *found = NULL; + while (fgets(line, sizeof line, f)) { + char *eq = strchr(line, '='); + if (!eq) continue; /* 非 key=value 行(注释/空行)跳过 */ + char *k = line; + while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */ + int n = (int)(eq - k); + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; /* key 右去空白 */ + if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* key 不精确匹配 */ + found = eq + 1; /* 命中所求键 */ + break; + } + fclose(f); + if (!found) return def; + while (*found == ' ' || *found == '\t') found++; /* value 左去空白;atoi 天然停于非数字 */ + return atoi(found); +} diff --git a/meow_ui_vol.h b/meow_ui_vol.h new file mode 100644 index 0000000..9a94bed --- /dev/null +++ b/meow_ui_vol.h @@ -0,0 +1,32 @@ +/* + * meow_ui_vol.h — SDK 状态栏音量换算/读取(meow_ui_vol.c)。 + * 不随 meow_ui.h 发布给应用;仅供 meow_ui_sys.c 与宿主单测(test_meow_ui_vol.c)使用。 + */ +#ifndef MEOW_UI_VOL_H +#define MEOW_UI_VOL_H + +/* 音量语义常量(与 launcher MEOW_VOL_MAX/DEFAULT、meow_settings 表逐一致): + * audio.volume 默认 60,取值域 0-100(硬上限 MEOW_UI_VOL_MAX); + * audio.volume_max 默认 100(软上限,clamp 上限,用户可调小); + * audio.volume_step 默认 1(跳步;只影响「每次音量键改多少」,与状态栏展示无关—— + * SDK 只读当前值,步进由改动方(launcher)承担)。 */ +#define MEOW_UI_VOL_DEFAULT 60 +#define MEOW_UI_VOL_MAX 100 +#define MEOW_UI_VOL_MAX_DEF 100 + +/* 纯函数:把软件音量夹到 [0, MEOW_UI_VOL_MAX],再夹到 volume_max 软上限。 + * 语义与 launcher 的 vol_clamp(meow_launcherd.c)一致: + * - 硬上限 100(0-100 口径); + * - 尊重 audio.volume_max(用户把上限调小则音量被夹到该值); + * - volume_max 异常(<0)兜底为 0(不进负值)。 + * 无文件/全局依赖,宿主 gcc 可直接测。 */ +int meow_ui_vol_clamp(int vol, int vol_max); + +/* 读 settings.conf 里一个 int 键。扁平 "key=value" 格式(launcher meow_settings.c + * 落盘为 "key = value",带空格;这里兼容无空格/多空格两种写式)。 + * 解析:'=' 切分,key/value 去首尾空白,key 精确匹配(避免 audio.volume 误配 + * audio.volume_max)。未命中/打开失败/键缺失回退 def。 + * path 由调用方传入(设备传导 MEOW_SETTINGS_FILE;宿主单测指向临时文件)。 */ +int meow_ui_vol_read(const char *path, const char *key, int def); + +#endif /* MEOW_UI_VOL_H */ diff --git a/test_meow_ui_vol.c b/test_meow_ui_vol.c new file mode 100644 index 0000000..cf1d7a3 --- /dev/null +++ b/test_meow_ui_vol.c @@ -0,0 +1,145 @@ +/* + * test_meow_ui_vol.c — 宿主单元测试:SDK 状态栏音量换算纯函数 + settings.conf 读取。 + * + * 直接编译 meow_ui_vol.c(纯函数,无 meow_ui 依赖),用临时 settings 文件验证: + * V1 解析:launcher 落盘 "key = value"(带空格)形式能正确读到值(根因回归—— + * 旧代码朴素前缀匹配 "audio.volume=" 永远读不到 → 卡默认 60); + * V2 解析:无空格 "key=value" / 多空格 / 头注释行 / 键缺失回退默认 / 精确匹配 + * (audio.volume 不误配 audio.volume_max); + * V3 clamp:volume_max 上限、硬上限 100、负值兜底 0、上限异常(<0)兜底 0; + * V4 管道(镜像 meow_ui_sys.c settings_vol):读 + clamp 到 volume_max; + * V5 展示串:与 meow_ui_sys.c statusbar_right_str 同款 "VOL %d%%",断言显示值即 + * 当前夹后音量(含 step 语义:a.volume_step 只决定步进,展示原样不/舍入)。 + * + * 编译(宿主 gcc,mipsel 无关): + * gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I. + */ +#include "meow_ui_vol.h" + +#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++; +} + +/* 写一份临时 settings 文件(lines 为已含 '\n' 的行内容;NULL 结束) */ +static const char *tmp_path; +static void write_settings(const char **lines) +{ + FILE *f = fopen(tmp_path, "w"); + if (!f) { perror("fopen"); exit(2); } + for (int i = 0; lines[i]; i++) fputs(lines[i], f); + fclose(f); +} + +/* 镜像 meow_ui_sys.c 的 settings_vol():读 audio.volume + volume_max,再 clamp。 */ +static int settings_vol_of(const char *path) +{ + 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); + return meow_ui_vol_clamp(vol, vol_max); +} + +/* 状态栏右侧 VOL 串组装(与 meow_ui_sys.c statusbar_right_str 同款)。 */ +static void vol_display(int vol, int vol_max, char *out, size_t n) +{ + snprintf(out, n, "VOL %d%%", meow_ui_vol_clamp(vol, vol_max)); +} + +int main(void) +{ + char base[64]; + snprintf(base, sizeof base, "/tmp/meow_vol_%d.conf", (int)getpid()); + tmp_path = base; + + /* ---- V1 解析:launcher 落盘格式 "key = value"(带空格)——根因回归 ---- */ + { + const char *L[] = { "# meow-settings 1\n", "audio.volume = 82\n", NULL }; + write_settings(L); + check("V1 launcher 空格格式 audio.volume=82 -> 82", + meow_ui_vol_read(tmp_path, "audio.volume", -1) == 82); + } + + /* ---- V2 解析:无空格 / 多空格 / 注释 / 缺键 / 精确匹配 ---- */ + { + const char *L[] = { "audio.volume=40\n", NULL }; + write_settings(L); + check("V2 无空格 audio.volume=40 -> 40", + meow_ui_vol_read(tmp_path, "audio.volume", -1) == 40); + + const char *L2[] = { " audio.volume = 7 \n", NULL }; + write_settings(L2); + check("V2 多空格 audio.volume = 7 -> 7", + meow_ui_vol_read(tmp_path, "audio.volume", -1) == 7); + + const char *L3[] = { "display.brightness = 100\n", NULL }; + write_settings(L3); + check("V2 键缺失 -> 默认 -1", meow_ui_vol_read(tmp_path, "audio.volume", -1) == -1); + + const char *L4[] = { "audio.volume_max = 90\n", NULL }; + write_settings(L4); + check("V2 audio.volume 不误配 audio.volume_max -> 默认", + meow_ui_vol_read(tmp_path, "audio.volume", -1) == -1); + check("V2 单独读 audio.volume_max=90 -> 90", + meow_ui_vol_read(tmp_path, "audio.volume_max", -1) == 90); + } + + /* ---- V3 clamp 纯函数 ---- */ + check("V3 clamp(60,100)=60", meow_ui_vol_clamp(60, 100) == 60); + check("V3 clamp(80,60)=60 (尊重 volume_max)", meow_ui_vol_clamp(80, 60) == 60); + check("V3 clamp(80,100)=80", meow_ui_vol_clamp(80, 100) == 80); + check("V3 clamp(150,100)=100 (硬上限)", meow_ui_vol_clamp(150, 100) == 100); + check("V3 clamp(150,80)=80", meow_ui_vol_clamp(150, 80) == 80); + check("V3 clamp(-5,100)=0 (负值兜底)", meow_ui_vol_clamp(-5, 100) == 0); + check("V3 clamp(60,0)=0 (上限 0)", meow_ui_vol_clamp(60, 0) == 0); + check("V3 clamp(60,-10)=0 (上限异常兜底)", meow_ui_vol_clamp(60, -10) == 0); + check("V3 clamp(120,255)=100 (硬上限优先于大上限)", meow_ui_vol_clamp(120, 255) == 100); + + /* ---- V4 管道:读 + clamp 到 volume_max(镜像 settings_vol)---- */ + { + const char *L[] = { "audio.volume = 82\n", "audio.volume_max = 100\n", NULL }; + write_settings(L); + check("V4 vol=82 max=100 -> 82", settings_vol_of(tmp_path) == 82); + + const char *L2[] = { "audio.volume = 82\n", "audio.volume_max = 60\n", NULL }; + write_settings(L2); + check("V4 vol=82 max=60 -> 60 (clamp)", settings_vol_of(tmp_path) == 60); + + const char *L3[] = { "audio.volume = 150\n", "audio.volume_max = 100\n", NULL }; + write_settings(L3); + check("V4 vol=150 max=100 -> 100 (硬上限)", settings_vol_of(tmp_path) == 100); + + const char *L4[] = { "audio.volume = -3\n", "audio.volume_max = 100\n", NULL }; + write_settings(L4); + check("V4 vol=-3 -> 0", settings_vol_of(tmp_path) == 0); + + const char *L5[] = { "display.brightness = 100\n", NULL }; /* 无音量键 */ + write_settings(L5); + check("V4 无 audio.volume -> 默认 60", settings_vol_of(tmp_path) == MEOW_UI_VOL_DEFAULT); + } + + /* ---- V5 展示串:VOL %d%(step 只决定步进,展示原样不/舍入)---- */ + { + char s[16]; + vol_display(60, 100, s, sizeof s); + check("V5 vol=60 -> \"VOL 60%\"", !strcmp(s, "VOL 60%")); + vol_display(80, 60, s, sizeof s); + check("V5 vol=80 max=60 -> \"VOL 60%\" (显示夹后值)", !strcmp(s, "VOL 60%")); + vol_display(-5, 100, s, sizeof s); + check("V5 vol=-5 -> \"VOL 0%\"", !strcmp(s, "VOL 0%")); + vol_display(37, 100, s, sizeof s); + check("V5 vol=37 -> \"VOL 37%\" (step 值原样展示)", !strcmp(s, "VOL 37%")); + } + + unlink(tmp_path); + + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +} diff --git a/test_notify_integration.c b/test_notify_integration.c new file mode 100644 index 0000000..6ea53d9 --- /dev/null +++ b/test_notify_integration.c @@ -0,0 +1,202 @@ +/* + * test_notify_integration.c — 宿主集成测试:meow_notify_send/cancel 全链路(任务 #5)。 + * + * 直接把 meow_ui_notify.c + meow_notify_ipc.c 编进来,仅 stub 两个 SDK 能力 + * (meow_log 空实现 / meow_ui_time_ms 单调毫秒),用本地 AF_UNIX stub socket 回环: + * + * S1 send(general,timeout=0) → 返回本地 id(1),stub 回 nid=101,active_count=1; + * S2 send(media) → media=1、category=media(经 stub 捕获帧校验); + * S3 同类型再发(未满配额)→ 返回本地 id(3); + * S4 cancel(本地id=1) → 返回 1,stub 收到 MSG_NOTIFY_DISMISS(负载 nid=101); + * S5 配额满(每类型4)后发第 5 条 → 丢最旧:该 5 条 send 的 replaces_id == 被丢最旧的 nid; + * S6 无 launcher(connect 失败)→ send 仍返回本地 id(>0)、不崩;cancel 也静默不崩。 + * + * 编译(宿主 gcc): + * gcc -o test_notify_integration test_notify_integration.c \ + * meow_ui_notify.c meow_notify_ipc.c -I. -I../meow_launcherd -lpthread + */ +#include "meow_ui.h" +#include "meow_notify_ipc.h" +#include "meow_ipc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- stub SDK 依赖(避免链整库) ---- */ +void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; } +uint64_t meow_ui_time_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; +} + +/* ---- stub server 捕获(每次连接一帧) ---- */ +#define MAXCAP 16 +typedef struct { int type; m_notify_t n; uint32_t dismiss_id; uint32_t nid; } srec_t; +static srec_t g_rec[MAXCAP]; +static int g_nrec = 0; +static int g_stop = 0; +static int g_listen_fd = -1; +static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; + +static void *server(void *arg) +{ + (void)arg; + while (!g_stop) { + int c = accept(g_listen_fd, NULL, NULL); + if (c < 0) { + if (errno == EAGAIN && g_stop) break; /* 非阻塞:listener 被关闭 */ + if (errno == EBADF) break; + usleep(1000); + continue; + } + uint8_t hdr[MEOW_IPC_HDR_LEN]; + size_t off = 0; + while (off < sizeof hdr) { ssize_t r = read(c, hdr + off, sizeof hdr - off); if (r <= 0) break; off += (size_t)r; } + if (off < sizeof hdr) { close(c); continue; } + uint32_t type, len, req_id; + memcpy(&req_id, hdr + 8, 4); + memcpy(&len, hdr + 12, 4); + memcpy(&type, hdr + 6, 2); + if (len > MEOW_IPC_MAX) { close(c); continue; } + uint8_t pay[MEOW_IPC_MAX]; + off = 0; + while (off < len) { ssize_t r = read(c, pay + off, len - off); if (r <= 0) break; off += (size_t)r; } + + pthread_mutex_lock(&g_mu); + int idx = g_nrec < MAXCAP ? g_nrec++ : (MAXCAP - 1); + memset(&g_rec[idx], 0, sizeof g_rec[idx]); + g_rec[idx].type = (int)type; + if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) memcpy(&g_rec[idx].n, pay, sizeof(m_notify_t)); + if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_rec[idx].dismiss_id, pay, 4); + /* 分配 nid = 100 + NOTIFY 序号 */ + uint32_t seq = 0; + for (int i = 0; i <= idx; i++) if (g_rec[i].type == MSG_NOTIFY) seq++; + g_rec[idx].nid = (type == MSG_NOTIFY) ? (100 + seq) : 0; + uint32_t ack_nid = g_rec[idx].nid; + pthread_mutex_unlock(&g_mu); + + if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS) { + m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; a.status = ack_nid; + uint8_t rhdr[MEOW_IPC_HDR_LEN]; + uint32_t mag = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION, t = (uint16_t) + (type == MSG_NOTIFY ? MSG_NOTIFY_ACK : MSG_ACK); uint32_t rl = sizeof a; + memcpy(rhdr+0,&mag,4); memcpy(rhdr+4,&ver,2); memcpy(rhdr+6,&t,2); memcpy(rhdr+8,&req_id,4); memcpy(rhdr+12,&rl,4); + size_t w0 = 0; while (w0 < sizeof rhdr) { ssize_t w = write(c, rhdr+w0, sizeof rhdr-w0); if (w<=0) break; w0 += (size_t)w; } + size_t w1 = 0; while (w1 < rl) { ssize_t w = write(c, (uint8_t*)&a+w1, rl-w1); if (w<=0) break; w1 += (size_t)w; } + } + close(c); + } + return NULL; +} + +static int g_listen_made = 0; +static int make_listen(char *path, size_t n) +{ + snprintf(path, n, "/tmp/meow_notify_intg_%d.sock", (int)getpid()); + unlink(path); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un a; memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; strncpy(a.sun_path, path, sizeof a.sun_path - 1); + if (bind(fd, (struct sockaddr*)&a, sizeof a) < 0 || listen(fd, 4) < 0) { close(fd); return -1; } + fcntl(fd, F_SETFL, O_NONBLOCK); /* 非阻塞 listener:accept() 可被 g_stop 截断 */ + g_listen_fd = fd; g_listen_made = 1; + return 0; +} +static void stop_server(void) +{ + if (g_listen_made) { g_stop = 1; close(g_listen_fd); g_listen_made = 0; } +} + +static srec_t rec_at(int i) +{ + srec_t r; pthread_mutex_lock(&g_mu); r = g_rec[i]; pthread_mutex_unlock(&g_mu); return r; +} + +static int failures = 0; +static void check(const char *name, int cond) +{ + printf("%s %s\n", cond ? "PASS" : "FAIL", name); + if (!cond) failures++; +} + +int main(void) +{ + setenv("MEOW_LOG_APP", "intgapp", 1); + char sock[108]; + if (make_listen(sock, sizeof sock) != 0) { fprintf(stderr, "cannot listen\n"); return 2; } + setenv("MEOW_IPC_SOCK", sock, 1); + pthread_t th; pthread_create(&th, NULL, server, NULL); + + /* S1 通用通知 */ + meow_notify nt; memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.urgency = MEOW_NOTIFY_URG_NORMAL; + nt.title = "t1"; nt.timeout_ms = 0; + uint32_t id1 = meow_notify_send(&nt); + check("S1 send 返回本地 id=1", id1 == 1); + check("S1 active_count=1", meow_notify_active_count() == 1); + check("S1 stub 回 nid=101", rec_at(0).nid == 101); + + /* S2 媒体通知:单例 + media=1 */ + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_MEDIA; nt.title = "music"; nt.timeout_ms = -1; + uint32_t id2 = meow_notify_send(&nt); + check("S2 媒体返本地 id=2", id2 == 2); + check("S2 category=media", !strcmp(rec_at(1).n.category, "media")); + check("S2 media=1", rec_at(1).n.media == 1); + check("S2 expire=0xFFFFFFFF", rec_at(1).n.expire_timeout == 0xFFFFFFFFu); + + /* S3 同类型再发(general 未满 4)→ 新本地 id */ + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.title = "t3"; nt.timeout_ms = 0; + uint32_t id3 = meow_notify_send(&nt); + check("S3 再发返本地 id=3", id3 == 3); + check("S3 第2条general nid=103", rec_at(2).nid == 103); + + /* S4 cancel(id1) → DISMISS nid=101 */ + int rc = meow_notify_cancel(id1); + check("S4 cancel 返回 1", rc == 1); + check("S4 stub 收到 MSG_NOTIFY_DISMISS", rec_at(3).type == MSG_NOTIFY_DISMISS); + check("S4 dismiss 负载 nid=101", rec_at(3).dismiss_id == 101); + check("S4 本地表已撤(count=2)", meow_notify_active_count() == 2); + + /* S5 配额满 → 丢最旧 + replaces_id 复用被丢 nid。 + 当前 general 有 id3(1 条);再发 4 条到 4 条(第 4 条触顶后丢最旧)。 */ + for (int i = 0; i < 4; i++) { + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.title = "quota"; nt.timeout_ms = 0; + (void)meow_notify_send(&nt); + } + /* 现在 general 应仍有 4 条(第5条丢最旧 id3=本地,即被降级替换) */ + check("S5 配额满后每类型仍=4", meow_notify_count_in_type(MEOW_NOTIFY_CAT_GENERAL) == 4); + /* 被丢最旧 = id3(第2条 general,本地 id=3),其 launcher nid=103(rec[2])。 + 触发丢最旧的是 S5 循环第 4 次 send = 第 7 条 NOTIFY(rec[7]),其 replaces_id==103。 */ + check("S5 丢最旧后 send replaces_id=被丢nid(103)", rec_at(7).n.replaces_id == 103); + + stop_server(); + pthread_join(th, NULL); + + /* S6 无 launcher 降级 */ + char dead[108]; snprintf(dead, sizeof dead, "/tmp/meow_notify_intg_dead_%d.sock", (int)getpid()); + unlink(dead); + setenv("MEOW_IPC_SOCK", dead, 1); + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_EMAIL; nt.title = "no-launcher"; nt.timeout_ms = 0; + uint32_t id6 = meow_notify_send(&nt); + check("S6 无 launcher 仍返回本地 id(>0)", id6 > 0); + check("S6 无 launcher active_count 正常", meow_notify_active_count() >= 1); + check("S6 cancel 无 launcher 静默不崩", meow_notify_cancel(id6) == 1); + + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +} diff --git a/test_notify_ipc.c b/test_notify_ipc.c new file mode 100644 index 0000000..f1ba227 --- /dev/null +++ b/test_notify_ipc.c @@ -0,0 +1,281 @@ +/* + * test_notify_ipc.c — 宿主测试:任务 #5 SDK↔IPC 桥(meow_notify_ipc.{h,c})。 + * + * 用本地 AF_UNIX stub socket(回环)验证: + * T1 枚举↔字符串映射:CAT_PROGRESS→"progress"、CAT_MEDIA→"media" + media=1; + * T2 timeout 映射:-1(ONGOING)→expire=0xFFFFFFFF + ongoing=1;0(NEVER)→expire=0; + * >0→该毫秒 + ongoing=0; + * T3 回读 MSG_NOTIFY_ACK 拿 launcher 分配的 nid;replaces_id 原样透传;progress 钳位; + * T4 无 socket 降级:connect 失败 → meow_notify_ipc_send 返回 0,不崩; + * T5 dismiss:meow_notify_ipc_dismiss(nid) 发出 MSG_NOTIFY_DISMISS 帧(负载=nid)。 + * + * 编译(宿主 gcc,不依赖 meow_ui.a 其余 TU): + * gcc -o test_notify_ipc test_notify_ipc.c meow_notify_ipc.c \ + * -I. -I../meow_launcherd -lpthread + * 运行:./test_notify_ipc (全部 PASS 退出码 0,任一 FAIL 退出码 1) + */ +#include "meow_notify_ipc.h" +#include "meow_ui.h" +#include "meow_ipc.h" /* 帧头 / m_notify_t / m_ack_t / MSG_*(同源校验) */ + +#include +#include +#include +#include +#include +#include +#include + +/* ---- 捕获:stub 服务端解析到的帧内容(mutex 保护;客户端返回后读取) ---- */ +typedef struct { + uint32_t type; /* 收到的帧类型 */ + m_notify_t n; /* MSG_NOTIFY 负载 */ + uint32_t dismiss_id; /* MSG_NOTIFY_DISMISS 负载 */ + uint32_t req_id; + uint32_t nid_to_return; /* ACK 中回填的 status */ + int got; /* 已收到一帧 */ +} cap_t; +static cap_t g_cap; +static pthread_mutex_t g_cap_mu = PTHREAD_MUTEX_INITIALIZER; + +static void cap_store(uint32_t type, uint32_t req_id, const uint8_t *pay, uint32_t len, + uint32_t nid_to_return) +{ + pthread_mutex_lock(&g_cap_mu); + memset(&g_cap, 0, sizeof g_cap); + g_cap.type = type; + g_cap.req_id = req_id; + g_cap.nid_to_return = nid_to_return; + if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) memcpy(&g_cap.n, pay, sizeof(m_notify_t)); + if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_cap.dismiss_id, pay, 4); + g_cap.got = 1; + pthread_mutex_unlock(&g_cap_mu); +} + +/* ---- stub 服务端线程:accept 一个连接 → 读一帧 → 校验由主线程做,这里只收 + 回 ACK ---- */ +static int g_listen_fd = -1; +static void *server_thread(void *arg) +{ + (void)arg; + int c = accept(g_listen_fd, NULL, NULL); + if (c < 0) return NULL; + /* 读 16B 头 */ + uint8_t hdr[MEOW_IPC_HDR_LEN]; + uint32_t off = 0; + while (off < sizeof hdr) { + ssize_t r = read(c, hdr + off, sizeof hdr - off); + if (r <= 0) { close(c); return NULL; } + off += (size_t)r; + } + uint32_t magic, len; + uint16_t ver, type; + memcpy(&magic, hdr, 4); + memcpy(&ver, hdr + 4, 2); + memcpy(&type, hdr + 6, 2); + memcpy(&len, hdr + 12, 4); + uint32_t req_id; memcpy(&req_id, hdr + 8, 4); + if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) { close(c); return NULL; } + if (len > MEOW_IPC_MAX) { close(c); return NULL; } + uint8_t pay[MEOW_IPC_MAX]; + off = 0; + while (off < len) { + ssize_t r = read(c, pay + off, len - off); + if (r <= 0) { close(c); return NULL; } + off += (size_t)r; + } + cap_store(type, req_id, pay, len, (uint32_t)strtoul(getenv("TEST_NID"), NULL, 0)); + + /* MSG_NOTIFY → 回 m_ack_t(code=OK, status=nid_to_return); + * MSG_NOTIFY_DISMISS → 回 m_ack_t(code=OK, status=0,同 launcher 行为)。 */ + if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS) { + m_ack_t a; + memset(&a, 0, sizeof a); + a.code = MEOW_OK; + a.status = (type == MSG_NOTIFY) ? (uint32_t)strtoul(getenv("TEST_NID"), NULL, 0) : 0; + uint8_t rhdr[MEOW_IPC_HDR_LEN]; + uint32_t rmagic = MEOW_IPC_MAGIC; + uint16_t rver = MEOW_IPC_VERSION, rtype = (uint16_t)(type == MSG_NOTIFY ? MSG_NOTIFY_ACK : MSG_ACK); + uint32_t rlen = sizeof a; + memcpy(rhdr + 0, &rmagic, 4); + memcpy(rhdr + 4, &rver, 2); + memcpy(rhdr + 6, &rtype, 2); + memcpy(rhdr + 8, &req_id, 4); + memcpy(rhdr + 12, &rlen, 4); + off = 0; + while (off < sizeof rhdr) { ssize_t w = write(c, rhdr + off, sizeof rhdr - off); if (w <= 0) break; off += (size_t)w; } + off = 0; + while (off < rlen) { ssize_t w = write(c, (uint8_t*)&a + off, rlen - off); if (w <= 0) break; off += (size_t)w; } + } + close(c); + return NULL; +} + +static int failures = 0; + +static void check(const char *name, int cond) +{ + printf("%s %s\n", cond ? "PASS" : "FAIL", name); + if (!cond) failures++; +} + +/* 建 listening socket,返回 fd;路径写入 sock_path。 */ +static int arb_listen(char *sock_path, size_t slen) +{ + static int count = 0; + snprintf(sock_path, slen, "/tmp/meow_notify_ipc_%d_%d.sock", (int)getpid(), count++); + unlink(sock_path); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { perror("socket"); return -1; } + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + strncpy(a.sun_path, sock_path, sizeof a.sun_path - 1); + if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { perror("bind"); close(fd); return -1; } + if (listen(fd, 1) < 0) { perror("listen"); close(fd); return -1; } + return fd; +} + +int main(void) +{ + setenv("MEOW_LOG_APP", "testapp", 1); /* 固定 app_id,供 stub 校验 */ + + /* ---------- T1/T2/T3:映射 + ACK 回 nid ---------- */ + { + char sock[108]; + g_listen_fd = arb_listen(sock, sizeof sock); + if (g_listen_fd < 0) { fprintf(stderr, "cannot create listener\n"); return 2; } + setenv("MEOW_IPC_SOCK", sock, 1); + setenv("TEST_NID", "0x1234", 1); + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + + meow_notify nt; + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_PROGRESS; + nt.urgency = MEOW_NOTIFY_URG_NORMAL; + nt.title = "下载中"; + nt.body = "70%"; + nt.timeout_ms = 5000; /* >0 → expire=5000, ongoing=0 */ + nt.progress = 150; /* 钳位 → 100 */ + uint32_t nid = meow_notify_ipc_send(&nt, 0); + pthread_join(th, NULL); + close(g_listen_fd); + + check("T3 send 返回 launcher nid(0x1234)", nid == 0x1234); + pthread_mutex_lock(&g_cap_mu); + int got = g_cap.got; + const m_notify_t *cn = &g_cap.n; + pthread_mutex_unlock(&g_cap_mu); + check("T3 stub 收到 MSG_NOTIFY 帧", got && g_cap.type == MSG_NOTIFY); + check("T1 CAT_PROGRESS→category=progress", !strcmp(cn->category, "progress")); + check("T2 timeout>0→expire_timeout=5000", cn->expire_timeout == 5000); + check("T2 timeout>0→ongoing=0", cn->ongoing == 0); + check("T1 media=0(非媒体)", cn->media == 0); + check("T1 progress 钳位 150→100", cn->progress == 100); + check("T2 replaces_id 透传=0", cn->replaces_id == 0); + check("T1 app_id=testapp", !strcmp(cn->app_id, "testapp")); + check("T1 summary=下载中", !strcmp(cn->summary, "下载中")); + } + + /* ---------- T1/T2:媒体类型 + ONGOING(-1) ---------- */ + { + char sock[108]; + g_listen_fd = arb_listen(sock, sizeof sock); + setenv("MEOW_IPC_SOCK", sock, 1); + setenv("TEST_NID", "0x9999", 1); + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + + meow_notify nt; + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_MEDIA; + nt.urgency = MEOW_NOTIFY_URG_CRITICAL; + nt.title = "Now Playing"; + nt.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING; /* -1 */ + uint32_t nid = meow_notify_ipc_send(&nt, 0); + pthread_join(th, NULL); + close(g_listen_fd); + + check("T3 send(media/ongoing) 返回 nid(0x9999)", nid == 0x9999); + check("T1 CAT_MEDIA→category=media", !strcmp(g_cap.n.category, "media")); + check("T1 CAT_MEDIA→media=1", g_cap.n.media == 1); + check("T2 ONGOING(-1)→expire=0xFFFFFFFF", g_cap.n.expire_timeout == 0xFFFFFFFFu); + check("T2 ONGOING(-1)→ongoing=1", g_cap.n.ongoing == 1); + } + + /* ---------- T2:NEVER(0) 超时 ---------- */ + { + char sock[108]; + g_listen_fd = arb_listen(sock, sizeof sock); + setenv("MEOW_IPC_SOCK", sock, 1); + setenv("TEST_NID", "0x1", 1); + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + meow_notify nt; + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_GENERAL; + nt.title = "x"; + nt.timeout_ms = MEOW_NOTIFY_TIMEOUT_NEVER; /* 0 */ + (void)meow_notify_ipc_send(&nt, 0); + pthread_join(th, NULL); + close(g_listen_fd); + check("T2 NEVER(0)→expire=0 & ongoing=0", + g_cap.n.expire_timeout == 0 && g_cap.n.ongoing == 0); + check("T1 CAT_GENERAL→category=general", !strcmp(g_cap.n.category, "general")); + } + + /* ---------- T3:replaces_id 透传 ---------- */ + { + char sock[108]; + g_listen_fd = arb_listen(sock, sizeof sock); + setenv("MEOW_IPC_SOCK", sock, 1); + setenv("TEST_NID", "0xBEEF", 1); + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + meow_notify nt; + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_MESSAGE; + nt.title = "repl"; + nt.timeout_ms = 0; + (void)meow_notify_ipc_send(&nt, 0x42u); + pthread_join(th, NULL); + close(g_listen_fd); + check("T3 replaces_id 透传=0x42", g_cap.n.replaces_id == 0x42u); + } + + /* ---------- T4:无 socket 降级不崩 ---------- */ + { + /* 指向不存在路径 */ + char dead[108]; + snprintf(dead, sizeof dead, "/tmp/meow_notify_ipc_dead_%d.sock", (int)getpid()); + unlink(dead); + setenv("MEOW_IPC_SOCK", dead, 1); + meow_notify nt; + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_EMAIL; + nt.title = "no-socket"; + nt.timeout_ms = 0; + uint32_t nid = meow_notify_ipc_send(&nt, 0); + check("T4 无 socket→返回 0(降级不崩)", nid == 0); + meow_notify_ipc_dismiss(0x1234); /* 无 socket 静默,不崩 */ + check("T4 dismiss 无 socket 静默不崩", 1); + } + + /* ---------- T5:dismiss 发 MSG_NOTIFY_DISMISS ---------- */ + { + char sock[108]; + g_listen_fd = arb_listen(sock, sizeof sock); + setenv("MEOW_IPC_SOCK", sock, 1); + pthread_t th; + pthread_create(&th, NULL, server_thread, NULL); + meow_notify_ipc_dismiss(0xABCDu); + pthread_join(th, NULL); + close(g_listen_fd); + check("T5 stub 收到 MSG_NOTIFY_DISMISS 帧", g_cap.got && g_cap.type == MSG_NOTIFY_DISMISS); + check("T5 dismiss 负载=nid=0xABCD", g_cap.dismiss_id == 0xABCDu); + } + + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +} diff --git a/test_notify_post.c b/test_notify_post.c new file mode 100644 index 0000000..beac070 --- /dev/null +++ b/test_notify_post.c @@ -0,0 +1,150 @@ +/* + * test_notify_post.c — 宿主交叉验证:SDK send 的 IPC 帧 → launcher 真·中央存储。 + * + * 把 SDK 的 meow_notify_ipc.c(映射+传输)与 launcher 的 meow_notif.c(真·中央存储, + * meow_notif_post/dismiss)接到一起,stub 掉 meow_settings 后端;服务端复刻 + * meow_launcherd.c:6427-6440 的 m_notify_t→meow_notif_t 映射 + MSG_NOTIFY_ACK 应答。 + * + * 验证:SDK meow_notify_ipc_send() 产出的帧被 meow_notif_post() 真实落库并回分配 nid>0; + * 随后 meow_notify_ipc_dismiss() 经 meow_notif_dismiss() 撤下(count 归 0)。 + * + * 编译(宿主 gcc): + * gcc -o test_notify_post test_notify_post.c meow_notify_ipc.c \ + * ../meow_launcherd/meow_notif.c -I. -I../meow_launcherd -lpthread + */ +#include "meow_notify_ipc.h" +#include "meow_ui.h" +#include "meow_ipc.h" +#include "meow_notif.h" /* launcher 真·中央存储 */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- stub:settings 后端(键缺省 → meow_notif.c 用默认配额) ---- */ +int meow_settings_get_int(const char *key, int *out) { (void)key; (void)out; return MEOW_ERR_NOENT; } + +/* ---- 复刻 launcher 的 m_notify_t → meow_notif_t 映射(meow_launcherd.c:6427-6440) ---- + * 注:与 launcher 同款 snprintf(dest 较 src 小会有 format-truncation 提示),launcher 自身 + * 以其编译 flags 构建无警告;此处仅为宿主测试,用 pragma 压掉以免 -Werror 干扰。 */ +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" +#endif +static void launcher_map(const m_notify_t *mn, meow_notif_t *n) +{ + memset(n, 0, sizeof *n); + n->id = mn->replaces_id; /* 0=>新建;>0=>替换(不存在则新建) */ + snprintf(n->app_id, sizeof n->app_id, "%s", mn->app_id); + snprintf(n->category, sizeof n->category, "%s", mn->category); + snprintf(n->summary, sizeof n->summary, "%s", mn->summary); + snprintf(n->body, sizeof n->body, "%s", mn->body); + snprintf(n->app_icon, sizeof n->app_icon, "%s", mn->app_icon); + snprintf(n->target_activity, sizeof n->target_activity, "%s", mn->target_activity); + n->expire_timeout = mn->expire_timeout; + n->progress = mn->progress; + n->ongoing = mn->ongoing; + n->media = mn->media; +} +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +static int g_listen_fd = -1; +static int g_stop = 0; +static uint32_t g_posted_nid = 0; + +static void reply_ack(int c, uint16_t type, uint32_t req_id, uint32_t status) +{ + m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; a.status = status; + uint8_t rhdr[MEOW_IPC_HDR_LEN]; + uint32_t mag = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION, t = type; uint32_t rl = sizeof a; + memcpy(rhdr+0,&mag,4); memcpy(rhdr+4,&ver,2); memcpy(rhdr+6,&t,2); memcpy(rhdr+8,&req_id,4); memcpy(rhdr+12,&rl,4); + size_t o = 0; while (o < sizeof rhdr) { ssize_t w = write(c, rhdr+o, sizeof rhdr-o); if (w<=0) break; o += (size_t)w; } + o = 0; while (o < rl) { ssize_t w = write(c, (uint8_t*)&a+o, rl-o); if (w<=0) break; o += (size_t)w; } +} + +static void *server(void *arg) +{ + (void)arg; + meow_notif_init(); + while (!g_stop) { + int c = accept(g_listen_fd, NULL, NULL); + if (c < 0) { if (errno == EAGAIN && g_stop) break; if (errno == EBADF) break; usleep(1000); continue; } + uint8_t hdr[MEOW_IPC_HDR_LEN]; size_t o = 0; + while (o < sizeof hdr) { ssize_t r = read(c, hdr+o, sizeof hdr-o); if (r<=0) break; o += (size_t)r; } + if (o < sizeof hdr) { close(c); continue; } + uint32_t type, len, req_id; memcpy(&type, hdr+6, 2); memcpy(&req_id, hdr+8, 4); memcpy(&len, hdr+12, 4); + if (len > MEOW_IPC_MAX) { close(c); continue; } + uint8_t pay[MEOW_IPC_MAX]; o = 0; + while (o < len) { ssize_t r = read(c, pay+o, len-o); if (r<=0) break; o += (size_t)r; } + + if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) { + m_notify_t mn; memcpy(&mn, pay, sizeof mn); + meow_notif_t n; launcher_map(&mn, &n); + uint32_t nid = 0; + int rc = meow_notif_post(&n, &nid); + if (rc == NOTIF_OK) g_posted_nid = nid; + reply_ack(c, MSG_NOTIFY_ACK, req_id, (rc == NOTIF_OK) ? nid : (uint32_t)rc); + fprintf(stderr, "[post-srv] post rc=%d nid=%u cat=%s\n", rc, nid, mn.category); + } else if (type == MSG_NOTIFY_DISMISS && len >= 4) { + uint32_t id; memcpy(&id, pay, 4); + int rc = meow_notif_dismiss(id); + reply_ack(c, MSG_ACK, req_id, 0); + fprintf(stderr, "[post-srv] dismiss id=%u rc=%d\n", id, rc); + } + close(c); + } + return NULL; +} + +static int make_listen(char *path, size_t n) +{ + snprintf(path, n, "/tmp/meow_notify_post_%d.sock", (int)getpid()); + unlink(path); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un a; memset(&a, 0, sizeof a); a.sun_family = AF_UNIX; + strncpy(a.sun_path, path, sizeof a.sun_path - 1); + if (bind(fd, (struct sockaddr*)&a, sizeof a) < 0 || listen(fd, 4) < 0) { close(fd); return -1; } + fcntl(fd, F_SETFL, O_NONBLOCK); + g_listen_fd = fd; + return 0; +} + +static int failures = 0; +static void check(const char *name, int cond) { printf("%s %s\n", cond ? "PASS" : "FAIL", name); if (!cond) failures++; } + +int main(void) +{ + setenv("MEOW_LOG_APP", "intgapp", 1); + char sock[108]; + if (make_listen(sock, sizeof sock) != 0) { fprintf(stderr, "cannot listen\n"); return 2; } + setenv("MEOW_IPC_SOCK", sock, 1); + pthread_t th; pthread_create(&th, NULL, server, NULL); + + /* SDK send → launcher 真存储。media + ONGOING 走全部映射分支。 */ + meow_notify nt; memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_MEDIA; nt.urgency = MEOW_NOTIFY_URG_NORMAL; + nt.title = "正在播放"; nt.timeout_ms = -1; nt.progress = 40; + uint32_t nid = meow_notify_ipc_send(&nt, 0); + check("send 经 launcher 真存储回 nid>0", nid != 0); + check("send 拿到 launcher 分配的 nid(与 post 一致)", g_posted_nid == nid); + check("launcher 存储 count(media app)=1", meow_notif_count("intgapp") == 1); + + /* SDK dismiss → launcher 撤下 */ + meow_notify_ipc_dismiss(nid); + check("dismiss 后 launcher 存储 count=0", meow_notif_count("intgapp") == 0); + + g_stop = 1; close(g_listen_fd); + pthread_join(th, NULL); + meow_notif_shutdown(); + + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +}