Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/meow_notif.c
T
MeowOS e6f0b665e3 refactor(launcher): 拆分 meow_launcherd.c(9778行)→meow_launcherd/ 多文件(agent-149)
- 14 模块 .c + meow_launcher_internal.h(642): util/font_theme/fb_compositor/settings_page/power_menu/notif_center/app_launcher/main_menu/audio/backlight/net/keys/about/statusbar; meow_launcherd.c 已删
- 按行逐字搬移(0-diff), 118函数提升非static+47组extern(内部头), #if0退役区不编入(源码留.pre_split.bak)
- 关于本机小改: 作者 AskaEth/17 + info/item[13]防越界 + s_dd_modal 超高滚动(clamp/触摸滚动/音量键跟随/裁剪, 仅超高启用)
- build_launcher.sh 编译列表→模块; 文档/注释引用 meow_launcherd.c→模块/目录
- 编译0warning/-Wall -Wextra告警集与原文一致(77条无新增)/qemu-mipsel冒烟一致(--scan/--sysdump); launcher=c0799357(505KB; md5因多文件重编译与原文异,非回归依据)
- 备份 meow_launcherd.c.pre_split.bak/meow_launcherd.pre_split.bak; M2.69(0.7.64)已推卡并用户验证OK
2026-08-30 21:38:30 +08:00

747 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <stdarg.h>
#include <time.h>
#include <sys/stat.h> /* mkdir / stat */
#include <sys/types.h>
#include <unistd.h> /* open/close/write/pread/lseek/ftruncate */
#include <fcntl.h> /* O_* */
#include <errno.h> /* 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);
}
}