/* main_menu.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" #include "meow_ams_start.h" /* MSG_AMS_START 拉起决策(meow_ams_start.c):run_menu 消费用 */ /* set by run_menu: the player's panel was blanked (screen timeout) when the * menu opened. fb_open() unblanks it so the menu is visible; on session end * restore_backlight() re-blanks (brightness 0 + FBIOBLANK(4)) to put the * physical panel back into the state the player's display state machine * assumes — otherwise the player resumes believing the panel is blanked * while the panel is actually lit (frozen frame). Blanking is idempotent * and matches what the player itself did, so it cannot wedge the player. */ int menu_opened_blank = 0; /* launch_app/launch_activity 返回 -1 时的 toast:缺二进制依赖(app.json depends)→ 「缺依赖: 」; * 其余启动失败沿用「CANNOT START: 」。原因由 meow_dep.c 的 meow_dep_block_reason() 提供。 */ static void launch_fail_toast(char *toast, size_t n, const char *name) { const char *bd = meow_dep_block_reason(); if (bd && bd[0]) snprintf(toast, n, "缺依赖: %s", bd); else snprintf(toast, n, "CANNOT START: %s", name); } /* ---- config (MeowAPPs/meow.conf): pure black background by default ---- */ uint32_t menu_bg = 0x000000; static int touch_fd = -1; static int t_last_x = -1, t_last_y = -1; static int t_down = 0; static int t_down_x = 0, t_down_y = 0; /* M2.23 手势原点锁定:BTN_TOUCH=1 只标「已按下」,坐标(goodix)不保证先于按下边沿, * 故 t_down_x/y 延迟到「本次按下之后」的首个有效 ABS 才锁定,避免把上一手势残留 * (-1/旧坐标) 当本次原点。t_down_valid=0 表示本次尚未锁到有效坐标。 */ static int t_down_valid = 0; /* ⑨ 顶部热区「跟手下拉」进入通知中心的 per-gesture latch:进入后置 1,本次手势不再二次进入; * 由下次 touch-down 复位。防止「拖动中切入 run_notif_center 后返回」把同次手势再判成打开/滑动。 */ static int menu_live_open_pending = 0; /* Bug 2/4 (2026-08-28): after an app exits we return to the menu, but touch * events that were generated DURING the app run (the goodix ghost touch the * v6.4 watchdog guards against, plus the back-tap/press that actually exited * the app) are still queued on the launcher's touch fd and get replayed as * fresh menu taps. A replayed tap on an app row relaunches the app (symptom: * BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO * PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen * flashes back to the player — Bug 4). We drain the queue immediately after * the app exits and ignore touches for a short cooldown that covers the * measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */ static unsigned long long menu_touch_cooldown_until = 0; static void menu_drain_touch(void) { if (touch_fd >= 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } } } static void menu_arm_touch(long ms) { menu_drain_touch(); menu_touch_cooldown_until = now_ms() + ms; } /* Drain the touch queue + arm the post-app cooldown (call right after the * app exits, before the menu resumes polling touch). The drain always runs so * touches queued by the launch-attempt tail are cleared; the cooldown only * arms when an app actually ran (app_ran) so a failed launch doesn't lock the * menu out for 2.5s. */ static void menu_after_app_exit(int app_ran) { menu_drain_touch(); /* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。 * 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短, * 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */ if (app_ran) menu_touch_cooldown_until = now_ms() + 1200; } /* Header block (status bar + MEOWAPPS title + thin divider) covering the * region y=0..58, kept separate from the app-list body so the ~1s clock tick * (draw_clock) never redraws or tears the list. draw_menu fills the whole * background then calls draw_header_content. The menu is pre-drawn before * fb_norm_buf0 so the initial pan shows a complete frame. */ static int menu_title_size(void); /* 前向声明:draw_header_content 用(定义在下方布局帮助块) */ static void draw_header_content(void) { draw_statusbar(); const meow_ui_theme_t *th = s_ui_theme(); int tsize = menu_title_size(); /* 标题字号接 font.size.title */ /* M2.6 标题真字体(字号接 font.size.title;回退点阵3x);MD3 on_surface 主文字色 */ if (mfont_ready) { meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(th->on_surface), tsize); fill_rect(0, 48 + meow_font_line_height(tsize) + 2, fb_xres, 2, th->outline); } else { draw_text(24, 28, "MEOWAPPS", 3, th->on_surface); fill_rect(0, 56, fb_xres, 2, th->outline); } } /* 每秒一次刷新状态栏时钟(顶部 HH:MM:SS,唯一每秒都变的值)。Stage-One 状态栏收敛后, * launcher 不再自绘状态栏——draw_clock 只经 paint_sb_clock() → sb_sdk_refresh() 把权威 * 状态喂进 SDK 全局状态栏 + 标脏顶条,由 SDK present() 顶条重绘(时钟/电量/音量等一并刷新)。 * 绝不动应用列表(仅顶条 28px 脏),故列表不重绘/不撕裂;无 amixer/sysfs 每帧开销。 */ void draw_clock(void) { paint_sb_clock(); /* → sb_sdk_refresh():喂 SDK + 标脏顶条,present() 时 SDK 重绘状态栏 */ } /* M2.36 右向小箭头(进入/下拉提示)。点阵后备字库(5x7)缺 '>' 字形(glyph_index 落到空 * 格),故用像素画一个 4x7 的 '>';顶点在 (x,y) 指向右。设置页选择器/设置项行复用。 */ void s_draw_chevron(int x, int y, uint32_t color) { for (int i = 0; i < 4; i++) { put_px(x - i, y - i, color); put_px(x - i, y + i, color); } } /* M2.39 左向小箭头(返回提示)。点阵后备字库同样缺 '<' 字形('‹' 会渲染成方框), * 故用像素画一个指向左的 4x7 '<';顶点在 (x,y)。设置页底部「返回」/任何左向提示复用。 */ void s_draw_chevron_left(int x, int y, uint32_t color) { for (int i = 0; i < 4; i++) { put_px(x + i, y - i, color); put_px(x + i, y + i, color); } } /* ================================================================== */ /* 主界面 MD3 双布局(卡片宫格 card / 列表 list)+ 应用图标 + 背景 */ /* 布局切换:设置键 ui.home_layout(card=0 默认 / list=1)。 */ /* 图标:读 app.json icon(相对应用目录/绝对),stb_image 解码一次进 */ /* 缓存(menu_icons_load);缺失/失败 → 主题色圆角块+首字母。 */ /* 背景:display.background(纯色名 或 图片路径 MeowAPPs/backgrounds/xx.png)*/ /* 图片 → cover 居中裁剪铺底 + 叠加层(深色叠黑/浅色叠白, */ /* 强度 display.bg_overlay_alpha 缺省 128)。 */ /* 观感:MD3 语义色全程(s_ui_theme()),无散落硬编码;标题字号接 */ /* font.size.title。状态栏由 SDK 全局绘制(不在此改动)。 */ /* ================================================================== */ /* 布局几何常量(480×720;app 区在标题栏之下、设置行之上) */ #define MENU_GRID_PAD 12 /* 宫格卡片外边距 */ #define MENU_GRID_GAP 10 /* 宫格卡片间隙 */ #define MENU_GRID_COLS 2 #define MENU_GRID_CARD_H 112 /* 宫格卡片高 */ #define MENU_GRID_RADIUS 16 /* 卡片圆角 */ #define MENU_GRID_ICON 44 /* 宫格图标 ink 高(方) */ #define MENU_LIST_ROW_H 60 /* 列表行高(大图标行) */ #define MENU_LIST_ICON 40 /* 列表图标 ink 高(方) */ #define MENU_LIST_ICON_X 24 /* 列表图标左缘 */ /* 应用图标缓存(进主界面加载一次,不每帧解码;index 对齐 apps[]) */ static meow_img *s_app_icons[MAX_APPS]; /* 当前按压命中的应用格(-1=无);触摸按下置位、抬起清除(按压态视觉) */ static int g_menu_pressed = -1; /* 背景状态(display.background:纯色名 or 图片路径) */ static meow_img *s_bg_img = NULL; /* 解码后的背景图(缓存,NULL=纯色) */ static int s_bg_is_img = 0; /* 1=图片背景 */ static int s_bg_overlay = 128; /* 叠加层强度 0-255 */ static uint16_t *s_bg_cache = NULL; /* cover+overlay 后的全屏 RGB565 快照 */ static int s_bg_cache_w = 0, s_bg_cache_h = 0; static unsigned s_bg_sig = 0; /* 背景设置签名(变化才重建缓存) */ static int s_bg_ready = 0; /* 背景是否已初始化(首次必然构建,防签名 0 碰撞) */ /* 当前主界面布局:0=card(默认) 1=list */ static int menu_layout(void) { int v = 0; if (meow_settings_get_enum("ui.home_layout", &v) != 0) v = 0; return v ? 1 : 0; } /* 标题字号(读 font.size.title,钳制到设置页取值域 16-40) */ static int menu_title_size(void) { int t = 28; if (meow_settings_get_int("font.size.title", &t) != 0) t = 28; if (t < 16) t = 16; if (t > 40) t = 40; return t; } /* 应用区顶缘:标题基线(y=48) + 标题行高 + 分隔线下方留白,且不小于 ROW_TOP。 * 大标题(字号>36)时下移,避免与首行卡片重叠。 */ static int menu_apps_top(void) { int t = ROW_TOP; if (mfont_ready) { int d = 48 + meow_font_line_height(menu_title_size()) + 2 + 10; if (d > t) t = d; } return t; } /* 返回足够对比的前景色(YIQ 亮度阈值 140),用于主题色底上的文字/字母。 */ static uint32_t menu_contrast(uint32_t c) { int r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF; long y = (r * 299 + g * 587 + b * 114) / 1000; return y >= 140 ? 0x000000 : 0xFFFFFF; } /* 按 on_surface 亮度判定当前主题是否深色(浅色主题 on_surface 为深色小值)。 * 用于图片背景叠加层配色:深色叠黑 / 浅色叠白(用户定案,固定跟明暗主题)。 */ static int menu_is_dark(const meow_ui_theme_t *th) { return th ? (th->on_surface > 0x808080) : 1; } /* 背景设置签名:display.background 串 + 叠加层强度 + 明暗 + ui.style(§8.1.1:背景底随 * style×mode 换族)。变化才重建缓存——ui.style 切换必须入签名,否则切风格后主界面背景滞留旧族。 */ static unsigned menu_bg_sig(const meow_ui_theme_t *th, int ov) { char bg[MEOW_SET_MAX_LEN]; if (meow_settings_get_str("display.background", bg, sizeof bg) != 0) bg[0] = 0; int style = 0; if (meow_settings_get_enum("ui.style", &style) != 0) style = 0; unsigned h = 2166136261u; for (const char *p = bg; *p; p++) { h ^= (unsigned char)*p; h *= 16777619u; } h = h * 31u + (unsigned)ov; h = h * 31u + (unsigned)menu_is_dark(th); h = h * 31u + (unsigned)style; return h; } /* 解码后背景图路径解析:绝对/相对卡根(MeowOS 约定图片放 MeowOS/opt 或卡根, * 计划定案为 MeowAPPs/backgrounds/xx.png → 卡根相对)。 */ static meow_img *menu_bg_load_img(const char *bg) { char path[320]; if (bg[0] == '/') snprintf(path, sizeof path, "%s", bg); else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", bg); return meow_img_load_file(path); } /* 重建背景缓存:铺基色 → cover 居中裁剪图片 → 叠加层 → 快照 480×720 到 s_bg_cache。 */ static void menu_bg_cache_build(const meow_ui_theme_t *th) { int W = fb_xres, H = fb_yres; fill_rect(0, 0, W, H, menu_bg); if (!s_bg_is_img || !s_bg_img) { free(s_bg_cache); s_bg_cache = NULL; return; } int iw = meow_img_w(s_bg_img), ih = meow_img_h(s_bg_img); if (iw <= 0 || ih <= 0) { free(s_bg_cache); s_bg_cache = NULL; return; } /* cover:整数定点求 scale=max(W/iw,H/ih),居中裁剪;dw/dh 向上取整保证铺满(不露边) */ long long sw = (long long)W * 65536 / iw; long long sh = (long long)H * 65536 / ih; long long s = sw > sh ? sw : sh; int dw = (int)(((long long)iw * s + 65535) / 65536); int dh = (int)(((long long)ih * s + 65535) / 65536); int ox = (dw - W) / 2, oy = (dh - H) / 2; if (dw > 0 && dh > 0) meow_ui_img_scaled(-ox, -oy, dw, dh, s_bg_img); /* 叠加层(深色叠黑/浅色叠白,强度 s_bg_overlay) */ int ov = s_bg_overlay; if (ov < 0) ov = 0; if (ov > 255) ov = 255; if (ov > 0) meow_ui_fill_rect_alpha(0, 0, W, H, menu_is_dark(th) ? 0x000000 : 0xFFFFFF, ov); /* 快照全屏 565 到缓存(仅支持 16bpp;32bpp 回退纯色) */ int pitch = meow_ui_back_buf_pitch(); if (meow_ui_back_buf_bpp() != 16) { free(s_bg_cache); s_bg_cache = NULL; return; } if (s_bg_cache && (s_bg_cache_w != W || s_bg_cache_h != H)) { free(s_bg_cache); s_bg_cache = NULL; } if (!s_bg_cache) { s_bg_cache = (uint16_t *)malloc((size_t)W * H * 2); if (!s_bg_cache) return; s_bg_cache_w = W; s_bg_cache_h = H; } const unsigned char *bb = meow_ui_back_buf(); for (int y = 0; y < H; y++) memcpy(s_bg_cache + (size_t)y * W, bb + (size_t)y * pitch, (size_t)W * 2); } /* 背景准备:读设置、检测变化、重载图片/重建缓存。draw_menu 每次调用(变化才重建)。 */ static void menu_bg_prepare(const meow_ui_theme_t *th) { int ov = 128; if (meow_settings_get_int("display.bg_overlay_alpha", &ov) != 0) ov = 128; if (ov < 0) ov = 0; if (ov > 255) ov = 255; unsigned sig = menu_bg_sig(th, ov); if (s_bg_ready && sig == s_bg_sig) { if (ov != s_bg_overlay) s_bg_overlay = ov; return; } s_bg_sig = sig; s_bg_overlay = ov; s_bg_ready = 1; char bg[MEOW_SET_MAX_LEN]; if (meow_settings_get_str("display.background", bg, sizeof bg) != 0) bg[0] = 0; if (s_bg_img) { meow_img_free(s_bg_img); s_bg_img = NULL; } s_bg_is_img = 0; menu_bg = 0x000000; if (strchr(bg, '/')) { /* 含路径分隔符 → 图片路径 */ /* 基色用主题 surface:图片透明区/加载失败时回退,避免生硬黑底 */ menu_bg = th->surface; s_bg_img = menu_bg_load_img(bg); if (s_bg_img) s_bg_is_img = 1; } else { /* 纯色名(沿用既有 black/blue/green 语义;随 ui.style 换族) */ int mstyle = 0; if (meow_settings_get_enum("ui.style", &mstyle) != 0) mstyle = 0; if (mstyle == MEOW_STYLE_MIUIX) { /* MIUIx 暖色族(与 SDK bg_surfaces 同色) */ if (!strcmp(bg, "blue")) menu_bg = 0x101A2E; else if (!strcmp(bg, "green")) menu_bg = 0x14251A; else menu_bg = 0x181310; /* black(默认)=MIUIx 深色面 */ } else { /* MD3(既有) */ if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */ else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */ else menu_bg = 0x000000; /* black(默认) */ } } menu_bg_cache_build(th); } /* 背景绘制:图片缓存 blit(快)或纯色填充。 */ static void menu_bg_paint(void) { int W = fb_xres, H = fb_yres; if (s_bg_is_img && s_bg_img && s_bg_cache) { int pitch = meow_ui_back_buf_pitch(); for (int y = 0; y < H; y++) memcpy(fb + (size_t)y * pitch, s_bg_cache + (size_t)y * W, (size_t)W * 2); } else { fill_rect(0, 0, W, H, menu_bg); } } /* 应用图标路径:app.json icon 相对应用目录(dirname(exec))/ 绝对;失败回退卡根相对。 */ static void menu_icon_path(const struct app *a, char *out, int olen) { if (a->icon[0] == '/') { snprintf(out, olen, "%s", a->icon); return; } const char *sl = strrchr(a->exec, '/'); if (sl && a->icon[0]) { int n = (int)(sl - a->exec); char dir[180]; if (n >= (int)sizeof dir) n = (int)sizeof dir - 1; memcpy(dir, a->exec, (size_t)n); dir[n] = 0; snprintf(out, olen, "%s/%s", dir, a->icon); } else { snprintf(out, olen, "/usr/data/mnt/sd_0/%s", a->icon); } } /* 加载单个应用图标(解码一次进缓存);失败返回 NULL(调用方回落首字母占位)。 */ static meow_img *menu_app_icon_load(const struct app *a) { if (!a->icon[0]) return NULL; char p[220]; menu_icon_path(a, p, sizeof p); meow_img *img = meow_img_load_file(p); if (!img && a->icon[0] != '/') { /* 应用目录相对失败 → 回退卡根相对 */ snprintf(p, sizeof p, "/usr/data/mnt/sd_0/%s", a->icon); img = meow_img_load_file(p); } logmsg("icon: %s -> %s %s", a->id[0] ? a->id : a->name, p, img ? "OK" : "FAIL"); /* M2.87 排查 */ return img; } /* 进主界面/热重载:批量加载应用图标(一次,不每帧解码;先释放旧图防泄漏)。 */ static void menu_icons_load(struct app *apps, int napps) { for (int i = 0; i < MAX_APPS; i++) if (s_app_icons[i]) { meow_img_free(s_app_icons[i]); s_app_icons[i] = NULL; } for (int i = 0; i < napps; i++) s_app_icons[i] = menu_app_icon_load(&apps[i]); } /* 主界面退出:释放图标缓存。 */ static void menu_icons_free(void) { for (int i = 0; i < MAX_APPS; i++) if (s_app_icons[i]) { meow_img_free(s_app_icons[i]); s_app_icons[i] = NULL; } g_menu_pressed = -1; } /* 首字符(UTF-8 整码,中文取第一字) */ static void menu_first_glyph(const char *nm, char out[4]) { out[0] = out[1] = out[2] = out[3] = 0; if (!nm || !nm[0]) { out[0] = '?'; return; } char c0 = nm[0]; out[0] = c0; if ((unsigned char)c0 >= 0x80 && nm[1]) { out[1] = nm[1]; if ((unsigned char)c0 >= 0xE0 && nm[2]) out[2] = nm[2]; } } /* 名称截断(按可用像素宽;超出加 '…')。 */ static void menu_name_fit(const char *name, int maxw, int size, char *out, int olen) { char buf[64]; snprintf(buf, sizeof buf, "%s", name); if (ui_width(buf, size) <= maxw) { snprintf(out, olen, "%s", buf); return; } int n = (int)strlen(buf); while (n > 0 && ui_width(buf, size) > maxw) { /* 回退一个 UTF-8 字符边界 */ do { buf[--n] = 0; } while (n > 0 && ((unsigned char)buf[n] & 0xC0) == 0x80); } snprintf(out, olen, "%s...", buf); } /* 图标绘制:primary 圆角垫块始终垫底(MD3 图标 tile),首字母叠加其上,最后有图叠加 * meow_ui_img_cover(居中裁方)。 * 修复:原实现 `if (icon) { meow_ui_img_cover; return; }` 在图标为透明背景 logo(不透明像素 * 极少/无)时整区只剩卡面,且因 icon!=NULL 完全跳过占位 →「图标区+首字母占位都不见了」。 * 现让垫块+首字母先画:不透明/方图盖住它们(观感=纯图);透明图则露出垫块+首字母(占位)。 * 图标区任何情况下都非空,且首字母占位不再被图标句柄吞掉。 */ static void menu_draw_icon(const struct app *a, meow_img *icon, int x, int y, int sz, const meow_ui_theme_t *th) { meow_gfx_fill_round(x, y, sz, sz, sz * 3 / 10, th->primary); char g[4]; menu_first_glyph(a->name, g); uint32_t fg = menu_contrast(th->primary); int cw = ui_width(g, 20); ui_draw(x + sz / 2 - cw / 2, y + sz / 2 - meow_font_line_height(20) / 2, g, fg, 20); if (icon) { meow_ui_img_cover(x, y, sz, icon); } } /* 应用格几何(grid/list 统一入口;供 draw 与 touch 命中共用)。 */ static void menu_cell_rect(int i, int layout, int apps_top, int *cx, int *cy, int *cw, int *ch) { if (layout == 0) { /* card grid */ int row = i / MENU_GRID_COLS, col = i % MENU_GRID_COLS; *cw = (fb_xres - 2 * MENU_GRID_PAD - (MENU_GRID_COLS - 1) * MENU_GRID_GAP) / MENU_GRID_COLS; *ch = MENU_GRID_CARD_H; *cx = MENU_GRID_PAD + col * (*cw + MENU_GRID_GAP); *cy = apps_top + row * (*ch + MENU_GRID_GAP); } else { /* list */ *cw = fb_xres - 16; *ch = MENU_LIST_ROW_H; *cx = 8; *cy = apps_top + i * MENU_LIST_ROW_H; } } /* 触摸命中应用格;未命中返回 -1。 */ static int menu_hit_app(int x, int y, struct app *apps, int napps, int state, int layout, int apps_top) { (void)apps; /* 命中判定只依赖几何;apps 保留作接口(0-warning) */ if (state != 0 || napps <= 0) return -1; for (int i = 0; i < napps; i++) { int cx, cy, cw, ch; menu_cell_rect(i, layout, apps_top, &cx, &cy, &cw, &ch); if (x >= cx && x < cx + cw && y >= cy && y < cy + ch) return i; } return -1; } /* 宫格卡片:surface_container 底 + 圆角 + 选中/按压态 tonal 高亮 + 图标 + 名称。 */ static void menu_draw_grid_cell(const struct app *a, meow_img *icon, int x, int y, int w, int h, int sel, int pressed, const meow_ui_theme_t *th) { meow_gfx_fill_round(x, y, w, h, MENU_GRID_RADIUS, th->surface_container); if (sel || pressed) { int alpha = pressed ? 72 : 40; /* MD3 状态层:按压更浓 */ meow_gfx_fill_round_alpha(x, y, w, h, MENU_GRID_RADIUS, th->primary, alpha); } int sz = (h > MENU_GRID_ICON + 40) ? MENU_GRID_ICON : h - 40; int icx = x + (w - sz) / 2, icy = y + 12; menu_draw_icon(a, icon, icx, icy, sz, th); char nm[40]; menu_name_fit(a->name, w - 14, 18, nm, sizeof nm); int name_y = y + 12 + sz + 10; int nw = ui_width(nm, 18); ui_draw(x + (w - nw) / 2, name_y, nm, a->ok ? th->on_surface : th->error, 18); if (!a->ok) { int ew = ui_width("[缺失]", 14); ui_draw(x + (w - ew) / 2, name_y + meow_font_line_height(18) + 2, "[缺失]", th->error, 14); } else if (meow_bg_is_backgrounded(a->id)) { /* M4:后台保活中标注(播放中转后台的 App;run_menu 1s tick 按 change_seq 刷新) */ int bw = ui_width("·后台", 14); ui_draw(x + (w - bw) / 2, name_y + meow_font_line_height(18) + 2, "·后台", th->on_surface_variant, 14); } } /* 列表行:图标 + 名称 + 副标题(package/id)+ 选中/按压 tonal 高亮。 */ static void menu_draw_list_cell(const struct app *a, meow_img *icon, int x, int y, int w, int h, int sel, int pressed, const meow_ui_theme_t *th) { meow_gfx_fill_round(x, y, w, h, 12, th->surface_container); if (sel || pressed) { int alpha = pressed ? 72 : 40; meow_gfx_fill_round_alpha(x, y, w, h, 12, th->primary, alpha); } int sz = MENU_LIST_ICON; int icy = y + (h - sz) / 2; menu_draw_icon(a, icon, x + MENU_LIST_ICON_X, icy, sz, th); int tx = x + MENU_LIST_ICON_X + sz + 14; char nm[40]; menu_name_fit(a->name, w - (tx - x) - 12, 20, nm, sizeof nm); int name_y, sub_y; if (mfont_ready) { int lh20 = meow_font_line_height(20), lh14 = meow_font_line_height(14); name_y = y + (h - (lh20 + 4 + lh14)) / 2; sub_y = name_y + lh20 + 4; } else { name_y = y + 8; sub_y = y + 30; } ui_draw(tx, name_y, nm, a->ok ? th->on_surface : th->error, 20); /* 副标题:package(无则 id),on_surface_variant;M4 后台保活中前缀「·后台 」 */ char sub[48]; snprintf(sub, sizeof sub, "%s%s", meow_bg_is_backgrounded(a->id) ? "·后台 " : "", a->package[0] ? a->package : (a->id[0] ? a->id : "")); if (sub[0]) { char subf[44]; menu_name_fit(sub, w - (tx - x) - 12, 14, subf, sizeof subf); ui_draw(tx, sub_y, subf, th->on_surface_variant, 14); } } /* 应用格绘制入口:按布局分派。 */ static void menu_draw_app_cell(const struct app *a, meow_img *icon, int x, int y, int w, int h, int sel, int pressed, int layout, const meow_ui_theme_t *th) { if (layout == 0) menu_draw_grid_cell(a, icon, x, y, w, h, sel, pressed, th); else menu_draw_list_cell(a, icon, x, y, w, h, sel, pressed, th); } static void draw_menu(struct app apps[MAX_APPS], int napps, int state, int cursor, int toast_ms, const char *toast) { const meow_ui_theme_t *th = s_ui_theme(); int layout = menu_layout(); int apps_top = menu_apps_top(); int bsize = 20; meow_settings_get_int("font.size.body", &bsize); if (bsize < 12) bsize = 12; if (bsize > 28) bsize = 28; /* 背景(纯色 or 图片 cover+overlay;缓存变化才重建,不每帧解码) */ menu_bg_prepare(th); menu_bg_paint(); draw_header_content(); if (state == 1) { if (mfont_ready) { ui_draw(30, 200, "没有存储卡", th->on_surface, bsize); ui_draw(30, 234, "请插入TF卡后重开菜单", th->on_surface_variant, bsize); } else { draw_text(30, 200, "NO STORAGE CARD", 2, th->on_surface); draw_text(30, 240, "INSERT TF CARD", 2, th->on_surface_variant); draw_text(30, 272, "THEN RE-OPEN MENU", 2, th->on_surface_variant); } } else if (state == 2 || napps == 0) { if (mfont_ready) { ui_draw(30, 200, "没有找到应用", th->on_surface, bsize); ui_draw(30, 234, "请放入应用到", th->on_surface_variant, bsize); ui_draw(30, 268, "MeowAPPs//app.json", th->on_surface_variant, bsize); } else { draw_text(30, 200, "NO APPS FOUND", 2, th->on_surface); draw_text(30, 240, "PUT APP IN", 2, th->on_surface_variant); draw_text(30, 272, "MeowAPPs//app.json", 2, th->on_surface_variant); } } else { for (int i = 0; i < napps; i++) { int cx, cy, cw, ch; menu_cell_rect(i, layout, apps_top, &cx, &cy, &cw, &ch); menu_draw_app_cell(&apps[i], (i < MAX_APPS) ? s_app_icons[i] : NULL, cx, cy, cw, ch, i == cursor, g_menu_pressed == i, layout, th); } } /* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前; * 音量导航可把光标移到此处(cursor==napps),触摸点击该行进入设置页。 */ { int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */ int selset = (cursor == napps); /* 光标已移到设置行 */ meow_gfx_fill_round(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container); if (selset) meow_gfx_fill_round_alpha(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->primary, 40); if (mfont_ready) ui_draw(24, sy, "系统设置", th->on_surface, bsize); else draw_text(24, sy, "SETTINGS", 2, th->on_surface); s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, th->on_surface_variant); } /* exit row */ meow_gfx_fill_round(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container); /* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */ if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", th->on_surface, bsize); else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, th->on_surface); /* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。字号 20 真实行高 meow_font_line_height(20)。 * 非 mfont 回退分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */ #define FOOTY_PAD 8 { char ver[32]; snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); int foot_y = fb_yres - meow_font_line_height(bsize) - FOOTY_PAD; ui_draw(fb_xres - 8 - ui_width(ver, bsize), foot_y, ver, th->on_surface_variant, bsize); } if (toast_ms > 0 && toast && toast[0]) { fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, th->surface_variant); ui_draw(30, TOAST_Y, toast, th->error, bsize); } } /* ================================================================== */ /* M2.3A 电源菜单(Android 11 样式):长按电源 3s 弹出;8s 强制关机 */ /* 半透明深色遮罩 + 居中 5 个大按钮(2 列×3 行,ASCII 点阵,不耦合字体) */ /* 与「单击=息屏」「电源+音量减=切MeowOS」按电源按住时长区分。 */ /* ================================================================== */ /* M2.3A sim 钩子(电源菜单验证,物理留用户): * --sim-power-hold N —— 主循环 N ms 后强制 pwr=1(模拟按住电源),驱动 * 3s 弹电源菜单 / 8s 强制关机两条路径。 * --sim-power-pick N —— 电源菜单打开 2s 后自动选中按钮 N(0..4;M2.29 起 5 钮)。 * --sim-no-exec —— 免执行:电源动作(关机/重启/回HibyOS)/8s强关仅打 log, * 不真正 poweroff/reboot(安全 sim 用)。 */ static long sim_power_hold_ms = 0; int sim_power_pick = -1; int sim_no_exec = 0; void fill_circle(int cx, int cy, int r, uint32_t color) { for (int dy = -r; dy <= r; dy++) for (int dx = -r; dx <= r; dx++) if (dx * dx + dy * dy <= r * r) put_px(cx + dx, cy + dy, color); } /* 圆角矩形 */ void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color) { fill_rect(x0 + r, y0, w - 2 * r, h, color); fill_rect(x0, y0 + r, w, h - 2 * r, color); fill_circle(x0 + r, y0 + r, r, color); fill_circle(x0 + w - r, y0 + r, r, color); fill_circle(x0 + r, y0 + h - r, r, color); fill_circle(x0 + w - r, y0 + h - r, r, color); } /* returns: 0 = stay in menu, 1 = exit menu (BACK TO PLAYER / sim-exit), * 2 = launch request */ /* M2.8 Bug 3-residual「统一重绘」:把当前 MeowOS 界面内容整体重绘到 buffer0 并强制 pan 到 * buffer0。息屏唤醒(电源单击亮屏)时调用 —— 唤醒不是 draw 事件:FBIOBLANK(4) 下电 -> * FBIOBLANK(0) 上电后,LCDC 指向的内容可能已失效/为空,必须把当前真实界面(应用列表主界面, * 含「没有存储卡/没有找到应用」态)重新画一遍再强制 re-pan,而不是仅翻页到陈旧/空缓冲。 * signature 传入当前界面状态(由 run_menu 持有),不把应用列表局部状态全局化。 */ static void meow_repaint_surface(struct app apps[MAX_APPS], int napps, int state, int cursor) { /* M2.11 E:记录唤醒上下文(时刻/亮度/当前 screen_on/待 yoffset),便于定位偶发「唤醒后黑屏 * 又自愈」的竞态。 */ int pre_bri = read_brightness(); int pre_screen = meow_screen_on; logmsg("wake repaint: pre bri=%d screen_on=%d (state=%d napps=%d cursor=%d)", pre_bri, pre_screen, state, napps, cursor); draw_menu(apps, napps, state, cursor, 0, ""); /* 完整重绘当前视图到 back_buf */ fb_pan_buf0_force(); /* 确保落在被显示的缓冲(buffer0) */ meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit) */ int post_bri = read_brightness(); logmsg("wake repaint done: post bri=%d (after force-pan buffer0)", post_bri); } /* set by on_sigterm; run_menu breaks on it so the menu closes cleanly and * the main loop restores the player (must be declared before run_menu) */ volatile sig_atomic_t sigterm_flag = 0; /* M2.2 sim 钩子:--sim-exit-tap N —— 菜单打开 N ms 后模拟「点 BACK TO PLAYER 行」, * 用于验证 MeowOS 模式下 BACK TO PLAYER 显示需重启提示(而非恢复死帧)。 */ static long sim_exit_tap_ms = 0; static int sim_exit_tap_done = 0; /* real menu loop */ /* M2.77 应用列表热重载:MeowAPPs 目录签名(自身 + 各应用目录 mtime 的 FNV 混合), * run_menu 2s 轮询比对,变化即重扫——覆盖 SSH/面板推新包、删包、应用目录内容变动等场景 * (此前仅「无卡→有卡」过渡才重扫,常驻主界面下推包不刷新)。 */ static unsigned long long apps_dir_sig(void) { unsigned long long sig = 1469598103934665603ULL; /* FNV-1a offset */ struct stat st; if (stat(MEOWAPPS, &st) != 0) return 0; sig ^= (unsigned long long)st.st_mtime; sig *= 1099511628211ULL; DIR *d = opendir(MEOWAPPS); if (d) { struct dirent *de; while ((de = readdir(d))) { if (de->d_name[0] == '.') continue; char p[300]; snprintf(p, sizeof p, "%s/%s", MEOWAPPS, de->d_name); if (stat(p, &st) == 0 && S_ISDIR(st.st_mode)) { sig ^= (unsigned long long)st.st_mtime * 256 + (unsigned char)de->d_name[0]; sig *= 1099511628211ULL; } } closedir(d); } return sig; } /* M2.83 修补:字库目录签名(根+各 family 目录 mtime)。meow_font_scan_presets 内含逐套 * sha256 校验(gbk 套 6.8MB 级),绝不能每 2s 轮询全量扫——只在目录签名变化时扫。 */ static unsigned long long fonts_dir_sig(void) { char root[300]; snprintf(root, sizeof root, "/usr/data/mnt/sd_0/%s", MEOW_FONTS_CFG); unsigned long long sig = 1469598103934665603ULL; struct stat st; if (stat(root, &st) != 0) return 0; sig ^= (unsigned long long)st.st_mtime; sig *= 1099511628211ULL; DIR *d = opendir(root); if (d) { struct dirent *de; while ((de = readdir(d))) { if (de->d_name[0] == '.') continue; char p[400]; snprintf(p, sizeof p, "%s/%s", root, de->d_name); if (stat(p, &st) == 0 && S_ISDIR(st.st_mode)) { sig ^= (unsigned long long)st.st_mtime * 256 + (unsigned char)de->d_name[0]; sig *= 1099511628211ULL; } } closedir(d); } return sig; } static void run_menu(struct app apps[MAX_APPS], int napps, int state, long sim_tap_ms, long sim_exit_ms) { int cursor = 0; unsigned long long t0 = now_ms(); int toast_ms = 0; char toast[64] = ""; int want_exit = 0; /* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。 * meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */ unsigned long long last_input_ms = now_ms(); int any_input = 0; int fbfd = -1; int had_player = 0; /* M2.2:菜单打开时是否存在存活播放器 */ long sim_tap_done = 0, sim_exit_done = 0; long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */ unsigned long long apps_sig = 0; /* M2.77:应用目录签名(0=未初始化,首圈建立基线) */ unsigned long long fonts_sig = 0; /* M2.83:字库目录签名(门控 scan_presets,防 2s 轮询 sha256 风暴) */ load_config(); /* meow.conf on the card: bg_color etc. */ menu_icons_load(apps, napps); /* MD3:应用图标解码一次进缓存(不每帧解码) */ /* A-1/A-2:launcher 用 SDK 渲染栈 —— 初始化 SDK(自建 back_buf/读几何/载字库), * 并把刷新率档位/恒满速开关交给 SDK present 帧率门控(去中心化:launcher 不再自带一套)。 * set_statusbar(1)(Stage-One 状态栏收敛):launcher 系统界面(主界面/设置页/电源菜单/ * 通知中心)顶部状态栏统一由 SDK 全局状态栏绘制(MUI_SBAR_H=28),launcher 不再自绘 * (draw_statusbar/sb_redraw 退役,改由 sb_sdk_refresh() 喂权威状态 + 标脏顶条驱动)。 * 与 App 同一实现/同一主题/同日/夜。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ meow_ui_init(NULL); meow_ui_set_statusbar(1); /* 「同一个状态栏」闭环(FRAMEWORK_DESIGN §10.2 / MEOW_APP_SDK §3.1):SDK 全局状态栏 * 仅系统进程(launcher)渲染。launcher 是系统 UI 渲染宿主 → 置 system_mode=1 让 SDK 渲染顶条 * /提示线;App 进程默认 0(不渲染、只 inset,由 launcher 叠加)。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ meow_ui_sys_set_system_mode(1); meow_ui_set_refresh_rate(refresh_rate_cfg); meow_ui_set_refresh_always_max(refresh_always_max); /* record the brightness BEFORE we touch it, so any menu exit can * restore the player's pre-menu value (0 = player standby -> default 60) */ int raw_brightness = read_brightness(); saved_brightness = (raw_brightness > 0) ? raw_brightness : 60; logmsg("menu saved brightness = %d", saved_brightness); /* remember whether the player had blanked the panel: fb_open() below * unblanks so the menu is visible, and restore_backlight() re-blanks * on exit to keep the player's display state machine consistent */ menu_opened_blank = (raw_brightness <= 0); fbfd = fb_open(); meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */ /* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */ meow_font_ensure(); meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */ /* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份 * 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才 * 冻结 + 备份,退出时恢复其帧。 */ had_player = (find_hiby_pid() > 0); meow_mode = had_player ? 0 : 1; /* 权威判定:每次按播放器是否存活重置 */ /* M2.11 A:每次进 MeowOS(热键切换/开机选 MeowOS/覆盖菜单)都全量重抓系统状态快照, * HibyOS 里改过(蓝牙/WiFi/音量/亮度等)也同步。主循环 10ms 绝不读系统态。 */ meow_sys_refresh(); g_sys.mode = meow_mode; g_sys.napps = napps; if (had_player) { stop_player(); /* freeze the player so the menu owns fb0 */ } else { logmsg("meow mode: hiby_player dead, self-drawn home (no stop/backup)"); } ensure_backlight(); /* only if the panel is dark, never yank brightness */ unblank_panel(); /* player frozen: it can no longer re-blank */ /* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户 * display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限 * (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。 * 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */ if (!had_player) apply_user_brightness(); if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */ draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */ fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */ meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit 到 fb0) */ /* touch: event reads (event1 is not grabbed) */ touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno)); /* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */ menu_arm_touch(TAP_ARM_DELAY_MS); /* key polling */ struct keypoll kp_power, kp_keys; kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); /* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */ 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); logmsg("menu open: napps=%d state=%d cursor=%d", napps, state, cursor); /* key edge baselines: sample the initial state so keys already held * when the menu opens (e.g. the hotkey release tail) do not fire * edges; kp.prev cannot be used for this because kp_poll() syncs it * to cur before we compare */ 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; } /* M2.3A 电源按住计时:≥8s 强制关机;≥3s 弹电源菜单;与单击息屏/热键按时长区分 */ unsigned long long pwr_hold_at = 0; int pwr_menu_state = 0; /* M2.18 Bug 2: 一次性标志——本次电源按下已触发「自动息屏唤醒」时置位,其释放 * 只唤醒不 toggle(防同一次按键被唤醒路径与单单击 toggle 各消费一次 → 闪黑)。 */ int pwr_woke = 0; /* M2.34: 一次性标志——电源菜单刚关闭(无论确认/取消/超时)。返回主循环后,本次 * 电源按下-松开不得被 3130「松开→单击」路径误判为「电源单击」→ toggle 息屏,否则: * ①菜单点关机/重启:关机/重启画面被息屏覆盖;②焦点在取消键单击电源:取消同时熄屏。 * 清除时机:电源松开(3130 松开分支末尾)或下一次电源按下边沿。 */ int pwr_suppress_click = 0; /* The menu was already drawn into buffer0 above (pre-drawn before the pan * to buffer0), so a redraw is only needed when something actually changes: * cursor movement (input), app exit/enter, toast start/end, or a future * notification-badge change. NEVER on a periodic timer — see the big * comment on the redraw block below. */ int dirty = 0; /* 结构变化(app 进出/state/唤醒/热键…)→ 全帧重画 */ int need_menu_paint = 0; /* M2.16:需 draw_menu 全画 back_buf(光标/toast/test 变化) */ int last_cursor = 0; /* pre-drawn frame showed cursor 0 */ int toast_active = 0; /* tracks toast_ms>0 for start/end redraw edges */ long last_status_tick = 0; /* ~1s status-bar-only refresh (clock) */ for (;;) { unsigned long long t = now_ms(); long el = (long)(t - t0); any_input = 0; /* M2.17:本圈是否有输入(重置 auto screen-off 计时) */ /* SIGTERM during the menu: close it cleanly (restore + resume happen * in the hotkey block / main loop after we return) */ if (sigterm_flag) { logmsg("menu: sigterm, closing"); want_exit = 1; break; } /* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */ meow_ipc_process(); /* AMSD_DESIGN §4.3:amsd `am start` → meow_ams_start_consume 消费 pending-start, * 复用既有启动链路(launch_app/launch_activity)真正拉起目标 App。 * 镜像 sim-tap 启动路径:launch 后 menu_after_app_exit + dirty 重绘菜单。 */ { const char *ids[MAX_APPS]; for (int i = 0; i < napps; i++) ids[i] = apps[i].id; meow_ams_ctx_set(apps, napps); /* ops.launch 的 idx→struct app 映射 */ int app_ran = 0, idx = -1; int r = meow_ams_start_consume(ids, napps, state, toast, sizeof toast, &app_ran, &idx); if (r == 1) { if (!app_ran && idx >= 0 && idx < napps) { /* 启动失败:复用 run_menu 的失败 toast(缺依赖/exec 失败,含 app 名)。 */ launch_fail_toast(toast, sizeof toast, apps[idx].name); } toast_ms = TOAST_MS; menu_after_app_exit(app_ran); last_input_ms = now_ms(); /* M2.17:回主界面即视为输入 */ dirty = 1; } else if (r == 2) { /* 应用列表未就绪:pending 保留(下次重扫即拉起),仅提示,不红冲免忙循环。 */ toast_ms = TOAST_MS; } } meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ meow_media_tick(); /* 阶段D附录B:媒体源死亡(播放中无 META 超时)清槽回空态 */ /* sim hooks */ if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) { logmsg("SIM TAP row 0"); sim_tap_done = 1; if (napps > 0 && state == 0) { /* fall through: select row 0 */ int ret = launch_app(&apps[0]); /* app exited: RETURN TO THE MENU (v6.3) — closing the menu * here would leave the app's last frame (e.g. BYE) on fb0, * and the player only redraws on input events (none arrive, * injection is not delivered), so the screen would stay * dark until a key press. Staying in the menu keeps the * menu visible; BACK TO PLAYER then resumes the player. * Bug 2/3/4: a queued touch from the app run must NOT be * replayed as a fresh menu tap (relaunch / flash-to-player), * and the "CANNOT START" toast only fires on a real launch * failure (ret != 0), never after a clean app exit. */ menu_after_app_exit(ret == 0); last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ dirty = 1; /* app wrote buffer0 during its run -> redraw menu */ if (ret != 0) { launch_fail_toast(toast, sizeof toast, apps[0].name); toast_ms = TOAST_MS; } } else { snprintf(toast, sizeof toast, "NO APPS"); toast_ms = TOAST_MS; } } if (sim_exit_ms > 0 && !sim_exit_done && el >= sim_exit_ms) { logmsg("SIM EXIT"); sim_exit_done = 1; want_exit = 1; break; } /* M2.6 sim:点 BACK TO PLAYER 行 → MeowOS 模式弹「需重启」确认框并留下; * 会话覆盖模式则正常退出。 */ if (sim_exit_tap_ms > 0 && !sim_exit_tap_done && el >= sim_exit_tap_ms) { logmsg("SIM TAP exit row"); sim_exit_tap_done = 1; if (meow_mode) { int rb = confirm_reboot_player(); menu_arm_touch(TAP_ARM_DELAY_MS); dirty = 1; logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb); } else { want_exit = 1; break; } } /* M2.2 MeowOS 系统拔营:无播放器时由 launcher 接管官方交给 * hiby_player/sys_server 的两件事 —— TF 卡挂载 + WiFi 联网。 * 后台每 2s 重试,不阻塞主界面;卡片就绪后重扫应用列表并就地刷新。 * 仅 meow 常驻模式需要(HibyOS 覆盖模式播放器仍在,/或其管理)。 * M2.11 C:TF 卡热插拔最佳努力 —— 拔出检测(节点消失→无卡)、 * 插入检测(节点出现→自动挂载+重扫+重载字库)。不阻塞、不崩。 */ if (meow_mode && (long)(t - t0) - meow_last_poll >= 2000) { meow_last_poll = (long)(t - t0); ensure_wifi(); /* M2.86 开机亮度竞态:launcher 被 sysui 在 S70 早拉起,/usr/data(UBIFS) 未就绪 → * 首次 settings_load 读到纯默认值(display.brightness=101→clamp 100),apply_user_brightness * 已挂起 meow_brightness_pending(不盲目写 100)。每 2s 重试 settings_load 直到读到 * settings.conf;settings_load 内置在「未读到→已读到」转换时补应用真实用户亮度。 */ if (meow_brightness_pending && !meow_settings_loaded()) settings_load(); if (!sd_device_present()) { /* 卡被拔出:设备节点消失 → 无卡态。停止扫描;留 state=1(NO STORAGE CARD)。 * 若此前是正常态,切到无卡并重绘。不杀正在跑的应用(其由 D 的监测处理)。 */ if (state == 0 || state == 2) { state = 1; dirty = 1; logmsg("hotplug: card removed -> state=1"); } g_sys.sd_mounted = 0; log_poll_card(); /* 卡日志不可用时回退 /tmp 缓冲 */ } else if (ensure_card_mounted()) { /* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */ int was_nocard = (state == 1); if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); } if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */ /* M2.83 修补:字库重扫改为签名门控——扫描含逐套 sha256(gbk 套 6.8MB 级), * 每 2s 全量扫会把 CPU 烧满(真机实测 100%)。只在字体目录签名变化时扫。 */ { unsigned long long fsig = fonts_dir_sig(); if (fsig != fonts_sig) { fonts_sig = fsig; meow_font_scan_presets(); } } if (was_nocard || state != 0 || napps == 0) { struct app fresh[MAX_APPS]; int fn = scan_apps(fresh); int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0); if (fstate == 0 && fn > 0) { for (int i = 0; i < fn; i++) apps[i] = fresh[i]; napps = fn; state = 0; menu_icons_load(apps, napps); /* MD3:图标缓存随重扫刷新 */ logmsg("hotplug: card ready -> %d apps loaded", napps); dirty = 1; } else if (fstate != state) { state = fstate; logmsg("hotplug: state -> %d", state); dirty = 1; } } g_sys.sd_mounted = 1; log_poll_card(); /* M2.77 应用列表热重载:目录签名变化(推新包/删包/应用目录变动)→ 重扫刷新。 * 与上面「无卡→有卡」过渡重扫互补:常驻主界面下目录内容变化也覆盖。 */ { unsigned long long asig = apps_dir_sig(); if (apps_sig && asig && asig != apps_sig) { struct app fresh[MAX_APPS]; int fn = scan_apps(fresh); int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0); if (fstate == 0 && fn > 0) { memcpy(apps, fresh, sizeof fresh); napps = fn; state = 0; if (cursor >= napps) cursor = napps - 1; menu_icons_load(apps, napps); /* MD3:图标缓存随热重载刷新 */ dirty = 1; logmsg("hot-reload: MeowAPPs changed -> %d apps", napps); } else if (fstate != state) { state = fstate; napps = (fn > 0) ? fn : 0; dirty = 1; logmsg("hot-reload: state -> %d", state); } } apps_sig = asig; } } else if (!sd_mounted()) { /* 卡在但未挂载(正挂载/失败):保持当前显示不误判;不崩。 */ g_sys.sd_mounted = 0; } } /* keys: poll and handle edges. Power is NOT handled here anymore * (v6.5): inside the menu the power key is a pure screen-off/on * toggle handled by sys_server/kernel (the player being SIGSTOPped * does not affect it) — the launcher must NOT touch brightness/panel * or exit the menu on it. Exit is only via the BACK TO PLAYER touch * row (below) or sim-exit. */ 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; } if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */ /* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */ int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */ (void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */ /* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。 * 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */ (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; /* M2.6 电源语义(MeowOS 自持面板,无播放器拔河): * 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */ if (meow_mode) { if (pwr && !m_prev_pwr) { pwr_hold_at = t; pwr_menu_state = 0; any_input = 1; pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ /* M2.18 Bug 2: 若这次按下正好落在「自动息屏」态(auto_slept 且屏已关), * 本次电源键只做唤醒(下方通用醒屏分支 meow_panel_on()),其释放不再走 * 单单击 toggle —— 否则同一次按下先被唤醒点亮、又在释放时被 toggle 关掉 * (症状:自动息屏后点电源「闪一下黑、再点才亮」)。 */ if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; } if (pwr && pwr_hold_at) { /* M2.10/M2.11 B:8s 强关只在该「按住」从未打开过电源菜单时才触发 * (!pwr_menu_state)。一旦打开过菜单(用户已互动/取消),run_menu 的 * 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消 * 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。 * M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */ if (t - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) { if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); } m_prev_pwr = pwr; continue; } else if (t - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) { pwr_menu_state = 1; logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */ /* M2.31:菜单打开期间主循环阻塞,last_input_ms 不会刷新;若停留超过自动息屏 * 超时,取消返回后会被误判「空闲」而立即熄屏(用户点取消瞬间黑屏)。 * 菜单返回即视为「刚有输入」,重置自动息屏计时。 */ last_input_ms = now_ms(); /* M2.11 B:菜单已关闭(取消)。清空按住计时与菜单态:仍按住/随后松开的电源 * 按「新按下」处理,绝不把已互动的 hold 再累计到 8s 强关。 */ pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */ dirty = 1; /* 取消后重绘应用列表 */ menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */ /* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。 * 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」, * 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at, * 随后稳定值转 0 时又被误判「单击」→ meow_toggle_panel 把面板关掉(黑屏且 * 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw * 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw * 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */ 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); continue; } } if (!pwr && m_prev_pwr) { /* 松开:短按(<3s 且菜单未开)→ 单击 = 息屏/亮屏切换 */ if (pwr_suppress_click) { /* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏 * (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */ logmsg("pm: power release after power-menu -> no toggle (suppress)"); } else if (pwr_hold_at && (t - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); meow_toggle_panel(); if (meow_screen_on) { /* M2.8 Bug 3-residual:唤醒不是 draw 事件,LCDC 在 FBIOBLANK(4)->(0) * 后指向的内容可能已失效/为空;必须把当前界面(应用列表)整体重绘到 * buffer0 并强制 re-pan,而非仅翻页/仅 dirty 重绘(M2.7 仍黑屏根因)。 * M2.x 锁屏:唤醒后若需锁屏(system.pin_lock_on_wake=1 且已配 PIN)→ * 先弹阻塞式锁屏页;解锁才重绘主屏,息屏则留 dirty 下次再锁。 */ meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */ if (lck_wake_should_lock()) { int lr = run_lock_screen(); /* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */ 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); if (lr == 1) meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */ else dirty = 1; /* 锁屏期间电源息屏 → 屏已关,留 dirty 下次亮屏再锁 */ } else { meow_repaint_surface(apps, napps, state, cursor); } } else { dirty = 1; /* 息屏:面板已黑,留 dirty 供下次亮屏重绘 */ last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟 */ } } else if (pwr_woke) { /* M2.18 Bug 2:本次电源按下已由自动息屏唤醒分支消费,释放只记录、不 toggle。 */ logmsg("pm: power release after auto screen-off wake -> no toggle (wake only)"); } pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ } m_prev_pwr = pwr; } /* volume/play edges against the launcher-maintained baselines * (previously dead code: prev was re-seeded from cur each pass, so * no edge could ever fire and the menu keys could not navigate or * launch). * M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp), * 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */ if (meow_mode) { if (vold && !m_prev_vold) { any_input = 1; settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */ logmsg("volume DOWN <- ramp step=%d", settings_volume_step); } else if (volu && !m_prev_volu) { any_input = 1; settings_volume_adj(+settings_volume_step); logmsg("volume UP <- ramp step=%d", settings_volume_step); } } else { /* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。 * 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */ if (vold && !m_prev_vold && napps > 0 && state == 0) { any_input = 1; cursor++; if (cursor > napps) cursor = 0; /* napps=设置行 */ logmsg("menu cursor -> %d", cursor); } if (volu && !m_prev_volu && napps > 0 && state == 0) { any_input = 1; cursor--; if (cursor < 0) cursor = napps; /* 回绕到设置行 */ logmsg("menu cursor -> %d", cursor); } } /* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */ if (meow_mode && vol_ramp_dir != 0) { int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */ int going = vol_ramp_tick(held); g_sys.vol = settings_volume_get(); paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */ if (!going) logmsg("volume ramp end -> %d", g_sys.vol); } /* M2.19:播放键不再是「应用列表的确认/打开」——它让位给全局媒体控制 * (播放/暂停,路由到当前活跃媒体会话)。菜单/列表的确认只走触摸点选 * (见下方 touch 段),物理确认交给电源键(power 菜单/确认框),播放键 * 在 MeowOS 主循环里不再触发 launch_app。媒体预留:后续媒体会话接管, * 此处仅作 any_input(any_input 仍用于重置自动息屏计时)。(原播放键=打开 * 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */ if (play && !m_prev_play) { any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */ /* 阶段D附录B:媒体键路由到最近活跃媒体会话(播放/暂停切换)。无源=打点。 */ const meow_media_session_t *ms = meow_media_current(); if (ms) { uint32_t cmd = (ms->state == MEOW_MEDIA_PLAYING) ? 2u /* 暂停 */ : 1u /* 播放 */; int rc = meow_media_playctl(cmd, 0); logmsg("play key: media control -> cmd=%u rc=%d (%s)", cmd, rc, ms->app_id); } else { logmsg("play key: media-reserved (no media source)"); } } m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; /* touch (event reads) */ if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */ struct pollfd pfd = { touch_fd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == 16) { uint16_t type = *(uint16_t *)(ev + 8); uint16_t code = *(uint16_t *)(ev + 10); int32_t val = *(int32_t *)(ev + 12); if (type == EV_ABS) { if (code == ABS_X || code == ABS_MT_POSITION_X) t_last_x = val; else if (code == ABS_Y || code == ABS_MT_POSITION_Y) t_last_y = val; /* M2.23 原点锁定:goodix 事件时序不保证 ABS 与 BTN_TOUCH 先后, * 按下边沿只标 t_down,原点延迟到「本次按下之后」的首个有效坐标才锁定, * 杜绝把上一手势残留(-1/旧坐标)当本次手势起点。 */ if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) { t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1; /* MD3 按压态:锁定原点后立即判定命中的应用格(按下即显示高亮) */ g_menu_pressed = menu_hit_app(t_last_x, t_last_y, apps, napps, state, menu_layout(), menu_apps_top()); if (g_menu_pressed >= 0) { int cx, cy, cw, ch; menu_cell_rect(g_menu_pressed, menu_layout(), menu_apps_top(), &cx, &cy, &cw, &ch); meow_dirty_add(cx, cy, cw, ch); need_menu_paint = 1; } } /* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手): * 原点锁定 + 起点在顶部热区(y= 0 && t_down_y < SB_BAR_H && (t_last_y - t_down_y) >= NCP_DRAG_ENTER_SLOP) { menu_live_open_pending = 1; /* 本次手势仅触发一次 */ int loy = t_down_y; logmsg("menu: top-edge live drag-down -> notification center (dy=%d)", t_last_y - loy); t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */ menu_arm_touch(TAP_ARM_DELAY_MS); notif_click_t click; memset(&click, 0, sizeof click); click.app_idx = -1; click.status = NOTIF_CLICK_STATUS_NONE; run_notif_center(apps, napps, &click, 1, t_down_x, loy); /* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */ 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; /* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */ if (click.status == NOTIF_CLICK_STATUS_OK && click.app_idx >= 0 && click.app_idx < napps) { logmsg("menu: notif click -> launch idx=%d chain=\"%s\"", click.app_idx, click.chain); int ret = click.chain[0] ? launch_activity(&apps[click.app_idx], click.chain) : launch_app(&apps[click.app_idx]); menu_after_app_exit(ret == 0); last_input_ms = now_ms(); dirty = 1; if (ret != 0) { launch_fail_toast(toast, sizeof toast, apps[click.app_idx].name); toast_ms = TOAST_MS; } } else if (click.status == NOTIF_CLICK_STATUS_NOROUTE) { logmsg("menu: notif click NO ROUTE"); snprintf(toast, sizeof toast, "NO ROUTE"); toast_ms = TOAST_MS; dirty = 1; } else if (click.status == NOTIF_CLICK_STATUS_NOSRC) { logmsg("menu: notif click SOURCE NOT FOUND"); snprintf(toast, sizeof toast, "SOURCE NOT FOUND"); toast_ms = TOAST_MS; dirty = 1; } continue; /* 本圈不再按 tap/其余处理(剩余事件因 t_down=0 成 no-op) */ } } else if (type == EV_KEY && code == BTN_TOUCH) { if (val && !t_down) { /* Bug 2/4: ignore touches inside the post-app * cooldown (goodix ghost + back-tap replay). */ if (now_ms() < menu_touch_cooldown_until) { /* M2.18 排查乱跳:记录被冷却丢弃的按下(坐标 + 剩余冷却), * 便于定位「概率进 Hello」是否 ghost/重放越过冷却。 */ logmsg("menu touch dropped by cooldown (at %d,%d, rem %llu ms)", t_last_x, t_last_y, menu_touch_cooldown_until - now_ms()); } else { any_input = 1; /* M2.17:触摸重置自动息屏计时 */ t_down = 1; t_down_valid = 0; /* 原点待首个有效坐标锁定(防残留 -1/旧值) */ t_last_x = -1; t_last_y = -1; /* 本次以「无坐标」起步(防跨手势残留) */ menu_live_open_pending = 0; /* ⑨ 新触摸:复位 live-open latch */ } } else if (!val && t_down) { t_down = 0; if (!t_down_valid) { /* 本次按下从未锁到有效坐标(坐标滞后/ghost 空按下):丢本次。 */ logmsg("menu tap dropped: no valid origin (last %d,%d)", t_last_x, t_last_y); t_down_x = t_down_y = -1; } else { int tx = t_down_x, ty = t_down_y; int dx = t_last_x - tx, dy = t_last_y - ty; int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; t_down_valid = 0; /* MD3 按压态:抬手清除按压高亮(无论 tap/滑动,先回退到常态) */ if (g_menu_pressed >= 0) { int cx, cy, cw, ch; menu_cell_rect(g_menu_pressed, menu_layout(), menu_apps_top(), &cx, &cy, &cw, &ch); meow_dirty_add(cx, cy, cw, ch); g_menu_pressed = -1; need_menu_paint = 1; } 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 notification center (release edge, fallback)"); menu_arm_touch(TAP_ARM_DELAY_MS); notif_click_t click; memset(&click, 0, sizeof click); click.app_idx = -1; click.status = NOTIF_CLICK_STATUS_NONE; run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */ /* 重播按键防抖基线,防面板内电源/音量边沿被 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; /* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */ if (click.status == NOTIF_CLICK_STATUS_OK && click.app_idx >= 0 && click.app_idx < napps) { logmsg("menu: notif click -> launch idx=%d chain=\"%s\"", click.app_idx, click.chain); int ret = click.chain[0] ? launch_activity(&apps[click.app_idx], click.chain) : launch_app(&apps[click.app_idx]); menu_after_app_exit(ret == 0); last_input_ms = now_ms(); dirty = 1; if (ret != 0) { launch_fail_toast(toast, sizeof toast, apps[click.app_idx].name); toast_ms = TOAST_MS; } } else if (click.status == NOTIF_CLICK_STATUS_NOROUTE) { logmsg("menu: notif click NO ROUTE"); snprintf(toast, sizeof toast, "NO ROUTE"); toast_ms = TOAST_MS; dirty = 1; } else if (click.status == NOTIF_CLICK_STATUS_NOSRC) { logmsg("menu: notif click SOURCE NOT FOUND"); snprintf(toast, sizeof toast, "SOURCE NOT FOUND"); toast_ms = TOAST_MS; dirty = 1; } continue; /* 本圈不再按 tap/其余处理 */ } /* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、 * 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 * 阶段D附录B:主界面「右缘/中部向左横滑」→ 进入 Now Playing 大页。 * 手势分流(MEOW_APP_SDK §5.2):顶部下滑=通知中心(已在上方处理)、 * 左缘右滑=返回(左缘起点不至此),此处只接管「中部/右缘向左」横滑。 */ if (dx <= -MENU_SWIPE_THRESH && adx > ady && tx >= MEOW_EDGE_W && ty >= SB_BAR_H) { logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy); menu_arm_touch(TAP_ARM_DELAY_MS); run_now_playing(); /* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */ 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; continue; } if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) { logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy); } else if (tx >= 0 && ty >= 0) { if (ty >= EXIT_ROW - 4 && ty < EXIT_ROW + ROW_H - 8) { logmsg("menu: touch exit row"); if (meow_mode) { /* M2.6 G:播放器已死,弹出「返回播放器需重启」确认框。 * 是=写选单默认HibyOS + reboot;否=留在 MeowOS。 * 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */ logmsg("meow mode: BACK TO PLAYER -> confirm reboot"); int rb = confirm_reboot_player(); menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ dirty = 1; if (rb) { logmsg("reboot-to-player: yes, rebooting"); } /* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */ } else { want_exit = 1; break; } } /* M2.36 B2b:系统设置行(位于 Exit 行上方,y ∈ [588-4, 588+52-8))。 * 命中 → 进设置页。阻塞式 run_settings_page 返回后,重播 kd_* 并以 * 当前 raw 对齐 m_prev_*(M2.25 黑屏修复同款),杜绝「设置页内按电源 * 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */ else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) { logmsg("menu: touch settings row -> settings page"); menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */ run_settings_page(); 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; /* 全帧重画应用列表 */ } /* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与 * 应用行动作非互斥。应用很多(napps*ROW_H 上探到 EXIT 带)时, * 一个 tap 会同时命中退出行(<~684)与应用行带,触发确认重启又启动 * 应用(或直接 break 走人)→ 概率乱跳。退出行优先,命中即不再 * 落到应用行。普通小列表两区间不重叠,行为不变。 * MD3 双布局:应用格用 menu_hit_app(按 ui.home_layout 几何命中)。 */ else { int idx = menu_hit_app(tx, ty, apps, napps, state, menu_layout(), menu_apps_top()); if (idx >= 0) { logmsg("menu: touch row %d", idx); int ret = launch_app(&apps[idx]); /* app exited: return to menu (v6.3). * menu_after_app_exit() drains queued * app-run touches so they are not * replayed as row/tap gestures. */ menu_after_app_exit(ret == 0); last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ dirty = 1; /* app wrote buffer0 -> redraw menu */ if (ret != 0) { launch_fail_toast(toast, sizeof toast, apps[idx].name); toast_ms = TOAST_MS; } } } } t_down_x = t_down_y = -1; /* 复位,下一手势从零开始 */ } } } } if (want_exit) break; } } /* ---- M2.17 超时自动息屏(主界面;主循环 10ms 低频检查,不阻塞)---- * 任何输入重置 last_input;空闲 > screen_off_timeout(0=常亮)且未在 keep_awake 页 → 息屏。 * 自动息屏后任意输入唤醒(电源单击路径已在 meow_mode 块处理;这里处理其他输入/触摸)。 * black 模式:面板仍上电、触摸未挂起,故在屏下额外探测一次 BTN_TOUCH 按下 → 触发唤醒 * (不处理点击;唤醒后下一圈正常触摸处理)。panel 模式触摸已被 FBIOBLANK(4) 挂起,仅按键唤醒。 */ if (meow_auto_slept && !meow_screen_on && screen_off_mode == 1 && touch_fd >= 0) { struct pollfd spfd = { touch_fd, POLLIN, 0 }; if (poll(&spfd, 1, 0) > 0) { unsigned char ev[16]; while (read(touch_fd, 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) any_input = 1; } } } /* M2.18 Bug 1: 用 now_ms() 而非本圈顶部捕获的 t —— launch_app() 阻塞期间 t 已过期 * (停在 app 启动前小数),若仍写 t 会把 last_input 倒拨到 app 启动时刻;写 now_ms() * 才是「输入发生」的真实墙钟。配合下方 idle_ms_since 的回绕保护,杜绝误判空闲。 */ if (any_input) last_input_ms = now_ms(); if (meow_auto_slept && any_input && !meow_screen_on) { logmsg("auto screen-off: input wake (meow_mode=%d keep_awake=%d)", meow_mode, fg_keep_awake()); meow_panel_on(); meow_auto_slept = 0; last_input_ms = now_ms(); /* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */ if (lck_wake_should_lock()) { int lr = run_lock_screen(); 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); if (lr == 1) meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */ else dirty = 1; /* 锁屏期间电源息屏 → 屏已关,留 dirty 下次亮屏再锁 */ } else { meow_repaint_surface(apps, napps, state, cursor); /* M2.8 唤醒重绘 */ } } if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { /* M2.18 Bug 1: 用 idle_ms_since 取差,杜绝 t= (unsigned long long)screen_off_timeout * 1000) { meow_panel_off(); meow_auto_slept = 1; logmsg("auto screen-off after %ds idle (mode=%s keep_awake=%d, t=%llu last=%llu)", screen_off_timeout, screen_off_mode ? "black" : "panel", fg_keep_awake(), t, last_input_ms); } } /* Redraw only when something actually changed: cursor movement (an * input event), app exit/enter (which rewrote buffer0), toast * start/end, or a future notification-badge change. NEVER on a * periodic timer. * * v6.8 BUG (the ~3fps list flicker): the v6.8 "500ms safety redraw" * rewrote the ENTIRE menu into the live-displayed buffer0 every 500ms * (the v6.8 fix made last_redraw=el so the timer finally fired), plus * draw_statusbar()'s amixer popen on every redraw. An in-place full * rewrite of the buffer the LCDC is scanning produces visible tearing/ * pulsing on the app list, and the slow amixer popen stalls the loop. * Full-redraw in place is only correct right after a REAL change; the * steady-state list must not be touched. */ /* M2.16:只在真变化时标脏(精确到格子),绝无"动一下就全屏重绘"。 * 光标移动 → 旧/新两行;toast 起止 → toast 区;其余结构变化 → dirty=1(全帧)。 * Stage-One 状态栏收敛:音量渐变/时钟只经 paint_sb_right/draw_clock → sb_sdk_refresh() * 把权威状态喂进 SDK 全局状态栏 + 标脏顶条(0,0,xres,28),由 SDK present() 顶条重绘。 */ if (cursor != last_cursor) { /* M2.36 B2b:光标可能在应用格(0..napps-1)或「系统设置」行(napps,位于 Exit 行上方)。 * 应用格用 menu_cell_rect(按 ui.home_layout 几何)标脏;设置行固定 y=EXIT_ROW-ROW_H。 */ if (cursor == napps || last_cursor == napps) meow_dirty_add(8, EXIT_ROW - ROW_H - 4, fb_xres - 16, ROW_H - 8); if (state == 0 && napps > 0) { int layout = menu_layout(), apps_top = menu_apps_top(); if (last_cursor >= 0 && last_cursor < napps) { int cx, cy, cw, ch; menu_cell_rect(last_cursor, layout, apps_top, &cx, &cy, &cw, &ch); meow_dirty_add(cx, cy - 2, cw, ch + 4); } if (cursor >= 0 && cursor < napps) { int cx, cy, cw, ch; menu_cell_rect(cursor, layout, apps_top, &cx, &cy, &cw, &ch); meow_dirty_add(cx, cy - 2, cw, ch + 4); } } last_cursor = cursor; need_menu_paint = 1; } if (toast_active != (toast_ms > 0)) { /* toast start/end */ toast_active = (toast_ms > 0); meow_dirty_add(20, TOAST_Y - 6, fb_xres - 40, 40); need_menu_paint = 1; } if (dirty) { /* 结构变化 → 全帧重画 + 全屏 blit */ meow_dirty_full(); need_menu_paint = 1; dirty = 0; } /* Bug2 残帧防护 (agent-x):App 退出/结构变化触发全帧重画时,用 fb_pan_buf0_force() * 无条件把 LCDC pan 回 buffer0(菜单/所有 app 都画 buffer0 且从不自己 pan)。仅 * fb_norm_buf0() 会在缓存的 fb_active_yoffset==0 时短路——若 App 运行期把物理 * yoffset 留到了 buffer1(如其内部双缓冲翻页),缓存仍为 0 导致真正的 re-pan 从未 * 发出,重画后的菜单落在 buffer0 而 LCDC 仍显示 buffer1 的 App 残帧(=菜单区残帧)。 * 只在真重画(need_menu_paint)时 force-pan,不每圈 ioctl;重复 pan 到同一 yoffset * 是硬件 no-op,无频闪(fb_pan_buf0_force 注释)。 */ if (need_menu_paint) { draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */ fb_pan_buf0_force(); /* 真重画 → 无条件 pan 回 buffer0(防 App 残留 buffer1) */ need_menu_paint = 0; } else { fb_norm_buf0(); /* 无重画 → 短路;yoffset 非 0 才 pan */ } /* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏 * 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。 * 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */ meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */ if (toast_ms > 0) { toast_ms -= POLL_MS; if (toast_ms < 0) toast_ms = 0; } /* Clock refresh ~1s: redraw ONLY the clock digits (and the right info * cell if battery/wifi changed), never the list. */ if (el - last_status_tick >= 1000) { last_status_tick = el; meow_sys_light(); /* 同步电量/WiFi/音量进 g_sys */ draw_clock(); /* Stage-One:喂 SDK 全局状态栏 + 标脏顶条;SDK present() 重绘 */ fb_norm_buf0(); meow_ui_flush(); /* blit 状态栏整条,不动列表 */ /* M4:后台保活表变化(转后台/回前台/回收)→ 重绘列表刷新「·后台」标注 */ { static uint32_t bg_seq_last = 0; uint32_t seq = meow_bgplay_change_seq(); if (seq != bg_seq_last) { bg_seq_last = seq; dirty = 1; } } } usleep(POLL_MS * 1000); } menu_icons_free(); /* MD3:主界面退出释放应用图标缓存 */ if (touch_fd >= 0) close(touch_fd); /* v6.6: restore the player's frame + pan back BEFORE releasing fb (the * player is event-driven and won't redraw on its own — see §18.15). * M2.2:仅当存在存活播放器才恢复其帧;MeowOS 常驻主界面(无播放器)绝不 * 恢复死帧——屏幕保持自绘的应用列表(buffer0),不遗留播放器残帧。 */ if (had_player) fb_restore_player(); if (fbfd >= 0) { if (fb_fd == fbfd) fb_fd = -1; close(fbfd); } logmsg("menu close (exit=%d meow_mode=%d had_player=%d)", want_exit, meow_mode, had_player); } /* ================================================================== */ /* app launch */ /* ================================================================== */ static void on_sigterm(int s) { (void)s; sigterm_flag = 1; } /* Drain pending events from all input devices (non-blocking read to EAGAIN). * Called right before launching an app so residual events (the tap/key that * selected the row, the hotkey combo's release tail) are consumed before the * app opens its own fds. Note: events generated AFTER the drain still reach * the app (multicast); the app therefore also drains itself at startup and * only exits on a fresh press edge (see hello.c). */ void drain_input(void) { for (int i = 0; i < 4; i++) { char dev[32]; snprintf(dev, sizeof dev, "/dev/input/event%d", i); int fd = open(dev, O_RDONLY | O_NONBLOCK); if (fd < 0) continue; unsigned char ev[16]; while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } close(fd); } } /* ================================================================== */ /* main */ /* ================================================================== */ static void usage(void) { fprintf(stderr, "usage: meow_launcherd [--sim-hotkey N] [--sim-tap N] [--sim-exit N]\n" " [--sim-exit-tap N] [--sim-power-hold N]\n" " [--sim-power-pick N] [--sim-no-exec]\n" " [--dumpkeys N] [--scan] [--sysdump]\n" " [--pkg-install ] [--pkg-export ]\n" " [--ipc-only N] [--ipc-sock ] [--appinfo ]\n" " [--fontscan ]\n"); } /* /tmp/meow_boot_mode:开机模式标记(bootmenu 在 S90 选定后写 "meow"/"hiby",tmpfs 开机清空)。 * M2.89 回归修复用,见 main() 内「开机选单期不抢屏」。 */ #define MEOW_BOOT_MODE_PATH "/tmp/meow_boot_mode" /* 拉起独占音频服务(AUDIOD_DESIGN.md §1):launcher 确认 MeowOS 模式后 `meow_svcd start audiod`。 * 幂等(svcd cmd_start 在 pid>0 时 no-op)、best-effort(audiod 未装/失败静默)、 * 不阻塞主循环(double-fork + 仅 waitpid 立即退出的中间子进程,避免僵尸 + 不占用 fb)。 * 双重门控:audiod 自身读 /tmp/meow_boot_mode,非 meow 即 exit(0)——HibyOS 下物理不存在。 */ static void meow_audiod_kick(void) { if (access("/usr/bin/meow_audiod", X_OK) != 0) return; /* 固件未装 audiod → 静默 */ pid_t pid = fork(); if (pid < 0) return; if (pid == 0) { pid_t pid2 = fork(); /* double-fork:隔离执行者,防僵尸 */ if (pid2 != 0) _exit(0); /* 中间子进程立即退出,由父进程 waitpid 回收 */ { /* 执行者:重定向 stdio 到 /dev/null,exec meow_svcd start meow_audiod(M6.1 服务名统一) */ int devnull = open("/dev/null", O_RDWR); if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); if (devnull > 2) close(devnull); } execl("/usr/bin/meow_svcd", "meow_svcd", "start", "meow_audiod", (char *)0); _exit(127); } } waitpid(pid, NULL, 0); /* 等中间子进程退出(即刻返回,不实质阻塞) */ logmsg("audiod: meow_svcd start meow_audiod requested"); } int main(int argc, char **argv) { long sim_hotkey = 0, sim_tap = 0, sim_exit = 0, dumpkeys = 0; int do_scan = 0, do_sysdump = 0, sim_vol = 0; long ipc_only_ms = 0; const char *appinfo_path = NULL; const char *fontscan_root = NULL; /* --fontscan :宿主/qemu 测试字形扫描 */ const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-exit") && i + 1 < argc) sim_exit = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-exit-tap") && i + 1 < argc) sim_exit_tap_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-power-hold") && i + 1 < argc) sim_power_hold_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-power-pick") && i + 1 < argc) sim_power_pick = atoi(argv[++i]); else if (!strcmp(argv[i], "--sim-no-exec")) sim_no_exec = 1; else if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]); else if (!strcmp(argv[i], "--scan")) do_scan = 1; else if (!strcmp(argv[i], "--sysdump")) do_sysdump = 1; else if (!strcmp(argv[i], "--sim-vol") && i + 1 < argc) sim_vol = atoi(argv[++i]); else if (!strcmp(argv[i], "--ipc-only") && i + 1 < argc) ipc_only_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--ipc-sock") && i + 1 < argc) { snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]); } else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i]; else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i]; else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i]; else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; } else { usage(); return 1; } } /* unified log: TF card (or /tmp boot buffer when the card is absent) */ log_cleanup_old(); if (log_open_card() == 0) fprintf(stderr, "log file: %s\n", log_file); else fprintf(stderr, "log: no TF card, using /tmp boot buffer (%s)\n", BOOT_BUF_TMP); logmsg("=== meow_launcherd start (sim_hotkey=%ld sim_tap=%ld sim_exit=%ld dumpkeys=%ld scan=%d) ===", sim_hotkey, sim_tap, sim_exit, dumpkeys, do_scan); load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per * menu open so edits apply without a daemon restart) */ settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */ /* M2.6:启动扫描一次 TF 卡 MeowOS/fonts///,填充「字体文件」下拉 preset。 * 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */ meow_font_scan_presets(); if (fontscan_root) { /* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/// 并打印 preset(qemu/宿主可测)。 * 用于验证 font.json 解析(含 weight)/三件套/缺件跳过/sha 校验 + preset 动态化逻辑。 */ meow_font_scan_presets_root(fontscan_root); printf("fonts=%d\n", font_presets_n); for (int i = 0; i < font_presets_n; i++) printf(" %d %s -> %s (fam=%s wt=%s)\n", i, font_preset_labels[i], font_presets[i], font_preset_fams[i], font_preset_wts[i] ? font_preset_wts[i] : "regular"); return 0; } if (sim_vol > 0) { /* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落, * 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */ settings_volume = 60; /* 固定起点,便于对比 */ settings_volume_step = (sim_vol > 5) ? 5 : (sim_vol < 1 ? 1 : sim_vol); int base = settings_volume; /* UP */ settings_volume_adj(+settings_volume_step); for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } int up = settings_volume; /* DOWN */ settings_volume_adj(-settings_volume_step); for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } int down = settings_volume; printf("sim-vol: step=%d base=%d up=%d down=%d (each 1%% logged 'DAC vol -> L/R=')\n", settings_volume_step, base, up, down); /* 长按:held=1 持续延段爬升 → 连续渐变到上限 clamp */ settings_volume = 60; settings_volume_adj(+settings_volume_step); for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(1); usleep(20000); } printf("sim-vol-held: step=%d long-press top=%d (expect clamp 100)\n", settings_volume_step, settings_volume); return 0; } if (pkg_install_mpk) { /* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */ int r = meow_pkg_install(pkg_install_mpk); printf("pkg_install %s -> %d\n", pkg_install_mpk, r); return r; } if (pkg_export_name && pkg_export_out) { /* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */ int r = meow_pkg_export(pkg_export_name, pkg_export_out); printf("pkg_export %s -> %s (%d)\n", pkg_export_name, pkg_export_out, r); return r; } if (do_sysdump) { /* M2.11 测试钩子:安全打印系统状态快照(不碰 fb/输入设备,可在常驻 launcher * 旁运行验证 A/E 的 BT/WiFi/电量/音量/亮度抓取),然后退出。 */ meow_sys_refresh(); printf("batt=%d%% chg=%d mv=%d\n", g_sys.batt, g_sys.charging, g_sys.batt_mv); printf("wifi_on=%d ip=%s ssid=%s\n", g_sys.wifi_on, g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-"); printf("bt_seen=%d bt_on=%d\n", g_sys.bt_seen, g_sys.bt_on); printf("vol=%d brightness=%d\n", g_sys.vol, g_sys.brightness); printf("data_free_kb=%ld sd_mounted=%d sd_free=%ld sd_total=%ld\n", g_sys.data_free_kb, g_sys.sd_mounted, g_sys.sd_free_kb, g_sys.sd_total_kb); printf("mode=%d napps=%d ver=%s\n", g_sys.mode, g_sys.napps, g_sys.ver); printf("render: eff_rate=%d cfg=%d always_max=%d frames=%llu\n", meow_ui_effective_rate(), refresh_rate_cfg, refresh_always_max, meow_ui_frames_presented()); return 0; } if (do_scan) { struct app apps[MAX_APPS]; int n = scan_apps(apps); printf("state=%d napps=%d\n", n < 0 ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0), n < 0 ? 0 : n); for (int i = 0; i < n && n > 0; i++) printf(" %d %s -> %s ok=%d\n", i, apps[i].name, apps[i].exec, apps[i].ok); return 0; } if (appinfo_path) { /* 阶段 D · M1 debug:解析单个 app.json 并打印路由/框架/服务/图标(qemu 可测静态路由) */ FILE *f = fopen(appinfo_path, "r"); if (!f) { printf("appinfo: cannot open %s\n", appinfo_path); return 1; } struct app a; memset(&a, 0, sizeof a); meow_parse_app_json(f, &a); fclose(f); printf("name=%s\n", a.name[0] ? a.name : "(missing)"); printf("exec=%s\n", a.exec[0] ? a.exec : "(missing)"); printf("ok=%d\n", (a.exec[0] && access(a.exec, X_OK) == 0) ? 1 : 0); printf("framework=%d\n", a.framework ? 1 : 0); printf("icon=%s\n", a.icon[0] ? a.icon : "(none)"); printf("routes=%d\n", a.nroute); for (int i = 0; i < a.nroute; i++) printf(" route.%s -> %s (depth=%d keep_awake=%d)\n", a.routes[i].act, a.routes[i].chain, chain_depth(a.routes[i].chain), a.routes[i].keep_awake); printf("services=%d\n", a.nsvc); for (int i = 0; i < a.nsvc; i++) printf(" service.%s -> mode:%d restart:%d max:%d window_ms:%d\n", a.svcs[i].name, a.svcs[i].mode, a.svcs[i].restart, a.svcs[i].max, a.svcs[i].window_ms); return 0; } if (ipc_only_ms > 0) { /* 阶段 D · M0/M1 sim:仅跑 IPC 骨架(免 fb/输入设备),供 qemu 下用 meow_ipc * 验证 HELLO→ACK、ACT_QUERY→链路。无卡时 /tmp 缓冲承载日志。 */ fprintf(stderr, "ipc-only: socket=%s\n", ipc_sock_path); int rc = meow_ipc_init(); if (rc != 0) { fprintf(stderr, "ipc-only: IPC init failed, exiting\n"); return 1; } unsigned long long s0 = now_ms(); while (!sigterm_flag && now_ms() - s0 < (unsigned long long)ipc_only_ms) { meow_ipc_process(); meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ usleep(POLL_MS * 1000); } ipc_shutdown = 1; unlink(ipc_sock_path); logmsg("=== meow_launcherd ipc-only exit ==="); return 0; } /* pid file */ int pfd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (pfd >= 0) { char pbuf[16]; int l = snprintf(pbuf, sizeof pbuf, "%d\n", getpid()); write(pfd, pbuf, l); close(pfd); } signal(SIGTERM, on_sigterm); signal(SIGINT, on_sigterm); signal(SIGHUP, SIG_IGN); /* 阶段 D · M0:启动 IPC 线程(AF_UNIX /usr/data/meow_ipc.sock)。幂等;失败只 log, * 主循环仍跑(10ms 热键轮询不受影响)。线程创建晚于进程但有独立生命周期。 */ meow_ipc_init(); struct keypoll kp_power, kp_keys; /* retry opening the input devices (they may not exist yet at boot) */ for (int tries = 0; tries < 30; tries++) { if (kp_open(&kp_power, DEV_POWER) == 0 && kp_open(&kp_keys, DEV_KEYS) == 0) break; logmsg("input devices not ready, retry %d/30 in 1s", tries + 1); log_poll_card(); /* flush boot buffer even if input devices lag */ sleep(1); } if (!kp_power.ok || !kp_keys.ok) logmsg("WARNING: input devices unavailable, launcher blind"); /* ===== MeowOS 常驻主界面(M2.2 修复批次)===== * 播放器已死(find_hiby_pid()==0:bootmenu MeowOS 分支 exec、或热键切 MeowOS 后 exec, * 二者皆此语境)时,launcher == MeowOS 主界面常驻: * - 启动即扫描并自绘应用列目录(run_menu 画 buffer0 + FBIOPAN yoffset=0), * 绝不等到首次热键才画 —— 修复「切到 MeowOS 后屏幕停留在播放器最后一帧」; * - 无存活播放器 → run_menu 内不 SIGSTOP / 不备份 / 不 SIGCONT 死播放器、不恢复死帧; * - BACK TO PLAYER → 显示「需重启」提示并留在主界面(不渲染死帧); * - 仅 SIGTERM 退出(rcS/SSH 关停),退出时屏幕保持应用列表主界面(无残帧)。 * 若此时竟有存活播放器(不应用于 MeowOS,防御性),回落到下面的旧覆盖路径。 */ /* ===== M2.89 回归修复:开机选单期不抢屏 ===== * sysui 在 S70 即拉起 launcher(早于 S90 bootmenu 写 /tmp/meow_boot_mode)。 * 旧判定 is_meow_mode()=find_hiby_pid()<=0 在选单阶段恒真(播放器尚未启动), * launcher 误入 MeowOS 主屏 → 与选单/HibyOS 播放器抢 fb0 与触摸输入 * (开机选 HibyOS 后黑屏/选单被主屏重绘覆盖的回归根因)。 * 修复:mode 文件不存在且 bootmenu 存活 → 怠速等它写(选单无超时,不设等待上限; * bootmenu 消失且仍无文件 → 非开机语境如 SSH 手跑/S92 兜底,回退旧判定)。 * 读到 hiby → 怠速守护(不碰 fb):轮询 mode 文件,变 meow(HibyOS 热键切换)→ 落地主屏。 */ char boot_mode[16] = { 0 }; if (!sigterm_flag) { int announced = 0; while (access(MEOW_BOOT_MODE_PATH, F_OK) != 0 && !sigterm_flag) { if (proc_by_comm("meow_bootmenu") <= 0) break; /* 非开机语境:回退旧判定 */ if (!announced) { logmsg("boot: mode undecided, bootmenu active -> idle wait (no fb grab)"); announced = 1; } usleep(200 * 1000); } FILE *mf = fopen(MEOW_BOOT_MODE_PATH, "r"); if (mf) { if (fscanf(mf, "%15s", boot_mode) != 1) boot_mode[0] = 0; fclose(mf); } while (!sigterm_flag && !strcmp(boot_mode, "hiby")) { /* HibyOS 会话怠速:bootmenu supervisor 管热键/播放器;mode 变 meow(热键切换)即落地。 */ usleep(500 * 1000); mf = fopen(MEOW_BOOT_MODE_PATH, "r"); if (mf) { if (fscanf(mf, "%15s", boot_mode) != 1) boot_mode[0] = 0; fclose(mf); } } if (boot_mode[0]) logmsg("boot mode = %s", boot_mode); } meow_mode = !strcmp(boot_mode, "meow") ? 1 : !strcmp(boot_mode, "hiby") ? 0 : is_meow_mode(); /* M2.12:卡在则把 /usr/data/mnt/sd_0/MeowOS/opt/bin 前插 PATH(外围 CLI 已迁卡, * 按需可用;卡不在跳过,绝不崩)。SSH 会话另由 rootfs /etc/profile.d/meow_card_path.sh 处理。 */ if (ensure_card_mounted() || sd_mounted()) { setenv("PATH", "/usr/data/mnt/sd_0/MeowOS/opt/bin:/usr/bin:/bin:/usr/sbin:/sbin", 1); logmsg("PATH: card MeowOS/opt/bin prepended"); } else { logmsg("PATH: no card, MeowOS/opt/bin skipped"); } if (!sigterm_flag && meow_mode) { /* M2.89:怠速等待期间被 sysui stop(HibyOS 选定)→ 直走退出尾 */ /* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */ ensure_card_mounted(); ensure_wifi(); meow_audiod_kick(); /* 确认 MeowOS 模式 → 拉起独占音频服务(幂等、best-effort、不阻塞主循环) */ struct app apps[MAX_APPS]; int n = scan_apps(apps); int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); int napps = n < 0 ? 0 : n; logmsg("MeowOS mode: hiby_player NOT running -> resident app-list home " "(state=%d napps=%d), drain-to-home immediately", state, napps); run_menu(apps, napps, state, sim_tap, sim_exit); logmsg("=== meow_launcherd exit (MeowOS mode) ==="); /* M4:关机/退出前回收后台保活 App + 无头服务(有界 SIGTERM→SIGKILL→reap); * 清 §10.5 自抑标记,防 launcher 重启后残留标记抑制后续 App present。 */ meow_bgplay_kill_all(); unlink(NOTIF_ACTIVE); if (log_fd >= 0) { close(log_fd); log_fd = -1; } ipc_shutdown = 1; unlink(ipc_sock_path); unlink(PID_PATH); return 0; } /* hotkey state: power_voldown (default) + legacy seq */ int prev_pwr = 0, prev_prev = 0; int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_nxt = 0; int pwr_hold_armed = 0; /* power held, within trigger window */ unsigned long long pwr_down_at = 0; int seq_key_prev = 0; /* the configured seq key, edge-tracked */ int seq_hits = 0; unsigned long long seq_last_at = 0; struct vol_snap seq_vol0 = { -1, -1 }; long sim_hk_done = 0; unsigned long long t0 = now_ms(); int log_tick = 0; /* periodic card-log retry (~1s) while no card log */ if (hotkey_mode == HOTKEY_MODE_PWRVOL) logmsg("ready, hotkey = power_voldown (hold power, tap VOL_DOWN)"); else logmsg("ready, hotkey = %d taps of key code %d (gap %dms)", seq_len, seq_codes[0], seq_gap_ms); while (!sigterm_flag) { unsigned long long t = now_ms(); long el = (long)(t - t0); /* 阶段 D · M0:排空 IPC 队列(非阻塞,零延迟;热键轮询 10ms 不受 socket 阻塞) */ meow_ipc_process(); meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ /* sample current key states */ kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); int pwr = 0, prev = 0, vold = 0, volu = 0, play = 0, nxt = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev = 1; 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_keys.cur[w] & (1UL << (KEY_NEXT % 32))) nxt = 1; } /* sim hotkey: emulate the configured hotkey. * power_voldown (default): HOLD power from N, VOL_DOWN tap at N+800 * (within the 60-2500ms window) — this drives pwr/vold so the * real detection below sees the same DOWN edges a physical press * would, and fires the menu at ~N+800. * seq: 3 quick VOL_UP taps at N, N+300, N+600. */ if (sim_hotkey > 0 && !sim_hk_done) { if (hotkey_mode == HOTKEY_MODE_PWRVOL) { if (el >= sim_hotkey && el < sim_hotkey + 3000) pwr = 1; if (el >= sim_hotkey + 800 && el < sim_hotkey + 950) vold = 1; } else { if (el >= sim_hotkey && el < sim_hotkey + 150) volu = 1; if (el >= sim_hotkey + 300 && el < sim_hotkey + 450) volu = 1; if (el >= sim_hotkey + 600 && el < sim_hotkey + 750) volu = 1; if (el >= sim_hotkey + 750) sim_hk_done = 1; } } if (vold != prev_vold) logmsg("key VOL_DOWN %s", vold ? "DOWN" : "UP"); if (volu != prev_volu) logmsg("key VOL_UP %s", volu ? "DOWN" : "UP"); if (play != prev_play) logmsg("key PLAY %s", play ? "DOWN" : "UP"); if (nxt != prev_nxt) logmsg("key NEXT %s", nxt ? "DOWN" : "UP"); if (pwr != prev_pwr) logmsg("key POWER %s", pwr ? "DOWN" : "UP"); if (prev != prev_prev) logmsg("key PREVIOUS %s", prev ? "DOWN" : "UP"); /* hotkey trigger (v6.5). * DEFAULT — power_voldown: the launcher arms on the power DOWN edge * and fires when a VOL_DOWN DOWN edge lands while power is still * held, 60-2500ms after the power press. The player sees the power * press (its screen-off flow starts — accepted side effect) and the * VOL_DOWN tap (volume -1 — accepted). The menu open runs the * verified unblank + bring-backlight-only-if-dark logic, so a * power-initiated screen-off is undone for the menu. Power release * or window expiry disarms; a VOL_DOWN before 60ms or after 2500ms * is ignored (a plain volume press while holding power). * LEGACY — seq: N quick taps of one key within seq_gap_ms opens the * menu (single/double presses stay ordinary volume keys); the taps * raise volume, snapshotted and restored on trigger. */ if (pwr && !prev_pwr && hotkey_mode == HOTKEY_MODE_PWRVOL) { pwr_hold_armed = 1; pwr_down_at = t; logmsg("hotkey: power DOWN, arm hold-window"); } if (!pwr && prev_pwr) pwr_hold_armed = 0; /* release: disarm */ int hotkey_fired = 0; if (hotkey_mode == HOTKEY_MODE_PWRVOL) { /* disarm once the hold window has passed (power still held) */ if (pwr_hold_armed && pwr && t - pwr_down_at > HK_PWRWIN_MAX) pwr_hold_armed = 0; if (pwr_hold_armed && pwr && vold && !prev_vold) { long since = (long)(t - pwr_down_at); if (since >= HK_PWRWIN_MIN && since <= HK_PWRWIN_MAX) { logmsg("HOTKEY: power held + VOL_DOWN tap (%ldms) -> menu", since); hotkey_fired = 1; } else { logmsg("hotkey: POWER+VOL_DOWN ignored (%ldms outside window)", since); } } } else { int seq_key = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_power.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; if (kp_keys.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; } if (seq_key && !seq_key_prev) { /* DOWN edge */ if (seq_hits == 0) seq_vol0 = vol_snapshot(); else if (t - seq_last_at > (unsigned)seq_gap_ms) { seq_hits = 0; /* gap too long: restart */ seq_vol0 = vol_snapshot(); } seq_hits++; seq_last_at = t; logmsg("seq tap %d/%d", seq_hits, seq_len); if (seq_hits >= seq_len) { logmsg("SEQ HOTKEY: %d taps of key %d -> menu", seq_len, seq_codes[0]); vol_restore(seq_vol0); seq_hits = 0; hotkey_fired = 1; } } else if (!seq_key && seq_key_prev) { /* UP edge: nothing */ } seq_key_prev = seq_key; if (seq_hits > 0 && t - seq_last_at > (unsigned)seq_gap_ms * 2) seq_hits = 0; } if (hotkey_fired) { struct app apps[MAX_APPS]; int n = scan_apps(apps); int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); int napps = n < 0 ? 0 : n; run_menu(apps, napps, state, sim_tap, sim_exit); /* restore the player's panel state FIRST (re-blank if the menu * opened while blanked), THEN let it resume — so the player never * sees a panel that contradicts its display state machine * (v6.2 hotfix) */ restore_backlight(); cont_player(); sim_hk_done = 1; pwr_hold_armed = 0; /* absorb current state as baseline for the menu keys */ kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); prev_vold = prev_volu = prev_play = prev_nxt = 0; prev_pwr = prev_prev = 0; seq_key_prev = 0; seq_hits = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) prev_vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) prev_volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) prev_nxt = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) prev_pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev_prev = 1; } /* the menu just proved the card is present: flush */ log_poll_card(); continue; } prev_vold = vold; prev_volu = volu; prev_play = play; prev_nxt = nxt; prev_pwr = pwr; prev_prev = prev; if (dumpkeys > 0 && el >= dumpkeys) { logmsg("dumpkeys duration reached, exit"); break; } /* card may have mounted after S90: open the card log and move the * /tmp boot buffer into it (no menu needed to see boot logs) */ if (log_fd < 0 && (++log_tick % 100) == 0) log_poll_card(); /* NOTE: the Bug B backlight keeper was REMOVED (2026-08-28 hotfix). * It restored brightness + FBIOBLANK(0) after the player's bkl_time * screen timeout, which unblanked the panel behind the player's back * and froze its display state machine (UI gone, keys dead — device * log: keeper at 40.9s, player wedged). The player owns the panel * blank state; the launcher never touches fb0/backlight outside the * menu/app sessions anymore. The player's own "背光时间" timeout is * expected stock behaviour (set it to 常亮 in the player settings UI * if unwanted). */ usleep(POLL_MS * 1000); } /* If the player is still stopped we never made it through the menu path's * restore+resume (e.g. SIGKILL-free SIGTERM race): restore the panel state * before resuming. When the player is already resumed the menu path did it, * and re-running restore_backlight here would write brightness onto a panel * the player still believes is blanked — leave it alone. */ if (player_stopped) { fb_restore_player(); restore_backlight(); } cont_player(); logmsg("=== meow_launcherd exit ==="); meow_bgplay_kill_all(); /* M4:退出前回收后台保活 App + 无头服务(有界) */ unlink(NOTIF_ACTIVE); /* M4:清 §10.5 自抑标记,防残留抑制后续 App present */ if (log_fd >= 0) { close(log_fd); log_fd = -1; } ipc_shutdown = 1; unlink(ipc_sock_path); unlink(PID_PATH); return 0; }