/* meow_notif.c — MeowOS 通知存储 + 分发(阶段 D · M2a) * * 实现与设计:NOTIFICATION_SYSTEM.md §1-§8 + FRAMEWORK_DESIGN §4/附C。 * - 通知数据结构:对齐 freedesktop.org Desktop Notifications 字段 * (app_name/app_icon/summary/body/actions[]/hints{}/expire_timeout) * + MeowOS 扩展(target_activity / progress,来自 hints)。 * - 内存存储:固定活跃槽表(ring 语义——每 App×category 配额,满丢该类型最旧条)。 * - 配额(§7,用户拍板):每 App ≤ notify.categories 个类型;每类型活跃 * 通知 ≤ notify.category_quota;媒体通知每 App ≤ notify.media_quota(允许多媒体 App * 同时)。系统事件=App(app_id="system"),同走配额。配额只管内存活跃态。 * - TTL(§8):通知创建 10s 后自动转储到 TF(无论是否过期/被清)——本批只做 * dump 钩子/日志/回调(notif_dump_cb),实际 TF 落盘 M2c 后置。ONGOING 通知 * 转储后仍保留内存(服务死随清)。 * - 转储钩子:meow_notif_dump_now()(关机前转储所有未上卡); * meow_notif_before_poweroff()(dump + 清板载)。 * * 空洞实现风格:本文件自包含(不依赖 launcher 主模块的 logmsg/now_ms —— 二者现归 util.c),配额经 * meow_settings(B2a)读 notify.categories/category_quota/media_quota(键缺省用默认值), * 文件 I/O 失败仅打点,绝不崩。注释风格中文 +「M2.a 说明」。 */ #include #include #include #include /* strcasecmp */ #include #include #include /* mkdir / stat */ #include #include /* open/close/write/pread/lseek/ftruncate */ #include /* O_* */ #include /* errno/strerror */ #include "meow_notif.h" #include "meow_settings.h" /* B2a:读 notify.* 配额(MEOW_OK/MEOW_ERR_*) */ /* == 应用/槽位容量(确定静态内存;配额常量由 meow_settings 或默认覆盖)== */ #define MEOW_NOTIF_MAX_APPS 8 /* 同时跟踪的应用数(含 "system") */ #define MEOW_NOTIF_MAX_CATS 4 /* 每 App 类型上限(notify.categories 上限) */ #define MEOW_NOTIF_MAX_SLOTS (MEOW_NOTIF_MAX_APPS * MEOW_NOTIF_MAX_CATS * MEOW_NOTIF_CAT_QUOTA_DEF) /* 128 */ /* 应用条目:已声明类型集 + 活跃媒体通知计数 */ typedef struct notif_app { char app_id[MEOW_NOTIF_APP_ID_MAX]; char cats[MEOW_NOTIF_MAX_CATS][MEOW_NOTIF_CAT_MAX]; int ncats; int media_count; /* 当前活跃媒体通知数 */ } notif_app_t; static notif_app_t g_apps[MEOW_NOTIF_MAX_APPS]; static int g_napps = 0; static meow_notif_t g_slots[MEOW_NOTIF_MAX_SLOTS]; static uint32_t g_next_id = 1; static int g_dumped_total = 0; /* 会话内累计转储计数(统计用) */ /* 配额(运行时从 meow_settings notify.* 读;键缺省用默认) */ static int g_max_cats = MEOW_NOTIF_CATS_DEF; 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:")---- */ static void notif_log(const char *fmt, ...) { char msg[256]; va_list ap; va_start(ap, fmt); vsnprintf(msg, sizeof msg, fmt, ap); va_end(ap); fprintf(stderr, "notif: %s\n", msg); } /* ---- 时钟:墙钟 epoch ms(created_ms,供归档/显示)+ monotonic ms(TTL 判定)---- */ static uint64_t wall_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; } static uint64_t mono_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; } /* ---- 短串拷贝(保证 NUL 终止 + 截断到 len-1)---- */ static void n_strncpy(char *dst, const char *src, size_t len) { if (!dst || len == 0) return; if (!src) { dst[0] = 0; return; } strncpy(dst, src, len - 1); 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)。 * 字段见 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)/app_icon(48)/target_activity(48) * 全引号转义后仍远小于此。 */ #define MEOW_NOTIF_CSV_LINE_MAX 1024 static const char *g_tf_path = MEOW_NOTIF_TF_LOG; static size_t g_tf_max = MEOW_NOTIF_LOG_MAX_BYTES; /* 递归 mkdir -p:沿 path 把每一级目录建出来(EEXIST 无害;无卡/只读则静默失败) */ static void tf_mkdirs(const char *path) { char tmp[512]; n_strncpy(tmp, path, sizeof tmp); for (char *p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; } } } /* 全量写入(处理短写/EINTR) */ static int tf_write_all(int fd, const char *buf, size_t len) { size_t off = 0; while (off < len) { ssize_t w = write(fd, buf + off, len - off); if (w < 0) { if (errno == EINTR) continue; return -1; } off += (size_t)w; } return 0; } /* CSV 字段:去换行(\r\n->空格,保证单行)+ 转义(双引号包裹,内部 " 翻倍) */ static void csv_field(char *dst, size_t dstsz, const char *src) { if (!dst || dstsz == 0) return; size_t o = 0; dst[o++] = '"'; for (const char *p = src ? src : ""; *p; p++) { if (o + 4 > dstsz) break; /* 留 2 字节给翻倍引号 + 收尾引号 + NUL */ if (*p == '\r' || *p == '\n') dst[o++] = ' '; /* 去换行 */ else if (*p == '"') { dst[o++] = '"'; dst[o++] = '"'; } else dst[o++] = *p; } if (o + 1 >= dstsz) o = dstsz - 1; dst[o++] = '"'; dst[o] = '\0'; } /* 生成一行 CSV: * 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,%u,%d,%s,%s,%s,%u,%u,%u\n", (unsigned long long)n->created_ms, (unsigned)n->id, 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); if (r < 0) return 0; return ((size_t)r < bufsz) ? (size_t)r : bufsz - 1; } /* 滚动截断:文件超过 g_tf_max 时删除最旧行(从尾部读窗口定位第一条换行作为保留起点), * 把结尾 ≤max 字节的完整行重写回文件头。窗口内无换行则保留整窗(只保证 ≤max)。 */ static int tf_trim(const char *path) { int fd = open(path, O_RDWR); if (fd < 0) return -1; struct stat st; if (fstat(fd, &st) != 0) { close(fd); return -1; } off_t size = st.st_size; if (size <= (off_t)g_tf_max) { close(fd); return 0; } off_t start = size - (off_t)g_tf_max; /* 保留窗口起点 */ off_t keep = start; /* 最终保留起点(行对齐后) */ int found = 0; char chunk[8192]; for (off_t pos = start; pos < size && !found; ) { size_t want = (size_t)((size - pos) < (off_t)sizeof chunk ? (size - pos) : (off_t)sizeof chunk); ssize_t rd = pread(fd, chunk, want, pos); if (rd <= 0) break; for (ssize_t i = 0; i < rd; i++) { if (chunk[i] == '\n') { keep = pos + i + 1; found = 1; break; } } pos += rd; if (rd < (ssize_t)want) break; /* 读到文件尾 */ } /* 重写:把 [keep, size) 拷贝回文件头,再 ftruncate 掉尾部旧数据 */ off_t remain = size - keep; if (lseek(fd, 0, SEEK_SET) < 0) { close(fd); return -1; } off_t wpos = 0; char buf[8192]; while (remain > 0) { size_t want = (size_t)(remain < (off_t)sizeof buf ? remain : (off_t)sizeof buf); ssize_t rd = pread(fd, buf, want, keep + wpos); if (rd <= 0) break; if (tf_write_all(fd, buf, (size_t)rd) < 0) { close(fd); return -1; } wpos += rd; remain -= rd; } ftruncate(fd, wpos); close(fd); return 0; } /* 把一条通知追加为一行 CSV 到 TF 日志(含 mkdir -p 与 10MB 滚动截断)。 * 返回 NOTIF_OK(0);无卡/不可写/参数非法返回负(降级:仅打点,不崩)。 */ int meow_notif_dump_tf(const meow_notif_t *n) { if (!n) return NOTIF_ERR_INVAL; if (!g_tf_path || !g_tf_path[0]) return NOTIF_ERR_INVAL; tf_mkdirs(g_tf_path); /* 目录不存在则 mkdir -p*/ char line[MEOW_NOTIF_CSV_LINE_MAX]; size_t len = tf_line(n, line, sizeof line); if (len == 0) return NOTIF_ERR_INVAL; int fd = open(g_tf_path, O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd < 0) { notif_log("tf: append open fail %s (%s)", g_tf_path, strerror(errno)); return NOTIF_ERR_IO; } if (tf_write_all(fd, line, len) != 0) { close(fd); notif_log("tf: append write fail %s (%s)", g_tf_path, strerror(errno)); return NOTIF_ERR_IO; } long size = -1; struct stat st; if (fstat(fd, &st) == 0) size = (long)st.st_size; close(fd); if (size > (long)g_tf_max) { /* >10MB:滚动截断,保留最新 */ tf_trim(g_tf_path); notif_log("tf: rolled to <= %zuB", g_tf_max); } return NOTIF_OK; } /* 开机清板载通知:清空转储日志文件,保留目录(通知#6 / §8:启动时清板载) */ void meow_notif_clear_tf(void) { if (!g_tf_path || !g_tf_path[0]) return; tf_mkdirs(g_tf_path); int fd = open(g_tf_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd >= 0) close(fd); else notif_log("tf: clear fail %s (%s)", g_tf_path, strerror(errno)); } /* 覆盖转储日志路径/滚动上限(宿主单测用;NULL/0 恢复默认) */ void meow_notif_set_tf_path(const char *path) { g_tf_path = (path && path[0]) ? path : MEOW_NOTIF_TF_LOG; } void meow_notif_set_tf_max(size_t max_bytes) { g_tf_max = max_bytes ? max_bytes : MEOW_NOTIF_LOG_MAX_BYTES; } /* ---- 配额读取(B2a;键缺省/取失败用默认值)---- */ static void read_quota(void) { int v; if (meow_settings_get_int("notify.categories", &v) == MEOW_OK && v > 0) g_max_cats = v; if (meow_settings_get_int("notify.category_quota", &v) == MEOW_OK && v > 0) g_cat_quota = v; if (meow_settings_get_int("notify.media_quota", &v) == MEOW_OK && v > 0) g_media_quota = v; notif_log("quota: cats=%d cat_quota=%d media_quota=%d", g_max_cats, g_cat_quota, g_media_quota); } /* ---- 生命周期 ---- */ int meow_notif_init(void) { if (g_inited) return NOTIF_OK; memset(g_apps, 0, sizeof g_apps); memset(g_slots, 0, sizeof g_slots); g_napps = 0; g_next_id = 1; g_dumped_total = 0; read_quota(); /* B2a:notify.* 配额 */ g_inited = 1; /* 通知#6:启动清板载通知 + 内存表(内存表上面已 memset;这里清 TF 转储日志, * 保留目录)。每会话从零开始(通知会话级),不被电源事件清(before_poweroff 只管内存)。 */ meow_notif_clear_tf(); notif_log("init: slots=%d tf_log=%s (dump_cb wired by launcher -> meow_notif_dump_tf)", MEOW_NOTIF_MAX_SLOTS, g_tf_path); return NOTIF_OK; } int meow_notif_shutdown(void) { if (!g_inited) return NOTIF_OK; /* 退出前归档所有未转储项(不清理;before_poweroff 负责清板载) */ meow_notif_dump_now(); return NOTIF_OK; } /* ---- App 表查找/创建 ---- */ static notif_app_t *find_app(const char *app_id) { for (int i = 0; i < g_napps; i++) if (!strcmp(g_apps[i].app_id, app_id)) return &g_apps[i]; return NULL; } static notif_app_t *get_or_add_app(const char *app_id) { notif_app_t *a = find_app(app_id); if (a) return a; if (g_napps >= MEOW_NOTIF_MAX_APPS) return NULL; /* 满:丢弃(防御) */ a = &g_apps[g_napps++]; memset(a, 0, sizeof *a); n_strncpy(a->app_id, app_id, sizeof a->app_id); return a; } /* 某 App 已声明的类型是否包含 cat;无=返回 -1 */ static int app_cat_index(const notif_app_t *a, const char *cat) { for (int i = 0; i < a->ncats; i++) if (!strcmp(a->cats[i], cat)) return i; return -1; } /* 声明类型:已在集合→ok;否则若未超上限则加入;超上限→返回 NOTIF_ERR_QUOTA。 */ static int declare_cat(notif_app_t *a, const char *cat) { if (app_cat_index(a, cat) >= 0) return NOTIF_OK; if (a->ncats >= MEOW_NOTIF_MAX_CATS || a->ncats >= g_max_cats) return NOTIF_ERR_QUOTA; /* App 类型数超限 */ n_strncpy(a->cats[a->ncats++], cat, sizeof a->cats[0]); return NOTIF_OK; } /* ---- 媒体通知判定:category 命中 MEOW_NOTIF_MEDIA_CAT(前缀)---- */ static int is_media_cat(const char *cat) { return (cat && strncasecmp(cat, MEOW_NOTIF_MEDIA_CAT, strlen(MEOW_NOTIF_MEDIA_CAT)) == 0); } /* ---- 槽位 ---- */ static int slot_free_index(void) { for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) if (!g_slots[i].active) return i; return -1; } /* 统计某 App × 某类型 的活跃(active 且未撤下)通知数 —— 配额「活跃(未过期)」含义: * 瞬时通知 10s 后已释放(不占位);ONGOING 常驻(转储后仍占位)——故只看 dismissed。 */ static int count_active_cat(const notif_app_t *a, const char *cat) { int n = 0; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (s->active && !s->dismissed && !strcmp(s->app_id, a->app_id) && !strcmp(s->category, cat)) n++; } return n; } /* 统计某 App 的活跃媒体通知数(同样只看 dismissed;ONGOING 媒体仍占配额) */ static int count_active_media(const notif_app_t *a) { int n = 0; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (s->active && !s->dismissed && s->media && !strcmp(s->app_id, a->app_id)) n++; } return n; } /* 找某 App × 某类型 最旧(created_ms 最小)活跃通知槽;无=返回 -1 */ static int oldest_slot_cat(const notif_app_t *a, const char *cat) { int best = -1; uint64_t best_ms = UINT64_MAX; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (s->active && !s->dismissed && !strcmp(s->app_id, a->app_id) && !strcmp(s->category, cat)) { if (s->created_ms < best_ms) { best_ms = s->created_ms; best = i; } } } return best; } /* 找某 App 最旧媒体通知槽;无=返回 -1 */ static int oldest_media_slot(const notif_app_t *a) { int best = -1; uint64_t best_ms = UINT64_MAX; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (s->active && !s->dismissed && s->media && !strcmp(s->app_id, a->app_id)) { if (s->created_ms < best_ms) { best_ms = s->created_ms; best = i; } } } return best; } /* 归档(dump)单条:调 dump_cb + 标 dumped。返回是否首次转储。 */ static int archive_one(meow_notif_t *s) { if (!s->active || s->dumped) return 0; if (g_dump_cb) g_dump_cb(s); /* M2c 替换为 TF 落盘;本批默认日志/空 */ s->dumped = 1; g_dumped_total++; notif_log("dump: id=%u app=%s cat=%s ongoing=%d dumped_total=%d", s->id, s->app_id, s->category, s->ongoing, g_dumped_total); return 1; } /* 释放槽(清 active,槽位可复用) */ static void slot_release(int idx) { if (idx < 0 || idx >= MEOW_NOTIF_MAX_SLOTS) return; memset(&g_slots[idx], 0, sizeof g_slots[idx]); } /* 按 id 找活跃槽;无=返回 -1 */ static int slot_by_id(uint32_t id) { for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) if (g_slots[i].active && g_slots[i].id == id) return i; return -1; } /* ---- 核心:post ---- */ int meow_notif_post(const meow_notif_t *n, uint32_t *out_id) { int rc = NOTIF_OK; if (out_id) *out_id = 0; if (!g_inited) return NOTIF_ERR_NOTINIT; if (!n || !n->app_id[0]) return NOTIF_ERR_INVAL; if (!n->category[0]) return NOTIF_ERR_INVAL; /* 更新(replace)语义:id>0 且已存在 → 就地更新字段 */ if (n->id != 0) { int idx = slot_by_id(n->id); if (idx >= 0) { meow_notif_t *s = &g_slots[idx]; n_strncpy(s->category, n->category, sizeof s->category); n_strncpy(s->summary, n->summary, sizeof s->summary); n_strncpy(s->body, n->body, sizeof s->body); n_strncpy(s->app_icon, n->app_icon, sizeof s->app_icon); n_strncpy(s->actions, n->actions, sizeof s->actions); n_strncpy(s->hints, n->hints, sizeof s->hints); 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; } /* 指定 id 但不存在 → 落为新建(freedesktop 行为:replaces_id 不存在则新建) */ } notif_app_t *a = get_or_add_app(n->app_id); if (!a) { notif_log("post: app table full, drop app=%s", n->app_id); return NOTIF_ERR_NOMEM; } rc = declare_cat(a, n->category); if (rc != NOTIF_OK) { notif_log("post: QUOTA app=%s cat=%s (cats=%d max=%d)", n->app_id, n->category, a->ncats, g_max_cats); return rc; } int media = (n->media != 0) || is_media_cat(n->category); /* 配额 + 满丢该类型最旧条 */ if (media) { if (count_active_media(a) >= g_media_quota) { int old = oldest_media_slot(a); if (old >= 0) { notif_log("post: media quota full app=%s -> drop id=%u", n->app_id, g_slots[old].id); slot_release(old); /* 满丢最旧媒体通知 */ } } } else { if (count_active_cat(a, n->category) >= g_cat_quota) { int old = oldest_slot_cat(a, n->category); if (old >= 0) { notif_log("post: cat quota full app=%s cat=%s -> drop id=%u", n->app_id, n->category, g_slots[old].id); slot_release(old); /* 满丢该类型最旧条 */ } } } int idx = slot_free_index(); if (idx < 0) { notif_log("post: NOMEM slots full app=%s", n->app_id); return NOTIF_ERR_NOMEM; } meow_notif_t *s = &g_slots[idx]; memset(s, 0, sizeof *s); s->id = g_next_id++; n_strncpy(s->app_id, n->app_id, sizeof s->app_id); n_strncpy(s->category, n->category, sizeof s->category); n_strncpy(s->summary, n->summary, sizeof s->summary); n_strncpy(s->body, n->body, sizeof s->body); n_strncpy(s->app_icon, n->app_icon, sizeof s->app_icon); n_strncpy(s->actions, n->actions, sizeof s->actions); n_strncpy(s->hints, n->hints, sizeof s->hints); 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, s->category, s->media, s->ongoing); return NOTIF_OK; } /* ---- dismiss:撤下 + 立即归档 + 释放槽 ---- */ int meow_notif_dismiss(uint32_t id) { if (!g_inited) return NOTIF_ERR_NOTINIT; int idx = slot_by_id(id); if (idx < 0) return NOTIF_ERR_NOENT; meow_notif_t *s = &g_slots[idx]; archive_one(s); /* 未被清/过期也已转储(§8) */ s->dismissed = 1; notif_log("dismiss: id=%u app=%s", id, s->app_id); slot_release(idx); 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) { if (!g_inited) return -NOTIF_ERR_NOTINIT; int n = 0; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (s->active && !s->dismissed && (!app_id || !app_id[0] || !strcmp(s->app_id, app_id))) n++; } return n; } /* ---- list ---- */ int meow_notif_list(const char *app_id, int (*cb)(const meow_notif_t *n, void *ctx), void *ctx) { if (!g_inited) return -NOTIF_ERR_NOTINIT; if (!cb) return -NOTIF_ERR_INVAL; int n = 0; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (!s->active || s->dismissed) continue; if (app_id && app_id[0] && strcmp(s->app_id, app_id)) continue; n++; if (cb(s, ctx) != 0) break; } return n; } /* ---- dump_now:把当前状态「排空到 TTL 归档态」——归档所有未转储;非 ONGOING 释放槽, * ONGOING 转储后保留(常驻)。等价「立即触发所有 slot 的 §8 转储」。停机前用。 ---- */ int meow_notif_dump_now(void) { if (!g_inited) return -NOTIF_ERR_NOTINIT; int n = 0; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (!s->active || s->dumped) continue; int was_ongoing = s->ongoing; n += archive_one(s); if (!was_ongoing) slot_release(i); /* 与 tick 一致:非 ONGOING 转储后释放槽 */ else notif_log("dump_now: ongoing id=%u archived (kept for UI)", s->id); } return n; } /* ---- tick:TTL 转储 + 释放非 ONGOING;ONGOING 转储后保留 ---- */ void meow_notif_tick(void) { if (!g_inited) return; uint64_t now = mono_ms(); for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (!s->active || s->dumped) continue; if (now >= s->dump_at_ms) { /* §8:10s 到点自动转储 */ int was_ongoing = s->ongoing; archive_one(s); if (!was_ongoing) slot_release(i); /* 非 ONGOING:转储后释放槽 */ else notif_log("tick: ongoing id=%u archived (kept for UI)", s->id); } } } /* ---- before_poweroff:转储所有未上卡 + 清板载 ---- */ void meow_notif_before_poweroff(void) { if (!g_inited) return; int n = meow_notif_dump_now(); notif_log("before_poweroff: dumped=%d, clearing board", n); memset(g_slots, 0, sizeof g_slots); memset(g_apps, 0, sizeof g_apps); g_napps = 0; g_next_id = 1; } /* ---- 设置 dump 回调 ---- */ 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)。 */ int meow_notif_serialize_list(const char *app_id, uint8_t *buf, int buf_len) { if (!g_inited || !buf || buf_len < (int)(2 + sizeof(uint16_t))) return -NOTIF_ERR_INVAL; /* 简化 v1:序列化 count(u16)+ 每条固定 128B 之摘要记录(id/category/summary/body/ * app_icon/target_activity/ongoing/progress/media/dismissed)。M5 全套结构后调。 */ uint16_t count = 0; uint8_t *p = buf + 2; int cap = buf_len - 2; const uint16_t rec = 128; for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) { meow_notif_t *s = &g_slots[i]; if (!s->active || s->dismissed) continue; if (app_id && app_id[0] && strcmp(s->app_id, app_id)) continue; if ((int)(rec) > cap) break; memset(p, 0, rec); uint32_t id = s->id; memcpy(p, &id, 4); /* +0 id */ n_strncpy((char *)(p + 4), s->category, 24); /* +4 category[24] */ n_strncpy((char *)(p + 28), s->summary, 64); /* +28 summary[64] */ n_strncpy((char *)(p + 92), s->body, 24); /* +92 body[24](v1 截断) */ p[116] = s->ongoing; p[117] = s->media; p[118] = s->dismissed; /* +119 target_activity[8] 预留;v1 截断,M5 扩充 */ memcpy(p + 119, s->target_activity, 8); p += rec; cap -= rec; count++; } memcpy(buf, &count, 2); return 2 + (int)count * rec; } /* ---- 调试打印 ---- */ void meow_notif_dump(const char *log_prefix) { const char *lp = log_prefix ? log_prefix : ""; notif_log("%sactive=%d apps=%d next_id=%u dumped_total=%d", lp, meow_notif_count(NULL), g_napps, g_next_id, g_dumped_total); for (int i = 0; i < g_napps; i++) { notif_log("%s app=%s cats=%d media=%d", lp, g_apps[i].app_id, g_apps[i].ncats, g_apps[i].media_count); } 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 urg=%u dumped=%d", lp, i, s->id, s->app_id, s->category, s->summary, s->ongoing, s->media, s->urgency, s->dumped); } }