diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md index 311c8cf..c0a26b9 100644 --- a/NOTIFICATION_SYSTEM.md +++ b/NOTIFICATION_SYSTEM.md @@ -29,13 +29,13 @@ ``` id(uint32) / app_name / app_icon / summary / body / actions[] / hints{} expire_timeout / created_ms / dismissed / ongoing(bool) -target_activity(可选, 来自 hints) / progress(可选 int) +target_activity(可选, 来自 hints) / progress(可选 int) / urgency(0-3, 缺省 NORMAL) ``` -统一从 IPC/D-Bus 两路解析入该结构。 +统一从 IPC/D-Bus 两路解析入该结构。`app_icon`(SDK `icon`):空时按 `app_id` 回退该 app 默认图标(launcher 由 app.json `icon` 提供;通知中心 UI 兜底)。 ## 5. 存储分层(用户已定 2026-08-29) - **关键配置优先板载**:设置类(notify.* 配额等)存板载 `/usr/data/meow/settings.conf`(B2a),确保**无卡时系统正常工作**。 -- **实时通知 → 本地内存**(板载,动态分配/ring)。 +- **实时通知 → 本地内存**(板载,动态分配/ring)。**progress 实时进度 → 仅改内存活跃槽**(用户定案 2026-08-30);**落盘快照只在创建时/结束(100% 或 dismiss)**,避免每条 live 写卡。 - **超时/转储 → TF 卡** `MeowOS/notifications/`(**系统约定:系统文件统一放 MeowOS/ 下**,非 MeowData/——MeowData 是应用专属数据根)(转储区,**appended 日志行 CSV**,见 §8)。 - **无 TF 卡可运行**(本地内存态即可,不依赖卡;TF 卡在时才转储)。转储/日志类是可降级数据(无卡用 /tmp 回退),配置类永远板载。 - **本地数据清理策略**: @@ -61,6 +61,8 @@ target_activity(可选, 来自 hints) / progress(可选 int) - **异常关机**:开机时清理板载残留通知数据。 - TF 卡转储区 `MeowOS/notifications/`:保留**最新 10MB**(ring/截断),**不被电源事件清理**(持久留存)。 - **转储格式(定案 #6,M2.5x)**:`MeowOS/notifications/` 写为 **appended 日志行**(每条通知一行,CSV 风格:`id,app_id,分类,标题,时间,…`),**按总大小滚动截断**(保留最新 **10MB**)。与既有「10s 转储」衔接:创建 10s 后即按此格式追加一行 `[源:用户 2026-08-30]`。 +- **CSV 列(模型补全,2026-08-30)**:`timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,ongoing,media,dismissed`(`urgency` 0-3、progress 当前值 -1=无;`app_icon`=SDK `icon`,空→app 图标回退)。 +- **progress 落盘规则(模型补全,用户定案)**:**里程碑落盘**——快照只在**创建时 + 结束(100% 或 dismiss)** 触发;实时进度(`meow_notify_update_progress` → `MSG_NOTIFY_PROGRESS`)**仅改内存活跃槽,不每帧落盘**。 - **启动时清板载 + 清内存通知表**:开机(含异常关机后)清理板载残留通知数据,并**清空内存通知表**(在既有「异常关机:开机时清理板载残留」基础上**补充内存表一并清**);正常关机/重启前先转储未上卡通知再清板载(沿用既有规则)`[源:用户 2026-08-30]`。 - **无 TF 卡**:系统可运行(内存态通知即可),不依赖卡。 diff --git a/docs/MEOWOS_SPEC_REF.md b/docs/MEOWOS_SPEC_REF.md index 6329186..215e292 100644 --- a/docs/MEOWOS_SPEC_REF.md +++ b/docs/MEOWOS_SPEC_REF.md @@ -172,6 +172,12 @@ - **每 App 最多 4 类型;每类型活动 ≤4;媒体单例(每 App 仅 1 条,可多 App)**;系统事件抽象为 App(`app_id=system`) 走同一配额 `[NOTIFICATION_SYSTEM §7][meow_ui.h][DEV_NOTES #11]` - 通知创建 **10s 后自动转储到 TF `MeowOS/notifications/`**(保留最新 10MB ring,不被电源事件清);正常关机先转储再清板载;无卡可运行 `[NOTIFICATION_SYSTEM §8]` - **通知 TF 转储(定案 #6,M2.5x)**:转储格式 = **appended 日志行**(每条通知一行,CSV 风格:`id,app_id,分类,标题,时间,…`)+ **按总大小滚动截断**(保留最新 **10MB**)。**启动时清板载通知 + 清内存通知表**(开机清残留/内存表);正常关机/重启前**先转储未上卡通知**再清板载(沿用既有规则)`[源:用户 2026-08-30]` +- **通知模型补全(2026-08-30 用户定案,全链路 SDK→IPC→存储→dump)**: + - 字段:SDK `meow_notify` / IPC `m_notify_t` / 存储 `meow_notif_t` 全链路补 **`urgency`(0-3,缺省 NORMAL)**、**`icon`(name/path,NULL/空 → 用该 app 默认图标,launcher 按 app_id 回退 app.json 的 icon;每条可独立指定)**;`progress` 已有(0-100,-1=无)。 + - 实时进度:`meow_notify_update_progress(local_id, progress)`(SDK)→ IPC `MSG_NOTIFY_PROGRESS(0x0024)` → launcher 只改**内存活跃槽**(不落盘)。 + - **里程碑落盘**:TF 快照只在**创建时 + 结束(100% 或 dismiss)** 触发(沿用 `meow_notif_dump_tf`,progress 写当时值);实时进度**不每帧落盘**。 + - **CSV 列**(`meow_notif_dump_tf`/`tf_line`):`timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,ongoing,media,dismissed`。 + - 说明:旧调用(无 urgency/progress/icon)兼容——urgency 缺省 NORMAL、progress=-1、icon=NULL→app 图标。`[源:用户 2026-08-30]` ### 5.4 配置版本单点 / 刷新率 / 息屏 - /etc/meowos_version 版本单点(见 §1.1)`[DEV_NOTES #4]` diff --git a/patch_v2/hello_app/hello.c b/patch_v2/hello_app/hello.c index 396fd7e..a1ce2cf 100644 --- a/patch_v2/hello_app/hello.c +++ b/patch_v2/hello_app/hello.c @@ -94,13 +94,14 @@ static void notif_send(int which) n.body = "常驻不自动过期(timeout=-1),需手动取消"; n.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING; break; - case 5: /* 带 target_activity(SDK 结构暂无 icon 字段,故只填跳转目标) */ + case 5: /* 带 target_activity + icon(SDK 结构含 icon 字段;NULL/空=用 app 默认图标) */ label = "TGT"; n.cat = MEOW_NOTIFY_CAT_MESSAGE; n.title = "测试-跳转Activity"; n.body = "点击跳转 target_activity=main"; n.timeout_ms = 6000; n.target_activity = "main"; + n.icon = NULL; /* 空 → launcher 用 app 默认图标(app.json icon) */ break; default: return; diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index 8042f9b..c60a4eb 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -44,6 +44,7 @@ enum { MSG_NOTIFY_ACK = 0x0021, /* ↓ 落库结果 */ MSG_NOTIFY_DISMISS = 0x0022, /* ↑ 撤下通知 */ MSG_NOTIFY_LIST = 0x0023, /* ↓ 下发通知清单(重同步) */ + MSG_NOTIFY_PROGRESS = 0x0024,/* ↑ 实时更新通知进度(仅改内存活跃槽,不落盘) */ MSG_ACT_DECLARE = 0x0030, /* ↑ (重)声明路由表 */ MSG_ACT_QUERY = 0x0031, /* ↑ 解析 activity 链路 */ MSG_ACT_RESULT = 0x0032, /* ↓ 返回链路 */ @@ -134,9 +135,11 @@ typedef struct m_act_declare { } m_act_declare_t; /* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。 - * - summary/body/app_icon/expire_timeout 直通 freedesktop 字段; + * - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon` + * (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退); * - target_activity/progress 为 MeowOS 专有(存储层映射为 hints 的 * meow.target_activity / meow.progress); + * - urgency 为 MeowOS 扩展(0-3,缺省 NORMAL;旧调用置 0=旧 pad 位); * - actions[]/hints{} v1 走定长空串(M2b D-Bus 桥再填);media/ongoing 标记。 * 宿主:NOTIFICATION_SYSTEM.md §2/§4 + FRAMEWORK_DESIGN §4/附C。 */ typedef struct m_notify { @@ -144,14 +147,22 @@ typedef struct m_notify { char category[24]; /* 通知类型(媒体通知="media") */ char summary[64]; char body[96]; - char app_icon[48]; + char app_icon[48]; /* SDK icon;空=launcher 按 app_id 回退 app 图标 */ char target_activity[MEOW_ACT_MAX]; /* MeowOS:点击跳转目标 activity */ uint32_t replaces_id; /* 0=新建;>0=替换已存在通知 */ uint32_t expire_timeout; /* ms;0xFFFFFFFF=ONGOING 常驻 */ int32_t progress; /* MeowOS:0-100;<0=无 */ uint8_t media; /* 1=媒体通知 */ uint8_t ongoing; /* 1=常驻通知 */ - uint16_t pad; -} m_notify_t; /* 定长 LE */ + uint8_t urgency; /* 0-3(低/普通/高/紧急);越界由 SDK 钳位缺省 NORMAL;旧调用置 0=旧 pad 位 → LOW/静默 */ + uint8_t pad; +} m_notify_t; /* 定长 LE;尾部 4 字节(media/ongoing/urgency/pad)与旧版 pad 等宽 */ + +/* MSG_NOTIFY_PROGRESS ↑:实时更新一条已存在通知的进度(仅改 launcher 内存活跃槽, + * 不落盘——TF 快照只在创建/结束(100% 或 dismiss)写)。 */ +typedef struct m_notify_progress { + uint32_t nid; /* launcher 分配的通知 id(MSG_NOTIFY_ACK.status) */ + int32_t progress; /* 0-100;<0=清除进度条 */ +} m_notify_progress_t; /* 8 B */ #endif /* MEOW_IPC_H */ diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 558053b..5ac9553 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_launcherd.c b/patch_v2/meow_launcherd/meow_launcherd.c index dbf2df7..03ff2b7 100644 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ b/patch_v2/meow_launcherd/meow_launcherd.c @@ -32,9 +32,13 @@ * app_id="system" 同走配额);TTL 10s 转储钩子(§8,TF 落盘 M2c 后置,预留 notif_dump_cb); * 满丢该类型最旧条(ring 语义);ONGOING 常驻(转储后仍保留,服务死随清)。 * - meow_settings 默认表新增 notify.categories/category_quota/media_quota(B2a 后端)。 - * - IPC:ipc_dispatch 新增 MSG_NOTIFY→MSG_NOTIFY_ACK、MSG_NOTIFY_DISMISS→MSG_ACK; + * - IPC:ipc_dispatch 新增 MSG_NOTIFY→MSG_NOTIFY_ACK、MSG_NOTIFY_DISMISS→MSG_ACK、 + * MSG_NOTIFY_PROGRESS→MSG_ACK(实时进度仅改内存活跃槽,不落盘); * MSG_NOTIFY_LIST 为守护→App 下行(M5 接入)。meow_ipc 测试钩子新增 notify/dismiss 子命令。 * - meow_notif_before_poweroff() 挂入 sys_do_reboot():正常电源事件前先转储未上卡通知再清板载。 + * - 通知模型补全(2026-08-30,用户定案):SDK→IPC→存储→dump 全链路补 urgency(0-3)/ + * icon(空→按 app_id 回退该 app 默认图标);progress 实时仅改内存(MSG_NOTIFY_PROGRESS), + * TF 快照只在创建/结束(100% 或 dismiss)(meow_notif_dump_tf 写当时值)。 * * B2a 统一系统设置后端(2026-08-29): * - 新增 meow_settings.h/.c(SYSTEM_SETTINGS.md §3/MEOWOS_API §2):全部系统设置 @@ -2460,6 +2464,17 @@ static void notif_dump_tf_placeholder(const meow_notif_t *n) meow_notif_dump_tf(n); } +/* ---- 通知图标回退回调(用户定案:icon 为空 → 用该 app 默认图标)---- + * post 的 app_icon 为空时调用(meow_notif.c 的 notif_resolve_icon),按 app_id 在 app + * 注册表(scan_apps 静态 app.json 的 icon)里找该 app 的图标(name/path 或空)。 */ +static int notif_app_icon_fallback(const char *app_id, char *out, size_t outsz) +{ + if (!app_id || !app_id[0] || !out || outsz == 0) return 0; + struct meow_acct *a = meow_acct_find(app_id); + if (a && a->icon[0]) { snprintf(out, outsz, "%s", a->icon); return 1; } + return 0; /* 无注册/无图标 → 保持空(通知中心 UI 再回退) */ +} + /* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf; * 此处仅注册监听(一次性)+ 把 backend 当前值同步到缓存全局。重载(应用期 * watchdog 可能改音量)等价于 meow_settings_init() + 手动同步。 */ @@ -2469,6 +2484,7 @@ static void settings_load(void) meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */ meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */ meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */ + meow_notif_set_icon_cb(notif_app_icon_fallback); /* icon 空 → 按 app_id 回退该 app 默认图标(app.json icon) */ if (!settings_cache_registered) { meow_settings_add_listener(settings_sync_cache_listener); settings_cache_registered = 1; @@ -6951,6 +6967,7 @@ static void ipc_dispatch(struct meow_ipc_msg *m) snprintf(n.target_activity, sizeof n.target_activity, "%s", mn.target_activity); n.expire_timeout = mn.expire_timeout; n.progress = mn.progress; + n.urgency = mn.urgency; /* 0-3(低/普通/高/紧急);旧调用=0 → LOW(静默),新调用缺省 NORMAL */ n.ongoing = mn.ongoing; n.media = mn.media; uint32_t nid = 0; @@ -6973,6 +6990,19 @@ static void ipc_dispatch(struct meow_ipc_msg *m) logmsg("ipc: NOTIFY_DISMISS id=%u rc=%d -> ACK", id, rc); break; } + case MSG_NOTIFY_PROGRESS: { + m_notify_progress_t p; + memset(&p, 0, sizeof p); + memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p); + /* 实时进度:仅改 launcher 内存活跃槽(不落盘);progress>=100 视为结束 → 里程碑落盘 */ + int rc = meow_notif_update_progress(p.nid, p.progress); + m_ack_t a; + a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; + a.status = 0; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: NOTIFY_PROGRESS nid=%u p=%d rc=%d -> ACK", p.nid, p.progress, rc); + break; + } case MSG_NOTIFY_LIST: /* ↓ 守护→App 下发通知清单(重同步)。本帧不来自客户端(无请求类型),故不在此应答; * 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */ diff --git a/patch_v2/meow_launcherd/meow_notif.c b/patch_v2/meow_launcherd/meow_notif.c index 907ee95..f4f7c42 100644 --- a/patch_v2/meow_launcherd/meow_notif.c +++ b/patch_v2/meow_launcherd/meow_notif.c @@ -58,6 +58,7 @@ static int g_cat_quota = MEOW_NOTIF_CAT_QUOTA_DEF; static int g_media_quota = MEOW_NOTIF_MEDIA_QUOTA_DEF; static meow_notif_dump_cb g_dump_cb = NULL; +static meow_notif_icon_cb g_icon_cb = NULL; static int g_inited = 0; /* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "notif:")---- */ @@ -94,14 +95,27 @@ static void n_strncpy(char *dst, const char *src, size_t len) dst[len - 1] = 0; } +/* app_icon 回退(用户定案):post 的 app_icon 为空时,经 g_icon_cb(app_id) 填该 app 默认图标; + * 无回调/回调未填 → 保持空(通知中心 UI 再回退)。每条可独立图标(非空即用)。 */ +static void notif_resolve_icon(meow_notif_t *s) +{ + if (!g_icon_cb) return; + if (s->app_icon[0]) return; + char tmp[MEOW_NOTIF_ICON_MAX]; + if (g_icon_cb(s->app_id, tmp, sizeof tmp) > 0 && tmp[0]) + n_strncpy(s->app_icon, tmp, sizeof s->app_icon); +} + /* ================= TF 卡转储(通知#6 / M2c,NOTIFICATION_SYSTEM.md §5/§8)================= * 每条通知一行 CSV 追加到 <卡根>/MeowOS/notifications/notifications.log,并按日志总大小 - * 滚动截断(保留最新 10MB)。系统文件统一放 <卡根>/MeowOS/ 下(TF_CARD_LAYOUT.md)。 */ + * 滚动截断(保留最新 10MB)。系统文件统一放 <卡根>/MeowOS/ 下(TF_CARD_LAYOUT.md)。 + * 字段见 tf_line():timestamp,id,app_id,app_icon,category,urgency,progress,title,body, + * target_activity,ongoing,media,dismissed(CSV 转义/去换行)。 */ #define MEOW_NOTIF_TF_DIR "/usr/data/mnt/sd_0/MeowOS/notifications" #define MEOW_NOTIF_TF_LOG MEOW_NOTIF_TF_DIR "/notifications.log" #define MEOW_NOTIF_LOG_MAX_BYTES ((size_t)10 * 1024 * 1024) /* §8:保留最新 10MB */ -/* 单行 CSV 上限:app_id(32)/category(24)/summary(64)/body(96)/target_activity(48) +/* 单行 CSV 上限:app_id(32)/category(24)/summary(64)/body(96)/app_icon(48)/target_activity(48) * 全引号转义后仍远小于此。 */ #define MEOW_NOTIF_CSV_LINE_MAX 1024 @@ -155,26 +169,31 @@ static void csv_field(char *dst, size_t dstsz, const char *src) } /* 生成一行 CSV: - * timestamp,id,app_id,category,urgency,title,body,target_activity,ongoing,media,dismissed - * title<-summary;urgency:meow_notif 结构无该字段(保持既有 API),列值留空 ""。 - * 返回写入长度(不含 NUL)。 */ + * timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,ongoing,media,dismissed + * title<-summary;app_icon<-SDK icon(空→储存时已按 app_id 回退 app 图标);urgency 0-3(缺省 NORMAL); + * progress 写当前值(-1=无进度)。返回写入长度(不含 NUL)。 */ static size_t tf_line(const meow_notif_t *n, char *buf, size_t bufsz) { char a_app [2 + 2 * MEOW_NOTIF_APP_ID_MAX + 2]; char a_cat [2 + 2 * MEOW_NOTIF_CAT_MAX + 2]; char a_sum [2 + 2 * MEOW_NOTIF_SUMMARY_MAX + 2]; char a_body[2 + 2 * MEOW_NOTIF_BODY_MAX + 2]; + char a_icon[2 + 2 * MEOW_NOTIF_ICON_MAX + 2]; char a_tgt [2 + 2 * MEOW_NOTIF_ACTIVITY_MAX + 2]; csv_field(a_app, sizeof a_app, n->app_id); csv_field(a_cat, sizeof a_cat, n->category); csv_field(a_sum, sizeof a_sum, n->summary); csv_field(a_body, sizeof a_body, n->body); + csv_field(a_icon, sizeof a_icon, n->app_icon); csv_field(a_tgt, sizeof a_tgt, n->target_activity); int r = snprintf(buf, bufsz, - "%llu,%u,%s,%s,\"\",%s,%s,%s,%u,%u,%u\n", + "%llu,%u,%s,%s,%s,%u,%d,%s,%s,%s,%u,%u,%u\n", (unsigned long long)n->created_ms, (unsigned)n->id, - a_app, a_cat, a_sum, a_body, a_tgt, + a_app, a_icon, a_cat, + (unsigned)n->urgency, + (int)n->progress, + a_sum, a_body, a_tgt, n->ongoing ? 1u : 0u, n->media ? 1u : 0u, n->dismissed ? 1u : 0u); @@ -477,8 +496,10 @@ int meow_notif_post(const meow_notif_t *n, uint32_t *out_id) n_strncpy(s->target_activity, n->target_activity, sizeof s->target_activity); s->expire_timeout = n->expire_timeout; s->progress = n->progress; + s->urgency = n->urgency; s->ongoing = n->ongoing; s->media = n->media; + notif_resolve_icon(s); /* 空 app_icon → app 图标回退 */ if (out_id) *out_id = s->id; notif_log("post: UPDATE id=%u app=%s cat=%s", s->id, s->app_id, s->category); return NOTIF_OK; @@ -533,10 +554,12 @@ int meow_notif_post(const meow_notif_t *n, uint32_t *out_id) n_strncpy(s->target_activity, n->target_activity, sizeof s->target_activity); s->expire_timeout = n->expire_timeout; s->progress = n->progress; + s->urgency = n->urgency; s->ongoing = n->ongoing; s->media = media ? 1 : 0; s->created_ms = wall_ms(); s->dump_at_ms = mono_ms() + MEOW_NOTIF_TTL_MS; /* §8:创建 10s 转储 */ + notif_resolve_icon(s); /* 空 app_icon → app 图标回退 */ s->active = 1; if (out_id) *out_id = s->id; notif_log("post: NEW id=%u app=%s cat=%s media=%d ongoing=%d", s->id, s->app_id, @@ -558,6 +581,26 @@ int meow_notif_dismiss(uint32_t id) return NOTIF_OK; } +/* ---- 实时更新进度:仅改内存活跃槽;progress>=100 视为「结束」→ 立即归档(里程碑落盘)---- */ +int meow_notif_update_progress(uint32_t id, int progress) +{ + if (!g_inited) return NOTIF_ERR_NOTINIT; + int p = (progress < -1) ? -1 : (progress > 100 ? 100 : progress); /* 钳位 [-1,100] */ + int idx = slot_by_id(id); + if (idx < 0) return NOTIF_ERR_NOENT; + meow_notif_t *s = &g_slots[idx]; + s->progress = p; /* 实时:只改内存活跃槽,不落盘 */ + if (p >= 100) { /* 结束(100%):里程碑落盘 + 释放非 ONGOING */ + int was_ongoing = s->ongoing; + archive_one(s); /* 此刻快照(progress=100 写入 dump) */ + if (!was_ongoing) slot_release(idx); + else notif_log("progress: id=%u reached 100%%, archived (kept for UI)", id); + } else { + notif_log("progress: id=%u p=%d (memory only)", id, p); + } + return NOTIF_OK; +} + /* ---- count ---- */ int meow_notif_count(const char *app_id) { @@ -641,6 +684,12 @@ void meow_notif_set_dump_cb(meow_notif_dump_cb cb) g_dump_cb = cb; /* NULL=恢复默认(无回调,仅日志;M2c 接 TF) */ } +/* ---- 设置图标回退回调 ---- */ +void meow_notif_set_icon_cb(meow_notif_icon_cb cb) +{ + g_icon_cb = cb; /* NULL=禁用(空 app_icon 保持空,通知中心 UI 再回退) */ +} + /* ---- MSG_NOTIFY_LIST 序列化(守护→App 下发清单;M5 通知中心/重同步用)---- * 负载: [u16 count] + count 条 定长 meow_notif 记录(records 以 app_id/category/ * summary/body/app_icon/target_activity 为主,精简 payload,适配 MEOW_IPC_CARGO)。 */ @@ -690,8 +739,8 @@ void meow_notif_dump(const char *log_prefix) for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (!s->active) continue; - notif_log("%s slot%02d id=%u app=%s cat=%s sum=%s ong=%d media=%d dumped=%d", + notif_log("%s slot%02d id=%u app=%s cat=%s sum=%s ong=%d media=%d urg=%u dumped=%d", lp, i, s->id, s->app_id, s->category, s->summary, - s->ongoing, s->media, s->dumped); + s->ongoing, s->media, s->urgency, s->dumped); } } diff --git a/patch_v2/meow_launcherd/meow_notif.h b/patch_v2/meow_launcherd/meow_notif.h index 7791816..12dfa57 100644 --- a/patch_v2/meow_launcherd/meow_notif.h +++ b/patch_v2/meow_launcherd/meow_notif.h @@ -66,14 +66,15 @@ typedef struct meow_notif { char category[MEOW_NOTIF_CAT_MAX]; /* 通知类型(媒体通知="media") */ char summary[MEOW_NOTIF_SUMMARY_MAX]; /* 标题(单行) */ char body[MEOW_NOTIF_BODY_MAX]; /* 正文(可多行) */ - char app_icon[MEOW_NOTIF_ICON_MAX]; /* 图标 icon name 或 path */ + char app_icon[MEOW_NOTIF_ICON_MAX]; /* 图标 icon name 或 path(SDK icon;空→app 图标回退) */ char actions[MEOW_NOTIF_ACTIONS_MAX]; /* actions[](NUL 分隔;v1 空,M2b 填) */ char hints[MEOW_NOTIF_HINTS_MAX]; /* hints{} 原始 "k=v;..." 串(v1 空,M2b 填) */ uint32_t expire_timeout; /* 超时 ms;0xFFFFFFFF=ONGOING 常驻 */ uint64_t created_ms; /* 创建时刻(墙钟 epoch ms,供归档/显示) */ uint64_t dump_at_ms; /* 转储时刻(monotonic ms,TTL=创建+10s;ONGOING 亦到此转储) */ - int32_t progress; /* MeowOS:0-100 进度;<0=无 */ + int32_t progress; /* MeowOS:0-100 进度;<0=无(实时仅改内存,落盘只在创建/结束) */ char target_activity[MEOW_NOTIF_ACTIVITY_MAX]; /* MeowOS:点击跳转目标 activity */ + uint8_t urgency; /* 紧急度 0-3(低/普通/高/紧急);缺省 NORMAL */ uint8_t ongoing; /* 1=常驻通知(服务死随清) */ uint8_t media; /* 1=媒体通知(走 notify.media_quota) */ uint8_t dismissed; /* 1=已撤下 */ @@ -84,6 +85,10 @@ typedef struct meow_notif { /* 转储回调(M2c 落盘 / 本批默认仅日志):通知归档时调用。 */ typedef void (*meow_notif_dump_cb)(const meow_notif_t *n); +/* 图标回退回调:post 的 n->app_icon 为空时调用,按 app_id 填该 app 默认图标(name/path)。 + * 返回 1=已填 out;0/空=无(通知中心 UI 再回退)。NULL=禁用回退(保持空 app_icon)。 */ +typedef int (*meow_notif_icon_cb)(const char *app_id, char *out, size_t outsz); + /* == 生命周期 == */ int meow_notif_init(void); /* 幂等;从 meow_settings 读 notify.* 配额(B2a) */ int meow_notif_shutdown(void); /* 保存/清理(进程退出前调;本批=归档未转储项) */ @@ -119,12 +124,19 @@ void meow_notif_before_poweroff(void); /* 覆盖转储回调(NULL=恢复默认日志;M2c 接 TF 落盘)。 */ void meow_notif_set_dump_cb(meow_notif_dump_cb cb); +/* 覆盖图标回退回调(NULL=禁用,空 app_icon 保持空;M5 通知中心 UI 再回退)。 */ +void meow_notif_set_icon_cb(meow_notif_icon_cb cb); + +/* 实时更新一条活跃通知的进度(0-100,<0=清除)。仅改内存活跃槽;progress>=100 视为「结束」, + * 立即归档(落盘快照)并(非 ONGOING)释放槽——对应「里程碑落盘」。返回 NOTIF_OK/NOTIF_ERR_NOENT。 */ +int meow_notif_update_progress(uint32_t id, int progress); + /* == TF 卡转储(通知#6 / M2c,NOTIFICATION_SYSTEM.md §5/§8 + MEOWOS_SPEC_REF §5.3)== * 把 dump_cb 接到 meow_notif_dump_tf 即把每条通知追加为一行 CSV 到 * <卡根>/MeowOS/notifications/notifications.log,并按日志总大小滚动截断 * (保留最新 10MB,不被电源事件清;启动时 meow_notif_init 先清板载+内存表)。 - * 字段按 meow_notif 结构(title<-summary;urgency 结构无字段故列留空;均做 CSV - * 转义/去换行)。无卡/不可写仅打点降级,绝不崩。 */ + * 字段按 meow_notif 结构(title<-summary;app_icon<-SDK icon 空→app 图标回退;urgency 0-3; + * progress 当前值,-1=无;均做 CSV 转义/去换行)。无卡/不可写仅打点降级,绝不崩。 */ int meow_notif_dump_tf(const meow_notif_t *n); /* 0=成功;负=失败(无卡/IO/参数) */ void meow_notif_clear_tf(void); /* 开机清板载通知(清空文件,保留目录) */ void meow_notif_set_tf_path(const char *path); /* 覆盖转储日志路径;NULL=恢复默认卡路径(宿主单测用) */ diff --git a/patch_v2/meow_launcherd/test_notif_tf.c b/patch_v2/meow_launcherd/test_notif_tf.c index 97a9cbb..bebd499 100644 --- a/patch_v2/meow_launcherd/test_notif_tf.c +++ b/patch_v2/meow_launcherd/test_notif_tf.c @@ -53,9 +53,37 @@ static void make_notif(meow_notif_t *n, uint32_t id, const char *sum, const char strcpy(n->summary, sum); strcpy(n->body, body); strcpy(n->target_activity, "act"); + n->urgency = 0; /* 0=LOW(缺省值) */ + n->progress = -1; /* 无进度 */ n->ongoing = 1; } +/* 回读指定 summary 的活跃通知(meow_notif_list 回调;按 g_find_key 匹配) */ +static meow_notif_t g_got; static int g_got_found = 0; static char g_find_key[64]; +static int find_cb(const meow_notif_t *n, void *ctx) +{ + (void)ctx; + if (n && strcmp(n->summary, g_find_key) == 0) { g_got = *n; g_got_found = 1; return 1; } + return 0; +} +static meow_notif_t find_notif(const char *summary) +{ + g_got_found = 0; memset(&g_got, 0, sizeof g_got); + snprintf(g_find_key, sizeof g_find_key, "%s", summary); + meow_notif_list(NULL, find_cb, NULL); + return g_got_found ? g_got : (meow_notif_t){0}; +} + +/* 图标回退 stub:com.app → "app_icon_name";其他 app → 无 */ +static int stub_icon_cb(const char *app_id, char *out, size_t outsz) +{ + if (app_id && !strcmp(app_id, "com.app")) { snprintf(out, outsz, "app_icon_name"); return 1; } + return 0; +} + +/* dump_cb 类型是 void,而 meow_notif_dump_tf 返回 int——包一层(同 launcher notif_dump_tf_placeholder) */ +static void dump_tf_wrapper(const meow_notif_t *n){ (void)meow_notif_dump_tf(n); } + /* --- 1) CSV 转义 + 去换行 --- */ static void test_csv_escape(const char *path) { @@ -71,9 +99,9 @@ static void test_csv_escape(const char *path) char *s = read_all(path, NULL); CHECK(s != NULL, "file exists after dump"); const char *expect = - "1234567890123,42,\"com.app\",\"general\",\"\",\"Hel\"\"lo, world line2\"," - "\"Body with\ttab\",\"act\",1,1,0\n"; /* ongoing=1 media=1 dismissed=0 */ - CHECK(s && strcmp(s, expect) == 0, "CSV line matches expected (escape+newline-strip)"); + "1234567890123,42,\"com.app\",\"\",\"general\",0,-1,\"Hel\"\"lo, world line2\"," + "\"Body with\ttab\",\"act\",1,1,0\n"; /* app_icon="" urgency=0 progress=-1 ongoing=1 media=1 dismissed=0 */ + CHECK(s && strcmp(s, expect) == 0, "CSV line matches expected (escape+newline-strip+new cols)"); if (s) { fprintf(stderr, " got: %s", s); fprintf(stderr, " exp: %s", expect); } free(s); } @@ -134,24 +162,92 @@ static void test_init_clear(const char *path) free(s); } +/* --- 4) icon 回退:app_icon 空 → 按 app_id 用该 app 默认图标;非空 → 保留 --- */ +static void test_icon_fallback(const char *path) +{ + meow_notif_set_icon_cb(stub_icon_cb); + meow_notif_before_poweroff(); /* 清内存表,回到干净态(dump_cb=NULL 不落盘) */ + meow_notif_set_tf_path(path); + meow_notif_set_tf_max(1024 * 1024); + + /* 空 app_icon → 回退 app 图标 */ + meow_notif_t n; + make_notif(&n, 0, "icon-test", ""); + uint32_t id = 0; + CHECK(meow_notif_post(&n, &id) == NOTIF_OK, "icon-fallback post ok"); + meow_notif_t got = find_notif("icon-test"); + CHECK(got.id != 0 && !strcmp(got.app_icon, "app_icon_name"), "empty app_icon → app icon fallback"); + + /* 非空 app_icon → 独立图标(不回退) */ + make_notif(&n, 0, "icon-custom", ""); + n.app_icon[0] = 0; strcpy(n.app_icon, "custom/icon.png"); + CHECK(meow_notif_post(&n, &id) == NOTIF_OK, "icon-custom post ok"); + got = find_notif("icon-custom"); + CHECK(got.id != 0 && !strcmp(got.app_icon, "custom/icon.png"), "non-empty app_icon kept (per-notif icon)"); +} + +/* --- 5) progress 实时(仅内存)vs 里程碑(100% 落盘 + 释放) --- */ +static void test_progress_live(const char *path) +{ + meow_notif_set_icon_cb(NULL); + meow_notif_set_dump_cb(NULL); + meow_notif_before_poweroff(); /* 清内存表,回到干净态 */ + meow_notif_set_tf_path(path); + meow_notif_set_tf_max(1024 * 1024); + meow_notif_set_dump_cb(dump_tf_wrapper); /* 归档即写 CSV;验证落盘快照 */ + + meow_notif_t n; + make_notif(&n, 0, "live", ""); + n.ongoing = 0; /* 非 ONGOING,便于验证 100% 释放 */ + n.progress = 0; + uint32_t id = 0; + CHECK(meow_notif_post(&n, &id) == NOTIF_OK, "progress post ok"); + CHECK(meow_notif_count(NULL) == 1, "one active after post"); + + /* 实时 <100%:仅内存,不落盘 */ + CHECK(meow_notif_update_progress(id, 70) == NOTIF_OK, "update_progress(70) ok"); + CHECK(meow_notif_count(NULL) == 1, "still active after live update (not dumped)"); + long len = 0; char *s = read_all(path, &len); + CHECK((s == NULL || len == 0), "no CSV written on live update (<100%)"); + free(s); + + /* 里程碑 100%:立即归档落盘 + 释放非 ONGOING 槽 */ + CHECK(meow_notif_update_progress(id, 100) == NOTIF_OK, "update_progress(100) ok"); + CHECK(meow_notif_count(NULL) == 0, "slot released after 100% milestone"); + s = read_all(path, &len); + CHECK(s != NULL && len > 0, "CSV written on 100% milestone"); + CHECK(s && strstr(s, ",100,\"live\"") != NULL, "milestone CSV has progress=100 title=live"); + if (s) fprintf(stderr, " milestone row: %s", s); + free(s); + + /* 已释放 id → NOENT */ + CHECK(meow_notif_update_progress(id, 50) == NOTIF_ERR_NOENT, "update on released id -> NOENT"); + + meow_notif_set_dump_cb(NULL); /* 复位 */ +} + int main(void) { char tmp[] = "/tmp/meow_notif_tf_test_XXXXXX"; char *dir = mkdtemp(tmp); if (!dir) { perror("mkdtemp"); return 2; } - char p1[512], p2[512], p3[512]; + char p1[512], p2[512], p3[512], p4[512], p5[512]; snprintf(p1, sizeof p1, "%s/csv.log", dir); snprintf(p2, sizeof p2, "%s/trim.log", dir); snprintf(p3, sizeof p3, "%s/init.log", dir); + snprintf(p4, sizeof p4, "%s/icon.log", dir); + snprintf(p5, sizeof p5, "%s/prog.log", dir); CHECK(p1[0] != 0, "tmpdir ready"); test_csv_escape(p1); test_trim(p2); test_init_clear(p3); + test_icon_fallback(p4); + test_progress_live(p5); /* 清理:删除日志文件 + 目录(保底) */ - unlink(p1); unlink(p2); unlink(p3); rmdir(dir); + unlink(p1); unlink(p2); unlink(p3); unlink(p4); unlink(p5); rmdir(dir); fprintf(stderr, "\n%s (failures=%d)\n", g_fail ? "== TEST FAILED ==" : "== TEST PASSED ==", g_fail); return g_fail ? 1 : 0; diff --git a/patch_v2/sdk/meow_notify_ipc.c b/patch_v2/sdk/meow_notify_ipc.c index a4c768b..6486fed 100644 --- a/patch_v2/sdk/meow_notify_ipc.c +++ b/patch_v2/sdk/meow_notify_ipc.c @@ -6,13 +6,15 @@ * 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。 + * - MSG_NOTIFY_DISMISS(0x0022) ↑ 负载 = uint32 nid; + * - MSG_NOTIFY_PROGRESS(0x0024) ↑ 负载 = m_notify_progress_t(nid+progress,实时进度仅内存)。 * * 设计要点: * - 只做「一次性 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; + * SDK `icon` → app_icon(空则 launcher 按 app_id 回退 app 图标);urgency 0-3 直通(越界钳位 NORMAL); * - 优雅降级:无 socket(connect 失败)/写失败/ACK 超时(SO_RCVTIMEO)/非 ACK 应答 → * 返回 0(调用方 meow_ui_notify.c 保留本地 id 兜底),绝不崩; * - 线程安全:app_id 经 pthread_once 缓存;socket 路径每次调用读 env MEOW_IPC_SOCK 覆盖 @@ -82,8 +84,8 @@ static uint32_t timeout_to_expire(int timeout_ms) return (uint32_t)timeout_ms; /* 0(NEVER)/ >0(毫秒) */ } -/* 构建 IPC 负载 m_notify_t(app_icon/actions/hints v1 留空,M2b D-Bus 桥再填)。 - * 仅在持有调用者互斥之外、发送前调用(纯 CPU 拷贝,不阻塞)。 */ +/* 构建 IPC 负载 m_notify_t(app_icon<->SDK icon;urgency 0-3;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); @@ -91,6 +93,7 @@ static void build_m_notify(const meow_notify *n, uint32_t replaces_id, m_notify_ 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->app_icon, sizeof mn->app_icon, "%s", n->icon ? n->icon : ""); snprintf(mn->target_activity, sizeof mn->target_activity, "%s", n->target_activity ? n->target_activity : ""); mn->replaces_id = replaces_id; /* 0=新建;>0=更新(不存在则落为新建,见 launcher) */ @@ -98,6 +101,9 @@ static void build_m_notify(const meow_notify *n, uint32_t replaces_id, m_notify_ 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; + /* urgency:越界钳位到 NORMAL(缺省);0-3 直通(launcher 侧再回退缺省) */ + mn->urgency = ((int)n->urgency >= 0 && (int)n->urgency < (int)MEOW_NOTIFY_URG_COUNT) + ? (uint8_t)n->urgency : (uint8_t)MEOW_NOTIFY_URG_NORMAL; } /* ============================================================ */ @@ -241,3 +247,36 @@ void meow_notify_ipc_dismiss(uint32_t nid) } close(fd); } + +void meow_notify_ipc_update_progress(uint32_t nid, int progress) +{ + int fd; + if (nid == 0) return; + m_notify_progress_t p; + memset(&p, 0, sizeof p); + p.nid = nid; + p.progress = (progress < -1) ? -1 : (progress > 100 ? 100 : progress); + fd = conn_path(ipc_sock_path()); + if (fd < 0) return; /* 无 socket:静默 */ + set_ack_timeout(fd); + if (!send_frame(fd, MSG_NOTIFY_PROGRESS, 0x4E4F5450u /* "NOTP" */, &p, (uint32_t)sizeof p)) { + close(fd); /* 写失败:静默 */ + return; + } + /* 读回应答(launcher 对 PROGRESS 回 MSG_ACK,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/patch_v2/sdk/meow_notify_ipc.h b/patch_v2/sdk/meow_notify_ipc.h index c1da243..f02a5a0 100644 --- a/patch_v2/sdk/meow_notify_ipc.h +++ b/patch_v2/sdk/meow_notify_ipc.h @@ -4,9 +4,10 @@ * 定位(任务 #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 0x0020 ↑:推送/更新通知(负载 m_notify_t;含 SDK icon→app_icon、urgency); * - MSG_NOTIFY_ACK 0x0021 ↓:落库结果(m_ack_t,status=launcher 分配的通知 id nid); - * - MSG_NOTIFY_DISMISS 0x0022 ↑:撤下通知(负载 uint32 nid)。 + * - MSG_NOTIFY_DISMISS 0x0022 ↑:撤下通知(负载 uint32 nid); + * - MSG_NOTIFY_PROGRESS 0x0024 ↑:实时更新通知进度(负载 m_notify_progress_t,仅改内存活跃槽)。 * * 约定:只做「一次性发送 + 读 ACK」,不造 IPC 订阅循环;SDK 本地活动表(meow_ui_notify.c) * 保留(app 进程内即时查询/配额自检),真正的通知中心数据在 launcher 中央存储,二者以 @@ -38,6 +39,10 @@ uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id); * 静默(不崩、不回读 ACK)。 */ void meow_notify_ipc_dismiss(uint32_t nid); +/* 一次性发送 MSG_NOTIFY_PROGRESS 更新 launcher 侧已存在通知的进度(仅改内存活跃槽, + * 不落盘)。best-effort:写失败/无 socket 静默。 */ +void meow_notify_ipc_update_progress(uint32_t nid, int progress); + #ifdef __cplusplus } #endif diff --git a/patch_v2/sdk/meow_ui.h b/patch_v2/sdk/meow_ui.h index 298e379..3614006 100644 --- a/patch_v2/sdk/meow_ui.h +++ b/patch_v2/sdk/meow_ui.h @@ -297,12 +297,13 @@ typedef enum { } meow_notify_urgency; /* 发送负载(对齐 freedesktop Notify() 字段;SDK 内部复制副本,不持 app 外部指针, - * app 只需保证 title/body/target_activity 指针在调用返回前有效)。 */ + * app 只需保证 icon/title/body/target_activity 指针在调用返回前有效)。 */ typedef struct meow_notify { meow_notify_category cat; /* 类型(未声明则按 4 种上限自动注册) */ - meow_notify_urgency urgency; /* 紧急度 */ + meow_notify_urgency urgency; /* 紧急度(缺省 MEOW_NOTIFY_URG_NORMAL;越界钳位) */ const char *title; /* summary:标题,单行(必填,NULL 则发送失败) */ const char *body; /* body:正文,可多行(可 NULL) */ + const char *icon; /* app_icon:图标 icon name 或 path;NULL/空 = 用该 app 默认图标(launcher 按 app_id 回退) */ int timeout_ms; /* 见上超时语义;推荐 MEOW_NOTIFY_TIMEOUT_* */ const char *target_activity; /* 通知中心点击跳转的 Activity 名(可 NULL;M3 导航预留) */ int progress; /* 进度 0-100;-1=不显示进度条(缺省 -1) */ @@ -318,6 +319,11 @@ uint32_t meow_notify_send(const meow_notify *n); /* 取消一条通知。1=成功;0=未找到(id 无效/已过期/已取消)。 */ int meow_notify_cancel(uint32_t id); +/* 实时更新一条活动通知的进度(0-100,-1=清除进度条)。local_id=meow_notify_send 的返回值。 + * 语义(用户定案):仅改 SDK 内部活动槽 + 经 IPC 通知 launcher 改**内存活跃槽**;不落盘—— + * TF 快照只在「创建时 / 结束(100% 或 dismiss)」触发。返回 1=找到并更新;0=未找到/非法。 */ +int meow_notify_update_progress(uint32_t local_id, int progress); + /* 查询当前活动(未过期、未取消)通知数/是否还有。 */ int meow_notify_active_count(void); int meow_notify_count_in_type(meow_notify_category cat); diff --git a/patch_v2/sdk/meow_ui_gfx.c b/patch_v2/sdk/meow_ui_gfx.c index d6a9798..e7841af 100644 --- a/patch_v2/sdk/meow_ui_gfx.c +++ b/patch_v2/sdk/meow_ui_gfx.c @@ -67,8 +67,9 @@ static char *g_dump_path = NULL; * g_sbar_collapsed = 目标态(1=上收);g_sbar_collapse_h = 当前实际上收偏移 * (0..MUI_SBAR_H:0=完全展开,MUI_SBAR_H=内容全部上移出屏、仅留提示线)。 * 基座让 meow_ui_statusbar_set_collapsed(on) 立即生效(true→只画提示线/内容上移); - * 过渡(每帧插值)留待后续代理或经 meow_ui_statusbar_animate(ms) 启用的简易线性逼近。 */ -#define MUI_SBAR_HINT_H 6 /* 上收状态下顶部提示线高(px) */ + * 过渡(每帧插值)留待后续代理或经 meow_ui_statusbar_animate(ms) 启用的简易线性逼近。 + * 「收合=只留提示线」占高 = MUI_SBAR_COLLAPSE_H(=MEOW_SBAR_HOTZONE_FS=4,meow_ui_sys.h), + * 与 inset(meow_ui_statusbar_height())/顶部下滑热区/收合渲染同口径——replaced MUI_SBAR_HINT_H。 */ static int g_sbar_collapsed = 0; /* 目标:1=上收 */ static int g_sbar_collapse_h = 0; /* 当前上收偏移(0..MUI_SBAR_H) */ static int g_sbar_collapse_anim_ms = 0; /* 过渡时长(0=瞬切);meow_ui_statusbar_animate 设置 */ @@ -357,7 +358,8 @@ void meow_ui_swipe_hint(int x) if (!g_init_ok) return; const meow_theme *th = meow_theme_get(); /* 进度条画在状态栏下沿(z 在状态栏之下,不被全局状态栏覆盖)。 - * 有状态栏(非全屏)→ y = 状态栏高度;无状态栏(全屏/隐藏)→ y=0。 */ + * 有状态栏(非全屏)→ y = 状态栏高度;收合(collapsed)→ y=提示线下缘 + * (MUI_SBAR_COLLAPSE_H,与提示线不重叠);无状态栏(全屏/隐藏)→ y=0。 */ int hy = meow_ui_statusbar_height(); if (x > 0) { if (x > g_xres) x = g_xres; @@ -544,15 +546,16 @@ static int statusbar_collapse_in_transition(void) /* 上收状态栏:内容整体上移出屏(不画时钟/电量),仅在顶部画一条「提示线」。 * 与 meow_ui_swipe_hint(左滑返回进度条,5px,动态)不同:此线是「状态栏可下拉还原」的 - * 常驻提示(主题强调色下边缘)。原状态栏整块清回主题背景,避免残影(app inset 之内)。 */ + * 常驻提示(主题强调色下边缘)。收合只占/清「提示线」高度区域(MUI_SBAR_COLLAPSE_H=4), + * 不再清整 MUI_SBAR_H(28) 行 —— app 内容按 inset=提示线高可上移到提示线下缘,无 28px 空条。 */ static void statusbar_collapsed_render(int h) { (void)h; const meow_theme *th = meow_theme_get(); int w = meow_ui_width(); - /* h 越大内容越贴顶;基座:折叠态整块清底 + 顶部提示线(提示线下缘=主题 accent) */ - meow_ui_fill_rect(0, 0, w, MUI_SBAR_H, th->bg); - meow_ui_fill_rect(0, MUI_SBAR_HINT_H - 1, w, 1, th->accent); + /* h 越大内容越贴顶;基座:折叠态清「提示线」区域底 + 顶部提示线(提示线下缘=主题 accent) */ + meow_ui_fill_rect(0, 0, w, MUI_SBAR_COLLAPSE_H, th->bg); + meow_ui_fill_rect(0, MUI_SBAR_COLLAPSE_H - 1, w, 1, th->accent); } /* ---- 公共 API(基座实现;动画插值骨架见 statusbar_collapse_step)---- */ @@ -576,9 +579,10 @@ void meow_ui_statusbar_animate(int ms) int meow_ui_statusbar_hotzone(void) { /* 非全屏=整条状态栏可视;全屏时缩小到 MEOW_SBAR_HOTZONE_FS(保留一处下拉入口)。 - * 上收(collapsed、非全屏)时热区=提示线下缘高度。全屏态由 meow_ui_sys 维护。 */ + * 上收(collapsed、非全屏)时热区=提示线下缘高度(MUI_SBAR_COLLAPSE_H,与渲染/inset 同口径)。 + * 全屏态由 meow_ui_sys 维护。 */ if (meow_ui_sys_statusbar_active()) - return g_sbar_collapsed ? MUI_SBAR_HINT_H : MUI_SBAR_H; + return g_sbar_collapsed ? MUI_SBAR_COLLAPSE_H : MUI_SBAR_H; return MEOW_SBAR_HOTZONE_FS; } @@ -602,10 +606,12 @@ static void present(void) if (!g_refresh_always_max) { if (g_last_present_ms && now - g_last_present_ms < (unsigned long long)period) return; } - /* 全局状态栏(A-0)+ 上收(collapsed)基座:上屏前把状态栏画进 back_buf 顶部 MUI_SBAR_H 行。 - * 全屏/隐藏时 active()==0 不画(沉浸),app 内容可占满整屏。触发条件: - * 数据已变(1s 轻刷/显隐切换) / 顶条被 app 内容触及 / 上收动画未到位(逐帧推进)。 - * 展开态仍走 meow_ui_sys_statusbar_render()(meow_ui_sys.c 不改,留给后续代理); + /* 全局状态栏(A-0)+ 上收(collapsed)基座:上屏前把状态栏画进 back_buf 顶部行。 + * 展开态=整条(MUI_SBAR_H 行);收合态只占/清「提示线」区域(MUI_SBAR_COLLAPSE_H 行), + * app 内容(inset=提示线高)可上移到提示线下缘,无 28px 空条。全屏/隐藏时 active()==0 + * 不画(沉浸),app 内容可占满整屏。触发条件:数据已变(1s 轻刷/显隐切换) / + * 顶条被 app 内容触及 / 上收动画未到位(逐帧推进)。展开态仍走 + * meow_ui_sys_statusbar_render()(meow_ui_sys.c 不改,留给后续代理); * 上收折叠态(collapse_h>=MUI_SBAR_H)由基座只画提示线(内容上移出屏)。 */ if (meow_ui_sys_statusbar_active() && (meow_ui_sys_statusbar_force() || statusbar_touched() || diff --git a/patch_v2/sdk/meow_ui_input.c b/patch_v2/sdk/meow_ui_input.c index 7a155d2..8c7d0b9 100644 --- a/patch_v2/sdk/meow_ui_input.c +++ b/patch_v2/sdk/meow_ui_input.c @@ -317,8 +317,12 @@ void meow_ui_run(const meow_ui_events *ev) if (in_startup) continue; /* 启动窗口进度条静音 */ int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */ /* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。 - * 全屏/沉浸不画系统条(无返回手势);APP 若注册 on_swipe_progress 仍可自绘。 */ - if (meow_ui_sys_statusbar_active()) meow_ui_swipe_hint(xc); + * M2.5x/A-13:全屏/收合下「左缘返回手势保留」,顶部动态提示条(meow_ui_swipe_hint) + * 仍需绘制——不再以 meow_ui_sys_statusbar_active()(全屏/隐藏=0)开关,否则全屏 + * 时滑条完全不显示。条位 y=meow_ui_statusbar_height():非全屏=状态栏下沿、 + * 全屏=0、收合=提示线下缘(MUI_SBAR_COLLAPSE_H),与提示线不重叠加。 + * APP 若注册 on_swipe_progress 仍可自绘(系统条照常画)。 */ + meow_ui_swipe_hint(xc); if (g_events_set && g_events.on_swipe_progress) g_events.on_swipe_progress(x0, dx); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); diff --git a/patch_v2/sdk/meow_ui_notify.c b/patch_v2/sdk/meow_ui_notify.c index dbad728..489ab52 100644 --- a/patch_v2/sdk/meow_ui_notify.c +++ b/patch_v2/sdk/meow_ui_notify.c @@ -35,7 +35,10 @@ /* ---- 内部活动表尺寸(配额见 meow_ui.h) ---- */ #define MEOW_NOTIFY_MAX_ACTIVE (MEOW_NOTIFY_MAX_TYPES * MEOW_NOTIFY_MAX_PER_TYPE) -/* 活动通知槽(内部;标题/正文等复制副本,不持 app 外部指针,避免栈字符串悬垂) */ +/* 活动槽内 icon 缓冲上限(与 IPC app_icon[48] 一致;SDK 复制副本,不持 app 外部指针) */ +#define MEOW_SLOT_ICON_MAX 48 + +/* 活动通知槽(内部;标题/正文/图标等复制副本,不持 app 外部指针,避免栈字符串悬垂) */ typedef struct { uint32_t id; /* 本地唯一 id(app 侧句柄);0=空闲槽 */ uint32_t ipc_id; /* launcher 中央存储分配的通知 id(nid);0=未同步/降级 */ @@ -47,6 +50,7 @@ typedef struct { char title[64]; char body[128]; char target_activity[32]; + char icon[MEOW_SLOT_ICON_MAX]; /* 图标 name/path;空=用 app 默认图标(launcher 回退) */ int progress; /* 0-100;-1=无进度条 */ } notify_slot_t; @@ -111,6 +115,7 @@ static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uin snprintf(s->body, sizeof s->body, "%s", n->body ? n->body : ""); snprintf(s->target_activity, sizeof s->target_activity, "%s", n->target_activity ? n->target_activity : ""); + snprintf(s->icon, sizeof s->icon, "%s", n->icon ? n->icon : ""); s->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress); } @@ -253,6 +258,28 @@ int meow_notify_cancel(uint32_t id) return rc; } +int meow_notify_update_progress(uint32_t local_id, int progress) +{ + int p, i, found = 0; + uint32_t ipc_id = 0; + if (local_id == 0) return 0; + p = (progress < -1) ? -1 : (progress > 100 ? 100 : progress); /* 钳位 [-1,100] */ + 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 == local_id) { + g_slots[i].progress = p; /* 实时:仅改内存活动槽,不落盘 */ + ipc_id = g_slots[i].ipc_id; /* launcher 侧 nid;0=未同步/降级 */ + found = 1; + break; + } + } + pthread_mutex_unlock(&g_mu); + /* 落点(best-effort):经 IPC 通知 launcher 改其内存活跃槽;无 launcher/未同步则静默。 */ + if (found && ipc_id) meow_notify_ipc_update_progress(ipc_id, p); + return found; +} + int meow_notify_active_count(void) { int i, n = 0; diff --git a/patch_v2/sdk/test_notify_integration.c b/patch_v2/sdk/test_notify_integration.c index 6ea53d9..2f50f0a 100644 --- a/patch_v2/sdk/test_notify_integration.c +++ b/patch_v2/sdk/test_notify_integration.c @@ -41,7 +41,7 @@ uint64_t meow_ui_time_ms(void) /* ---- stub server 捕获(每次连接一帧) ---- */ #define MAXCAP 16 -typedef struct { int type; m_notify_t n; uint32_t dismiss_id; uint32_t nid; } srec_t; +typedef struct { int type; m_notify_t n; m_notify_progress_t prog; 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; @@ -77,6 +77,7 @@ static void *server(void *arg) 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_PROGRESS && len >= sizeof(m_notify_progress_t)) memcpy(&g_rec[idx].prog, pay, sizeof(m_notify_progress_t)); if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_rec[idx].dismiss_id, pay, 4); /* 分配 nid = 100 + NOTIFY 序号 */ uint32_t seq = 0; @@ -85,7 +86,7 @@ static void *server(void *arg) uint32_t ack_nid = g_rec[idx].nid; pthread_mutex_unlock(&g_mu); - if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS) { + if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS || type == MSG_NOTIFY_PROGRESS) { 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) @@ -140,11 +141,13 @@ int main(void) /* 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; + nt.title = "t1"; nt.timeout_ms = 0; nt.icon = "os.meow.hello/icon.png"; 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); + check("S1 映射 urgency=NORMAL(1)", rec_at(0).n.urgency == MEOW_NOTIFY_URG_NORMAL); + check("S1 映射 icon→app_icon", !strcmp(rec_at(0).n.app_icon, "os.meow.hello/icon.png")); /* S2 媒体通知:单例 + media=1 */ memset(&nt, 0, sizeof nt); @@ -182,6 +185,25 @@ int main(void) 触发丢最旧的是 S5 循环第 4 次 send = 第 7 条 NOTIFY(rec[7]),其 replaces_id==103。 */ check("S5 丢最旧后 send replaces_id=被丢nid(103)", rec_at(7).n.replaces_id == 103); + /* S5b update_progress:新发一条 progress → 本地 id → update → stub 捕获 MSG_NOTIFY_PROGRESS */ + memset(&nt, 0, sizeof nt); + nt.cat = MEOW_NOTIFY_CAT_PROGRESS; nt.title = "prog"; nt.timeout_ms = 0; + nt.progress = 10; + uint32_t idp = meow_notify_send(&nt); + int ni = -1; + for (int i = 0; i < MAXCAP; i++) { + srec_t r = rec_at(i); + if (r.type == MSG_NOTIFY && !strcmp(r.n.summary, "prog")) { ni = i; break; } + } + check("S5b 找到 prog 通知帧", ni >= 0); + int rc_up = meow_notify_update_progress(idp, 70); + check("S5b update_progress 返回 1", rc_up == 1); + srec_t pr = rec_at(ni + 1); + check("S5b stub 捕获 MSG_NOTIFY_PROGRESS 帧", pr.type == MSG_NOTIFY_PROGRESS); + check("S5b progress nid=该通知 launcher nid", ni >= 0 && pr.prog.nid == rec_at(ni).nid); + check("S5b progress 载=70", pr.prog.progress == 70); + check("S5b update_progress(未知 id) 返回 0", meow_notify_update_progress(0xFFFFFF, 50) == 0); + stop_server(); pthread_join(th, NULL); diff --git a/patch_v2/sdk/test_notify_ipc.c b/patch_v2/sdk/test_notify_ipc.c index f1ba227..c7f5dc3 100644 --- a/patch_v2/sdk/test_notify_ipc.c +++ b/patch_v2/sdk/test_notify_ipc.c @@ -30,6 +30,7 @@ typedef struct { uint32_t type; /* 收到的帧类型 */ m_notify_t n; /* MSG_NOTIFY 负载 */ + m_notify_progress_t prog; /* MSG_NOTIFY_PROGRESS 负载 */ uint32_t dismiss_id; /* MSG_NOTIFY_DISMISS 负载 */ uint32_t req_id; uint32_t nid_to_return; /* ACK 中回填的 status */ @@ -47,6 +48,7 @@ static void cap_store(uint32_t type, uint32_t req_id, const uint8_t *pay, uint32 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_PROGRESS && len >= sizeof(m_notify_progress_t)) memcpy(&g_cap.prog, pay, sizeof(m_notify_progress_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); @@ -87,7 +89,7 @@ static void *server_thread(void *arg) /* 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) { + if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS || type == MSG_NOTIFY_PROGRESS) { m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; @@ -152,9 +154,10 @@ int main(void) meow_notify nt; memset(&nt, 0, sizeof nt); nt.cat = MEOW_NOTIFY_CAT_PROGRESS; - nt.urgency = MEOW_NOTIFY_URG_NORMAL; + nt.urgency = MEOW_NOTIFY_URG_HIGH; nt.title = "下载中"; nt.body = "70%"; + nt.icon = "os.meow.hello/icon.png"; nt.timeout_ms = 5000; /* >0 → expire=5000, ongoing=0 */ nt.progress = 150; /* 钳位 → 100 */ uint32_t nid = meow_notify_ipc_send(&nt, 0); @@ -175,6 +178,8 @@ int main(void) 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, "下载中")); + check("T1 urgency(HIGH)→wire urgency=2", cn->urgency == MEOW_NOTIFY_URG_HIGH); + check("T1 icon→app_icon", !strcmp(cn->app_icon, "os.meow.hello/icon.png")); } /* ---------- T1/T2:媒体类型 + ONGOING(-1) ---------- */ @@ -275,6 +280,21 @@ int main(void) check("T5 dismiss 负载=nid=0xABCD", g_cap.dismiss_id == 0xABCDu); } + /* ---------- T6:update_progress 发 MSG_NOTIFY_PROGRESS(nid + progress 钳位)---------- */ + { + 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_update_progress(0x1234, 250); /* 钳位 250→100 */ + pthread_join(th, NULL); + close(g_listen_fd); + check("T6 stub 收到 MSG_NOTIFY_PROGRESS 帧", g_cap.got && g_cap.type == MSG_NOTIFY_PROGRESS); + check("T6 progress 负载 nid=0x1234", g_cap.prog.nid == 0x1234); + check("T6 progress 钳位 250→100", g_cap.prog.progress == 100); + } + printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", failures, failures == 1 ? "" : "s"); return failures ? 1 : 0; diff --git a/patch_v2/sdk/test_notify_post.c b/patch_v2/sdk/test_notify_post.c index beac070..b3d0440 100644 --- a/patch_v2/sdk/test_notify_post.c +++ b/patch_v2/sdk/test_notify_post.c @@ -49,6 +49,7 @@ static void launcher_map(const m_notify_t *mn, meow_notif_t *n) snprintf(n->target_activity, sizeof n->target_activity, "%s", mn->target_activity); n->expire_timeout = mn->expire_timeout; n->progress = mn->progress; + n->urgency = mn->urgency; /* 0-3;旧调用=0 → LOW(静默) */ n->ongoing = mn->ongoing; n->media = mn->media; } @@ -59,6 +60,15 @@ static void launcher_map(const m_notify_t *mn, meow_notif_t *n) static int g_listen_fd = -1; static int g_stop = 0; static uint32_t g_posted_nid = 0; +static meow_notif_t g_stored; /* 回读 launcher 存储的最近一条通知(store_cb) */ +static int g_store_called = 0; + +static int store_cb(const meow_notif_t *n, void *ctx) +{ + (void)ctx; + if (n) { g_stored = *n; g_store_called = 1; } + return 1; /* 只取第一条 */ +} static void reply_ack(int c, uint16_t type, uint32_t req_id, uint32_t status) { @@ -130,13 +140,21 @@ int main(void) /* 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.cat = MEOW_NOTIFY_CAT_MEDIA; nt.urgency = MEOW_NOTIFY_URG_HIGH; nt.title = "正在播放"; nt.timeout_ms = -1; nt.progress = 40; + nt.icon = "os.meow.hello/icon.png"; 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); + /* 回读 launcher 存储,验证 urgency/icon/progress 全链路落库 */ + g_store_called = 0; memset(&g_stored, 0, sizeof g_stored); + meow_notif_list(NULL, store_cb, NULL); + check("post 存储 urgency=HIGH(2)", g_store_called && g_stored.urgency == MEOW_NOTIFY_URG_HIGH); + check("post 存储 icon→app_icon", g_stored.app_icon[0] && !strcmp(g_stored.app_icon, "os.meow.hello/icon.png")); + check("post 存储 progress=40", g_stored.progress == 40); + /* SDK dismiss → launcher 撤下 */ meow_notify_ipc_dismiss(nid); check("dismiss 后 launcher 存储 count=0", meow_notif_count("intgapp") == 0);