- 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
747 lines
29 KiB
C
747 lines
29 KiB
C
/* 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);
|
||
}
|
||
}
|