2015 lines
112 KiB
C
2015 lines
112 KiB
C
/* main_menu.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||
#include "meow_launcher_internal.h"
|
||
|
||
|
||
/* 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;
|
||
|
||
|
||
/* ---- 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 串 + 叠加层强度 + 明暗。变化才重建缓存。 */
|
||
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;
|
||
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);
|
||
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 语义) */
|
||
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);
|
||
}
|
||
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);
|
||
}
|
||
|
||
/* 图标绘制:有图 meow_ui_img_cover(居中裁方);无图 → 主题色圆角块 + 首字母。 */
|
||
static void menu_draw_icon(const struct app *a, meow_img *icon, int x, int y, int sz,
|
||
const meow_ui_theme_t *th)
|
||
{
|
||
if (icon) { meow_ui_img_cover(x, y, sz, icon); return; }
|
||
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);
|
||
}
|
||
|
||
/* 应用格几何(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);
|
||
}
|
||
}
|
||
|
||
/* 列表行:图标 + 名称 + 副标题(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 */
|
||
char sub[40];
|
||
snprintf(sub, sizeof sub, "%s", 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/<id>/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/<id>/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;
|
||
}
|
||
|
||
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=未初始化,首圈建立基线) */
|
||
|
||
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();
|
||
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) {
|
||
snprintf(toast, sizeof toast, "CANNOT START: %s", 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();
|
||
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:卡挂载后重配多字体(此前无全局可配) */
|
||
meow_font_scan_presets(); /* M2.82+:卡就绪后重扫字库(启动时卡未挂载会只有默认项) */
|
||
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 仍黑屏根因)。 */
|
||
meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */
|
||
last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */
|
||
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<SB_BAR_H) + 下拉达 NCP_DRAG_ENTER_SLOP →
|
||
* 立即切入 run_notif_center(此时手指仍按下拖动),由面板内拖动路径把 progress
|
||
* 映射为手指位移(跟手 1:1),松手由面板内锚定判定补到展开/收回(半程可回)。
|
||
* 原「抬手边沿 mnp_top_swipe_down」打开改为「拖动中即开」(该抬手边沿保留为防御)。
|
||
* 先在本次 ABS 内触发一次(menu_live_open_pending latch);
|
||
* 进入前 t_down=0 使返回后同次手势的剩余事件被忽略(防 double-open/残留)。 */
|
||
if (t_down && t_down_valid && !menu_live_open_pending &&
|
||
t_down_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) {
|
||
snprintf(toast, sizeof toast, "CANNOT START: %s",
|
||
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<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 (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) {
|
||
snprintf(toast, sizeof toast, "CANNOT START: %s",
|
||
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) {
|
||
snprintf(toast, sizeof toast, "CANNOT START: %s", 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();
|
||
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<last_input 无符号回绕误判「空闲30s」。 */
|
||
if (idle_ms_since(t, last_input_ms) >= (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;
|
||
}
|
||
if (need_menu_paint) {
|
||
draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */
|
||
need_menu_paint = 0;
|
||
}
|
||
/* 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 于空脏。 */
|
||
fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */
|
||
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 状态栏整条,不动列表 */
|
||
}
|
||
|
||
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.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
|
||
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
|
||
" [--fontscan <card_root>]\n");
|
||
}
|
||
|
||
|
||
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 <root>:宿主/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/<family>/<weight>/,填充「字体文件」下拉 preset。
|
||
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */
|
||
meow_font_scan_presets();
|
||
|
||
if (fontscan_root) {
|
||
/* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/<family>/<weight>/ 并打印 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,防御性),回落到下面的旧覆盖路径。 */
|
||
meow_mode = 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 (meow_mode) {
|
||
/* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */
|
||
ensure_card_mounted();
|
||
ensure_wifi();
|
||
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) ===");
|
||
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 ===");
|
||
if (log_fd >= 0) { close(log_fd); log_fd = -1; }
|
||
ipc_shutdown = 1; unlink(ipc_sock_path);
|
||
unlink(PID_PATH);
|
||
return 0;
|
||
}
|
||
|