Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_notif_panel.c
T
MeowOS dafe31784d 通知中心 ⑧⑨: 一键清除(保留活动通知) + 面板跟手下拉半程可回(agent-133)
- ⑧ 一键清除: mnp_clearable(ongoing/media/progress) 红线保留活动通知; 先 meow_notif 转储(notifications.log+10MB滚动) 再清空显示; 勿扰左半+清除右半(NCP_DND_SPLIT_X=fb_xres/2)对称, 命中互斥
- ⑨ 面板跟手下拉/半程可回: reveal 连续映射 1:1 跟手(mnp_drag_progress/reveal), 松手 mnp_drag_anchor(fix,40%)=1→补到1024/0→回0收回; 与⑦行左滑/列表滚动/meow_edge_back 互斥; 复用快照+局部刷(panel帧/新露出条)
- meow_ui.a 零改动(d249c6e6); launcher=b140e9e5; 宿主189项ok; 新增0warn
- NOTIFICATION_SYSTEM §9 增 ⑧⑨; MEOW_APP_SDK §5.7 收合动画落点标注
2026-08-30 17:45:15 +08:00

557 lines
31 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.
/* test_notif_panel.c — 宿主单测:阶段B 通知中心面板(纯逻辑 + 面板读取的存储契约)。
*
* 直接 #include "meow_notif.c" + "meow_settings.c" 于同一 TU(同 test_notif_tf.c 手法),
* 以「真实 meow_notif 存储 + 真实 meow_settings 后端」验证面板真正读取的字段契约;
* 再测 meow_notif_panel.h 的纯逻辑(滑条映射/亮度 clamp/顶部下滑判定/时间显示/行命中/滚动钳位)。
* 不依赖 launcher 其它 static / fb / 触摸。
*
* 运行:gcc -D_GNU_SOURCE -o test_notif_panel test_notif_panel.c && ./test_notif_panel
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
/* stub:meow_notif.c 的 read_quota() 经 meow_settings_get_int 读 notify.* 配额。
* <0 => 键缺失 => 用默认(MEOW_NOTIF_CATS_DEF=4 / CAT_QUOTA_DEF=4 / MEDIA_QUOTA_DEF=1)。
* meow_notif.c 对 meow_settings 的唯一依赖就是这一个函数。 */
int meow_settings_get_int(const char *key, int *out)
{
(void)key; (void)out;
return -2; /* MEOW_ERR_NOENT */
}
/* 先把 tf 转储路径指向临时目录,避免污染真实卡路径(meow_notif_init 会 clear tf)*/
static char g_tflog[600];
#include "meow_notif.c" /* 真实通知存储:post/list/count/dismiss/update_progress/... */
#include "meow_notif_panel.h" /* 面板纯逻辑(header-only) */
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \
else fprintf(stderr, "ok: %s\n", msg); \
} while (0)
/* 收集列表到固定槽(回调式,模拟面板 ncp_collect_cb 的字段读取) */
#define MAXG 16
typedef struct { uint32_t id; char summary[96]; char body[96]; char icon[64];
char app[48]; char tgt[48]; int progress; uint8_t urgency; uint64_t ms; } gslot_t;
static gslot_t g_got[MAXG]; static int g_nslots = 0;
static int list_grab_cb(const meow_notif_t *n, void *ctx)
{
(void)ctx;
if (g_nslots >= MAXG) return 1;
gslot_t *s = &g_got[g_nslots++];
memset(s, 0, sizeof *s);
s->id = n->id;
snprintf(s->summary, sizeof s->summary, "%s", n->summary);
snprintf(s->body, sizeof s->body, "%s", n->body);
snprintf(s->icon, sizeof s->icon, "%s", n->app_icon);
snprintf(s->app, sizeof s->app, "%s", n->app_id);
snprintf(s->tgt, sizeof s->tgt, "%s", n->target_activity);
s->progress = n->progress;
s->urgency = n->urgency;
s->ms = n->created_ms;
return 0;
}
static int grab_list(void)
{
g_nslots = 0;
meow_notif_list(NULL, list_grab_cb, NULL);
return g_nslots;
}
static gslot_t *find_slot(const char *summary)
{
for (int i = 0; i < g_nslots; i++)
if (!strcmp(g_got[i].summary, summary)) return &g_got[i];
return NULL;
}
static uint64_t wall_now_ms(void)
{
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
static void test_pure_logic(void)
{
fprintf(stderr, "--- P1 pure logic ---\n");
/* mnp_top_swipe_down */
CHECK(mnp_top_swipe_down(40, 5, 3, 90, 28, 65) == 1, "top hotzone down-dominant -> open");
CHECK(mnp_top_swipe_down(10, 5, 3, 90, 28, 65) == 0, "left-edge (down_x<24) reserved -> no open");
CHECK(mnp_top_swipe_down(40, 30, 3, 90, 28, 65) == 0, "origin below hotzone -> no open");
CHECK(mnp_top_swipe_down(40, 5, 3, 40, 28, 65) == 0, "below swipe threshold -> no open");
CHECK(mnp_top_swipe_down(40, 5, 100, 70, 28, 65) == 0, "horizontal-dominant -> no open");
CHECK(mnp_top_swipe_down(40, 5, -3, 90, 28, 65) == 1, "down with minor left drift still opens");
/* mnp_slider_val_from_x */
CHECK(mnp_slider_val_from_x(20, 20, 440, 1, 100) == 1, "slider x=left -> min");
CHECK(mnp_slider_val_from_x(460, 20, 440, 1, 100) == 100, "slider x=right -> max");
int mid = mnp_slider_val_from_x(240, 20, 440, 1, 100);
CHECK(mid >= 1 && mid <= 100, "slider x=mid within [min,max]");
CHECK(mnp_slider_val_from_x(0, 20, 440, 1, 100) == 1, "slider x out-left clamped to min");
CHECK(mnp_slider_val_from_x(999, 20, 440, 1, 100) == 100, "slider x out-right clamped to max");
/* mnp_step_round:跳步吸附(音量滑条去抖/减少逐值写 DAC) */
CHECK(mnp_step_round(37, 1) == 37, "step_round step=1 -> unchanged (continuous, same as settings slider)");
CHECK(mnp_step_round(0, 1) == 0, "step_round step=1 -> 0 kept");
CHECK(mnp_step_round(37, 5) == 35, "step_round 37 step=5 -> 35 (nearest multiple of 5)");
CHECK(mnp_step_round(38, 5) == 40, "step_round 38 step=5 -> 40 (rounds up to nearest)");
CHECK(mnp_step_round(50, 5) == 50, "step_round 50 step=5 -> exactly on step");
CHECK(mnp_step_round(0, 5) == 0, "step_round 0 step=5 -> 0");
CHECK(mnp_step_round(100, 5) == 100, "step_round 100 step=5 -> 100");
CHECK(mnp_step_round(99, 5) == 100, "step_round 99 step=5 -> 100 (rounds up to max)");
CHECK(mnp_step_round(2, 4) == 4, "step_round 2 step=4 -> 4 (round to nearest multiple)");
CHECK(mnp_step_round(-3, 5) == 0, "step_round negative clamps to 0 (value domain >=0)");
/* mnp_brightness_clamp */
CHECK(mnp_brightness_clamp(0, 1) == 1, "brightness clamp below min -> min");
CHECK(mnp_brightness_clamp(150, 1) == 100, "brightness clamp above 100 -> 100");
CHECK(mnp_brightness_clamp(50, 1) == 50, "brightness 50 kept");
CHECK(mnp_brightness_clamp(3, 5) == 5, "brightness below custom min -> custom min");
CHECK(mnp_brightness_clamp(-7, -3) == 1, "brightness clamp forces vmin>=1");
/* mnp_time_ago */
uint64_t now = wall_now_ms();
char tb[32];
mnp_time_ago(now - 5000, now, tb, sizeof tb);
CHECK(!strcmp(tb, "刚刚"), "time_ago <60s -> 刚刚");
mnp_time_ago(now - 5 * 60000u, now, tb, sizeof tb);
CHECK(!strcmp(tb, "5分钟前"), "time_ago 5min -> 5分钟前");
mnp_time_ago(now - 3 * 3600000u, now, tb, sizeof tb);
CHECK(strlen(tb) == 5 && tb[2] == ':', "time_ago >1h -> HH:MM");
/* mnp_row_at */
CHECK(mnp_row_at(250, 252, 696, 92) == -1, "row_at above list -> -1");
CHECK(mnp_row_at(300, 252, 696, 92) == 0, "row_at first row -> 0");
CHECK(mnp_row_at(400, 252, 696, 92) == 1, "row_at second row -> 1");
CHECK(mnp_row_at(700, 252, 696, 92) == -1, "row_at below list -> -1");
/* mnp_scroll_clamp */
CHECK(mnp_scroll_clamp(0, 2000, 444) == 0, "scroll clamp lower -> 0");
CHECK(mnp_scroll_clamp(9999, 2000, 444) == 1556, "scroll clamp upper -> total-view");
CHECK(mnp_scroll_clamp(-5, 2000, 444) == 0, "scroll clamp negative -> 0");
CHECK(mnp_scroll_clamp(5, 400, 444) == 0, "content fits view -> 0");
}
/* P4 通知列表滚动:方向映射(input.swipe_invert)+ 回弹判定(纯逻辑,header-only)。
* 语义与设置页列表一致:invert=0 手指上滑(d<0) → target 增大(内容上滚看后面项);
* invert=1(反向)手指上滑 → target 减小。进滚动锁 base 保证触发瞬间不跳变。 */
static void test_scroll_pure(void)
{
fprintf(stderr, "--- P4 notification list scroll (swipe_invert direction + rebound) ---\n");
int total = 2000, view = 444, max_scroll = total - view; /* 1556 */
/* mnp_scroll_max */
CHECK(mnp_scroll_max(2000, 444) == 1556, "scroll_max overflow -> total-view");
CHECK(mnp_scroll_max(400, 444) == 0, "scroll_max content fits -> 0");
CHECK(mnp_scroll_max(444, 444) == 0, "scroll_max equal -> 0");
CHECK(mnp_scroll_max(445, 444) == 1, "scroll_max 1px over -> 1");
/* mnp_scroll_base:进滚动锁基准(invert=0 同设置页 `scroll + d`;invert=1 反向) */
CHECK(mnp_scroll_base(100, -45, 0) == 55, "base invert=0 finger-up -> scroll+d");
CHECK(mnp_scroll_base(100, -45, 1) == 145, "base invert=1 finger-up -> scroll-d");
/* mnp_scroll_target:触发瞬间 target 回到原 scroll(连续跟手,不跳变) */
CHECK(mnp_scroll_target(55, -45, 0) == 100, "target invert=0 at engage -> back to scroll");
CHECK(mnp_scroll_target(145, -45, 1) == 100, "target invert=1 at engage -> back to scroll");
/* 方向映射:invert=0 手指持续上滑(更负) → target 增大;invert=1 → target 减小 */
CHECK(mnp_scroll_target(55, -200, 0) == 255, "target invert=0 finger-up more -> increases");
CHECK(mnp_scroll_target(145, -200, 1) == -55, "target invert=1 finger-up more -> decreases");
CHECK(mnp_scroll_target(55, 200, 0) == -145, "target invert=0 finger-down -> decreases");
CHECK(mnp_scroll_target(145, 200, 1) == 345, "target invert=1 finger-down -> increases");
/* clamp 落位:进滚动 + 拖到边界 → 钳到 [0,max_scroll] */
CHECK(mnp_scroll_clamp(mnp_scroll_target(55, -200, 0), total, view) == 255, "clamp invert=0 up -> in range");
CHECK(mnp_scroll_clamp(mnp_scroll_target(145, -200, 1), total, view) == 0, "clamp invert=1 up -> bottom(0)");
CHECK(mnp_scroll_clamp(mnp_scroll_target(55, 200, 0), total, view) == 0, "clamp invert=0 down -> top(0)");
/* mnp_scroll_rebound:边内容差 tol=48;界内不触发,越过才触发 */
CHECK(mnp_scroll_rebound(0, max_scroll, 48) == 0, "rebound at top(0) in-tol -> 0");
CHECK(mnp_scroll_rebound(-47, max_scroll, 48) == 0, "rebound top overshoot 47<48 -> 0");
CHECK(mnp_scroll_rebound(-49, max_scroll, 48) == 1, "rebound top overshoot 49>48 -> 1");
CHECK(mnp_scroll_rebound(max_scroll, max_scroll, 48) == 0, "rebound at bottom in-tol -> 0");
CHECK(mnp_scroll_rebound(max_scroll + 47, max_scroll, 48) == 0, "rebound bottom overshoot 47<48 -> 0");
CHECK(mnp_scroll_rebound(max_scroll + 49, max_scroll, 48) == 1, "rebound bottom overshoot 49>48 -> 1");
CHECK(mnp_scroll_rebound(500, max_scroll, 48) == 0, "rebound mid-range -> 0");
}
/* P3 开关平移动画状态机:p 推进/翻转/中断、offset 计算、裁切边界、关闭到 0 才退出。
* 用任意 fix/ph 参数化(默认 1024/692,通知中心面板 PH=720-28=692);纯函数不依赖 fb。 */
static void test_anim_pure(void)
{
fprintf(stderr, "--- P3 open/close slide animation state machine ---\n");
enum { FIX = 1024, PH = 692 };
/* mnp_anim_off:p→offset 线性映射(∈[-PH,0];p=0 全隐藏 / p=FIX 到位) */
CHECK(mnp_anim_off(0, FIX, PH) == -PH, "anim_off p=0 -> -PH (fully hidden above)");
CHECK(mnp_anim_off(FIX, FIX, PH) == 0, "anim_off p=FIX -> 0 (settled open)");
CHECK(mnp_anim_off(FIX / 2, FIX, PH) == -(PH / 2), "anim_off mid -> -PH/2");
/* mnp_anim_step:打开推进(0→FIX) */
CHECK(mnp_anim_step(0, FIX, FIX, 180, 180) == FIX, "step dt=dur -> reaches FIX in one tick");
CHECK(mnp_anim_step(0, FIX, FIX, 90, 180) == FIX / 2, "step dt=dur/2 -> half progress");
CHECK(mnp_anim_step(0, FIX, FIX, 10, 180) >= 1, "step small dt -> at least 1 (monotonic, no stall)");
CHECK(mnp_anim_step(0, FIX, FIX, 0, 180) == 0, "step dt=0 -> no move");
CHECK(mnp_anim_step(0, FIX, FIX, 100000, 180) == FIX, "step huge dt -> clamped at FIX");
/* 反向推进(关闭:FIX→0,线性步进率 = dt*fix/dur,非剩余比例) */
CHECK(mnp_anim_step(FIX, 0, FIX, 90, 180) == FIX / 2, "step close dt=dur/2 -> half back");
CHECK(mnp_anim_step(FIX / 2, 0, FIX, 45, 180) == FIX / 4, "step reversal mid -> toward 0 linearly");
CHECK(mnp_anim_step(FIX / 4, 0, FIX, 100000, 180) == 0, "step reversal huge dt -> clamped at 0");
/* 打断(打开中→关闭→再打开):平滑转向,不跳变到目标 */
int p = mnp_anim_step(0, FIX, FIX, 90, 180); /* 打开到半程 (512) */
int p_before = p;
p = mnp_anim_step(p, 0, FIX, 30, 180); /* 打断:转为关闭,小步推进 */
CHECK(p < p_before && p > 0, "interrupt open->close: heading to 0 (not target), mid value ok");
int s2 = 0;
while (s2++ < 1000 && p != 0) p = mnp_anim_step(p, 0, FIX, 30, 180);
CHECK(p == 0, "interrupt close completes at 0");
p = mnp_anim_step(p, FIX, FIX, 180, 180); /* 关闭到位后再打开 */
CHECK(p == FIX, "reopen after interrupt -> settled open");
/* mnp_anim_covered_bot:裁切边界(off∈[-PH,0] → bot∈[top, top+PH]) */
CHECK(mnp_anim_covered_bot(28, PH, -PH) == 28, "covered_bot fully hidden -> top (statusbar bottom)");
CHECK(mnp_anim_covered_bot(28, PH, 0) == 28 + PH, "covered_bot settled -> top+PH");
CHECK(mnp_anim_covered_bot(28, PH, -(PH / 2)) == 28 + PH / 2, "covered_bot halfway -> top+PH/2");
CHECK(mnp_anim_covered_bot(28, PH, 0) <= 720 && mnp_anim_covered_bot(28, PH, -PH) >= 28,
"covered_bot within [28,720] — 不盖状态栏、不出屏底");
/* 完整关闭:多次 tick 才到 0;单调不增;且只在 p==0 才算退出 */
p = FIX;
int steps = 0, prev = p, monotonic = 1;
while (steps++ < 100000 && p != 0) {
int np = mnp_anim_step(p, 0, FIX, 10, 180);
if (np > prev) monotonic = 0; /* 关闭方向必须单调减小 */
if (np < 0 || np > FIX) monotonic = 0;
p = np; prev = np;
}
CHECK(p == 0, "close animation terminates at p=0 (exit condition)");
CHECK(steps > 1, "close takes multiple ticks (smooth sliding, not a jump)");
CHECK(monotonic, "close progress monotonic non-increasing");
CHECK(mnp_anim_step(0, 0, FIX, 10, 180) == 0, "at p==0 with target 0 stays put (would break)");
}
/* P5 ⑥ 勿扰过滤 + ⑦ 行左滑删除(纯逻辑,header-only):
* - mnp_dnd_should_show:dnd=0 全显示;dnd=1 只 urgency>=2(HIGH/CRITICAL)。
* - mnp_swipe_clamp:跟手 clamp 到 [-row_w,0]。
* - mnp_swipe_delete:松手阈值(行宽百分比 / 绝对像素,取大者)。
* - mnp_swipe_step:落位动画线性步进(回弹/滑出,正反向,到底停,半程取消)。
* 另加一段 mini 状态机:手指拖 → 松手过阈值=滑出删除 / 未过=回弹复原(跟手、半程可回)。 */
static void test_dnd_swipe_pure(void)
{
fprintf(stderr, "--- P5 DND filter + row left-swipe ---\n");
/* mnp_dnd_should_show */
CHECK(mnp_dnd_should_show(0, 0) == 1, "dnd off -> LOW(0) shows");
CHECK(mnp_dnd_should_show(1, 0) == 1, "dnd off -> NORMAL(1) shows");
CHECK(mnp_dnd_should_show(2, 0) == 1, "dnd off -> HIGH(2) shows");
CHECK(mnp_dnd_should_show(3, 0) == 1, "dnd off -> CRITICAL(3) shows");
CHECK(mnp_dnd_should_show(0, 1) == 0, "dnd on -> LOW(0) hidden");
CHECK(mnp_dnd_should_show(1, 1) == 0, "dnd on -> NORMAL(1) hidden");
CHECK(mnp_dnd_should_show(2, 1) == 1, "dnd on -> HIGH(2) shows");
CHECK(mnp_dnd_should_show(3, 1) == 1, "dnd on -> CRITICAL(3) shows");
/* mnp_swipe_clamp(跟手 offset clamp:off∈[-row_w,0]) */
CHECK(mnp_swipe_clamp(-200, 456) == -200, "swipe_clamp in-range kept");
CHECK(mnp_swipe_clamp(30, 456) == 0, "swipe_clamp rightward -> 0");
CHECK(mnp_swipe_clamp(-500, 456) == -456, "swipe_clamp past left -> -row_w");
CHECK(mnp_swipe_clamp(-456, 456) == -456, "swipe_clamp exactly -row_w kept");
CHECK(mnp_swipe_clamp(0, 456) == 0, "swipe_clamp 0 kept");
/* mnp_swipe_delete(row_w=456, frac_pct=40 → 40%=182.4;min_px=110;OR 语义 → 有效阈值=110px) */
CHECK(mnp_swipe_delete(-80, 456, 40, 110) == 0, "delete -80 (<110 & <182.4) -> cancel");
CHECK(mnp_swipe_delete(-110, 456, 40, 110) == 1, "delete -110 (>=min_px) -> delete");
CHECK(mnp_swipe_delete(-150, 456, 40, 110) == 1, "delete -150 (>=min_px, <40%) -> delete");
CHECK(mnp_swipe_delete(-183, 456, 40, 110) == 1, "delete -183 (>=40%) -> delete");
CHECK(mnp_swipe_delete(-456, 456, 40, 110) == 1, "delete full slide -> delete");
CHECK(mnp_swipe_delete(0, 456, 40, 110) == 0, "delete 0 -> cancel");
/* 窄行:40%=24px(row_w=60),min_px=110 不起作用 → 阈值=24px */
CHECK(mnp_swipe_delete(-25, 60, 40, 110) == 1, "delete narrow row -25 (>=40%) -> delete");
CHECK(mnp_swipe_delete(-20, 60, 40, 110) == 0, "delete narrow row -20 (<40%) -> cancel");
/* mnp_swipe_step(线性步进:off 向 target 走 dt*rate,可正/反向,到底停) */
CHECK(mnp_swipe_step(-100, 0, 10, 3) == -70, "step springback +30px/tick");
CHECK(mnp_swipe_step(-100, 0, 0, 3) == -100, "step dt=0 -> unchanged");
CHECK(mnp_swipe_step(-100, 0, 1000, 3) == 0, "step huge dt -> clamp at 0 (springback done)");
CHECK(mnp_swipe_step(-50, -456, 10, 3) == -80, "step slide-out leftward");
CHECK(mnp_swipe_step(-50, -456, 1000, 3) == -456, "step huge dt -> clamp at -row_w (deleted)");
CHECK(mnp_swipe_step(-30, -30, 10, 3) == -30, "step at target -> stays");
CHECK(mnp_swipe_step(0, 0, 10, 3) == 0, "step 0 target 0 -> stays");
CHECK(mnp_swipe_step(-1000, 0, 5, 3) == -985, "step linear rate not exponential (small dt)");
/* mini 状态机:跟手拖 → 松手判定 → 动画到目标(半程取消 vs 删除)。 */
int row_w = 456, off = 0;
/* 拖 80px 左(低于阈值 110)→ 松手回弹复原 */
off = mnp_swipe_clamp(-80, row_w);
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: drag -80 -> cancel (springback)");
int target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0;
off = mnp_swipe_step(off, target, 1000, 3);
CHECK(off == 0, "mini: springback reaches 0 (row restored)");
/* 拖 150px 左(>=110)→ 松手滑出删除 */
off = mnp_swipe_clamp(-150, row_w);
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: drag -150 -> delete");
target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0;
off = mnp_swipe_step(off, target, 1000, 3);
CHECK(off == -row_w, "mini: slide-out reaches -row_w (dismiss)");
/* 半程可反向:拖 200 左再反向回摄(dx 变 +30)→ off 回 0(跟手可回、松手在途取消) */
off = mnp_swipe_clamp(-200, row_w);
CHECK(off == -200, "mini: drag -200 follows finger");
off = mnp_swipe_clamp(30, row_w); /* 反向拖回原点左侧以右 → clamp 0 */
CHECK(off == 0, "mini: reverse drag -> row slides back to 0 (halfway cancel)");
/* 半程取消:拖 150 左(未达 182.4 的 40% 但达 110px 阈值)→ 松手删;若再拖回 <110 → 回弹 */
off = mnp_swipe_clamp(-150, row_w);
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: -150 passed threshold -> delete");
off = mnp_swipe_clamp(-100, row_w); /* 又拖回 100 <110 */
CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: -100 back under threshold -> cancel");
}
/* P6 ⑧ 一键清除判定 + ⑨ 面板跟手下拉/半程可回(纯逻辑,header-only):
* - mnp_clearable:红线(ongoing / media / progress>=0 进行中)一律保留;非活动普通通知可清。
* - mnp_drag_progress:reveal→progress(0/fix/线性/越界 clamp)。
* - mnp_drag_reveal:seed+delta 正向/反向/边界 clamp(连续、半程可回)。
* - mnp_drag_anchor:松手锚定(≥40%→前去 fix;<40%→回 0)。
* 另加 mini 状态机:开(跟手下拉→松手过阈值展开/未过收回)+ 关(上滑跟手→松手未过阈值回弹重展/过阈值收回)。 */
static void test_clear_drag_pure(void)
{
fprintf(stderr, "--- P6 clear predicate + panel drag-follow ---\n");
enum { FIX = 1024, PH = 692 };
/* mnp_clearable */
CHECK(mnp_clearable(1, 0, -1) == 0, "clearable: ongoing -> keep");
CHECK(mnp_clearable(0, 1, -1) == 0, "clearable: media -> keep");
CHECK(mnp_clearable(0, 0, 50) == 0, "clearable: progress in-progress(50) -> keep");
CHECK(mnp_clearable(0, 0, 0) == 0, "clearable: progress bar(0) -> keep");
CHECK(mnp_clearable(0, 0, -1) == 1, "clearable: plain inactive -> clear");
CHECK(mnp_clearable(0, 1, 50) == 0, "clearable: media+progress -> keep");
CHECK(mnp_clearable(1, 0, 50) == 0, "clearable: ongoing+progress -> keep");
/* mnp_drag_progress(reveal∈[0,ph] → p∈[0,fix]) */
CHECK(mnp_drag_progress(0, FIX, PH) == 0, "drag_progress reveal=0 -> 0");
CHECK(mnp_drag_progress(PH, FIX, PH) == FIX, "drag_progress reveal=ph -> fix");
CHECK(mnp_drag_progress(PH / 2, FIX, PH) == FIX / 2, "drag_progress mid -> half");
CHECK(mnp_drag_progress(-50, FIX, PH) == 0, "drag_progress negative reveal -> 0");
CHECK(mnp_drag_progress(PH + 50, FIX, PH) == FIX, "drag_progress reveal>ph -> fix");
CHECK(mnp_drag_progress(173, FIX, 692) == FIX * 173 / 692, "drag_progress linear 173/692");
/* mnp_drag_reveal(seed+delta → [0,ph],连续/反向/边界) */
CHECK(mnp_drag_reveal(0, 0, PH) == 0, "drag_reveal seed0 delta0 -> 0");
CHECK(mnp_drag_reveal(0, 300, PH) == 300, "drag_reveal open: seed0+300 -> 300 (1:1)");
CHECK(mnp_drag_reveal(PH, -300, PH) == PH - 300, "drag_reveal close: seed ph -300");
CHECK(mnp_drag_reveal(100, -200, PH) == 0, "drag_reveal clamps below 0 (reverse past top)");
CHECK(mnp_drag_reveal(100, 1000, PH) == PH, "drag_reveal clamps above ph (drag past bottom)");
CHECK(mnp_drag_reveal(0, -50, PH) == 0, "drag_reveal reverse on hidden stays 0");
CHECK(mnp_drag_reveal(50, 0, 0) == 0, "drag_reveal ph<=0 -> 0");
/* mnp_drag_anchor(≥fix*40/100=409 → 1;<409 → 0) */
CHECK(mnp_drag_anchor(409, FIX, 40) == 1, "drag_anchor at 40% (409) -> go open");
CHECK(mnp_drag_anchor(410, FIX, 40) == 1, "drag_anchor above 40% -> go open");
CHECK(mnp_drag_anchor(1024, FIX, 40) == 1, "drag_anchor full -> go open");
CHECK(mnp_drag_anchor(408, FIX, 40) == 0, "drag_anchor below 40% -> collapse");
CHECK(mnp_drag_anchor(0, FIX, 40) == 0, "drag_anchor 0 -> collapse");
CHECK(mnp_drag_anchor(0, FIX, 0) == 1, "drag_anchor thr_pct=0 -> always open");
/* mini 状态机:跟手下拉打开(seed 从0 升)→ 松手锚定 */
int seed = 0, p = 0;
int dy = 0;
/* 手指下移 300px:reveal=seed+(tly-drag_y0) */
seed = 0; p = mnp_drag_progress(mnp_drag_reveal(seed, 300, PH), FIX, PH);
CHECK(p > 0 && p < FIX, "mini open-follow: drag 300px -> partial progress");
CHECK(mnp_drag_anchor(p, FIX, 40) == 1, "mini open-follow: 300/692=43% >= 40% -> open");
/* 反向拖回 200(reveal=200,即拖 200)→ 半程取消(29% < 40%) */
p = mnp_drag_progress(mnp_drag_reveal(seed, 200, PH), FIX, PH);
CHECK(p < FIX && mnp_drag_anchor(p, FIX, 40) == 0, "mini open-follow: 200/692=29% < 40% -> collapse(cancel)");
/* 全拉满 → 满 */
CHECK(mnp_drag_anchor(mnp_drag_progress(mnp_drag_reveal(seed, PH, PH), FIX, PH), FIX, 40) == 1,
"mini open-follow: drag full -> open");
/* 关闭:seed=PH 上滑 600 → reveal=PH-600 → 过阈值收回 */
int close_p = mnp_drag_progress(mnp_drag_reveal(PH, -600, PH), FIX, PH);
CHECK(close_p < FIX && mnp_drag_anchor(close_p, FIX, 40) == 0, "mini close-follow: up 600 -> close");
/* 关闭:上滑仅 150(reveal=PH-150,仍高)→ 松手回弹重展 */
int reopen_p = mnp_drag_progress(mnp_drag_reveal(PH, -150, PH), FIX, PH);
CHECK(reopen_p > 0 && mnp_drag_anchor(reopen_p, FIX, 40) == 1, "mini close-follow: up 150 -> reopen");
}
/* P7 ⑧ 一键清除(存储契约):先用 mnp_clearable + meow_notif_list/dismiss 模拟面板 ncp_clear_all,
* 验证「先转储留档(dismiss 触发 dump_cb)→ 清出显示」且红线项(ongoing/media/progress)保留。 */
typedef struct { uint32_t ids[8]; int n; } clear_ctx_t;
static int clear_collect_cb(const meow_notif_t *n, void *ctx)
{
clear_ctx_t *c = ctx;
if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0;
if (c->n >= 8) return 1;
c->ids[c->n++] = n->id;
return 0;
}
/* 通用「收集全部活跃 id」(兼作测试排空用:非仅可清)。 */
static int clear_all_cb(const meow_notif_t *n, void *ctx)
{
clear_ctx_t *c = ctx;
if (c->n >= 8) return 1;
c->ids[c->n++] = n->id;
return 0;
}
static void test_clear_storage(void)
{
fprintf(stderr, "--- P7 clear-all storage contract (dump then clear, keep ongo/media/progress) ---\n");
/* 排空上一测试留下的所有活跃通知,保证本测试从干净基数开始(count 断言可用绝对值)。 */
for (int pass = 0; pass < 8 && meow_notif_count(NULL) > 0; pass++) {
clear_ctx_t c2; memset(&c2, 0, sizeof c2);
meow_notif_list(NULL, clear_all_cb, &c2); /* clear_all_cb:收集全部(非仅可清) */
if (c2.n == 0) break;
for (int i = 0; i < c2.n; i++) meow_notif_dismiss(c2.ids[i]);
}
meow_notif_t n; memset(&n, 0, sizeof n);
/* 普通(可清) */
snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general");
snprintf(n.summary, sizeof n.summary, "普通1"); n.progress = -1; n.urgency = 1;
uint32_t idA = 0; CHECK(meow_notif_post(&n, &idA) == NOTIF_OK, "clear st: post plain1");
/* 普通2(可清) */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general");
snprintf(n.summary, sizeof n.summary, "普通2"); n.progress = -1; n.urgency = 1;
uint32_t idB = 0; CHECK(meow_notif_post(&n, &idB) == NOTIF_OK, "clear st: post plain2");
/* 媒体(红线:保留) */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "media");
snprintf(n.summary, sizeof n.summary, "媒体"); n.ongoing = 1; n.media = 1; n.progress = 30; n.urgency = 2;
uint32_t idM = 0; CHECK(meow_notif_post(&n, &idM) == NOTIF_OK, "clear st: post media");
/* 进度进行中(红线:保留) */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general");
snprintf(n.summary, sizeof n.summary, "下载中"); n.progress = 60; n.urgency = 2;
uint32_t idP = 0; CHECK(meow_notif_post(&n, &idP) == NOTIF_OK, "clear st: post progress");
CHECK(meow_notif_count(NULL) == 4, "clear st: count 4 before clear");
/* 按 mnp_clearable 收集 + dismiss(= dump_cb + 释放槽,模拟 ncp_clear_all) */
clear_ctx_t c; memset(&c, 0, sizeof c);
meow_notif_list(NULL, clear_collect_cb, &c);
int cleared = 0;
for (int i = 0; i < c.n; i++) { meow_notif_dismiss(c.ids[i]); cleared++; }
CHECK(cleared == 2, "clear st: exactly 2 plain cleared");
CHECK(meow_notif_count(NULL) == 2, "clear st: 2 remaining after clear (media+progress kept)");
grab_list();
CHECK(find_slot("媒体") != NULL, "clear st: media kept");
CHECK(find_slot("下载中") != NULL, "clear st: progress kept");
CHECK(find_slot("普通1") == NULL && find_slot("普通2") == NULL, "clear st: plain dismissed");
}
static void test_storage_contract(void)
{
fprintf(stderr, "--- P0 storage contract (panel reads) ---\n");
/* meow_notif_init:应清内存表 + 清 tf 文件(路径已指临时);配额用默认(stub 返回 NOENT) */
CHECK(meow_notif_init() == NOTIF_OK, "meow_notif_init ok");
CHECK(meow_notif_count(NULL) == 0, "init clears memory table");
CHECK(g_max_cats == 4 && g_cat_quota == 4 && g_media_quota == 1,
"quota defaults from meow_settings backend (stub NOENT -> defaults)");
meow_notif_t n; memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.hello");
snprintf(n.category, sizeof n.category, "general");
snprintf(n.summary, sizeof n.summary, "你好世界");
snprintf(n.body, sizeof n.body, "这是正文第二行");
snprintf(n.app_icon, sizeof n.app_icon, "apps/os.meow.hello/icon.png");
snprintf(n.target_activity, sizeof n.target_activity, "activity_main");
n.expire_timeout = 0; n.progress = -1; n.urgency = 1;
uint32_t id1 = 0;
CHECK(meow_notif_post(&n, &id1) == NOTIF_OK, "post general ok");
CHECK(id1 != 0, "post assigns id");
/* 媒体通知:ongoing + 进度 */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.hello");
snprintf(n.category, sizeof n.category, "media");
snprintf(n.summary, sizeof n.summary, "正在播放");
n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 20; n.urgency = 2;
uint32_t id2 = 0;
CHECK(meow_notif_post(&n, &id2) == NOTIF_OK, "post media ok");
/* 高紧急 + 进度 */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "system");
snprintf(n.category, sizeof n.category, "event");
snprintf(n.summary, sizeof n.summary, "系统提醒");
snprintf(n.body, sizeof n.body, "电量低");
n.progress = 50; n.urgency = 3;
uint32_t id3 = 0;
CHECK(meow_notif_post(&n, &id3) == NOTIF_OK, "post event(high) ok");
CHECK(meow_notif_count(NULL) == 3, "count == 3 after posts");
/* 遍历(面板 ncp_collect_cb 同款字段读取)*/
int got = grab_list();
CHECK(got == 3, "list returns 3 active notifs");
gslot_t *s1 = find_slot("你好世界");
CHECK(s1 != NULL, "panel sees summary 你好世界");
if (s1) {
CHECK(!strcmp(s1->app, "os.meow.hello"), "app_id");
CHECK(!strcmp(s1->body, "这是正文第二行"), "body");
CHECK(!strcmp(s1->icon, "apps/os.meow.hello/icon.png"), "app_icon");
CHECK(!strcmp(s1->tgt, "activity_main"), "target_activity");
CHECK(s1->progress == -1, "general progress=-1 (none)");
CHECK(s1->urgency == 1, "general urgency=NORMAL(1)");
CHECK(s1->ms > 0, "created_ms set");
}
gslot_t *s2 = find_slot("正在播放");
CHECK(s2 != NULL && s2->progress == 20 && s2->urgency == 2, "media progress=20 urgency=HIGH(2)");
gslot_t *s3 = find_slot("系统提醒");
CHECK(s3 != NULL && s3->progress == 50 && s3->urgency == 3, "event progress=50 urgency=CRITICAL(3)");
/* dismiss → 面板列表减一(点通知=阅读)*/
CHECK(meow_notif_dismiss(id1) == NOTIF_OK, "dismiss general ok");
CHECK(meow_notif_count(NULL) == 2, "count == 2 after dismiss");
grab_list();
CHECK(find_slot("你好世界") == NULL, "dismissed notif no longer listed");
/* 里程碑 100% → 释放(非 ONGOING):面板列表再减一 */
CHECK(meow_notif_update_progress(id3, 100) == NOTIF_OK, "update_progress(100) ok");
CHECK(meow_notif_count(NULL) == 1, "milestone released event slot -> count 1");
grab_list();
CHECK(find_slot("系统提醒") == NULL, "100% milestone notif no longer listed");
/* 剩余媒体通知仍活跃(ONGOING 常驻)*/
CHECK(find_slot("正在播放") != NULL, "ongoing media stays listed");
/* 配额:单 app 媒体单例(media_quota=1)——再发一条 media 顶掉旧媒体 */
memset(&n, 0, sizeof n);
snprintf(n.app_id, sizeof n.app_id, "os.meow.hello");
snprintf(n.category, sizeof n.category, "media");
snprintf(n.summary, sizeof n.summary, "下一首");
n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 60;
uint32_t id4 = 0;
CHECK(meow_notif_post(&n, &id4) == NOTIF_OK, "post second media ok");
CHECK(meow_notif_count(NULL) == 1, "media quota=1 -> old media replaced, count stays 1");
grab_list();
CHECK(find_slot("下一首") != NULL && find_slot("正在播放") == NULL,
"media singleton replaced oldest");
}
int main(void)
{
char tmp[] = "/tmp/meow_notif_panel_XXXXXX";
char *dir = mkdtemp(tmp);
if (!dir) { perror("mkdtemp"); return 2; }
snprintf(g_tflog, sizeof g_tflog, "%s/notif.log", dir);
meow_notif_set_tf_path(g_tflog);
meow_notif_set_tf_max(1024 * 1024);
test_pure_logic();
test_scroll_pure();
test_anim_pure();
test_dnd_swipe_pure();
test_clear_drag_pure();
test_storage_contract();
test_clear_storage();
unlink(g_tflog); rmdir(dir);
fprintf(stderr, "\n%s (failures=%d)\n", g_fail ? "== TEST FAILED ==" : "== TEST PASSED ==", g_fail);
return g_fail ? 1 : 0;
}