/* * meow_ui_input.c — 输入层(B1.4)。 * * 建立输入线程,读 /dev/input/event*(event1=触摸 goodix-ts、event0=物理键 * md-gpio-keys、event2=ADC 键),翻译成语义事件(KEY/TAP/SWIPE)压入环形队列; * meow_ui_run / IME 循环弹出分发。应用不再自读 event*。 * * 触摸:BTN_TOUCH 按下记录原点,抬起时位移 < 阈值 → TAP,否则按主导轴 → SWIPE。 * 物理键:read() 事件流 EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.55/agent-217 起系统按键 * 语义在 SDK 输入层消化: * 电源 116 → 维持现状(息屏/亮屏/长按菜单由内核/sys_server/watchdog),SDK 不动、不下发 APP; * 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,volume_step/volume_max * 约束,与 launcher 设置页/watchdog 同源); * 播放/暂停 164、上一曲 165/19、下一曲 163 → 转发到当前媒体会话(App 是媒体源→本地回调, * 否则经 IPC 发 launcher 转发)。 * 「App 收到的是语义事件」:上述音量/媒体键在 SDK 消化后**仍下发 MEOW_EV_KEY**(on_key),但 * App 不得把它们映射成自定义行为(APPS.md §3.4.1 语义不变)。 * 真机链路兜底(本次修复):物理键的按下不一定进入 evdev 事件流(事件流可被 EVIOCGRAB 独占或 * 驱动不投递;APPS.md §4.4 明确 launcher 用 EVIOCGKEY 状态轮询读键、「与 grab/投递无关」)。故 * 输入线程除 read() 事件流外,每轮还对键设备做一次 EVIOCGKEY 状态位图轮询,把 0→1/1→0 边沿并入 * 同一套系统键语义(poll_sys_keys_eviocgkey);事件流与位图双源经 sys_key_press/sys_key_release * 的「同码 120ms 窗口」去重,一次物理按下只触发一步系统语义 + 一次 MEOW_EV_KEY 下发。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "meow_ui_sys.h" #include "meow_ui_vol.h" #include "meow_ui_media.h" #include #include #include #include #include #include #include #include #include #include /* linux input 事件常量(本设备实测) */ #define EV_SYN 0 #define EV_KEY 1 #define EV_ABS 3 #define BTN_TOUCH 0x14a #define ABS_X 0x00 #define ABS_Y 0x01 #define ABS_MT_POSITION_X 0x35 #define ABS_MT_POSITION_Y 0x36 #ifndef EVIOCGKEY #define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) #endif #ifndef KEY_MAX #define KEY_MAX 0x2ff #endif #define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */ #define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */ #define MEOW_LONG_PRESS_MS 500u /* §6.3 通用长按默认阈值(ms;meow_ui_set_long_press_ms 可配) */ #define MEOW_LONG_PRESS_SLOP 12 /* §6.3 按下后「仍算按住」的最大位移(px);超此取消长按 */ #define QUEUE_N 128 #define MAX_DEVS 8 #define MEOW_APP_ACTIVITY_FILE "/tmp/meow_app_activity" /* M2.71-bug5:App 活动→launcher 重置息屏计时 */ /* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)── * 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理 * (路由 meow_ui_sys_notif_pull),不下发给 APP 的 on_swipe(顶部热区为系统保留手势)。 * 与左缘返回(SWIPE_LEFT)/ meow_ui_swipe_hint 互不影响——后者仍全屏生效,详见下方触摸机。 */ #define MEOW_EV_NOTIF_PULL 5 /* 顶部下滑 → 通知中心判定:纵向 DOWN 手势的起点(origin_y)是否落在顶部热区内。 * 热区由 meow_ui_statusbar_hotzone() 给出(非全屏=状态栏高、全屏=MEOW_SBAR_HOTZONE_FS、 * 收合=提示线高)。供宿主单测直调(判据与触摸状态机共用)。 */ int meow_ui_input_is_notif_pull(int origin_y, int hotzone) { return origin_y >= 0 && origin_y < hotzone; } static int g_thread_started = 0; static pthread_t g_thread; static volatile int g_thread_run = 1; /* 环形队列(互斥保护) */ static meow_input_ev g_q[QUEUE_N]; static volatile int g_qh = 0, g_qt = 0; static pthread_mutex_t g_qm = PTHREAD_MUTEX_INITIALIZER; /* 事件循环状态 */ static volatile int g_exit = 0; static meow_ui_events g_events; static int g_events_set = 0; static uint64_t last_tick_ms = 0; /* M2.22(C):启动 cooldown。应用刚进入时,用户点列表那几下(BTN_TOUCH 残留/抬手尾巴) * 可能被当作「左缘滑动」→ 立即返回。窗口期丢弃 SWIPE(尤其 LEFT),仍接受 TAP。 */ static uint64_t startup_cooldown_until = 0; #define STARTUP_COOLDOWN_MS 500 static uint64_t now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; } /* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */ static const char *meow_settings_path(void) { const char *e = getenv("MEOW_SETTINGS_FILE"); return (e && e[0]) ? e : "/usr/data/meow/settings.conf"; } /* ---- 插孔消抖(M2.92,与 launcher/watchdog 255 同款)---- * 插拔 3.5/4.4 耳机的电气瞬态会在音量键 ADC(event2)上产生幽灵 VOL_UP/DOWN 按下 * (真机实证:插 4.4 同帧 ramp 爬满上限)。插孔状态变化后 800ms 内忽略音量键边沿。 * 节流:500ms 才真读 /sys 一次(不在热路径每圈读文件)。 */ static int g_jack_combo = -1; /* hs*2+bal 缓存,-1=未读 */ static unsigned long long g_jack_changed_at = 0, g_jack_polled_at = 0; static int meow_input_jack_debounce(void) { unsigned long long now = meow_ui_time_ms(); if (now - g_jack_polled_at >= 500) { g_jack_polled_at = now; int hs = -1, bal = -1; char b[8]; int fd = open("/sys/class/switch/headset/state", O_RDONLY); if (fd >= 0) { ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n > 0) { b[n] = 0; hs = atoi(b); } } fd = open("/sys/class/switch/balance/state", O_RDONLY); if (fd >= 0) { ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n > 0) { b[n] = 0; bal = atoi(b); } } if (hs >= 0 && bal >= 0) { int combo = hs * 2 + bal; if (g_jack_combo >= 0 && combo != g_jack_combo) g_jack_changed_at = now; g_jack_combo = combo; } } return g_jack_changed_at && (now - g_jack_changed_at) < 800; } /* ---- §10.5 叠加让位:通知中心叠加会话期输入排空(播放中 App 不被 SIGSTOP 的配套)---- * 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,ncm 播放线程同进程), * App 输入线程仍在读同一批 evdev 设备(每 fd 各收一份事件副本,与 launcher 互不剥夺)——若不 * 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发)。 * 故标记存在期:排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作 * 叠加期由 launcher/watchdog 单侧承担)。200ms 节流缓存存在性(与 present 层同口径,别每圈 stat)。 */ static int overlay_input_suppressed(void) { static char s_path[108]; static int s_path_init = 0; static unsigned long long s_last_chk = 0; static int s_last_res = 0; if (!s_path_init) { const char *e = getenv("MEOW_NOTIF_ACTIVE_FILE"); /* 宿主/单测覆写(进程内只解析一次) */ snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : "/tmp/meow_notif_active"); s_path_init = 1; } unsigned long long now = now_ms(); if (s_last_chk && now - s_last_chk < 200) return s_last_res; s_last_chk = now; s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0; return s_last_res; } /* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)---- * 物理键是系统资源(APPS.md §3.4.1),语义全局统一。本函数在输入线程执行系统动作: * 电源 116 → 息屏/亮屏/长按菜单(内核/sys_server/watchdog),SDK 不动(维持现状); * 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,与 launcher * 设置页/watchdog 同源:volume_step/volume_max 约束),仅按下边沿(1)一步; * 播放/上次曲 164/165/19/163 → 转发到当前媒体会话(App 是媒体源→本地回调;否则 IPC→launcher)。 * 仅处理按下边沿(val==1)触发系统动作;重复(2)/抬起(0)不重复执行。on_key 的下发由输入线程 * 在调用本函数后另行 push(音量/媒体键仍通知 App,App 不得映射)。 */ static void meow_input_handle_sys_key(int code, int val) { switch (code) { case MEOW_KEY_POWER: /* 电源:维持现状,SDK 不动 */ break; case MEOW_KEY_VOLUMEDOWN: case MEOW_KEY_VOLUMEUP: if (val == 1 && !meow_input_jack_debounce()) { /* 插孔瞬态期忽略幽灵音量边沿 */ int dir = (code == MEOW_KEY_VOLUMEUP) ? +1 : -1; int nv = meow_ui_vol_step(meow_settings_path(), dir); /* settings 语义+持久化 */ meow_ui_vol_dac_write(nv); /* 写真实 CS43198 DAC */ } break; case MEOW_KEY_PLAYPAUSE: case MEOW_KEY_PREVIOUS: case MEOW_KEY_NEXT: case 19: /* KEY_PREVIOUSSONG 旧码 */ if (val == 1) meow_ui_media_sys_key(code); break; default: break; } } void meow_input_drain(void) { pthread_mutex_lock(&g_qm); g_qh = 0; g_qt = 0; pthread_mutex_unlock(&g_qm); } static void push_ev(int type, int a, int b) { pthread_mutex_lock(&g_qm); int n = (g_qt + 1) % QUEUE_N; if (n == g_qh) { /* 满:丢弃 */ pthread_mutex_unlock(&g_qm); return; } g_q[g_qt].type = (uint8_t)type; g_q[g_qt].a = a; g_q[g_qt].b = b; g_qt = n; pthread_mutex_unlock(&g_qm); } int meow_input_pop(meow_input_ev *ev) { if (!ev) return 0; pthread_mutex_lock(&g_qm); if (g_qh == g_qt) { pthread_mutex_unlock(&g_qm); return 0; } *ev = g_q[g_qh]; g_qh = (g_qh + 1) % QUEUE_N; pthread_mutex_unlock(&g_qm); return 1; } void meow_input_inject(int type, int a, int b) { push_ev(type, a, b); } /* ---- 触摸状态机(M2.22 B:手势级干净起点 + 原点严格锁定,杜绝残留坐标污染)---- * 背景:goodix 事件时序不保证 BTN_TOUCH 与 ABS 的先后;旧版在按下边沿用 t_cx/t_cy * 立即锁原点,而 t_cx/t_cy 是上一手势的残留 —— 上一次「中部 tap」留下的坐标被本次 * 按下当原点(>EDGE_W),导致后续左缘侧滑不返回、进度条不出。且进程内不自愈。 * 修法:每次抬手彻底清空全部轨迹状态;原点只在「本次按下之后」出现的首个有效 ABS * 锁定(touch_update,gated 于 t_down && !t_origin_valid && x>=0 && y>=0)。 */ static int t_down = 0; static int t_press_valid = 0; /* BTN_TOUCH=1 已见(本次按下的 gate),等首个有效 ABS 才锁原点 */ static int t_dx = 0, t_dy = 0; /* 按下原点(BTN_TOUCH 后首个有效 ABS 锁定) */ static int t_cx = 0, t_cy = 0; /* 当前坐标(-1=本次尚未见到有效坐标) */ static int t_origin_valid = 0; /* 原点是否已锁定 */ static uint64_t t_down_ms = 0; /* 按下时刻(monotonic ms),供实时按住时长用 */ /* M2.71-bug2/6:拖动过程事件上次上报坐标(坐标变化才入队 MEOW_EV_TOUCH_MOVE,防刷屏)。 */ static int t_mv_x = -1, t_mv_y = -1; /* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */ static int t_long_fired = 0; /* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */ static int g_long_press_ms = MEOW_LONG_PRESS_MS; /* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */ static meow_long_press_fn g_long_press_cb = NULL; /* 按下边沿:只标记按下,绝不用传入坐标立即锁原点(避免用上一手势残留坐标当原点)。 * t_cx/t_cy 归 -1 —— 本次手势以「无坐标」起步,只接受随后到来的有效 ABS。 */ static void touch_press(int x, int y) { (void)x; (void)y; t_down = 1; t_press_valid = 1; t_origin_valid = 0; t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = now_ms(); /* 记按下时刻:供实时按住时长/按住视觉 */ t_mv_x = -1; t_mv_y = -1; /* M2.71:拖动过程起点未定 */ t_long_fired = 0; /* §6.3:每次新按压复位长按触发标记 */ } /* 由 ABS_X/ABS_Y 驱动:若本次按下且原点未锁且坐标有效 → 锁原点;再更新当前坐标。 * M2.71-bug2/6:原点锁定即报 MEOW_EV_TOUCH_DOWN(拖动起点);随后坐标变化报 MEOW_EV_TOUCH_MOVE * (坐标变了才报,省队列)。这两类事件供 slider 拖动 / 列表&页面滚动跟手。 */ static void touch_update(int x, int y) { if (t_down && !t_origin_valid && x >= 0 && y >= 0) { t_dx = x; t_dy = y; t_origin_valid = 1; t_mv_x = x; t_mv_y = y; push_ev(MEOW_EV_TOUCH_DOWN, x, y); } if (x >= 0) t_cx = x; if (y >= 0) t_cy = y; if (t_down && t_origin_valid && t_cx >= 0 && t_cy >= 0 && (t_cx != t_mv_x || t_cy != t_mv_y)) { t_mv_x = t_cx; t_mv_y = t_cy; push_ev(MEOW_EV_TOUCH_MOVE, t_cx, t_cy); } } static void touch_release(void) { if (!t_down) return; t_down = 0; t_press_valid = 0; int was_long = t_long_fired; /* §6.3:本次按压是否已触发过长按(抬手不再产生点击/滑动) */ t_long_fired = 0; /* 本次按下从未锁到有效坐标(幽灵触摸/坐标未就绪):丢弃,且清空全部轨迹。 */ if (!t_origin_valid) { t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1; return; } t_origin_valid = 0; int mdx = t_cx - t_dx, mdy = t_cy - t_dy; int adx = mdx < 0 ? -mdx : mdx, ady = mdy < 0 ? -mdy : mdy; /* 长按已触发:本次手势由长按消费,抬手不再产生 TAP/SWIPE/NOTIF_PULL(抑制点击,§6.3)。 */ if (was_long) { t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1; return; } /* 手势判定只在「抬手(release)边沿」做:按下/移动过程不触发 SWIPE,仅报 PROGRESS。 * * 返回手势("左滑"语义):原点确实在左缘 + 向右移动 ≥SWIPE_THRESH。设备实际为 * 「左缘向内(右)划」≥65px(M2.21:90→65 用户实测更顺手;与 watchdog 兜底一致)。 * 原点 t_dx 是本次按下后首个有效 ABS 锁定的真实按下点(非残留),且 t_cx/t_dx 已 * 被手势级清空,绝不继承上一手势坐标,确保: * ① 中部右滑不会误判成左缘返回(t_dx>=EDGE_W); * ② 先点中部再侧滑不返回(原点锁定在中部,>=EDGE_W),符合「必须从左缘起划」; * ③ 右移严格正向(t_cx-t_dx>=0),杜绝「向左划也出条/误返回」。 * SDK 统一上报为 MEOW_SWIPE_LEFT(设计"左滑=返回"契约,hello 用 on_swipe(LEFT) 退出)。 * * 屏幕中部滑动不是返回手势:中部右滑→RIGHT(内容导航),中部左滑→非返回(落 TAP, * 避免任意处左滑误退出)。纵向滑动→UP/DOWN 照旧。 */ if (adx >= SWIPE_THRESH || ady >= SWIPE_THRESH) { /* 顶部下滑 → 通知中心(系统保留手势,不下发 APP):起点在顶部热区 + 有真实向下位移。 * 在主导轴判定(adx vs ady)之前先行识别并置最高优先级——顶起「起点同时落在顶部热区与 * 左缘」的边界(定案:起点 y 在顶部热区 → 顶部手势优先)。若无此优先,下落时含右向分量的 * 手势会被水平分量抢先判成 MEOW_SWIPE_LEFT → 漏给 App on_swipe → 根页退出(真机闪退链)。 * 热区高度随状态变化(非全屏=整条状态栏、全屏=MEOW_SBAR_HOTZONE_FS 缩小、收合=提示线高) * ——全屏 app 只在顶部极窄热区被系统占用,故「不影响全屏 app」。 * 仅当真「向下拉」(mdy>0 且 ady≥SWIPE_THRESH)才认:纯水平右滑(左缘返回)或上滑不误判。 */ if (mdy > 0 && ady >= SWIPE_THRESH && meow_ui_input_is_notif_pull(t_dy, meow_ui_statusbar_hotzone())) { push_ev(MEOW_EV_NOTIF_PULL, t_dy, ady > adx ? ady : adx); } else if (adx >= ady) { if (t_dx >= 0 && t_dx < EDGE_W && (t_cx - t_dx) >= SWIPE_THRESH) { push_ev(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, adx); /* 左缘返回手势 */ } else if (mdx > 0) { push_ev(MEOW_EV_SWIPE, MEOW_SWIPE_RIGHT, adx); /* 中部右滑:内容 */ } else { push_ev(MEOW_EV_TAP, t_cx, t_cy); /* 中部左滑:非返回 */ } } else { int dir = mdy > 0 ? MEOW_SWIPE_DOWN : MEOW_SWIPE_UP; push_ev(MEOW_EV_SWIPE, dir, ady > adx ? ady : adx); } } else { push_ev(MEOW_EV_TAP, t_cx, t_cy); } /* 手势级干净起点:清空全部轨迹,下一手势从零开始,杜绝污染无自愈。 */ t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1; } /* ---- 实时触摸态(内部:控件绘制按压/按住视觉用)---- * 直接读触摸状态机当前值(与语义事件队列无关;无按下/坐标未就绪时 x/y 返回 -1)。 */ int meow_input_touch_down(void) { return t_down; } int meow_input_touch_x(void) { return t_cx; } int meow_input_touch_y(void) { return t_cy; } unsigned int meow_input_touch_hold_ms(void) { if (!t_down) return 0; uint64_t e = now_ms() - t_down_ms; if (e > 0xFFFFFFFFu) e = 0xFFFFFFFFu; return (unsigned int)e; } /* 宿主单测/内部注入:模拟按压(down=1 时 x,y=压点、hold_ms=已按住时长),设备运行时无调用方。 */ void meow_input_touch_set(int down, int x, int y, unsigned hold_ms) { if (down) { t_down = 1; t_press_valid = 1; t_origin_valid = 1; t_long_fired = 0; t_cx = x; t_cy = y; t_dx = x; t_dy = y; uint64_t now = now_ms(); t_down_ms = (now > (uint64_t)hold_ms) ? (now - (uint64_t)hold_ms) : 0; } else { t_down = 0; t_press_valid = 0; t_origin_valid = 0; t_long_fired = 0; t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = 0; t_mv_x = -1; t_mv_y = -1; } } /* 宿主单测/内部注入:模拟按下后移动到 (x,y)(保原点、更新当前坐标;位移超长按容忍即取消)。 * 对应输入线程 touch_update 的位移分支(这里不产生语义事件,仅更新坐标状态)。 */ void meow_input_touch_move(int x, int y) { if (!t_down || !t_origin_valid) return; t_cx = x; t_cy = y; t_mv_x = x; t_mv_y = y; } /* 宿主单测/内部注入:模拟一次抬手(真实 touch_release;含长按后抑制 TAP/SWIPE)。 */ void meow_input_touch_release(void) { touch_release(); } /* ---- §6.3 通用长按公共 API + 阈值跨越检测 ---- * meow_input_long_press_poll:主循环(meow_ui_run 每帧)与宿主单测调用。读触摸状态机: * 按下、原点/坐标已就绪、未触发过长按、位移 ≤ 容忍、按住时长 ≥ 阈值 → 置 t_long_fired, * 入队 MEOW_EV_LONG_PRESS(a=x,b=y=当前按下点),返回 1;否则返回 0。幂等(一次按压一次)。 */ void meow_ui_set_long_press_cb(meow_long_press_fn cb) { g_long_press_cb = cb; } void meow_ui_set_long_press_ms(int ms) { g_long_press_ms = (ms > 0) ? ms : (int)MEOW_LONG_PRESS_MS; } int meow_ui_long_press_ms(void) { return g_long_press_ms; } int meow_input_long_press_poll(void) { if (!g_long_press_cb) return 0; /* 未注册回调:长按整体关闭,保持旧点击语义(短按/长按都 TAP) */ if (!t_down || t_long_fired || !t_origin_valid || t_cx < 0 || t_cy < 0) return 0; int mdx = t_cx - t_dx, mdy = t_cy - t_dy; int amx = mdx < 0 ? -mdx : mdx, amy = mdy < 0 ? -mdy : mdy; int move = amx > amy ? amx : amy; if (move > MEOW_LONG_PRESS_SLOP) return 0; /* 位移超容忍:取消 */ if (meow_input_touch_hold_ms() < (unsigned)g_long_press_ms) return 0; /* 未达阈值 */ t_long_fired = 1; push_ev(MEOW_EV_LONG_PRESS, t_cx, t_cy); return 1; } /* ---- 系统键(音量/媒体)去重 + EVIOCGKEY 状态轮询 ---- * M2.55/agent-217 之后 SDK 在输入线程消化系统键语义;但真机物理键(jz adc keyboard event2、 * md-gpio-keys event0、earpods_adc event3)的按下不一定进入 evdev 事件流(事件流可被其它进程 * EVIOCGRAB 独占或驱动不投递),而 launcher 全程用 EVIOCGKEY 状态轮询读键(APPS.md §4.4, * 「与 grab/投递无关」)。为让 App 前台物理音量/媒体键可靠生效,输入线程除 read() 事件流外, * 还按固定节奏轮询内核按键位图(EVIOCGKEY),把 0→1/1→0 边沿并入同一套系统键语义。 * 双源(事件流 + 状态位图)对同一次物理按下各报一次,用「同码 120ms 窗口」去重,保证 * 一次按下只触发一步系统语义 + 一次 MEOW_EV_KEY 下发(App on_key 收到语义事件,不得映射)。 */ static int g_sys_press_code = -1; static uint64_t g_sys_press_ms = 0; #define SYS_KEY_DEDUP_MS 120 static void sys_key_press(int code) { uint64_t now = now_ms(); if (code == g_sys_press_code && now - g_sys_press_ms < SYS_KEY_DEDUP_MS) return; /* 事件流 + EVIOCGKEY 双源去重:同码窗口内只消化一次 */ g_sys_press_code = code; g_sys_press_ms = now; meow_input_handle_sys_key(code, 1); push_ev(MEOW_EV_KEY, code, 1); } static void sys_key_release(int code) { push_ev(MEOW_EV_KEY, code, 0); } /* 对一个 event 设备做一次 EVIOCGKEY 状态边沿检测(prev 为该设备上一状态位图,原地更新)。 * 只在系统键码(音量/媒体)上找边沿;电源(116)不进此表——SDK 维持「不动、不下发」纪律。 */ static void poll_sys_keys_eviocgkey(int fd, unsigned char *prev) { unsigned char cur[KEY_MAX / 8 + 1]; if (ioctl(fd, EVIOCGKEY(sizeof cur), cur) < 0) return; static const int sys_codes[] = { MEOW_KEY_VOLUMEDOWN, MEOW_KEY_VOLUMEUP, MEOW_KEY_PLAYPAUSE, MEOW_KEY_PREVIOUS, MEOW_KEY_NEXT, 19, }; for (unsigned ci = 0; ci < sizeof sys_codes / sizeof sys_codes[0]; ci++) { int code = sys_codes[ci]; int b = (cur[code / 8] >> (code % 8)) & 1; int pb = (prev[code / 8] >> (code % 8)) & 1; if (b == pb) continue; prev[code / 8] = (unsigned char)(b ? (prev[code / 8] | (1u << (code % 8))) : (prev[code / 8] & (unsigned char)~(1u << (code % 8)))); if (b) sys_key_press(code); /* 0→1:按下(去重后触发一步系统语义) */ else sys_key_release(code); /* 1→0:抬起 */ } } /* ---- 输入线程:poll 所有 event 设备 + EVIOCGKEY 系统键轮询 ---- */ static void *input_thread(void *arg) { (void)arg; struct pollfd fds[MAX_DEVS]; unsigned char prev_key[MAX_DEVS][KEY_MAX / 8 + 1]; int nfd = 0; char dev[32]; for (int i = 0; i < MAX_DEVS; i++) { snprintf(dev, sizeof dev, "/dev/input/event%d", i); int e = open(dev, O_RDONLY | O_NONBLOCK); if (e >= 0) { fds[nfd].fd = e; fds[nfd].events = POLLIN; fds[nfd].revents = 0; /* EVIOCGKEY 上一状态初始化为当前按下位图:启动时已按住的键不误判为新按下 */ memset(prev_key[nfd], 0, sizeof prev_key[nfd]); if (ioctl(e, EVIOCGKEY(sizeof prev_key[nfd]), prev_key[nfd]) < 0) memset(prev_key[nfd], 0, sizeof prev_key[nfd]); nfd++; } } int overlay_was = 0; /* §10.5:上一圈是否处于叠加抑制(进入沿复位触摸状态机用) */ while (g_thread_run) { if (nfd == 0) { usleep(20000); continue; } int r = poll(fds, nfd, 20); /* §10.5 叠加让位:通知中心叠加会话期(标记存在)排空并丢弃本进程事件副本——面板输入 * 由 launcher 接管,App 不得再消费(防面板点击被 App 双重触发);EVIOCGKEY 位图静默 * 对齐(不产生边沿语义/系统键动作,解除后不会把面板期按键误判为新边沿)。 */ if (overlay_input_suppressed()) { if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */ overlay_was = 1; t_down = 0; t_press_valid = 0; t_origin_valid = 0; t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1; t_long_fired = 0; } if (r > 0) for (int i = 0; i < nfd; i++) { if (!(fds[i].revents & POLLIN)) continue; unsigned char junk[64]; while (read(fds[i].fd, junk, sizeof junk) > 0) { /* 整批丢弃 */ } } for (int i = 0; i < nfd; i++) { unsigned char cur[KEY_MAX / 8 + 1]; if (ioctl(fds[i].fd, EVIOCGKEY(sizeof cur), cur) == 0) memcpy(prev_key[i], cur, sizeof cur); } continue; } overlay_was = 0; if (r < 0) { if (errno != EINTR) usleep(20000); } else if (r > 0) { for (int i = 0; i < nfd; i++) { if (!(fds[i].revents & POLLIN)) continue; unsigned char ev[16]; while (read(fds[i].fd, ev, sizeof ev) == (ssize_t)sizeof ev) { 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) { if (t_down) touch_update(val, t_cy); /* 锁原点+更新当前(t_down 时才驱动) */ else t_cx = val; /* 左滑进度实时上报(视觉条):原点确实在左缘(t_dxt_dx)——grow/回缩全程由「非全屏已验证的同一条路径」驱动,全屏仅 * 垂直位置 hy 不同(hy=meow_ui_statusbar_height()),别无全屏特判。 * 抬手触发仍由 touch_release 判定;这里只报过程(事件载荷 b=当前X)。 * t_dx 是本次按下后首个有效 ABS 锁定的真实按下点(非残留),且每次抬手已 * 清空,杜绝「中部点一下出条 / 向左划也出条」。 */ if (t_down && t_origin_valid && t_dx >= 0 && t_dx < EDGE_W && t_cx > t_dx) push_ev(MEOW_EV_SWIPE_PROGRESS, t_dx, t_cx); } else if (code == ABS_Y || code == ABS_MT_POSITION_Y) { if (t_down) touch_update(t_cx, val); else t_cy = val; } } else if (type == EV_KEY) { if (code == BTN_TOUCH) { if (val && !t_down) touch_press(t_cx, t_cy); else if (!val && t_down) touch_release(); } else if (code == MEOW_KEY_POWER) { /* M2.55(接 M2.24 纪律):电源=息屏/亮屏/长按菜单,由内核/sys_server/ * watchdog 统一处理,SDK 不动、也不下发 APP。维持现状。 */ } else if (code == MEOW_KEY_VOLUMEDOWN || code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 19) { /* 系统键(音量/媒体):真实物理键可能不进事件流,另经 EVIOCGKEY 状态轮询 * 兜底(poll_sys_keys_eviocgkey);双源经 sys_key_press/sys_key_release 去重, * 一次按下只一步系统语义 + 一次 MEOW_EV_KEY 下发(App on_key 不得映射)。 */ if (val == 1) sys_key_press(code); else if (val == 0) sys_key_release(code); } else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */ push_ev(MEOW_EV_KEY, code, 1); } else if (val == 0) { /* 抬起 */ push_ev(MEOW_EV_KEY, code, 0); } } } } } /* EVIOCGKEY 系统键状态轮询:无论事件流是否投递都跑(真实物理键可能不进事件流, * 与 launcher kp_poll 同源的按键位图读取兜底;事件流 + 位图双源在 sys_key_press 去重)。 */ for (int i = 0; i < nfd; i++) poll_sys_keys_eviocgkey(fds[i].fd, prev_key[i]); } for (int i = 0; i < nfd; i++) close(fds[i].fd); return NULL; } void meow_input_start(void) { if (g_thread_started) return; g_thread_started = 1; g_thread_run = 1; pthread_create(&g_thread, NULL, input_thread, NULL); } /* ---- 事件循环 ---- */ void meow_ui_request_exit(void) { g_exit = 1; } /* M2.71-bug5:App 活动信号(节流 ≤2/s 写标记文件,限 I/O)。 * 仅真机写(宿主/qemu 无 launcher 消费,避免噪音);launcher 前台 app 循环消费它重置息屏计时。 */ static uint64_t g_activity_last_ms = 0; void meow_ui_activity_ping(void) { uint64_t now = now_ms(); if (now - g_activity_last_ms < 500) return; /* 节流:任意交互连续触摸也只 ~2 次/s */ g_activity_last_ms = now; if (!meow_gfx_is_device()) return; /* 离屏/宿主:无 launcher 消费 */ int fd = open(MEOW_APP_ACTIVITY_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd >= 0) { if (write(fd, "1", 1) < 0) { } close(fd); } } void meow_ui_run(const meow_ui_events *ev) { if (ev) { g_events = *ev; g_events_set = 1; } g_exit = 0; meow_input_start(); last_tick_ms = now_ms(); startup_cooldown_until = now_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */ while (!g_exit) { /* agent-119:实时触摸按压/按住/释放 → 即时重绘命中按钮区(不等 app 重绘)。 * IME 打开时软键盘为覆盖层且拦截触摸,不驱动背后按钮视觉,跳过。 * §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动;启动窗口不触发)。 */ if (!meow_ime_active()) { meow_widget_touch_refresh(); if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll(); } else if (meow_ime_mode_pinyin()) { /* 拼音 IME 打开:喂长按轮询(长按退格清空字段)。英文模式不喂(保持既有行为)。 */ if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll(); } meow_input_ev e; int got = 0; while (meow_input_pop(&e)) { if (g_exit) break; /* 已请求退出:不再分发剩余事件,杜绝「退出后又触发」的偶发再进/退 */ got = 1; meow_ui_activity_ping(); /* M2.71-bug5:任意交互事件都喂 launcher 重置息屏计时(节流) */ int in_startup = (now_ms() < startup_cooldown_until); /* M2.22(C) */ if (e.type == MEOW_EV_TAP) { if (in_startup) continue; /* 启动窗口也吞 TAP:旧手势残留点按(如上一次左滑抬起) * 在进入瞬间命中 UI 按钮(如全屏按钮)会误触发——丢弃。 */ /* 抬手未达标:清视觉条 */ meow_ui_swipe_hint(-1); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); if (meow_ime_active() && meow_ime_handle_event(&e)) continue; /* §6.1 触摸分流:③ 系统全局(IME/状态栏…已在上面消费) → ② App 全局控件 * (命中返回 1=消费,否则落到 ① active Activity 页面控件)。 */ if (meow_ui_global_hit(e.a, e.b)) continue; if (g_events_set && g_events.on_tap) g_events.on_tap(e.a, e.b); } else if (e.type == MEOW_EV_KEY) { if (g_events_set && g_events.on_key) g_events.on_key(e.a, e.b); } else if (e.type == MEOW_EV_SWIPE) { if (in_startup) continue; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */ /* 抬手达标:清视觉条(左滑返回),触发行消失 */ meow_ui_swipe_hint(-1); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); /* 拼音 IME 候选栏横滑翻页(英文模式 handle_event 返回 0 → 沿用既有行为)。 */ if (meow_ime_active() && meow_ime_handle_event(&e)) continue; /* A-0 全局左缘返回(方案A): * - APP 注册 on_swipe → APP 接管,自行决定(meow_ui_request_exit / 页内返回); * - APP 未注册 on_swipe 且非全屏(sb_active)→ SDK 默认退出 app(返回=退出); * - 全屏/沉浸 → 无系统返回手势,仅 APP 自己处理。 * 扩展点:阶段D M3 导航栈落地后,「默认退出」升级为「按 nav_stack 回退」。 */ int sb_active = meow_ui_sys_statusbar_active(); if (g_events_set && g_events.on_swipe) { g_events.on_swipe(e.a, e.b, 0); } else if (e.a == MEOW_SWIPE_LEFT && sb_active) { meow_ui_request_exit(); } } else if (e.type == MEOW_EV_NOTIF_PULL) { if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */ /* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发 * APP on_swipe;路由到系统钩子(App 进程只检测+经 IPC 上报 MSG_NOTIF_OPEN, * 通知中心由 launcher 渲染,见 FRAMEWORK_DESIGN §10.5)。 * 起点若同时落在左缘,拖动期可能已画过 MEOW_EV_SWIPE_PROGRESS 左缘指示条, * 抬手定判为 NOTIF_PULL 后须清掉(否则通知中心打开后残留返回条)。 */ meow_ui_swipe_hint(-1); meow_ui_sys_notif_pull(e.a); /* e.a = 手势按下原点 Y */ } else if (e.type == MEOW_EV_TOUCH_DOWN || e.type == MEOW_EV_TOUCH_MOVE) { /* M2.71-bug2/6:拖动过程(跟手)。IME 弹出时触摸优先喂键盘(键盘覆盖层拦截, * on_touch_* 丢弃,避免按到键盘却在背后控件上锁拖拽);启动窗口也吞(防残留点按 * 误锁拖拽)。模态(dialog/sheet)期间本主循环被阻塞,天然不在此分发。 * IME 打开时先把压点喂给拼音(候选栏滑动翻页判定 g_ime_swipe_y),再丢弃。 */ if (in_startup) continue; if (meow_ime_active()) { meow_ime_handle_event(&e); continue; } if (g_events_set && g_events.on_touch_down && e.type == MEOW_EV_TOUCH_DOWN) g_events.on_touch_down(e.a, e.b); if (g_events_set && g_events.on_touch_move && e.type == MEOW_EV_TOUCH_MOVE) g_events.on_touch_move(e.a, e.b); got = 1; /* 交互事件:不睡眠,保持跟手帧同步 */ } else if (e.type == MEOW_EV_SWIPE_PROGRESS) { if (in_startup) continue; /* 启动窗口进度条静音 */ int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */ /* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。 * 全屏/收合下「左缘返回手势保留」,顶部动态提示条(meow_ui_swipe_hint)照常绘制 * (与 agent-108:不再以 meow_ui_sys_statusbar_active()(全屏/隐藏=0)开关,否则 * 全屏时滑条完全不显示)。条位 y=meow_ui_statusbar_height():非全屏=状态栏下沿 * (28)、全屏=顶部、收合=提示线下缘(MUI_SBAR_COLLAPSE_H),与提示线不重叠加—— * 全屏与非全屏共用同一条绘制/进度路径,只 hy 不同,无全屏特有的坐标/方向/最大值逻辑。 * APP 若注册 on_swipe_progress 仍可自绘(系统条照常画;x_start+dx==当前X,幂等)。 */ meow_ui_swipe_hint(xc); if (g_events_set && g_events.on_swipe_progress) g_events.on_swipe_progress(x0, dx); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); } else if (e.type == MEOW_EV_LONG_PRESS) { /* §6.3 通用长按:按下 ≥ 阈值未抬手触发。坐标=触发时刻按下点(App 用 * meow_widget_hit/meow_listview_hit 判定命中控件/行)。已注册回调则分发; * 其抬手已被 touch_release 抑制(不再产生 TAP/SWIPE),此处无需吞后续 tap。 */ if (in_startup) continue; /* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */ if (meow_ime_active() && meow_ime_handle_event(&e)) continue; if (g_long_press_cb) g_long_press_cb(e.a, e.b); } } uint64_t now = now_ms(); if (g_events_set && g_events.on_tick) { if (now - last_tick_ms >= 16) { last_tick_ms = now; g_events.on_tick(); } } /* A-0 全局状态栏 1s 轻刷:电量/WiFi/音量 + 时钟。时钟每秒变 → 重画顶条并上屏, * 即使 APP 空闲(未主动 flush)也保持时钟走动。 * 「同一个状态栏」闭环:仅系统进程(launcher)刷新顶条;App 进程不渲染状态栏, * 此处不再 flush 顶条(避免把 launcher 叠画的系统顶条冲成 App 背景)。数据仍刷新 * (meow_ui_sys_refresh 无副作用),App 侧 inset/热区不受影响。 */ static uint64_t last_sys_ms = 0; if (now - last_sys_ms >= 1000) { last_sys_ms = now; meow_ui_sys_refresh(); if (meow_ui_sys_render_enabled() && meow_ui_sys_statusbar_active()) meow_ui_flush_region(0, 0, meow_ui_width(), MUI_SBAR_H); } if (meow_ime_active()) { /* 软键盘覆盖层随帧绘制(30fps 节流,省 CPU) */ static uint64_t last_ime_ms = 0; uint64_t n = now_ms(); if (n - last_ime_ms >= 33) { last_ime_ms = n; meow_ime_draw(); meow_ui_flush(); } if (!got) usleep(16000); } else if (!got) { usleep(16000); } } } /* M2.71-IME-fix:供 meow_ui_ime.c 的阻塞 IME 循环周期性喂 App on_tick。 * 阻塞 IME 循环不经过 meow_ui_run,无法靠主循环喂 tick,故在此开独立入口; * 与 meow_ui_run 共用 last_tick_ms(二者互斥运行,无竞争)。调用方在 meow_ime_draw 之前调用, * 使 App 重绘先进 back_buf、IME 面板随后覆盖(IME 始终最上,不被 App 重绘盖住)。 */ void meow_ui_feed_tick(void) { if (!(g_events_set && g_events.on_tick)) return; uint64_t now = now_ms(); if (now - last_tick_ms < 16) return; last_tick_ms = now; g_events.on_tick(); }