/* ui/lock/lock.c — 锁屏页(ACT_LOCK):APP 启动 PIN(页内自绘数字键盘)。 * * 流程(用户原话「允许用户在APP设置中设置APP启动 PIN」): * - lock_view_should_show() = logic/pin 的 mr_pin_enabled();app.c 据此在启动时先 push * 本页,验证通过前书库不进入/不绘制(view 注册表只画已进入的页,widget 还按 owner 过滤); * - 本页 on_enter 建 12 键数字键盘(1-9 / 退格·0·确认,owner=ACT_LOCK)。**不再调系统 * 输入法**:真机 goodix 抬手长尾会让 MEOW_IME_NUM 打开即被「点背景=取消」关掉,表现为 * 「点好几下才弹键盘」;页内键盘完全离线、无 IME 会话,首次点按必响应; * - 4-8 位数字(logic/pin 的 MR_PIN_MIN_LEN/MAX_LEN),顶部只显示星号/圆点数位,不回显明文; * 满 4 位后「确认」才可用;错误 → 清空 + 提示;通过 → page_pop 回书库; * - 长按「退格」清空已输入(不做惩罚性计数/锁定);SDK 长按是 opt-in,需 app.c 在锁屏 * 为顶层页时注册 meow_ui_set_long_press_cb(事件仍先走本页 view 分发),否则真机上 * MEOW_EV_LONG_PRESS 不会产生、长按只会退化成单删一位; * - 左缘返回**必须**由本页 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c 的 * on_swipe;否则 app.c 对非 library 页无条件 page_pop,PIN 会被绕过);TAP/LONG_PRESS * 也一律消费,物理键不接(返回 0,交 app.c 的 on_key 仅日志);屏底 28px 上滑热区 * 不被键盘覆盖(键盘底边留 28+12px); * - PIN 未启用时 app.c 直接 push 书库,本页不会被进入。 * * 安全口径(诚实标注):PIN 防的是随手翻看,不是安全边界——TF 卡可拔出直读,配置目录 * 可改。存储为加盐迭代哈希(logic/pin.c),不引入加密库;锁屏文案也如实告知用户。 * * 控件归属(§6.1,M3 桩注释①):on_create 时 activity 栈为空,控件一律在 on_enter 建。 * 键位顺序(宿主测试依赖):1..9、退格、0、确认 = 下标 0..11。 */ #include "tuku_internal.h" #include "logic/pin.h" #include #include #define LK_KEY_COUNT 12 #define LK_GRID_COLS 3 #define LK_GRID_ROWS 4 #define LK_GAP 10 /* 键间距(≥8;触摸目标互不粘连) */ #define LK_KEY_MIN_H 64 /* 最小键高 */ #define LK_KEY_MAX_H 96 #define LK_KEY_MAX_W 160 #define LK_SIDE 24 /* 键盘左右留白 */ #define LK_BOTTOM_HOT 28 /* 屏底上滑回桌面热区:键盘不得覆盖 */ #define LK_BOTTOM_PAD 12 /* 再留一点呼吸,避免贴住热区 */ #define LK_HEAD_MIN 220 /* 键盘上方留给标题/圆点/提示的最小高度 */ /* 键下标 = 创建序(宿主测试依赖此序)。 */ enum { LK_K1 = 0, LK_K9 = 8, LK_KBACK = 9, LK_K0 = 10, LK_KOK = 11 }; static mr_page_ctrl g_ctrl; static meow_widget *g_keys[LK_KEY_COUNT]; static char g_pin[MR_PIN_MAX_LEN + 1]; static int g_pin_len; static char g_msg[64]; static int g_msg_err; /* meow_view on_event 的事件码:公共伞头未导出(sdk/meow_ui_internal.h: * MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3 / MEOW_EV_LONG_PRESS=8,值稳定),按契约镜像。 */ #define MR_VEV_TAP 2 #define MR_VEV_SWIPE 3 #define MR_VEV_LONG_PRESS 8 int lock_view_should_show(void) { return mr_pin_enabled(); } /* ============================================================ */ /* 几何(480×720:键 ~137×96,四行 + 屏底热区留白) */ /* ============================================================ */ static void lock_keypad_geom(int *x0, int *y0, int *kw, int *kh) { int w = meow_ui_width(); int h = meow_ui_height(); int sb = meow_ui_statusbar_height(); int bottom = h - LK_BOTTOM_HOT - LK_BOTTOM_PAD; int top_min = sb + LK_HEAD_MIN; int kw_ = (w - 2 * LK_SIDE - (LK_GRID_COLS - 1) * LK_GAP) / LK_GRID_COLS; if (kw_ > LK_KEY_MAX_W) kw_ = LK_KEY_MAX_W; if (kw_ < LK_KEY_MIN_H) kw_ = LK_KEY_MIN_H; int kh_ = (bottom - top_min - (LK_GRID_ROWS - 1) * LK_GAP) / LK_GRID_ROWS; if (kh_ > LK_KEY_MAX_H) kh_ = LK_KEY_MAX_H; if (kh_ < LK_KEY_MIN_H) kh_ = LK_KEY_MIN_H; *kw = kw_; *kh = kh_; *x0 = (w - (LK_GRID_COLS * kw_ + (LK_GRID_COLS - 1) * LK_GAP)) / 2; *y0 = bottom - (LK_GRID_ROWS * kh_ + (LK_GRID_ROWS - 1) * LK_GAP); if (*y0 < top_min) *y0 = top_min; /* 极小屏兜底;480×720 不会触发 */ } static void lock_key_rect(int idx, int *x, int *y, int *w, int *h) { int x0, y0, kw, kh; lock_keypad_geom(&x0, &y0, &kw, &kh); int row = idx / LK_GRID_COLS, col = idx % LK_GRID_COLS; *x = x0 + col * (kw + LK_GAP); *y = y0 + row * (kh + LK_GAP); *w = kw; *h = kh; } /* ============================================================ */ /* PIN 状态 */ /* ============================================================ */ static void lock_refresh(void) { meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); } /* 「确认」仅在 ≥4 位时可用;其余情况保持禁用(SDK 按状态机变淡且不响应命中)。 */ static void lock_sync_confirm(void) { if (g_keys[LK_KOK]) meow_widget_set_enabled(g_keys[LK_KOK], g_pin_len >= MR_PIN_MIN_LEN); } static void lock_clear_digits(void) { g_pin_len = 0; g_pin[0] = '\0'; lock_sync_confirm(); } static void lock_append_digit(int d) { if (g_pin_len >= MR_PIN_MAX_LEN) return; /* 8 位上限:多余按键忽略 */ g_pin[g_pin_len++] = (char)('0' + d); g_pin[g_pin_len] = '\0'; g_msg[0] = '\0'; g_msg_err = 0; lock_sync_confirm(); lock_refresh(); } static void lock_backspace(void) { if (g_pin_len > 0) g_pin[--g_pin_len] = '\0'; g_msg[0] = '\0'; g_msg_err = 0; lock_sync_confirm(); lock_refresh(); } static void lock_clear_all(void) { lock_clear_digits(); g_msg[0] = '\0'; g_msg_err = 0; lock_refresh(); } static void lock_confirm(void) { if (g_pin_len < MR_PIN_MIN_LEN) { snprintf(g_msg, sizeof g_msg, "请输入至少 %d 位数字", MR_PIN_MIN_LEN); g_msg_err = 1; lock_refresh(); return; } if (!mr_pin_verify(g_pin)) { /* 与设置页同一校验入口(logic/pin) */ meow_log(MEOW_LOG_INFO, "lock: pin wrong (len=%d), buffer cleared", g_pin_len); snprintf(g_msg, sizeof g_msg, "PIN 错误,请重试"); g_msg_err = 1; lock_clear_digits(); lock_refresh(); return; } g_msg[0] = '\0'; g_msg_err = 0; meow_log(MEOW_LOG_INFO, "lock: pin ok"); meow_view_page_pop(); /* 通过 → 回书库(ACT_LIBRARY on_enter) */ } /* ============================================================ */ /* 布局/绘制 */ /* ============================================================ */ static void lock_draw(meow_view *v, const meow_theme *th, void *ctx) { (void)v; (void)ctx; int w = meow_ui_width(); int h = meow_ui_height(); int sb = meow_ui_statusbar_height(); int x0, y0, kw, kh; lock_keypad_geom(&x0, &y0, &kw, &kh); (void)x0; (void)kw; (void)kh; /* 自绘整页底色:验证前不露任何书库内容(本页是顶层唯一 entered 页)。 */ meow_ui_fill_rect(0, sb, w, h - sb, th->bg); int title_y = sb + 36; meow_ui_text_center(w / 2, title_y, "图库已锁定", th->font_title, th->onSurface); /* 已输入位数:星号=已输入,点=空位;不回显明文。 */ char dots[MR_PIN_MAX_LEN * 2 + 1]; int n = 0; for (int i = 0; i < MR_PIN_MAX_LEN; i++) { dots[n++] = (i < g_pin_len) ? '*' : '.'; if (i + 1 < MR_PIN_MAX_LEN) dots[n++] = ' '; } dots[n] = '\0'; int dots_y = title_y + meow_ui_text_height(th->font_title) + 18; meow_ui_text_center(w / 2, dots_y, dots, th->font_title, g_pin_len > 0 ? th->primary : th->onSurfaceVariant); int status_y = dots_y + meow_ui_text_height(th->font_title) + 8; if (g_msg[0]) meow_ui_text_center(w / 2, status_y, g_msg, th->font_body, g_msg_err ? th->error : th->primary); else meow_ui_text_center(w / 2, status_y, "输入 4-8 位数字后点确认", th->font_body, th->onSurfaceVariant); /* 安全口径(如实告知),紧贴键盘上方。 */ int note_y = y0 - 54; meow_ui_text_center(w / 2, note_y, "PIN 仅用于防止随手查看,", th->font_body, th->onSurfaceVariant); meow_ui_text_center(w / 2, note_y + 24, "TF 卡可被直接读取,不构成安全边界。", th->font_body, th->onSurfaceVariant); } static void lock_place(meow_view *v) { int w = meow_ui_width(); int h = meow_ui_height(); v->x = 0; v->y = 0; v->w = w; v->h = h; for (int i = 0; i < LK_KEY_COUNT; i++) { int x, y, kw, kh; lock_key_rect(i, &x, &y, &kw, &kh); if (g_keys[i]) meow_widget_set_rect(g_keys[i], x, y, kw, kh); } } /* ============================================================ */ /* 事件 */ /* ============================================================ */ static int lock_on_event(meow_view *v, int ev, int a, int b, void *ctx) { (void)v; (void)ctx; /* 左缘返回(以及任何滑动)一律吞掉:锁屏不允许退出 App,也不允许绕过 PIN。 */ if (ev == MR_VEV_SWIPE) return 1; if (ev == MR_VEV_LONG_PRESS) { /* 长按「退格」= 清空(可选增强;其余长按也只吞掉,不透传其它页)。 */ if (meow_widget_hit(g_keys[LK_KBACK], a, b)) lock_clear_all(); return 1; } if (ev == MR_VEV_TAP) { for (int i = 0; i < LK_KEY_COUNT; i++) { if (!g_keys[i] || !meow_widget_hit(g_keys[i], a, b)) continue; if (i <= LK_K9) lock_append_digit(i + 1); /* 1..9 */ else if (i == LK_KBACK) lock_backspace(); else if (i == LK_K0) lock_append_digit(0); else lock_confirm(); return 1; } return 1; /* 锁屏页消费所有点击(含空白区),不向 App 其它页透传 */ } return 0; /* 物理键交 app.c 的 on_key 仅日志(不映射数字/确认) */ } /* ============================================================ */ /* view 生命周期 */ /* ============================================================ */ static void lock_page_enter(meow_view *v, void *ctx) { ((mr_page_ctrl *)ctx)->active = 1; meow_ui_set_fullscreen(0); meow_ui_set_page_bg(meow_theme_get()->bg); g_pin_len = 0; g_pin[0] = '\0'; g_msg[0] = '\0'; g_msg_err = 0; static const char *const labels[LK_KEY_COUNT] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "退格", "0", "确认", }; for (int i = 0; i < LK_KEY_COUNT; i++) { if (g_keys[i]) continue; /* page_push 已先 meow_ui_activity_push(ACT_LOCK) → 控件 owner 正确。 */ int variant = (i == LK_KOK) ? MEOW_BTN_FILLED : (i == LK_KBACK) ? MEOW_BTN_OUTLINED : MEOW_BTN_TONAL; g_keys[i] = meow_btn_create_variant(0, 0, 96, LK_KEY_MIN_H, labels[i], variant); if (g_keys[i]) meow_view_add_widget(v, g_keys[i]); } lock_place(v); lock_sync_confirm(); meow_log(MEOW_LOG_INFO, "lock: pad ready (12 keys, screen %dx%d)", meow_ui_width(), meow_ui_height()); mr_nav_report(); meow_ui_paint_full(); } static void lock_page_leave(meow_view *v, void *ctx) { (void)v; ((mr_page_ctrl *)ctx)->active = 0; } static void lock_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; for (int i = 0; i < LK_KEY_COUNT; i++) g_keys[i] = NULL; /* 控件随 meow_view_free 释放(v->widgets) */ } meow_view *lock_view_create(void) { static const meow_view_hooks h = { .on_enter = lock_page_enter, .on_leave = lock_page_leave, .on_destroy = lock_page_destroy, .on_draw = lock_draw, .on_event = lock_on_event, }; return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl); }