Files

327 lines
13 KiB
C
Raw Permalink 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_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 <stdio.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
/* ---- 内部活动表尺寸(配额见 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];
}