Merge branch 'feat/mangareader-settings-and-pin-lock'
This commit is contained in:
commit
8cbb240931
12 files changed
+1742
-53
No files matched your search
+1
-1
@@ -12,7 +12,7 @@ SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
|||||||
|
|
||||||
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \
|
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \
|
||||||
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
||||||
logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c \
|
logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c logic/pin.c \
|
||||||
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
|
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
|
||||||
ls -la picbrowser
|
ls -la picbrowser
|
||||||
file picbrowser
|
file picbrowser
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/* logic/pin.c — APP 启动 PIN:加盐迭代哈希 + meow_config 持久化(纯 C)。
|
||||||
|
*
|
||||||
|
* 设计取舍(见 pin.h 头注释):PIN 防随手翻看,不是安全边界。本文件不引入加密库,
|
||||||
|
* 只做「每次设置新随机盐 + FNV-1a 迭代 1000 轮」的轻量哈希,避免明文 PIN 直接躺在
|
||||||
|
* 配置里。配置落 TF 数据目录,卡被拔走即可读/改——这是产品口径接受的局限。
|
||||||
|
*
|
||||||
|
* 依赖面:只 include include/meow_ui_config.h(get/set/remove/int 声明),不 include
|
||||||
|
* meow_ui.h、不碰 UI,宿主 gcc 可直接编译 + 测试存根替换配置层。
|
||||||
|
*/
|
||||||
|
#include "pin.h"
|
||||||
|
#include "include/meow_ui_config.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define PIN_KEY_ENABLED "pin.enabled"
|
||||||
|
#define PIN_KEY_HASH "pin.hash"
|
||||||
|
#define PIN_KEY_SALT "pin.salt"
|
||||||
|
|
||||||
|
#define PIN_HEX_LEN 16 /* 64-bit 摘要/盐的 hex 字符数 */
|
||||||
|
#define PIN_HEX_BUF 17 /* 16 + NUL */
|
||||||
|
#define PIN_ROUNDS 1000 /* 迭代轮数(防直接反查,不防暴力枚举) */
|
||||||
|
#define PIN_SALT_BYTES 8 /* 8 字节盐 → 16 位 hex */
|
||||||
|
|
||||||
|
/* ---------- 哈希 ---------- */
|
||||||
|
|
||||||
|
/* FNV-1a(64-bit)逐段吸收,返回新状态。 */
|
||||||
|
static uint64_t fnv1a_update(uint64_t h, const void *data, size_t n)
|
||||||
|
{
|
||||||
|
const unsigned char *p = (const unsigned char *)data;
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
h ^= (uint64_t)p[i];
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pin → 16 位 hex 摘要:FNV-1a(salt, pin) 起手,再做 PIN_ROUNDS 轮
|
||||||
|
* 「上一轮摘要(大端 8 字节)+ 盐」的 FNV-1a。同一 PIN 每次设置因盐不同而摘要不同。 */
|
||||||
|
static void pin_digest(const char *pin, const char *salt, char out[PIN_HEX_BUF])
|
||||||
|
{
|
||||||
|
uint64_t h = 1469598103934665603ULL; /* FNV-1a 64-bit offset basis */
|
||||||
|
h = fnv1a_update(h, salt, strlen(salt));
|
||||||
|
h = fnv1a_update(h, pin, strlen(pin));
|
||||||
|
for (int r = 0; r < PIN_ROUNDS; r++) {
|
||||||
|
unsigned char be[8];
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
be[i] = (unsigned char)((h >> (8 * (7 - i))) & 0xFF);
|
||||||
|
h = fnv1a_update(h, be, sizeof be);
|
||||||
|
h = fnv1a_update(h, salt, strlen(salt));
|
||||||
|
}
|
||||||
|
snprintf(out, PIN_HEX_BUF, "%016llx", (unsigned long long)h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成 16 位 hex 随机盐:优先 /dev/urandom;无设备时用时间/pid/地址/计数器做降级熵
|
||||||
|
* (宿主测试与裁剪系统仍可用——降级盐的强度如实标注:不构成密码学随机)。 */
|
||||||
|
static void pin_gen_salt(char out[PIN_HEX_BUF])
|
||||||
|
{
|
||||||
|
unsigned char b[PIN_SALT_BYTES];
|
||||||
|
int ok = 0;
|
||||||
|
|
||||||
|
FILE *f = fopen("/dev/urandom", "rb");
|
||||||
|
if (f) {
|
||||||
|
ok = (fread(b, 1, sizeof b, f) == sizeof b);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
static uint64_t ctr;
|
||||||
|
uint64_t x = (uint64_t)time(NULL)
|
||||||
|
^ ((uint64_t)clock() << 21)
|
||||||
|
^ ((uint64_t)getpid() << 40)
|
||||||
|
^ (uint64_t)(uintptr_t)&b
|
||||||
|
^ (++ctr * 0x9E3779B97F4A7C15ULL);
|
||||||
|
for (int i = 0; i < PIN_SALT_BYTES; i++)
|
||||||
|
b[i] = (unsigned char)((x >> (8 * i)) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < PIN_SALT_BYTES; i++)
|
||||||
|
snprintf(out + i * 2, 3, "%02x", b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 配置层小包装 ---------- */
|
||||||
|
static int cfg_get(const char *key, char *out, int outsz)
|
||||||
|
{
|
||||||
|
return meow_config_get(key, out, outsz) == MEOW_CFG_OK ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cfg_set(const char *key, const char *val)
|
||||||
|
{
|
||||||
|
return meow_config_set(key, val) == MEOW_CFG_OK ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int is_hex16(const char *s)
|
||||||
|
{
|
||||||
|
if (!s) return 0;
|
||||||
|
for (int i = 0; i < PIN_HEX_LEN; i++) {
|
||||||
|
char c = s[i];
|
||||||
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
|
||||||
|
}
|
||||||
|
return s[PIN_HEX_LEN] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 公共 API ---------- */
|
||||||
|
|
||||||
|
int mr_pin_valid(const char *pin)
|
||||||
|
{
|
||||||
|
if (!pin) return 0;
|
||||||
|
int n = 0;
|
||||||
|
while (pin[n]) {
|
||||||
|
if (pin[n] < '0' || pin[n] > '9') return 0;
|
||||||
|
n++;
|
||||||
|
if (n > MR_PIN_MAX_LEN) return 0;
|
||||||
|
}
|
||||||
|
return n >= MR_PIN_MIN_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_enabled(void)
|
||||||
|
{
|
||||||
|
char hash[PIN_HEX_BUF + 8];
|
||||||
|
char salt[PIN_HEX_BUF + 8];
|
||||||
|
|
||||||
|
if (meow_config_int(PIN_KEY_ENABLED, 0) != 1) return 0;
|
||||||
|
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
|
||||||
|
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
|
||||||
|
return is_hex16(hash) && is_hex16(salt); /* 配置残缺 → fail-closed */
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_set(const char *pin)
|
||||||
|
{
|
||||||
|
char salt[PIN_HEX_BUF];
|
||||||
|
char hash[PIN_HEX_BUF];
|
||||||
|
char old_salt[PIN_HEX_BUF + 8];
|
||||||
|
int had_old_salt;
|
||||||
|
|
||||||
|
if (!mr_pin_valid(pin)) return MR_PIN_ERR_FORMAT;
|
||||||
|
|
||||||
|
pin_gen_salt(salt);
|
||||||
|
pin_digest(pin, salt, hash);
|
||||||
|
had_old_salt = (cfg_get(PIN_KEY_SALT, old_salt, (int)sizeof old_salt) == 0);
|
||||||
|
|
||||||
|
/* 先写盐与摘要、最后打开 enabled:中途失败只会留下「未启用」,不会出现
|
||||||
|
* enabled=1 但缺少盐/哈希的半残状态。摘要写失败时把盐还原成旧值,使已启用的旧
|
||||||
|
* PIN 仍可校验(否则「新盐+旧哈希」会让新旧 PIN 都验不过)。 */
|
||||||
|
if (cfg_set(PIN_KEY_SALT, salt) != 0) return MR_PIN_ERR_STORE;
|
||||||
|
if (cfg_set(PIN_KEY_HASH, hash) != 0) {
|
||||||
|
if (had_old_salt) cfg_set(PIN_KEY_SALT, old_salt);
|
||||||
|
else meow_config_remove(PIN_KEY_SALT);
|
||||||
|
return MR_PIN_ERR_STORE;
|
||||||
|
}
|
||||||
|
if (cfg_set(PIN_KEY_ENABLED, "1") != 0) {
|
||||||
|
/* 重设时 enabled 本来就是 "1":新盐/哈希已一致生效,按成功返回;
|
||||||
|
* 首次开启时 enabled 仍为 0 → 如实报落盘失败。 */
|
||||||
|
return meow_config_int(PIN_KEY_ENABLED, 0) == 1 ? MR_PIN_OK : MR_PIN_ERR_STORE;
|
||||||
|
}
|
||||||
|
return MR_PIN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_verify(const char *pin)
|
||||||
|
{
|
||||||
|
char hash[PIN_HEX_BUF + 8];
|
||||||
|
char salt[PIN_HEX_BUF + 8];
|
||||||
|
char calc[PIN_HEX_BUF];
|
||||||
|
|
||||||
|
if (!mr_pin_valid(pin)) return 0;
|
||||||
|
if (!mr_pin_enabled()) return 0;
|
||||||
|
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
|
||||||
|
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
|
||||||
|
|
||||||
|
pin_digest(pin, salt, calc);
|
||||||
|
unsigned diff = 0;
|
||||||
|
for (int i = 0; i < PIN_HEX_LEN; i++)
|
||||||
|
diff |= (unsigned)((unsigned char)hash[i] ^ (unsigned char)calc[i]);
|
||||||
|
diff |= (unsigned)(hash[PIN_HEX_LEN] != '\0');
|
||||||
|
return diff == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_change(const char *old_pin, const char *new_pin)
|
||||||
|
{
|
||||||
|
if (!mr_pin_enabled()) return MR_PIN_ERR_STATE;
|
||||||
|
if (!mr_pin_valid(new_pin)) return MR_PIN_ERR_FORMAT;
|
||||||
|
if (!mr_pin_verify(old_pin)) return MR_PIN_ERR_WRONG;
|
||||||
|
return mr_pin_set(new_pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_disable(void)
|
||||||
|
{
|
||||||
|
meow_config_remove(PIN_KEY_HASH);
|
||||||
|
meow_config_remove(PIN_KEY_SALT);
|
||||||
|
if (cfg_set(PIN_KEY_ENABLED, "0") != 0) return MR_PIN_ERR_STORE;
|
||||||
|
return MR_PIN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_get_salt(char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return 0;
|
||||||
|
out[0] = '\0';
|
||||||
|
if (cfg_get(PIN_KEY_SALT, out, outsz) != 0) return 0;
|
||||||
|
return out[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_pin_get_hash(char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return 0;
|
||||||
|
out[0] = '\0';
|
||||||
|
if (cfg_get(PIN_KEY_HASH, out, outsz) != 0) return 0;
|
||||||
|
return out[0] != '\0';
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* logic/pin.h — APP 启动 PIN(纯 C;宿主可裸测)。
|
||||||
|
*
|
||||||
|
* 定位(诚实标注):PIN 只挡「随手翻看」——TF 卡可拔出直读,数据目录明文可改,
|
||||||
|
* 故不引入加密库、不宣称安全边界。存储口径:加盐迭代哈希(FNV-1a ×1000 轮,
|
||||||
|
* 每次设置新随机盐,hex 输出),落在 meow_config_*(App 自有 KV → TF MeowData)。
|
||||||
|
* 盐/哈希只做「不知道 PIN 无法直接重算」的轻量门槛,防不了离线暴力或改配置。
|
||||||
|
*
|
||||||
|
* 配置键(meow_config_*,值均为字符串):
|
||||||
|
* pin.enabled = "1"/"0"
|
||||||
|
* pin.hash = 16 位 hex(64-bit 摘要)
|
||||||
|
* pin.salt = 16 位 hex(每次设置重新生成)
|
||||||
|
* enabled=1 但 hash/salt 缺失或格式不合法时一律按「未启用」处理(fail-closed)。
|
||||||
|
*
|
||||||
|
* 本模块只依赖 include/meow_ui_config.h 的三个声明(get/set/remove/int),不碰 UI;
|
||||||
|
* 宿主测试链 test_pin.c 内的 meow_config_* 存根即可,无需 SDK/设备。
|
||||||
|
*/
|
||||||
|
#ifndef MR_PIN_H
|
||||||
|
#define MR_PIN_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MR_PIN_MIN_LEN 4 /* 允许的最短位数 */
|
||||||
|
#define MR_PIN_MAX_LEN 8 /* 允许的最长位数 */
|
||||||
|
|
||||||
|
/* 返回码(set/change/disable)。verify 用 1/0 表示对/错,不返回错误码。 */
|
||||||
|
#define MR_PIN_OK 0 /* 成功 */
|
||||||
|
#define MR_PIN_ERR_FORMAT (-1) /* PIN 非法:非 4-8 位纯数字(NULL/空/符号/超长) */
|
||||||
|
#define MR_PIN_ERR_STATE (-2) /* 状态不满足:change 时尚未启用 */
|
||||||
|
#define MR_PIN_ERR_WRONG (-3) /* change 时原 PIN 校验失败 */
|
||||||
|
#define MR_PIN_ERR_STORE (-4) /* meow_config_* 落盘失败(无数据目录/只读/IO) */
|
||||||
|
|
||||||
|
/* 4-8 位纯 ASCII 数字('0'-'9')→ 1;其余(含 NULL、空串、符号、全角数字)→ 0。 */
|
||||||
|
int mr_pin_valid(const char *pin);
|
||||||
|
|
||||||
|
/* 当前是否启用:1=已启用且 hash/salt 完整合法;0=未启用或配置损坏(fail-closed)。 */
|
||||||
|
int mr_pin_enabled(void);
|
||||||
|
|
||||||
|
/* 设置/重设 PIN(生成新盐 + 迭代哈希并立即落盘)。成功即生效(下次启动进锁屏)。
|
||||||
|
* 返回 MR_PIN_OK / MR_PIN_ERR_FORMAT / MR_PIN_ERR_STORE。 */
|
||||||
|
int mr_pin_set(const char *pin);
|
||||||
|
|
||||||
|
/* 校验:1=匹配;0=不匹配/未启用/输入非法(不区分,避免侧信道式提示)。 */
|
||||||
|
int mr_pin_verify(const char *pin);
|
||||||
|
|
||||||
|
/* 修改:先校验 old,再以新盐重设 new。失败时旧 PIN 保持有效(先校验后写盘)。
|
||||||
|
* 返回 MR_PIN_OK / MR_PIN_ERR_STATE / MR_PIN_ERR_FORMAT / MR_PIN_ERR_WRONG / MR_PIN_ERR_STORE。 */
|
||||||
|
int mr_pin_change(const char *old_pin, const char *new_pin);
|
||||||
|
|
||||||
|
/* 关闭 PIN:删 hash/salt 并把 enabled 置 0(幂等;调用方已通过锁屏验证时无需再输 PIN)。
|
||||||
|
* 返回 MR_PIN_OK / MR_PIN_ERR_STORE。 */
|
||||||
|
int mr_pin_disable(void);
|
||||||
|
|
||||||
|
/* 诊断/测试用:读回当前 hex 盐/哈希。返回 1=读成功,0=未设。 */
|
||||||
|
int mr_pin_get_salt(char *out, int outsz);
|
||||||
|
int mr_pin_get_hash(char *out, int outsz);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* MR_PIN_H */
|
||||||
+49
-2
@@ -1,14 +1,61 @@
|
|||||||
/* logic/reader.c — 阅读方向状态(默认 RTL;M4 的阅读器直接复用)。 */
|
/* logic/reader.c — 阅读方向状态(默认 RTL;M4/M6 的阅读器直接复用)。
|
||||||
|
*
|
||||||
|
* M5 评审 P1 修复:方向设置要「下次启动生效」。最小持久化(不新增任何方向状态):
|
||||||
|
* - mr_reader_set_direction 成功切换后 best-effort 写 meow_config("reader.dir","rtl"/"ltr");
|
||||||
|
* 写失败只打 WARN,不阻断切换(本次运行仍生效);
|
||||||
|
* - mr_reader_get_direction 首次调用惰性读一次配置恢复;无配置/值非法 → 默认 RTL;
|
||||||
|
* 之后走纯内存快路径(不再读配置)。
|
||||||
|
* 依赖面:只 include 配置/日志两个模块头(不含 UI);宿主测试用
|
||||||
|
* test/settings_reader_stub.c 顶替 meow_config_* 与 meow_log。
|
||||||
|
*/
|
||||||
#include "reader.h"
|
#include "reader.h"
|
||||||
|
#include "include/meow_ui_config.h"
|
||||||
|
#include "include/meow_ui_log.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define MR_DIR_CFG_KEY "reader.dir"
|
||||||
|
#define MR_DIR_CFG_LTR "ltr"
|
||||||
|
#define MR_DIR_CFG_RTL "rtl"
|
||||||
|
|
||||||
static int g_direction = MR_DIR_RTL;
|
static int g_direction = MR_DIR_RTL;
|
||||||
|
static int g_restored; /* 0=尚未从配置恢复(首次 get/set 时读一次) */
|
||||||
|
|
||||||
|
static void reader_restore_once(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
|
||||||
|
if (g_restored) return;
|
||||||
|
g_restored = 1; /* 无配置也只读一次:之后纯内存 */
|
||||||
|
if (meow_config_get(MR_DIR_CFG_KEY, buf, sizeof buf) != MEOW_CFG_OK) return;
|
||||||
|
if (strcmp(buf, MR_DIR_CFG_LTR) == 0) g_direction = MR_DIR_LTR;
|
||||||
|
else if (strcmp(buf, MR_DIR_CFG_RTL) == 0) g_direction = MR_DIR_RTL;
|
||||||
|
/* 其它值:忽略,保持默认 RTL */
|
||||||
|
}
|
||||||
|
|
||||||
int mr_reader_get_direction(void)
|
int mr_reader_get_direction(void)
|
||||||
{
|
{
|
||||||
|
reader_restore_once();
|
||||||
return g_direction;
|
return g_direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
void mr_reader_set_direction(int dir)
|
void mr_reader_set_direction(int dir)
|
||||||
{
|
{
|
||||||
if (dir == MR_DIR_RTL || dir == MR_DIR_LTR) g_direction = dir;
|
if (dir != MR_DIR_RTL && dir != MR_DIR_LTR) return;
|
||||||
|
|
||||||
|
reader_restore_once(); /* 先恢复:避免「没 get 过就 set」把配置里的旧方向当默认值 */
|
||||||
|
if (g_direction == dir) return; /* 无变化不写盘 */
|
||||||
|
g_direction = dir;
|
||||||
|
|
||||||
|
if (meow_config_set(MR_DIR_CFG_KEY, dir == MR_DIR_LTR ? MR_DIR_CFG_LTR : MR_DIR_CFG_RTL)
|
||||||
|
!= MEOW_CFG_OK)
|
||||||
|
meow_log(MEOW_LOG_WARN, "reader.dir: save failed (best-effort, kept in memory)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef MR_READER_TEST_HOOKS
|
||||||
|
void mr_reader_test_reset(void)
|
||||||
|
{
|
||||||
|
g_direction = MR_DIR_RTL;
|
||||||
|
g_restored = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
+46
-5
@@ -1,13 +1,54 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# run_tests.sh — 宿主 gcc 构建并运行图库 logic 层测试(不链 SDK、不需要设备)。
|
# run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
|
||||||
|
# - test_library:书库扫描/自然排序/阅读方向(46 项)
|
||||||
|
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
|
||||||
|
# 坏值/非法值/只读一次/写失败只告警)
|
||||||
|
# - test_pin:启动 PIN 格式/设置/校验/修改/禁用/残缺配置/落盘失败(logic/pin.c 为纯 C,
|
||||||
|
# 只引用 meow_config_* 声明,测试内以内存 KV 存根顶替 → 不链 SDK)。
|
||||||
|
# - test_settings_lock_host(可选):设置页/锁屏与 SDK view/activity/widget 接线的宿主集成
|
||||||
|
# 验证(owner 归属 + 锁屏 SWIPE 拦截)。需要 SDK **源码树**(不只 meow_ui.a);
|
||||||
|
# 交付环境只有库+头时自动 SKIP。
|
||||||
|
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
|
||||||
|
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
gcc -Wall -Wextra -O1 -I. \
|
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||||
test/test_library.c logic/library.c logic/reader.c \
|
|
||||||
|
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||||
|
test/test_library.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||||
-o test/test_library
|
-o test/test_library
|
||||||
./test/test_library
|
./test/test_library
|
||||||
gcc -Wall -Wextra -O1 -I. \
|
|
||||||
|
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||||
test/test_reader_core.c logic/library.c logic/reader.c \
|
test/test_reader_core.c logic/library.c logic/reader.c \
|
||||||
logic/reader/page_source.c logic/reader/nav.c \
|
logic/reader/page_source.c logic/reader/nav.c test/settings_reader_stub.c \
|
||||||
-o test/test_reader_core
|
-o test/test_reader_core
|
||||||
./test/test_reader_core
|
./test/test_reader_core
|
||||||
|
|
||||||
|
gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
|
||||||
|
test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||||
|
-o test/test_settings_reader_persist
|
||||||
|
./test/test_settings_reader_persist
|
||||||
|
|
||||||
|
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||||
|
test/test_pin.c logic/pin.c \
|
||||||
|
-o test/test_pin
|
||||||
|
./test/test_pin
|
||||||
|
|
||||||
|
if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||||
|
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||||
|
test/test_settings_lock_host.c ui/settings/settings.c ui/lock/lock.c \
|
||||||
|
logic/pin.c logic/reader.c \
|
||||||
|
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||||
|
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||||
|
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||||
|
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||||
|
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||||
|
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||||
|
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||||
|
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||||
|
-pthread -lm -o test/test_settings_lock_host
|
||||||
|
./test/test_settings_lock_host
|
||||||
|
else
|
||||||
|
echo "SKIP test_settings_lock_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* settings_reader_stub.c — 宿主测试用 meow_config_* 与 meow_log 最小实现(内存 KV)。
|
||||||
|
*
|
||||||
|
* 只为让 logic/reader.c 在不链 SDK 的宿主测试里可编译/可断言:
|
||||||
|
* - meow_config_get/set:8 个键的内存表,可注入写失败(模拟无卡/只读);
|
||||||
|
* - meow_log:只统计 WARN 次数(验证「写失败只告警、不阻断切换」)。
|
||||||
|
* 与 test_pin.c 自带存根同思路,但抽成共用文件供多个 reader 相关测试链接。
|
||||||
|
*/
|
||||||
|
#include "settings_reader_stub.h"
|
||||||
|
#include "include/meow_ui_config.h"
|
||||||
|
#include "include/meow_ui_log.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define ST_MAX 8
|
||||||
|
static struct { char key[64]; char val[64]; int used; } g_kv[ST_MAX];
|
||||||
|
static int g_fail_writes;
|
||||||
|
static int g_warns;
|
||||||
|
|
||||||
|
static int kv_find(const char *key)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ST_MAX; i++)
|
||||||
|
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mr_test_cfg_clear(void)
|
||||||
|
{
|
||||||
|
memset(g_kv, 0, sizeof g_kv);
|
||||||
|
g_fail_writes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mr_test_cfg_put(const char *key, const char *val)
|
||||||
|
{
|
||||||
|
if (!key || !key[0]) return;
|
||||||
|
int i = kv_find(key);
|
||||||
|
if (i < 0) {
|
||||||
|
for (i = 0; i < ST_MAX; i++) if (!g_kv[i].used) break;
|
||||||
|
if (i >= ST_MAX) return;
|
||||||
|
g_kv[i].used = 1;
|
||||||
|
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
|
||||||
|
}
|
||||||
|
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_test_cfg_get(const char *key, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return 0;
|
||||||
|
out[0] = '\0';
|
||||||
|
int i = kv_find(key);
|
||||||
|
if (i < 0) return 0;
|
||||||
|
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mr_test_cfg_fail_writes(int on)
|
||||||
|
{
|
||||||
|
g_fail_writes = on ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_test_log_warns(void)
|
||||||
|
{
|
||||||
|
return g_warns;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mr_test_log_clear(void)
|
||||||
|
{
|
||||||
|
g_warns = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 被测代码调用的 SDK 接口 ---- */
|
||||||
|
int meow_config_set(const char *key, const char *val)
|
||||||
|
{
|
||||||
|
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
|
||||||
|
if (g_fail_writes) return MEOW_CFG_ERR_IO;
|
||||||
|
mr_test_cfg_put(key, val);
|
||||||
|
return MEOW_CFG_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_config_get(const char *key, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||||
|
out[0] = '\0';
|
||||||
|
if (!mr_test_cfg_get(key, out, outsz)) return MEOW_CFG_ERR_NOENT;
|
||||||
|
return MEOW_CFG_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void meow_log(meow_log_level lvl, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
(void)fmt;
|
||||||
|
if (lvl >= MEOW_LOG_WARN) g_warns++;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* settings_reader_stub.h — 宿主测试用 meow_config_* 与 meow_log 存根(logic/reader.c 的依赖面)。
|
||||||
|
*
|
||||||
|
* 内存 KV(不落盘)+ 测试辅助:读/写/清配置、注入写失败、统计 WARN 次数。
|
||||||
|
* 供 test_library、test_reader_core、test_settings_reader_persist 共用——
|
||||||
|
* 这几个测试都不链 SDK,reader.c 的持久化失败/成功路径靠这里可控。
|
||||||
|
*/
|
||||||
|
#ifndef SETTINGS_READER_STUB_H
|
||||||
|
#define SETTINGS_READER_STUB_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void mr_test_cfg_clear(void); /* 清空全部键 */
|
||||||
|
void mr_test_cfg_put(const char *key, const char *val); /* 直接写入(可注入坏值) */
|
||||||
|
int mr_test_cfg_get(const char *key, char *out, int outsz); /* 1=键存在 */
|
||||||
|
void mr_test_cfg_fail_writes(int on); /* 1=后续 set 全部失败 */
|
||||||
|
int mr_test_log_warns(void); /* meow_log(WARN..) 次数 */
|
||||||
|
void mr_test_log_clear(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* SETTINGS_READER_STUB_H */
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
/* test_pin.c — 启动 PIN(logic/pin)宿主测试:纯 C,不链 SDK。
|
||||||
|
*
|
||||||
|
* 用内存 KV 存根顶替 meow_config_get/set/int/remove(与 SDK 语义一致:
|
||||||
|
* 未设 → MEOW_CFG_ERR_NOENT;写失败可由 g_fail_write 注入)。
|
||||||
|
* 覆盖:格式校验边界(4/8 位合法;3/9 位、空串、NULL、符号、全角数字非法)、
|
||||||
|
* 设置/校验、错 PIN、重设换盐、change 全分支、disable、配置残缺 fail-closed、
|
||||||
|
* 落盘失败、落盘布局(pin.enabled/hash/salt)。
|
||||||
|
*
|
||||||
|
* 编译(在 tuku/ 下):
|
||||||
|
* gcc -Wall -Wextra -O1 -I. -I"$SDK" test/test_pin.c logic/pin.c -o test/test_pin
|
||||||
|
* 或直接 test/run_tests.sh。
|
||||||
|
*/
|
||||||
|
#include "logic/pin.h"
|
||||||
|
#include "include/meow_ui_config.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define KV_MAX 8
|
||||||
|
static struct { char key[64]; char val[128]; int used; } g_kv[KV_MAX];
|
||||||
|
static int g_fail_write; /* 1=模拟全部落盘失败 */
|
||||||
|
static const char *g_fail_key; /* 非 NULL=只让该键写失败(模拟半途失败) */
|
||||||
|
|
||||||
|
static int kv_find(const char *key)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < KV_MAX; i++)
|
||||||
|
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void kv_clear(void)
|
||||||
|
{
|
||||||
|
memset(g_kv, 0, sizeof g_kv);
|
||||||
|
g_fail_write = 0;
|
||||||
|
g_fail_key = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_config_set(const char *key, const char *val)
|
||||||
|
{
|
||||||
|
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
|
||||||
|
if (g_fail_write || (g_fail_key && strcmp(g_fail_key, key) == 0)) return MEOW_CFG_ERR_IO;
|
||||||
|
int i = kv_find(key);
|
||||||
|
if (i < 0) {
|
||||||
|
for (i = 0; i < KV_MAX; i++) if (!g_kv[i].used) break;
|
||||||
|
if (i >= KV_MAX) return MEOW_CFG_ERR_IO;
|
||||||
|
g_kv[i].used = 1;
|
||||||
|
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
|
||||||
|
}
|
||||||
|
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
|
||||||
|
return MEOW_CFG_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_config_get(const char *key, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||||
|
out[0] = '\0';
|
||||||
|
int i = kv_find(key);
|
||||||
|
if (i < 0) return MEOW_CFG_ERR_NOENT;
|
||||||
|
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
|
||||||
|
return MEOW_CFG_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_config_int(const char *key, int def)
|
||||||
|
{
|
||||||
|
char buf[32];
|
||||||
|
char *end = NULL;
|
||||||
|
long v;
|
||||||
|
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
|
||||||
|
v = strtol(buf, &end, 10);
|
||||||
|
if (end == buf) return def;
|
||||||
|
return (int)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_config_bool(const char *key, int def)
|
||||||
|
{
|
||||||
|
return meow_config_int(key, def) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void meow_config_remove(const char *key)
|
||||||
|
{
|
||||||
|
int i = kv_find(key);
|
||||||
|
if (i >= 0) g_kv[i].used = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *meow_config_dir(void) { return "/tmp/meowcfg_test"; }
|
||||||
|
|
||||||
|
static int g_pass, g_fail;
|
||||||
|
#define CHECK(cond, ...) do { \
|
||||||
|
if (cond) { g_pass++; } \
|
||||||
|
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static int is_hex16(const char *s)
|
||||||
|
{
|
||||||
|
if (!s) return 0;
|
||||||
|
if (strlen(s) != 16) return 0;
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_format(void)
|
||||||
|
{
|
||||||
|
CHECK(mr_pin_valid(NULL) == 0, "NULL invalid");
|
||||||
|
CHECK(mr_pin_valid("") == 0, "empty invalid");
|
||||||
|
CHECK(mr_pin_valid("123") == 0, "3 digits invalid");
|
||||||
|
CHECK(mr_pin_valid("1234") == 1, "4 digits valid (min)");
|
||||||
|
CHECK(mr_pin_valid("12345") == 1, "5 digits valid");
|
||||||
|
CHECK(mr_pin_valid("12345678") == 1, "8 digits valid (max)");
|
||||||
|
CHECK(mr_pin_valid("123456789") == 0, "9 digits invalid");
|
||||||
|
CHECK(mr_pin_valid("12a4") == 0, "letter invalid");
|
||||||
|
CHECK(mr_pin_valid("12 4") == 0, "space invalid");
|
||||||
|
CHECK(mr_pin_valid(" 1234") == 0, "leading space invalid");
|
||||||
|
CHECK(mr_pin_valid("1234 ") == 0, "trailing space invalid");
|
||||||
|
CHECK(mr_pin_valid("-123") == 0, "sign invalid");
|
||||||
|
/* 全角数字(UTF-8 EF BC 9x)不是 ASCII 数字 */
|
||||||
|
CHECK(mr_pin_valid("\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\xEF\xBC\x94") == 0,
|
||||||
|
"fullwidth digits invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_default_state(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_enabled() == 0, "default disabled");
|
||||||
|
CHECK(mr_pin_verify("1234") == 0, "verify on disabled = 0");
|
||||||
|
CHECK(mr_pin_set("123") == MR_PIN_ERR_FORMAT, "set short -> format error");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "failed set keeps disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_set_verify(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set 1234 ok");
|
||||||
|
CHECK(mr_pin_enabled() == 1, "enabled after set");
|
||||||
|
CHECK(mr_pin_verify("1234") == 1, "verify correct");
|
||||||
|
CHECK(mr_pin_verify("4321") == 0, "verify wrong");
|
||||||
|
CHECK(mr_pin_verify("123") == 0, "verify short");
|
||||||
|
CHECK(mr_pin_verify("12345") == 0, "verify longer");
|
||||||
|
CHECK(mr_pin_verify("") == 0, "verify empty");
|
||||||
|
CHECK(mr_pin_verify(NULL) == 0, "verify NULL");
|
||||||
|
CHECK(mr_pin_verify("12a4") == 0, "verify non-digit");
|
||||||
|
|
||||||
|
char salt[32] = "", hash[32] = "";
|
||||||
|
CHECK(mr_pin_get_salt(salt, sizeof salt) == 1, "salt present");
|
||||||
|
CHECK(mr_pin_get_hash(hash, sizeof hash) == 1, "hash present");
|
||||||
|
CHECK(is_hex16(salt), "salt is 16 hex (got %s)", salt);
|
||||||
|
CHECK(is_hex16(hash), "hash is 16 hex (got %s)", hash);
|
||||||
|
CHECK(strcmp(hash, "1234") != 0, "hash is not the raw pin");
|
||||||
|
|
||||||
|
/* 落盘布局:enabled="1"(明文 PIN 不落盘:配置里只有 16 位 hex 摘要与盐) */
|
||||||
|
char buf[64] = "";
|
||||||
|
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "1") == 0,
|
||||||
|
"pin.enabled=1 on disk");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reset_new_salt(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set first");
|
||||||
|
char salt1[32] = "", hash1[32] = "";
|
||||||
|
mr_pin_get_salt(salt1, sizeof salt1);
|
||||||
|
mr_pin_get_hash(hash1, sizeof hash1);
|
||||||
|
|
||||||
|
CHECK(mr_pin_set("5678") == MR_PIN_OK, "set second (overwrite)");
|
||||||
|
char salt2[32] = "", hash2[32] = "";
|
||||||
|
mr_pin_get_salt(salt2, sizeof salt2);
|
||||||
|
mr_pin_get_hash(hash2, sizeof hash2);
|
||||||
|
CHECK(strcmp(salt1, salt2) != 0, "new salt on each set");
|
||||||
|
CHECK(strcmp(hash1, hash2) != 0, "new hash on each set");
|
||||||
|
CHECK(mr_pin_verify("5678") == 1, "new pin verifies");
|
||||||
|
CHECK(mr_pin_verify("1234") == 0, "old pin rejected after overwrite");
|
||||||
|
|
||||||
|
/* 同一 PIN 重设:盐不同 → 摘要也不同(防跨设备彩虹表/同值比对)。 */
|
||||||
|
CHECK(mr_pin_set("5678") == MR_PIN_OK, "re-set same pin");
|
||||||
|
char salt3[32] = "", hash3[32] = "";
|
||||||
|
mr_pin_get_salt(salt3, sizeof salt3);
|
||||||
|
mr_pin_get_hash(hash3, sizeof hash3);
|
||||||
|
CHECK(strcmp(salt2, salt3) != 0, "salt differs for same pin");
|
||||||
|
CHECK(strcmp(hash2, hash3) != 0, "hash differs for same pin");
|
||||||
|
CHECK(mr_pin_verify("5678") == 1, "same pin still verifies");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_change(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change while disabled -> state");
|
||||||
|
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
|
||||||
|
CHECK(mr_pin_change("9999", "5678") == MR_PIN_ERR_WRONG, "change wrong old -> wrong");
|
||||||
|
CHECK(mr_pin_verify("1234") == 1, "old still valid after wrong-change");
|
||||||
|
CHECK(mr_pin_verify("5678") == 0, "new not applied after wrong-change");
|
||||||
|
|
||||||
|
CHECK(mr_pin_change("1234", "12") == MR_PIN_ERR_FORMAT, "change to short new -> format");
|
||||||
|
CHECK(mr_pin_verify("1234") == 1, "old still valid after bad-new");
|
||||||
|
|
||||||
|
CHECK(mr_pin_change("1234", "12345678") == MR_PIN_OK, "change ok (8 digits)");
|
||||||
|
CHECK(mr_pin_verify("12345678") == 1, "new valid after change");
|
||||||
|
CHECK(mr_pin_verify("1234") == 0, "old invalid after change");
|
||||||
|
CHECK(mr_pin_enabled() == 1, "still enabled after change");
|
||||||
|
|
||||||
|
CHECK(mr_pin_change(NULL, "0000") == MR_PIN_ERR_WRONG, "change NULL old -> wrong");
|
||||||
|
CHECK(mr_pin_change("12345678", NULL) == MR_PIN_ERR_FORMAT, "change NULL new -> format");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_disable(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_disable() == MR_PIN_OK, "disable while disabled is idempotent");
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup for disable");
|
||||||
|
CHECK(mr_pin_disable() == MR_PIN_OK, "disable ok");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "disabled after disable");
|
||||||
|
CHECK(mr_pin_verify("1234") == 0, "verify fails after disable");
|
||||||
|
|
||||||
|
char buf[32] = "";
|
||||||
|
CHECK(meow_config_get("pin.hash", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "hash key removed");
|
||||||
|
CHECK(meow_config_get("pin.salt", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "salt key removed");
|
||||||
|
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "0") == 0,
|
||||||
|
"enabled=0 on disk");
|
||||||
|
|
||||||
|
/* 禁用后可重新开启(新盐)。 */
|
||||||
|
CHECK(mr_pin_set("8765") == MR_PIN_OK, "re-enable after disable");
|
||||||
|
CHECK(mr_pin_verify("8765") == 1, "re-enabled pin verifies");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_corrupt_state(void)
|
||||||
|
{
|
||||||
|
/* enabled=1 但缺 hash → fail-closed */
|
||||||
|
kv_clear();
|
||||||
|
meow_config_set("pin.enabled", "1");
|
||||||
|
meow_config_set("pin.salt", "0011223344556677");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "enabled without hash -> fail closed");
|
||||||
|
CHECK(mr_pin_verify("1234") == 0, "verify fail closed");
|
||||||
|
|
||||||
|
/* hash 长度/字符非法 */
|
||||||
|
kv_clear();
|
||||||
|
meow_config_set("pin.enabled", "1");
|
||||||
|
meow_config_set("pin.salt", "0011223344556677");
|
||||||
|
meow_config_set("pin.hash", "zz");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "garbage hash -> fail closed");
|
||||||
|
|
||||||
|
/* salt 非法 */
|
||||||
|
kv_clear();
|
||||||
|
meow_config_set("pin.enabled", "1");
|
||||||
|
meow_config_set("pin.salt", "");
|
||||||
|
meow_config_set("pin.hash", "0011223344556677");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "empty salt -> fail closed");
|
||||||
|
|
||||||
|
/* change 在残缺状态 → state 错误(不误判为已启用) */
|
||||||
|
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change on corrupt -> state");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_store_failure(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
g_fail_write = 1;
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_ERR_STORE, "set with failing store -> store error");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "still disabled after store failure");
|
||||||
|
g_fail_write = 0;
|
||||||
|
|
||||||
|
/* 写盘失败时不得把状态改成「已启用」——set 先写盐/摘要、最后写 enabled。 */
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set ok after store recovers");
|
||||||
|
g_fail_write = 1;
|
||||||
|
CHECK(mr_pin_disable() == MR_PIN_ERR_STORE, "disable with failing store -> store error");
|
||||||
|
g_fail_write = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_partial_write(void)
|
||||||
|
{
|
||||||
|
/* 摘要写失败:把盐还原成旧值,已启用的旧 PIN 仍可校验(不会新旧都验不过)。 */
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
|
||||||
|
g_fail_key = "pin.hash";
|
||||||
|
CHECK(mr_pin_set("5678") == MR_PIN_ERR_STORE, "hash write failure -> store error");
|
||||||
|
CHECK(mr_pin_enabled() == 1, "still enabled after partial write");
|
||||||
|
CHECK(mr_pin_verify("1234") == 1, "old PIN still valid after partial write");
|
||||||
|
CHECK(mr_pin_verify("5678") == 0, "new PIN not applied after partial write");
|
||||||
|
g_fail_key = NULL;
|
||||||
|
|
||||||
|
/* 重设时 enabled 已是 "1":该键写失败不影响一致性 → 按成功返回。 */
|
||||||
|
g_fail_key = "pin.enabled";
|
||||||
|
CHECK(mr_pin_set("4321") == MR_PIN_OK, "enabled-already-1 write failure is consistent");
|
||||||
|
CHECK(mr_pin_verify("4321") == 1, "new PIN active");
|
||||||
|
g_fail_key = NULL;
|
||||||
|
|
||||||
|
/* 首次开启时 enabled 写失败 → 未启用(fail-closed)。 */
|
||||||
|
kv_clear();
|
||||||
|
g_fail_key = "pin.enabled";
|
||||||
|
CHECK(mr_pin_set("1111") == MR_PIN_ERR_STORE, "first enable with enabled-write failure");
|
||||||
|
CHECK(mr_pin_enabled() == 0, "not enabled after enabled-write failure");
|
||||||
|
g_fail_key = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_verify_does_not_mutate(void)
|
||||||
|
{
|
||||||
|
kv_clear();
|
||||||
|
CHECK(mr_pin_set("2468") == MR_PIN_OK, "setup");
|
||||||
|
char salt1[32] = "", salt2[32] = "";
|
||||||
|
mr_pin_get_salt(salt1, sizeof salt1);
|
||||||
|
(void)mr_pin_verify("0000");
|
||||||
|
(void)mr_pin_verify("2468");
|
||||||
|
mr_pin_get_salt(salt2, sizeof salt2);
|
||||||
|
CHECK(strcmp(salt1, salt2) == 0, "verify does not change salt");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_format();
|
||||||
|
test_default_state();
|
||||||
|
test_set_verify();
|
||||||
|
test_reset_new_salt();
|
||||||
|
test_change();
|
||||||
|
test_disable();
|
||||||
|
test_corrupt_state();
|
||||||
|
test_store_failure();
|
||||||
|
test_partial_write();
|
||||||
|
test_verify_does_not_mutate();
|
||||||
|
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/* test_settings_lock_host.c — 设置页/锁屏与 SDK 骨架的宿主集成测试(可选,需 SDK 源码树)。
|
||||||
|
*
|
||||||
|
* 目的:钉住 M3 骨架评审点名的两条坑(M5 桩注释①/②),这两条只有把真实 SDK
|
||||||
|
* view/activity/widget 链起来才能验证:
|
||||||
|
* ① 非根页控件必须在 on_enter 建(此时 activity 栈顶=本页)——否则 owner 绑到
|
||||||
|
* ACT_LIBRARY,跨页命中泄漏;
|
||||||
|
* ② 锁屏必须在自身 view 的 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c
|
||||||
|
* 的 on_swipe),否则左缘返回会把 ACT_LOCK page_pop 掉、PIN 被绕过。
|
||||||
|
* 另外覆盖:设置页回到书库(topbar 返回)、控件跨进出复用不重建、阅读方向经
|
||||||
|
* mr_reader_set_direction 切换、lock_view_should_show() 随 PIN 状态变化、
|
||||||
|
* 主题模式切换后整帧重绘不崩,以及**真实 MEOW_IME_NUM 键盘**驱动的两条端到端路径:
|
||||||
|
* 锁屏错 PIN(留在锁屏)→ 对 PIN(page_pop 回书库)、设置页两遍输入开启 PIN。
|
||||||
|
*
|
||||||
|
* 编译:见 test/run_tests.sh —— 它把本测试、settings/lock/pin 源码与 SDK 源码
|
||||||
|
* (-DMEOW_UI_HOST_TEST,附 test_host_stub.c 打桩 font/global)一起编;SDK 对象清单在脚本里。
|
||||||
|
* 只发布 meow_ui.a(无 SDK 源码)的交付环境,run_tests.sh 自动 SKIP 本测试。
|
||||||
|
*/
|
||||||
|
#include "tuku_internal.h"
|
||||||
|
#include "logic/pin.h"
|
||||||
|
#include "meow_ui_internal.h" /* meow_widget_owner(宿主测试专用内部头) */
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
static int g_fail;
|
||||||
|
#define CHECK(cond, ...) do { \
|
||||||
|
if (cond) { printf(" PASS\n"); } \
|
||||||
|
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* app.c 的导航上报在测试里不需要网络侧实现。 */
|
||||||
|
void mr_nav_report(void) {}
|
||||||
|
|
||||||
|
static int g_lib_enters;
|
||||||
|
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
|
||||||
|
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
|
||||||
|
|
||||||
|
/* 清掉本次测试写入的三个小配置并删临时目录(meow_config 每键一文件)。 */
|
||||||
|
static void cleanup_cfg(const char *dir)
|
||||||
|
{
|
||||||
|
static const char *keys[3] = { "pin.enabled.cfg", "pin.hash.cfg", "pin.salt.cfg" };
|
||||||
|
char p[512];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
snprintf(p, sizeof p, "%s/%s", dir, keys[i]);
|
||||||
|
unlink(p);
|
||||||
|
}
|
||||||
|
rmdir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 数字键盘注入(宿主没有真实触摸):按 MEOW_IME_NUM 键位投递 TAP,关闭键盘后由被测代码校验。
|
||||||
|
* NUM 布局(sdk/meow_ui_ime.c build_kb):KB_TOP=420;首行 470 是符号行 "-/.:*#+=",
|
||||||
|
* 数字九键行为 518/566/614(x0=70/190/310,键宽 100),底行(ESC|0|退格+OK)为 662;
|
||||||
|
* 点 y<420 的背景=关闭键盘。 */
|
||||||
|
/* ============================================================ */
|
||||||
|
typedef struct { const char *digits; int passes; } pin_job;
|
||||||
|
|
||||||
|
static void key_tap(char d)
|
||||||
|
{
|
||||||
|
int row, col;
|
||||||
|
if (d == '0') { row = 3; col = 1; }
|
||||||
|
else {
|
||||||
|
int v = d - '1';
|
||||||
|
if (v < 0 || v > 8) return;
|
||||||
|
row = v / 3;
|
||||||
|
col = v % 3;
|
||||||
|
}
|
||||||
|
/* 数字行 y=518/566/614(首行 470 是符号行 "-/.:*#+="),底行 662;列心 120/240/360。 */
|
||||||
|
meow_input_inject(MEOW_EV_TAP, 70 + col * 120 + 50, 518 + row * 48 + 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *pin_injector(void *arg)
|
||||||
|
{
|
||||||
|
pin_job *job = (pin_job *)arg;
|
||||||
|
usleep(150000); /* 等 text_input 开键盘并 drain 完残留事件 */
|
||||||
|
for (int p = 0; p < job->passes; p++) {
|
||||||
|
if (p) usleep(300000); /* 两遍输入之间等第二遍开键盘 */
|
||||||
|
for (const char *s = job->digits; *s; s++) {
|
||||||
|
key_tap(*s);
|
||||||
|
usleep(50000);
|
||||||
|
}
|
||||||
|
usleep(50000);
|
||||||
|
meow_input_inject(MEOW_EV_TAP, 240, 100); /* y<420:关闭键盘(非空→校验) */
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lock_try_pin(const char *digits)
|
||||||
|
{
|
||||||
|
int sb = meow_ui_statusbar_height();
|
||||||
|
int cy = sb + (meow_ui_height() - sb) / 2;
|
||||||
|
pin_job job = { digits, 1 };
|
||||||
|
pthread_t th;
|
||||||
|
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
|
||||||
|
meow_view_dispatch_event(MEOW_EV_TAP, 240, cy + 40); /* 点「输入 PIN」按钮 */
|
||||||
|
pthread_join(th, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_tap_pin_row(void){
|
||||||
|
/* 布局:sb + 56 顶栏 + 6 + 段头34 + 方向行64 + 12 + 段头34 → PIN 列表首行中心 +32 */
|
||||||
|
int y = meow_ui_statusbar_height() + 56 + 6 + 34 + 64 + 12 + 34 + 32;
|
||||||
|
pin_job job = { "2468", 2 };
|
||||||
|
pthread_t th;
|
||||||
|
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
|
||||||
|
meow_view_dispatch_event(MEOW_EV_TAP, 200, y);
|
||||||
|
pthread_join(th, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
const char *base = getenv("TMPDIR");
|
||||||
|
if (!base || !base[0]) base = ".";
|
||||||
|
char tmpl[512];
|
||||||
|
snprintf(tmpl, sizeof tmpl, "%s/mr_settings_host_XXXXXX", base);
|
||||||
|
char *data = mkdtemp(tmpl);
|
||||||
|
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||||
|
setenv("MEOW_DATA_DIR", data, 1);
|
||||||
|
|
||||||
|
meow_ui_opts o = { 480, 720 };
|
||||||
|
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||||
|
|
||||||
|
static const meow_view_hooks lh = { .on_enter = lib_enter, .on_leave = lib_leave };
|
||||||
|
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||||||
|
meow_view *settings = settings_view_create();
|
||||||
|
meow_view *lock = lock_view_create();
|
||||||
|
int sb = meow_ui_statusbar_height();
|
||||||
|
|
||||||
|
meow_view_page_push(ACT_LIBRARY);
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "start at library");
|
||||||
|
|
||||||
|
/* ---- 设置页:on_enter 建控件 + owner 归属 ---- */
|
||||||
|
meow_view_page_push(ACT_SETTINGS);
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active");
|
||||||
|
CHECK(settings->nwidgets == 5, "settings built 5 widgets (got %d)", settings->nwidgets);
|
||||||
|
int owner_ok = 1;
|
||||||
|
for (int i = 0; i < settings->nwidgets; i++)
|
||||||
|
if (meow_widget_owner(settings->widgets[i]) != ACT_SETTINGS) owner_ok = 0;
|
||||||
|
CHECK(owner_ok, "settings widgets owned by ACT_SETTINGS (no cross-page leak)");
|
||||||
|
|
||||||
|
/* 布局几何:顶栏贴状态栏、三组列表不越界、页脚在屏内。 */
|
||||||
|
int r_tb[4] = { 0 }, r_about[4] = { 0 };
|
||||||
|
meow_widget_rect(settings->widgets[0], r_tb);
|
||||||
|
meow_widget_rect(settings->widgets[3], r_about);
|
||||||
|
CHECK(r_tb[1] == sb && r_tb[3] == 56, "topappbar sits below statusbar");
|
||||||
|
CHECK(r_about[1] + r_about[3] < meow_ui_height() - 60,
|
||||||
|
"about list leaves room for footer (bottom=%d)", r_about[1] + r_about[3]);
|
||||||
|
printf(" geometry: topbar y=%d, about list y=%d h=%d (screen %dx%d)\n",
|
||||||
|
r_tb[1], r_about[1], r_about[3], meow_ui_width(), meow_ui_height());
|
||||||
|
|
||||||
|
/* 阅读方向行:段头 34 + 行中心。 */
|
||||||
|
int dir_y = sb + 56 + 6 + 34 + 32;
|
||||||
|
int d0 = mr_reader_get_direction();
|
||||||
|
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 120, dir_y) == 1,
|
||||||
|
"tap on direction row consumed by settings on_event");
|
||||||
|
CHECK(mr_reader_get_direction() != d0,
|
||||||
|
"direction toggled through mr_reader_set_direction");
|
||||||
|
|
||||||
|
/* topbar 返回 → page_pop 回书库 */
|
||||||
|
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 22, sb + 28) == 1, "back tap consumed");
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back popped to library");
|
||||||
|
CHECK(g_lib_enters == 2, "library re-entered after pop (enters=%d)", g_lib_enters);
|
||||||
|
|
||||||
|
/* 再次进入:控件复用(不在 on_enter 重复创建/泄漏) */
|
||||||
|
meow_view_page_push(ACT_SETTINGS);
|
||||||
|
CHECK(settings->nwidgets == 5, "settings widgets reused (got %d)", settings->nwidgets);
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active again");
|
||||||
|
|
||||||
|
/* 设置页不消费 SWIPE:交给 app.c 的 page_pop */
|
||||||
|
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 200) == 0,
|
||||||
|
"settings leaves SWIPE to app.c (back = pop)");
|
||||||
|
meow_view_page_pop(); /* 手动模拟 app.c 的返回行为,回到书库 */
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "manual pop back to library");
|
||||||
|
|
||||||
|
/* ---- 锁屏:PIN 状态驱动 + SWIPE 拦截(坑②) ---- */
|
||||||
|
mr_pin_disable();
|
||||||
|
CHECK(lock_view_should_show() == 0, "lock hidden when PIN disabled");
|
||||||
|
CHECK(mr_pin_set("1357") == MR_PIN_OK, "enable PIN");
|
||||||
|
CHECK(lock_view_should_show() == 1, "lock shown when PIN enabled");
|
||||||
|
|
||||||
|
meow_view_page_push(ACT_LOCK);
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LOCK, "lock active");
|
||||||
|
CHECK(lock->nwidgets == 1, "lock built its button on enter (got %d)", lock->nwidgets);
|
||||||
|
CHECK(meow_widget_owner(lock->widgets[0]) == ACT_LOCK, "lock button owned by ACT_LOCK");
|
||||||
|
|
||||||
|
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 240) == 1,
|
||||||
|
"lock consumes SWIPE (left-edge back cannot bypass PIN)");
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after swipe");
|
||||||
|
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 5, 400) == 1, "lock consumes stray taps");
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after stray tap");
|
||||||
|
|
||||||
|
CHECK(mr_pin_verify("1357") == 1, "stored PIN verifies");
|
||||||
|
CHECK(mr_pin_verify("1358") == 0, "wrong PIN rejected");
|
||||||
|
|
||||||
|
/* ---- 锁屏真解锁路径:点按钮 → MEOW_IME_NUM 输入 → 校验 → pop(错误则留在锁屏) ---- */
|
||||||
|
lock_try_pin("1358"); /* 错 PIN:留在锁屏 */
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LOCK, "wrong PIN keeps lock (retry allowed)");
|
||||||
|
lock_try_pin("1357"); /* 正确:page_pop 回书库 */
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "correct PIN pops lock to library");
|
||||||
|
|
||||||
|
/* ---- 设置页两遍输入开启 PIN(MEOW_IME_NUM) ---- */
|
||||||
|
mr_pin_disable();
|
||||||
|
meow_view_page_push(ACT_SETTINGS);
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active for PIN enable");
|
||||||
|
settings_tap_pin_row();
|
||||||
|
CHECK(mr_pin_enabled() == 1, "two-pass entry enabled the PIN");
|
||||||
|
CHECK(mr_pin_verify("2468") == 1, "PIN entered in settings verifies");
|
||||||
|
meow_view_page_pop();
|
||||||
|
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back from settings to library");
|
||||||
|
|
||||||
|
/* 主题切换:app.c listener 整帧 invalidate;页面按 meow_theme_get() 重画不崩。 */
|
||||||
|
meow_theme_set_mode(MEOW_THEME_LIGHT);
|
||||||
|
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||||
|
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||||
|
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||||
|
CHECK(1, "theme switch repaint does not crash");
|
||||||
|
|
||||||
|
mr_pin_disable();
|
||||||
|
meow_view_free(lock);
|
||||||
|
meow_view_free(settings);
|
||||||
|
meow_view_free(lib);
|
||||||
|
meow_ui_shutdown();
|
||||||
|
cleanup_cfg(data);
|
||||||
|
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||||
|
g_fail ? "see failures above" : "integration checks passed");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/* test_settings_reader_persist.c — 阅读方向持久化宿主测试(M5 评审 P1 修复)。
|
||||||
|
*
|
||||||
|
* 被测:logic/reader.c 的 meow_config("reader.dir") 持久化。
|
||||||
|
* 「新进程语义」用 reader.c 的测试钩子 mr_reader_test_reset()(-DMR_READER_TEST_HOOKS)
|
||||||
|
* 模拟:清掉静态恢复标记,配置存根保留——等价于重启后首次 get 从配置恢复。
|
||||||
|
*
|
||||||
|
* 覆盖:
|
||||||
|
* T1 无配置默认 RTL,且 get 不写配置;
|
||||||
|
* T2 set LTR 落盘 + 内存立即生效;T3 reset 后从配置恢复 LTR;
|
||||||
|
* T4 set RTL 落盘 + reset 恢复 RTL;
|
||||||
|
* T5 坏值("sideways")忽略 → 默认 RTL,且 get 不回写坏值;
|
||||||
|
* T6 非法方向(99)忽略且不落盘;
|
||||||
|
* T7 坏值后 set 合法方向会覆盖配置;
|
||||||
|
* T8 配置只读一次(首次 get 后配置再变不影响本次内存态 = 快路径);
|
||||||
|
* T9 写失败:切换仍生效(内存)、只打一条 WARN、配置未变。
|
||||||
|
*
|
||||||
|
* 编译(tuku/ 下,SDK 仅需公共头):
|
||||||
|
* gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
|
||||||
|
* test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||||
|
* -o test/test_settings_reader_persist && ./test/test_settings_reader_persist
|
||||||
|
*/
|
||||||
|
#include "logic/reader.h"
|
||||||
|
#include "test/settings_reader_stub.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* reader.c 在 -DMR_READER_TEST_HOOKS 下导出(正式构建无此符号,故不写进 reader.h,
|
||||||
|
* 避免生产头暴露测试入口)。 */
|
||||||
|
extern void mr_reader_test_reset(void);
|
||||||
|
|
||||||
|
static int g_pass, g_fail;
|
||||||
|
#define CHECK(cond, ...) do { \
|
||||||
|
if (cond) { g_pass++; } \
|
||||||
|
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static int cfg_is(const char *want)
|
||||||
|
{
|
||||||
|
char buf[32] = "";
|
||||||
|
if (!mr_test_cfg_get("reader.dir", buf, (int)sizeof buf)) return 0;
|
||||||
|
return strcmp(buf, want) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cfg_has(const char *key)
|
||||||
|
{
|
||||||
|
char buf[32];
|
||||||
|
return mr_test_cfg_get(key, buf, (int)sizeof buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_default_and_ltr(void)
|
||||||
|
{
|
||||||
|
/* T1 无配置 → 默认 RTL;get 是只读的。 */
|
||||||
|
mr_test_cfg_clear();
|
||||||
|
mr_reader_test_reset();
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T1 default RTL with no config");
|
||||||
|
CHECK(!cfg_has("reader.dir"), "T1 get does not write config");
|
||||||
|
|
||||||
|
/* T2 set LTR:内存立即生效 + 落盘 "ltr"。 */
|
||||||
|
mr_reader_set_direction(MR_DIR_LTR);
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T2 memory updated immediately");
|
||||||
|
CHECK(cfg_is("ltr"), "T2 set LTR persists reader.dir=ltr");
|
||||||
|
|
||||||
|
/* T3 模拟重启 → 从配置恢复 LTR。 */
|
||||||
|
mr_reader_test_reset();
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T3 restore LTR after restart");
|
||||||
|
|
||||||
|
/* T4 反向:set RTL 落盘并恢复。 */
|
||||||
|
mr_reader_set_direction(MR_DIR_RTL);
|
||||||
|
CHECK(cfg_is("rtl"), "T4 set RTL persists reader.dir=rtl");
|
||||||
|
mr_reader_test_reset();
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T4 restore RTL after restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_bad_values(void)
|
||||||
|
{
|
||||||
|
/* T5 坏值忽略 → 默认 RTL,且 get 不改写坏值。 */
|
||||||
|
mr_test_cfg_clear();
|
||||||
|
mr_test_cfg_put("reader.dir", "sideways");
|
||||||
|
mr_reader_test_reset();
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T5 bad value ignored -> default RTL");
|
||||||
|
CHECK(cfg_is("sideways"), "T5 get leaves bad value untouched");
|
||||||
|
|
||||||
|
/* T6 非法方向:忽略且不落盘。 */
|
||||||
|
mr_test_cfg_clear();
|
||||||
|
mr_reader_test_reset();
|
||||||
|
mr_reader_set_direction(99);
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T6 invalid dir ignored (memory)");
|
||||||
|
CHECK(!cfg_has("reader.dir"), "T6 invalid dir writes nothing");
|
||||||
|
|
||||||
|
/* T7 坏值后 set 合法方向 → 覆盖配置并在重启后生效。 */
|
||||||
|
mr_test_cfg_clear();
|
||||||
|
mr_test_cfg_put("reader.dir", "bogus");
|
||||||
|
mr_reader_test_reset();
|
||||||
|
mr_reader_set_direction(MR_DIR_LTR);
|
||||||
|
CHECK(cfg_is("ltr"), "T7 valid set overwrites bad config");
|
||||||
|
mr_reader_test_reset();
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T7 restored after overwrite");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_read_once_and_failure(void)
|
||||||
|
{
|
||||||
|
/* T8 配置只读一次:首次 get 之后配置变化不影响内存(快路径)。 */
|
||||||
|
mr_test_cfg_clear();
|
||||||
|
mr_reader_test_reset();
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T8 first get with no config");
|
||||||
|
mr_test_cfg_put("reader.dir", "ltr");
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T8 config read only once (fast path)");
|
||||||
|
|
||||||
|
/* T9 写失败:切换仍生效、WARN 一条、配置不变。 */
|
||||||
|
mr_test_cfg_clear();
|
||||||
|
mr_reader_test_reset();
|
||||||
|
mr_test_log_clear();
|
||||||
|
mr_test_cfg_fail_writes(1);
|
||||||
|
mr_reader_set_direction(MR_DIR_LTR);
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T9 switch still applies on write failure");
|
||||||
|
CHECK(mr_test_log_warns() == 1, "T9 one WARN on write failure (got %d)", mr_test_log_warns());
|
||||||
|
CHECK(!cfg_has("reader.dir"), "T9 config unchanged on failure");
|
||||||
|
mr_test_cfg_fail_writes(0);
|
||||||
|
mr_reader_set_direction(MR_DIR_RTL);
|
||||||
|
CHECK(cfg_is("rtl") && mr_test_log_warns() == 1, "T9 recovery writes rtl, no extra WARN");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_default_and_ltr();
|
||||||
|
test_bad_values();
|
||||||
|
test_read_once_and_failure();
|
||||||
|
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
+166
-22
@@ -1,49 +1,185 @@
|
|||||||
/* ui/lock/lock.c — 锁屏页(ACT_LOCK)M3 桩 + M5 接缝。
|
/* ui/lock/lock.c — 锁屏页(ACT_LOCK):APP 启动 PIN。
|
||||||
*
|
*
|
||||||
* M5 在此实现「APP 启动 PIN」:lock_view_should_show() 读配置决定是否锁,
|
* 流程(用户原话「允许用户在APP设置中设置APP启动 PIN」):
|
||||||
* 锁屏页负责 PIN 输入/校验后 page_pop 回书库。本批桩恒返回 0(不锁),页面仅可编译占位。
|
* - lock_view_should_show() = logic/pin 的 mr_pin_enabled();app.c 据此在启动时先 push
|
||||||
|
* 本页,验证通过前书库不进入/不绘制(view 注册表只画已进入的页,widget 还按 owner 过滤);
|
||||||
|
* - 本页 on_enter 建「输入 PIN」按钮(owner=ACT_LOCK),点按弹系统数字键盘(MEOW_IME_NUM),
|
||||||
|
* 校验通过 → page_pop 回书库;错误 → 就地提示重试(不做惩罚性锁定/计数);
|
||||||
|
* - 左缘返回**必须**由本页 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c 的
|
||||||
|
* on_swipe;否则 app.c 对非 library 页无条件 page_pop,PIN 会被绕过);
|
||||||
|
* - PIN 未启用时 app.c 直接 push 书库,本页不会被进入。
|
||||||
*
|
*
|
||||||
* 接缝注意(M5):
|
* 安全口径(诚实标注):PIN 防的是随手翻看,不是安全边界——TF 卡可拔出直读,配置目录
|
||||||
* ① meow_view 的 on_create 在 meow_view_create 内运行(此时 activity 栈空);本页是非根页,
|
* 可改。存储为加盐迭代哈希(logic/pin.c),不引入加密库;锁屏文案也如实告知用户。
|
||||||
* 控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式
|
*
|
||||||
* meow_ui_activity_push(ACT_LOCK) → 建控件 → meow_ui_activity_pop()
|
* 控件归属(§6.1,M3 桩注释①):on_create 时 activity 栈为空,控件一律在 on_enter 建。
|
||||||
* (examples/hello/ui/demo/demo.c:515-522);否则 owner 绑到 ACT_LIBRARY,
|
|
||||||
* 锁屏控件会从书库页被命中(§6.1 跨页泄漏)。
|
|
||||||
* ② 锁屏必须经自身 view 的 on_event 消费 SWIPE:meow_view_dispatch_event 先于 App 的
|
|
||||||
* on_swipe(sdk/meow_ui_view.h:83-88);否则 app.c 对非 library 页的左缘滑动无条件
|
|
||||||
* page_pop,PIN 会被绕过。
|
|
||||||
*/
|
*/
|
||||||
#include "tuku_internal.h"
|
#include "tuku_internal.h"
|
||||||
|
#include "logic/pin.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
static mr_page_ctrl g_ctrl;
|
static mr_page_ctrl g_ctrl;
|
||||||
|
|
||||||
|
static meow_widget *g_btn;
|
||||||
|
|
||||||
|
static char g_msg[64];
|
||||||
|
static int g_msg_err;
|
||||||
|
static int g_waiting; /* 1=数字键盘打开中(上部提示改为输入说明) */
|
||||||
|
static char g_ime_pin[MR_PIN_MAX_LEN + 12];
|
||||||
|
|
||||||
|
/* meow_view on_event 的事件码:公共伞头未导出(sdk/meow_ui_internal.h:
|
||||||
|
* MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3,值稳定),此处按契约镜像。 */
|
||||||
|
#define MR_VEV_TAP 2
|
||||||
|
#define MR_VEV_SWIPE 3
|
||||||
|
|
||||||
int lock_view_should_show(void)
|
int lock_view_should_show(void)
|
||||||
{
|
{
|
||||||
return 0; /* M5:读 APP 启动 PIN 配置后返回 1 */
|
return mr_pin_enabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* PIN 输入(系统数字键盘) */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void ime_pin_change(const char *text, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
snprintf(g_ime_pin, sizeof g_ime_pin, "%s", text ? text : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阻塞数字键盘;返回 1=收集到非空文本(关闭键盘即结束/校验)。
|
||||||
|
* SDK 现状:键盘「确定」只置内部 submit 标记不关闭,关闭路径(取消/点背景)又把标记清 0,
|
||||||
|
* 故 meow_ui_text_input 的返回值不可作“确认”判据;文本经 on_change 实时收集,空=取消。 */
|
||||||
|
static int lock_read_pin(char *out, int outsz)
|
||||||
|
{
|
||||||
|
meow_ime_ctx c;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
c.mode = MEOW_IME_NUM;
|
||||||
|
c.maxlen = (int)sizeof g_ime_pin;
|
||||||
|
c.on_change = ime_pin_change;
|
||||||
|
c.ctx = NULL;
|
||||||
|
|
||||||
|
g_ime_pin[0] = '\0';
|
||||||
|
g_msg[0] = '\0';
|
||||||
|
g_msg_err = 0;
|
||||||
|
g_waiting = 1; /* 锁屏上部提示改为输入说明 */
|
||||||
|
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); /* 键盘区背景快照取当前帧 */
|
||||||
|
(void)meow_ui_text_input(&c);
|
||||||
|
g_waiting = 0;
|
||||||
|
|
||||||
|
snprintf(out, (size_t)outsz, "%s", g_ime_pin);
|
||||||
|
return out[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lock_attempt(void)
|
||||||
|
{
|
||||||
|
char pin[32] = "";
|
||||||
|
if (!lock_read_pin(pin, (int)sizeof pin)) {
|
||||||
|
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||||
|
return; /* 关闭键盘未输入:留在锁屏 */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mr_pin_verify(pin)) {
|
||||||
|
snprintf(g_msg, sizeof g_msg, "PIN 错误,请重试");
|
||||||
|
g_msg_err = 1;
|
||||||
|
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_msg[0] = '\0';
|
||||||
|
g_msg_err = 0;
|
||||||
|
meow_view_page_pop(); /* 通过 → 回书库(ACT_LIBRARY on_enter) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 布局/绘制 */
|
||||||
|
/* ============================================================ */
|
||||||
static void lock_draw(meow_view *v, const meow_theme *th, void *ctx)
|
static void lock_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||||||
{
|
{
|
||||||
(void)v; (void)ctx;
|
(void)v; (void)ctx;
|
||||||
int w = meow_ui_width();
|
int w = meow_ui_width();
|
||||||
int h = meow_ui_height();
|
int h = meow_ui_height();
|
||||||
int cy = h / 2;
|
int sb = meow_ui_statusbar_height();
|
||||||
|
int cy = sb + (h - sb) / 2;
|
||||||
|
|
||||||
meow_ui_text_center(w / 2, cy - 80, "LOCKED", th->font_title, th->primary);
|
/* 自绘整页底色:验证前不露任何书库内容(本页是顶层唯一 entered 页)。 */
|
||||||
meow_ui_text_center(w / 2, cy - 36, "Startup PIN (M5)", th->font_body, th->onSurface);
|
meow_ui_fill_rect(0, sb, w, h - sb, th->bg);
|
||||||
meow_ui_text_center(w / 2, cy + 8, "PIN entry will be implemented in M5",
|
|
||||||
th->font_body, th->onSurfaceVariant);
|
meow_ui_text_center(w / 2, cy - 150, "图库已锁定", th->font_title, th->onSurface);
|
||||||
|
if (g_waiting) {
|
||||||
|
meow_ui_text_center(w / 2, cy - 104, "输入 4-8 位数字后关闭键盘", th->font_body,
|
||||||
|
th->onSurface);
|
||||||
|
meow_ui_text_center(w / 2, cy - 76, "空输入 = 取消,回到锁屏", th->font_body,
|
||||||
|
th->onSurfaceVariant);
|
||||||
|
} else {
|
||||||
|
meow_ui_text_center(w / 2, cy - 104, "输入启动 PIN 继续", th->font_body,
|
||||||
|
th->onSurfaceVariant);
|
||||||
|
meow_ui_text_center(w / 2, cy - 76, "4-8 位数字 · 系统数字键盘", th->font_body,
|
||||||
|
th->onSurfaceVariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_msg[0]) {
|
||||||
|
meow_ui_text_center(w / 2, cy - 26, g_msg, th->font_body,
|
||||||
|
g_msg_err ? th->error : th->primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安全口径(如实告知):PIN 不是安全边界。 */
|
||||||
|
meow_ui_text_center(w / 2, h - 86, "PIN 仅用于防止随手查看,", th->font_body,
|
||||||
|
th->onSurfaceVariant);
|
||||||
|
meow_ui_text_center(w / 2, h - 58, "TF 卡可被直接读取,不构成安全边界。", th->font_body,
|
||||||
|
th->onSurfaceVariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void lock_place(meow_view *v)
|
||||||
|
{
|
||||||
|
int w = meow_ui_width();
|
||||||
|
int h = meow_ui_height();
|
||||||
|
int sb = meow_ui_statusbar_height();
|
||||||
|
int cy = sb + (h - sb) / 2;
|
||||||
|
|
||||||
|
v->x = 0; v->y = 0; v->w = w; v->h = h;
|
||||||
|
|
||||||
|
int bw = 220, bh = 52;
|
||||||
|
if (g_btn) meow_widget_set_rect(g_btn, (w - bw) / 2, cy + 20, bw, bh);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 事件 */
|
||||||
|
/* ============================================================ */
|
||||||
|
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_TAP) {
|
||||||
|
int x = a, y = b;
|
||||||
|
if (meow_widget_hit(g_btn, x, y)) lock_attempt();
|
||||||
|
return 1; /* 锁屏页消费点击,不向 App 其它页透传 */
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* view 生命周期 */
|
||||||
|
/* ============================================================ */
|
||||||
static void lock_page_enter(meow_view *v, void *ctx)
|
static void lock_page_enter(meow_view *v, void *ctx)
|
||||||
{
|
{
|
||||||
((mr_page_ctrl *)ctx)->active = 1;
|
((mr_page_ctrl *)ctx)->active = 1;
|
||||||
v->x = 0;
|
|
||||||
v->y = 0;
|
|
||||||
v->w = meow_ui_width();
|
|
||||||
v->h = meow_ui_height();
|
|
||||||
meow_ui_set_fullscreen(0);
|
meow_ui_set_fullscreen(0);
|
||||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||||
|
|
||||||
|
g_msg[0] = '\0';
|
||||||
|
g_msg_err = 0;
|
||||||
|
|
||||||
|
if (!g_btn) {
|
||||||
|
/* page_push 已先 meow_ui_activity_push(ACT_LOCK) → 控件 owner 正确。 */
|
||||||
|
g_btn = meow_btn_create_variant(0, 0, 220, 52, "输入 PIN", MEOW_BTN_FILLED);
|
||||||
|
if (g_btn) meow_view_add_widget(v, g_btn);
|
||||||
|
}
|
||||||
|
lock_place(v);
|
||||||
mr_nav_report();
|
mr_nav_report();
|
||||||
meow_ui_paint_full();
|
meow_ui_paint_full();
|
||||||
}
|
}
|
||||||
@@ -54,12 +190,20 @@ static void lock_page_leave(meow_view *v, void *ctx)
|
|||||||
((mr_page_ctrl *)ctx)->active = 0;
|
((mr_page_ctrl *)ctx)->active = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void lock_page_destroy(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v; (void)ctx;
|
||||||
|
g_btn = NULL; /* 控件随 meow_view_free 释放(v->widgets) */
|
||||||
|
}
|
||||||
|
|
||||||
meow_view *lock_view_create(void)
|
meow_view *lock_view_create(void)
|
||||||
{
|
{
|
||||||
static const meow_view_hooks h = {
|
static const meow_view_hooks h = {
|
||||||
.on_enter = lock_page_enter,
|
.on_enter = lock_page_enter,
|
||||||
.on_leave = lock_page_leave,
|
.on_leave = lock_page_leave,
|
||||||
|
.on_destroy = lock_page_destroy,
|
||||||
.on_draw = lock_draw,
|
.on_draw = lock_draw,
|
||||||
|
.on_event = lock_on_event,
|
||||||
};
|
};
|
||||||
return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||||||
}
|
}
|
||||||
+411
-23
@@ -1,45 +1,420 @@
|
|||||||
/* ui/settings/settings.c — 设置页(ACT_SETTINGS)M3 骨架。
|
/* ui/settings/settings.c — 设置页(ACT_SETTINGS):MD3 分组表单。
|
||||||
*
|
*
|
||||||
* M5 在此实现 APP 启动 PIN 等设置项;本批只落可编译桩,入口为书库 TopAppBar ⚙。
|
* 内容(用户原话「允许用户在APP设置中设置APP启动 PIN」+「同步系统主题状态」):
|
||||||
* 返回:左缘返回(app.c SWIPE_LEFT → page_pop)。
|
* 阅读:阅读方向 RTL/LTR —— 直接读写 M3 的 mr_reader_get/set_direction(不另立状态);
|
||||||
|
* 安全:启动 PIN 开关(开启需两遍 MEOW_IME_NUM 输入确认)、修改 PIN;
|
||||||
|
* 关于:版本号、主题状态(跟随系统 + 当前明暗,随系统实时同步)。
|
||||||
*
|
*
|
||||||
* 接缝注意(M5):meow_view 的 on_create 在 meow_view_create 内运行(activity 栈空);
|
* 观感/主题:SDK 的 meow_listview 容器模式(MD3 surfaceContainer 圆角卡 + 行间空隙)
|
||||||
* 本页是非根页,控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式
|
* + 段头标签;开关/箭头由 SDK renderer 用主题语义色绘制;所有颜色取 meow_theme_get(),
|
||||||
* meow_ui_activity_push(ACT_SETTINGS) → 建控件 → meow_ui_activity_pop()
|
* 不缓存颜色——app.c 的主题 listener 整帧 invalidate,本页 on_draw/src 回调下一帧即用新色。
|
||||||
* (examples/hello/ui/demo/demo.c:515-522);否则 owner 绑到 ACT_LIBRARY,设置控件会从
|
*
|
||||||
* 书库页被命中(§6.1 跨页泄漏)。
|
* 控件归属(§6.1,M3 桩注释①):本页非根页,控件**必须**在 on_enter 里建(此时
|
||||||
|
* activity 栈顶=ACT_SETTINGS),否则 owner 会绑到 ACT_LIBRARY 造成跨页命中。
|
||||||
|
*
|
||||||
|
* 返回:左缘返回由 app.c 的 on_swipe 统一 page_pop(本页 on_event 不消费 SWIPE)。
|
||||||
|
* PIN 输入:SDK 系统数字键盘(MEOW_IME_NUM),不自绘键盘;文本经 on_change 实时收集,
|
||||||
|
* 「非空 + 关闭键盘 = 校验、空 = 取消」(SDK 阻塞 IME 的返回值不能当确认判据,见
|
||||||
|
* settings_read_pin 注释);键盘打开期间步骤提示画在键盘面板上方(置顶 widget)。
|
||||||
*/
|
*/
|
||||||
#include "tuku_internal.h"
|
#include "tuku_internal.h"
|
||||||
|
#include "logic/pin.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define ST_TOP_H 56
|
||||||
|
#define ST_SEC_H 34
|
||||||
|
#define ST_ROW_H 64
|
||||||
|
#define ST_GAP 12
|
||||||
|
|
||||||
|
/* meow_view on_event 的事件码:公共伞头未导出(定义在 sdk/meow_ui_internal.h:
|
||||||
|
* MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3,值稳定)。此处按契约镜像,避免 App include SDK 内部头。
|
||||||
|
* (已在交付报告里作为 SDK 缺口提出:建议公共头导出事件枚举。) */
|
||||||
|
#define MR_VEV_TAP 2
|
||||||
|
|
||||||
|
/* PIN 行下标(g_lv_pin) */
|
||||||
|
#define ST_PIN_ROW_SWITCH 0
|
||||||
|
#define ST_PIN_ROW_CHANGE 1
|
||||||
|
|
||||||
static mr_page_ctrl g_ctrl;
|
static mr_page_ctrl g_ctrl;
|
||||||
|
|
||||||
|
static meow_widget *g_tb;
|
||||||
|
static meow_widget *g_lv_dir; /* 阅读:1 行 */
|
||||||
|
static meow_widget *g_lv_pin; /* 安全:2 行 */
|
||||||
|
static meow_widget *g_lv_about; /* 关于:2 行 */
|
||||||
|
static meow_widget *g_overlay; /* 置顶提示条(仅拨号键盘期间显示步骤文案) */
|
||||||
|
|
||||||
|
static int g_yl_dir, g_yl_pin, g_yl_about; /* 段头标签 y */
|
||||||
|
static int g_note_y;
|
||||||
|
static int g_ov_y, g_ov_h; /* 提示条几何(App 自记;widget 句柄不透明) */
|
||||||
|
|
||||||
|
static char g_note[96];
|
||||||
|
static int g_note_err;
|
||||||
|
static int g_pin_on; /* 缓存的 PIN 启用态(避免每帧读 3 个小配置文件的 TF IO) */
|
||||||
|
static char g_theme_line[64];
|
||||||
|
static char g_prompt[64]; /* 非空=正在输入 PIN(键盘打开期间顶部提示条) */
|
||||||
|
static char g_ime_pin[MR_PIN_MAX_LEN + 12];
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 文案/状态 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static int theme_is_dark(const meow_theme *th)
|
||||||
|
{
|
||||||
|
int r = (int)((th->surface >> 16) & 0xFF);
|
||||||
|
int g = (int)((th->surface >> 8) & 0xFF);
|
||||||
|
int b = (int)(th->surface & 0xFF);
|
||||||
|
return (r * 299 + g * 587 + b * 114) / 1000 < 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void theme_line_update(void)
|
||||||
|
{
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
meow_theme_mode m = meow_theme_get_mode();
|
||||||
|
const char *mode = (m == MEOW_THEME_AUTO) ? "跟随系统"
|
||||||
|
: (m == MEOW_THEME_DARK) ? "固定深色" : "固定浅色";
|
||||||
|
snprintf(g_theme_line, sizeof g_theme_line, "%s · 当前%s", mode,
|
||||||
|
theme_is_dark(th) ? "深色" : "浅色");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_note(const char *msg, int is_err)
|
||||||
|
{
|
||||||
|
snprintf(g_note, sizeof g_note, "%s", msg ? msg : "");
|
||||||
|
g_note_err = is_err;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_repaint(void)
|
||||||
|
{
|
||||||
|
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* PIN 输入(系统数字键盘) */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void ime_pin_change(const char *text, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
snprintf(g_ime_pin, sizeof g_ime_pin, "%s", text ? text : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阻塞弹出 MEOW_IME_NUM 收集 4-8 位数字。返回 1=收到非空文本;0=空(视为取消)。
|
||||||
|
* 说明(SDK 现状,如实记录):阻塞 IME 只以「关闭键盘」结束;数字键盘的「确定」只置内部
|
||||||
|
* submit 标记而不关闭,随后「取消/点背景」关闭时该标记又被清 0(sdk/meow_ui_ime.c 的
|
||||||
|
* K_ESC/背景路径),故 meow_ui_text_input 的返回值不可作为「确认」判据。本页改用
|
||||||
|
* on_change 实时收集文本:非空 + 关闭键盘 = 校验;空 = 取消(提示条照此告知用户)。
|
||||||
|
* 键盘面板覆盖屏底(y≥420),步骤提示画在其上方,否则打字时看不见当前步骤。 */
|
||||||
|
static int settings_read_pin(const char *prompt, char *out, int outsz)
|
||||||
|
{
|
||||||
|
meow_ime_ctx c;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
c.mode = MEOW_IME_NUM;
|
||||||
|
c.maxlen = (int)sizeof g_ime_pin;
|
||||||
|
c.on_change = ime_pin_change;
|
||||||
|
c.ctx = NULL;
|
||||||
|
|
||||||
|
g_ime_pin[0] = '\0';
|
||||||
|
settings_note("", 0);
|
||||||
|
snprintf(g_prompt, sizeof g_prompt, "%s", prompt);
|
||||||
|
settings_repaint(); /* 先画提示条(键盘区背景快照从当前帧取) */
|
||||||
|
(void)meow_ui_text_input(&c);
|
||||||
|
g_prompt[0] = '\0';
|
||||||
|
|
||||||
|
snprintf(out, (size_t)outsz, "%s", g_ime_pin);
|
||||||
|
return out[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 两遍输入(开启/修改共用):1=一致且合法(写入 out);0=取消/不一致/非法(已置 note)。 */
|
||||||
|
static int settings_pin_two_pass(const char *prompt, char *out, int outsz)
|
||||||
|
{
|
||||||
|
char a[32] = "", b[32] = "";
|
||||||
|
|
||||||
|
if (!settings_read_pin(prompt, a, (int)sizeof a)) {
|
||||||
|
settings_note("已取消", 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!mr_pin_valid(a)) {
|
||||||
|
settings_note("PIN 需为 4-8 位数字,未保存", 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!settings_read_pin("再次输入以确认", b, (int)sizeof b)) {
|
||||||
|
settings_note("已取消", 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(a, b) != 0) {
|
||||||
|
settings_note("两次输入不一致,未保存", 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
snprintf(out, (size_t)outsz, "%s", a);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_toggle_pin(void)
|
||||||
|
{
|
||||||
|
if (!g_pin_on) {
|
||||||
|
char pin[32] = "";
|
||||||
|
if (!settings_pin_two_pass("设置启动 PIN(4-8 位数字)", pin, (int)sizeof pin)) return;
|
||||||
|
int r = mr_pin_set(pin);
|
||||||
|
if (r == MR_PIN_OK) settings_note("启动 PIN 已开启 · 下次启动生效", 0);
|
||||||
|
else settings_note("保存失败:数据目录不可写", 1);
|
||||||
|
g_pin_on = mr_pin_enabled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = mr_pin_disable();
|
||||||
|
if (r == MR_PIN_OK) settings_note("启动 PIN 已关闭", 0);
|
||||||
|
else settings_note("关闭失败:配置不可写", 1);
|
||||||
|
g_pin_on = mr_pin_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_change_pin(void)
|
||||||
|
{
|
||||||
|
if (!g_pin_on) {
|
||||||
|
settings_note("请先开启启动 PIN", 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char pin[32] = "";
|
||||||
|
if (!settings_pin_two_pass("设置新 PIN(4-8 位数字)", pin, (int)sizeof pin)) return;
|
||||||
|
int r = mr_pin_set(pin); /* 重设即换新盐;旧 PIN 失效 */
|
||||||
|
if (r == MR_PIN_OK) settings_note("PIN 已更新 · 下次启动生效", 0);
|
||||||
|
else settings_note("保存失败:数据目录不可写", 1);
|
||||||
|
g_pin_on = mr_pin_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 行数据源(src2:每帧重取 → 开关/副标题随状态即时刷新) */
|
||||||
|
/* ============================================================ */
|
||||||
|
static int src_dir(int idx, meow_list_item *it, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if (idx != 0) return 0;
|
||||||
|
memset(it, 0, sizeof *it);
|
||||||
|
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
|
||||||
|
it->title = "阅读方向";
|
||||||
|
it->subtitle = rtl ? "右到左 · 日式漫画(RTL)" : "左到右 · 常规漫画(LTR)";
|
||||||
|
it->right = MEOW_LIST_RIGHT_SWITCH;
|
||||||
|
it->on = rtl;
|
||||||
|
it->enabled = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int src_pin(int idx, meow_list_item *it, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
memset(it, 0, sizeof *it);
|
||||||
|
int on = g_pin_on;
|
||||||
|
if (idx == ST_PIN_ROW_SWITCH) {
|
||||||
|
it->title = "启动 PIN";
|
||||||
|
it->subtitle = on ? "已启用 · 启动时先输入 PIN"
|
||||||
|
: "未设置 · 启动直接进入书库";
|
||||||
|
it->right = MEOW_LIST_RIGHT_SWITCH;
|
||||||
|
it->on = on;
|
||||||
|
it->enabled = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (idx == ST_PIN_ROW_CHANGE) {
|
||||||
|
it->title = "修改 PIN";
|
||||||
|
it->subtitle = on ? "重新设置 4-8 位数字" : "开启后可修改";
|
||||||
|
it->right = MEOW_LIST_RIGHT_ARROW;
|
||||||
|
it->enabled = on; /* 未启用时变淡且不响应命中 */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int src_about(int idx, meow_list_item *it, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
memset(it, 0, sizeof *it);
|
||||||
|
if (idx == 0) {
|
||||||
|
it->title = "版本";
|
||||||
|
it->subtitle = "0.1 · os.meow.picbrowser";
|
||||||
|
it->enabled = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (idx == 1) {
|
||||||
|
theme_line_update();
|
||||||
|
it->title = "主题";
|
||||||
|
it->subtitle = g_theme_line;
|
||||||
|
it->enabled = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 布局 */
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 置顶提示条(z=MEOW_Z_TOP):键盘面板从屏底覆盖,步骤文案必须画在其上方;
|
||||||
|
* 仅 g_prompt 非空时绘制,否则透明不挡顶栏。
|
||||||
|
* 注:meow_widget 对 App 不透明(公共头只给句柄),坐标由本文件自己记录。 */
|
||||||
|
static void settings_overlay_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||||||
|
{
|
||||||
|
(void)w; (void)ctx;
|
||||||
|
if (!g_prompt[0]) return;
|
||||||
|
int wd = meow_ui_width();
|
||||||
|
meow_ui_fill_rect(0, g_ov_y, wd, g_ov_h, th->surfaceContainerHighest);
|
||||||
|
meow_ui_text_center(wd / 2, g_ov_y + 28, g_prompt, th->font_title, th->onSurface);
|
||||||
|
meow_ui_text_center(wd / 2, g_ov_y + 76, "输入后关闭键盘即校验 · 空输入=取消",
|
||||||
|
th->font_body, th->onSurfaceVariant);
|
||||||
|
meow_ui_fill_rect(0, g_ov_y + g_ov_h - 1, wd, 1, th->outlineVariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_build(void)
|
||||||
|
{
|
||||||
|
/* 控件先以占位矩形创建,统一由 settings_place() 摆位(避免两处布局数学不一致)。 */
|
||||||
|
g_tb = meow_topappbar_create(0, 0, 1, 1, "设置");
|
||||||
|
if (g_tb) meow_topappbar_set_back(g_tb, 1);
|
||||||
|
|
||||||
|
g_lv_dir = meow_listview_create(0, 0, 1, 1, 1, src_dir, NULL, MEOW_LIST_SEL_NONE);
|
||||||
|
g_lv_pin = meow_listview_create(0, 0, 1, 1, 2, src_pin, NULL, MEOW_LIST_SEL_NONE);
|
||||||
|
g_lv_about = meow_listview_create(0, 0, 1, 1, 2, src_about, NULL, MEOW_LIST_SEL_NONE);
|
||||||
|
|
||||||
|
/* 列表:MD3 容器卡 + 双行(标题/副标题)+ 右尾件(开关/箭头)。
|
||||||
|
* 容器模式由 SDK 默认 renderer 绘制(surfaceContainer 圆角卡 + 行间空隙)。 */
|
||||||
|
meow_widget *lvs[3] = { g_lv_dir, g_lv_pin, g_lv_about };
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (!lvs[i]) continue;
|
||||||
|
meow_listview_set_item_h(lvs[i], ST_ROW_H);
|
||||||
|
meow_listview_set_item_renderer(lvs[i], 2, 0, 1);
|
||||||
|
meow_listview_set_container(lvs[i], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_overlay = meow_widget_custom_create(0, 0, 1, 1,
|
||||||
|
settings_overlay_render, NULL, NULL);
|
||||||
|
if (g_overlay) meow_widget_set_z(g_overlay, MEOW_Z_TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 布局参数(窗口可能变化;每次进入重新摆位)。 */
|
||||||
|
static void settings_place(meow_view *v)
|
||||||
|
{
|
||||||
|
int w = meow_ui_width();
|
||||||
|
int sb = meow_ui_statusbar_height();
|
||||||
|
|
||||||
|
v->x = 0; v->y = 0; v->w = w; v->h = meow_ui_height();
|
||||||
|
|
||||||
|
int lx = 8, lw = w - 16;
|
||||||
|
int y = sb + ST_TOP_H + 6;
|
||||||
|
|
||||||
|
if (g_tb) meow_widget_set_rect(g_tb, 0, sb, w, ST_TOP_H);
|
||||||
|
|
||||||
|
g_yl_dir = y; y += ST_SEC_H;
|
||||||
|
if (g_lv_dir) meow_widget_set_rect(g_lv_dir, lx, y, lw, ST_ROW_H);
|
||||||
|
y += ST_ROW_H + ST_GAP;
|
||||||
|
|
||||||
|
g_yl_pin = y; y += ST_SEC_H;
|
||||||
|
if (g_lv_pin) meow_widget_set_rect(g_lv_pin, lx, y, lw, ST_ROW_H * 2);
|
||||||
|
y += ST_ROW_H * 2 + ST_GAP;
|
||||||
|
|
||||||
|
g_yl_about = y; y += ST_SEC_H;
|
||||||
|
if (g_lv_about) meow_widget_set_rect(g_lv_about, lx, y, lw, ST_ROW_H * 2);
|
||||||
|
y += ST_ROW_H * 2 + 14;
|
||||||
|
|
||||||
|
g_note_y = y;
|
||||||
|
|
||||||
|
g_ov_y = sb;
|
||||||
|
g_ov_h = 128;
|
||||||
|
if (g_overlay) meow_widget_set_rect(g_overlay, 0, g_ov_y, w, g_ov_h);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settings_section_label(int y, const char *text, const meow_theme *th)
|
||||||
|
{
|
||||||
|
meow_ui_text(24, y + 6, text, th->font_body, th->primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 绘制 */
|
||||||
|
/* ============================================================ */
|
||||||
static void settings_draw(meow_view *v, const meow_theme *th, void *ctx)
|
static void settings_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||||||
{
|
{
|
||||||
(void)v; (void)ctx;
|
(void)v; (void)ctx;
|
||||||
int w = meow_ui_width();
|
int w = meow_ui_width();
|
||||||
int sb = meow_ui_statusbar_height();
|
|
||||||
|
|
||||||
meow_ui_fill_rect(0, sb, w, 56, th->surfaceContainer);
|
settings_section_label(g_yl_dir, "阅读", th);
|
||||||
meow_ui_text(w / 2, sb + 16, "Settings", th->font_title, th->onSurface);
|
settings_section_label(g_yl_pin, "安全", th);
|
||||||
meow_ui_text_center(w / 2, sb + 100, "APP options", th->font_body, th->onSurfaceVariant);
|
settings_section_label(g_yl_about, "关于", th);
|
||||||
meow_ui_text_center(w / 2, sb + 140, "Startup PIN — M5", th->font_body, th->onSurface);
|
|
||||||
meow_ui_text_center(w / 2, sb + 180, "Reader direction — M4", th->font_body,
|
if (g_note[0]) {
|
||||||
th->onSurface);
|
meow_ui_text(24, g_note_y, g_note, th->font_body,
|
||||||
meow_ui_text_center(w / 2, sb + 240, "Theme follows the system automatically",
|
g_note_err ? th->error : th->primary);
|
||||||
th->font_body, th->onSurfaceVariant);
|
}
|
||||||
meow_ui_text_center(w / 2, sb + 300, "left-edge swipe = back", th->font_body,
|
int hy = g_note_y + (g_note[0] ? 30 : 0);
|
||||||
|
meow_ui_text(24, hy, "改动即时保存,下次启动生效", th->font_body, th->onSurfaceVariant);
|
||||||
|
meow_ui_text(24, hy + 28, "PIN 仅防随手查看,非安全边界", th->font_body,
|
||||||
th->onSurfaceVariant);
|
th->onSurfaceVariant);
|
||||||
|
meow_ui_fill_rect(0, meow_ui_height() - 1, w, 1, th->outlineVariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 事件 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static int settings_on_event(meow_view *v, int ev, int a, int b, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v; (void)ctx;
|
||||||
|
if (ev == MR_VEV_TAP) {
|
||||||
|
int x = a, y = b;
|
||||||
|
if (g_tb) {
|
||||||
|
int hit = meow_topappbar_hit(g_tb, x, y);
|
||||||
|
if (hit == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; }
|
||||||
|
if (hit >= 0) return 1;
|
||||||
|
}
|
||||||
|
if (g_lv_dir) {
|
||||||
|
int idx = meow_listview_hit(g_lv_dir, x, y);
|
||||||
|
if (idx == 0) {
|
||||||
|
mr_reader_set_direction(mr_reader_get_direction() == MR_DIR_RTL
|
||||||
|
? MR_DIR_LTR : MR_DIR_RTL);
|
||||||
|
settings_note(mr_reader_get_direction() == MR_DIR_RTL
|
||||||
|
? "阅读方向:右到左(RTL)"
|
||||||
|
: "阅读方向:左到右(LTR)", 0);
|
||||||
|
settings_repaint();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g_lv_pin) {
|
||||||
|
int idx = meow_listview_hit(g_lv_pin, x, y);
|
||||||
|
if (idx == ST_PIN_ROW_SWITCH) {
|
||||||
|
settings_toggle_pin();
|
||||||
|
settings_repaint();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (idx == ST_PIN_ROW_CHANGE) {
|
||||||
|
settings_change_pin();
|
||||||
|
settings_repaint();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g_lv_about) {
|
||||||
|
int idx = meow_listview_hit(g_lv_about, x, y);
|
||||||
|
if (idx == 1) {
|
||||||
|
settings_note("主题跟随系统设置,App 内不可改", 0);
|
||||||
|
settings_repaint();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (idx == 0) return 1; /* 版本行:仅展示 */
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* SWIPE 不消费:app.c 对非 library 页的左缘返回统一 page_pop。 */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* view 生命周期 */
|
||||||
|
/* ============================================================ */
|
||||||
static void settings_page_enter(meow_view *v, void *ctx)
|
static void settings_page_enter(meow_view *v, void *ctx)
|
||||||
{
|
{
|
||||||
((mr_page_ctrl *)ctx)->active = 1;
|
((mr_page_ctrl *)ctx)->active = 1;
|
||||||
v->x = 0;
|
meow_ui_set_fullscreen(0); /* 保留系统状态栏(与书库一致) */
|
||||||
v->y = 0;
|
|
||||||
v->w = meow_ui_width();
|
|
||||||
v->h = meow_ui_height();
|
|
||||||
meow_ui_set_fullscreen(0);
|
|
||||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||||
|
|
||||||
|
if (!g_tb) {
|
||||||
|
/* 栈顶此时已是 ACT_SETTINGS(page_push 先 push 再 on_enter)→ 控件 owner 正确。 */
|
||||||
|
settings_build();
|
||||||
|
meow_widget *all[5] = { g_tb, g_lv_dir, g_lv_pin, g_lv_about, g_overlay };
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
if (all[i]) meow_view_add_widget(v, all[i]);
|
||||||
|
}
|
||||||
|
settings_place(v);
|
||||||
|
g_pin_on = mr_pin_enabled(); /* 进入页时读一次;页内改动由操作函数同步 */
|
||||||
mr_nav_report();
|
mr_nav_report();
|
||||||
meow_ui_paint_full();
|
meow_ui_paint_full();
|
||||||
}
|
}
|
||||||
@@ -50,12 +425,25 @@ static void settings_page_leave(meow_view *v, void *ctx)
|
|||||||
((mr_page_ctrl *)ctx)->active = 0;
|
((mr_page_ctrl *)ctx)->active = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void settings_page_destroy(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v; (void)ctx;
|
||||||
|
/* 控件随 meow_view_free 统一释放(v->widgets);这里只清指针,避免悬垂。 */
|
||||||
|
g_tb = NULL;
|
||||||
|
g_lv_dir = NULL;
|
||||||
|
g_lv_pin = NULL;
|
||||||
|
g_lv_about = NULL;
|
||||||
|
g_overlay = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
meow_view *settings_view_create(void)
|
meow_view *settings_view_create(void)
|
||||||
{
|
{
|
||||||
static const meow_view_hooks h = {
|
static const meow_view_hooks h = {
|
||||||
.on_enter = settings_page_enter,
|
.on_enter = settings_page_enter,
|
||||||
.on_leave = settings_page_leave,
|
.on_leave = settings_page_leave,
|
||||||
|
.on_destroy = settings_page_destroy,
|
||||||
.on_draw = settings_draw,
|
.on_draw = settings_draw,
|
||||||
|
.on_event = settings_on_event,
|
||||||
};
|
};
|
||||||
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user