/* * meow_ui_notify.c — MeowOS 应用通知 API(meow_notify_*,M2.53 最小可用版)。 * * 定位:app 只需声明类型 + meow_notify_send() 即可发通知,SDK 管配额/活动表/降级。 * 本切片为「SDK 侧应用可调用 API」第一个纵向切片——只做 SDK 层;通知中心 UI、 * D-Bus 守护(org.freedesktop.Notifications)、TF 转储是后续独立任务。 * * 行为约定(NOTIFICATION_SYSTEM.md §2/§7,2026-08-29 用户拍板): * - 类型注册:app 声明支持的类型,最多 MEOW_NOTIFY_MAX_TYPES(4)种; * meow_notify_send 也会自动注册未声明类型(受同样 4 种上限约束)。 * - 配额:每类型活动(未过期)通知上限 MEOW_NOTIFY_MAX_PER_TYPE(4),满则丢最旧 * (替换);媒体类型单例(每 app 仅保留最近 1 条,重发替换——可多媒体 App)。 * - 超时语义 [实现定案]:timeout_ms > 0 为该毫秒数后过期;0 = 不自动过期(直到 * meow_notify_cancel);-1 = 常驻(ONGOING),同样不自动过期。 * - 落点(任务 #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 * 的应用里调用),且 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 #include #include /* ---- 内部活动表尺寸(配额见 meow_ui.h) ---- */ #define MEOW_NOTIFY_MAX_ACTIVE (MEOW_NOTIFY_MAX_TYPES * MEOW_NOTIFY_MAX_PER_TYPE) /* 活动槽内 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=未同步/降级 */ int active; /* 1=活动(未取消未过期),0=空闲 */ meow_notify_category cat; meow_notify_urgency urg; int timeout_ms; uint64_t created_ms; 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; static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static notify_slot_t g_slots[MEOW_NOTIFY_MAX_ACTIVE]; static int g_cats[MEOW_NOTIFY_MAX_TYPES]; /* 已声明类型(按注册顺序) */ static int g_cat_n = 0; static uint32_t g_next_id = 1; /* 字符串表(与枚举序号对齐) */ static const char *g_urg_str[] = { "low", "normal", "high", "critical" }; static const char *g_cat_str[] = { "general", "message", "email", "event", "progress", "media" }; /* ---- 内部工具(以下均须持有 g_mu 时调用) ---- */ /* 该分类当前活动数。 */ static int notify_count_cat(meow_notify_category cat) { int i, n = 0; for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) if (g_slots[i].active && g_slots[i].cat == cat) n++; return n; } /* 该分类是否已被 app 声明。 */ static int notify_is_declared(meow_notify_category cat) { int i; for (i = 0; i < g_cat_n; i++) if (g_cats[i] == (int)cat) return 1; return 0; } /* 清掉已过期活动槽(TTL 用 meow_ui_time_ms() 单调毫秒)。 */ static void notify_purge_expired(void) { uint64_t now = meow_ui_time_ms(); int i; for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) { notify_slot_t *s = &g_slots[i]; if (!s->active) continue; if (s->timeout_ms > 0 && (now - s->created_ms) >= (uint64_t)s->timeout_ms) { s->active = 0; s->id = 0; s->ipc_id = 0; } } } /* 填充一个活动槽(改前须确认槽空闲或已被显式复用)。 */ 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) ? n->urgency : MEOW_NOTIFY_URG_NORMAL; s->timeout_ms = n->timeout_ms; s->created_ms = now; snprintf(s->title, sizeof s->title, "%s", n->title ? n->title : ""); 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); } /* 落点(任务 #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) { 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; } /* ============================================================ */ /* 公共 API(见 meow_ui.h §9.1) */ /* ============================================================ */ int meow_notify_register_type(meow_notify_category cat) { int rc = 0; if ((int)cat < 0 || (int)cat >= (int)MEOW_NOTIFY_CAT_COUNT) return -1; pthread_mutex_lock(&g_mu); if (notify_is_declared(cat)) rc = -1; /* 重复声明 */ else if (g_cat_n >= MEOW_NOTIFY_MAX_TYPES) rc = -1; /* 超 4 种上限 */ else g_cats[g_cat_n++] = (int)cat; pthread_mutex_unlock(&g_mu); return rc; } uint32_t meow_notify_send(const meow_notify *n) { uint32_t id, nid, replaces_id = 0; uint64_t now, oldest; int i, slot_idx, count, oldest_idx; if (!n || (int)n->cat < 0 || (int)n->cat >= (int)MEOW_NOTIFY_CAT_COUNT) return 0; if (!n->title) return 0; pthread_mutex_lock(&g_mu); notify_purge_expired(); /* 未声明类型:自动注册(≤4 种,超过则拒绝本次发送) */ if (!notify_is_declared(n->cat)) { if (g_cat_n >= MEOW_NOTIFY_MAX_TYPES) { pthread_mutex_unlock(&g_mu); return 0; } g_cats[g_cat_n++] = (int)n->cat; } slot_idx = -1; 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; 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++) if (g_slots[i].active && g_slots[i].cat == n->cat && g_slots[i].created_ms < oldest) { oldest = g_slots[i].created_ms; oldest_idx = i; } 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) { for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) if (!g_slots[i].active) { slot_idx = i; break; } } if (slot_idx < 0) { /* 理论不可达(上方已丢最旧腾位);兜底防御 */ pthread_mutex_unlock(&g_mu); return 0; } now = meow_ui_time_ms(); id = g_next_id++; if (g_next_id == 0) g_next_id = 1; /* 防回绕 */ notify_fill(&g_slots[slot_idx], n, id, now); 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; } 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; pthread_mutex_lock(&g_mu); notify_purge_expired(); for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) if (g_slots[i].active) n++; pthread_mutex_unlock(&g_mu); return n; } int meow_notify_count_in_type(meow_notify_category cat) { int n; pthread_mutex_lock(&g_mu); notify_purge_expired(); n = notify_count_cat(cat); pthread_mutex_unlock(&g_mu); return n; } int meow_notify_has_active(void) { int i, rc = 0; pthread_mutex_lock(&g_mu); notify_purge_expired(); for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) if (g_slots[i].active) { rc = 1; break; } pthread_mutex_unlock(&g_mu); return rc; } const char *meow_notify_urgency_str(meow_notify_urgency u) { /* 钳位(同 meow_ui_log.c 防越界枚举触发 OOB 读) */ if ((int)u >= 0 && (int)u < (int)MEOW_NOTIFY_URG_COUNT) return g_urg_str[u]; return g_urg_str[MEOW_NOTIFY_URG_NORMAL]; } const char *meow_notify_category_str(meow_notify_category c) { if ((int)c >= 0 && (int)c < (int)MEOW_NOTIFY_CAT_COUNT) return g_cat_str[c]; return g_cat_str[MEOW_NOTIFY_CAT_GENERAL]; }