阶段B 通知中心UI(agent-123) + 进程模型/文档完整(agent-124) + 对外README(agent-125)
- 通知中心=launcher侧系统级叠加层(§6.1③ 仅内部): meow_notif_panel.h(纯逻辑) + meow_launcherd.c(内联 draw/run_notif_center + run_menu顶部热区下滑接入) + test_notif_panel.c(56单测) - 通知中心零旁路走 meow_settings_*; 通知列表读 meow_notif; 左缘返回复用 meow_edge_back; 局部脏区; 系统级控件无公共API(App无法自建悬浮层) - meow_ui.a 零改动(d249c6e6); meow_launcherd=4652901a(0warn) - FRAMEWORK_DESIGN §10 进程模型/Supervisor树(svcd服务子进程化隔离防级联崩) ; MEOW_APP_SDK §6.1 系统UI归launcher+SDK/APP进程隔离; NOTIFICATION_SYSTEM §9 归属; SYSTEM_SETTINGS/SPEC_REF/DEVELOPMENT_PLAN 补齐 - README 重写为面向第三方(进度/架构/构建/部署/SDK开发; 固件打包标注本地工具不入库, 无敏感) - 遗留: SDK(App进程)侧通知中心/activity跳转 依赖 M3 导航栈(独立里程碑); 真机待验证
This commit is contained in:
1 parent
b6fe812251
commit
2b17f69f24
11 files changed
+1498
-60
No files matched your search
Binary file not shown.
@@ -325,7 +325,7 @@
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/poll.h>
|
||||
#include <poll.h> /* 0-warning:musl 把 <sys/poll.h> 重定向到 <poll.h>,用后者消除 -W#warnings */
|
||||
#include "meow_touch.h" /* M2.25:Launcher 统一触摸接口(电源菜单/确认框复用) */
|
||||
#include "pm_icons.h" /* M2.32:Material Symbols rounded 图标(电源菜单圆钮,alpha 位图+主题色) */
|
||||
#include "sb_icons.h" /* MD3 状态栏/通知中心图标(Material Symbols rounded,32x32 单通道 alpha) */
|
||||
@@ -343,6 +343,7 @@
|
||||
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
|
||||
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
|
||||
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
|
||||
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
|
||||
|
||||
/* ---- paths ---- */
|
||||
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
|
||||
@@ -3286,6 +3287,11 @@ static int launch_app(struct app *a);
|
||||
* 「系统设置」行时调用,返回后由调用方重置按键防抖基线防面板误切换)。 */
|
||||
static void run_settings_page(void);
|
||||
|
||||
/* 阶段B:通知中心面板(定义在下方 run_settings_page 之后;run_menu 顶部热区下滑调用)。
|
||||
* *want_launch 初始为 -1;若点击通知命中可启动 app,返回时置为 apps[] 下标(run_menu 据此
|
||||
* 走标准启动链路)。返回后由调用方重置按键防抖基线防面板误切换。 */
|
||||
static void run_notif_center(struct app *apps, int napps, int *want_launch);
|
||||
|
||||
static int touch_fd = -1;
|
||||
static int t_last_x = -1, t_last_y = -1;
|
||||
static int t_down = 0;
|
||||
@@ -4432,6 +4438,43 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||||
t_down_valid = 0;
|
||||
logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty,
|
||||
t_last_x, t_last_y, dx, dy);
|
||||
/* 阶段B:顶部热区下滑打开通知中心。判定在「滑动」分支内先于左缘返回——
|
||||
* 左缘(down_x<MEOW_EDGE_W)起点交给 meow_edge_back 优先消费;
|
||||
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
|
||||
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
|
||||
logmsg("menu: top-edge swipe-down -> notification center");
|
||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||||
int want_launch = -1;
|
||||
run_notif_center(apps, napps, &want_launch);
|
||||
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
last_input_ms = now_ms();
|
||||
menu_arm_touch(800);
|
||||
dirty = 1;
|
||||
/* 点击通知命中可启动 app → 走标准启动链路(与触摸行同路径)。 */
|
||||
if (want_launch >= 0 && want_launch < napps) {
|
||||
logmsg("menu: notification launch app idx=%d", want_launch);
|
||||
int ret = launch_app(&apps[want_launch]);
|
||||
menu_after_app_exit(ret == 0);
|
||||
last_input_ms = now_ms();
|
||||
dirty = 1;
|
||||
if (ret != 0) {
|
||||
snprintf(toast, sizeof toast, "CANNOT START: %s",
|
||||
apps[want_launch].name);
|
||||
toast_ms = TOAST_MS;
|
||||
}
|
||||
}
|
||||
continue; /* 本圈不再按 tap/其余处理 */
|
||||
}
|
||||
/* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、
|
||||
* 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */
|
||||
if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) {
|
||||
@@ -6715,6 +6758,725 @@ done:
|
||||
logmsg("settings page closed");
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* 阶段B:通知中心面板(状态栏下滑,系统级叠加层,§6.1 ③ 仅框架内部) */
|
||||
/* - 入口:run_menu 顶部热区下滑 → run_notif_center 阻塞子循环。 */
|
||||
/* - 结构:顶部把手/标题 + 快捷开关行(WiFi/蓝牙/主题/亮度/音量) + */
|
||||
/* 亮度/音量滑条 + 通知列表(app 图标+标题+内容+时间+进度)。 */
|
||||
/* - 只读写 meow_settings_* / meow_notif 存储,零旁路;MD3 接 s_ui_theme()。*/
|
||||
/* - 手势:面板内上滑/点空白/左缘左滑关闭(左缘复用 meow_edge_back)。 */
|
||||
/* - 物理键:系统语义不变(电源=息屏/亮屏/菜单/8s强关、音量=调音量、 */
|
||||
/* 播放=媒体预留),绝不映射为返回/确认/exit(APPS §3.4.1)。 */
|
||||
/* - 性能:打开/交互只标面板体区域脏(状态栏 0-27 由 draw_statusbar */
|
||||
/* 单独维护),绝不整屏重绘;滑条拖动实时 set_*(后端 clamp),抬手落盘。*/
|
||||
/* ================================================================== */
|
||||
|
||||
/* ---- 面板布局常量 ---- */
|
||||
#define NCP_TOP SB_BAR_H /* 28:面板顶缘(状态栏下) */
|
||||
#define NCP_GRAB_Y 32
|
||||
#define NCP_TITLE_Y 46
|
||||
#define NCP_CHIP_Y 86
|
||||
#define NCP_CHIP_H 44
|
||||
#define NCP_CHIP_W 70
|
||||
#define NCP_CHIP_GAP 8
|
||||
#define NCP_CHIP_N 5
|
||||
#define NCP_SLIDER_X 20
|
||||
#define NCP_SLIDER_W (fb_xres - 40)
|
||||
#define NCP_SL1_Y 162 /* 亮度滑条 track 顶(label 在 -26 =136) */
|
||||
#define NCP_SL2_Y 214 /* 音量滑条 track 顶(label 在 -26 =188) */
|
||||
#define NCP_DIV_Y 258
|
||||
#define NCP_LIST_TOP 266
|
||||
#define NCP_LIST_BOT 694
|
||||
#define NCP_ROW_H 92
|
||||
#define NCP_ROW_X 12
|
||||
#define NCP_ROW_W (fb_xres - 24)
|
||||
#define NCP_ICON_X 26
|
||||
#define NCP_TEXT_X 72
|
||||
#define NCP_TEXT_W (NCP_ROW_W - (NCP_TEXT_X - NCP_ROW_X) - 10)
|
||||
#define NCP_FOOT_Y 700
|
||||
#define NCP_MAX_ROWS 24 /* 面板一次缓冲的通知行上限(防御) */
|
||||
|
||||
/* chip 索引 */
|
||||
enum { NCP_CHIP_WIFI=0, NCP_CHIP_BT=1, NCP_CHIP_THEME=2, NCP_CHIP_BRIGHT=3,
|
||||
NCP_CHIP_VOL=4, NCP_CHIP_COUNT=5 };
|
||||
|
||||
/* 通知行快照(面板读取字段 + 缓存的 icon 句柄)。 */
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
char app_id[MEOW_NOTIF_APP_ID_MAX];
|
||||
char summary[MEOW_NOTIF_SUMMARY_MAX];
|
||||
char body[MEOW_NOTIF_BODY_MAX];
|
||||
char icon[MEOW_NOTIF_ICON_MAX];
|
||||
char target[MEOW_NOTIF_ACTIVITY_MAX];
|
||||
uint64_t created_ms;
|
||||
int progress;
|
||||
uint8_t urgency;
|
||||
meow_img *img; /* 缓存图标(meow_img_load_file 结果;NULL=回退首字符圆点) */
|
||||
} ncp_row_t;
|
||||
|
||||
static ncp_row_t g_rows[NCP_MAX_ROWS];
|
||||
static int g_nrows = 0;
|
||||
static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */
|
||||
static int g_ncp_napps = 0;
|
||||
|
||||
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
|
||||
static void ncp_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_id → 显示名(apps[] 匹配则用 name,否则原样 app_id;无则空串) */
|
||||
static const char *ncp_app_name(const char *app_id)
|
||||
{
|
||||
if (app_id && g_ncp_apps)
|
||||
for (int i = 0; i < g_ncp_napps; i++)
|
||||
if (!strcmp(g_ncp_apps[i].id, app_id)) return g_ncp_apps[i].name;
|
||||
return app_id ? app_id : "";
|
||||
}
|
||||
|
||||
/* 通知图标加载回调:把 app_icon(相对卡根/绝对)解成绝对路径,加载 meow_img。 */
|
||||
static void ncp_load_icon(ncp_row_t *r)
|
||||
{
|
||||
if (!r) return;
|
||||
r->img = NULL;
|
||||
if (!r->icon[0]) return;
|
||||
char path[MEOW_NOTIF_ICON_MAX + 80];
|
||||
if (r->icon[0] == '/') snprintf(path, sizeof path, "%s", r->icon);
|
||||
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", r->icon);
|
||||
r->img = meow_img_load_file(path);
|
||||
}
|
||||
|
||||
static int ncp_collect_cb(const meow_notif_t *n, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
|
||||
ncp_row_t *r = &g_rows[g_nrows++];
|
||||
memset(r, 0, sizeof *r);
|
||||
r->id = n->id;
|
||||
ncp_strncpy(r->app_id, n->app_id, sizeof r->app_id);
|
||||
ncp_strncpy(r->summary, n->summary, sizeof r->summary);
|
||||
ncp_strncpy(r->body, n->body, sizeof r->body);
|
||||
ncp_strncpy(r->icon, n->app_icon, sizeof r->icon);
|
||||
ncp_strncpy(r->target, n->target_activity, sizeof r->target);
|
||||
r->created_ms = n->created_ms;
|
||||
r->progress = n->progress;
|
||||
r->urgency = n->urgency;
|
||||
ncp_load_icon(r);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 释放全部缓存图标 + 清行数(面板关闭/列表刷新前用)。 */
|
||||
static void ncp_rows_free(void)
|
||||
{
|
||||
for (int i = 0; i < g_nrows; i++)
|
||||
if (g_rows[i].img) { meow_img_free(g_rows[i].img); g_rows[i].img = NULL; }
|
||||
g_nrows = 0;
|
||||
}
|
||||
|
||||
/* 重新采集通知列表(比 meow_notif_count 变化时调用)。 */
|
||||
static int ncp_refresh_rows(void)
|
||||
{
|
||||
ncp_rows_free();
|
||||
meow_notif_list(NULL, ncp_collect_cb, NULL);
|
||||
return g_nrows;
|
||||
}
|
||||
|
||||
/* 通知列表是否变化(简单按活跃数比;新条/撤下/TTL 释放都反映在 count)。 */
|
||||
static int ncp_rows_changed(void)
|
||||
{
|
||||
int c = meow_notif_count(NULL);
|
||||
if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS;
|
||||
return c != g_nrows;
|
||||
}
|
||||
|
||||
/* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */
|
||||
static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
|
||||
{
|
||||
if (!s) s = "";
|
||||
if (ui_width(s, size) <= maxw) { ui_draw(x, y, s, color, size); return; }
|
||||
int ell = ui_width("...", size);
|
||||
int limit = maxw - ell;
|
||||
int len = 0;
|
||||
char buf[128];
|
||||
while (s[len] && len < (int)sizeof buf - 8) {
|
||||
/* 取当前字节对应的 UTF-8 码长(1-4),避免切断多字节字符 */
|
||||
unsigned char c0 = (unsigned char)s[len];
|
||||
int cp = 1;
|
||||
if ((c0 & 0xE0) == 0xC0) cp = 2;
|
||||
else if ((c0 & 0xF0) == 0xE0) cp = 3;
|
||||
else if ((c0 & 0xF8) == 0xF0) cp = 4;
|
||||
if (len + cp >= (int)sizeof buf - 1) break;
|
||||
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
|
||||
buf[len + cp] = 0;
|
||||
if (ui_width(buf, size) > limit) { buf[len] = 0; break; }
|
||||
len += cp;
|
||||
}
|
||||
if (len > 0) {
|
||||
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
|
||||
ui_draw(x, y, buf, color, size);
|
||||
} else {
|
||||
ui_draw(x, y, "...", color, size);
|
||||
}
|
||||
}
|
||||
|
||||
/* 画一个小圆点(近似圆 = 圆角方 r=radius;主题 chip 用)。 */
|
||||
static void ncp_dot(int cx, int cy, int r, uint32_t col)
|
||||
{
|
||||
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, col);
|
||||
}
|
||||
|
||||
/* 画 32×32 图标(复用 sb_blit_icon;ink 垂直中心对齐 cy)。 */
|
||||
static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h)
|
||||
{
|
||||
int saved = g_sb_icon_cy;
|
||||
g_sb_icon_cy = cy;
|
||||
sb_blit_icon(icon, slot_x, col, ink_h);
|
||||
g_sb_icon_cy = saved;
|
||||
}
|
||||
|
||||
/* chip 位置(5 个,居中排布)。返回下标对应 chip 的 x 左缘。 */
|
||||
static int ncp_chip_x(int chip)
|
||||
{
|
||||
int total = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
|
||||
int x0 = (fb_xres - total) / 2;
|
||||
return x0 + chip * (NCP_CHIP_W + NCP_CHIP_GAP);
|
||||
}
|
||||
static int ncp_chip_hit(int chip, int tx, int ty)
|
||||
{
|
||||
int x = ncp_chip_x(chip);
|
||||
return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H);
|
||||
}
|
||||
|
||||
/* 面板当前 5 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */
|
||||
static void ncp_chip_state(int chip, int *on, char *val, int vlen)
|
||||
{
|
||||
*on = 0;
|
||||
if (val && vlen > 0) val[0] = 0;
|
||||
if (chip == NCP_CHIP_WIFI) {
|
||||
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
|
||||
*on = v;
|
||||
} else if (chip == NCP_CHIP_BT) {
|
||||
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
|
||||
*on = v;
|
||||
} else if (chip == NCP_CHIP_THEME) {
|
||||
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
|
||||
*on = (v == 1);
|
||||
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
|
||||
} else if (chip == NCP_CHIP_BRIGHT) {
|
||||
int v = 0; meow_settings_get_int("display.brightness", &v);
|
||||
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
|
||||
int vc = mnp_brightness_clamp(v, vmin);
|
||||
if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc);
|
||||
*on = (vc >= 50); /* 过半视为"亮"态 */
|
||||
} else if (chip == NCP_CHIP_VOL) {
|
||||
int v = 0; meow_settings_get_int("audio.volume", &v);
|
||||
if (val && vlen > 0) snprintf(val, vlen, "%d%%", v);
|
||||
*on = (v > 0); /* 非静音=开 */
|
||||
}
|
||||
}
|
||||
|
||||
/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。
|
||||
* 返回 1=主题变化(需重刷状态栏);0=其它。 */
|
||||
static int ncp_chip_apply(int chip)
|
||||
{
|
||||
if (chip == NCP_CHIP_WIFI) {
|
||||
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
|
||||
meow_settings_set_switch("network.wifi_enabled", !v);
|
||||
s_net_toggle_wifi(!v);
|
||||
s_net_refresh();
|
||||
} else if (chip == NCP_CHIP_BT) {
|
||||
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
|
||||
meow_settings_set_switch("network.bluetooth_enabled", !v);
|
||||
s_net_toggle_bt(!v);
|
||||
s_net_refresh();
|
||||
} else if (chip == NCP_CHIP_THEME) {
|
||||
/* 快捷主题:切到 manual(确保生效),再翻转 dark/light(display.theme)。 */
|
||||
int mode = THEME_MODE_MANUAL;
|
||||
meow_settings_get_enum("display.theme_mode", &mode);
|
||||
if (mode != THEME_MODE_MANUAL) meow_settings_set_enum("display.theme_mode", THEME_MODE_MANUAL);
|
||||
int v = 0; meow_settings_get_enum("display.theme", &v);
|
||||
meow_settings_set_enum("display.theme", v ? 0 : 1);
|
||||
paint_sb_right(); /* 状态栏文字/底色随主题刷新 */
|
||||
meow_settings_save();
|
||||
return 1; /* 主题变化 → 重刷状态栏 */
|
||||
} else if (chip == NCP_CHIP_BRIGHT) {
|
||||
/* 快捷亮度:在「极暗(下限)」与「100」之间切换。 */
|
||||
int cur = 0; meow_settings_get_int("display.brightness", &cur);
|
||||
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
|
||||
if (mnp_brightness_clamp(cur, vmin) < 100) {
|
||||
meow_settings_set_int("display.brightness", 100);
|
||||
write_brightness(100);
|
||||
g_sys.brightness = 100;
|
||||
} else {
|
||||
meow_settings_set_int("display.brightness", vmin);
|
||||
write_brightness(vmin);
|
||||
g_sys.brightness = vmin;
|
||||
}
|
||||
paint_sb_right();
|
||||
} else if (chip == NCP_CHIP_VOL) {
|
||||
/* 快捷音量:静音<->恢复上一次非零音量。 */
|
||||
static int last_nz = 60;
|
||||
int cur = 0; meow_settings_get_int("audio.volume", &cur);
|
||||
if (cur > 0) { last_nz = vol_clamp(cur); meow_settings_set_int("audio.volume", 0); }
|
||||
else { meow_settings_set_int("audio.volume", vol_clamp(last_nz)); }
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
meow_dac_vol_flush(); /* 抬手=无条件直写真实 DAC */
|
||||
}
|
||||
meow_settings_save();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。 */
|
||||
static void ncp_draw_list(int scroll)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
if (g_nrows == 0) {
|
||||
ui_draw(NCP_ROW_X + 8, NCP_LIST_TOP + 40, "暂无通知", th->on_surface_variant, 22);
|
||||
return;
|
||||
}
|
||||
int total = g_nrows * NCP_ROW_H;
|
||||
scroll = mnp_scroll_clamp(scroll, total, NCP_LIST_BOT - NCP_LIST_TOP);
|
||||
for (int i = 0; i < g_nrows; i++) {
|
||||
int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll;
|
||||
if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */
|
||||
const ncp_row_t *r = &g_rows[i];
|
||||
meow_gfx_fill_round(NCP_ROW_X, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container);
|
||||
/* 图标 */
|
||||
int icx = NCP_ICON_X, icy = ry + (NCP_ROW_H - 32) / 2;
|
||||
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
|
||||
else {
|
||||
/* 回退:主题色圆点 + 首字符 */
|
||||
const char *nm = ncp_app_name(r->app_id);
|
||||
ncp_dot(icx + 16, icy + 16, 16, th->primary);
|
||||
char ch = nm[0] ? nm[0] : '?';
|
||||
char cbuf[4] = { ch, 0, 0, 0 };
|
||||
/* 首字符若是多字节(中文),取整码 */
|
||||
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
|
||||
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
|
||||
int cw = ui_width(cbuf, 20);
|
||||
ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20);
|
||||
}
|
||||
/* app 名 + 时间 */
|
||||
ui_draw(NCP_TEXT_X, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16);
|
||||
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
|
||||
ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16), ry + 12, tbuf, th->on_surface_variant, 16);
|
||||
/* 标题 + 内容 */
|
||||
ncp_text(NCP_TEXT_X, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W);
|
||||
ncp_text(NCP_TEXT_X, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W);
|
||||
/* 进度(progress>=0):细条 */
|
||||
if (r->progress >= 0) {
|
||||
int px = NCP_TEXT_X, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14;
|
||||
fill_rect(px, py, pw, 4, th->surface_variant);
|
||||
int fw = r->progress * pw / 100;
|
||||
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
|
||||
}
|
||||
}
|
||||
/* 滚动条 */
|
||||
if (total > NCP_LIST_BOT - NCP_LIST_TOP) {
|
||||
int view = NCP_LIST_BOT - NCP_LIST_TOP;
|
||||
int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24;
|
||||
int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view);
|
||||
meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 3, th->surface_container);
|
||||
meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant);
|
||||
}
|
||||
}
|
||||
|
||||
/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。
|
||||
* 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过,
|
||||
* 使脏区仅限面板体,绝不整屏重绘。 */
|
||||
static void draw_notif_center(int scroll, int redraw_sb)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int W = fb_xres, H = fb_yres;
|
||||
fill_rect(0, 0, W, H, th->surface);
|
||||
/* 面板体(状态栏之下 surface_variant) */
|
||||
fill_rect(0, NCP_TOP, W, H - NCP_TOP, th->surface_variant);
|
||||
|
||||
/* 把手 */
|
||||
int grab_w = 44, grab_x = (W - grab_w) / 2;
|
||||
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
|
||||
/* 标题 + 「上滑/点空白关闭」 提示 */
|
||||
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
|
||||
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
|
||||
|
||||
/* 快捷开关行 */
|
||||
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "音量" };
|
||||
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
|
||||
int chip_x0 = (W - total_chip) / 2;
|
||||
for (int c = 0; c < NCP_CHIP_N; c++) {
|
||||
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
|
||||
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
|
||||
uint32_t bg = on ? th->primary : th->surface_container;
|
||||
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
|
||||
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
|
||||
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
|
||||
int icon_cy = NCP_CHIP_Y + 14;
|
||||
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_THEME) {
|
||||
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
|
||||
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
|
||||
}
|
||||
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
|
||||
else if (c == NCP_CHIP_VOL) ncp_sb_icon(SB_I_VOL, icon_cx, icon_cy, fg, 22);
|
||||
char label[24];
|
||||
snprintf(label, sizeof label, "%s", chip_lbl[c]);
|
||||
if (val[0] && (c == NCP_CHIP_BRIGHT || c == NCP_CHIP_VOL))
|
||||
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
|
||||
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
|
||||
label, fg, 14);
|
||||
}
|
||||
|
||||
/* 亮度滑条 */
|
||||
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
|
||||
int bval = 0; meow_settings_get_int("display.brightness", &bval);
|
||||
bval = mnp_brightness_clamp(bval, bmin);
|
||||
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
|
||||
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
|
||||
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
|
||||
th->on_surface_variant, 18);
|
||||
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
|
||||
|
||||
/* 音量滑条 */
|
||||
int vval = 0; meow_settings_get_int("audio.volume", &vval);
|
||||
vval = vol_clamp(vval);
|
||||
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
|
||||
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
|
||||
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
|
||||
th->on_surface_variant, 18);
|
||||
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
|
||||
|
||||
/* 分隔线 */
|
||||
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
|
||||
|
||||
/* 通知列表 */
|
||||
ncp_draw_list(scroll);
|
||||
|
||||
/* 底部提示 */
|
||||
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
|
||||
th->on_surface_variant, 16);
|
||||
|
||||
/* 状态栏画在面板体之上(0-27 单独由 draw_statusbar 维护/标脏,不落面板体脏区)。
|
||||
* 仅 redraw_sb=1(打开/主题变化)才重绘,交互时跳过以保持脏区只限面板体。 */
|
||||
if (redraw_sb) draw_statusbar();
|
||||
}
|
||||
|
||||
/* 触摸滑条带命中:origin 落在 (slugX, slugY) 的滑条行带内。返回 1=命中。 */
|
||||
static int ncp_slider_band(int tx, int ty, int sy)
|
||||
{
|
||||
return (tx >= NCP_SLIDER_X - 14 && tx <= NCP_SLIDER_X + NCP_SLIDER_W + 14 &&
|
||||
ty >= sy - 30 && ty <= sy + 24);
|
||||
}
|
||||
|
||||
/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘;亮度 clamp、音量 clamp)。 */
|
||||
static void ncp_slider_apply(int which, int tx, int save_now)
|
||||
{
|
||||
if (which == 1) {
|
||||
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
|
||||
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, bmin, 100);
|
||||
meow_settings_set_int("display.brightness", v);
|
||||
write_brightness(v);
|
||||
g_sys.brightness = v;
|
||||
paint_sb_right();
|
||||
if (save_now) meow_settings_save();
|
||||
} else if (which == 2) {
|
||||
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0,
|
||||
vol_clamp(settings_volume_max));
|
||||
meow_settings_set_int("audio.volume", v);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
if (save_now) { meow_dac_vol_flush(); meow_settings_save(); }
|
||||
else meow_dac_vol_write_throttled();
|
||||
}
|
||||
}
|
||||
|
||||
/* 通知点击:dismiss(标记阅读)+ 若 app 有 entry 记录则请求启动。返回 1=应关闭面板。 */
|
||||
static int ncp_row_tap(int tx, int ty, int scroll, int *want_launch)
|
||||
{
|
||||
int eff = ty - NCP_LIST_TOP + scroll;
|
||||
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
|
||||
if (row < 0 || row >= g_nrows) return 0;
|
||||
const ncp_row_t *r = &g_rows[row];
|
||||
meow_notif_dismiss(r->id); /* 点击 = 阅读,撤下 */
|
||||
/* 跳转:app entry 记录(apps[] 内且 ok)→ 记 want_launch(run_menu 走标准启动)。
|
||||
* target_activity 为后续 M3 导航预留下的跳转目标(activity 级路由后置)。 */
|
||||
if (want_launch) *want_launch = -1;
|
||||
if (g_ncp_apps)
|
||||
for (int i = 0; i < g_ncp_napps; i++)
|
||||
if (!strcmp(g_ncp_apps[i].id, r->app_id) && g_ncp_apps[i].ok) {
|
||||
if (want_launch) *want_launch = i;
|
||||
break;
|
||||
}
|
||||
return 1; /* 点击即关(启动由 run_menu 执行;无记录则仅关闭) */
|
||||
}
|
||||
|
||||
/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */
|
||||
static void run_notif_center(struct app *apps, int napps, int *want_launch)
|
||||
{
|
||||
if (want_launch) *want_launch = -1;
|
||||
int list_scroll = 0;
|
||||
unsigned char evbuf16[16];
|
||||
g_ncp_apps = apps; g_ncp_napps = napps;
|
||||
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
unsigned long long last_tick = now_ms();
|
||||
unsigned long long last_act = now_ms();
|
||||
unsigned long long last_listchk = now_ms();
|
||||
|
||||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||
int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */
|
||||
int closed = 0; /* 本次触发放置:关面板(跳回 run_menu) */
|
||||
int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */
|
||||
int wake_suppress = 0;
|
||||
unsigned long long pwr_hold_at = 0;
|
||||
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
|
||||
|
||||
int need_sb = 1; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */
|
||||
|
||||
/* 首次绘制 */
|
||||
ncp_refresh_rows();
|
||||
list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP);
|
||||
draw_notif_center(list_scroll, 1);
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
need_sb = 0;
|
||||
|
||||
for (;;) {
|
||||
unsigned long long now = now_ms();
|
||||
if (sigterm_flag) break;
|
||||
int need_paint = 0;
|
||||
|
||||
/* 自动息屏(同设置页语义) */
|
||||
if (meow_auto_slept && !meow_screen_on) {
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int wak = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wak = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wak = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wak = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wak = 1;
|
||||
}
|
||||
if (!wak && screen_off_mode == 1 && tfd >= 0) {
|
||||
struct pollfd spfd = { tfd, POLLIN, 0 };
|
||||
if (poll(&spfd, 1, 0) > 0) {
|
||||
unsigned char ev[16];
|
||||
while (read(tfd, ev, sizeof ev) == 16) {
|
||||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||||
int32_t val = *(int32_t *)(ev + 12);
|
||||
if (type == EV_KEY && code == BTN_TOUCH && val) wak = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wak) {
|
||||
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
||||
draw_notif_center(list_scroll, 1);
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
need_sb = 0;
|
||||
}
|
||||
usleep(POLL_MS * 1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||||
} else {
|
||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (play && !m_prev_play) {
|
||||
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
||||
last_act = now;
|
||||
}
|
||||
if (pwr && !m_prev_pwr) {
|
||||
pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; last_act = now;
|
||||
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
|
||||
}
|
||||
if (pwr && pwr_hold_at) {
|
||||
if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) {
|
||||
if (sim_no_exec) logmsg("notif-center pm: [sim-no-exec] hold %dms -> would forceoff", power_forceoff_ms);
|
||||
else { logmsg("notif-center pm: hold %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
|
||||
} else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) {
|
||||
pwr_menu_state = 1;
|
||||
logmsg("notif-center pm: hold %dms -> power menu", power_longpress_menu_ms);
|
||||
show_power_menu();
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1; need_paint = 1;
|
||||
goto ncp_keys_seeded;
|
||||
}
|
||||
}
|
||||
if (!pwr && m_prev_pwr) {
|
||||
if (pwr_suppress_click)
|
||||
logmsg("notif-center pm: release after menu -> no toggle");
|
||||
else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
|
||||
logmsg("notif-center pm: single-click -> toggle panel");
|
||||
meow_toggle_panel(); meow_auto_slept = 0; last_act = now_ms(); need_paint = 1;
|
||||
} else if (pwr_woke) {
|
||||
logmsg("notif-center pm: release after wake -> no toggle");
|
||||
}
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0;
|
||||
}
|
||||
}
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
need_paint = 1;
|
||||
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||||
ncp_keys_seeded:
|
||||
;
|
||||
|
||||
/* 触摸 */
|
||||
if (tfd >= 0 && meow_screen_on) {
|
||||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||||
if (poll(&pfd, 1, 0) > 0) {
|
||||
while (read(tfd, evbuf16, sizeof evbuf16) == 16) {
|
||||
uint16_t type = *(uint16_t *)(evbuf16 + 8), code = *(uint16_t *)(evbuf16 + 10);
|
||||
int32_t val = *(int32_t *)(evbuf16 + 12);
|
||||
if (type == EV_ABS) {
|
||||
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
|
||||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
|
||||
if (td && !td_ok && tlx >= 0 && tly >= 0) {
|
||||
tdx = tlx; tdy = tly; td_ok = 1;
|
||||
/* 按下原点落在滑条带 → 开始拖动 */
|
||||
if (drag_slider == 0) {
|
||||
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; ncp_slider_apply(1, tlx, 0); }
|
||||
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; ncp_slider_apply(2, tlx, 0); }
|
||||
}
|
||||
}
|
||||
if (drag_slider == 1 && td_ok) { ncp_slider_apply(1, tlx, 0); need_paint = 1; }
|
||||
else if (drag_slider == 2 && td_ok) { ncp_slider_apply(2, tlx, 0); need_paint = 1; }
|
||||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||||
if (val && !td) {
|
||||
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
|
||||
else { td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0; }
|
||||
} else if (!val && td) {
|
||||
td = 0;
|
||||
if (drag_slider) { /* 滑条拖动结束 → 落盘 */
|
||||
ncp_slider_apply(drag_slider, tlx >= 0 ? tlx : tdx, 1);
|
||||
drag_slider = 0; need_paint = 1;
|
||||
} else if (td_ok) {
|
||||
int dx = tlx - tdx, dy = tly - tdy;
|
||||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||||
/* 左缘左滑 = 关闭(复用全局左缘返回语义) */
|
||||
if (meow_edge_back(tdx, dx, dy)) { closed = 1; break; }
|
||||
if (adx < 60 && ady < 60) { /* tap */
|
||||
int tx = tdx, ty = tdy;
|
||||
int hit = 0;
|
||||
for (int c = 0; c < NCP_CHIP_COUNT; c++)
|
||||
if (ncp_chip_hit(c, tx, ty)) {
|
||||
if (ncp_chip_apply(c)) need_sb = 1; /* 主题变化 → 重刷状态栏 */
|
||||
hit = 1; need_paint = 1; break;
|
||||
}
|
||||
if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { ncp_slider_apply(1, tx, 1); hit = 1; need_paint = 1; }
|
||||
else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { ncp_slider_apply(2, tx, 1); hit = 1; need_paint = 1; }
|
||||
if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) {
|
||||
if (ncp_row_tap(tx, ty, list_scroll, want_launch)) {
|
||||
hit = 1;
|
||||
closed = 1; /* 点击通知:dismiss + 记录启动 → 关面板 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) { closed = 1; break; } /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */
|
||||
} else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) {
|
||||
closed = 1; break; /* 面板内上滑 → 关闭 */
|
||||
}
|
||||
/* 其余横滑/下滑忽略(不关闭不动作) */
|
||||
}
|
||||
td_ok = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (closed) break; /* 上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */
|
||||
|
||||
/* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */
|
||||
if (now - last_listchk >= 500) {
|
||||
last_listchk = now;
|
||||
meow_notif_tick();
|
||||
if (ncp_rows_changed()) {
|
||||
ncp_refresh_rows();
|
||||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||||
NCP_LIST_BOT - NCP_LIST_TOP);
|
||||
need_paint = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
|
||||
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
|
||||
* 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。 */
|
||||
if (need_paint) {
|
||||
last_act = now;
|
||||
draw_notif_center(list_scroll, need_sb);
|
||||
need_sb = 0;
|
||||
if (after_modal) { meow_dirty_full(); after_modal = 0; }
|
||||
else meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
|
||||
meow_ui_flush();
|
||||
}
|
||||
/* ~1s 状态栏时钟 */
|
||||
if (now - last_tick >= 1000) {
|
||||
last_tick = now;
|
||||
draw_clock();
|
||||
fb_norm_buf0();
|
||||
meow_ui_flush();
|
||||
}
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_power); kp_close(&kp_keys);
|
||||
ncp_rows_free();
|
||||
logmsg("notification center closed");
|
||||
}
|
||||
|
||||
/* Bring the panel up for the menu, but ONLY when it is actually dark:
|
||||
* brightness 0 (the player's own screen-off/standby writes brightness 0
|
||||
* then FBIOBLANK(4); on this device bl_power stays 0, so brightness==0 is
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/* meow_notif_panel.h — 通知中心面板的纯逻辑(header-only,宿主可单测)。
|
||||
*
|
||||
* 定位:把通知中心面板里「无副作用、可独立判定」的部分抽成 static inline,
|
||||
* 供 launcher(meow_launcherd.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现,
|
||||
* 避免两端漂移;绘制/触摸循环等依赖 launcher static 符号的部分留在 meow_launcherd.c 内。
|
||||
*
|
||||
* 不依赖 launcher 头 / SDK 头,仅 <stdint.h> <string.h> <stdio.h> <time.h> —— 纯宿主可编。
|
||||
*/
|
||||
#ifndef MEOW_NOTIF_PANEL_H
|
||||
#define MEOW_NOTIF_PANEL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
/* ---- 手势判定 ---- */
|
||||
|
||||
/* 左缘宽度:与 launcher meow_touch.h MEOW_EDGE_W(24) / SDK 全局左缘返回同口径。
|
||||
* 顶部下滑打开通知中心时,左缘起点保留给「左缘返回」,不与其抢占(spec-ref §5.1)。 */
|
||||
#ifndef MNP_EDGE_W
|
||||
#define MNP_EDGE_W 24
|
||||
#endif
|
||||
|
||||
/* 顶部热区下滑 → 打开通知中心(纯函数)。
|
||||
* down_x/down_y=抬手原点;dx/dy=抬手位移(当前-原点,即 t_last - t_down);hotzone=顶部热区高
|
||||
* (launcher=SB_BAR_H=28);thresh=下滑触发位移阈值(launcher=MENU_SWIPE_THRESH=65)。
|
||||
* 返回 1=应打开。约束:起点不在左缘(down_x>=MNP_EDGE_W,左缘保留给返回)+ 起点落在顶部热区
|
||||
* (down_y ∈ [0,hotzone))+ 向下主导(dy>=thresh 且 |dy|>|dx|)。调用方通常先判左缘返回
|
||||
* (meow_edge_back)+ 本函数,两者互斥。 */
|
||||
static inline int mnp_top_swipe_down(int down_x, int down_y, int dx, int dy,
|
||||
int hotzone, int thresh)
|
||||
{
|
||||
if (down_x < MNP_EDGE_W) return 0; /* 左缘保留给返回手势 */
|
||||
if (down_y < 0 || down_y >= hotzone) return 0;
|
||||
if (dy < thresh) return 0; /* 未达下滑阈值 / 向上 */
|
||||
int ax = dx < 0 ? -dx : dx;
|
||||
return ax < dy; /* 向下主导(横向位移小于竖向) */
|
||||
}
|
||||
|
||||
/* 竖向主导上滑(面板内关闭):dy<=-thresh 且 |dy|>|dx|。返回 1=应关闭。 */
|
||||
static inline int mnp_vertical_up(int dx, int dy, int thresh)
|
||||
{
|
||||
if (dy > -thresh) return 0;
|
||||
int ax = dx < 0 ? -dx : dx;
|
||||
return ax < -dy;
|
||||
}
|
||||
|
||||
/* ---- 滑条 ---- */
|
||||
|
||||
/* 滑条值映射:px(屏幕坐标)→ 值,clamp 到 [min,max]。sx=track 左缘、sw=track 宽。
|
||||
* 与 launcher 设置页 s_slider_val_from_x 同构(此处参数化,供面板亮度/音量滑条与单测用)。 */
|
||||
static inline int mnp_slider_val_from_x(int px, int sx, int sw, int min, int max)
|
||||
{
|
||||
int range = max - min;
|
||||
int v = min + (range ? (px - sx) * range / sw : 0);
|
||||
if (v < min) v = min;
|
||||
if (v > max) v = max;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---- 亮度 clamp ---- */
|
||||
|
||||
/* 亮度 clamp:下限 vmin(=display.brightness_min,≥1 防黑屏)、上限 100(防闪屏)。
|
||||
* vmin<1 时钳到 1。与 SYSTEM_SETTINGS §1 / spec-ref A-3 一致(旧文档 25 为陈旧值)。 */
|
||||
static inline int mnp_brightness_clamp(int v, int vmin)
|
||||
{
|
||||
if (vmin < 1) vmin = 1;
|
||||
if (v < vmin) v = vmin;
|
||||
if (v > 100) v = 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---- 时间显示 ---- */
|
||||
|
||||
/* 通知时间(created_ms 墙钟 epoch ms)→ 显示串。
|
||||
* <60s「刚刚」;<60min「N分钟前」;否则本地「HH:MM」。now_ms=当前墙钟 epoch ms。 */
|
||||
static inline void mnp_time_ago(uint64_t created_ms, uint64_t now_ms, char *buf, int len)
|
||||
{
|
||||
if (!buf || len <= 0) return;
|
||||
if (now_ms < created_ms) now_ms = created_ms; /* 时钟回拨防御 */
|
||||
uint64_t diff = now_ms - created_ms;
|
||||
if (diff < 60000u) { if (len >= 1) snprintf(buf, (size_t)len, "刚刚"); return; }
|
||||
if (diff < 3600000u) {
|
||||
snprintf(buf, (size_t)len, "%llu分钟前", (unsigned long long)(diff / 60000u));
|
||||
return;
|
||||
}
|
||||
time_t t = (time_t)(created_ms / 1000u);
|
||||
struct tm tm;
|
||||
if (localtime_r(&t, &tm))
|
||||
snprintf(buf, (size_t)len, "%02d:%02d", tm.tm_hour, tm.tm_min);
|
||||
else if (len >= 1) snprintf(buf, (size_t)len, "--:--");
|
||||
}
|
||||
|
||||
/* ---- 列表命中 ---- */
|
||||
|
||||
/* 通知列表行命中:y 为绝对纵坐标;list_top/list_bot 为列表区上下缘;row_h 为行高。
|
||||
* 返回行号(0 起);越界返回 -1。 */
|
||||
static inline int mnp_row_at(int y, int list_top, int list_bot, int row_h)
|
||||
{
|
||||
if (y < list_top || y >= list_bot) return -1;
|
||||
int r = (y - list_top) / row_h;
|
||||
return r;
|
||||
}
|
||||
|
||||
/* 列表滚动 clainmp:把 scroll 钳到 [0, total-view]。content 高度=total(总行数*row_h)。 */
|
||||
static inline int mnp_scroll_clamp(int scroll, int total, int view)
|
||||
{
|
||||
if (total <= view) return 0;
|
||||
int mx = total - view;
|
||||
if (scroll < 0) scroll = 0;
|
||||
if (scroll > mx) scroll = mx;
|
||||
return scroll;
|
||||
}
|
||||
|
||||
#endif /* MEOW_NOTIF_PANEL_H */
|
||||
@@ -0,0 +1,236 @@
|
||||
/* 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_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");
|
||||
}
|
||||
|
||||
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_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;
|
||||
}
|
||||
Reference in new issue
Block a user