- reader: app.c/shelf.c/reader.c 全套UI; 新增 reader_conf(reader.conf)/settings(App设置页)/reader_ipc(音量翻页IPC响应); app.json加volume_page - launcher: keys.c meow_volpage_*门 + util.c app_id→fd映射(HELLO记fd/断开清) + app_launcher.c 前台三处接线(进入sink/按键路径volpage_key/离开clear) → 前台声明volume_page的App音量键→MSG_KEY_PAGEUP/PAGEDOWN翻页(非阅读器前台音量照旧) - sdk: meow_select.c 底部面板28 + meow_ui_dialog.c 对话框按钮胶囊(h/2) + test_meow_ui_select.c修采样 - meowpanel: /api/net/policy·/api/power/policy 加防自锁+policy默认关两条护栏(panel.policy_enabled默认0) - 文档: MEOW_APP_SDK.md §12.6 + meowpanel README端点表 0warning;宿主测试全过;二进制留批量批次未入库
304 lines
12 KiB
C
304 lines
12 KiB
C
/* keys.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||
#include "meow_launcher_internal.h"
|
||
|
||
int kd_tick(struct kd_state *s, int raw)
|
||
{
|
||
unsigned long long now = now_ms();
|
||
int ev = KD_NONE;
|
||
if (raw != s->raw) {
|
||
if (s->raw == 0 && raw != 0) { /* 任何跳变都重记稳定窗口(含原始 raw 变化) */ }
|
||
s->raw = raw; s->since = now;
|
||
}
|
||
int want = s->raw;
|
||
if (want != s->stable) {
|
||
if (now - s->since >= KEY_DEBOUNCE_MS) {
|
||
s->stable = want;
|
||
if (s->stable && !s->down) { s->down = 1; s->down_at = now; s->longpressed = 0; ev = KD_DOWN; }
|
||
else if (!s->stable && s->down) { s->down = 0; ev = KD_UP; }
|
||
}
|
||
}
|
||
if (s->down && !s->longpressed && now - s->down_at >= CLICK_MS) s->longpressed = 1;
|
||
return ev;
|
||
}
|
||
|
||
|
||
/* Unblank the panel AFTER the player is frozen: with the player SIGSTOPped
|
||
* it can no longer re-blank, so the menu (drawn on fb0) stays visible even
|
||
* when the menu opened while the player's screen timeout had blanked the
|
||
* panel. The blank state still belongs to the player's state machine — on
|
||
* session end restore_backlight() re-blanks when menu_opened_blank, so the
|
||
* player resumes into a consistent (blanked) state. */
|
||
void unblank_panel(void)
|
||
{
|
||
int fbfd = open(FB_DEV, O_RDWR);
|
||
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); }
|
||
logmsg("panel unblanked (player frozen)");
|
||
}
|
||
|
||
void meow_panel_off(void)
|
||
{
|
||
meow_saved_brightness = read_brightness();
|
||
if (meow_saved_brightness <= 0) meow_saved_brightness = 60;
|
||
write_brightness(0, "meow_panel_off");
|
||
if (screen_off_mode == 0) {
|
||
/* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */
|
||
int f = open(FB_DEV, O_RDWR);
|
||
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
|
||
} else {
|
||
/* black:仅背光 0 全黑(面板仍上电,触摸可唤醒) */
|
||
}
|
||
meow_screen_on = 0;
|
||
logmsg("meow panel OFF (mode=%s brightness 0%s)",
|
||
screen_off_mode ? "black" : "panel",
|
||
screen_off_mode ? "" : " + blank 4");
|
||
}
|
||
|
||
void meow_panel_on(void)
|
||
{
|
||
int f = open(FB_DEV, O_RDWR);
|
||
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
|
||
/* 亮度恢复以**用户设置**为准,而非 meow_saved_brightness(可能过期)。
|
||
* watchdog 独立息屏(wd_panel_off 直接 write_brightness(0),见 meow_watchdog.c)**不会**
|
||
* 更新本进程 meow_saved_brightness——若用其恢复会回错值(真机「PIN 亮屏亮度跳高」,
|
||
* 2026-09-03)。settings 已从盘上读到→按 display.brightness(clamp)恢复;未读到(早启动)
|
||
* →回退 meow_saved_brightness 现值(保持旧行为,防开机竞态)。 */
|
||
if (meow_settings_loaded()) {
|
||
int b = meow_settings_brightness_clamped();
|
||
if (b > 0) { write_brightness(b, "meow_panel_on"); meow_saved_brightness = b; }
|
||
} else {
|
||
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on");
|
||
}
|
||
meow_screen_on = 1;
|
||
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
|
||
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
|
||
* 到非 buffer0 / 失效位置,缓存仍是 0 导致真正的 re-pan 从未发出 —— M2.7 只加 fb_norm_buf0
|
||
* 仍黑屏的原因。主界面都画在 buffer0(mmap 偏移 0),此处强制 re-pan 保证唤醒后落在 buffer0。
|
||
* 当前界面内容的重绘由调用方(run_menu 醒屏分支)经 meow_repaint_surface 完成。 */
|
||
fb_pan_buf0_force();
|
||
logmsg("meow panel ON (blank 0 + brightness %d)", meow_saved_brightness);
|
||
}
|
||
|
||
void meow_toggle_panel(void) { if (meow_screen_on) meow_panel_off(); else meow_panel_on(); }
|
||
|
||
|
||
int kp_open(struct keypoll *kp, const char *dev)
|
||
{
|
||
memset(kp, 0, sizeof *kp);
|
||
kp->fd = open(dev, O_RDONLY | O_NONBLOCK);
|
||
if (kp->fd < 0) { logmsg("open %s: %s", dev, strerror(errno)); return -1; }
|
||
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) {
|
||
logmsg("EVIOCGKEY %s: %s", dev, strerror(errno));
|
||
close(kp->fd); return -1;
|
||
}
|
||
memcpy(kp->prev, kp->cur, sizeof kp->cur);
|
||
kp->ok = 1;
|
||
logmsg("keypoll %s ready", dev);
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* 关闭 keypoll(设置页/下拉框等自开设备用;幂等,ok=0 时不动) */
|
||
void kp_close(struct keypoll *kp)
|
||
{
|
||
if (kp->ok && kp->fd >= 0) close(kp->fd);
|
||
kp->ok = 0;
|
||
}
|
||
|
||
|
||
/* Poll device key state; call cb(code, is_down) for each edge.
|
||
* Returns 1 if any edge. */
|
||
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
|
||
void *ctx)
|
||
{
|
||
if (!kp->ok) return 0;
|
||
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0;
|
||
int changed = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp->cur[w] == kp->prev[w]) continue;
|
||
for (int b = 0; b < 32; b++) {
|
||
int c = (kp->cur[w] >> b) & 1;
|
||
int p = (kp->prev[w] >> b) & 1;
|
||
if (c != p) {
|
||
int code = w * 32 + b;
|
||
changed = 1;
|
||
if (cb) cb(code, c, ctx);
|
||
}
|
||
}
|
||
kp->prev[w] = kp->cur[w];
|
||
}
|
||
return changed;
|
||
}
|
||
|
||
|
||
int kp_is_down(const struct keypoll *k, int code)
|
||
{
|
||
return (k->cur[code / 32] >> (code % 32)) & 1;
|
||
}
|
||
|
||
int hotkey_mode = HOTKEY_MODE_PWRVOL;
|
||
|
||
int seq_codes[MAX_SEQ_LEN];
|
||
|
||
int seq_len = 0;
|
||
|
||
int seq_gap_ms = 600;
|
||
|
||
|
||
static int key_name_code(const char *n)
|
||
{
|
||
if (!strcmp(n, "volup")) return KEY_VOLUMEUP;
|
||
if (!strcmp(n, "voldown")) return KEY_VOLUMEDOWN;
|
||
if (!strcmp(n, "play")) return KEY_PLAYPAUSE;
|
||
if (!strcmp(n, "next")) return KEY_NEXT;
|
||
if (!strcmp(n, "previous")) return KEY_PREVIOUSSONG;
|
||
if (!strcmp(n, "power")) return KEY_POWER;
|
||
return -1;
|
||
}
|
||
|
||
|
||
void load_config(void)
|
||
{
|
||
menu_bg = 0x000000;
|
||
hotkey_mode = HOTKEY_MODE_PWRVOL; /* default: hold power + VOL_DOWN tap */
|
||
seq_len = 0;
|
||
seq_gap_ms = 600;
|
||
char path[256];
|
||
snprintf(path, sizeof path, "%s/meow.conf", MEOWAPPS);
|
||
FILE *f = fopen(path, "r");
|
||
if (f) {
|
||
char buf[128];
|
||
while (fgets(buf, sizeof buf, f)) {
|
||
if (!strncmp(buf, "bg_color=", 9)) {
|
||
unsigned int v;
|
||
if (sscanf(buf + 9, "%x", &v) == 1) menu_bg = v & 0xFFFFFF;
|
||
} else if (!strncmp(buf, "hotkey=", 7)) {
|
||
char *p = buf + 7;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!strncmp(p, "seq", 3)) hotkey_mode = HOTKEY_MODE_SEQ;
|
||
else hotkey_mode = HOTKEY_MODE_PWRVOL;
|
||
} else if (!strncmp(buf, "hotkey_seq=", 11)) {
|
||
char *p = buf + 11;
|
||
seq_len = 0;
|
||
/* BUGFIX: the outer loop must consume the EOL — with
|
||
* *p staying at '\n' the empty-name branch never advanced p
|
||
* and the daemon spun forever (found on-device: a hotkey_seq
|
||
* line wedged load_config in a busy loop). */
|
||
while (*p && *p != '\n' && *p != '\r' && seq_len < MAX_SEQ_LEN) {
|
||
char name[16];
|
||
int k = 0;
|
||
while (*p && *p != ',' && *p != '\n' && *p != '\r' && k < 15)
|
||
name[k++] = *p++;
|
||
name[k] = 0;
|
||
if (k >= 15) /* malformed long name: skip to separator/EOL */
|
||
while (*p && *p != ',' && *p != '\n' && *p != '\r') p++;
|
||
if (*p == ',') p++;
|
||
int c = key_name_code(name);
|
||
if (c > 0) seq_codes[seq_len++] = c;
|
||
}
|
||
} else if (!strncmp(buf, "hotkey_seq_gap=", 15)) {
|
||
int g = atoi(buf + 15);
|
||
if (g >= 200 && g <= 2000) seq_gap_ms = g;
|
||
}
|
||
}
|
||
fclose(f);
|
||
}
|
||
if (seq_len == 0) { seq_codes[0] = KEY_VOLUMEUP; seq_len = 3; } /* seq fallback volup x3 */
|
||
if (hotkey_mode == HOTKEY_MODE_PWRVOL)
|
||
logmsg("config: bg_color=%06x hotkey=power_voldown (window %d-%dms)",
|
||
menu_bg, HK_PWRWIN_MIN, HK_PWRWIN_MAX);
|
||
else
|
||
logmsg("config: bg_color=%06x hotkey=seq (%d taps of code %d gap=%dms)",
|
||
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 系统级音量键翻页门(READER_UI_DESIGN §E;用户 2026-09-04 拍板)。
|
||
*
|
||
* 语义:音量键是系统资源(默认调音量)。本门在「当前台 App 为其 app.json 声明
|
||
* volume_page 的应用(MVP:os.meow.reader)」时,把 VOL_UP/VOL_DOWN 转换成系统翻页
|
||
* 信号(MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该前台 App;其余情况一概
|
||
* 放行(return 0),音量基本行为/插孔消抖/音量反转等绝不破坏。
|
||
*
|
||
* 职责分离(关键,且最小侵入):
|
||
* - 本模块只提供「判定 + 门」:meow_volpage_app_allows() 读 app.json 声明;
|
||
* meow_volpage_key() 在「已武装 sink 且按下 VOL」时消费并发下发;
|
||
* - 武装(sink 注册)= 前台应用进入体积页声明 app 时调用 meow_volpage_sink()
|
||
* (应用存在连接后其 fd 生效);离开/退出时 meow_volpage_sink_clear()。
|
||
* 该接线落在「前台会话的按键路径」(app_launcher.c / meow_launcherd.c 主循环),
|
||
* 属主会话统一收编范围,此处不越界改动。
|
||
*
|
||
* 声明字段(app.json):`"volume_page": true`(或 1)。未来其它 App 可声明复用。
|
||
* ========================================================================= */
|
||
|
||
/* 前台翻页 sink(本模块私有;单写者=前台会话上下文,读=按键门)。 */
|
||
static char g_volpage_app[MEOW_APP_ID_MAX] = "";
|
||
static int g_volpage_fd = -1;
|
||
|
||
void meow_volpage_sink_clear(void); /* 前向声明(sink 内调用) */
|
||
|
||
/* 读 <MEOWAPPS>/<app_id>/app.json 是否声明 volume_page=true|1。1=声明;0=否/读不到。 */
|
||
int meow_volpage_app_allows(const char *app_id)
|
||
{
|
||
if (!app_id || !app_id[0]) return 0;
|
||
char path[320];
|
||
snprintf(path, sizeof path, "%s/%s/app.json", MEOWAPPS, app_id);
|
||
FILE *f = fopen(path, "r");
|
||
if (!f) return 0;
|
||
/* 轻量扫描:找 "volume_page" → 跳过空白 → true/1。app.json 为紧凑 JSON。 */
|
||
char buf[1024];
|
||
buf[0] = 0;
|
||
size_t rd = fread(buf, 1, sizeof buf - 1, f);
|
||
buf[rd] = 0;
|
||
fclose(f);
|
||
const char *p = buf;
|
||
while ((p = strstr(p, "volume_page")) != NULL) {
|
||
p += strlen("volume_page");
|
||
while (*p == ' ' || *p == '\t' || *p == ':' || *p == '"') p++;
|
||
if (strncmp(p, "true", 4) == 0) return 1;
|
||
if (*p == '1' && (p[1] < '0' || p[1] > '9')) return 1;
|
||
/* 未命中则继续找(防字段重复);跳过本段 */
|
||
while (*p && *p != ',' && *p != '}') p++;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 武装/清除 sink。app_id 非空且 fd>=0 → 记录;否则清除。幂等(同 app_id+fd 不重复记日志,
|
||
* 供前台会话循环每圈安全重调,也兼容断线重连换 fd)。 */
|
||
void meow_volpage_sink(const char *app_id, int fd)
|
||
{
|
||
if (!app_id || !app_id[0] || fd < 0) { meow_volpage_sink_clear(); return; }
|
||
if (g_volpage_fd == fd && !strcmp(g_volpage_app, app_id)) return; /* 同 app_id+fd:幂等 */
|
||
snprintf(g_volpage_app, sizeof g_volpage_app, "%s", app_id);
|
||
g_volpage_fd = fd;
|
||
logmsg("volpage: sink armed app=%s fd=%d", g_volpage_app, g_volpage_fd);
|
||
}
|
||
|
||
void meow_volpage_sink_clear(void)
|
||
{
|
||
if (g_volpage_fd >= 0)
|
||
logmsg("volpage: sink cleared app=%s", g_volpage_app[0] ? g_volpage_app : "?");
|
||
g_volpage_app[0] = 0;
|
||
g_volpage_fd = -1;
|
||
}
|
||
|
||
int meow_volpage_armed(void) { return g_volpage_fd >= 0; }
|
||
|
||
/* 按键门:按下 VOL_UP/DOWN 且 sink 已武装 → 下发翻页信号并消费(返回 1)。
|
||
* 未武装 / 非 VOL / 抬起 → 返回 0(放行给音量路径)。 */
|
||
int meow_volpage_key(int code, int is_down)
|
||
{
|
||
if (!is_down) return 0; /* 只响应按下边沿 */
|
||
if (g_volpage_fd < 0) return 0; /* 未武装:音量照旧 */
|
||
if (code == KEY_VOLUMEUP) {
|
||
(void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEUP, 1, NULL, 0);
|
||
return 1;
|
||
}
|
||
if (code == KEY_VOLUMEDOWN) {
|
||
(void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEDOWN, 1, NULL, 0);
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|