Files
MeowOS-For-R3P2/patch_v2/sdk/meow_ui_input.c
T

692 lines
37 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <poll.h>
#include <pthread.h>
#include <sys/ioctl.h>
/* 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;
}
/* ---- 系统按键语义(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++;
}
}
while (g_thread_run) {
if (nfd == 0) { usleep(20000); continue; }
int r = poll(fds, nfd, 20);
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_dx<EDGE_W) 且 向右移动
* (t_cx>t_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();
}