Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_notif_panel.c
T
MeowOS a8ec8377ed 通知中心增强(agent-126/127/128/130) + App内通知中心方案(agent-129)
- 开关平移动画(时间轴180ms可打断/快照memcpy/顶28不盖状态栏) agent-126
- 音量/亮度滑条复用设置页方案(跳步+上限clamp+写DAC去抖/状态栏带仅redraw_sb清绘) agent-127
- 通知列表滚动(方向随input.swipe_invert, 滚动≠关闭, 回弹才关) agent-128
- 去音量chip(静音↔恢复last_nz, NCP_CHIP_N 5→4: WiFi/蓝牙/主题/亮度) agent-130
- meow_ui.a 零改动(d249c6e6); launcher md5 c7893faf; 宿主 90+/0warn
- ④App前台划出通知中心方案: FRAMEWORK_DESIGN §10.5 + NOTIFICATION_SYSTEM §9 + MEOW_APP_SDK §6.1(M3跨进程: SDK全局状态栏下滑唤出+半程可回+点通知拉app)
2026-08-30 17:01:47 +08:00

351 lines
18 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)");
}
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_storage_contract();
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;
}