Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/meow_notif.c
T
MeowOS Dev 245bee2946 MeowOS for HiBy R3 Pro II — 公共发布版本(历史已重置)
• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC)
• 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库)
• 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo
增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
2026-08-30 01:12:34 +08:00

503 lines
19 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 + 清板载)。
*
* 空洞实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg/now_ms),配额经
* 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 "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 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;
}
/* ---- 配额读取(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;
notif_log("init: slots=%d default dump_cb=log-only (M2c -> TF)", MEOW_NOTIF_MAX_SLOTS);
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->ongoing = n->ongoing;
s->media = n->media;
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->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 转储 */
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;
}
/* ---- 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) */
}
/* ---- 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 dumped=%d",
lp, i, s->id, s->app_id, s->category, s->summary,
s->ongoing, s->media, s->dumped);
}
}