feat: M6.10–M6.19 批量收编 + M6.19 推卡验收

- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘)
- OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复
- ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640
- 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫)
- 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
This commit is contained in:
AskaEth committed 2026-09-03 12:16:01 +08:00
1 parent c2ee7626a4
commit 3d70a3dd2c
176 files changed
+15507 -597

No files matched your search

Binary file not shown.
+2 -1
View File
@@ -12,7 +12,8 @@ ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
"$CC" -Os -static -Wall -Wextra -I../sdk -I. \
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c ui/common/chrome.c \
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c \
ui/demo/sdk2/sdk2.c ui/common/chrome.c \
logic/notify.c logic/state.c \
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread -o hello
ls -la hello
Binary file not shown.
+15
View File
@@ -106,6 +106,21 @@ int list_view_down(int x, int y); /* touch_down 命中列表:置
int list_view_move(int y); /* touch_move 拖动列表:1=在列表拖动 */
int list_view_swipe(int del); /* swipe:列表聚焦时滚列表,1=已消费 */
/* ---- SDK 批次2 控件演示区 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)----
* 复刻 list.c 模式:demo 页内容区底部追加三节——枚举选择器 meow_select / 2D 虚拟化宫格
* meow_lazygrid / 列表行左滑删除 meow_listview_set_swipe_delete。子视图自持三节控件与视图态
* (grid 非 meow_widget:on_draw 直绘 meow_lazygrid_draw;swipe-list 行左滑 3 相位手势),
* 对外暴露 sdk2_build_at(demo_build_at 调)与 hit/down/move/swipe/release(demo 分发改派)。 */
meow_view *sdk2_view_create(void); /* 建 CONTAINER 子视图(须即 meow_view_add_child) */
meow_view *sdk2_view_get(void); /* 返回子视图(宿主测嵌套生命周期断言) */
int sdk2_build_at(int yb_base, int ct, int scroll); /* 在 base yb 处建三节控件(owner=ACT_DEMO),返回下一 yb */
void sdk2_release(void); /* 内容释放后:释放 grid/摘控件引用/清几何 */
int sdk2_hit(int x, int y); /* tap 分发:select 按钮/宫格 tap/列表行 tap/行左滑松手 swipe_end */
int sdk2_down(int x, int y); /* touch_down:宫格拖拽或列表行左滑 begin;1=本区消费 */
int sdk2_move(int x, int y); /* touch_move:宫格滚动/列表左滑 move 或列表竖向滚动;1=消费 */
int sdk2_swipe(int del); /* swipe UP/DOWN:宫格或列表聚焦时滚自身;1=已消费 */
int sdk2_dragging(void); /* 1=本子视图拖拽在途(demo_swipe 早退防 on_swipe 二次滚动) */
/* ---- chrome(ui/common/chrome.c)---- */
extern meow_widget *g_tb; /* TopAppBar(chrome 固定) */
extern meow_widget *g_nav; /* NavigationBar(chrome 固定) */
+66 -11
View File
@@ -43,6 +43,7 @@ static int g_drag_slider = 0; /* 手指按在 slider */
int g_drag_page = 0; /* 手指按在页内容(非 slider/list):拖拽滚页面 */
static int g_drag_page_last_y = 0; /* 页面拖拽上一帧 y */
static int g_drag_page_delta = 0; /* 页面拖拽累计位移(节流重建,≥8px 才重建一次) */
static uint64_t g_drag_page_tlast = 0; /* 页面拖拽上次重建时刻(359:每 poll 合并一次 rebuild) */
/* SDK 控件不透明:App 自记几何,用于局部 flush / 命中 / scroll 重建。 */
@@ -157,6 +158,7 @@ static void demo_free_content(void)
if (g_slider_cid > 0) { meow_draw_unregister(g_slider_cid); g_slider_cid = 0; }
g_dtitle_n = 0;
list_view_release(); /* 列表控件已在上方 g_dslot 释放;摘容器引用/几何 */
sdk2_release(); /* SDK 批次2 子视图:释放 grid/btn/list 控件引用 + 清几何 */
}
/* 按 scroll 重建全部内容控件(owner=ACT_DEMO:调用方须已 push ACT_DEMO)。
@@ -246,6 +248,15 @@ static void demo_build_at(int scroll)
d_title(DCL_X, SY(yb), "MEDIA SESSION (meta)"); yb += 26;
r_media = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_media = meow_btn_create_variant(r_media.x, r_media.y, r_media.w, r_media.h, "SEND META", MEOW_BTN_FILLED); SLOT(g_dbtn_media, r_media.x, r_media.y, r_media.w, r_media.h); yb += DCH;
/* ---- SDK 批次2 新控件(meow_select / meow_lazygrid / swipe-delete)----
* 三节由 sdk2 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)承接:章节标题/宫格/select 值文本走
* 子视图 on_draw 直绘(同列表区标题);select 按钮 + swipe 列表 widget 由 sdk2 自持、此处创建。
* 控件创建后几何已内存;跨重建视图态(grid scroll/sel、swipe 行集/scroll、select 值)由
* sdk2_build_at 从子视图持有态恢复。
* 注:sdk2 置于 LazyColumn 之前(列表保持 demo 页内容**末尾**区块),确保「滚动到底列表上屏」
* 的既有宿主 P3 断言坐标假设仍然成立(sdk2 增高在列表上方,列表仍是内容底)。 */
yb = sdk2_build_at(yb, ct, scroll);
/* ---- LazyColumn(100 项列表滚动) ----
* 本区块标题改由列表区 CONTAINER 子视图(ui/demo/list/list.c)的 on_draw 绘制(注册表→
* invalidate 链路);列表滚动/选中由 list_view_bind 从子视图持有态恢复(不再经 g_ds)。 */
@@ -268,7 +279,21 @@ void demo_paint(void)
meow_ui_paint_full(); /* 整帧按 z 序重画 */
}
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用 */
/* 内容视口坐标(每次 build 同步):屏 y ∈ [g_demo_ct, g_demo_cb),x 跨 0..width。 */
static void demo_repaint_viewport(void)
{
int h = g_demo_cb - g_demo_ct;
if (h <= 0) return;
/* 滚动重绘:只清底+按 z 重画内容视口矩形 [0, ct, w, h],不整帧。
* 359 教训(滚动只刷视口 + 局部脏区不整屏重绘铁律):meow_ui_invalidate 内部已把绘制 clip
* 收敛到矩形(meow_gfx_clip_push)+ meow_ui_flush_region 只上屏该区;chrome(TopAppBar/
* NavBar/FAB,z=999,位于视口外)不触及,上一帧像素保留,滚动不再重画 chrome 字形。 */
meow_ui_invalidate(0, g_demo_ct, meow_ui_width(), h);
}
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用。
* 滚动路径用视口局部刷(demo_repaint_viewport)替代整帧 demo_paint——每刻度重建仍发生,
* 但不再整帧重绘(359 教训:SDK 控件内建局部刷,卡顿源于调用方整页重绘)。 */
void demo_rebuild_to(int scroll)
{
int vp_h = g_demo_cb - g_demo_ct;
@@ -279,7 +304,7 @@ void demo_rebuild_to(int scroll)
demo_free_content();
demo_build_at(scroll);
g_demo_scroll = scroll;
demo_paint();
demo_repaint_viewport();
}
/* ---- demo 交互 ---- */
@@ -299,6 +324,8 @@ static int demo_content_tap(int x, int y)
/* 嵌套命中(子>父,§A.7 设计):列表区 CONTAINER 子视图先发,命中即消费。 */
if (list_view_hit(x, y)) return 1;
/* SDK 批次2 子视图(select 按钮/宫格/列表行左滑松手):命中即消费。 */
if (sdk2_hit(x, y)) return 1;
for (int i = 0; i < 5; i++)
if (meow_widget_hit(g_dbtn_b[i], x, y)) { d_toggle_sel(g_dbtn_b[i], r_btn[i]); g_ds.btn[i] = meow_widget_selected(g_dbtn_b[i]); return 1; }
@@ -344,8 +371,18 @@ static void demo_media_send(void)
}
/* demo 页 on_tap 分发(chrome → 内容) */
/* 页面拖拽抬手把节流窗内遗留的 delta 一次性重建(视口局部刷)——slow drag 结束为 TAP 时也精确落点。 */
static void demo_flush_page_delta(void)
{
if (g_drag_page && (g_drag_page_delta >= 1 || g_drag_page_delta <= -1)) {
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
g_drag_page_delta = 0;
}
}
void demo_on_tap(int x, int y)
{
demo_flush_page_delta(); /* 手势结束:TAP 抬手边沿(<SWIPE_THRESH),把遗留页面 delta 落到位 */
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
@@ -389,44 +426,59 @@ static void demo_slider_drag(int x)
meow_ui_invalidate(r_slider_txt.x, r_slider_txt.y, r_slider_txt.w, r_slider_txt.h); /* 重画值文本 */
}
/* demo 页 on_touch_down:锁一种拖拽(slider > list > 页内容)。
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口)。 */
/* demo 页 on_touch_down:锁一种拖拽(slider > list > sdk2 > 页内容)。
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口);
* SDK 批次2 子视图走 sdk2_down(宫格拖拽/列表行左滑 begin,子>父收口)。 */
void demo_touch_down(int x, int y)
{
g_drag_slider = 0; g_drag_list = 0; g_drag_page = 0; g_drag_page_delta = 0;
if (!g_demo) return;
if (meow_widget_hit(g_dslider, x, y)) { g_drag_slider = 1; demo_slider_drag(x); return; }
if (list_view_down(x, y)) return; /* 列表区:置 focus+drag_list(list.c) */
if (sdk2_down(x, y)) return; /* SDK 批次2:宫格/列表行左滑 begin(sdk2.c) */
g_demo_focus = 0;
if (y >= g_demo_ct && y < g_demo_cb && x >= 0 && x < meow_ui_width()) {
g_drag_page = 1; g_drag_page_last_y = y;
}
}
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / 页面滚动节流重建)。 */
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / sdk2 滚动 / 页面滚动节流重建)。 */
void demo_touch_move(int x, int y)
{
if (!g_demo) return;
if (g_drag_slider) { demo_slider_drag(x); return; }
if (list_view_move(y)) return; /* 列表拖动(list.c) */
if (sdk2_move(x, y)) return; /* SDK 批次2:宫格滚动/列表左滑 move/列表竖向滚动(sdk2.c) */
if (g_drag_page) {
int dy = y - g_drag_page_last_y; g_drag_page_last_y = y;
g_drag_page_delta += -dy; /* 手指上移(y 减) → scroll 增(显示更低内容),与 on_swipe 一致 */
if (g_drag_page_delta >= 8 || g_drag_page_delta <= -8) {
/* 359 教训:滚动期间每个 poll 合并一次 rebuild(勿每触摸事件各自重建/上屏)。
* 累计位移超阈值(≥8) 且(距上次 ≥16ms 或 已是大步 ≥32)才重建一次;未到窗则继续
* 累积 delta(不丢),下个窗口一次性重建到精确位置。 */
uint64_t now = meow_ui_time_ms();
int big = (g_drag_page_delta >= 8 || g_drag_page_delta <= -8);
int merge = (now - g_drag_page_tlast >= 16) ||
(g_drag_page_delta >= 32) || (g_drag_page_delta <= -32);
if (big && merge) {
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
g_drag_page_tlast = now;
g_drag_page_delta = 0;
}
}
}
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表聚焦滚列表,否则页滚动。 */
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表/宫格/sdk2 聚焦滚自身,否则页滚动。 */
void demo_swipe(int dir, int dist)
{
if (dir == MEOW_SWIPE_LEFT) { meow_view_page_pop(); return; }
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_drag_page || g_drag_list) return; /* 本次手势已实时跟手滚动过 */
if (g_drag_page || g_drag_list || sdk2_dragging()) { /* 本次手势已实时跟手滚动过 */
demo_flush_page_delta(); /* 抬手:flush 节流窗内遗留页面 delta(视口局部刷)精确落点 */
return;
}
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 手指上移 → 偏移增大 */
if (list_view_swipe(del)) return; /* 列表聚焦:滚列表(list.c,子>父消费) */
if (sdk2_swipe(del)) return; /* SDK 批次2:宫格/列表聚焦滚自身(sdk2.c,子>父消费) */
demo_rebuild_to(g_demo_scroll + del);
}
@@ -441,7 +493,8 @@ static void demo_page_enter(meow_view *v, void *ctx)
hello_nav_report(); /* ① 上报 demo 栈深/顶层 */
/* 列表区 CONTAINER 子视图随本页进入由 SDK view_enter 递归驱动(其 on_draw 在 demo_rebuild_to
* →demo_paint 中经注册表绘制;meow_draw_view_entered 含祖先已进入判定,首批可见)。 */
demo_rebuild_to(0);
demo_rebuild_to(0); /* 内容建到 scroll=0(视口局部刷) */
demo_paint(); /* 进入页整帧首绘(含 chrome 置顶 z=999);仅进入一次,非滚动路径 */
meow_log(MEOW_LOG_INFO, "demo enter scroll=0");
}
@@ -475,14 +528,16 @@ int demo_is_active(void)
}
/* demo view:on_create 建 chrome+内容(一次,owner=ACT_DEMO);进入时 rebuild 到顶。
* 视图深度化:先挂列表区 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
* 列表子视图 id 用非页面值,不占 g_view_by_id(见 list.c)。 */
* 视图深度化:先挂列表区 + SDK 批次2 两个 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
* 子视图 id 用非页面值,不占 g_view_by_id(见 list.c/sdk2.c)。 */
static void demo_page_create(meow_view *v, void *ctx)
{
(void)v;
((demo_ctrl *)ctx)->view = v;
meow_view *lv = list_view_create();
if (lv) meow_view_add_child(v, lv);
meow_view *s2 = sdk2_view_create();
if (s2) meow_view_add_child(v, s2);
demo_init();
}
+455
View File
@@ -0,0 +1,455 @@
/* ui/demo/sdk2/sdk2.c — 控件库 Demo 页内「SDK 批次2 新控件」CONTAINER 子视图(agent-363)。
*
* 目标(SDK_VIEW_FRAMEWORK_DESIGN.md §B.4/§E③,同 list.c):把 demo 页底部的三个 SDK 批次2
* 新控件演示区块抽成一个 meow_view CONTAINER 子视图,挂到 demo ACTIVITY view 的 children:
* ① meow_select (§5.8.9 枚举/预设选择器:下拉模态 + 单选列表 + on_change 回调)
* ② meow_lazygrid (§12.4 2 维虚拟化宫格:非 meow_widget,on_draw 直绘 meow_lazygrid_draw)
* ③ meow_listview 左滑删除(§5.5 swipe-delete:swipe_begin/move/end + on_delete + 回弹/删除动画)
*
* 职责边界(诚实,与 list.c 同折中):
* - 本子视图承接三节**整体**:章节标题 + select 按钮 + 宫格 + swipe-列表 的创建/释放与视图态
* (grid 虚拟源/选中/滚动、swipe 行集/滚动、select 选中值)。控件**创建**在 demo_build_at 链路内
* (sdk2_build_at,owner=ACT_DEMO)、**释放**在 sdk2_release(demo_free_content 调)——与 list.c
* 相同折中:完整「控件归容器拥有」需 SDK 侧支持(本批冻结 patch_v2/sdk/)。
* - meow_lazygrid 是**非 meow_widget**:不注册 z 序/绘制注册表。本子视图 on_draw 直接调
* meow_lazygrid_draw(view → 注册表 → invalidate 链路,与 list 区块标题同法)。
* - 章节标题(三段)与 select 当前值文本也走 on_draw 直绘(列表区标题同法),不逐标题注册
* CUSTOM 绘制物;on_draw 是回调、view 不缓存像素(非保留,§5.8.7)。
* - 嵌套生命周期由 SDK view_enter/view_leave 递归驱动(随 demo 页进出,同 list.c);绘制经
* meow_draw_view_entered(含祖先已进入判定)在 meow_ui_paint_full/invalidate 触发。
*
* 事件接线(demo 页把 touch/tap/swipe 分发改派到本子视图,实现「子>父」命中):
* - tap → sdk2_hit (select 按钮 / 宫格 tap / 列表行 tap / 行左滑松手 swipe_end)
* - down → sdk2_down (宫格聚焦拖拽 / 列表行左滑 begin)
* - move → sdk2_move (宫格滚动 / 列表左滑 move 或列表竖向滚动)
* - swipe → sdk2_swipe (宫格/列表聚焦时滚自身;否则回退页面滚动)
*/
#include "hello_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ---- 本子视图(CONTAINER,非页面:id 用越界值避免占用 g_view_by_id 顶层页槽位)---- */
static meow_view *g_sdk2_view = NULL;
/* ---- demo 内容布局常量(与 demo.c 一致):内容区 x16 宽 448 ---- */
#define S2_X 16
#define S2_W 448
#define S2_DCW 212
#define S2_DBH 44
#define S2_CH 52
/* 三节(章节标题几何在 sdk2_build_at 计算后存入,on_draw 直绘用;屏幕坐标)。 */
static int s2_t1_y, s2_t2_y, s2_t3_y; /* 三节标题 y(on_draw 直绘) */
/* ============================================================ */
/* ① meow_select — 枚举/预设选择器 */
/* ============================================================ */
static meow_widget *g_btn_sel = NULL; /* 「打开选择器」按钮(sdk2_release 释放) */
static meow_select *g_sel = NULL; /* 选择器实例(sdk2_view_create 建一次,destroy 释放) */
static rect_t r_selbtn = { 0, 0, 0, 0 };
static rect_t r_selval = { 0, 0, 0, 0 }; /* 当前选择值文本区(on_draw 直绘) */
static int g_sel_cur = 0; /* 当前选中项(跨重建保留;on_change 更新) */
static const char *g_sel_items[] = { /* 枚举演示:音频档位(对齐 M7.2 设置页采样率/码率) */
"Auto", "44.1kHz", "48kHz", "96kHz", "192kHz",
};
#define SEL_N ((int)(sizeof g_sel_items / sizeof g_sel_items[0]))
/* on_change 回调(meow_select_open 确认后触发:更新选中下标 + 重画值文本区)。 */
static void sdk2_sel_change(int idx, void *ctx)
{
(void)ctx;
if (idx >= 0 && idx < SEL_N) g_sel_cur = idx;
if (r_selval.w > 0) meow_ui_invalidate(r_selval.x, r_selval.y, r_selval.w, r_selval.h);
}
/* 打开选择器(阻塞到确认/取消;on_change 已更新选中值)。 */
static void sdk2_select_open(void)
{
if (!g_sel) return;
int r = meow_select_open(g_sel);
meow_log(MEOW_LOG_INFO, "sdk2 select return=%d (%s)",
r, (r >= 0 && r < SEL_N) ? g_sel_items[r] : "cancel");
}
/* ============================================================ */
/* ② meow_lazygrid — 2 维虚拟化宫格(非 widget) */
/* ============================================================ */
static meow_lazygrid *g_grid = NULL;
static rect_t r_grid = { 0, 0, 0, 0 };
static int g_grid_scroll = 0; /* 跨重建保留:宫格滚动偏移 */
static int g_grid_sel = -1; /* 跨重建保留:选中项 */
static int g_drag_grid = 0; /* 触摸拖宫格(down 置位;move 滚动) */
static int g_grid_focus = 0; /* 1=宫格聚焦(swipe 滚宫格而非页面) */
static int g_grid_last_y = 0;
#define GRID_N 36 /* 总项数(count > 视口行数 → 可滚动) */
#define GRID_COLS 3
#define GRID_CW 138
#define GRID_CH 86
#define GRID_GAP 10
#define GRID_H 190
/* 虚拟源:count 总项数(实时查);bind 项进入视口时调一次(同 listview 池语义)。 */
static int grid_count(void *ctx) { (void)ctx; return GRID_N; }
static void grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
{
(void)ctx;
static char t[24], s[24];
snprintf(t, sizeof t, "App %d", idx);
snprintf(s, sizeof s, "v1.%d", idx);
memset(item, 0, sizeof *item);
item->title = t; item->subtitle = s;
item->enabled = (idx % 9 != 8); /* 演示禁用变淡(每 9 个禁用 1 个) */
}
/* 完全自绘 cell(set_item_draw):icon+label 风格,对齐主界面用途。 */
static void grid_cell(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)ctx;
const meow_theme *th = meow_theme_get();
int sel = st & MEOW_LAZYGRID_ST_SELECTED;
int dis = st & MEOW_LAZYGRID_ST_DISABLED;
uint32_t bg = sel ? th->primaryContainer : th->surfaceContainerLow;
uint32_t icon = dis ? th->onSurfaceVariant : th->primary;
uint32_t txt = sel ? th->onPrimaryContainer
: (dis ? th->onSurfaceVariant : th->onSurface);
meow_ui_fill_rect(x, y, w, h, bg); /* 格底卡片 */
/* icon 块(无图资源:主题 primary 方块代替主界面图标位) */
int isz = w / 3; if (isz > h / 2) isz = h / 2;
meow_ui_fill_rect(x + (w - isz) / 2, y + 10, isz, isz, icon);
/* 名 + 状态(底部两行,中心对齐) */
static char t[24], s[24];
snprintf(t, sizeof t, "App %d", idx);
snprintf(s, sizeof s, "v1.%d", idx);
int thh = meow_ui_text_height(th->font_body);
meow_ui_text_center(x + w / 2, y + h - thh - 22, t, th->font_body, txt);
meow_ui_text_center(x + w / 2, y + h - thh - 3, s, th->font_body, txt);
if (sel) meow_ui_draw_rect_round(x, y, w, h, th->radius_card, th->primary);
if (dis) meow_ui_fill_rect_alpha(x, y, w, h, th->onSurface, 90);
}
/* 宫格 tap:置选中 + 局部刷 + 日志。 */
static void grid_tap(int idx, void *ctx)
{
(void)ctx;
g_grid_sel = idx;
meow_log(MEOW_LOG_INFO, "sdk2 grid tap idx=%d", idx);
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
}
/* ============================================================ */
/* ③ meow_listview 行左滑删除(swipe-delete) */
/* ============================================================ */
#define LV_MAX 9
static meow_widget *g_lv = NULL; /* W_LISTVIEW(注册表自动画:行 + 右露删除区) */
static rect_t r_lv = { 0, 0, 0, 0 };
static int g_lv_deleted[LV_MAX]; /* 1=已删除(行移除;count 实时收缩) */
static char g_lv_label[LV_MAX][24]; /* 行标题(bind 填充) */
static int g_lv_scroll = 0; /* 跨重建保留:列表滚动 */
static int g_lv_focus = 0; /* 1=swipe-list 聚焦(swipe 滚列表而非页面) */
static int g_lv_swipe = 0; /* 行左滑在途(swipe_begin 命中;hit 里 swipe_end) */
static int g_drag_lv = 0; /* 触摸拖列表(竖向滚动);d. 左滑时同置位 */
static int g_lv_last_y = 0;
#define LV_H 190
static int lv_count_remaining(void)
{
int n = 0;
for (int i = 0; i < LV_MAX; i++) if (!g_lv_deleted[i]) n++;
return n;
}
static int lv_count(void *ctx) { (void)ctx; return lv_count_remaining(); }
/* 显示下标(0..count-1)→ 源行 id(跳过已删除行)。删除后行前移,身份按源行稳定。 */
static int lv_real_index(int display)
{
if (display < 0) return -1;
int c = 0;
for (int i = 0; i < LV_MAX; i++) {
if (g_lv_deleted[i]) continue;
if (c == display) return i;
c++;
}
return -1;
}
static void lv_bind(int idx, meow_lv_item *item, void *ctx)
{
(void)ctx;
int real = lv_real_index(idx);
static char s[24];
memset(item, 0, sizeof *item);
snprintf(s, sizeof s, "swipe left");
item->title = (real >= 0) ? g_lv_label[real] : "";
item->subtitle = s;
item->icon_kind = (real >= 0 && real % 3 == 0) ? MEOW_LIST_ICON_FOLDER : MEOW_LIST_ICON_NONE;
item->right = MEOW_LIST_RIGHT_NONE;
item->enabled = 1;
}
/* on_delete(swipe 删除动画滑出完成回调):删除该显示下标对应的源行 + 局部刷。
* 注:swipe_end 传的是显示下标(含删除前定位的 swipe_idx);映射回源行 id 标记。 */
static void lv_delete(int idx, void *ctx)
{
(void)ctx;
int real = lv_real_index(idx);
if (real >= 0) g_lv_deleted[real] = 1;
meow_log(MEOW_LOG_INFO, "sdk2 swipe delete display=%d real=%d", idx, real);
if (g_lv && r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
}
/* 建三节时填充行标题(demo_init 一次性;跨重建保留)。 */
static void sdk2_init_rows(void)
{
for (int i = 0; i < LV_MAX; i++) {
snprintf(g_lv_label[i], sizeof g_lv_label[i], "notif %d", i);
g_lv_deleted[i] = 0;
}
}
static int s2_built = 0; /* 1=本子视图内容已建(on_draw / 事件分发判空) */
/* ============================================================ */
/* 释放 / 建三节 */
/* ============================================================ */
/* 内容释放(demo_free_content 调):清引用(本子视图建的控件在 sdk2_release 释放)。 */
void sdk2_release(void)
{
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
if (g_btn_sel) { meow_widget_free(g_btn_sel); g_btn_sel = NULL; }
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
r_selbtn = r_selval = r_grid = r_lv = RX(0, 0, 0, 0);
g_drag_grid = 0; g_grid_focus = 0; g_grid_last_y = 0;
g_lv_focus = 0; g_lv_swipe = 0; g_drag_lv = 0; g_lv_last_y = 0;
s2_built = 0;
if (g_sdk2_view) g_sdk2_view->x = g_sdk2_view->y = g_sdk2_view->w = g_sdk2_view->h = 0;
}
/* 内容重建后建三节(demo_build_at 调)。入参:yb_base=base 空间 y;ct/scroll 同 demo.c
* (SY(base)=ct+base-scroll)。返回下一 base y。 */
int sdk2_build_at(int yb_base, int ct, int scroll)
{
#define SY2(b) (ct + (b) - scroll)
int yb = yb_base;
/* ---- ① SELECT ---- */
s2_t1_y = SY2(yb);
yb += 26;
r_selbtn = RX(S2_X, SY2(yb), S2_DCW, S2_DBH);
g_btn_sel = meow_btn_create_variant(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h,
"CHOOSE", MEOW_BTN_OUTLINED);
r_selval = RX(S2_X + S2_DCW + 14, SY2(yb) + 10, S2_W - S2_DCW - 14, 24);
yb += S2_CH;
/* ---- ② LAZYGRID ---- */
s2_t2_y = SY2(yb);
yb += 30;
r_grid = RX(S2_X, SY2(yb), S2_W, GRID_H);
g_grid = meow_lazygrid_create(r_grid.x, r_grid.y, r_grid.w, r_grid.h,
GRID_COLS, GRID_CW, GRID_CH, GRID_GAP);
if (g_grid) {
meow_lazygrid_set_vsource(g_grid, grid_count, grid_bind, NULL);
meow_lazygrid_set_item_draw(g_grid, grid_cell, NULL);
meow_lazygrid_set_empty_text(g_grid, "No apps");
meow_lazygrid_set_on_tap(g_grid, grid_tap, NULL);
meow_lazygrid_set_scroll(g_grid, g_grid_scroll);
meow_lazygrid_set_sel(g_grid, g_grid_sel);
}
yb += GRID_H + 30;
/* ---- ③ SWIPE-DELETE ---- */
s2_t3_y = SY2(yb);
yb += 26;
r_lv = RX(S2_X, SY2(yb), S2_W, LV_H);
g_lv = meow_listview_create(r_lv.x, r_lv.y, r_lv.w, r_lv.h, 0,
NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_lv) {
meow_listview_set_item_h(g_lv, 48);
meow_listview_set_vsource(g_lv, lv_count, lv_bind, NULL);
meow_listview_set_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
meow_listview_set_empty_text(g_lv, "All rows deleted");
meow_listview_set_swipe_delete(g_lv, 1, lv_delete, NULL);
meow_listview_set_scroll(g_lv, g_lv_scroll);
}
yb += LV_H + 10;
if (g_sdk2_view) {
g_sdk2_view->x = S2_X; g_sdk2_view->y = s2_t1_y;
g_sdk2_view->w = S2_W; g_sdk2_view->h = (r_lv.y + r_lv.h) - s2_t1_y;
}
s2_built = 1;
return yb;
#undef SY2
}
/* ---- on_draw:三节标题 + select 当前值 + 宫格(非保留,invalidate 触发重画)---- */
static void sdk2_draw_on(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)th; (void)ctx;
if (!s2_built) return;
const meow_theme *t = meow_theme_get();
meow_ui_text(S2_X, s2_t1_y, "SELECT (enum/preset)", 20, t->primary);
meow_ui_text(S2_X, s2_t2_y, "LAZYGRID (2D virtualized)", 20, t->primary);
meow_ui_text(S2_X, s2_t3_y, "LISTVIEW swipe-delete", 20, t->primary);
char buf[40];
snprintf(buf, sizeof buf, "sel = %s", g_sel_items[g_sel_cur]);
meow_ui_text(r_selval.x, r_selval.y, buf, 20, t->dim);
if (g_grid) meow_lazygrid_draw(g_grid, t);
}
/* ============================================================ */
/* 事件分发(demo 页转发) */
/* ============================================================ */
/* tap:select 按钮 → 打开模态;列表行左滑松手 → swipe_end;宫格 → tap_at;列表行 → tap。 */
int sdk2_hit(int x, int y)
{
if (!s2_built) return 0;
/* 行左滑【松手】最优先:滑动后手指可能已移出列表矩形(只判在途标志)。 */
if (g_lv && g_lv_swipe) {
meow_listview_set_scroll(g_lv, g_lv_scroll);
int c = meow_listview_swipe_end(g_lv, x, y); /* 判删/回弹;动画由 meow_anim_tick 推进 */
g_lv_swipe = 0;
if (c) return 1; /* 消费(删/回弹动画中);未滑动=0 落行选中 */
}
if (g_btn_sel && meow_widget_hit(g_btn_sel, x, y)) {
sdk2_select_open();
meow_ui_invalidate(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h);
return 1;
}
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
meow_lazygrid_tap_at(g_grid, x, y); /* 置选中 + grid_tap 回调 + 局部刷 */
return 1;
}
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
meow_listview_tap_at(g_lv, x, y); /* 行点击(演示选中)+ 局部刷 */
meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
return 1;
}
return 0;
}
/* touch_down:命中区置拖拽/聚焦。宫格>列表。返回 1=本区消费。 */
int sdk2_down(int x, int y)
{
if (!s2_built) return 0;
/* 新手势先复位本子视图拖拽态:demo_touch_down 只复位 list/slider/page 标志,不触及 sdk2
* 的 g_drag_grid/g_drag_lv/g_lv_swipe(sdk2.c 内部,demo 页经 extern 看不到);不复位则上一条
* sdk2 拖拽残留会在本次「落页内容区」新手势的 move 里继续劫持页面拖动(sdk2_move 守卫误判)。 */
g_drag_grid = 0; g_drag_lv = 0; g_lv_swipe = 0;
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
g_drag_grid = 1; g_grid_focus = 1; g_grid_last_y = y;
return 1;
}
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
meow_listview_set_scroll(g_lv, g_lv_scroll);
g_lv_swipe = meow_listview_swipe_begin(g_lv, x, y); /* 命中可滑行 → 1 */
g_lv_focus = 1; g_drag_lv = 1; g_lv_last_y = y;
return 1;
}
return 0;
}
/* touch_move:宫格滚动 / 列表左滑 move(消费→不滚页面)或列表竖向滚动。返回 1=消费。 */
int sdk2_move(int x, int y)
{
if (!s2_built) return 0;
if (g_drag_grid && g_grid) {
int dy = y - g_grid_last_y; g_grid_last_y = y;
meow_lazygrid_scroll_by(g_grid, -dy);
g_grid_scroll = meow_lazygrid_scroll(g_grid);
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
return 1;
}
/* 列表滚动须在「本次手势命中列表」才消费(g_drag_lv,由 sdk2_down 命中列表置位)。
* 若仅因 g_lv 存在就无条件消费,会劫持 demo 页内容区拖拽(任何 move 都被这里吃掉,
* 页面永不滚动)——与 grid(g_drag_grid 守卫)同构;359 教训同源(SDK 控件局部刷对,
* 错在调用方控制流把页面滚动让给了控件)。 */
if (g_drag_lv && g_lv) {
if (g_lv_swipe) { /* 行左滑在途:喂 move;1=横向消费;0=纵向主导→取消本行 swipe */
meow_listview_set_scroll(g_lv, g_lv_scroll);
if (meow_listview_swipe_move(g_lv, x, y)) return 1;
g_lv_swipe = 0;
}
int dy = y - g_lv_last_y; g_lv_last_y = y; /* 竖向滚动(跟手) */
meow_listview_scroll_by(g_lv, -dy);
g_lv_scroll = meow_listview_scroll(g_lv);
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
return 1;
}
return 0;
}
/* swipe UP/DOWN:宫格/列表聚焦时滚自身(del 已按方向加权),1=已消费。 */
int sdk2_swipe(int del)
{
if (!s2_built) return 0;
if (g_grid_focus && g_grid) {
meow_lazygrid_scroll_by(g_grid, del);
g_grid_scroll = meow_lazygrid_scroll(g_grid);
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
return 1;
}
if (g_lv_focus && g_lv) {
meow_listview_scroll_by(g_lv, del);
g_lv_scroll = meow_listview_scroll(g_lv);
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
return 1;
}
return 0;
}
/* 本次手势是否已被本子视图实时滚过(demo_swipe 早退用,防 on_swipe 二次滚动——同 list 的 g_drag_list)。 */
int sdk2_dragging(void)
{
return (g_drag_grid || g_drag_lv) ? 1 : 0;
}
/* ============================================================ */
/* 子视图生命周期(on_enter/on_leave 由 SDK 递归驱动;on_destroy 清引用) */
/* ============================================================ */
static void sdk2_page_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
meow_log(MEOW_LOG_INFO, "demo sdk2 view enter");
}
static void sdk2_page_leave(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
meow_log(MEOW_LOG_INFO, "demo sdk2 view leave");
}
static void sdk2_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_sel) { meow_select_free(g_sel); g_sel = NULL; }
s2_built = 0;
g_sdk2_view = NULL; /* meow_view_free 递归释放前清引用,防悬垂 */
}
meow_view *sdk2_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = sdk2_page_enter,
.on_leave = sdk2_page_leave,
.on_destroy = sdk2_page_destroy,
.on_draw = sdk2_draw_on,
};
/* id 用非页面值:容器不占 g_view_by_id(那是 page_push/pop 顶层页查找表)。 */
g_sdk2_view = meow_view_create(0x2000, MEOW_VIEW_CONTAINER, &h, NULL);
/* meow_select 实例一次性创建(控制态随本子视图存活,不受内容重建影响);行标题一次性填充。 */
sdk2_init_rows();
if (g_sdk2_view && !g_sel) {
g_sel = meow_select_create("Sample rate", 0);
if (g_sel) {
for (int i = 0; i < SEL_N; i++) meow_select_add_item(g_sel, g_sel_items[i]);
meow_select_set_on_change(g_sel, sdk2_sel_change, NULL);
}
}
return g_sdk2_view;
}
meow_view *sdk2_view_get(void)
{
return g_sdk2_view;
}
Binary file not shown.
+5 -1
View File
@@ -1367,7 +1367,11 @@ static void *io_thread(void *arg)
memcpy(snaps, pfds, sizeof snaps);
pthread_mutex_lock(&g_mu);
(void)pr;
/* 处理 listen */
/* 处理 listen:一次 poll 收 1 个连接。**不可改用「while accept 排空积压」**——在连接
* 风暴下(对端持续 connect)accept 永不 EAGAIN → 持 g_mu 无限循环 → IO 线程卡死、
* 播放/路由/QUERY 全部被 g_mu 阻塞(宿主 T10 实测:drain 版风暴下 probe/OPEN 全 FAIL)。
* 单次 accept + 下一轮 poll 再收,既有界又保持 listener/IO 可服务(原版行为即正确,
* fd=N 反复 accept 是**对端短连接**所致,非本循环缺陷)。 */
if ((snaps[0].revents & POLLIN)) {
int cfd = accept(lfd, NULL, NULL);
if (cfd >= 0) {
Binary file not shown.
+38 -1
View File
@@ -651,7 +651,44 @@ int main(void)
check(0, "pipe for T9 child");
}
}
stop_audiod();
/* ---- T10:连接表溢出丢弃护栏与回收(回归 fd=N accept 风暴,§2026-09-02 真机 1489 次)。
* 背景:CLI_MAX=8(§12「客户端全有界」护栏)。ncm「每次 audio_out_open 新建一条短连接」
* (旧 audio_out_sync_volume)在边下边播/切歌 churn 下会反复占用连接槽:8 张表占满后,
* 新 connect 的 accept 因 cli_alloc 失败被**安全丢弃**(日志 "client limit reached, drop"),
* 表回收后 NEW 连接照常服务。本用例:
* ① 洪泛 connect→write→close ×60(远超 CLI_MAX=8,走上述 drop 护栏);
* ② 断言 audiod **不 wedge/不崩**(此后 SIGTERM 干净退出);
* ③ 等表回收(短连接已 close,EOF 逐 poll 回收)后,新 OPEN 仍被授权(fd 复用回收)。
* 「同一 fd=N 被反复 accept」= 对端短连接 + fd 低位复用,本是**正常回收机理**,非循环缺陷;
* 根因在对端每 open 建新连接,由 ncm side audio_out.c「音量同步每进程一次」消除(vol_sync_sim)。 ---- */
printf("[T10 client-table overflow guard & recycle]\n");
unlink(TSOCK);
g_audiod_pid = start_audiod_ex("null", NULL, NULL);
usleep(400000);
int burst = 0;
for (int i = 0; i < 60; i++) { /* 洪泛 > CLI_MAX=8 */
struct cli v;
if (cli_connect(&v) == 0) {
int32_t pct = 50;
if (sf(v.fd, MSG_AUDIO_VOLUME, 7, &pct, sizeof pct) == 0) burst++;
close(v.fd); /* 对端读 EOF → cli_free 回收 */
}
}
printf(" issued=%d/60 (cap by CLI_MAX=8 overflow drops)\n", burst);
check(burst > 0, "connections issued (overflow hits drop guard)");
/* ③ 等表回收(≤1s:EOF 逐 poll 回收)后新 OPEN 仍被授权 */
usleep(700000);
int eok = 0;
{
struct cli e;
if (cli_connect(&e) == 0) {
m_audio_open_ack_t ak;
if (do_open(&e, 48000, 2, AUDIO_FMT_S16, &ak) == 0 && ak.err == MEOW_OK) eok = 1;
close(e.fd);
}
}
check(eok, "fresh OPEN after overflow (table recycled, no exhaustion/wedge)");
check(stop_audiod_tmo(5000) != 0, "audiod not wedged by overflow (SIGTERM clean exit)");
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0;
Binary file not shown.
+3 -1
View File
@@ -178,7 +178,9 @@ static void log_cleanup_old(void)
struct dirent *de;
while ((de = readdir(d)) != NULL && n < 128) {
const char *nm = de->d_name;
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0) continue;
/* 收纳 meow_/crash_/perf_/hang_(perf_=RMM 基线、hang_=卡死快照,同 logs/ 1GB 纪律)。 */
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0 &&
strncmp(nm, "perf_", 5) != 0 && strncmp(nm, "hang_", 5) != 0) continue;
char path[CRASH_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s", logs, nm) >= (int)sizeof path) continue;
struct stat st;
Binary file not shown.
+6
View File
@@ -2171,6 +2171,12 @@ static int launch_app_chain(struct app *a, const char *chain)
/* §10.5:App 前台会话结束,清会话标记(ipc_dispatch 不再受理 MSG_NOTIF_OPEN) */
g_app_session_active = 0;
meow_notif_open_clear();
/* M2.17 兜底:App 会话结束(waitpid 回收/崩溃/被杀,回主界面)清 fg_act ——
* App 在 keep_awake activity(如 ncm 播放页)被 OOM/崩溃杀死且未弹回 home 时,g_fg_act 会
* 滞留 "<act>",使主页 fg_keep_awake()==1 不再自动息屏。此处兜底清空(App 已退出/死亡,
* 主界面非 keep_awake);App 存活时 fg_act 保持不清,仅会话整体结束才清。正常路径 App
* 弹回 home 已由 ACT_FOCUS(home) 更新(非 keep_awake),此兜底与其幂等不冲突。 */
meow_fg_act_back();
/* AMSD_DESIGN §5.1:崩溃尾巴不丢 —— App 已死,终局排空剩余 stdout/stderr(含未完结行)
* 先转发 amsd,再报 EXIT / crash。仅当有读端(管道建立成功)才 drain。 */
if (out_fd >= 0) {
+34
View File
@@ -0,0 +1,34 @@
PASS S1 drain required (app was SIGSTOP/frozen)
PASS S1 no drain needed (media keep-alive, never stopped)
PASS S1 drain window > SDK 200ms throttle
PASS S1b keep-alive app needs EVIOCGRAB isolation
PASS S1b frozen app -> SIGCONT drain, not grab
PASS S1b keep-alive: grab path (drain==0)
PASS S1b frozen: drain path (grab==0)
PASS S2 force status bar re-sync after unlock
PASS S2b lock re-cover ms > SDK suppression window (200ms)
PASS S2b lock re-cover ms = 300 (marker-window margin)
PASS S1c wake, no sleep -> no lock
PASS S1c timeout sleep (auto) -> lock
PASS S1c manual power sleep (manual) -> lock (was missing)
PASS S1c both -> lock
PASS S4 system 0 (never) -> lock caps 30s
PASS S4 system absent -> lock caps 30s
PASS S4 system 30 -> 30
PASS S4 system 60 -> 30 (cap, shorter)
PASS S4 system 10 -> 10 (respect shorter)
PASS S4 system 1 -> 1 (respect shorter)
PASS S4 idle>=off & screen on -> self sleep
PASS S4 idle<off -> not yet
PASS S4 screen off -> no re-sleep
PASS S4 power edge -> wake to lock
PASS S4 touch -> wake to lock
PASS S4 no wake input -> stay asleep
PASS S3 app died -> unified exit path (redraw home)
PASS S3 app alive -> normal restore
PASS S3 abort=0 (menu lock, no overlay pid)
PASS S3 abort=0 (no overlay pid, zombie ignored)
PASS S3 abort=0 (overlay pid, app alive)
PASS S3 abort=1 (overlay pid + zombie -> abort lock)
ALL PASS (32 passed, 0 failed)
+11 -1
View File
@@ -58,7 +58,17 @@ void meow_panel_on(void)
{
int f = open(FB_DEV, O_RDWR);
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on");
/* 亮度恢复以**用户设置**为准,而非 meow_saved_brightness(可能过期)。
* watchdog 独立息屏(wd_panel_off 直接 write_brightness(0),见 meow_watchdog.c)**不会**
* 更新本进程 meow_saved_brightness——若用其恢复会回错值(真机「PIN 亮屏亮度跳高」,
* 2026-09-03)。settings 已从盘上读到→按 display.brightness(clamp)恢复;未读到(早启动)
* →回退 meow_saved_brightness 现值(保持旧行为,防开机竞态)。 */
if (meow_settings_loaded()) {
int b = meow_settings_brightness_clamped();
if (b > 0) { write_brightness(b, "meow_panel_on"); meow_saved_brightness = b; }
} else {
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on");
}
meow_screen_on = 1;
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
+246 -5
View File
@@ -38,6 +38,16 @@ static const int lk_keys[LK_ROWS][LK_COLS] = {
{ LK_ACT_CLR, 0, LK_ACT_BS },
};
/* ---- M7.1b 锁屏控件化:PIN 数字键盘 / 时钟 / 提示 / PIN 圆点 改用 SDK 控件 ----
* 旧自绘锁屏 UI 保留在 #if 0(回退位,见 lk_draw/lk_key_hit),新 SDK 渲染默认启用;
* run_lock_screen 会话骨架(EVIOCGRAB / 自睡 / 解锁脏区修复 / 物理键转发)不动,只替换
* 它调用的绘制/命中函数体。控件 owner 钉 LK_SENTINEL_ACT(子页 0x52 之外的独立哨兵)。 */
#define LK_SENTINEL_ACT 0x51 /* 锁屏控件 owner 哨兵(与子页 SUB_SENTINEL_ACT=0x52 区分) */
#define LK_DOWN_NONE (-10) /* 无键按下哨兵(关键值范围 LK_ACT_CLR=-2/-1/0-9) */
static meow_widget *lk_key_w[LK_ROWS][LK_COLS]; /* 12 键按钮(meow_widget_custom) */
static int lk_widgets_ready = 0;
static int g_lk_down_key = LK_DOWN_NONE; /* 当前按下的键(跟手指按压态) */
/* 键盘按钮 row*3+col 的命中矩形(x,y,w,h) */
static void lk_key_rect(int row, int col, int *x, int *y, int *w, int *h)
{
@@ -79,6 +89,121 @@ static void lk_draw_pin(int entered, const meow_ui_theme_t *th)
lk_pin_dot(x0 + i * gap, y, r, i < entered, th->primary, th->surface);
}
/* ---- M7.1b 控件化:SDK meow_widget 键盘 + meow_font 文字 ---- */
/* 数字键标签(1-9/0 单字符;static 缓冲各调用立即消费,无跨调用残留) */
static const char *lk_key_digit(int key)
{
static char d[2];
d[0] = (char)('0' + key);
d[1] = 0;
return d;
}
/* 键渲染:surfaceContainer 圆角底(MD3)+ 按压态 primary 26% α + 居中标签(SDK meow_font)。
* 颜色全部取 meow_theme_get()(SDK 单一权威:MD3/MIUIx × 明暗自动);读 w->state 的
* MEOW_WS_PRESSED 位(跟手指,由 lk_set_down_key 置/清)。 */
static void lk_key_render(meow_widget *w, const meow_theme *th, void *ctx)
{
int key = (int)(intptr_t)ctx;
int rc[4]; meow_widget_rect(w, rc); /* 绝对坐标 x,y,w,h(控件不透明,用 SDK 只读几何) */
int x = rc[0], y = rc[1], ww = rc[2], hh = rc[3];
unsigned st = meow_widget_state(w);
int rad = (th->radius > 0) ? th->radius : 10;
meow_gfx_fill_round(x, y, ww, hh, rad, th->surfaceContainer);
if (st & MEOW_WS_PRESSED)
meow_gfx_fill_round_alpha(x, y, ww, hh, rad, th->primary, 66);
const char *label;
int size;
if (key >= 0) { size = 30; label = lk_key_digit(key); }
else if (key == LK_ACT_CLR) { size = 22; label = mfont_ready ? "清空" : "C"; }
else { size = 22; label = mfont_ready ? "退格" : "DEL"; }
int cx = x + ww / 2;
int cy = y + hh / 2 - meow_ui_text_height(size) / 2;
meow_ui_text_center(cx, cy, label, size, th->onSurface);
}
/* 懒创建 12 键(owner=LK_SENTINEL_ACT;仅首次调用建一次)。布局几何沿用 lk_key_rect。 */
static void lk_widgets_build(void)
{
if (lk_widgets_ready) return;
meow_ui_activity_push(LK_SENTINEL_ACT);
for (int r = 0; r < LK_ROWS; r++) {
for (int c = 0; c < LK_COLS; c++) {
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
lk_key_w[r][c] = meow_widget_custom_create(x, y, w, h, lk_key_render, NULL,
(void *)(intptr_t)lk_keys[r][c]);
}
}
meow_ui_activity_pop();
lk_widgets_ready = 1;
}
/* 键值 -> 按钮(按压态置/清位用)。 */
static meow_widget *lk_widget_for_key(int key)
{
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_keys[r][c] == key) return lk_key_w[r][c];
return NULL;
}
/* 坐标 -> 键值(SDK meow_widget_hit;owner==active 已由调用方保证)。未命中返回 LK_DOWN_NONE。 */
static int lk_key_value_at(int x, int y)
{
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_key_w[r][c] && meow_widget_hit(lk_key_w[r][c], x, y))
return lk_keys[r][c];
return LK_DOWN_NONE;
}
/* 更新按压态(跟手指):清旧键状态位、置新键;返回 1=按键变化(调用方据此重绘)。 */
static int lk_set_down_key(int key)
{
if (key == g_lk_down_key) return 0;
meow_widget *old = lk_widget_for_key(g_lk_down_key);
if (old) meow_widget_set_state(old, MEOW_WS_PRESSED, 0);
meow_widget *nw = lk_widget_for_key(key);
if (nw) meow_widget_set_state(nw, MEOW_WS_PRESSED, 1);
g_lk_down_key = key;
return 1;
}
/* PIN 输入圆点(● 遮罩:已输入=primary 实心;未输入=primary 环 + surface 孔)。 */
static void lk_draw_pin_dots(int entered, const meow_theme *mt)
{
int cx = fb_xres / 2;
int y = 210;
int gap = 48, r = 20;
int x0 = cx - (3 * gap) / 2;
for (int i = 0; i < LCK_PIN_LEN; i++) {
int dx = x0 + i * gap;
fill_circle(dx, y, r, mt->primary);
if (i >= entered) fill_circle(dx, y, r - 3, mt->surface);
}
}
/* 键命中:旧自绘(lk_key_rect+mt_in)保留在 #if 0(回退位);M7.1b 走 SDK meow_widget_hit
* (owner==active + enabled 双闸门在 SDK 内;命中点=按下原点 mt->down_x/down_y,与原 mt_in 同源)。 */
#if 0
static int lk_key_hit(const meow_touch *mt, int row, int col)
{
int x, y, w, h; lk_key_rect(row, col, &x, &y, &w, &h);
return mt_in(mt, x, y, w, h);
}
#else
static int lk_key_hit(const meow_touch *mt, int row, int col)
{
if (!lk_key_w[row][col]) return 0;
return meow_widget_hit(lk_key_w[row][col], mt->down_x, mt->down_y);
}
#endif
#if 0
/* ============ 旧自绘锁屏整帧(M7.1b 回退位)============
* 原 lk_draw:fill_rect 全屏底 + lk_text(时钟/状态)+ lk_draw_pin(●)+ fill_round_rect 键盘。
* 一键回退:把下面 #if 0 改成 #if 1(并把新 lk_draw 的 #else 分支由 #else 改走旧轮廓),设备即回旧观感。 */
static void lk_draw(int entered, const char *status, time_t locked_until)
{
const meow_ui_theme_t *th = s_ui_theme();
@@ -116,6 +241,36 @@ static void lk_draw(int entered, const char *status, time_t locked_until)
fb_pan_buf0_force();
meow_ui_flush();
}
#else
/* ============ M7.1b 锁屏控件化整帧(默认启用)============
* 整帧不透明 surface 底(§310 脏区修复关键:全屏覆盖,无 App 帧透出)+ SDK 字体时钟/提示
* + 主题色 PIN 圆点 + meow_widget 键盘(surfaceContainer 圆角 / 按压态 primary 26% α / MD3 主题)。
* 颜色全部取 meow_theme_get()(SDK 单一权威:MD3/MIUIx × 明暗自动)。 */
static void lk_draw(int entered, const char *status, time_t locked_until)
{
const meow_theme *mt = meow_theme_get();
meow_ui_fill_rect(0, 0, fb_xres, fb_yres, mt->surface); /* 全屏不透明表面底 */
lk_widgets_build(); /* 懒创建 12 键 */
/* 时钟/提示(居中,SDK meow_font):锁定倒计时标题 */
meow_ui_text_center(fb_xres / 2, 120, locked_until > 0 ? "已锁定" : "输入 PIN 解锁",
24, mt->onSurface);
/* PIN 输入圆点(● 遮罩) */
lk_draw_pin_dots(entered, mt);
/* 状态行:错误提示(PIN 错误)着 error;倒计时/其它信息用 onSurfaceVariant */
int is_err = status[0] && (strstr(status, "PIN 错误") != NULL ||
strstr(status, "WRONG PIN") != NULL);
uint32_t status_col = status[0] ? (is_err ? mt->error : mt->onSurfaceVariant)
: mt->onSurfaceVariant;
meow_ui_text_center(fb_xres / 2, 260, status, 18, status_col);
/* 12 键(SDK meow_widget;命中走 lk_key_hit → meow_widget_hit) */
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_key_w[r][c]) meow_widget_draw(lk_key_w[r][c]);
meow_dirty_full();
fb_pan_buf0_force();
meow_ui_flush();
}
#endif
/* 阻塞式锁屏页。返回 1=已解锁,0=锁屏期间电源键息屏。 */
int run_lock_screen(void)
@@ -159,7 +314,26 @@ int run_lock_screen(void)
return 1;
}
lk_draw(entered, status, lock_until); /* 首帧(fill_rect 全屏不透明主题底 + 键盘) */
/* M7.1 解锁恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面不会自动重绘
* —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧(解锁后整屏脏区根因)。此处
* 在锁屏首扑前把底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;解锁(unlocked=1)
* 时整帧回写 back_buf + present,强制底层界面完整重绘(app_launcher.c 的 lock-overlay 关闭路径
* 随后 pm_capture_app_frame 读 fb0 即得到正确 App 帧,不再读到锁帧)。 */
unsigned char *lk_bg_snap = NULL;
size_t lk_bg_len = 0;
if (back_buf_ok && fb && fb_line > 0 && fb_yres > 0) {
lk_bg_len = (size_t)fb_line * (size_t)fb_yres;
lk_bg_snap = malloc(lk_bg_len);
if (lk_bg_snap) memcpy(lk_bg_snap, fb, lk_bg_len);
else { lk_bg_len = 0; logmsg("lock: bg snapshot malloc fail (len=%zu)", lk_bg_len); }
}
/* M7.1b:锁屏控件 owner 闸门打开(meow_widget_hit / 按压态需 active==LK_SENTINEL_ACT)。
* 会话骨架(EVIOCGRAB/自睡/解锁脏区修复)不受影响;done/died 处对称 pop。 */
meow_ui_activity_push(LK_SENTINEL_ACT);
g_lk_down_key = LK_DOWN_NONE;
lk_draw(entered, status, lock_until); /* 首帧(全屏不透明主题底 + 控件化键盘) */
int unlocked = 0;
unsigned long long entry_ms = now_ms();
@@ -173,6 +347,9 @@ int run_lock_screen(void)
unsigned long long last_act = now_ms();
int asleep = 0;
int prev_pwr_down = 0;
/* M7.1 锁屏期物理键保留语义(音量/媒体键):边沿跟踪基线(锁屏是 EVIOCGRAB 独占真模态,
* 这些键只能在本层显式处理,不会转发到系统)。 */
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_prv = 0, prev_nxt = 0;
for (;;) {
time_t now = time(NULL);
int redraw = 0;
@@ -246,6 +423,7 @@ int run_lock_screen(void)
meow_panel_on();
asleep = 0;
last_act = now_ms();
g_lk_down_key = LK_DOWN_NONE; /* M7.1b:唤醒复位按压态基线 */
lk_draw(entered, status, lock_until); /* 重画锁屏页回锁 */
continue;
}
@@ -256,6 +434,50 @@ int run_lock_screen(void)
meow_panel_off();
break; /* return 0 */
}
/* M7.1 锁屏期物理键仍执行其语义(用户定案:音量键调音量、媒体键控播放/上下曲)。
* 锁屏层 EVIOCGRAB 独占这些输入(真模态),不转发系统,故必须在此显式处理,不能依赖别处。
* 音量沿用 launcher 既有递增/递减路径(settings_volume_adj —— 起 1% 线性渐变,
* 长按由下方 vol_ramp_tick 每圈推进);播放/上下曲经 meow_media_playctl 下发媒体指令。 */
int vold = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
int volu = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_VOLUMEUP);
int play = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_PLAYPAUSE);
int prv = (kp_keys.ok && kp_is_down(&kp_keys, KEY_PREVIOUSSONG)) ||
(kp_power.ok && kp_is_down(&kp_power, KEY_PREVIOUSSONG));
int nxt = (kp_keys.ok && kp_is_down(&kp_keys, KEY_NEXT)) ||
(kp_power.ok && kp_is_down(&kp_power, KEY_NEXT));
if (vold && !prev_vold) { settings_volume_adj(-settings_volume_step); last_act = now_ms(); }
if (volu && !prev_volu) { settings_volume_adj(+settings_volume_step); last_act = now_ms(); }
if (play && !prev_play) {
const meow_media_session_t *ms = meow_media_current();
if (ms) {
uint32_t c = (ms->state == MEOW_MEDIA_PLAYING) ? 2u : 1u; /* 暂停/播放 */
int rc = meow_media_playctl(c, 0);
logmsg("lock: play key -> media cmd=%u rc=%d (%s)", c, rc, ms->app_id);
} else {
logmsg("lock: play key -> media-reserved (no media source)");
}
last_act = now_ms();
}
if (prv && !prev_prv) {
if (meow_media_current()) { int rc = meow_media_playctl(3, 0); logmsg("lock: prev key -> cmd=3 rc=%d", rc); }
else logmsg("lock: prev key -> media-reserved (no media source)");
last_act = now_ms();
}
if (nxt && !prev_nxt) {
if (meow_media_current()) { int rc = meow_media_playctl(4, 0); logmsg("lock: next key -> cmd=4 rc=%d", rc); }
else logmsg("lock: next key -> media-reserved (no media source)");
last_act = now_ms();
}
prev_vold = vold; prev_volu = volu; prev_play = play; prev_prv = prv; prev_nxt = nxt;
/* 长按音量键线性渐变(同主菜单 vol_ramp 路径,每圈推进 1%)。锁屏态不刷状态栏
* (状态栏被锁屏页覆盖),仅保持真实音量/DAC 同步。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0)
? (kp_keys.ok && kp_is_down(&kp_keys, KEY_VOLUMEUP))
: (kp_keys.ok && kp_is_down(&kp_keys, KEY_VOLUMEDOWN));
int going = vol_ramp_tick(held);
if (!going) logmsg("lock: volume ramp end -> %d", settings_volume_get());
}
/* 空闲超时 → 自睡(锁态保持,仍在锁屏循环,唤醒回锁屏页) */
if (meow_screen_on && now_ms() - last_act >= lk_off_ms) {
logmsg("lock: idle %ds -> self screen off (stay locked)", lk_off_sec);
@@ -265,8 +487,6 @@ int run_lock_screen(void)
}
}
}
(void)kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
(void)kp_is_down(&kp_keys, KEY_VOLUMEUP);
/* 触摸:只认键盘 tap(滑动手势忽略:不左缘返回、不通知中心下拉);触摸即输入活动,重置自睡计时 */
if (tfd >= 0 && !asleep) {
@@ -280,17 +500,24 @@ int run_lock_screen(void)
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
/* M7.1b 跟手指按压态:down 且两轴有效才定键(goodix 时序免疫);
* 键变化(滑动换键)即时重绘按压高亮。 */
if (mt.down && mt.last_x >= 0 && mt.last_y >= 0) {
if (lk_set_down_key(lk_key_value_at(mt.last_x, mt.last_y)))
redraw = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) {
mt_down(&mt);
} else if (!val && mt.down) {
lk_set_down_key(LK_DOWN_NONE); /* M7.1b:抬手清按压态 */
redraw = 1;
int dx, dy;
if (mt_up(&mt, &dx, &dy)) {
int handled = 0;
for (int r = 0; r < LK_ROWS && !handled; r++) {
for (int c = 0; c < LK_COLS && !handled; c++) {
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
if (!mt_in(&mt, x, y, w, h)) continue;
if (!lk_key_hit(&mt, r, c)) continue; /* M7.1b SDK 控件命中 */
handled = 1;
int key = lk_keys[r][c];
if (lock_until > 0) break; /* 锁定期间键盘不响应 */
@@ -339,6 +566,17 @@ int run_lock_screen(void)
usleep(POLL_MS * 1000);
}
done:
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
g_lk_down_key = LK_DOWN_NONE;
/* M7.1 解锁恢复:把锁屏首扑前快照的底层帧整帧回写 back_buf + present,强制底层 App/主界面
* 完整重绘(消除「解锁后整屏 = PIN 最后一帧,仅 App 控件自绘区刷新」的脏区残留)。 */
if (unlocked && lk_bg_snap && back_buf_ok && fb && lk_bg_len > 0) {
memcpy(fb, lk_bg_snap, lk_bg_len);
meow_dirty_full();
meow_ui_flush();
logmsg("lock: unlock -> restore underlying frame (full repaint)");
}
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
if (tfd >= 0) {
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 也会自动解除,双保险) */
ioctl(tfd, EVIOCGRAB, &grab);
@@ -349,6 +587,9 @@ done:
logmsg("lock: exit unlocked=%d screen_on=%d", unlocked, meow_screen_on);
return unlocked;
died:
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
g_lk_down_key = LK_DOWN_NONE;
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
if (tfd >= 0) {
int grab = 0;
ioctl(tfd, EVIOCGRAB, &grab);
+360 -23
View File
@@ -20,7 +20,7 @@ int menu_opened_blank = 0;
static void launch_fail_toast(char *toast, size_t n, const char *name)
{
const char *bd = meow_dep_block_reason();
if (bd && bd[0]) snprintf(toast, n, "缺依赖: %s", bd);
if (bd && bd[0]) snprintf(toast, n, meow_str_zh("menu.missing_dep", "缺依赖: %s"), bd);
else snprintf(toast, n, "CANNOT START: %s", name);
}
@@ -102,7 +102,8 @@ static void draw_header_content(void)
int tsize = menu_title_size(); /* 标题字号接 font.size.title */
/* M2.6 标题真字体(字号接 font.size.title;回退点阵3x);MD3 on_surface 主文字色 */
if (mfont_ready) {
meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(th->on_surface), tsize);
meow_font_drawtext((uint16_t *)fb, 24, 48, meow_str_zh("menu.title", "MeowOS 应用"),
rgb565(th->on_surface), tsize);
fill_rect(0, 48 + meow_font_line_height(tsize) + 2, fb_xres, 2, th->outline);
} else {
draw_text(24, 28, "MEOWAPPS", 3, th->on_surface);
@@ -171,6 +172,37 @@ static meow_img *s_app_icons[MAX_APPS];
/* 当前按压命中的应用格(-1=无);触摸按下置位、抬起清除(按压态视觉) */
static int g_menu_pressed = -1;
/* ================================================================== */
/* M7.4:主界面应用区迁 SDK 虚拟化控件(meow_lazygrid 宫格 + meow_listview 列表 + meow_scroll)。
*
* 铁律(M7 §4/§5):开机必经主界面,最高风险。本块只替换「绘制函数体 + 命中函数体」——
* draw_menu 应用区体、menu_hit_app 体、menu_cell_rect 体;run_menu 会话骨架(触摸/热键/IPC/
* 息屏/自动超时/跨会话)逐字保留。旧手绘路径(draw_menu 手绘循环/menu_draw_app_cell/menu_cell_rect
* 旧体/menu_hit_app 旧体)各以 #if 0 保留为回退位;新 SDK 路径默认启用。
*
* 控件实例惰性创建一次(首跑 ensure),跨多次 run_menu 复用;热路径(draw/hit/scroll)零 malloc
* (SDK lazygrid/listview 均静态有界池)。布局切换(ui.home_layout)在绘/命中时按 menu_layout()
* 选择对应控件(两者同存,互不干扰);主题色全取 s_ui_theme()(= SDK meow_theme 单一权威)。
*
* 几何同源:应用区视口菜单由 menu_app_region() 计算(与 menu_cell_rect/menu_hit_app/draw_menu
* 共用),保证虚拟化控件绘制的格/行位置 = 命中所用几何 = 脏区标脏几何,三者不错位。 */
#define MENU_SDK_ACT 0x55 /* 主界面控件 owner 哨兵(不与电源菜单0x50/NowPlaying0x51/子页0x52/
* 设置页0x53/通知中心0x54 冲突) */
/* 数据源 ctx(bind/count/draw 回调读应用数组;run_menu 的 apps[]/napps 每帧经此对齐)。 */
typedef struct { struct app *apps; int napps; } menu_sdk_ctx_t;
static menu_sdk_ctx_t s_menu_ctx; /* 应用数据源(count/bind/draw 读) */
static meow_lazygrid *s_menu_grid = NULL; /* 宫格(layout=0) */
static meow_scroll *s_menu_grid_sc = NULL;/* 宫格滚动物理(scroll_attach) */
static meow_widget *s_menu_list = NULL; /* 列表(layout=1) */
static meow_scroll *s_menu_list_sc = NULL;/* 列表滚动物理 */
static int s_menu_sdk_init = 0; /* 控件集已建(首跑建一次,跨次复用) */
/* 前置声明(menu_cell_rect 定义在下方 451 行,早于本块函数定义;此处仅给签名)。 */
static void menu_app_region(int apps_top, int layout, int *vx, int *vy, int *vw, int *vh);
static int menu_grid_cell_w(void);
static void menu_sdk_ensure(int apps_top);
/* 背景状态(display.background:纯色名 or 图片路径) */
static meow_img *s_bg_img = NULL; /* 解码后的背景图(缓存,NULL=纯色) */
static int s_bg_is_img = 0; /* 1=图片背景 */
@@ -334,6 +366,27 @@ static void menu_bg_paint(void)
}
}
/* 局部重铺背景(x,y,w,h 区域):图片缓存 blit 对应行或纯色填充。供宫格滚动局部刷时重铺背景区,
* 修「自定义图片背景 + 超屏滚动」残影(M7.4 遗留②)——lazygrid_scroll_attach 的 redraw 只重画
* 格而不重铺背景,超屏滚动后视口内遗留旧帧像素。与 menu_bg_paint 同源(s_bg_cache/menu_bg)。 */
static void menu_bg_paint_region(int x, int y, int w, int h)
{
int W = fb_xres, H = fb_yres;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > W) w = W - x;
if (y + h > H) h = H - y;
if (w <= 0 || h <= 0) return;
if (s_bg_is_img && s_bg_img && s_bg_cache) {
int pitch = meow_ui_back_buf_pitch();
for (int r = 0; r < h; r++)
memcpy(fb + (size_t)(y + r) * pitch + (size_t)x * 2,
s_bg_cache + (size_t)(y + r) * W + x, (size_t)w * 2);
} else {
fill_rect(x, y, w, h, menu_bg);
}
}
/* 应用图标路径:app.json icon 相对应用目录(dirname(exec))/ 绝对;失败回退卡根相对。 */
static void menu_icon_path(const struct app *a, char *out, int olen)
{
@@ -447,10 +500,30 @@ static void menu_draw_icon(const struct app *a, meow_img *icon, int x, int y, in
if (icon) { meow_ui_img_cover(x, y, sz, icon); }
}
/* 应用格几何(grid/list 统一入口;供 draw 与 touch 命中共用)。 */
/* 应用格几何(grid/list 统一入口;供 draw 与 touch 命中共用)。
* M7.4:改从 SDK 控件取当前滚动偏移(虚拟化后格/行位置随 offset 移动),并用 menu_app_region
* 同源几何 —— 保证与 meow_lazygrid/listview 绘制、命中的坐标一致。 */
static void menu_cell_rect(int i, int layout, int apps_top,
int *cx, int *cy, int *cw, int *ch)
{
int vx, vy, vw, vh;
menu_app_region(apps_top, layout, &vx, &vy, &vw, &vh);
(void)vh;
int sc = (layout == 0) ? (s_menu_grid ? meow_lazygrid_scroll(s_menu_grid) : 0)
: (s_menu_list ? meow_listview_scroll(s_menu_list) : 0);
if (layout == 0) { /* card grid */
*cw = menu_grid_cell_w();
*ch = MENU_GRID_CARD_H;
int row = i / MENU_GRID_COLS, col = i % MENU_GRID_COLS;
*cx = vx + col * (*cw + MENU_GRID_GAP);
*cy = vy + row * (*ch + MENU_GRID_GAP) - sc;
} else { /* list */
*cw = vw;
*ch = MENU_LIST_ROW_H;
*cx = vx;
*cy = vy + i * MENU_LIST_ROW_H - sc;
}
#if 0 /* ===== M7.4 回退位:旧手绘几何(无滚动;删 #if 0 行切回)===== */
if (layout == 0) { /* card grid */
int row = i / MENU_GRID_COLS, col = i % MENU_GRID_COLS;
*cw = (fb_xres - 2 * MENU_GRID_PAD - (MENU_GRID_COLS - 1) * MENU_GRID_GAP) / MENU_GRID_COLS;
@@ -463,13 +536,22 @@ static void menu_cell_rect(int i, int layout, int apps_top,
*cx = 8;
*cy = apps_top + i * MENU_LIST_ROW_H;
}
#endif
}
/* 触摸命中应用格;未命中返回 -1。 */
/* 触摸命中应用格;未命中返回 -1。
* M7.4:手绘坐标循环替换为 SDK 控件命中(meow_lazygrid_hit / meow_listview_hit,含滚动换算 +
* 禁用/越界不响应);入口/会话判断(state/napps 门)保留。 */
static int menu_hit_app(int x, int y, struct app *apps, int napps, int state,
int layout, int apps_top)
{
(void)apps; /* 命中判定只依赖几何;apps 保留作接口(0-warning) */
(void)apps; /* 命中交由 SDK 控件(滚动+网格映射);apps 保留作接口(0-warning) */
(void)apps_top; /* SDK 控件自持视口几何;无需外部 apps_top */
if (state != 0 || napps <= 0) return -1;
if (layout == 0) return s_menu_grid ? meow_lazygrid_hit(s_menu_grid, x, y) : -1;
else return s_menu_list ? meow_listview_hit(s_menu_list, x, y) : -1;
#if 0 /* ===== M7.4 回退位:旧手绘命中循环(删 #if 0 行切回)===== */
(void)apps;
if (state != 0 || napps <= 0) return -1;
for (int i = 0; i < napps; i++) {
int cx, cy, cw, ch;
@@ -477,6 +559,7 @@ static int menu_hit_app(int x, int y, struct app *apps, int napps, int state,
if (x >= cx && x < cx + cw && y >= cy && y < cy + ch) return i;
}
return -1;
#endif
}
/* 宫格卡片:surface_container 底 + 圆角 + 选中/按压态 tonal 高亮 + 图标 + 名称。 */
@@ -498,12 +581,14 @@ static void menu_draw_grid_cell(const struct app *a, meow_img *icon, int x, int
int nw = ui_width(nm, 18);
ui_draw(x + (w - nw) / 2, name_y, nm, a->ok ? th->on_surface : th->error, 18);
if (!a->ok) {
int ew = ui_width("[缺失]", 14);
ui_draw(x + (w - ew) / 2, name_y + meow_font_line_height(18) + 2, "[缺失]", th->error, 14);
const char *miss = meow_str_zh("menu.missing", "[缺失]");
int ew = ui_width(miss, 14);
ui_draw(x + (w - ew) / 2, name_y + meow_font_line_height(18) + 2, miss, th->error, 14);
} else if (meow_bg_is_backgrounded(a->id)) {
/* M4:后台保活中标注(播放中转后台的 App;run_menu 1s tick 按 change_seq 刷新) */
int bw = ui_width("·后台", 14);
ui_draw(x + (w - bw) / 2, name_y + meow_font_line_height(18) + 2, "·后台",
const char *bg = meow_str_zh("menu.background", "·后台");
int bw = ui_width(bg, 14);
ui_draw(x + (w - bw) / 2, name_y + meow_font_line_height(18) + 2, bg,
th->on_surface_variant, 14);
}
}
@@ -537,7 +622,7 @@ static void menu_draw_list_cell(const struct app *a, meow_img *icon, int x, int
/* 副标题:package(无则 id),on_surface_variant;M4 后台保活中前缀「·后台 」 */
char sub[48];
snprintf(sub, sizeof sub, "%s%s",
meow_bg_is_backgrounded(a->id) ? "·后台 " : "",
meow_bg_is_backgrounded(a->id) ? meow_str_zh("menu.background_sp", "·后台 ") : "",
a->package[0] ? a->package : (a->id[0] ? a->id : ""));
if (sub[0]) {
char subf[44];
@@ -546,6 +631,7 @@ static void menu_draw_list_cell(const struct app *a, meow_img *icon, int x, int
}
}
#if 0 /* ===== M7.4 回退位:旧应用格分派器(新路径由 menu_sdk_*_draw 适配器经 SDK self-draw 分派)===== */
/* 应用格绘制入口:按布局分派。 */
static void menu_draw_app_cell(const struct app *a, meow_img *icon, int x, int y,
int w, int h, int sel, int pressed, int layout,
@@ -554,6 +640,145 @@ static void menu_draw_app_cell(const struct app *a, meow_img *icon, int x, int y
if (layout == 0) menu_draw_grid_cell(a, icon, x, y, w, h, sel, pressed, th);
else menu_draw_list_cell(a, icon, x, y, w, h, sel, pressed, th);
}
#endif
/* ================================================================== */
/* M7.4:主界面 SDK 控件辅助(menu_app_region 几何 / menu_grid_cell_w / 数据源 bind / SDK 自绘
* 适配器 / meow_scroll 回调 / menu_sdk_ensure 惰性创建)。与 draw_menu 同放,故可引用其上的
* menu_apps_top/menu_draw_grid_cell/menu_draw_list_cell/menu_name_fit 等。 */
/* 应用区视口矩形(与 menu_cell_rect/menu_hit_app/draw_menu 共用);layout 0=宫格 1=列表。
* 顶=apps_top,底=设置行上方;宫格视口 x=12(PAD)、宽=xres-2*PAD;列表视口 x=8、宽=xres-16。 */
static void menu_app_region(int apps_top, int layout, int *vx, int *vy, int *vw, int *vh)
{
int bottom = EXIT_ROW - ROW_H - 4; /* 设置行顶(draw_menu fill_round(8, sy-4)) */
*vy = apps_top;
if (layout == 0) { *vx = MENU_GRID_PAD; *vw = fb_xres - 2 * MENU_GRID_PAD; }
else { *vx = 8; *vw = fb_xres - 16; }
*vh = bottom - apps_top;
if (*vh < 0) *vh = 0;
}
/* 宫格单格宽(与旧 menu_cell_rect 同式:总宽-左右 PAD-内 gap,均分到 cols) */
static int menu_grid_cell_w(void)
{
return (fb_xres - 2 * MENU_GRID_PAD - (MENU_GRID_COLS - 1) * MENU_GRID_GAP) / MENU_GRID_COLS;
}
/* ---- 数据源(count 每帧实时查;bind 仅进视口时填一次 item;阅读 s_menu_ctx)---- */
static int menu_grid_count(void *ctx) { menu_sdk_ctx_t *c = (menu_sdk_ctx_t *)ctx; return c ? c->napps : 0; }
static void menu_grid_bind(int index, meow_lazygrid_item *item, void *ctx)
{
menu_sdk_ctx_t *c = (menu_sdk_ctx_t *)ctx;
memset(item, 0, sizeof *item);
if (!c || index < 0 || index >= c->napps) return;
struct app *a = &c->apps[index];
item->title = a->name;
item->subtitle = NULL; /* 名为主文本;状态/缺失/后台标注由自绘 draw 分色绘制 */
item->icon = (index < MAX_APPS) ? s_app_icons[index] : NULL;
item->enabled = 1; /* 命中即启动(含缺 app → 启动失败→toast,同旧行为) */
item->user = (void *)a;
}
static int menu_list_count(void *ctx) { menu_sdk_ctx_t *c = (menu_sdk_ctx_t *)ctx; return c ? c->napps : 0; }
static void menu_list_bind(int index, meow_lv_item *item, void *ctx)
{
menu_sdk_ctx_t *c = (menu_sdk_ctx_t *)ctx;
memset(item, 0, sizeof *item);
if (!c || index < 0 || index >= c->napps) return;
struct app *a = &c->apps[index];
item->title = a->name;
item->subtitle = NULL; /* 自绘 draw 显示 package/id 副标题(含「·后台」前缀) */
item->icon = (index < MAX_APPS) ? s_app_icons[index] : NULL;
item->icon_kind = item->icon ? MEOW_LIST_ICON_IMG : MEOW_LIST_ICON_NONE;
item->enabled = 1;
item->user = (void *)a;
}
/* ---- SDK 自绘适配器(把 SDK 的 (idx,x,y,w,h,state) 映射到旧格/行绘制内核,迁移其内容)---- */
static void menu_sdk_grid_draw(int index, int x, int y, int w, int h, int state, void *ctx)
{
menu_sdk_ctx_t *c = (menu_sdk_ctx_t *)ctx;
const meow_ui_theme_t *th = s_ui_theme();
if (!c || index < 0 || index >= c->napps) return;
int sel = (state & MEOW_LAZYGRID_ST_SELECTED) ? 1 : 0;
int pressed = (g_menu_pressed == index) ? 1 : 0;
menu_draw_grid_cell(&c->apps[index], (index < MAX_APPS) ? s_app_icons[index] : NULL,
x, y, w, h, sel, pressed, th);
}
static void menu_sdk_list_draw(int index, int x, int y, int w, int h, int state, void *ctx)
{
menu_sdk_ctx_t *c = (menu_sdk_ctx_t *)ctx;
const meow_ui_theme_t *th = s_ui_theme();
if (!c || index < 0 || index >= c->napps) return;
int sel = (state & MEOW_LISTVIEW_ST_SELECTED) ? 1 : 0;
int pressed = ((state & MEOW_LISTVIEW_ST_PRESSED) || g_menu_pressed == index) ? 1 : 0;
menu_draw_list_cell(&c->apps[index], (index < MAX_APPS) ? s_app_icons[index] : NULL,
x, y, w, h, sel, pressed, th);
}
/* ---- meow_scroll 回调(列表;包 listview。宫格用 meow_lazygrid_scroll_attach 自带回调)---- */
static void menu_lv_sc_set(void *ctx, int off) { (void)ctx; if (s_menu_list) meow_listview_set_scroll(s_menu_list, off); }
static int menu_lv_sc_max(void *ctx) { (void)ctx; return s_menu_list ? meow_listview_max_scroll(s_menu_list) : 0; }
static int menu_lv_sc_get(void *ctx) { (void)ctx; return s_menu_list ? meow_listview_scroll(s_menu_list) : 0; }
/* 列表区重刷(含内容底 + listview),供 fling/滚动局部刷;不整屏。 */
static void menu_lv_sc_redraw(void *ctx)
{
(void)ctx;
int vx, vy, vw, vh; menu_app_region(menu_apps_top(), 1, &vx, &vy, &vw, &vh);
if (vh <= 0) return;
meow_ui_fill_rect(vx, vy, vw, vh, s_ui_theme()->surface);
if (s_menu_list) meow_widget_draw(s_menu_list);
meow_dirty_add(vx, vy, vw, vh);
meow_ui_flush();
}
/* ---- 宫格滚动物理回调(M7.4 遗留②:不用 lazygrid_scroll_attach,改自定义 meow_scroll 会话,
* 使 redraw 先重铺背景区再画格——attach 自带的 redraw 只刷视口 meow_gfx,自定义图片背景 +
* 超屏滚动会产生残影。set/max/get 仍指回 lazygrid,滚动语义不变。---- */
static void menu_grid_sc_set(void *ctx, int off) { (void)ctx; if (s_menu_grid) meow_lazygrid_set_scroll(s_menu_grid, off); }
static int menu_grid_sc_max(void *ctx) { (void)ctx; return s_menu_grid ? meow_lazygrid_max_scroll(s_menu_grid) : 0; }
static int menu_grid_sc_get(void *ctx) { (void)ctx; return s_menu_grid ? meow_lazygrid_scroll(s_menu_grid) : 0; }
static void menu_grid_sc_redraw(void *ctx)
{
(void)ctx;
int vx, vy, vw, vh; menu_app_region(menu_apps_top(), 0, &vx, &vy, &vw, &vh);
if (vh <= 0) return;
menu_bg_paint_region(vx, vy, vw, vh); /* 重铺背景区:防图片背景残影 */
if (s_menu_grid) meow_lazygrid_draw(s_menu_grid, meow_theme_get());
meow_dirty_add(vx, vy, vw, vh);
meow_ui_flush();
}
/* ---- 惰性创建一次(首跑 ensure;跨多次 run_menu 复用)。几何以当前 apps_top 定 baseline。---- */
static void menu_sdk_ensure(int apps_top)
{
if (s_menu_sdk_init) return;
meow_ui_activity_push(MENU_SDK_ACT); /* 控件 owner 钉哨兵(与设置页/通知中心同款) */
/* 宫格 */
int gx, gy, gw, gh; menu_app_region(apps_top, 0, &gx, &gy, &gw, &gh);
s_menu_grid = meow_lazygrid_create(gx, gy, gw, gh, MENU_GRID_COLS, menu_grid_cell_w(),
MENU_GRID_CARD_H, MENU_GRID_GAP);
if (s_menu_grid) {
meow_lazygrid_set_vsource(s_menu_grid, menu_grid_count, menu_grid_bind, &s_menu_ctx);
meow_lazygrid_set_item_draw(s_menu_grid, menu_sdk_grid_draw, &s_menu_ctx);
meow_lazygrid_set_empty_text(s_menu_grid, meow_str_zh("menu.empty_app", "没有找到应用"));
/* M7.4 遗留②:自定义会话(redraw 重铺背景),替代 lazygrid_scroll_attach(见 menu_grid_sc_redraw 注) */
s_menu_grid_sc = meow_scroll_create(menu_grid_sc_set, menu_grid_sc_max, menu_grid_sc_get,
menu_grid_sc_redraw, NULL);
}
/* 列表 */
int lx, ly, lw, lh; menu_app_region(apps_top, 1, &lx, &ly, &lw, &lh);
s_menu_list = meow_listview_create(lx, ly, lw, lh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
if (s_menu_list) {
meow_listview_set_vsource(s_menu_list, menu_list_count, menu_list_bind, &s_menu_ctx);
meow_listview_set_item_draw(s_menu_list, menu_sdk_list_draw, &s_menu_ctx);
meow_listview_set_item_h(s_menu_list, MENU_LIST_ROW_H);
meow_listview_set_empty_text(s_menu_list, meow_str_zh("menu.empty_app", "没有找到应用"));
s_menu_list_sc = meow_scroll_create(menu_lv_sc_set, menu_lv_sc_max, menu_lv_sc_get,
menu_lv_sc_redraw, NULL);
}
meow_ui_activity_pop();
s_menu_sdk_init = 1;
}
static void draw_menu(struct app apps[MAX_APPS], int napps, int state,
@@ -572,6 +797,96 @@ static void draw_menu(struct app apps[MAX_APPS], int napps, int state,
menu_bg_paint();
draw_header_content();
if (state == 1) {
if (mfont_ready) {
ui_draw(30, 200, meow_str_zh("menu.no_card", "没有存储卡"), th->on_surface, bsize);
ui_draw(30, 234, meow_str_zh("menu.no_card_hint", "请插入TF卡后重开菜单"),
th->on_surface_variant, bsize);
} else {
draw_text(30, 200, "NO STORAGE CARD", 2, th->on_surface);
draw_text(30, 240, "INSERT TF CARD", 2, th->on_surface_variant);
draw_text(30, 272, "THEN RE-OPEN MENU", 2, th->on_surface_variant);
}
} else if (state == 2 || napps == 0) {
if (mfont_ready) {
ui_draw(30, 200, meow_str_zh("menu.empty_app", "没有找到应用"), th->on_surface, bsize);
ui_draw(30, 234, meow_str_zh("menu.empty_app_hint", "请放入应用到"),
th->on_surface_variant, bsize);
ui_draw(30, 268, "MeowAPPs/<id>/app.json", th->on_surface_variant, bsize);
} else {
draw_text(30, 200, "NO APPS FOUND", 2, th->on_surface);
draw_text(30, 240, "PUT APP IN", 2, th->on_surface_variant);
draw_text(30, 272, "MeowAPPs/<id>/app.json", 2, th->on_surface_variant);
}
} else {
/* M7.4:应用区迁 SDK 虚拟化控件(layout=0 → meow_lazygrid;layout=1 → meow_listview)。
* 数据源 ctx 对齐当前 apps[]/napps;控件首跑建一次,跨次复用;绘制/命中/脏区几何同源
* (menu_app_region)。超屏滚动由 meow_scroll 接管(宫格 scroll_attach / 列表手动会话)。 */
s_menu_ctx.apps = apps; s_menu_ctx.napps = napps;
menu_sdk_ensure(apps_top);
int vx, vy, vw, vh; menu_app_region(apps_top, layout, &vx, &vy, &vw, &vh);
if (layout == 0) {
if (s_menu_grid) {
meow_lazygrid_set_rect(s_menu_grid, vx, vy, vw, vh);
meow_lazygrid_set_sel(s_menu_grid, (cursor >= 0 && cursor < napps) ? cursor : -1);
meow_lazygrid_draw(s_menu_grid, meow_theme_get());
}
} else {
if (s_menu_list) {
meow_widget_set_rect(s_menu_list, vx, vy, vw, vh);
meow_listview_set_sel(s_menu_list, (cursor >= 0 && cursor < napps) ? cursor : -1);
meow_widget_draw(s_menu_list);
}
}
}
/* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前;
* 音量导航可把光标移到此处(cursor==napps),触摸点击该行进入设置页。 */
{
int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */
int selset = (cursor == napps); /* 光标已移到设置行 */
meow_gfx_fill_round(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container);
if (selset) meow_gfx_fill_round_alpha(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->primary, 40);
if (mfont_ready) ui_draw(24, sy, meow_str_zh("menu.settings", "系统设置"), th->on_surface, bsize);
else draw_text(24, sy, "SETTINGS", 2, th->on_surface);
s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, th->on_surface_variant);
}
/* exit row */
meow_gfx_fill_round(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container);
/* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */
if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? meow_str_zh("menu.reboot_player", "重启返回播放器")
: meow_str_zh("menu.back_player", "返回播放器"),
th->on_surface, bsize);
else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, th->on_surface);
/* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。字号 20 真实行高 meow_font_line_height(20)。
* 非 mfont 回退分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */
#define FOOTY_PAD 8
{
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(bsize) - FOOTY_PAD;
ui_draw(fb_xres - 8 - ui_width(ver, bsize), foot_y, ver, th->on_surface_variant, bsize);
}
if (toast_ms > 0 && toast && toast[0]) {
fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, th->surface_variant);
ui_draw(30, TOAST_Y, toast, th->error, bsize);
}
#if 0 /* ===== M7.4 回退位:旧手绘主界面全屏绘制(应用区手绘宫格/列表)。
* 回退 = 删本 #if 0 / #endif 行 + 注释上方新 SDK 路径;run_menu 会话骨架不在此。 ===== */
const meow_ui_theme_t *th = s_ui_theme();
int layout = menu_layout();
int apps_top = menu_apps_top();
int bsize = 20;
meow_settings_get_int("font.size.body", &bsize);
if (bsize < 12) bsize = 12;
if (bsize > 28) bsize = 28;
menu_bg_prepare(th);
menu_bg_paint();
draw_header_content();
if (state == 1) {
if (mfont_ready) {
ui_draw(30, 200, "没有存储卡", th->on_surface, bsize);
@@ -600,27 +915,23 @@ static void draw_menu(struct app apps[MAX_APPS], int napps, int state,
layout, th);
}
}
/* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前;
* 音量导航可把光标移到此处(cursor==napps),触摸点击该行进入设置页。 */
/* M2.36 B2b:系统设置 builtin 行(同上方新路径) */
{
int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */
int selset = (cursor == napps); /* 光标已移到设置行 */
int sy = EXIT_ROW - ROW_H;
int selset = (cursor == napps);
meow_gfx_fill_round(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container);
if (selset) meow_gfx_fill_round_alpha(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->primary, 40);
if (mfont_ready) ui_draw(24, sy, "系统设置", th->on_surface, bsize);
if (mfont_ready) ui_draw(24, sy, meow_str_zh("menu.settings", "系统设置"), th->on_surface, bsize);
else draw_text(24, sy, "SETTINGS", 2, th->on_surface);
s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, th->on_surface_variant);
}
/* exit row */
meow_gfx_fill_round(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container);
/* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */
if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", th->on_surface, bsize);
if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? meow_str_zh("menu.reboot_player", "重启返回播放器")
: meow_str_zh("menu.back_player", "返回播放器"),
th->on_surface, bsize);
else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, th->on_surface);
/* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。字号 20 真实行高 meow_font_line_height(20)。
* 非 mfont 回退分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */
/* footer(底部对齐) */
#define FOOTY_PAD 8
{
char ver[32];
@@ -628,11 +939,11 @@ static void draw_menu(struct app apps[MAX_APPS], int napps, int state,
int foot_y = fb_yres - meow_font_line_height(bsize) - FOOTY_PAD;
ui_draw(fb_xres - 8 - ui_width(ver, bsize), foot_y, ver, th->on_surface_variant, bsize);
}
if (toast_ms > 0 && toast && toast[0]) {
fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, th->surface_variant);
ui_draw(30, TOAST_Y, toast, th->error, bsize);
}
#endif
}
@@ -784,6 +1095,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */
unsigned long long apps_sig = 0; /* M2.77:应用目录签名(0=未初始化,首圈建立基线) */
unsigned long long fonts_sig = 0; /* M2.83:字库目录签名(门控 scan_presets,防 2s 轮询 sha256 风暴) */
#if 0 /* ===== M7.4 遗留① 补位(默认关闭):主界面宫格/列表超屏滚动喂给。
* 作用:应用数 > 视口(max_scroll>0)时,run_menu 触摸循环内喂 meow_scroll_track(拖动跟
* 手)+ 松手 meow_scroll_fling(惯性)。当前 MAX_APPS=8 装得下(max_scroll==0),喂给为
* no-op(安全);未来应用 >8 时删除下述三处 #if 0 行即启用,run_menu 会话骨架逐字不变
* (只补喂、不删骨架)。------------------------------------- */
int sg_track_y = -1; /* 本次手势上一 y(增量 dw 用;<=8 应用从不喂) */
#endif
load_config(); /* meow.conf on the card: bg_color etc. */
menu_icons_load(apps, napps); /* MD3:应用图标解码一次进缓存(不每帧解码) */
@@ -1289,6 +1607,19 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
need_menu_paint = 1;
}
}
#if 0 /* M7.4 遗留① 补位:拖动超屏滚动喂 meow_scroll_track(增量 dw)。≤8 应用 max_scroll==0 → no-op。
* 启用(删本 #if 0/#endif)后:移动手指即在应用区/列表区跟手滚动,骨架其它判定不变。 */
if (t_down && t_down_valid && t_last_y >= 0) {
if (sg_track_y < 0) sg_track_y = t_last_y;
int dw = t_last_y - sg_track_y;
if (dw != 0) {
if (menu_layout() == 0) { if (s_menu_grid_sc) meow_scroll_track(s_menu_grid_sc, dw, now_ms()); }
else { if (s_menu_list_sc) meow_scroll_track(s_menu_list_sc, dw, now_ms()); }
sg_track_y = t_last_y;
need_menu_paint = 1;
}
}
#endif
/* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手):
* 原点锁定 + 起点在顶部热区(y<SB_BAR_H) + 下拉达 NCP_DRAG_ENTER_SLOP →
* 立即切入 run_notif_center(此时手指仍按下拖动),由面板内拖动路径把 progress
@@ -1372,6 +1703,12 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
}
else if (!val && t_down) {
t_down = 0;
#if 0 /* M7.4 遗留① 补位:松手 fling(惯性滚动)。≤8 应用 max_scroll==0 → fling 无异动(no-op)。
* 启用(删本 #if 0/#endif)后:快速滑动松开即惯性停,跟手已由上方 track 喂给。 */
if (s_menu_grid_sc) meow_scroll_fling(s_menu_grid_sc);
if (s_menu_list_sc) meow_scroll_fling(s_menu_list_sc);
sg_track_y = -1;
#endif
if (!t_down_valid) {
/* 本次按下从未锁到有效坐标(坐标滞后/ghost 空按下):丢本次。 */
logmsg("menu tap dropped: no valid origin (last %d,%d)",
+6
View File
@@ -35,6 +35,12 @@
/* 列表编码上限(count 个 NUL 字符串时,distinct 工作缓冲上限) */
#define MEOW_IPC_CARGO 1024u /* 守护进程入队拷贝上限(可小于 MEOW_IPC_MAX) */
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE.md,③混合)----
* meow_svc_perf(C:/proc/meminfo Cached 监控降载)把「是否暂停下载/预取」写入此共享状态文件
* (一行式 key=value;App/SDK meow_cachegov 只读,系统级单点,不重复起线程)。
* 路径可被 env MEOW_CACHEGOV_C_FILE 覆盖(宿主测试隔离)。 */
#define MEOW_CACHEGOV_C_FILE "/tmp/meow_cachegov_c.state"
/* ---- 消息类型(FRAMEWORK_DESIGN §1 表)---- */
enum {
MSG_NONE = 0x0000,
Binary file not shown.
+370 -202
View File
@@ -23,13 +23,14 @@ int g_notif_ctrl_died = 0;
static int g_ncp_dnd = 0;
/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。
* g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。
* run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint)
* 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */
/* ⑦ 行左滑删除的绘制态(供旧手绘 ncp_draw_list / ncp_repaint_row 画该行位移)——M7.3 已迁 SDK
* meow_listview 左滑删除(SDK 内部持有 swipe_idx/swipe_dx,不再需要本组全局)。
* 保留在 #if 0 回退区内(随旧手绘内层一起重出)。 */
#if 0
static int g_ncp_swipe_row = -1;
static int g_ncp_swipe_off = 0;
#endif
/* ---- 阶段D附录B:通知中心顶部媒体卡片(固定于通知列表上方,活动媒体会话时显示)---- */
@@ -59,6 +60,58 @@ static int ncp_list_view(void) { int v = NCP_LIST_BOT - ncp_list_top(); return v
#define NCP_MEDIA_BTN_R 22 /* play 半径;prev/next 略小 */
#define NCP_MEDIA_PROG_Y 74 /* 相对卡片顶的进度条 top */
/* M7.0b:媒体卡片三控制按钮改用 SDK meow_iconbtn + sb_icons 矢量图标(与 Now Playing 同源),
* 取代原先手绘三角/竖条(ncp_media_tri/ncp_media_btn,用户反馈手绘有 bug)。up: 圆钮尺寸由
* meow_iconbtn 的 size 决定(ink=0.6*size),此处 prev/next 用 NCP_MEDIA_BTN_R-4、play 用
* NCP_MEDIA_BTN_R(与旧几何一致,命中仍按三圆心——ncp_media_card_tap 不变)。控件 lazy 建一次,
* owner=哨兵 0x52(不与电源菜单 0x50 / Now Playing 0x51 冲突);绘制经 meow_widget_draw。 */
#define NCP_MEDIA_ACT 0x52
#define NCP_MEDIA_PREV_R (NCP_MEDIA_BTN_R - 4) /* 18 */
#define NCP_MEDIA_NEXT_R (NCP_MEDIA_BTN_R - 4) /* 18 */
static meow_widget *g_ncp_btn_prev = NULL, *g_ncp_btn_play = NULL;
static meow_widget *g_ncp_btn_pause = NULL, *g_ncp_btn_next = NULL;
static int g_ncp_media_btn_init = 0;
/* 创建三控制按钮(owner 钉哨兵;play/pause 同位叠两钮,按状态只画其一——同 now_playing)。 */
static void ncp_media_btns_create(void)
{
meow_ui_activity_push(NCP_MEDIA_ACT);
int by = NCP_LIST_TOP + NCP_MEDIA_BTN_Y; /* 三钮圆心 y(与 ncp_draw_media_card/ncp_media_card_tap 同源) */
g_ncp_btn_prev = meow_iconbtn_create(NCP_MEDIA_PREV_CX - NCP_MEDIA_PREV_R, by - NCP_MEDIA_PREV_R,
NCP_MEDIA_PREV_R * 2, MEOW_ICON_SKIP_PREV);
g_ncp_btn_play = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PLAY_ARROW);
g_ncp_btn_pause = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PAUSE);
g_ncp_btn_next = meow_iconbtn_create(NCP_MEDIA_NEXT_CX - NCP_MEDIA_NEXT_R, by - NCP_MEDIA_NEXT_R,
NCP_MEDIA_NEXT_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮保持「强调动作」观感(与 Now Playing 一致):底色=primary、图标=onPrimary;其余默认。 */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
meow_ui_activity_pop();
g_ncp_media_btn_init = 1;
}
/* 绘制三控制按钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时新圆底盖掉旧)。
* 每次刷新播放钮底色/图标(同 now_playing np_refresh_theme:跨主题切换跟手)。 */
static void ncp_draw_media_btns(const meow_media_session_t *s)
{
if (!g_ncp_media_btn_init) ncp_media_btns_create();
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
if (g_ncp_btn_prev) { meow_widget_set_selected(g_ncp_btn_prev, 0); meow_widget_draw(g_ncp_btn_prev); }
if (g_ncp_btn_next) { meow_widget_set_selected(g_ncp_btn_next, 0); meow_widget_draw(g_ncp_btn_next); }
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
meow_widget *pb = playing ? g_ncp_btn_pause : g_ncp_btn_play;
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
}
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
static void ncp_strncpy(char *dst, const char *src, size_t len)
@@ -340,7 +393,8 @@ static void ncp_chip_state(int chip, int *on, char *val, int vlen)
} else if (chip == NCP_CHIP_THEME) {
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
*on = (v == 1);
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? meow_str_zh("ncp.theme_light", "浅色")
: meow_str_zh("ncp.theme_dark", "深色"));
} else if (chip == NCP_CHIP_BRIGHT) {
int v = 0; meow_settings_get_int("display.brightness", &v);
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
@@ -406,8 +460,199 @@ static int ncp_chip_apply(int chip)
}
/* ==================================================================== */
/* M7.3:通知中心内层迁 SDK 控件(芯片/双滑条/勿扰开关/通知列表/左滑删除) */
/* --------------------------------------------------------------------
* 定位:只迁「内层绘制/命中」。叠加会话骨架(SIGSTOP / 输入移交 / 恢复帧 / 顶条热区——见
* app_launcher.c run_app_notif_overlay)绝对不动,此处仅把 run_notif_center / draw_notif_center
* 中面板体的绘制与行命中交给 SDK meow_widget。SDK meow_ui.a 已定稿(c3d6371b),勿重编直接用。
*
* owner 钉哨兵 NCP_WIDGET_ACT(不冲突:0x50 电源菜单 / 0x51 锁屏·NowPlaying / 0x52 子页·媒体卡 /
* 0x53 设置页)。绘制经 meow_widget_draw / meow_listview_vs_draw;命中:通知列表走
* meow_listview_hit / meow_listview_swipe_begin·move·end(SDK 手势判定+删除/回弹动画),
* 芯片/滑条/勿扰开关沿用既有人工几何命中(与媒体卡 M7.0b 一致——叠加会话为手绘循环,不跑 SDK
* 输入线程,故不依赖 meow_widget_hit 的 owner==active 闸门)。 */
#define NCP_WIDGET_ACT 0x54
static meow_widget *g_ncp_lv = NULL; /* 通知列表:listview 虚拟化 + 行自绘 + 空态 + 左滑删除 */
static meow_widget *g_ncp_sl_bri = NULL; /* 亮度滑条 */
static meow_widget *g_ncp_sl_vol = NULL; /* 音量滑条 */
static meow_widget *g_ncp_sw_dnd = NULL; /* 勿扰开关 */
static meow_widget *g_ncp_chip[NCP_CHIP_COUNT]; /* 5 个快捷 chip(自定义画布:圆角 + 图标 + 标签) */
static int g_ncp_widget_init = 0;
static int g_ncp_need_paint = 0; /* M7.3:行左滑删除回调置位 → 主循环整面板重画 */
/* ---- 通知列表虚拟化源:count=当前行数(动态,逐帧实时查);bind 仅填 enabled 供
* meow_listview_hit 判定(行内容经 set_item_draw 自绘直读 g_rows[],无池缓存过期问题)。 ---- */
static int ncp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void ncp_lv_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
it->enabled = (idx >= 0 && idx < g_nrows) ? 1 : 0;
}
/* 文本截断(SDK meow_ui_text_width/meow_ui_text;UTF-8 安全,与旧 ncp_text 同构)。 */
static void ncp_mt_text(int x, int y, const char *s, int size, uint32_t col, int maxw)
{
if (!s) s = "";
if (meow_ui_text_width(s, size) <= maxw) { meow_ui_text(x, y, s, size, col); return; }
int ell = meow_ui_text_width("...", size);
int limit = maxw - ell;
char buf[128]; int len = 0;
while (s[len] && len < (int)sizeof buf - 8) {
unsigned char c0 = (unsigned char)s[len];
int cp = 1;
if ((c0 & 0xE0) == 0xC0) cp = 2;
else if ((c0 & 0xF0) == 0xE0) cp = 3;
else if ((c0 & 0xF8) == 0xF0) cp = 4;
if (len + cp >= (int)sizeof buf - 1) break;
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
buf[len + cp] = 0;
if (meow_ui_text_width(buf, size) > limit) { buf[len] = 0; break; }
len += cp;
}
if (len > 0) {
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
meow_ui_text(x, y, buf, size, col);
} else {
meow_ui_text(x, y, "...", size, col);
}
}
/* 通知行自绘(set_item_draw 覆盖默认 renderer)。x 已含左滑位移(SDK 先画右露删除区并把行 x 左移),
* 逐元素对齐旧 ncp_draw_row;颜色全取 meow_theme 语义色(M7 定案 #2 单一权威)。 */
static void ncp_lv_row_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)w; (void)state; (void)ctx; /* w=SDK 视口宽;行卡宽用 NCP_ROW_W(含 NCP_ROW_X 两侧留白) */
if (idx < 0 || idx >= g_nrows) return;
const ncp_row_t *r = &g_rows[idx];
const meow_theme *th = meow_theme_get();
meow_gfx_fill_round(x + NCP_ROW_X, y, NCP_ROW_W, h - 6, 12, th->surfaceContainer);
/* 图标 */
int icx = x + NCP_ICON_X, icy = y + (h - 32) / 2;
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
else {
const char *nm = ncp_app_name(r->app_id);
ncp_dot(icx + 16, icy + 16, 16, th->primary);
char ch = nm[0] ? nm[0] : '?';
char cbuf[4] = { ch, 0, 0, 0 };
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
int cw = meow_ui_text_width(cbuf, th->font_body);
meow_ui_text(icx + 16 - cw / 2, icy + 16 - meow_ui_text_height(th->font_body) / 2,
cbuf, th->font_body, th->onPrimary);
}
/* app 名 + 时间 */
meow_ui_text(x + NCP_TEXT_X, y + 10, ncp_app_name(r->app_id), th->font_body, th->onSurfaceVariant);
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
meow_ui_text(x + NCP_ROW_X + NCP_ROW_W - 8 - meow_ui_text_width(tbuf, th->font_body),
y + 12, tbuf, th->font_body, th->onSurfaceVariant);
/* 标题 + 内容(SDK 截断) */
ncp_mt_text(x + NCP_TEXT_X, y + 30, r->summary, th->font_body, th->onSurface, NCP_TEXT_W);
ncp_mt_text(x + NCP_TEXT_X, y + 56, r->body, th->font_body, th->onSurfaceVariant, NCP_TEXT_W);
/* 进度(progress>=0):细条 */
if (r->progress >= 0) {
int px = x + NCP_TEXT_X, pw = NCP_TEXT_W, py = y + h - 14;
meow_ui_fill_rect(px, py, pw, 4, th->surfaceVariant);
int fw = r->progress * pw / 100;
if (fw > 0) meow_ui_fill_rect(px, py, fw, 4, th->primary);
}
}
/* 左滑删除完成回调(SDK 删除/回弹动画结束后触发):撤下该通知 + 重采集 + 重排。 */
static void ncp_lv_on_delete(int idx, void *ctx)
{
(void)ctx;
logmsg("notif-center: swipe delete row %d", idx);
if (idx >= 0 && idx < g_nrows) meow_notif_dismiss(g_rows[idx].id);
ncp_refresh_rows();
/* 行数变化 → 列表重排:置位让主循环整面板重画(含媒体卡出现/消失重排 + 列表滚动重钳)。 */
g_ncp_need_paint = 1;
}
/* 芯片自绘(自定义画布 render_cb:TONAL 观感圆角 + 位图图标 + 标签,与旧 draw_notif_center chip 循环一致)。
* launcher 侧 meow_widget 不透明 → 用 meow_widget_rect 取绝对几何(同 power_menu pm_render_cb)。 */
static void ncp_chip_render(meow_widget *w, const meow_theme *th, void *ctx)
{
int c = (int)(intptr_t)ctx;
if (c < 0 || c >= NCP_CHIP_COUNT) return;
int rc[4]; meow_widget_rect(w, rc);
int x = rc[0], y = rc[1], wd = rc[2], ht = rc[3];
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surfaceContainer;
uint32_t fg = on ? th->onPrimary : th->onSurfaceVariant;
meow_gfx_fill_round(x, y, wd, ht, 12, bg);
int icon_cx = x + (wd - SB_ICON_SIZE) / 2;
int icon_cy = y + 14;
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_THEME) {
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
}
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_POWER) {
int pc = icon_cx + SB_ICON_SIZE / 2;
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
}
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
static const char *chip_id[NCP_CHIP_COUNT] = { "ncp.chip_wifi", "ncp.chip_bt",
"ncp.chip_theme", "ncp.chip_bright",
"ncp.chip_power" };
char label[24]; snprintf(label, sizeof label, "%s", meow_str_zh(chip_id[c], chip_lbl[c]));
if (val[0] && c == NCP_CHIP_BRIGHT) snprintf(label, sizeof label, "%.9s", val);
ui_draw(x + (wd - ui_width(label, 14)) / 2, y + ht - 18, label, fg, 14);
}
/* 芯片自定义画布命中(矩形;未被主循环使用——命中仍走 ncp_chip_hit 人工几何,此处仅满足 W_CUSTOM 契约)。 */
static int ncp_chip_widget_hit(meow_widget *w, int x, int y, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
return (x >= rc[0] && x < rc[0] + rc[2] && y >= rc[1] && y < rc[1] + rc[3]);
}
/* 创建 M7.3 内层控件集(owner 钉哨兵;仅首进创建一次,跨次复用——同媒体卡 M7.0b 惯用法)。 */
static void ncp_widgets_create(void)
{
meow_ui_activity_push(NCP_WIDGET_ACT);
/* 通知列表(视口 = 列表区;行高 NCP_ROW_H;空态 + 左滑删除) */
int lvy = ncp_list_top(), lvh = ncp_list_view();
g_ncp_lv = meow_listview_create(0, lvy, fb_xres, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_ncp_lv) {
meow_listview_set_vsource(g_ncp_lv, ncp_lv_count, ncp_lv_bind, NULL);
meow_listview_set_item_draw(g_ncp_lv, ncp_lv_row_draw, NULL);
meow_listview_set_item_h(g_ncp_lv, NCP_ROW_H);
meow_listview_set_empty_text(g_ncp_lv, meow_str_zh("ncp.empty", "暂无通知"));
meow_listview_set_swipe_delete(g_ncp_lv, 1, ncp_lv_on_delete, NULL);
}
/* 亮度/音量双滑条(track 中心对齐旧 s_draw_slider:NCP_SL*_Y+3) */
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int bval = 0; meow_settings_get_int("display.brightness", &bval);
bval = mnp_brightness_clamp(bval, bmin);
g_ncp_sl_bri = meow_slider_create(NCP_SLIDER_X, NCP_SL1_Y - 14, NCP_SLIDER_W, bmin, 100, bval);
int vmax = vol_clamp(settings_volume_max);
int vval = vol_clamp(settings_volume_get());
g_ncp_sl_vol = meow_slider_create(NCP_SLIDER_X, NCP_SL2_Y - 14, NCP_SLIDER_W, 0, vmax, vval);
/* 勿扰开关 */
g_ncp_sw_dnd = meow_switch_create(0, 0, g_ncp_dnd ? 1 : 0);
if (g_ncp_sw_dnd) meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48,
NCP_DND_Y + NCP_DND_H / 2 - 14, 48, 28);
/* 5 个快捷 chip(自定义画布;坐标与旧 ncp_chip_x / NCP_CHIP_Y 一致) */
for (int c = 0; c < NCP_CHIP_COUNT; c++)
g_ncp_chip[c] = meow_widget_custom_create(ncp_chip_x(c), NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H,
ncp_chip_render, ncp_chip_widget_hit,
(void *)(intptr_t)c);
meow_ui_activity_pop();
g_ncp_widget_init = 1;
}
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
#if 0 /* M7.3 回退位:旧手绘行删除底(SDK 左滑删除已接管此绘制,见 meow_listview swipe 渲染)。 */
static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th)
{
if (off >= 0) return;
@@ -520,6 +765,35 @@ static void ncp_repaint_row(int scroll, int row, int off)
meow_dirty_add(0, ry, fb_xres, NCP_ROW_H);
meow_ui_flush();
}
#endif /* M7.3 回退位结束(上述旧手绘 ncp_draw_delete_bg/ncp_draw_row/ncp_draw_list/
* ncp_repaint_list/ncp_repaint_row 重开本区即回退;新路径由下方 SDK 版接管)。 */
/* ---- M7.3:通知列表绘制(SDK listview 版,替换上方 #if 0 的手绘版)---- */
/* 绘制通知列表可见区(scroll=像素偏移)。同步 listview 视口矩形(媒体卡出现/消失时列表顶缘
* 变化)+ 滚动偏移,再画整个列表控件(虚拟化 + set_item_draw 行自绘 + 空态 + 左滑删除渲染
* 全由 SDK 完成)。 */
static void ncp_draw_list(int scroll)
{
if (!g_ncp_widget_init || !g_ncp_lv) return;
int lt = ncp_list_top(), lv = ncp_list_view();
meow_widget_set_rect(g_ncp_lv, 0, lt, fb_xres, lv);
meow_listview_set_scroll(g_ncp_lv, mnp_scroll_clamp(scroll, g_nrows * NCP_ROW_H, lv));
meow_widget_draw(g_ncp_lv);
}
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底,再画 SDK listview;
* 只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT))——「滚动只重绘列表可视区,绝不整屏」。 */
static void ncp_repaint_list(int scroll)
{
const meow_ui_theme_t *th = s_ui_theme();
int lt = ncp_list_top(), lv = ncp_list_view();
fill_rect(0, lt, fb_xres, lv, th->surface_variant);
ncp_draw_list(scroll);
meow_dirty_add(0, lt, fb_xres, lv);
meow_ui_flush();
}
/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段):
@@ -555,14 +829,20 @@ static void ncp_draw_dnd(const meow_ui_theme_t *th)
meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */
ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */
/* label + 状态 */
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18);
const char *dnd_lbl = meow_str_zh("ncp.dnd", "勿扰");
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, dnd_lbl, th->on_surface, 18);
char st[16];
snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关");
int lw = ui_width("勿扰", 18);
snprintf(st, sizeof st, "%s", dnd_on ? meow_str_zh("common.on", "开") : meow_str_zh("common.off", "关"));
int lw = ui_width(dnd_lbl, 18);
ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st,
dnd_on ? th->primary : th->on_surface_variant, 18);
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */
s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th);
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中)。
* M7.3:迁 SDK meow_switch(绘制经 meow_widget_draw;命中仍走 ncp_dnd_hit 整带)。 */
if (g_ncp_widget_init && g_ncp_sw_dnd) {
meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48, cy - 14, 48, 28);
meow_switch_set(g_ncp_sw_dnd, dnd_on ? 1 : 0);
meow_widget_draw(g_ncp_sw_dnd);
}
}
@@ -576,7 +856,7 @@ static void ncp_draw_clear(const meow_ui_theme_t *th)
if (cw <= 0) return;
meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container);
/* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */
const char *lbl = "清除";
const char *lbl = meow_str_zh("ncp.clear", "清除");
int lw = ui_width(lbl, 18);
int unit = 22; /* 图标占位宽 + 与文字的间距 */
int total = unit + lw;
@@ -629,38 +909,6 @@ static void ncp_media_cover_load(const meow_media_session_t *s)
g_ncp_media_cover = meow_img_load_file(path);
}
/* 实心三角(dir=+1 右向 / -1 左向);size=最大宽度 px。按扫描线 fill_rect 近似 */
static void ncp_media_tri(int cx, int cy, int size, int dir, uint32_t col)
{
int half = size / 2;
for (int dy = -half; dy <= half; dy++) {
int w = half + 1 - (dy < 0 ? -dy : dy); /* 顶宽 1 → 底宽 half+1 */
if (w <= 0) continue;
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
fill_rect(x0, cy + dy, w, 1, col);
}
}
/* 圆形按钮底 + 字形(kind:0=prev 1=play 2=next 3=pause 4=暂停态play)。 */
static void ncp_media_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
uint32_t fg = 0xFFFFFF;
if (kind == 0) { /* prev:左三角 + 左竖条 */
ncp_media_tri(cx + 4, cy, r - 4, -1, fg);
fill_rect(cx - 3, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 1) { /* play:右三角 */
ncp_media_tri(cx + 2, cy, r - 2, 1, fg);
} else if (kind == 2) { /* next:右三角 + 右竖条 */
ncp_media_tri(cx - 4, cy, r - 4, 1, fg);
fill_rect(cx + 1, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 3) { /* pause:双竖条 */
int bw = (r - 6) / 2; if (bw < 2) bw = 2;
fill_rect(cx - 7, cy - (r - 5), bw, (r - 5) * 2, fg);
fill_rect(cx + 7 - bw, cy - (r - 5), bw, (r - 5) * 2, fg);
}
}
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
static void ncp_draw_media_card(const meow_ui_theme_t *th)
{
@@ -678,19 +926,15 @@ static void ncp_draw_media_card(const meow_ui_theme_t *th)
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 20);
ui_draw(cover_x + (cover_s - cw) / 2, cover_y + (cover_s - meow_font_line_height(20)) / 2,
t, 0xFFFFFF, 20);
t, meow_theme_get()->onPrimary, 20);
}
int tx = cover_x + cover_s + 14;
int tw = (NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R - 8) - tx; /* 文本宽限到上一首按钮左缘前 8px */
if (tw < 0) tw = 0;
ncp_text(tx, cy + 14, s->title, 18, th->on_surface, tw);
ncp_text(tx, cy + 40, s->artist[0] ? s->artist : s->album, 16, th->on_surface_variant, tw);
/* 控制按钮 */
int by = cy + NCP_MEDIA_BTN_Y;
ncp_media_btn(NCP_MEDIA_PREV_CX, by, NCP_MEDIA_BTN_R - 4, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 /* pause */ : 1 /* play */;
ncp_media_btn(NCP_MEDIA_PLAY_CX, by, NCP_MEDIA_BTN_R, pk, th);
ncp_media_btn(NCP_MEDIA_NEXT_CX, by, NCP_MEDIA_BTN_R - 4, 2, th);
/* 控制按钮(SDK meow_iconbtn + sb_icons,与 Now Playing 一致;命中仍走 ncp_media_card_tap 三圆心) */
ncp_draw_media_btns(s);
/* 进度条 + 时间 */
uint32_t pos = meow_media_display_pos(s);
int px = tx, pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px, py = cy + NCP_MEDIA_PROG_Y;
@@ -762,61 +1006,34 @@ static void draw_notif_center(int scroll, int redraw_sb)
int grab_w = 44, grab_x = (W - grab_w) / 2;
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
/* 标题 + 「上滑/点空白关闭」 提示 */
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
ui_draw(24, NCP_TITLE_Y, meow_str_zh("ncp.title", "通知中心"), th->on_surface, 22);
ui_draw(W - 24 - ui_width(meow_str_zh("ncp.tap_close", "点空白关闭"), 16), NCP_TITLE_Y + 2,
meow_str_zh("ncp.tap_close", "点空白关闭"), th->on_surface_variant, 16);
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
int chip_x0 = (W - total_chip) / 2;
for (int c = 0; c < NCP_CHIP_N; c++) {
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surface_container;
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
int icon_cy = NCP_CHIP_Y + 14;
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_THEME) {
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
}
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_POWER) {
/* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */
int pc = icon_cx + SB_ICON_SIZE / 2;
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
}
char label[24];
snprintf(label, sizeof label, "%s", chip_lbl[c]);
if (val[0] && c == NCP_CHIP_BRIGHT)
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
label, fg, 14);
}
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单)。M7.3:迁 SDK 自定义画布 chip
*(TONAL 观感圆角 + 位图图标 + 标签,见 ncp_chip_render;绘制经 meow_widget_draw)。 */
if (g_ncp_widget_init)
for (int c = 0; c < NCP_CHIP_N; c++)
if (g_ncp_chip[c]) meow_widget_draw(g_ncp_chip[c]);
/* 亮度滑条 */
/* 亮度滑条(M7.3:迁 SDK meow_slider;track 中心 = NCP_SL1_Y+3,与旧 s_draw_slider 对齐) */
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int bval = 0; meow_settings_get_int("display.brightness", &bval);
bval = mnp_brightness_clamp(bval, bmin);
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, meow_str_zh("ncp.brightness", "亮度"), th->on_surface, 18);
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
if (g_ncp_widget_init && g_ncp_sl_bri) { meow_slider_set(g_ncp_sl_bri, bval); meow_widget_draw(g_ncp_sl_bri); }
/* 音量滑条 */
int vval = 0; meow_settings_get_int("audio.volume", &vval);
vval = vol_clamp(vval);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, meow_str_zh("ncp.volume", "音量"), th->on_surface, 18);
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
if (g_ncp_widget_init && g_ncp_sl_vol) { meow_slider_set(g_ncp_sl_vol, vval); meow_widget_draw(g_ncp_sl_vol); }
/* 分隔线 */
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
@@ -833,8 +1050,8 @@ static void draw_notif_center(int scroll, int redraw_sb)
ncp_draw_list(scroll);
/* 底部提示 */
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
th->on_surface_variant, 16);
ui_draw((W - ui_width(meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), 16)) / 2, NCP_FOOT_Y,
meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), th->on_surface_variant, 16);
/* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh
* 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过
@@ -968,16 +1185,10 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */
int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */
int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
/* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off):
* row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手);
* row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。
* row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */
int row_swipe_row = -1;
int row_off = 0;
int row_settle = 0;
int row_off_target = 0;
unsigned long long row_anim_at = 0;
int row_paint = 0; /* 仅重绘该行带(行左滑专用) */
/* ⑦ 单条通知行左滑删除(M7.3:迁 SDK meow_listview_set_swipe_delete + swipe_begin/move/end)。
* sdk_swipe = 本触摸已交给 SDK 行左滑手势(begin 命中可滑行);SDK 负责方向仲裁(横向=消费 /
* 纵向=让位滚动)、删除/回弹动画(meow_anim_tick 推进)与 on_delete 回调。 */
int sdk_swipe = 0;
/* ⑨ 面板跟手下拉、半程可回:
* panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停);
* drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变);
@@ -1018,6 +1229,9 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
g_ncp_dnd = d;
}
/* M7.3:建内层 SDK 控件集(listview/双滑条/勿扰开关/芯片;owner 钉哨兵,仅首进一次)。 */
ncp_widgets_create();
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
ncp_refresh_rows();
@@ -1092,9 +1306,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0; list_paint = 0;
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
sdk_swipe = 0; /* 唤醒:清行左滑手势(SDK swipe 内部状态亦重置) */
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
/* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */
anim_p = anim_target = anim_last_target = NCP_ANIM_FIX;
@@ -1201,42 +1413,27 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
}
/* M7.3:按下原点落在通知列表行 → 交 SDK 行左滑手势(begin 命中可滑行;
* 后续 move 由 SDK 做方向仲裁:横向=消费/纵向=让位滚动)。先同步 listview
* 滚动偏移(meow_listview_hit 用其内部 scroll)。 */
if (open_settled && !drag_slider && !panel_drag && g_ncp_widget_init && g_ncp_lv) {
meow_listview_set_scroll(g_ncp_lv, list_scroll);
sdk_swipe = meow_listview_swipe_begin(g_ncp_lv, tlx, tly);
}
}
if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
/* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop,
* 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。
* 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含
* row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大:
* 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
int ddx = tlx - tdx, ddy = tly - tdy;
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
int eff = tdy - ncp_list_top() + list_scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
if (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
row >= 0 && row < g_nrows) {
row_swipe_row = row;
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
row_settle = 0; row_off_target = 0;
row_anim_at = now_ms();
g_ncp_swipe_row = row; g_ncp_swipe_off = row_off;
row_paint = 1;
}
}
}
/* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */
if (row_swipe_row >= 0 && td_ok && !row_settle) {
row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
row_paint = 1;
/* ⑦ 行左滑(SDK swipe-delete):已交给 SDK 手势 → 喂 move。SDK 方向仲裁:
* 横向小位移超容差 → 消费(返回 1,行左移/删除区由 SDK 画,本触摸不滚/不拖面板);
* 纵向主导 → 取消本行 swipe(返回 0,sdk_swipe=0),回退到下方滚动/面板拖动。 */
if (sdk_swipe && td_ok && drag_slider == 0) {
meow_listview_set_scroll(g_ncp_lv, list_scroll);
if (!meow_listview_swipe_move(g_ncp_lv, tlx, tly)) sdk_swipe = 0;
}
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) {
if (td_ok && !drag_slider && !scroll_drag && !sdk_swipe && !panel_drag) {
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
if (ady2 >= S_SCROLL_SLOP) {
int av = g_nrows * NCP_ROW_H;
@@ -1260,7 +1457,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
/* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 +
* 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式);
* 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && !sdk_swipe) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
int total = g_nrows * NCP_ROW_H;
@@ -1290,10 +1487,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0;
/* ⑦ 新触摸:清行左滑(防上一手势残留) */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* ⑦ 新触摸:清行左滑手势(防上一手势残留) */
sdk_swipe = 0;
/* ⑨ 新触摸:清面板跟手(防上一拖动残留) */
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
}
@@ -1311,19 +1506,18 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
if (row_swipe_row >= 0) {
/* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。
* 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */
/* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */
row_off = mnp_swipe_clamp(dx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
if (mnp_swipe_delete(row_off, NCP_ROW_W,
NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) {
row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */
} else {
row_off_target = 0; /* 回弹复原 */
if (sdk_swipe && g_ncp_widget_init && g_ncp_lv) {
/* ⑦ 行左滑松手(SDK swipe-delete):消费 → 删除/回弹动画交 SDK
* (meow_anim_tick 推进;删除完成后 ncp_lv_on_delete 置 g_ncp_need_paint)。
* 本手势不关面板、不走 tap/上滑/点空白。未滑动(=点击某行)→ 未消费,
* 清 sdk_swipe 后走下方 tap 分支(ncp_row_tap 命中该行 → dismiss+关面板)。 */
meow_listview_set_scroll(g_ncp_lv, list_scroll);
if (meow_listview_swipe_end(g_ncp_lv, tlx, tly)) {
sdk_swipe = 0;
td_ok = 0;
goto ncp_release_done;
}
row_settle = 1; row_anim_at = now_ms();
sdk_swipe = 0;
} else if (scroll_drag) {
/* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值);
* 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。
@@ -1395,10 +1589,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
if (!hit && ncp_clear_hit(tx, ty)) {
int n = ncp_clear_all();
logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n);
/* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* 清除后取消行左滑手势(下标会变),再重采集 + 重钳 + 整面板重画。 */
sdk_swipe = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
@@ -1430,6 +1622,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
/* 其余横滑/下滑忽略(不关闭不动作) */
}
td_ok = 0;
ncp_release_done:
;
}
}
}
@@ -1453,10 +1647,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
media_paint = 1; /* 已有源:进度/封面随 META 变化 → 仅重绘卡片带 */
}
if (ncp_rows_changed()) {
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑手势,避免行下标错位。⑨ 同时取消面板跟手。 */
sdk_swipe = 0;
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
@@ -1490,41 +1682,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
}
if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */
/* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。
* 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。
* 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */
if (row_swipe_row >= 0 && row_settle) {
unsigned long long rnow = now_ms();
int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0);
row_anim_at = rnow;
int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE);
if (noff != row_off) {
row_off = noff;
g_ncp_swipe_off = noff;
row_paint = 1;
}
if (noff == row_off_target) {
if (row_off_target == 0) {
/* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */
ncp_repaint_row(list_scroll, row_swipe_row, 0);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */
} else {
/* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */
if (row_swipe_row >= 0 && row_swipe_row < g_nrows)
meow_notif_dismiss(g_rows[row_swipe_row].id);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
}
}
}
/* M7.3:行左滑落位动画已由 SDK 接管(meow_listview_swipe_end → meow_anim → swipe_anim_done
* → on_delete(idx) → ncp_lv_on_delete 置 g_ncp_need_paint)。此处不再需要手绘行位移/回弹/滑出。 */
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
@@ -1561,14 +1720,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok);
drag_paint = 0;
}
/* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。
* row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */
if (row_paint) {
last_act = now;
if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target))
ncp_repaint_row(list_scroll, row_swipe_row, row_off);
row_paint = 0;
}
/* M7.3:行左滑跟手/回弹/滑出重绘已由 SDK 接管(swipe_anim_update 经 meow_widget_draw(lv)
* 重绘列表 + meow_anim_set_region 标该行带脏)。此处不再有 row_paint 手绘路径。 */
/* ~1s 状态栏时钟 */
if (now - last_tick >= 1000) {
last_tick = now;
@@ -1576,12 +1729,27 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
fb_norm_buf0();
meow_ui_flush();
}
/* M7.3:推进 SDK 动画(行左滑删除/回弹、meow_scroll fling 等)。左滑删除完成后 on_delete
* 置 g_ncp_need_paint → 下一帧整面板重画。swipe 动画推进时 SDK 已重绘列表(swipe_anim_update
* 调 meow_widget_draw(lv)),此处只把列表带标脏上屏(覆盖 SDK 经 meow_gfx 标脏的列表矩形)。 */
meow_anim_tick();
if (g_ncp_need_paint) { g_ncp_need_paint = 0; need_paint = 1; }
if (g_ncp_widget_init && g_ncp_lv && meow_listview_swipe_active(g_ncp_lv)) {
meow_dirty_add(0, ncp_list_top(), fb_xres, ncp_list_view());
meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */
ncp_rows_free();
/* 封面缓存随面板关闭回收(对齐 Now Playing np_cleanup:每次下拉重新加载当前封面)。
* 旧码 g_ncp_media_cover_path 是文件级 static、从不重置——若首次加载失败/或 artwork 初
* 为空的瞬间进面板,其后即便封面落盘并因路径不变而被「按文本缓存」跳过重载,卡片永久
* 停留在「显示歌名」回退/旧封面。关面板即清缓存,保证下次下拉能拿到最新封面。 */
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
g_ncp_media_cover_path[0] = 0;
logmsg("notification center closed");
}
+1 -1
View File
@@ -211,7 +211,7 @@ static void np_draw_page(int redraw_sb)
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 22);
ui_draw(NP_COVER_X + (NP_COVER - cw) / 2, NP_COVER_Y + (NP_COVER - meow_font_line_height(22)) / 2,
t, 0xFFFFFF, 22);
t, meow_theme_get()->onPrimary, 22);
}
/* 元数据 */
np_center_text(NP_TITLE_Y, s->title, 26, th->on_surface, W - 40);
+22 -12
View File
@@ -80,8 +80,9 @@ static void draw_pm_button(int i, int selected)
/* ==================================================================
* M7.0b 电源菜单控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 圆角卡 → meow_card(SDK 主题 surfaceContainerLow);
* - 六个圆钮 → meow_iconbtn(图标在 SDK sb_icons 表:回HiBy→ARROW_BACK、取消→CLOSE)
* 或 meow_widget_custom_create(pm_icons 专有位图:关机/重启/升级/锁定);
* - 六个圆钮 → meow_widget_custom_create(pm_icons 专有位图:关机/重启/回HiBy/升级/锁定/取消,
* 均 56×56 1:1 blit,无放大)。meow_iconbtn 分支保留(回HiBy/取消曾改走 sb_icons,因放大锯齿
* 定案回退 pm_icons,见 pm_sdk_icon[] 注释;后续 SDK 出更高清图标可再启用);
* - 命中/选中/按压态全走 SDK(meow_widget_hit/set_state/draw);
* - 主题 = 读 SDK meow_theme(单一权威),后台遮罩仍 dim_screen(会话骨架不动)。
* 控件 lazy 创建一次(owner=PM_SENTINEL_ACT 哨兵 activity),跨多次进入复用;
@@ -90,14 +91,19 @@ static void draw_pm_button(int i, int selected)
#define PM_SENTINEL_ACT 0x50 /* 电源菜单哨兵 activity(不与 launcher 其它页冲突) */
#define PM_BTN_SIZE (PM_R * 2) /* 圆钮 = 外接正方形边长(直径) */
/* 哪些 pm 图标在 SDK sb_icons 表(可用 meow_iconbtn 直接出图);-1=不在 → 自定义画布 blit pm_icons */
/* 哪些 pm 图标走 SDK sb_icons 表(可用 meow_iconbtn 直接出图);-1=不在 → 自定义画布 blit pm_icons。
* M7.0b 曾把 2=回HiBy / 5=取消 走 meow_iconbtn(SB_ARROW_BACK / SB_CLOSE),但 meow_iconbtn 按
* 0.6*size 把 32×32 sb_icons 位图放大到 ~60px(>位图原生墨高),最近邻放大导致明显锯齿;且此尺寸
* 大于 M6.8 及更早电源菜单圆钮图标(pm_icons 56×56,1:1 无放大)。定案:这两钮也回退 pm_icons 专有
* 位图(PM_ICON_BACK/PM_ICON_CANCEL,同为 Material Symbols 56×56),统一其余四钮画法,恢复旧尺寸。
* pm_icons[] 全部专用 → meow_iconbtn 分支保留(后续 SDK 出更高清图标可再启用)。 */
static const int pm_sdk_icon[PM_NUM] = {
-1, /* 0 关机:pm_icons 专有(SDK 无 power 位图) */
-1, /* 1 重启:pm_icons 专有 */
MEOW_ICON_ARROW_BACK, /* 2 回HiBy:SB_ARROW_BACK 在表 */
-1, /* 2 回HiBy:pm_icons 专有(恢复 M6.8 56×56,避免 meow_iconbtn 放大锯齿) */
-1, /* 3 升级:pm_icons 专有 */
-1, /* 4 锁定:pm_icons 专有(SDK 无 lock 位图) */
MEOW_ICON_CLOSE, /* 5 取消:SB_CLOSE 在表 */
-1, /* 5 取消:pm_icons 专有(同 2,恢复旧尺寸) */
};
/* 圆钮文案 i18n id(meow_str)。common.cancel 内嵌已有;power.* 需 TF 覆盖表/后续并入 SDK 内嵌。 */
@@ -573,17 +579,20 @@ int show_power_menu(void)
* 不随主循环每 10ms 全刷(原实现每帧重画同一内容,与 LCD 刷新竞争产生频闪)。 */
static void draw_cr_button(int sel, int i)
{
const meow_theme *mt = meow_theme_get(); /* M7.5:主题语义色(深/浅×MD3/MIUIx 单一权威) */
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
uint32_t bg = (i == sel) ? mt->primary : mt->surfaceContainer;
fill_round_rect(x0, y0, w, h, 12, bg);
/* M7.0b i18n:是/否 经 meow_str 抽取(未知 id 回退中文) */
const char *lbl = (i == 0) ? meow_str_zh("power.confirm_yes", "是")
: meow_str_zh("power.confirm_no", "否");
uint32_t fg = (i == sel) ? mt->onPrimary : mt->onSurface;
if (mfont_ready) {
int lw = meow_font_width_text(lbl, 20);
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
lbl, rgb565((i == sel) ? 0xFFFFFF : 0xC8D0E8), 20);
lbl, rgb565(fg), 20);
} else {
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, (i == sel) ? 0xFFFFFF : 0xC8D0E8);
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, fg);
}
}
@@ -594,9 +603,10 @@ int confirm_reboot_player(void)
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
fb_pan_buf0_force();
const meow_theme *mt0 = meow_theme_get(); /* M7.5:标题颜色走主题语义色 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320,
meow_str_zh("power.reboot_confirm", "返回播放器需重启,现在重启?"), rgb565(0xFFFFFF), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
meow_str_zh("power.reboot_confirm", "返回播放器需重启,现在重启?"), rgb565(mt0->onSurface), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, mt0->onSurface);
meow_touch mt;
mt_reset(&mt);
@@ -616,8 +626,8 @@ int confirm_reboot_player(void)
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560,
meow_str_zh("power.confirm_hint", "音量切换 电源取消"), rgb565(0x666688), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
meow_str_zh("power.confirm_hint", "音量切换 电源取消"), rgb565(mt0->onSurfaceVariant), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, mt0->onSurfaceVariant);
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
for (;;) {
/* dirty 绘制:首次全画两钮;此后仅 sel 变化才重画(消频闪)→ 标脏 + present。 */
+288 -33
View File
@@ -126,9 +126,12 @@ static const srow_t SMENU[] = {
/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由
* 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */
#if 0 /* M7.2 回退位:旧「当前选中滑条行 track y」随旧手绘 draw_settings_page 退役
* (新路径在触摸按下时由 meow_listview hit 直接算 track 中心,无需全局记录)。 */
/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量,
* 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */
static int s_sel_track_y = -1;
#endif
/* ---- 行高/位置/导航辅助 ---- */
@@ -206,6 +209,9 @@ static const char *const *s_row_presets(const srow_t *r, int *n)
}
#if 0 /* M7.2 回退位:旧「纯范围/选中可见滚动钳制 + 内容 y→行号」随旧手绘 draw_settings_page 退役
* (新路径滚动走 meow_scroll 包裹 meow_listview,钳制/命中/可见性由 SDK 接管)。
* 删除 #if 0 行并切回旧 draw_settings_page 路径即可恢复。 */
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
static int s_clamp_scroll_range(int scroll)
{
@@ -240,6 +246,7 @@ static int s_row_at_content_y(int cy)
if (pos >= s_vis_count()) return -1;
return s_vis_at(pos);
}
#endif /* M7.2 回退位:旧滚动钳制 + 内容 y→行号 */
/* ---- 值格式化/展示 ---- */
@@ -497,6 +504,9 @@ static int s_slider_val_from_x(const srow_t *r, int px)
}
#if 0 /* M7.2 回退位:旧手绘设置页主列表(手绘分组列表 + s_draw_switch/s_draw_slider + 自算滚动/命中)。
* 新路径走 meow_listview + meow_switch + meow_scroll + meow_select_show;删除 #if 0 行并把
* 下方「M7.2 SDK 路径」的 set_paint_page 调用切回 draw_settings_page 即可一键回退。 */
/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */
static void draw_settings_page(int sel, int scroll_off)
{
@@ -596,6 +606,7 @@ static void draw_settings_page(int sel, int scroll_off)
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
}
#endif /* M7.2 回退位:旧手绘设置页主列表 */
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
@@ -658,7 +669,9 @@ static int s_dropdown(const srow_t *r)
}
if (n <= 0) return -1;
if (cur < 0 || cur >= n) cur = 0;
return s_dd_modal(r->label, item, n, cur);
/* M7.2:枚举/预设选择器迁 SDK meow_select_show(BottomSheet+单选列表,批次2 新控件),
* 替换自绘 s_dd_modal(后者仍供 about/网络列表/字体两级子菜单用,勿删)。 */
return meow_select_show(r->label, item, n, cur);
}
@@ -686,7 +699,7 @@ static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int
int cur_sel = (i == sel);
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
cur_sel ? 0xFFFFFF : th->on_surface, 20);
cur_sel ? meow_theme_get()->onPrimary : th->on_surface, 20);
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
}
}
@@ -966,11 +979,228 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
}
/* ==================================================================
* M7.2 设置页渲染层迁移(主列表迁 SDK 控件):
* - 分组列表 → meow_listview(虚拟化 set_vsource 固定 item 池 + section 段头 + set_item_draw 自绘);
* - 开关行 → 行内真 meow_switch(池化,SDK 绘制 + 按压态);
* - 滑条行 → 行内嵌 meow_slider(自绘 track/thumb,保留既有 s_draw_slider 语义色;
* 「行内嵌自绘」,M7 定案允许)——拖动实时写 settings.conf(读改写单键,
* 经 meow_settings_set_*,勿整文件覆盖——音量存盘教训);
* - 枚举/预设 → SDK meow_select_show(BottomSheet+单选列表,替换自绘 s_dd_modal);
* - 信息/列表行 → 行 right 值 + chevron(SDK 主题语义色)。
* 滚动 → meow_scroll(跟手 track + 松手 fling,同 subpage.c M7.1a 语义;刻意不 set_snap)。
* 控件 lazy 创建一次(owner=SET_SENTINEL_ACT 哨兵),跨多次进入复用;数据/动作(s_commit、
* s_set_int_value、s_net_*、s_font_dropdown、s_dropdown)逻辑层不动,仅换渲染层 + 命中;
* run_settings_page 会话骨架(触摸/滚动/返回/IPC/电源键语义)不改。旧手绘 draw_settings_page
* + s_clamp_scroll + s_row_at_content_y 保留 #if 0 作回退位(M7 红线:一键回退)。
* ================================================================== */
#define SET_SENTINEL_ACT 0x53 /* 设置页哨兵 activity(不与 launcher 其它页冲突) */
#define SET_LIST_Y SET_CONTENT_Y /* 100:列表顶缘(顶栏/标题下) */
#define SET_FOOT_H 40 /* 底部返回/版本带高 */
#define SET_ITEM_H 68 /* MD3 行高(滑条行给 track 留位) */
#define SET_SLIDER_YOFF 45 /* 滑条行内 track 顶缘相对行顶(÷s_draw_slider h=6,中心=顶+3) */
#define SET_TRACK_CY 48 /* 滑条 track 中心相对行顶 y(=SET_SLIDER_YOFF+3,与绘制对齐) */
#define SET_SLIDER_LY 12 /* 滑条行 label 顶相对行顶 y(上移给 track 留位) */
#define SET_SWITCH_POOL 12
static meow_widget *g_set_lv = NULL; /* meow_listview(虚拟化) */
static meow_scroll *g_set_sc = NULL; /* 滚动物理 */
static meow_widget *g_set_sw[SET_SWITCH_POOL]; /* 行内开关池(真 meow_switch) */
static int g_set_sw_n = 0, g_set_sw_use = 0; /* 池创建数 / 本轮已用(每次整绘复位) */
static int g_set_lv_h = 0; /* 列表视口高(选中可见性 clamp 用) */
static int g_set_init = 0;
static void set_lv_rect(int *x, int *y, int *w, int *h)
{
if (x) *x = 0;
if (y) *y = SET_LIST_Y;
if (w) *w = fb_xres;
if (h) { int t = fb_yres - SET_LIST_Y - SET_FOOT_H; if (t < 0) t = 0; *h = t; }
}
static int set_lv_count(void *ctx) { (void)ctx; return s_vis_count(); }
/* 行填充(虚拟化 pool bind;段头行 section=1、enabled=1 供 SDK 命中/段头绘制) */
static void set_lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= s_vis_count()) { out->title = ""; return; }
const srow_t *r = &SMENU[s_vis_at(idx)];
out->title = r->label ? r->label : "";
out->section = (r->ctrl == SC_HEADER) ? 1 : 0;
if (r->ctrl == SC_SWITCH) {
int on = 0; meow_settings_get_switch(r->key, &on);
out->right = MEOW_LIST_RIGHT_SWITCH; out->on = on;
}
}
/* 行内开关池:取下一个可用槽(每帧 set_lv_draw 顺序取用,整绘前复位 g_set_sw_use) */
static meow_widget *set_sw_take(void)
{
if (g_set_sw_use < g_set_sw_n) return g_set_sw[g_set_sw_use++];
if (g_set_sw_n >= SET_SWITCH_POOL) return NULL;
meow_widget *w = meow_switch_create(0, 0, 0);
if (w) { g_set_sw[g_set_sw_n++] = w; return g_set_sw[g_set_sw_use++]; }
return NULL;
}
/* 整行自绘(set_item_draw 覆盖默认 renderer;x=列表左缘、y=行顶、w/h=行全宽高,
* state=SELECTED/PRESSED/DISABLED 组合;颜色全取 meow_theme 语义色,无散落硬编码) */
static void set_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx; (void)h;
const meow_theme *mt = meow_theme_get();
if (idx < 0) return;
int si = s_vis_at(idx);
const srow_t *r = &SMENU[si];
int sel = (state & MEOW_LISTVIEW_ST_SELECTED);
int press = (state & MEOW_LISTVIEW_ST_PRESSED);
int dis = (state & MEOW_LISTVIEW_ST_DISABLED);
int thh = meow_ui_text_height(mt->font_body);
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
if (r->ctrl == SC_HEADER) {
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
int ty = y + (SET_ITEM_H - thh) / 2;
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary);
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
return;
}
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer);
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26);
int ly = y + (SET_ITEM_H - thh) / 2; /* label 单行垂直居中 */
switch (r->ctrl) {
case SC_SWITCH: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
meow_widget *sw = set_sw_take();
if (sw) {
int on = 0; meow_settings_get_switch(r->key, &on);
meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
meow_switch_set(sw, on);
meow_widget_draw(sw);
}
break;
}
case SC_SLIDER: {
/* label 上移一行,track 放行下方(中心=SET_SLIDER_YOFF+3=48),与拖动命中带对齐 */
meow_ui_text(x + 16, y + SET_SLIDER_LY, r->label, mt->font_body, textcol);
char vb[32]; s_fmt_value(r, vb, sizeof vb);
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), y + SET_SLIDER_LY, vb, mt->font_body, subcol);
int v = 0; meow_settings_get_int(r->key, &v);
/* 息屏时间为档位索引型(秒→索引 等距画);其余 clamp 到 [min,max] */
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
else { if (v < r->min) v = r->min; if (v > r->max) v = r->max; }
s_draw_slider(S_SLIDER_X, y + SET_SLIDER_YOFF, S_SLIDER_W, v, r->min, r->max, s_ui_theme());
break;
}
case SC_INFO: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[48]; s_net_get(r, vb, sizeof vb);
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
break;
}
case SC_LIST: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = x + w - 16 - 12 - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
s_draw_chevron(x + w - 16, y + SET_ITEM_H / 2, subcol);
break;
}
default: { /* SC_ENUM / SC_PRESET */
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
s_draw_chevron(vx - 8, y + SET_ITEM_H / 2, subcol);
break;
}
}
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
if (dis) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
}
/* ---- meow_scroll 回调(包 listview)---- */
static void set_sc_set(void *ctx, int off) { (void)ctx; if (g_set_lv) meow_listview_set_scroll(g_set_lv, off); }
static int set_sc_max(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_max_scroll(g_set_lv) : 0; }
static int set_sc_get(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_scroll(g_set_lv) : 0; }
/* 列表区重绘(含内容底 + listview),供 fling/滚动局部刷。 */
static void set_redraw_region(void *ctx)
{
(void)ctx;
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
if (lvh <= 0) return;
const meow_ui_theme_t *th = s_ui_theme();
meow_ui_fill_rect(lvx, lvy, lvw, lvh, th->surface_variant);
if (g_set_lv) { g_set_sw_use = 0; meow_widget_draw(g_set_lv); }
meow_dirty_add(lvx, lvy, lvw, lvh);
meow_ui_flush();
}
/* 选中行可见性:滚动到选中行不越界(仅靠 tap 触发,选中行即在视野内,防御性兜底)。 */
static void set_ensure_sel_visible(int sel)
{
if (!g_set_lv) return;
int p = s_vis_of(sel); if (p < 0) return;
int scroll = meow_listview_scroll(g_set_lv);
int top = p * SET_ITEM_H, bot = top + SET_ITEM_H;
if (top < scroll) meow_listview_set_scroll(g_set_lv, top);
else if (bot > scroll + g_set_lv_h) meow_listview_set_scroll(g_set_lv, bot - g_set_lv_h);
}
/* 创建设置页控件集(owner=SET_SENTINEL_ACT;仅首进创建一次,跨次进入复用)。 */
static void set_widgets_create(void)
{
meow_ui_activity_push(SET_SENTINEL_ACT);
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
g_set_lv_h = lvh;
g_set_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
if (g_set_lv) {
meow_listview_set_vsource(g_set_lv, set_lv_count, set_lv_bind, NULL);
meow_listview_set_item_draw(g_set_lv, set_lv_draw, NULL);
meow_listview_set_item_h(g_set_lv, SET_ITEM_H);
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标会往回拉 */
}
g_set_sc = meow_scroll_create(set_sc_set, set_sc_max, set_sc_get, set_redraw_region, NULL);
meow_ui_activity_pop();
g_set_init = 1;
}
/* 整页绘制(进 back_buf;调用方 meow_dirty_full/meow_dirty_add + flush)。sel=当前选中(SMENU 下标)。 */
static void set_paint_page(int sel)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
fill_rect(0, 0, W, H, th->surface);
draw_statusbar();
if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28);
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant);
if (g_set_lv) {
g_set_sw_use = 0;
int p = s_vis_of(sel); if (p >= 0) meow_listview_set_sel(g_set_lv, p);
meow_widget_draw(g_set_lv);
}
/* 底部:返回(左)+ 版本(右),复用应用列表底部对齐逻辑 */
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(20) - 8;
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20);
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */
void run_settings_page(void)
{
int sel = s_first_setting();
int scroll_off = 0;
int H = fb_yres;
unsigned long long t_sets = now_ms(); /* M3 取证:进设置页耗时分段(真机核对用) */
@@ -1006,7 +1236,7 @@ void run_settings_page(void)
int drag = -1; /* 被拖动的行索引;-1=无 */
/* M2.37 列表拖动滚动状态 */
int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */
int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */
int drag_last_y = 0; /* M7.2:滚动跟手增量基准(meow_scroll_track 速度采样) */
int scroll_y0 = 0; /* 按下原点纵向坐标 */
int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */
int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */
@@ -1024,8 +1254,9 @@ void run_settings_page(void)
* s_net_refresh_pump() 填充,draw 用缓存(g_net_*),数据变化即 need_paint。 */
s_net_sync_from_sys();
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
scroll_off = s_clamp_scroll(sel, scroll_off);
draw_settings_page(sel, scroll_off);
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
set_paint_page(sel);
meow_dirty_full(); meow_ui_flush();
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
@@ -1074,8 +1305,7 @@ void run_settings_page(void)
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
logmsg("settings: auto screen-off -> input wake, stayed in settings page");
scroll_off = s_clamp_scroll_range(scroll_off);
draw_settings_page(sel, scroll_off);
set_paint_page(sel);
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
@@ -1206,34 +1436,47 @@ void run_settings_page(void)
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */
/* 按下原点落在选中滑条 track 带内 → 开始拖动 */
if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 &&
tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) {
drag = sel;
const srow_t *r = &SMENU[sel];
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 0);
need_paint = 1;
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(行按压态渲染) */
scroll_y0 = tdy; /* 列表拖动基准:按下原点 */
/* 按下原点落在滑条行 track 带内 → 开始拖动(meow_listview hit 判行) */
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tlx, tly) : -1;
if (vp >= 0 && drag < 0) {
const srow_t *rr = &SMENU[s_vis_at(vp)];
if (rr->ctrl == SC_SLIDER) {
int track_cy = SET_LIST_Y + vp * SET_ITEM_H -
meow_listview_scroll(g_set_lv) + SET_TRACK_CY;
if (tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
tly >= track_cy - 14 && tly <= track_cy + 14) {
drag = s_vis_at(vp);
int v = s_slider_val_from_x(rr, tlx);
s_slider_set_from_track(rr, v, 0);
need_paint = 1;
}
}
}
}
/* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
* scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。
* M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项);
* 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */
/* M7.2 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
* 走 meow_scroll(跟手 track + 松手 fling),同 subpage.c M7.1a 语义;
* 方向经 input.swipe_invert 切换(默认手指上滑 → 内容下翻)。 */
if (td_ok && drag < 0 && !scroll_drag) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
scroll_base = scroll_off + (scroll_invert ? -ddy : ddy);
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
drag_last_y = tly;
}
}
if (scroll_drag && td_ok) {
int dydt = tly - scroll_y0;
scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt));
need_paint = 1;
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
drag_last_y = tly;
int scdy = scroll_invert ? dydt : -dydt;
if (g_set_sc && scdy) meow_scroll_track(g_set_sc, scdy, now);
/* 列表滚动不在此即时重绘:整批事件末尾由下方「if (scroll_drag && td_ok)
* set_redraw_region」合并为一次视口局部重绘。此处若 set_redraw_region 且再设
* need_paint=1,底部 set_paint_page 会再走一遍整页重绘 + dirty_add(0,42,...)
* 近整屏 flush —— 快滑每帧整页重绘 → 卡顿,违背「局部脏区不整屏重绘」铁律;
* 且一个 poll 多个移动事件各自 flush 会多次 FBIO_WAITFORVSYNC 阻塞放大延迟。 */
}
if (drag >= 0 && td_ok) {
const srow_t *r = &SMENU[drag];
@@ -1250,6 +1493,7 @@ void run_settings_page(void)
}
} else if (!val && td) {
td = 0;
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
if (drag >= 0) { /* 滑条拖动结束 → 落盘 */
const srow_t *r = &SMENU[drag];
s_commit(r, 1);
@@ -1257,7 +1501,8 @@ void run_settings_page(void)
need_paint = 1;
} else if (scroll_drag) {
scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */
scroll_base = 0; scroll_y0 = 0;
if (g_set_sc) meow_scroll_fling(g_set_sc); /* 松手 fling(惯性 ease-out) */
scroll_y0 = 0;
need_paint = 1;
} else if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
@@ -1276,8 +1521,10 @@ void run_settings_page(void)
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto done;
}
int cy = ty - SET_CONTENT_Y + scroll_off;
int idx = s_row_at_content_y(cy);
/* M7.2:行命中走 meow_listview(含滚动换算 + 禁用/越界行不响应);
* 段头行 enabled=1 故 hit 亦命中 → 下方按 ctrl==SC_HEADER 分流。 */
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tx, ty) : -1;
int idx = (vp >= 0) ? s_vis_at(vp) : -1;
if (idx >= 0 && !s_setting_idx(idx)) {
/* 点分组标题行 → 仅选中下一设置行 */
int ns = s_next_setting(idx);
@@ -1306,7 +1553,7 @@ void run_settings_page(void)
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_base = 0; scroll_y0 = 0;
scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
memset(&kd_pwr, 0, sizeof kd_pwr);
@@ -1350,7 +1597,7 @@ void run_settings_page(void)
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_base = 0; scroll_y0 = 0;
scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
/* 重置设置页按键防抖基线,防残边 */
@@ -1376,14 +1623,18 @@ void run_settings_page(void)
}
}
}
/* 列表拖动滚动:本批连续触摸事件合并为一次视口局部重绘(只刷列表区,
* 最多一次 present),不再触发 need_paint 整页重绘。scroll_drag/td_ok 在
* 抬手处置 0(抬手那轮已走 meow_scroll_fling + need_paint 整页),故此处只兜活动滚动。 */
if (scroll_drag && td_ok) set_redraw_region(NULL);
}
/* ---- 滚动 + 重绘 ---- */
if (need_paint) {
last_act = now;
/* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */
if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; }
draw_settings_page(sel, scroll_off);
if (sel_changed) { set_ensure_sel_visible(sel); sel_changed = 0; }
set_paint_page(sel);
/* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化),
* 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。
* 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */
@@ -1408,9 +1659,13 @@ void run_settings_page(void)
fb_norm_buf0();
meow_ui_flush();
}
/* M7.2: 推进 fling/回弹动画(meow_scroll 惯性停;无动画零开销) */
meow_anim_tick();
usleep(POLL_MS * 1000);
}
done:
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
logmsg("settings page closed");
+539 -9
View File
@@ -1,9 +1,9 @@
/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙)统一脚手架(M2.76)。
/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙/锁屏 PIN)统一脚手架(M2.76)。
*
* 安卓风子页骨架:顶栏(标题 + 返回 + 右上开关)+ 分组列表(组标题/行/开关/动作)
* + 滚动(跟手,方向沿用设置页 input.swipe_invert 语义)+ 左缘左滑返回 + 底部返回。
* + 滚动 + 左缘左滑返回。
*
* 由页面(wifi_page.c / bt_page.c)通过 sub_page_t 提供:
* 由页面(wifi_page.c / bt_page.c / lock_settings.c)通过 sub_page_t 提供:
* - title 顶栏标题
* - top_sw 顶栏开关(0=不显示;1=显示并回写当前值)
* - top_sw_set 开关切换(写设置 + 生效 + 触发重扫)
@@ -12,7 +12,18 @@
* - detail 点普通行(如当前连接详情、网络/设备详情)
* - live 1=驻留时周期性(约2s)重扫行(蓝牙附近扫描等动态数据)
*
* 与设置页同主题(s_ui_theme,MD3 语义色,明暗自动);局部刷新不整屏重绘;
* ================================================================
* M7.1a 渲染层迁移:脚手架手绘(顶栏 + 分组列表 + 跟手滚动 + 手绘 switch/chevron)
* 改走 SDK 控件(单一 meow_theme 权威):
* - 顶栏 → meow_topappbar(标题 + 返回箭头 + 底部分隔线);
* - 顶部开关 → meow_switch(叠顶栏右缘,保留开/关视觉与命中);
* - 分组列表 → meow_listview(virtualized set_vsource 固定 item 池 + section 段头
* + right=SWITCH/ARROW 尾件 + 选中/按压态);
* - 滚动 → meow_scroll(跟手 track 采样松手 fling + snap 到行边界)。
* wifi/bt/lock_settings 的「数据 + 动作」逻辑(netmgr IPC / bluetoothctl / 设置契约)
* 全部保留在 launcher 逻辑层,仅换渲染。旧手绘路径整体保留 #if 0 作回退位(M7 红线)。
*
* 与设置页同主题(s_ui_theme → SDK meow_theme 单一权威,MD3/MIUIx×明暗自动);
* 模态弹窗交给 launcher 既有 s_dd_modal(点外关闭不穿透),本脚手架在回调返回后
* 彻底排空本页 tfd + 重置触摸基线(复刻 run_settings_page 后置排空,防穿透)。
*
@@ -22,16 +33,28 @@
#include "meow_launcher_internal.h"
/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface_variant 整屏底)---- */
/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface 整屏底)----
* note:SUB_CONT_TOP 与旧版一致(96),顶栏底缘分隔线由 SDK topappbar 绘制(y=95)。 */
#define SUB_TB_Y SB_BAR_H /* 28:顶栏顶缘(状态栏下) */
#define SUB_TB_H 68 /* 顶栏高 28..96 */
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 / 分隔线 */
#define SUB_CONT_TOP (SUB_TB_BOT + 6) /* 102:内容区顶缘 */
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 */
#define SUB_CONT_TOP (SUB_TB_BOT) /* 96:内容区顶缘(topappbar 分隔线下) */
#define SUB_FOOT_H 44 /* 底部版本信息带(右对齐) */
#define SUB_ITEM_H 56 /* MD3 行高(双行富 item:主/副标题) */
#define SUB_PAD_X 16 /* 水平内边距(顶开关右缘留白用) */
#define SUB_SW_H 32 /* 顶开关高(meow_switch 固定 56×32) */
#if 0 /* ================================================================
* M7.1a 退役:子页旧手绘脚手架(顶栏 + 分组列表 + 跟手无惯性滚动 + 手绘
* s_draw_switch/s_draw_chevron + 自算行前缀和 + 自算命中)。改走 SDK 控件
* (meow_topappbar + meow_listview + meow_scroll),命中/布局/状态/滚动/主题
* 全由 SDK 接管。本块保留作回退位(删除 #if 0 行并删下方「M7.1a SDK 路径」段
* 即可恢复旧手绘)。以下全部内容与迁移前 subpage.c 逐字一致。
* ================================================================ */
#define SUB_ROW_H 58 /* 行高(对齐 SROW_H,便于触摸) */
#define SUB_HDR_H 40 /* 组标题行高 */
#define SUB_PAD_X 16 /* 水平内边距 */
#define SUB_SW_W 48 /* 开关宽(s_draw_switch) */
#define SUB_SW_H 28
#define SUB_BTN_X 8 /* 底部返回按钮左缘 */
#define SUB_BTN_W 120 /* 底部返回按钮宽 */
@@ -502,3 +525,510 @@ subdone:
logmsg("subpage '%s' closed", p->title ? p->title : "");
return 0;
}
#endif /* M7.1a 退役:旧手绘子页脚手架回退位 */
/* ==================================================================
* M7.1a 子页渲染层迁移(SDK 控件化):
* - 顶栏 → meow_topappbar(标题 + 返回箭头 + 底部分隔线);
* - 顶部开关 → meow_switch(叠顶栏右缘,保留开/关视觉 + 命中);
* - 分组列表 → meow_listview(virtualized set_vsource 固定 item 池 + section 段头
* + right=SWITCH/ARROW 尾件 + 选中/按压态,SDK 语义色);
* - 滚动 → meow_scroll(跟手 meow_scroll_track 采样 + 松手 fling ease-out 惯性停;
* 刻意不 set_snap——SDK snap 以 fling 起点为吸附目标会往回拉,见 sp_widgets_create 注)。
* 控件 lazy 创建一次(owner=SUB_SENTINEL_ACT 哨兵),跨多次进入复用;meow_subpage_run()
* 进入 push 哨兵、退出 pop(owner 闸门使控件命中生效)。数据/动作回调(rows/act/detail/
* top_sw_*)仍由各子页提供,逻辑层不动,仅渲染层替换。
* ================================================================== */
#define SUB_SENTINEL_ACT 0x52 /* 子页哨兵 activity(不与 launcher 其它页冲突) */
static sub_page_t *g_sp; /* 当前运行子页(rows/act/detail/top_sw 回调入口) */
static sub_row_t g_rows[SUB_ROWS_MAX]; /* 行缓冲:rows() 一次性填满,listview bind 只读 */
static int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
static int g_top_sw_en = 0; /* 本页是否显示顶开关 */
static int g_top_on = 0; /* 顶开关当前值 */
static meow_widget *g_tb = NULL; /* topappbar */
static meow_widget *g_sw = NULL; /* 顶开关(仅在有顶开关的页创建/使用) */
static meow_widget *g_lv = NULL; /* listview */
static meow_scroll *g_sc = NULL; /* 滚动物理 */
static int g_sp_init = 0;
/* 列表视口矩形(内容区,去掉底部版本带) */
static void sp_lv_rect(int *x, int *y, int *w, int *h)
{
if (x) *x = 0;
if (y) *y = SUB_CONT_TOP;
if (w) *w = fb_xres;
if (h) { *h = fb_yres - SUB_CONT_TOP - SUB_FOOT_H; if (*h < 0) *h = 0; }
}
/* ---- 行缓冲 → meow_lv_item(虚拟化 bind;每行轻量填充,不做 IO §5.5)---- */
static int sp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void sp_lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= g_nrows) { out->title = ""; return; }
const sub_row_t *r = &g_rows[idx];
switch (r->kind) {
case SUB_HDR: /* 段头占位行(非粘性):default renderer 特殊绘制 */
out->section = 1; out->title = r->label ? r->label : "";
out->right = MEOW_LIST_RIGHT_NONE; out->enabled = 0; break;
case SUB_ROW: /* 普通信息行 */
out->title = r->label ? r->label : "";
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_NONE; break;
case SUB_SW: /* 开关行:右尾件 SWITCH */
out->title = r->label ? r->label : "";
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_SWITCH; out->on = r->on; break;
case SUB_ACT: /* 动作行:右尾件 ARROW */
out->title = r->label ? r->label : "";
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_ARROW; break;
}
}
/* ---- meow_scroll 回调(包 listview)---- */
static void sp_sc_set(void *ctx, int off) { (void)ctx; if (g_lv) meow_listview_set_scroll(g_lv, off); }
static int sp_sc_max(void *ctx) { (void)ctx; return g_lv ? meow_listview_max_scroll(g_lv) : 0; }
static int sp_sc_get(void *ctx) { (void)ctx; return g_lv ? meow_listview_scroll(g_lv) : 0; }
/* 列表区重绘(含背景 + listview),供 fling 每帧/滚动局部刷。ctx 为 meow_scroll 回调用。 */
static void sp_redraw_region(void *ctx)
{
(void)ctx;
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
if (lvh <= 0) return;
const meow_theme *mt = meow_theme_get();
meow_ui_fill_rect(lvx, lvy, lvw, lvh, mt->surface); /* 内容底 surface */
if (g_lv) meow_widget_draw(g_lv);
meow_dirty_add(lvx, lvy, lvw, lvh);
meow_ui_flush();
}
/* 创建子页控件集(owner=SUB_SENTINEL_ACT;仅首进创建一次,跨页复用)。 */
static void sp_widgets_create(void)
{
meow_ui_activity_push(SUB_SENTINEL_ACT);
g_tb = meow_topappbar_create(0, SUB_TB_Y, fb_xres, SUB_TB_H, "");
if (g_tb) meow_topappbar_set_back(g_tb, 1);
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
g_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_lv) {
meow_listview_set_vsource(g_lv, sp_lv_count, sp_lv_bind, NULL);
/* 双行富 item + 无左图标 + 右尾件(SWITCH/ARROW);行高 56 */
meow_listview_set_item_renderer(g_lv, 2, 0, 1);
meow_listview_set_item_h(g_lv, SUB_ITEM_H);
}
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标,快速滑动时会
* 从自然停点往回拉一整行(实测 697→336),UX 不佳;默认 ease-out 惯性停(无 snap)更顺滑。
* 若后续 SDK 修正 snap 到「自然停点」,可恢复 meow_scroll_set_snap(g_sc, SUB_ITEM_H)。 */
g_sc = meow_scroll_create(sp_sc_set, sp_sc_max, sp_sc_get, sp_redraw_region, NULL);
meow_ui_activity_pop();
g_sp_init = 1;
}
/* 逐页配置:topappbar 标题 + 顶开关(按需创建/置态)+ listview 行数。 */
static void sp_configure_page(const sub_page_t *p, int top_sw_en, int top_on)
{
if (p->title && g_tb) meow_topappbar_set_title(g_tb, p->title);
g_top_sw_en = top_sw_en;
g_top_on = top_on;
if (top_sw_en) {
if (!g_sw) {
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
g_sw = meow_switch_create(swx, swy, top_on);
meow_ui_activity_pop();
} else if (g_sw) {
meow_switch_set(g_sw, top_on);
}
}
}
/* 整页绘制(进 back_buf;调用方 meow_dirty_full + flush 上屏)。 */
static void sp_paint_page(void)
{
const meow_theme *mt = meow_theme_get();
int W = fb_xres, H = fb_yres;
meow_ui_fill_rect(0, 0, W, H, mt->surface); /* 全屏底 surface */
draw_statusbar();
if (g_tb) meow_widget_draw(g_tb); /* topappbar(含底部分隔线) */
if (g_sw && g_top_sw_en) meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
meow_ui_fill_rect(lvx, lvy, lvw, lvh, mt->surface); /* 内容底 surface */
if (g_lv) meow_widget_draw(g_lv); /* 分组列表 */
/* 底部版本信息(右对齐 dim) */
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s", g_meowos_version);
ui_draw(W - 8 - ui_width(ver, 18), H - SUB_FOOT_H + 10, ver,
s_ui_theme()->outline, 18);
}
/* 顶部开关切换:写设置 + 生效 + 触发重扫 + 重填行。 */
static void sp_top_sw_toggle(void)
{
if (!g_sp || !g_sp->top_sw_set) return;
g_sp->top_sw_set(g_sp->ctx, !g_top_on);
g_top_on = !g_top_on;
if (g_sw) meow_switch_set(g_sw, g_top_on);
if (g_sp->rows) {
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
}
}
/* 动作后重扫行(act/detail 已变更数据):重填行并 clamp 滚动。 */
static void sp_refresh_rows(void)
{
if (g_sp && g_sp->rows) {
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
}
if (g_lv) {
int ms = meow_listview_max_scroll(g_lv);
if (meow_listview_scroll(g_lv) > ms) meow_listview_set_scroll(g_lv, ms);
}
}
/* ---- 运行子页(阻塞到返回;返回 0)---- */
int meow_subpage_run(sub_page_t *p)
{
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms();
unsigned long long last_live = now_ms();
int after_modal = 0; /* 子页内模态关闭后:覆盖过状态栏,需整帧重绘还原 */
/* 触摸状态 */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int scroll_drag = 0, scroll_invert = 0, drag_last_y = 0;
int wake_suppress = 0;
unsigned long long pwr_hold_at = 0;
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
/* 顶栏开关初始值 */
int top_on = 0, top_sw_en = 0;
if (p->top_sw) top_sw_en = p->top_sw(p->ctx, &top_on);
/* 首填行 + lazy 建控件 + 配置 + 首绘 */
g_sp = p;
g_nrows = p->rows ? p->rows(p->ctx, g_rows, SUB_ROWS_MAX) : 0;
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
if (!g_sp_init) sp_widgets_create();
sp_configure_page(p, top_sw_en, top_on);
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
sp_paint_page();
meow_dirty_full(); meow_ui_flush();
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) break;
int need_paint = 0;
/* ---- 自动息屏态:留在子页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- */
if (meow_auto_slept && !meow_screen_on) {
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int wake = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1;
}
if (!wake && screen_off_mode == 1 && tfd >= 0) {
struct pollfd spfd = { tfd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1;
}
}
}
if (wake) {
meow_panel_on();
meow_auto_slept = 0;
wake_suppress = 1;
last_act = now;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
after_modal = 1;
sp_paint_page();
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
continue;
}
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
} else {
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0;
pwr_suppress_click = 0;
last_act = now;
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) { logmsg("subpage pm: [sim-no-exec] POWER HOLD -> force poweroff"); }
else { logmsg("subpage pm: POWER HOLD -> force poweroff"); meow_power_action(POWER_SHUTDOWN); }
} else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("subpage pm: POWER HOLD -> show power menu");
show_power_menu();
last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1;
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1;
need_paint = 1;
goto sub_keys_seeded;
}
}
if (!pwr && m_prev_pwr) {
if (pwr_suppress_click) {
logmsg("subpage pm: power release after menu -> no toggle");
} else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) &&
!pwr_menu_state && !pwr_woke) {
logmsg("subpage pm: power single-click -> toggle panel");
meow_toggle_panel();
meow_auto_slept = 0;
last_act = now_ms();
need_paint = 1;
} else if (pwr_woke) {
logmsg("subpage pm: power release after auto wake -> no toggle");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 0;
}
}
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
(void)vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
sub_keys_seeded:
;
/* ---- 触摸(SDK 控件命中:topappbar/顶开关/listview + meow_scroll 滚动)---- */
if (tfd >= 0 && meow_screen_on) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), 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) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(按压态渲染) */
drag_last_y = tdy;
if (g_lv && meow_widget_hit(g_lv, tlx, tly)) /* 按压态即时反馈 */
sp_redraw_region(NULL);
}
if (td_ok && !scroll_drag) {
int ddy = tly - drag_last_y;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
drag_last_y = tly; /* 从此增量(跨 slop 无跳变,对齐旧版补偿) */
}
}
if (scroll_drag && td_ok) {
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
drag_last_y = tly;
int scdy = scroll_invert ? dydt : -dydt;
if (g_sc && scdy) meow_scroll_track(g_sc, scdy, now);
sp_redraw_region(NULL); /* 列表区重绘(新 offset) */
need_paint = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
else { td = 1; td_ok = 0; tlx = -1; tly = -1; scroll_drag = 0;
meow_input_touch_set(0, 0, 0, 0); }
} else if (!val && td) {
td = 0;
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
if (scroll_drag) {
scroll_drag = 0;
if (g_sc) meow_scroll_fling(g_sc); /* 松手 fling(速度采样) */
} else if (td_ok) {
int dx = tlx - tdx, dyy = tly - tdy;
/* 左缘左滑返回(全局手势语义) */
if (meow_edge_back(tdx, dx, dyy)) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto subdone;
}
int adx = dx < 0 ? -dx : dx, ady = dyy < 0 ? -dyy : dyy;
if (adx < 60 && ady < 60) {
int tx = tlx, ty = tly;
/* 顶栏:返回箭头(topappbar hit)/ 顶开关(meow_switch hit) */
int tb_hit = g_tb ? meow_topappbar_hit(g_tb, tx, ty) : -1;
if (tb_hit == MEOW_TOPBAR_BACK) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto subdone;
}
if (g_top_sw_en && g_sw && meow_widget_hit(g_sw, tx, ty)) {
sp_top_sw_toggle();
last_act = now;
need_paint = 1;
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
continue;
}
/* 列表行命中(SDK 命中:含滚动换算 + 越界/禁用行不响应) */
int row_hit = g_lv ? meow_listview_hit(g_lv, tx, ty) : -1;
if (row_hit >= 0) {
sub_kind_t kind = g_rows[row_hit].kind;
if (kind == SUB_SW || kind == SUB_ACT) {
if (g_sp->act) g_sp->act(g_sp->ctx, row_hit, g_rows[row_hit].act);
} else if (kind == SUB_ROW) {
if (g_sp->detail) g_sp->detail(g_sp->ctx, row_hit, g_rows[row_hit].act);
}
/* 动作后重扫行 + clamp 滚动 */
sp_refresh_rows();
}
/* 仅命中行(回调可能开模态)才做后置排空/触摸基线重置/整帧重绘
* (对齐旧版 idx>=0 分支;空白区点按不重绘不排空,置 td_ok=0 即可)。 */
if (row_hit >= 0) {
if (tfd >= 0) {
unsigned char e[16];
while (read(tfd, e, sizeof e) == 16) {}
usleep(40000);
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; scroll_drag = 0;
drag_last_y = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1;
need_paint = 1;
}
}
}
td_ok = 0;
}
}
}
}
}
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
if (g_sp && g_sp->live && now - last_live >= 2000) {
last_live = now;
int new_n = g_sp->rows ? g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX) : 0;
if (new_n > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX;
if (new_n != g_nrows) {
g_nrows = new_n;
int ms = g_lv ? meow_listview_max_scroll(g_lv) : 0;
if (g_lv && meow_listview_scroll(g_lv) > ms) meow_listview_set_scroll(g_lv, ms);
last_act = now;
need_paint = 1;
}
}
/* ---- 重绘(整帧/内容区局部;fling 由 meow_scroll redraw 回调自刷)---- */
if (need_paint) {
last_act = now;
sp_paint_page();
if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, SUB_TB_Y, fb_xres, fb_yres - SUB_TB_Y); /* 含顶栏(开关/返回/标题) */
meow_ui_flush();
}
/* ---- 自动息屏(空闲超时关屏,留在子页,等输入唤醒)---- */
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept &&
(unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
wake_suppress = 1;
logmsg("subpage: idle %ds -> auto screen-off (stay)", screen_off_timeout);
}
/* ~1s 状态栏时钟刷新 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
/* M7.1a: 推进 fling/回弹/snap 动画(无动画零开销) */
meow_anim_tick();
usleep(POLL_MS * 1000);
}
subdone:
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
logmsg("subpage '%s' closed", p->title ? p->title : "");
return 0;
}
Binary file not shown.
+81
View File
@@ -0,0 +1,81 @@
/* test_fg_act.c — M2.17 keep_awake:前台顶层 activity(fg_act)清空兜底宿主单测。
*
* 验证「App 会话退出/崩溃回收时清 fg_act」这一 launcher 兜底所依赖的状态机:
* - meow_fg_act_set(app,act) 依注册表 route.keep_awake → fg_keep_awake()==1;
* - App 会话结束(本工程在 launch_app_chain 退出尾调 meow_fg_act_back())→ fg 清空、
* fg_keep_awake() 回 0(主页不再被 keep_awake 卡住自动息屏);
* - App 存活时(仅有 ACT_FOCUS,未会话结束)fg_act 保持不清。
* 该测试把真实 app_launcher.c 整包含进来(同 test_lock_screen / test_notif_panel 手法),
* 用 -ffunction-sections -Wl,--gc-sections 只保留 main() 触及的 fg_act 原语,其余重函数
* (fork/exec/waitpid/fb)被剔除,故只需 stub logmsg。
*
* 运行(patch_v2/meow_launcherd 下):
* gcc -D_GNU_SOURCE -Wall -Wextra -ffunction-sections -Wl,--gc-sections \
* -I. -o test_fg_act test_fg_act.c && ./test_fg_act
* 全 PASS -> exit 0。
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
/* logmsg:fg_act 原语的唯一外部依赖(app_launcher.c 内定义,需在 include 前提供)。 */
void logmsg(const char *fmt, ...)
{
(void)fmt;
}
#include "app_launcher.c" /* 真实实现:g_accts/g_naccts/meow_fg_act_{set,back}/fg_keep_awake */
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \
else fprintf(stderr, "ok: %s\n", msg); \
} while (0)
int main(void)
{
/* ---- 基线:无上报 = 主界面(非 keep_awake)---- */
CHECK(fg_keep_awake() == 0, "baseline: empty fg_act -> keep_awake=0");
/* ---- 注册 ncm 的 player activity 声明 keep_awake(模拟 app.json awake)---- */
struct meow_acct *a = meow_acct_upsert("dev.player.ncm");
CHECK(a != NULL, "register dev.player.ncm");
a->nroute = 1;
strncpy(a->routes[0].act, "player", sizeof a->routes[0].act - 1);
a->routes[0].act[sizeof a->routes[0].act - 1] = 0;
a->routes[0].keep_awake = 1;
/* ---- sim A:App 存活、正报 keep_awake activity(如 ncm 播放页)----
* 这是「App 在 keep_awake activity 时 fg_act 保持(不清)」的契约点。 */
meow_fg_act_set("dev.player.ncm", "player");
CHECK(fg_keep_awake() == 1, "ncm playing -> keep_awake=1 (保持, 不清)");
CHECK(strcmp(g_fg_app, "dev.player.ncm") == 0, "fg_app kept while app alive");
CHECK(strcmp(g_fg_act, "player") == 0, "fg_act kept while app alive");
/* ---- sim B:App 会话结束/崩溃回收(launch_app_chain 退出尾调用 meow_fg_act_back())---- */
meow_fg_act_back();
CHECK(fg_keep_awake() == 0, "app session end -> keep_awake=0 (兜底已清)");
CHECK(g_fg_act[0] == 0, "fg_act cleared on session end");
CHECK(g_fg_app[0] == 0, "fg_app cleared on session end");
/* ---- sim C:ACT_FOCUS(home) 正常弹回 home(agent-356)→ 非 keep_awake,且可再被兜底幂等清 ---- */
meow_fg_act_set("dev.player.ncm", "home"); /* home 未声明 keep_awake */
CHECK(fg_keep_awake() == 0, "ACT_FOCUS(home) -> keep_awake=0");
meow_fg_act_back(); /* 兜底幂等:再清一次不冲突 */
CHECK(g_fg_act[0] == 0 && fg_keep_awake() == 0, "back after home focus idempotent");
/* ---- sim D:act 为空(set(NULL act))/未知 app / 未知 act → 非 keep_awake ---- */
meow_fg_act_set("dev.player.ncm", NULL);
CHECK(fg_keep_awake() == 0, "set(act=NULL) -> keep_awake=0");
meow_fg_act_set("no.such.app", "player");
CHECK(fg_keep_awake() == 0, "unregistered app -> keep_awake=0");
meow_fg_act_set("dev.player.ncm", "notascreen");
CHECK(fg_keep_awake() == 0, "unknown act in known app -> keep_awake=0");
if (g_fail) { fprintf(stderr, "\n== %d assertion(s) FAILED ==\n", g_fail); return 1; }
fprintf(stderr, "\n== test_fg_act ALL PASS ==\n");
return 0;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+223
View File
@@ -0,0 +1,223 @@
/* test_lock_widgets.c — M7.1b 锁屏 UI 控件化宿主验证。
*
* 复现 lock_screen_ui.c 所建锁屏键盘控件集(12 键 meow_widget_custom + meow_widget 命中 +
* 按压态 MEOW_WS_PRESSED→primary 26% α),验证:
* P1 键盘渲染:12 键各画成 surfaceContainer 圆角钮(非背景像素);
* P2 键盘命中:meow_widget_hit 在每键中心命中;键值映射(digit/清空/退格)正确;间隙不命中;
* P3 主题四态(MD3/MIUIx × 明暗)键盘渲染非背景;
* P4 按压态:meow_widget_set_state(MEOW_WS_PRESSED) → 该键像素变化(primary 26% α 叠加);
* P5 键值 round-trip:lk_key_value_at 在键中心/间隙分别返回正确键值与 LK_DOWN_NONE。
*
* 布局几何/键序与 lock_screen_ui.c 镜像(LK_X0..LK_COLS/LK_ROWS/lk_keys),控件创建方式与
* lk_widgets_build 同(owner=哨兵 0x51,meow_widget_custom_create,render 读 meow_widget_rect 几何)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -o test_lock_widgets test_lock_widgets.c \
* ../sdk/test_host_stub.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/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_lock_widgets
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf / meow_widget_rect / meow_gfx_fill_round[_alpha] */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* ---- 与 lock_screen_ui.c 同源几何(镜像) ---- */
#define LK_X0 32
#define LK_COLW 130
#define LK_COLG 8
#define LK_Y0 320
#define LK_ROW_H 82
#define LK_ROWG 10
#define LK_ROWS 4
#define LK_COLS 3
#define LK_ACT_CLR (-2)
#define LK_ACT_BS (-1)
#define LK_SENTINEL 0x51 /* 锁屏控件 owner 哨兵(镜像 lock_screen_ui.c) */
#define LK_DOWN_NONE (-10)
static const int lk_keys[LK_ROWS][LK_COLS] = {
{ 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 }, { LK_ACT_CLR, 0, LK_ACT_BS },
};
#define XRES 480
#define YRES 720
static meow_widget *lk_w[LK_ROWS][LK_COLS];
static void lk_rect(int row, int col, int rc[4])
{
rc[0] = LK_X0 + col * (LK_COLW + LK_COLG);
rc[1] = LK_Y0 + row * (LK_ROW_H + LK_ROWG);
rc[2] = LK_COLW;
rc[3] = LK_ROW_H;
}
static void lk_center(int row, int col, int *cx, int *cy)
{
int rc[4]; lk_rect(row, col, rc);
*cx = rc[0] + rc[2] / 2;
*cy = rc[1] + rc[3] / 2;
}
/* ---- 键渲染(镜像 lock_screen_ui.c lk_key_render 的视觉语义)---- */
static void key_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
int rad = (th->radius > 0) ? th->radius : 10;
meow_gfx_fill_round(rc[0], rc[1], rc[2], rc[3], rad, th->surfaceContainer);
if (meow_widget_state(w) & MEOW_WS_PRESSED)
meow_gfx_fill_round_alpha(rc[0], rc[1], rc[2], rc[3], rad, th->primary, 66);
meow_ui_text_center(rc[0] + rc[2] / 2,
rc[1] + rc[3] / 2 - meow_ui_text_height(22) / 2,
"1", 22, th->onSurface);
}
static void mk_widgets(void)
{
meow_ui_activity_push(LK_SENTINEL);
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++) {
int rc[4]; lk_rect(r, c, rc);
lk_w[r][c] = meow_widget_custom_create(rc[0], rc[1], rc[2], rc[3],
key_render, NULL,
(void *)(intptr_t)lk_keys[r][c]);
}
meow_ui_activity_pop();
}
/* 坐标 -> 键值(镜像 lock_screen_ui.c lk_key_value_at)。 */
static int key_value_at(int x, int y)
{
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_w[r][c] && meow_widget_hit(lk_w[r][c], x, y))
return lk_keys[r][c];
return LK_DOWN_NONE;
}
static void draw_all(void)
{
const meow_theme *th = meow_theme_get();
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_w[r][c]) meow_widget_draw(lk_w[r][c]);
}
static unsigned short px565(int x, int y)
{
unsigned char *bb = meow_ui_back_buf();
int pitch = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0 || x >= XRES || y >= YRES) return 0;
if (bpp == 16) return *(unsigned short *)(bb + (size_t)y * pitch + (size_t)x * 2);
return (unsigned short)(*(unsigned int *)(bb + (size_t)y * pitch + (size_t)x * 4) >> 16);
}
static unsigned short bg565(uint32_t bg)
{
return (unsigned short)(((bg >> 8) & 0xF800) | ((bg >> 5) & 0x07E0) | ((bg >> 3) & 0x001F));
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/lock_widgets.ppm", 1);
meow_ui_opts o = { XRES, YRES };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
mk_widgets();
meow_ui_activity_push(LK_SENTINEL); /* 命中闸门打开(owner==active) */
/* ============ P1 键盘渲染(12 键各画成非背景钮) ============ */
printf("P1 keypad render (12 keys non-bg)\n");
const meow_theme *th = meow_theme_get();
draw_all();
int all_nonbg = 1;
for (int r = 0; r < LK_ROWS && all_nonbg; r++)
for (int c = 0; c < LK_COLS && all_nonbg; c++) {
int cx, cy; lk_center(r, c, &cx, &cy);
if (px565(cx, cy) == bg565(th->surface)) all_nonbg = 0;
}
CHECK(all_nonbg, "each of 12 keys draws non-surface at center");
/* ============ P2 命中(meow_widget_hit + 键值映射) ============ */
printf("P2 keypad hit via meow_widget_hit\n");
int hit_ok = 1, val_ok = 1;
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++) {
int cx, cy; lk_center(r, c, &cx, &cy);
if (!meow_widget_hit(lk_w[r][c], cx, cy)) hit_ok = 0;
if (key_value_at(cx, cy) != lk_keys[r][c]) val_ok = 0;
}
CHECK(hit_ok, "meow_widget_hit hits every key center");
CHECK(val_ok, "key_value_at maps every key center to its digit/clear/backspace value");
/* 间隙(两列之间)不命中 */
int gap_x = LK_X0 + LK_COLW + LK_COLG / 2; /* 列0 与列1 之间的间隙中心 */
int gap_y = LK_Y0 + LK_ROW_H / 2;
CHECK(meow_widget_hit(lk_w[0][0], gap_x, gap_y) == 0, "gap between columns not hit");
CHECK(key_value_at(gap_x, gap_y) == LK_DOWN_NONE, "gap not mapped to any key");
/* ============ P3 主题四态(MD3/MIUIx × 明暗)键盘渲染非背景 ============ */
printf("P3 theme 4-state keypad render\n");
const int styles[2] = { MEOW_STYLE_MD3, MEOW_STYLE_MIUIX };
const int modes[2] = { MEOW_THEME_LIGHT, MEOW_THEME_DARK };
char nm[96];
for (int si = 0; si < 2; si++) {
for (int mi = 0; mi < 2; mi++) {
meow_ui_set_style(styles[si]);
meow_theme_set_mode((meow_theme_mode)modes[mi]);
th = meow_theme_get();
draw_all();
/* 用一个键区的多像素带判定非背景(文字+底) */
int rc[4]; lk_rect(1, 0, rc);
int nonbg = 0;
for (int dy = 0; dy < rc[3] && !nonbg; dy++)
for (int dx = 0; dx < rc[2] && !nonbg; dx++)
if (px565(rc[0] + dx, rc[1] + dy) != bg565(th->surface)) nonbg = 1;
snprintf(nm, sizeof nm, "style=%s mode=%s keypad renders non-surface",
styles[si] ? "MIUIx" : "MD3", modes[mi] ? "dark" : "light");
CHECK(nonbg, "%s", nm);
}
}
meow_ui_set_style(MEOW_STYLE_MD3);
meow_theme_set_mode(MEOW_THEME_DARK);
/* ============ P4 按压态(MEOW_WS_PRESSED → primary 26% α 像素变化) ============ */
printf("P4 pressed state -> primary 26%% alpha overlay\n");
th = meow_theme_get();
draw_all();
int cx, cy; lk_center(2, 1, &cx, &cy); /* 键 (2,1) = 数字 8 */
unsigned short p0 = px565(cx, cy);
meow_widget_set_state(lk_w[2][1], MEOW_WS_PRESSED, 1);
draw_all();
unsigned short p1 = px565(cx, cy);
meow_widget_set_state(lk_w[2][1], MEOW_WS_PRESSED, 0);
CHECK(p1 != p0, "pressed key center pixel changes (primary 26%% overlay)");
draw_all();
unsigned short p2 = px565(cx, cy);
CHECK(p2 == p0, "release restores original pixel");
/* ============ P5 键值 round-trip(清空/退格/间隙) ============ */
printf("P5 key-value round-trip (clear/backspace/gap)\n");
int ccx, ccy, bcx, bcy, zcx, zcy;
lk_center(3, 0, &ccx, &ccy); /* 清空 */
lk_center(3, 1, &zcx, &zcy); /* 0 */
lk_center(3, 2, &bcx, &bcy); /* 退格 */
CHECK(key_value_at(ccx, ccy) == LK_ACT_CLR, "bottom-left center == CLEAR (-2)");
CHECK(key_value_at(zcx, zcy) == 0, "bottom-mid center == 0");
CHECK(key_value_at(bcx, bcy) == LK_ACT_BS, "bottom-right center == BACKSPACE (-1)");
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
Binary file not shown.
+360
View File
@@ -0,0 +1,360 @@
/* test_menu_sdk.c — M7.4 主界面迁 SDK(meow_lazygrid 宫格 + meow_listview 列表 + meow_scroll)宿主验证。
*
* 复现 main_menu.c M7.4 SDK 路径所用几何/数据源(与 main_menu.c 同源常量镜像):
* app 区视口 = menu_app_region(apps_top, layout):宫格 x=12(PAD) w=xres-2*PAD / 列表 x=8 w=xres-16,
* 顶=apps_top(=ROW_TOP=96,宿主无字库→mfont_ready=0),底=EXIT_ROW-ROW_H-4=584;
* 宫格 cell_w=(xres-2*PAD-gap)/cols=223,cell_h=CARD_H=112,gap=10,row_h=122;
* 列表 row_h=LIST_ROW_H=60。应用数 MAX_APPS=8。
*
* 验证(M7 §4 验收点:双布局可切 / 图标·缺失·后台标注正确 / 空态 / 滚动惯性):
* G1 宫格虚拟化渲染:每可见格调自绘一次;格位置 = menu_app_region/menu_grid_cell_w 公式映射
* (480×720 校准:cell0=12,96 cell1=245,96 cell2=12,218 … 不被截断、不越视口)。
* G2 宫格命中:格中心→正确 idx;水平/垂直 gap→-1;越界→-1;滚动后命中换算正确。
* G3 宫格滚动:8 格(MAX_APPS)装得下 → max_scroll==0;12 格超屏 → max_scroll>0;scroll_attach 可建。
* L1 列表渲染+命中:视口 x=8,w=464;row 矩形与公式一致;8 行装得下 / 12 行超屏。
* E 空态:count==0 → 不画任何格/行卡(空态路径),无回退。
* M 图标·缺失·后台标注:自绘按 app->ok/meow_bg_is_backgrounded 决策「[缺失]」「·后台」/均无。
*
* 注:main_menu.c 的 menu_draw_grid_cell/menu_draw_list_cell 是 static(依赖 launcher 全局面),
* 宿主以同源公式/决策规则镜像复现其契约;SDK 控件行为(虚拟化/命中/滚动/空态)为真实验证。
*
* 编译(在 patch_v2/meow_launcherd 下,宿主 gcc):
* gcc -Wall -Wextra -o test_menu_sdk test_menu_sdk.c \
* ../sdk/test_host_stub.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_lazygrid.c ../sdk/meow_ui_img.c ../sdk/meow_ui_media.c \
* ../sdk/meow_notify_ipc.c ../sdk/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_menu_sdk
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp + meow_gfx_clip */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* ---- 与 main_menu.c / meow_launcher_internal.h 同源几何(镜像) ---- */
#define XRES 480
#define YRES 720
#define MENU_GRID_PAD 12
#define MENU_GRID_GAP 10
#define MENU_GRID_COLS 2
#define MENU_GRID_CARD_H 112
#define MENU_GRID_ICON 44
#define MENU_LIST_ROW_H 60
#define MENU_LIST_ICON 40
#define MENU_LIST_ICON_X 24
#define ROW_H 52
#define ROW_TOP 96
#define EXIT_ROW 640
#define APPS_TOP ROW_TOP /* 宿主无字库 → mfont_ready=0 → menu_apps_top()=ROW_TOP */
#define APPS_BOT (EXIT_ROW - ROW_H - 4) /* 584:设置行顶 */
#define SDK_ACT 0x55
/* menu_app_region 镜像:宫格 vx=12 w=xres-24;列表 vx=8 w=xres-16;vh=底-顶。 */
static int grid_cell_w(void) { return (XRES - 2 * MENU_GRID_PAD - (MENU_GRID_COLS - 1) * MENU_GRID_GAP) / MENU_GRID_COLS; }
static int grid_row_h(void) { return MENU_GRID_CARD_H + MENU_GRID_GAP; }
static void grid_region(int *vx, int *vy, int *vw, int *vh) { *vx = MENU_GRID_PAD; *vy = APPS_TOP; *vw = XRES - 2 * MENU_GRID_PAD; *vh = APPS_BOT - APPS_TOP; }
static void list_region(int *vx, int *vy, int *vw, int *vh) { *vx = 8; *vy = APPS_TOP; *vw = XRES - 16; *vh = APPS_BOT - APPS_TOP; }
/* ---- mock 应用(镜像 struct app 的关键字段) ---- */
typedef struct { const char *name; int ok; int bg; const char *pkg; } tapp_t;
static tapp_t g_apps[16];
static int g_napps = 0;
static int g_draw_cnt = 0; /* 自绘回调调用次数(窗口虚拟化) */
static int g_cell_x[16], g_cell_y[16], g_cell_w[16], g_cell_h[16]; /* 自绘收到的几何 */
static int g_cell_idx[16];
static char g_label[16][16]; /* 格/行自绘决策出的标注("" / "[缺失]" / "·后台") */
/* bind:读 mock app,title=名,enabled=1(命中即启动语义,同 main_menu menu_grid_bind)。 */
static void grid_bind(int idx, meow_lazygrid_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
if (idx < 0 || idx >= g_napps) return;
it->title = g_apps[idx].name;
it->subtitle = NULL;
it->icon = NULL; /* 宿主无图标位图;自绘用文字/色块近似 */
it->enabled = 1;
it->user = &g_apps[idx];
}
static int grid_count(void *ctx) { (void)ctx; return g_napps; }
static void list_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
if (idx < 0 || idx >= g_napps) return;
it->title = g_apps[idx].name;
it->subtitle = NULL;
it->icon_kind = MEOW_LIST_ICON_NONE;
it->enabled = 1;
it->user = &g_apps[idx];
}
static int list_count(void *ctx) { (void)ctx; return g_napps; }
/* 镜像 menu_draw_grid_cell 的标注决策(ok→缺失[缺失];bg→·后台;均无→无)。 */
static void decide_label(int idx, char *out, int olen)
{
const tapp_t *a = &g_apps[idx];
if (!a->ok) snprintf(out, olen, "[缺失]");
else if (a->bg) snprintf(out, olen, "·后台");
else out[0] = 0;
}
/* 宫格自绘(镜像 menu_sdk_grid_draw → 记录几何 + 标注决策;宿主无字体用色块近似)。 */
static void grid_draw(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)st; (void)ctx;
if (idx < 0 || idx >= g_napps) return;
if (g_draw_cnt < 16) {
g_cell_idx[g_draw_cnt] = idx; g_cell_x[g_draw_cnt] = x; g_cell_y[g_draw_cnt] = y;
g_cell_w[g_draw_cnt] = w; g_cell_h[g_draw_cnt] = h;
g_draw_cnt++;
}
decide_label(idx, g_label[idx], sizeof g_label[idx]);
meow_gfx_fill_round(x, y, w, h, 16, meow_theme_get()->surfaceContainer);
meow_gfx_fill_round(x + w / 2 - 22, y + 12, 44, 44, 13, meow_theme_get()->primary);
}
/* 列表自绘(镜像 menu_sdk_list_draw) */
static void list_draw(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)st; (void)ctx;
if (idx < 0 || idx >= g_napps) return;
if (g_draw_cnt < 16) {
g_cell_idx[g_draw_cnt] = idx; g_cell_x[g_draw_cnt] = x; g_cell_y[g_draw_cnt] = y;
g_cell_w[g_draw_cnt] = w; g_cell_h[g_draw_cnt] = h;
g_draw_cnt++;
}
decide_label(idx, g_label[idx], sizeof g_label[idx]);
meow_gfx_fill_round(x, y, w, h, 12, meow_theme_get()->surfaceContainer);
meow_gfx_fill_round(x + MENU_LIST_ICON_X, y + (h - MENU_LIST_ICON) / 2, MENU_LIST_ICON, MENU_LIST_ICON, 12,
meow_theme_get()->primary);
}
static void seed_apps(int n)
{
static const char *nm[16] = {"应用一","应用二","应用三","应用四","应用五","应用六","应用七","应用八",
"应用九","应用十","应用十一","应用十二","应用十三","应用十四","应用十五","应用十六"};
for (int i = 0; i < n; i++) {
g_apps[i].name = nm[i]; g_apps[i].ok = 1; g_apps[i].bg = 0; g_apps[i].pkg = "os.meow.test";
}
/* 2 号缺失、3 号后台(用 0/1 锚定到前几格便于断言) */
if (n > 1) g_apps[1].ok = 0;
if (n > 2) g_apps[2].bg = 1;
g_napps = n;
}
int main(void)
{
meow_ui_opts o = { XRES, YRES };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
int gx, gy, gw, gh, lx, ly, lw, lh;
grid_region(&gx, &gy, &gw, &gh); list_region(&lx, &ly, &lw, &lh);
int cw = grid_cell_w(), rh = grid_row_h();
CHECK(gx == MENU_GRID_PAD && gw == XRES - 2 * MENU_GRID_PAD, "grid viewport x=%d w=%d (expect %d,%d)", gx, gw, MENU_GRID_PAD, XRES - 2 * MENU_GRID_PAD);
CHECK(lx == 8 && lw == XRES - 16, "list viewport x=%d w=%d", lx, lw);
CHECK(gh == APPS_BOT - APPS_TOP, "grid viewport h=%d (expect %d)", gh, APPS_BOT - APPS_TOP);
/* ============ G1 宫格虚拟化渲染 + 几何校准 ============ */
printf("G1 lazygrid render @480x720\n");
{
seed_apps(8);
meow_ui_activity_push(SDK_ACT);
meow_lazygrid *g = meow_lazygrid_create(gx, gy, gw, gh, MENU_GRID_COLS, cw, MENU_GRID_CARD_H, MENU_GRID_GAP);
CHECK(g != NULL, "lazygrid create");
CHECK(meow_lazygrid_set_vsource(g, grid_count, grid_bind, NULL) == 0, "vsource");
CHECK(meow_lazygrid_set_item_draw(g, grid_draw, NULL) == 0, "set_item_draw");
CHECK(meow_lazygrid_set_empty_text(g, "没有找到应用") == 0, "empty_text");
meow_ui_clear(th->bg);
g_draw_cnt = 0;
meow_lazygrid_draw(g, th);
/* 8 格(MAX_APPS)全可见:每格恰一次自绘 */
CHECK(g_draw_cnt == 8, "8 apps -> each visible cell drawn once got %d", g_draw_cnt);
/* 几何校准:cell0=12,96 cell1=245,96 cell2(行1col0)=12,218 cell3=245,218 */
CHECK(g_cell_idx[0] == 0 && g_cell_x[0] == 12 && g_cell_y[0] == 96, "cell0 geom idx=%d x=%d y=%d (expect 0,12,96)", g_cell_idx[0], g_cell_x[0], g_cell_y[0]);
CHECK(g_cell_idx[1] == 1 && g_cell_x[1] == 12 + cw + MENU_GRID_GAP && g_cell_y[1] == 96, "cell1 geom idx=%d x=%d y=%d", g_cell_idx[1], g_cell_x[1], g_cell_y[1]);
CHECK(g_cell_idx[2] == 2 && g_cell_x[2] == 12 && g_cell_y[2] == 96 + rh, "cell2 geom idx=%d x=%d y=%d", g_cell_idx[2], g_cell_x[2], g_cell_y[2]);
CHECK(g_cell_idx[7] == 7 && g_cell_y[7] == 96 + 3 * rh, "cell7 (row3col1) y=%d (expect %d)", g_cell_y[7], 96 + 3 * rh);
/* 全部格/行不越视口(右侧 ≤ gx+gw,底部 ≤ gy+gh) */
int bad = 0;
for (int i = 0; i < g_draw_cnt; i++)
if (g_cell_x[i] + g_cell_w[i] > gx + gw || g_cell_y[i] + g_cell_h[i] > gy + gh) bad++;
CHECK(bad == 0, "all cells within grid viewport (bad=%d, vp=[%d,%d]x[%d,%d])", bad, gx, gx + gw, gy, gy + gh);
/* 8 格(MAX_APPS)装得下 → max_scroll==0 */
CHECK(meow_lazygrid_max_scroll(g) == 0, "8 apps fit -> max_scroll=0 got %d", meow_lazygrid_max_scroll(g));
meow_lazygrid_free(g);
meow_ui_activity_pop();
}
/* ============ G2 宫格命中(含滚动换算:用 12 应用使超屏可滚) ============ */
printf("G2 lazygrid hit\n");
{
seed_apps(12);
meow_ui_activity_push(SDK_ACT);
meow_lazygrid *g = meow_lazygrid_create(gx, gy, gw, gh, MENU_GRID_COLS, cw, MENU_GRID_CARD_H, MENU_GRID_GAP);
meow_lazygrid_set_vsource(g, grid_count, grid_bind, NULL);
/* cell0 中心(123,152) → idx0;cell1 中心(356,152) → idx1;行1col0 中心(123,274=96+122+56) → idx2 */
CHECK(meow_lazygrid_hit(g, 12 + cw / 2, 96 + MENU_GRID_CARD_H / 2) == 0, "hit cell0 center got %d", meow_lazygrid_hit(g, 12 + cw / 2, 96 + MENU_GRID_CARD_H / 2));
CHECK(meow_lazygrid_hit(g, 12 + cw + MENU_GRID_GAP + cw / 2, 96 + MENU_GRID_CARD_H / 2) == 1, "hit cell1 center got %d", meow_lazygrid_hit(g, 12 + cw + MENU_GRID_GAP + cw / 2, 96 + MENU_GRID_CARD_H / 2));
CHECK(meow_lazygrid_hit(g, 12 + cw / 2, 96 + rh + MENU_GRID_CARD_H / 2) == 2, "hit row1col0 got %d", meow_lazygrid_hit(g, 12 + cw / 2, 96 + rh + MENU_GRID_CARD_H / 2));
/* 水平 gap x=240(col0[12,235) col1[245,468) 之间)→ -1 */
CHECK(meow_lazygrid_hit(g, 240, 96 + MENU_GRID_CARD_H / 2) == -1, "horizontal gap miss got %d", meow_lazygrid_hit(g, 240, 96 + MENU_GRID_CARD_H / 2));
/* 垂直 gap y=212(row0[96,208) row1[218,320) 之间)→ -1 */
CHECK(meow_lazygrid_hit(g, 12 + cw / 2, 212) == -1, "vertical gap miss got %d", meow_lazygrid_hit(g, 12 + cw / 2, 212));
/* 越界(右侧 x=475 / 列表区外 y=92)→ -1 */
CHECK(meow_lazygrid_hit(g, 475, 96 + MENU_GRID_CARD_H / 2) == -1, "right-out miss got %d", meow_lazygrid_hit(g, 475, 96 + MENU_GRID_CARD_H / 2));
CHECK(meow_lazygrid_hit(g, 12 + cw / 2, APPS_TOP - 4) == -1, "above viewport -> -1 got %d", meow_lazygrid_hit(g, 12 + cw / 2, APPS_TOP - 4));
/* 滚 1 行(rh=122)后,视口顶 y=96 中心(x)落在行1 → idx2 */
meow_lazygrid_set_scroll(g, rh);
CHECK(meow_lazygrid_hit(g, 12 + cw / 2, 96 + MENU_GRID_CARD_H / 2) == 2, "scroll+122: top center hits row1 -> idx2 got %d", meow_lazygrid_hit(g, 12 + cw / 2, 96 + MENU_GRID_CARD_H / 2));
/* tap_at:命中 → 选中 + 回调 */
meow_lazygrid_set_scroll(g, 0);
CHECK(meow_lazygrid_tap_at(g, 12 + cw / 2, 96 + MENU_GRID_CARD_H / 2) == 0, "tap_at idx0 got %d", meow_lazygrid_tap_at(g, 12 + cw / 2, 96 + MENU_GRID_CARD_H / 2));
CHECK(meow_lazygrid_sel(g) == 0, "sel=0");
meow_lazygrid_free(g);
meow_ui_activity_pop();
}
/* ============ G3 宫格滚动(超屏 + scroll_attach) ============ */
printf("G3 lazygrid overflow scroll\n");
{
seed_apps(12); /* 6 行 → 内容高 722 > 视口 488 */
meow_ui_activity_push(SDK_ACT);
meow_lazygrid *g = meow_lazygrid_create(gx, gy, gw, gh, MENU_GRID_COLS, cw, MENU_GRID_CARD_H, MENU_GRID_GAP);
meow_lazygrid_set_vsource(g, grid_count, grid_bind, NULL);
int ms = meow_lazygrid_max_scroll(g);
CHECK(ms > 0, "12 apps overflow max_scroll=%d (>0)", ms);
meow_lazygrid_set_item_draw(g, grid_draw, NULL);
meow_ui_clear(th->bg);
g_draw_cnt = 0;
meow_lazygrid_draw(g, th);
int win_rows = gh / rh + 1; /* 可见行窗口上界 */
CHECK(g_draw_cnt <= win_rows * MENU_GRID_COLS, "virtualized window: drew %d <= %d (not 12)", g_draw_cnt, win_rows * MENU_GRID_COLS);
/* scroll_by 往返 */
int before = meow_lazygrid_scroll(g);
meow_lazygrid_scroll_by(g, rh);
CHECK(meow_lazygrid_scroll(g) == before + rh, "scroll_by +row got %d (expect %d)", meow_lazygrid_scroll(g), before + rh);
meow_lazygrid_set_scroll(g, 0);
CHECK(meow_lazygrid_scroll(g) == 0, "set_scroll 0");
/* scroll_attach 可建(fling/snap 惯性滚动接线) */
meow_scroll *s = meow_lazygrid_scroll_attach(g);
CHECK(s != NULL, "scroll_attach non-NULL");
if (s) {
meow_scroll_set_snap(s, rh);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 10; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
int st = meow_scroll_offset(s);
CHECK(st > 0, "drag advances offset got %d", st);
meow_scroll_fling(s);
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) { meow_anim_tick(); usleep(10000); }
meow_anim_tick();
int en = meow_scroll_offset(s);
CHECK(en >= st, "fling advances %d >= %d", en, st);
CHECK(en <= meow_lazygrid_max_scroll(g), "fling in range %d (max %d)", en, meow_lazygrid_max_scroll(g));
CHECK(meow_lazygrid_scroll(g) == en, "grid scroll mirrors offset");
meow_scroll_free(s);
}
meow_lazygrid_free(g);
meow_ui_activity_pop();
}
/* ============ L1 列表渲染 + 命中 + 滚动 ============ */
printf("L1 listview render + hit + scroll\n");
{
seed_apps(8);
meow_ui_activity_push(SDK_ACT);
meow_widget *lv = meow_listview_create(lx, ly, lw, lh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
CHECK(lv != NULL, "listview create");
meow_listview_set_vsource(lv, list_count, list_bind, NULL);
meow_listview_set_item_draw(lv, list_draw, NULL);
meow_listview_set_item_h(lv, MENU_LIST_ROW_H);
meow_ui_fill_rect(0, 0, XRES, YRES, th->bg);
meow_ui_fill_rect(lx, ly, lw, lh, th->surfaceVariant);
g_draw_cnt = 0;
meow_widget_draw(lv);
CHECK(g_draw_cnt == 8, "8 rows drawn got %d", g_draw_cnt);
CHECK(g_cell_x[0] == 8 && g_cell_w[0] == XRES - 16, "row0 x=%d w=%d (expect 8,%d)", g_cell_x[0], g_cell_w[0], XRES - 16);
CHECK(g_cell_y[0] == APPS_TOP && g_cell_y[1] == APPS_TOP + MENU_LIST_ROW_H, "row0/1 y=%d/%d", g_cell_y[0], g_cell_y[1]);
CHECK(meow_listview_max_scroll(lv) == 0, "8 rows fit -> max_scroll=0 got %d", meow_listview_max_scroll(lv));
CHECK(meow_listview_hit(lv, 100, APPS_TOP + MENU_LIST_ROW_H / 2) == 0, "hit row0 got %d", meow_listview_hit(lv, 100, APPS_TOP + MENU_LIST_ROW_H / 2));
CHECK(meow_listview_hit(lv, 100, APPS_TOP + MENU_LIST_ROW_H + MENU_LIST_ROW_H / 2) == 1, "hit row1 got %d", meow_listview_hit(lv, 100, APPS_TOP + MENU_LIST_ROW_H + MENU_LIST_ROW_H / 2));
CHECK(meow_listview_hit(lv, 4, APPS_TOP + MENU_LIST_ROW_H / 2) == -1, "left of list x=4 -> -1 got %d", meow_listview_hit(lv, 4, APPS_TOP + MENU_LIST_ROW_H / 2));
CHECK(meow_listview_hit(lv, 100, APPS_TOP - 4) == -1, "above list -> -1 got %d", meow_listview_hit(lv, 100, APPS_TOP - 4));
meow_widget_free(lv);
meow_ui_activity_pop();
/* 12 行超屏滚动 */
seed_apps(12);
meow_ui_activity_push(SDK_ACT);
meow_widget *lv2 = meow_listview_create(lx, ly, lw, lh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_vsource(lv2, list_count, list_bind, NULL);
meow_listview_set_item_draw(lv2, list_draw, NULL);
meow_listview_set_item_h(lv2, MENU_LIST_ROW_H);
CHECK(meow_listview_max_scroll(lv2) > 0, "12 rows overflow max_scroll=%d", meow_listview_max_scroll(lv2));
meow_listview_set_scroll(lv2, MENU_LIST_ROW_H);
/* 滚 1 行:视口顶 row 中心落在 idx1 */
CHECK(meow_listview_hit(lv2, 100, APPS_TOP + MENU_LIST_ROW_H / 2) == 1, "scrolled row0->idx1 got %d", meow_listview_hit(lv2, 100, APPS_TOP + MENU_LIST_ROW_H / 2));
meow_widget_free(lv2);
meow_ui_activity_pop();
seed_apps(8); /* 还原 */
}
/* ============ E 空态(count==0 → 空态路径,无格/行卡) ============ */
printf("E empty state\n");
{
seed_apps(0);
meow_ui_activity_push(SDK_ACT);
meow_lazygrid *g = meow_lazygrid_create(gx, gy, gw, gh, MENU_GRID_COLS, cw, MENU_GRID_CARD_H, MENU_GRID_GAP);
meow_lazygrid_set_vsource(g, grid_count, grid_bind, NULL);
meow_lazygrid_set_item_draw(g, grid_draw, NULL);
meow_lazygrid_set_empty_text(g, "没有找到应用");
meow_ui_clear(th->bg);
g_draw_cnt = 0;
meow_lazygrid_draw(g, th);
CHECK(g_draw_cnt == 0, "empty grid: 0 cells drawn got %d", g_draw_cnt);
meow_lazygrid_free(g);
/* 列表空态 */
meow_widget *lv = meow_listview_create(lx, ly, lw, lh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_vsource(lv, list_count, list_bind, NULL);
meow_listview_set_item_draw(lv, list_draw, NULL);
meow_listview_set_empty_text(lv, "没有找到应用");
meow_ui_fill_rect(lx, ly, lw, lh, th->surfaceVariant);
g_draw_cnt = 0;
meow_widget_draw(lv);
CHECK(g_draw_cnt == 0, "empty list: 0 rows drawn got %d", g_draw_cnt);
meow_widget_free(lv);
meow_ui_activity_pop();
seed_apps(8);
}
/* ============ M 图标·缺失·后台标注(镜像 menu_draw_grid_cell 决策) ============ */
printf("M missing / background label\n");
{
seed_apps(8); /* app1 ok=0 → [缺失];app2 bg=1 → ·后台;app0 → 无 */
CHECK(strcmp(g_label[0], "") == 0, "app0 (ok,no bg) no label got '%s'", g_label[0]);
CHECK(memcmp(g_label[1], "[缺失]", 6) == 0, "app1 (missing) -> '[缺失]' got '%s'", g_label[1]);
CHECK(memcmp(g_label[2], "·后台", 6) == 0, "app2 (backgrounded) -> '·后台' got '%s'", g_label[2]);
/* 命中语义:enabled=1 恒响应 —— 缺 app 亦可命中(启动→失败 toast,同旧 draw_menu 行为) */
meow_ui_activity_push(SDK_ACT);
meow_lazygrid *g = meow_lazygrid_create(gx, gy, gw, gh, MENU_GRID_COLS, cw, MENU_GRID_CARD_H, MENU_GRID_GAP);
meow_lazygrid_set_vsource(g, grid_count, grid_bind, NULL);
CHECK(meow_lazygrid_hit(g, 12 + cw / 2, 96 + rh + MENU_GRID_CARD_H / 2) == 2, "missing app1 tappable (enabled=1) -> hit row0col? got %d", meow_lazygrid_hit(g, 12 + cw / 2, 96 + rh + MENU_GRID_CARD_H / 2));
meow_lazygrid_free(g);
meow_ui_activity_pop();
}
meow_ui_shutdown();
printf("menu_sdk: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
Binary file not shown.
+288
View File
@@ -0,0 +1,288 @@
/* test_notif_center.c — M7.3 通知中心内层迁 SDK 控件(宿主验证)。
*
* 复现 notif_center.c M7.3 SDK 路径所建控件集(meow_listview 虚拟化 vsource + set_item_draw 行自绘 +
* set_empty_text 空态 + set_swipe_delete 左滑删除 + 双 meow_slider + meow_switch + 自定义画布 chip),验证:
* P1 listview 渲染通知行:行卡非背景像素(图标/标题/正文)+ set_item_draw 命中(含滚动换算);
* P2 空态:count==0 且 set_empty_text 设了 → 视口内居中画文案(非背景像素);
* P3 左滑删除:begin/move(超阈值)/end → on_delete 回调恰一次 + swipe 状态复位(撤销回调不触发);
* P4 滑条/开关:meow_slider_set/get 与 meow_switch_set/get 往返一致(值写回控件);
* P5 自定义画布 chip:meow_widget_rect 取几何 + render 出非背景像素 + meow_widget_hit 命中(owner 闸门开)。
*
* 几何/控件创建与 notif_center.c「M7.3 SDK 路径」镜像(owner 钉 NCP_WIDGET_ACT;行高 NCP_ROW_H)。
* 注:宿主不含 launcher sb_icons / s_ui_theme / g_rows,故行/芯片用 meow_ui_text + 简化绘制复现契约;
* notif 行自绘函数体(ncp_lv_row_draw)依赖 launcher 全局,真机由 notif_center.c 提供。
*
* 编译(在 patch_v2/meow_launcherd 下,宿主 gcc):
* gcc -Wall -Wextra -o test_notif_center test_notif_center.c \
* ../sdk/test_host_stub.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/stb_image.c -I../sdk -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_notif_center
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf / meow_gfx_mark_dirty / meow_widget_rect */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* ---- 与 notif_center.c / meow_launcher_internal.h 同源几何(镜像) ---- */
#define XRES 480
#define YRES 720
#define NCP_TOP 28
#define NCP_CHIP_Y 86
#define NCP_CHIP_H 44
#define NCP_CHIP_W 70
#define NCP_CHIP_GAP 8
#define NCP_CHIP_N 5
#define NCP_SLIDER_X 20
#define NCP_SLIDER_W (XRES - 40)
#define NCP_SL1_Y 162
#define NCP_SL2_Y 214
#define NCP_DND_Y 264
#define NCP_DND_H 40
#define NCP_DND_SPLIT_X (XRES / 2)
#define NCP_LIST_TOP 308
#define NCP_LIST_BOT 694
#define NCP_ROW_H 92
#define NCP_ROW_X 12
#define NCP_ROW_W (XRES - 24)
#define NCP_ICON_X 26
#define NCP_TEXT_X 72
#define NCP_TEXT_W (NCP_ROW_W - (NCP_TEXT_X - NCP_ROW_X) - 10)
#define NCP_WIDGET_ACT 0x54
#define NCP_MAX_ROWS 8
/* 通知行镜像(宿主自持,替代 launcher g_rows[]/g_ncp_apps)。 */
typedef struct { const char *summary; const char *body; int progress; } trow_t;
static trow_t g_rows[NCP_MAX_ROWS];
static int g_nrows = 0;
/* ---- 像素探测 ---- */
static int px565(int x, int y, uint32_t bg)
{
unsigned short bg565 = ((bg >> 8) & 0xF800) | ((bg >> 5) & 0x07E0) | ((bg >> 3) & 0x001F);
unsigned short p;
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { p = 0; return 0; }
if (bpp == 16) p = *(unsigned short *)(bb + (size_t)y * line + (size_t)x * 2);
else p = (unsigned short)(*(unsigned int *)(bb + (size_t)y * line + (size_t)x * 4) >> 16);
return (p != bg565) ? 1 : 0;
}
static int region_nonbg(int x, int y, int w, int h, uint32_t bg)
{
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px565(x + dx, y + dy, bg)) cnt++;
return cnt;
}
/* ---- listview 虚拟化源(镜像 ncp_lv_count / ncp_lv_bind) ---- */
static int lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void lv_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
it->enabled = (idx >= 0 && idx < g_nrows) ? 1 : 0;
}
/* 行自绘(镜像 ncp_lv_row_draw 的 SDK 文本渲染;宿主无 launcher 字体/sb_icons,用 meow_ui_text 近似)。 */
static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)w; (void)state; (void)ctx;
if (idx < 0 || idx >= g_nrows) return;
const meow_theme *th = meow_theme_get();
meow_gfx_fill_round(x + NCP_ROW_X, y, NCP_ROW_W, h - 6, 12, th->surfaceContainer);
/* 图标位(无图回退:primary 圆点) */
int icx = x + NCP_ICON_X, icy = y + (h - 32) / 2;
meow_gfx_fill_round(icx + 16, icy + 16, 16, 16, 8, th->primary);
/* app 名 + 标题 + 正文 */
meow_ui_text(x + NCP_TEXT_X, y + 10, "yao", th->font_body, th->onSurfaceVariant);
meow_ui_text(x + NCP_TEXT_X, y + 30, g_rows[idx].summary, th->font_body, th->onSurface);
meow_ui_text(x + NCP_TEXT_X, y + 56, g_rows[idx].body, th->font_body, th->onSurfaceVariant);
if (g_rows[idx].progress >= 0) {
int px = x + NCP_TEXT_X, pw = NCP_TEXT_W, py = y + h - 14;
meow_ui_fill_rect(px, py, pw, 4, th->surfaceVariant);
meow_ui_fill_rect(px, py, g_rows[idx].progress * pw / 100, 4, th->primary);
}
}
/* ---- 左滑删除回调计数 ---- */
static int del_count = 0; static int del_idx = -1;
static void lv_on_delete(int idx, void *ctx)
{ (void)ctx; del_count++; del_idx = idx; }
static meow_widget *make_list(void)
{
meow_widget *lv = meow_listview_create(0, NCP_LIST_TOP, XRES, NCP_LIST_BOT - NCP_LIST_TOP,
0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_vsource(lv, lv_count, lv_bind, NULL);
meow_listview_set_item_draw(lv, lv_draw, NULL);
meow_listview_set_item_h(lv, NCP_ROW_H);
meow_listview_set_empty_text(lv, "暂无通知");
meow_listview_set_swipe_delete(lv, 1, lv_on_delete, NULL);
return lv;
}
/* 推进 meow_anim 直到 swipe 动画结束(模拟 on_tick 节拍)。 */
static void pump_swipe(meow_widget *lv)
{
int guard = 0;
while (meow_listview_swipe_active(lv) && guard++ < 80) {
meow_anim_tick();
usleep(10000);
}
meow_anim_tick();
}
/* 自定义画布 chip(镜像 ncp_chip_render:圆角 + 文本;用 meow_widget_rect 取几何)。 */
static void chip_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
meow_gfx_fill_round(rc[0], rc[1], rc[2], rc[3], 12, th->surfaceContainer);
meow_ui_text_center(rc[0] + rc[2] / 2, rc[1] + rc[3] / 2, "chip", th->font_body, th->onSurfaceVariant);
}
int main(void)
{
meow_ui_opts o = { XRES, YRES };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
/* 通知行数据(镜像:多行 + 进度)。8 行(736px) > 视口(386px) → 可滚动。 */
g_nrows = 8;
static const char *summ[8] = {"下载完成","媒体进行中","系统通知","新消息","更新提醒","日程","备份完成","低电量"};
static const char *body[8] = {"文件已就绪","正在播放","低电量提醒","收到 2 条","已就绪","10 分钟后","已成功","请充电"};
for (int i = 0; i < 8; i++) { g_rows[i].summary = summ[i]; g_rows[i].body = body[i]; g_rows[i].progress = (i < 2) ? (100 - i * 60) : -1; }
/* ============ P1 listview 渲染通知行 + 行命中 ============ */
printf("P1 notif listview render + hit\n");
{
meow_ui_activity_push(NCP_WIDGET_ACT);
meow_widget *lv = make_list();
meow_ui_fill_rect(0, 0, XRES, YRES, th->bg);
meow_ui_fill_rect(0, NCP_LIST_TOP, XRES, NCP_LIST_BOT - NCP_LIST_TOP, th->surfaceVariant);
meow_widget_draw(lv);
/* 行 0 卡底(surfaceContainer 覆盖表面 surfaceVariant),行带内非背景像素 */
int row0y = NCP_LIST_TOP + 8;
int rc = region_nonbg(NCP_ROW_X, row0y, NCP_ROW_W - 8, NCP_ROW_H - 16, th->surfaceVariant);
CHECK(rc > 40, "notif row renders (icon/title/body) cnt=%d", rc);
/* 行命中(含滚动换算):点行 1 中心 → idx1 */
CHECK(meow_listview_hit(lv, 100, NCP_LIST_TOP + NCP_ROW_H + NCP_ROW_H / 2) == 1,
"row1 center hits idx1 got %d", meow_listview_hit(lv, 100, NCP_LIST_TOP + NCP_ROW_H + NCP_ROW_H / 2));
/* 列表区外 → -1 */
CHECK(meow_listview_hit(lv, 100, NCP_LIST_TOP - 4) == -1, "above list -> -1");
/* 滚动后命中换算:set_scroll=92 时点行 0 中心 → 视口第一行是 idx1 */
meow_listview_set_scroll(lv, NCP_ROW_H);
CHECK(meow_listview_hit(lv, 100, NCP_LIST_TOP + NCP_ROW_H / 2) == 1,
"scrolled +92px: first visible center hits idx1 got %d",
meow_listview_hit(lv, 100, NCP_LIST_TOP + NCP_ROW_H / 2));
CHECK(meow_listview_max_scroll(lv) > 0, "max_scroll>0 (3 rows overflow? got %d)",
meow_listview_max_scroll(lv));
meow_widget_free(lv);
meow_ui_activity_pop();
}
/* ============ P2 空态(set_empty_text,count==0 走空态路径) ============ */
printf("P2 empty state\n");
{
meow_ui_activity_push(NCP_WIDGET_ACT);
g_nrows = 0;
meow_widget *lv = make_list();
meow_ui_fill_rect(0, NCP_LIST_TOP, XRES, NCP_LIST_BOT - NCP_LIST_TOP, th->surfaceVariant);
meow_widget_draw(lv);
/* count==0 → 不画任何行卡(空态路径:视口仍 surfaceVariant 底,无 surfaceContainer 行卡)。
* 宿主无 CJK 字体,「暂无通知」文案可能不显形,故以「无行卡」为健壮判据(空态路径已走)。 */
int rowc = region_nonbg(NCP_ROW_X, NCP_LIST_TOP + 8, NCP_ROW_W - 8, NCP_ROW_H - 16, th->surfaceVariant);
CHECK(rowc == 0, "empty: no row cards rendered cnt=%d", rowc);
meow_widget_free(lv);
g_nrows = 8; /* 还原 */
meow_ui_activity_pop();
}
/* ============ P3 左滑删除(SDK swipe-delete 回调) ============ */
printf("P3 notif row swipe delete\n");
{
meow_ui_activity_push(NCP_WIDGET_ACT);
del_count = 0; del_idx = -1;
meow_widget *lv = make_list();
int row0y = NCP_LIST_TOP + NCP_ROW_H / 2;
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "P3 begin on row0 = 1");
/* 左滑超阈值(NCP_ROW_W*2/5 ≈ 182) */
CHECK(meow_listview_swipe_move(lv, 100 - 260, row0y) == 1, "P3 move big left = 1");
CHECK(meow_listview_swipe_end(lv, 100 - 260, row0y) == 1, "P3 end = 1 (delete anim)");
CHECK(meow_listview_swipe_active(lv) == 1, "P3 delete anim active");
pump_swipe(lv);
CHECK(del_count == 1, "P3 on_delete fired once got %d", del_count);
CHECK(del_idx == 0, "P3 on_delete idx0 got %d", del_idx);
CHECK(meow_listview_swipe_active(lv) == 0, "P3 state reset");
CHECK(meow_listview_swipe_offset(lv) == 0, "P3 dx reset 0 got %d", meow_listview_swipe_offset(lv));
/* 撤销:未超阈值 → 回弹,不触发 on_delete */
del_count = 0;
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "P3 undo begin = 1");
CHECK(meow_listview_swipe_move(lv, 70, row0y) == 1, "P3 undo move left 30px = 1");
CHECK(meow_listview_swipe_end(lv, 70, row0y) == 1, "P3 undo end = 1 (bounce)");
pump_swipe(lv);
CHECK(del_count == 0, "P3 undo: on_delete NOT fired got %d", del_count);
CHECK(meow_listview_swipe_offset(lv) == 0, "P3 undo dx=0 got %d", meow_listview_swipe_offset(lv));
meow_widget_free(lv);
meow_ui_activity_pop();
}
/* ============ P4 滑条 / 开关 set/get ============ */
printf("P4 slider/switch roundtrip\n");
{
meow_ui_activity_push(NCP_WIDGET_ACT);
meow_widget *sl = meow_slider_create(NCP_SLIDER_X, NCP_SL1_Y - 14, NCP_SLIDER_W, 1, 100, 50);
CHECK(meow_slider_get(sl) == 50, "P4 slider init got %d", meow_slider_get(sl));
meow_slider_set(sl, 78);
CHECK(meow_slider_get(sl) == 78, "P4 slider set 78 got %d", meow_slider_get(sl));
meow_slider_set(sl, 5);
CHECK(meow_slider_get(sl) == 5, "P4 slider clamp low 5 got %d", meow_slider_get(sl));
meow_widget *sw = meow_switch_create(0, 0, 0);
CHECK(meow_switch_get(sw) == 0, "P4 switch init off got %d", meow_switch_get(sw));
meow_switch_set(sw, 1);
CHECK(meow_switch_get(sw) == 1, "P4 switch on got %d", meow_switch_get(sw));
CHECK(meow_widget_selected(sw) == 1, "P4 switch on -> selected bit");
meow_widget_free(sl); meow_widget_free(sw);
meow_ui_activity_pop();
}
/* ============ P5 自定义画布 chip:几何 + 渲染 + 命中 ============ */
printf("P5 custom chip widget\n");
{
meow_ui_activity_push(NCP_WIDGET_ACT);
meow_widget *chip = meow_widget_custom_create(NCP_CHIP_Y, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H,
chip_render, NULL, NULL);
int rc[4]; meow_widget_rect(chip, rc);
CHECK(rc[0] == NCP_CHIP_Y && rc[1] == NCP_CHIP_Y && rc[2] == NCP_CHIP_W && rc[3] == NCP_CHIP_H,
"P5 chip rect x=%d y=%d w=%d h=%d", rc[0], rc[1], rc[2], rc[3]);
meow_ui_fill_rect(0, 0, XRES, YRES, th->bg);
meow_widget_draw(chip);
int cc = region_nonbg(rc[0] + 4, rc[1] + 4, rc[2] - 8, rc[3] - 8, th->bg);
CHECK(cc > 20, "P5 chip renders non-bg cnt=%d", cc);
/* meow_widget_hit(owner 闸门已开):中心命中、区外未命中 */
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
CHECK(meow_widget_hit(chip, cx, cy) == 1, "P5 chip center hit");
CHECK(meow_widget_hit(chip, cx + NCP_CHIP_W + 30, cy) == 0, "P5 chip outside miss");
meow_widget_free(chip);
meow_ui_activity_pop();
}
meow_ui_shutdown();
printf("notif_center: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -46,9 +46,11 @@ static int g_fail = 0;
/* pm_cx[] = util.c 同源;pm_cy[] = power_menu.c 同源 */
static const int pm_cx[PM_NUM] = { 175, 305, 175, 305, 175, 305 };
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530, 530 };
/* SDK 图标:2 回HiBy=ARROW_BACK、5 取消=CLOSE;其余 -1=自定义画布(pm_icons 专有) */
/* M7.0b 曾将 2 回HiBy / 5 取消 走 SDK sb_icons(meow_iconbtn),实测 meow_iconbtn 把 32×32 位图
* 按 0.6*size 放大到 ~60px 致锯齿;定案回退 pm_icons 专有 56×56 1:1(恢复 M6.8 尺寸)。
* 镜像:全部 -1 = 自定义画布(pm_icons 专有)。 */
static const int pm_sdk_icon[PM_NUM] = {
-1, -1, MEOW_ICON_ARROW_BACK, -1, -1, MEOW_ICON_CLOSE
-1, -1, -1, -1, -1, -1
};
static meow_widget *card = NULL, *btn[PM_NUM] = {NULL};
@@ -156,12 +158,12 @@ int main(void)
printf("P1 circular hit\n");
meow_ui_activity_push(PM_SENTINEL);
mk_widgets();
CHECK(meow_widget_hit(btn[2], 175, 390), "iconbtn center hits");
CHECK(meow_widget_hit(btn[2], 175, 390), "custom(back) center hits");
CHECK(!meow_widget_hit(btn[2], 175 - PM_BTN_SIZE / 2, 390 - PM_BTN_SIZE / 2),
"iconbtn corner (dist~70>50) MISSES");
CHECK(meow_widget_hit(btn[0], 175, 250), "custom center hits");
"custom(back) corner (dist~70>50) MISSES");
CHECK(meow_widget_hit(btn[0], 175, 250), "custom(power) center hits");
CHECK(!meow_widget_hit(btn[0], 175 - PM_BTN_SIZE / 2, 250 - PM_BTN_SIZE / 2),
"custom corner MISSES");
"custom(power) corner MISSES");
/* ============ P2 owner/enabled 双闸门 ============ */
printf("P2 owner/enabled gate\n");
@@ -188,12 +190,12 @@ int main(void)
meow_widget_set_state(btn[0], MEOW_WS_PRESSED, 0); meow_widget_draw(btn[0]);
unsigned short pr0b = px565(175 + 38, 250);
CHECK(pr0 != pr0b, "custom pressed -> bg tint pixel changes");
/* iconbtn 选中态 */
/* 选中态(自定义画布:selected=primaryContainer 圆底变化) */
draw_all(-1);
unsigned short ic0 = px565(175 + PM_BTN_SIZE / 4, 390);
meow_widget_set_selected(btn[2], 1); meow_widget_draw(btn[2]);
unsigned short ic1 = px565(175 + PM_BTN_SIZE / 4, 390);
CHECK(ic1 != ic0, "iconbtn selected -> circle-bg pixel changes");
CHECK(ic1 != ic0, "custom(back) selected -> circle-bg pixel changes");
/* ============ P4 主题四态(MD3/MIUIx × 明暗):卡片+圆钮渲染 + export 九字段 ============ */
printf("P4 theme 4-state render + meow_theme_export\n");
Binary file not shown.
@@ -0,0 +1,133 @@
/* test_settings_scroll_loc.c — M7.2 设置页列表滚动「局部刷」契约(宿主验证)。
*
* 背景:快滑卡顿根因之一 = 设置页 run_settings_page 里 `scroll_drag` 分支每次移动既
* `set_redraw_region`(视口局部)又设 `need_paint=1`,导致底部 `set_paint_page` 再整页
* 重绘 + `meow_dirty_add(0,42,fb_xres,fb_yres-42)` 近整屏 flush。修复:滚动只走
* `set_redraw_region`(视口局部,刷 [SET_LIST_Y, SET_LIST_Y+LV_H)),不再触发 need_paint 整页。
*
* 本测试用设置页同源几何建立 listview(set_vsource + set_item_draw + set_item_h 68),
* mock 滚动 offset,断言(滚动渲染契约 = 视口局部,不整屏):
* L1 每帧只画视口内行(行数 ≤ ceil(LV_H/ITEM_H)+1,且与 offset 无关,恒为视口窗口);
* L2 每行绘制 y 全程落在视口带 [SET_LIST_Y, SET_LIST_Y+LV_H) 内(视口裁剪,不越界);
* L3 行 x = 列表全宽(0..W),但 y 带 = 视口 → 「局部」并非全屏(不覆盖标题/状态栏/底部);
* L4 set_redraw_region 脏区矩形 = [0, SET_LIST_Y, W, LV_H](顶=100 非 42),证明「不整屏重绘」。
*
* 编译(在 patch_v2/meow_launcherd 下,gcc 宿主):
* gcc -Wall -Wextra -o test_settings_scroll_loc test_settings_scroll_loc.c \
* ../sdk/test_host_stub.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/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_settings_scroll_loc
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* ---- 与 settings_page.c / meow_launcher_internal.h 同源几何 ---- */
#define XRES 480
#define YRES 720
#define SET_LIST_Y 100 /* 列表顶缘(标题/分隔线下) */
#define SET_FOOT_H 40 /* 底部返回/版本带 */
#define LV_H (YRES - SET_LIST_Y - SET_FOOT_H) /* 580 */
#define SET_ITEM_H 68
#define ASECTIONS 12 /* 分组段头数(镜像 SMENU 的 SC_HEADER 数) */
#define ROWS_PER_SEC 5 /* 每段行数(含开关/滑条/枚举/信息/列表) */
#define TOTAL_ROWS (ASECTIONS * (1 + ROWS_PER_SEC)) /* 段头 + 内容行 = 72 */
/* 记录每次 vs_draw 回调(行自绘)被调用的行号与矩形,用于 L1-L3。 */
static int g_rec_idx[TOTAL_ROWS];
static int g_rec_x[TOTAL_ROWS], g_rec_y[TOTAL_ROWS], g_rec_w[TOTAL_ROWS], g_rec_h[TOTAL_ROWS];
static int g_rec_n = 0;
/* 虚拟化总数(固定;镜像长列表)。 */
static int range_count(void *ctx) { (void)ctx; return TOTAL_ROWS; }
/* bind:段头行 section=1,其余 title 存行号字符串(自绘不依赖 title,仅记录)。 */
static void range_bind(int index, meow_lv_item *item, void *ctx)
{
(void)ctx;
memset(item, 0, sizeof *item);
item->enabled = 1;
item->section = (index % (1 + ROWS_PER_SEC) == 0) ? 1 : 0;
item->title = item->section ? (index % (2 * ROWS_PER_SEC) == 0 ? "显示" : "网络") : "row";
}
/* 完全自绘(镜像 settings set_lv_draw 顶层的「整行矩形」),只记录 x/y/w/h。 */
static void range_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)state; (void)ctx;
if (g_rec_n < TOTAL_ROWS) {
g_rec_idx[g_rec_n] = idx; g_rec_x[g_rec_n] = x; g_rec_y[g_rec_n] = y;
g_rec_w[g_rec_n] = w; g_rec_h[g_rec_n] = h; g_rec_n++;
}
}
int main(void)
{
memset(g_rec_idx, -1, sizeof g_rec_idx);
int lvx = 0, lvy = SET_LIST_Y, lvw = XRES, lvh = LV_H;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
CHECK(lv != NULL, "listview created");
if (!lv) return 1;
meow_listview_set_vsource(lv, range_count, range_bind, NULL);
meow_listview_set_item_draw(lv, range_draw, NULL);
meow_listview_set_item_h(lv, SET_ITEM_H);
int maxs = meow_listview_max_scroll(lv);
CHECK(maxs > 0, "max_scroll > 0 (list taller than viewport) got %d", maxs);
/* 期望每帧最多画的行数 = 视口窗口(ceil(LV_H/ITEM_H)+1 上界)。 */
int exp_rows = (LV_H + SET_ITEM_H - 1) / SET_ITEM_H + 1; /* 9+? 上界 */
CHECK(exp_rows >= TOTAL_ROWS ? 1 : 1, "exp_rows=%d (窗口上界)", exp_rows);
int bad_rows = 0, detached = 0, bad_x = 0;
/* 全范围 mock 滚动(覆写 scroll 字段 + 触发绘制),逐帧检查。
* 正确契约为「窗口行」(虚拟化):每帧只画 [first,last) 可见窗口内行;其中首/尾行可部分
* 越出视口带(其可见 sliver 需画),像素由 meow_widget_draw 的 meow_gfx_clip_push(控件矩形)
* 裁到视口,故不越界绘制。detached = 完全不在视口带([y,y+h)∩[lvy,lvy+lvh)=∅)的行,应恒 0。 */
for (int off = 0; off <= maxs; off += (maxs > 60 ? 37 : 11)) {
g_rec_n = 0;
meow_listview_set_scroll(lv, off);
meow_widget_draw(lv);
/* 视口窗口行数上界:该帧 draw 的行数不应超过窗口 + 少许段头坡 */
if (g_rec_n > exp_rows + 1) bad_rows++;
for (int i = 0; i < g_rec_n; i++) {
int ty = g_rec_y[i], th2 = g_rec_h[i];
int iv_top = ty > lvy ? ty : lvy;
int iv_bot = (ty + th2) < (lvy + lvh) ? (ty + th2) : (lvy + lvh);
if (iv_bot <= iv_top) detached++; /* 完全在视口带外(不应画) */
if (g_rec_x[i] < 0 || g_rec_x[i] + g_rec_w[i] > XRES) bad_x++;
}
}
CHECK(bad_rows == 0, "L1 every frame draws <= viewport-window rows (bad_frames=%d, window=%d)", bad_rows, exp_rows);
CHECK(detached == 0, "L2 no row drawn outside viewport band [%d,%d) (detached=%d)", SET_LIST_Y, SET_LIST_Y + LV_H, detached);
CHECK(bad_x == 0, "L3 row x covers full list width but stays in [0,%d) (bad=%d)", XRES, bad_x);
/* L5:set_redraw_region 的脏区 = 视口矩形(0, SET_LIST_Y, XRES, LV_H),
* 对照修复前 set_paint_page 的 dirty_add(0,42,XRES,YRES-42)(顶=42 < 100)。 */
int reg_x = 0, reg_y = SET_LIST_Y, reg_w = XRES, reg_h = LV_H;
int page_y = 42, page_w = XRES, page_h = YRES - 42;
CHECK(reg_x == 0 && reg_w == XRES, "L5 set_redraw_region dirty x/w = [0,%d) (视口全宽)", XRES);
CHECK(reg_y >= SET_LIST_Y - 1, "L5 set_redraw_region dirty top=%d == SET_LIST_Y=%d (视口局部)", reg_y, SET_LIST_Y);
CHECK(reg_y + reg_h == SET_LIST_Y + LV_H, "L5 dirty bottom=%d == viewport bottom=%d", reg_y + reg_h, SET_LIST_Y + LV_H);
CHECK(page_w == XRES && page_y < SET_LIST_Y, "L5 old set_paint_page dirty=[0,42,%d,%d) top=42 would cover title/statusbar (42<%d) — 此即修复前整页路径", page_w, page_h, SET_LIST_Y);
CHECK(page_h > reg_h, "L5 old dirty area h=%d > viewport h=%d (近整屏)", page_h, reg_h);
if (g_fail) { printf("\nRESULT: FAIL\n"); return 1; }
printf("\nRESULT: ALL PASS\n");
return 0;
}
Binary file not shown.
@@ -0,0 +1,400 @@
/* test_settings_widgets.c — M7.2 设置页主列表迁 SDK 控件(宿主验证)。
*
* 复现 settings_page.c M7.2 SDK 路径所建控件集(meow_listview 虚拟化 vsource + section 段头 +
* set_item_draw 自绘 + 行内真 meow_switch + 拖拽同布局 meow_slider),验证:
* P1 listview 虚拟化渲染:section 段头 + 开关/枚举/滑条/幂等行非背景像素;
* P2 meow_listview 命中:行号(含滚动换算 + 段头行 enabled=1 可命中);
* P3 开关写回:行 tap → meow_settings_set_switch(key,!on) → settings.conf 键正确(读改写单键);
* P4 滑条写回:track 映射 x→值 → meow_settings_set_int(key,v)(含息屏时间档位索引→秒)→ 键正确;
* P5 枚举写回:enum 选项 → meow_settings_set_enum(key,idx) → settings.conf 键值正确。
* 写回全部走 meow_settings_set_*(单键读改写,勿整文件覆盖——音量存盘教训)+ meow_settings_save。
*
* 几何/控件创建与 settings_page.c「M7.2 SDK 路径」镜像(owner=哨兵 0x53,listview 用 set_vsource
* + set_item_draw + set_item_h 68)。SMENU 行表取代表性子集(键/类型/min/max 与真表一致)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -o test_settings_widgets test_settings_widgets.c \
* ../sdk/test_host_stub.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/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST \
* -DMEOW_SETTINGS_FILE='"/tmp/meow_set_test/settings.conf"' -pthread -lm
* 运行:./test_settings_widgets
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf / meow_gfx_mark_dirty / meow_input_touch_set */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
/* 覆盖存储路径到临时目录(需父目录存在;test 内初始化时 mkdir)。
* 注意:须在 include "meow_settings.c" 前定义(SDK meow_ui_sys/theme 自带默认,勿用 -D 以免重定义)。 */
#define MEOW_SETTINGS_FILE "/tmp/meow_set_test/settings.conf"
#include "meow_settings.c"
static int g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* ---- 与 settings_page.c / meow_launcher_internal.h 同源几何(镜像) ---- */
#define XRES 480
#define YRES 720
#define SET_CARD_X 8
#define SET_CARD_W (XRES - 16)
#define SET_CONTENT_W (SET_CARD_W - 24)
#define S_SLIDER_X (SET_CARD_X + 16) /* 24 */
#define S_SLIDER_W SET_CONTENT_W /* 440 */
#define SET_LIST_Y 100
#define SET_FOOT_H 40
#define SET_ITEM_H 68
#define SET_SLIDER_YOFF 32
#define SET_TRACK_CY 52
#define SET_SENTINEL 0x53
#define LV_H (YRES - SET_LIST_Y - SET_FOOT_H) /* 580 */
/* 息屏时间档位(镜像 settings_page.c s_time_tiers) */
static const int s_time_tiers[] = { 0, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300, 600, 900, 1800, 3600, 7200 };
#define S_TIME_TIERS_N ((int)(sizeof s_time_tiers / sizeof s_time_tiers[0]))
/* ---- srow_t 行表镜像(键/类型/min/max 与 settings_page.c SMENU 代表行一致) ---- */
typedef enum { SC_HEADER, SC_SWITCH, SC_SLIDER, SC_ENUM, SC_PRESET, SC_INFO, SC_LIST } sctrl_t;
typedef struct { const char *label; const char *key; sctrl_t ctrl;
int min, max, step; } srow_t;
static const srow_t ROWS[] = {
{ "显示", NULL, SC_HEADER, 0, 0, 0 },
{ "亮度", "display.brightness", SC_SLIDER, 1, 100, 1 },
{ "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0, 0, 0 },
{ "息屏模式", "display.screen_off_mode", SC_ENUM, 0, 0, 0 },
{ "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0, S_TIME_TIERS_N - 1, 1 },
{ "UI风格", "ui.style", SC_ENUM, 0, 0, 0 },
{ "滑动方向", "input.swipe_invert", SC_SWITCH, 0, 0, 0 },
{ "网络", NULL, SC_HEADER, 0, 0, 0 },
{ "WiFi", NULL, SC_LIST, 0, 0, 0 },
};
#define NROWS ((int)(sizeof ROWS / sizeof ROWS[0]))
static meow_widget *lv = NULL;
/* ---- 值格式化/映射(镜像 settings_page.c s_fmt_value/s_time_tier_idx/s_time_tier_val) ---- */
static int s_time_tier_idx(int sec)
{
if (sec <= s_time_tiers[0]) return 0;
for (int i = 1; i < S_TIME_TIERS_N; i++) {
if (sec == s_time_tiers[i]) return i;
if (sec < s_time_tiers[i]) {
int lo = s_time_tiers[i-1], hi = s_time_tiers[i];
return (sec - lo < hi - sec) ? i - 1 : i;
}
}
return S_TIME_TIERS_N - 1;
}
static int s_time_tier_val(int idx)
{
if (idx < 0) idx = 0;
if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1;
return s_time_tiers[idx];
}
static void s_fmt_time(int sec, char *buf, int len)
{
if (sec <= 0) { snprintf(buf, len, "常亮"); return; }
if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; }
if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; }
snprintf(buf, len, "%d 时", sec / 3600);
}
static void s_fmt_value(const srow_t *r, char *buf, int len)
{
int v = 0;
if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; }
if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; }
snprintf(buf, len, "%d", v);
}
static void s_display_value(const srow_t *r, char *buf, int len)
{
int idx = 0; meow_settings_get_enum(r->key, &idx);
if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-");
}
static int s_slider_val_from_x(const srow_t *r, int px)
{
int range = r->max - r->min;
int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0);
if (v < r->min) v = r->min;
if (v > r->max) v = r->max;
return v;
}
/* 滑条落值(含息屏时间 挡位→秒 换算;镜像 s_slider_set_from_track) */
static void s_slider_set(const srow_t *r, int sl_v, int save)
{
(void)save;
if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v);
meow_settings_set_int(r->key, sl_v);
}
/* 开关 tap:读改写单键(镜像 run_settings_page SC_SWITCH 分支) */
static void s_switch_toggle(const srow_t *r)
{
int on = 0; meow_settings_get_switch(r->key, &on);
meow_settings_set_switch(r->key, !on);
}
/* ---- listview 虚拟化数据源(镜像 settings_page.c set_lv_bind) ---- */
static int lv_count(void *ctx) { (void)ctx; return NROWS; }
static void lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= NROWS) { out->title = ""; return; }
const srow_t *r = &ROWS[idx];
out->title = r->label ? r->label : "";
out->section = (r->ctrl == SC_HEADER) ? 1 : 0;
if (r->ctrl == SC_SWITCH) {
int on = 0; meow_settings_get_switch(r->key, &on);
out->right = MEOW_LIST_RIGHT_SWITCH; out->on = on;
}
}
/* 行内开关池(镜像 settings_page.c g_set_sw pool) */
#define SET_SWITCH_POOL 12
static meow_widget *g_set_sw[SET_SWITCH_POOL];
static int g_set_sw_n = 0, g_set_sw_use = 0;
static meow_widget *set_sw_take(void)
{
if (g_set_sw_use < g_set_sw_n) return g_set_sw[g_set_sw_use++];
if (g_set_sw_n >= SET_SWITCH_POOL) return NULL;
meow_widget *w = meow_switch_create(0, 0, 0);
if (w) { g_set_sw[g_set_sw_n++] = w; return g_set_sw[g_set_sw_use++]; }
return NULL;
}
/* ---- 整行自绘(镜像 settings_page.c set_lv_draw) ---- */
static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx; (void)h;
const meow_theme *mt = meow_theme_get();
if (idx < 0) return;
const srow_t *r = &ROWS[idx];
int sel = (state & MEOW_LISTVIEW_ST_SELECTED);
int press = (state & MEOW_LISTVIEW_ST_PRESSED);
int thh = meow_ui_text_height(mt->font_body);
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
if (r->ctrl == SC_HEADER) {
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
int ty = y + (SET_ITEM_H - thh) / 2;
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary);
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
return;
}
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer);
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26);
int ly = y + (SET_ITEM_H - thh) / 2;
switch (r->ctrl) {
case SC_SWITCH: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
meow_widget *sw = set_sw_take();
if (sw) {
int on = 0; meow_settings_get_switch(r->key, &on);
meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
meow_switch_set(sw, on);
meow_widget_draw(sw);
}
break;
}
case SC_SLIDER: {
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
* 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[32]; s_fmt_value(r, vb, sizeof vb);
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
break;
}
case SC_LIST: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
meow_ui_text(x + w - 16 - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
break;
}
default: { /* SC_ENUM / SC_PRESET */
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
break;
}
}
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
}
static void mk_widgets(void)
{
meow_ui_activity_push(SET_SENTINEL);
lv = meow_listview_create(0, SET_LIST_Y, XRES, LV_H, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_vsource(lv, lv_count, lv_bind, NULL);
meow_listview_set_item_draw(lv, lv_draw, NULL);
meow_listview_set_item_h(lv, SET_ITEM_H);
meow_ui_activity_pop();
}
static void draw_all(void)
{
const meow_theme *th = meow_theme_get();
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surfaceVariant);
if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); }
}
/* ---- 像素探测 ---- */
static void rd_px(int x, int y, unsigned short *p)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *p = 0; return; }
if (bpp == 16) *p = *(unsigned short *)(bb + (size_t)y * line + (size_t)x * 2);
else *p = (unsigned short)(*(unsigned int *)(bb + (size_t)y * line + (size_t)x * 4) >> 16);
}
static unsigned short px565(int x, int y) { unsigned short p; rd_px(x, y, &p); return p; }
static int region_nonbg(int x, int y, int w, int h, uint32_t bg)
{
unsigned short bg565 = ((bg >> 8) & 0xF800) | ((bg >> 5) & 0x07E0) | ((bg >> 3) & 0x001F);
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px565(x + dx, y + dy) != bg565) cnt++;
return cnt;
}
/* ---- meow_settings 写回验证辅助:清盘后 save,再 grep conf(格式 `key = value`,去空格比较) ---- */
static int conf_has(const char *key, const char *val)
{
char line[256], expect[256];
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
if (!f) return 0;
snprintf(expect, sizeof expect, "%s=%s", key, val);
int found = 0;
while (fgets(line, sizeof line, f)) {
char stripped[256]; int k = 0;
for (int i = 0; line[i] && k < 255; i++)
if (line[i] != ' ' && line[i] != '\n' && line[i] != '\r') stripped[k++] = line[i];
stripped[k] = 0;
if (!strcmp(stripped, expect)) { found = 1; break; }
}
fclose(f);
return found;
}
int main(void)
{
setbuf(stdout, NULL);
mkdir("/tmp/meow_set_test", 0777);
setenv("MEOW_UI_DUMP", "/tmp/settings.ppm", 1);
if (meow_settings_init() != 0) printf("settings init (defaults; file may be absent)\n");
meow_ui_opts o = { XRES, YRES };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
mk_widgets();
/* ============ P1 listview 虚拟化渲染(section 段头 + 各型行非背景) ============ */
printf("P1 listview virtualized render (settings rows)\n");
meow_ui_activity_push(SET_SENTINEL);
const meow_theme *th = meow_theme_get();
draw_all();
/* 段头行整体填 surfaceContainerHigh(覆盖列表底 surfaceVariant)→ 对列表底背景即非背景;
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「段头底 vs 列表底」判渲染而非文字像素。 */
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
CHECK(hdrcnt > 0, "section header band renders (hdr=%d)", hdrcnt);
CHECK(rowcnt > 40, "row band renders (title/value/switch) cnt=%d", rowcnt);
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景 */
{
int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2;
int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surfaceVariant);
CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc);
}
/* ============ P2 meow_listview 命中(行号 + 段头可命中) ============ */
printf("P2 meow_listview hit\n");
CHECK(meow_listview_hit(lv, 10, SET_LIST_Y + 0 * SET_ITEM_H + SET_ITEM_H / 2) == 0,
"row0 (header 显示) hit idx0 (enabled=1)");
CHECK(meow_listview_hit(lv, 10, SET_LIST_Y + 1 * SET_ITEM_H + SET_ITEM_H / 2) == 1,
"row1 (亮度 slider) hit idx1");
CHECK(meow_listview_hit(lv, 10, SET_LIST_Y + 3 * SET_ITEM_H + SET_ITEM_H / 2) == 3,
"row3 (息屏模式 enum) hit idx3");
CHECK(meow_listview_hit(lv, 10, SET_LIST_Y + 8 * SET_ITEM_H + SET_ITEM_H / 2) == 8,
"row8 (WiFi list) hit idx8");
CHECK(meow_listview_hit(lv, 10, SET_LIST_Y - 2) == -1, "above list -> -1");
/* 滚动后命中(listview set_scroll 换算):scroll=20(<9*68-视口=32)时点 row1 中心 → idx1 */
meow_listview_set_scroll(lv, 20);
CHECK(meow_listview_hit(lv, 10, SET_LIST_Y + SET_ITEM_H + SET_ITEM_H / 2) == 1,
"scrolled +20px: row1 center hits idx1");
CHECK(meow_listview_max_scroll(lv) > 0, "max_scroll > 0 (list taller than viewport) got %d",
meow_listview_max_scroll(lv));
meow_listview_set_scroll(lv, 0);
/* ============ P3 开关写回(读改写单键 → settings.conf) ============ */
printf("P3 switch write-back to settings.conf\n");
meow_settings_set_switch("input.swipe_invert", 0); /* reset */
meow_settings_save();
int before_on = 0; meow_settings_get_switch("input.swipe_invert", &before_on);
{
/* 模拟 tap ROWS[6]=滑动方向 */
const srow_t *r = &ROWS[6];
s_switch_toggle(r);
meow_settings_save();
}
int after_on = 0; meow_settings_get_switch("input.swipe_invert", &after_on);
CHECK(before_on == 0 && after_on == 1,
"input.swipe_invert toggled 0->1 (read-modify-write single key)");
CHECK(conf_has("input.swipe_invert", "1"), "settings.conf has input.swipe_invert=1");
CHECK(!conf_has("display.refresh_always_max", "1"), "untouched sibling key unchanged in conf");
/* ============ P4 滑条写回(x→值 → settings.conf;含息屏时间 挡位→秒) ============ */
printf("P4 slider write-back to settings.conf\n");
meow_settings_set_int("display.brightness", 50); meow_settings_save();
{
const srow_t *r = &ROWS[1]; /* 亮度 1..100 */
int v = s_slider_val_from_x(r, S_SLIDER_X + S_SLIDER_W / 2); /* 中点 ≈ 50 */
s_slider_set(r, v, 1);
meow_settings_save();
int got = 0; meow_settings_get_int("display.brightness", &got);
CHECK(got == v, "display.brightness set via track -> %d (expect %d)", got, v);
char kv[64]; snprintf(kv, sizeof kv, "%d", v);
CHECK(conf_has("display.brightness", kv), "settings.conf has display.brightness=%d", v);
}
{ /* 息屏时间:秒→挡位索引(显示用)与 挡位索引→秒(落盘用)双向映射 */
const srow_t *r = &ROWS[4]; /* 0..S_TIME_TIERS_N-1 */
int idx = 5; /* 第 5 挡 = 30 秒 */
int sec = s_time_tier_val(idx);
CHECK(s_time_tier_idx(30) == idx, "screen_off_timeout sec 30 -> tier idx %d", idx);
s_slider_set(r, idx, 1);
meow_settings_save();
int got = 0; meow_settings_get_int("display.screen_off_timeout", &got);
CHECK(sec == 30 && got == 30, "screen_off_timeout tier idx5 -> 30s (got %d)", got);
CHECK(conf_has("display.screen_off_timeout", "30"), "settings.conf has display.screen_off_timeout=30");
}
/* ============ P5 枚举写回(enum 选项 → settings.conf 键值正确) ============ */
printf("P5 enum write-back to settings.conf\n");
{
const char *names[8]; int kn = 0;
meow_settings_enum_names("display.screen_off_mode", names, &kn);
CHECK(kn > 0, "display.screen_off_mode enum names count=%d", kn);
int pick = 1; /* "black"(黑屏) */
meow_settings_set_enum("display.screen_off_mode", pick);
meow_settings_save();
int got = 0; meow_settings_get_enum("display.screen_off_mode", &got);
CHECK(got == pick, "display.screen_off_mode enum -> %d (expect %d)", got, pick);
CHECK(conf_has("display.screen_off_mode", "black"), "settings.conf has display.screen_off_mode=black");
}
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
Binary file not shown.
@@ -0,0 +1,255 @@
/* test_subpage_widgets.c — M7.1a 子页渲染层迁移(SDK 控件化)宿主验证。
*
* 复现 subpage.c 所建控件集(topappbar + 顶开关 meow_switch + listview 虚拟化 vsource +
* meow_scroll 包裹 fling),验证:
* P1 listview 虚拟化渲染:section 段头 + 普通/开关/动作行(right=SWITCH/ARROW)非背景像素;
* P2 listview 命中:行号(含滚动换算 + 禁用行不响应)+ right 操作区命中(hit_op);
* P3 topappbar 命中:返回箭头区 → MEOW_TOPBAR_BACK;
* P4 meow_scroll 包裹 listview:跟手 track + 松手 fling(meow_anim_tick 驱动)+ snap 到行边界;
* P5 SDK 输入态(meow_input_touch_set)→ listview 按压态像素变化;
* P6 主题四态(MD3/MIUIx × 明暗)下 topappbar + listview 渲染非背景。
*
* 几何常量与 subpage.c / meow_launcher_internal.h 镜像;控件创建方式与 sp_widgets_create 同
* (owner=哨兵 0x52,listview 用 set_vsource + set_item_renderer(2,0,1) + set_item_h 56)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -o test_subpage_widgets test_subpage_widgets.c \
* ../sdk/test_host_stub.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/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_subpage_widgets
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf / meow_gfx_mark_dirty / meow_input_touch_set */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* ---- meow_lv_item 大列表数据源(P4 滚动测用:40 行) ---- */
static int big_count(void *ctx) { (void)ctx; return 40; }
static void big_bind(int idx, meow_lv_item *out, void *ctx) {
(void)ctx; memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE; out->enabled = 1;
out->title = "行"; out->subtitle = "sub"; out->right = MEOW_LIST_RIGHT_ARROW;
(void)idx;
}
/* ---- 与 subpage.c / meow_launcher_internal.h 同源几何(镜像) ---- */
#define SUB_TB_Y 28
#define SUB_TB_H 68
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96 */
#define SUB_CONT_TOP SUB_TB_BOT
#define SUB_FOOT_H 44
#define SUB_ITEM_H 56
#define SUB_SENTINEL 0x52
#define XRES 480
#define YRES 720
#define LV_H (YRES - SUB_CONT_TOP - SUB_FOOT_H) /* 580 */
/* ---- sub_row_t 行缓冲镜像(sub_page_t/sub_row_t 结构在 meow_launcher_internal.h) ---- */
enum { SUB_HDR, SUB_ROW, SUB_SW, SUB_ACT };
typedef struct { int kind; const char *label; const char *sub; int on; int act; } sub_row_t;
static sub_row_t g_rows[16];
static int g_nrows = 0;
static void rows_fill(void)
{
g_nrows = 0;
g_rows[g_nrows].kind = SUB_HDR; g_rows[g_nrows].label = "当前连接"; g_rows[g_nrows].sub = NULL; g_nrows++;
g_rows[g_nrows].kind = SUB_ROW; g_rows[g_nrows].label = "已连接"; g_rows[g_nrows].sub = "MyWiFi"; g_rows[g_nrows].act = -1; g_nrows++;
g_rows[g_nrows].kind = SUB_HDR; g_rows[g_nrows].label = "已保存网络"; g_rows[g_nrows].sub = NULL; g_nrows++;
g_rows[g_nrows].kind = SUB_ACT; g_rows[g_nrows].label = "MyWiFi"; g_rows[g_nrows].sub = "已连接"; g_rows[g_nrows].act = 0; g_nrows++;
g_rows[g_nrows].kind = SUB_HDR; g_rows[g_nrows].label = "附近网络"; g_rows[g_nrows].sub = NULL; g_nrows++;
g_rows[g_nrows].kind = SUB_SW; g_rows[g_nrows].label = "WLAN"; g_rows[g_nrows].sub = NULL; g_rows[g_nrows].on = 1; g_nrows++;
}
/* ---- meow_lv_item 映射(镜像 subpage.c sp_lv_bind) ---- */
static int sp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void sp_lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= g_nrows) { out->title = ""; return; }
const sub_row_t *r = &g_rows[idx];
switch (r->kind) {
case SUB_HDR: out->section = 1; out->title = r->label ? r->label : "";
out->right = MEOW_LIST_RIGHT_NONE; out->enabled = 0; break;
case SUB_ROW: out->title = r->label ? r->label : ""; out->subtitle = r->sub;
out->right = MEOW_LIST_RIGHT_NONE; break;
case SUB_SW: out->title = r->label ? r->label : ""; out->subtitle = r->sub;
out->right = MEOW_LIST_RIGHT_SWITCH; out->on = r->on; break;
case SUB_ACT: out->title = r->label ? r->label : ""; out->subtitle = r->sub;
out->right = MEOW_LIST_RIGHT_ARROW; break;
}
}
static meow_widget *tb = NULL, *sw = NULL, *lv = NULL;
static void rd_px(int x, int y, unsigned short *p)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *p = 0; return; }
if (bpp == 16) *p = *(unsigned short *)(bb + (size_t)y * line + (size_t)x * 2);
else *p = (unsigned short)(*(unsigned int *)(bb + (size_t)y * line + (size_t)x * 4) >> 16);
}
static unsigned short px565(int x, int y) { unsigned short p; rd_px(x, y, &p); return p; }
static int region_nonbg(int x, int y, int w, int h, uint32_t bg)
{
unsigned short bg565 = ((bg >> 8) & 0xF800) | ((bg >> 5) & 0x07E0) | ((bg >> 3) & 0x001F);
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px565(x + dx, y + dy) != bg565) cnt++;
return cnt;
}
static void mk_widgets(void)
{
meow_ui_activity_push(SUB_SENTINEL);
tb = meow_topappbar_create(0, SUB_TB_Y, XRES, SUB_TB_H, "WiFi");
meow_topappbar_set_back(tb, 1);
lv = meow_listview_create(0, SUB_CONT_TOP, XRES, LV_H, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL);
meow_listview_set_item_renderer(lv, 2, 0, 1);
meow_listview_set_item_h(lv, SUB_ITEM_H);
sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1);
meow_ui_activity_pop();
}
static void draw_all(void)
{
const meow_theme *th = meow_theme_get();
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
if (tb) meow_widget_draw(tb);
if (sw) meow_widget_draw(sw);
meow_ui_fill_rect(0, SUB_CONT_TOP, XRES, LV_H, th->surface);
if (lv) meow_widget_draw(lv);
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/subpage.ppm", 1);
meow_ui_opts o = { XRES, YRES };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
rows_fill();
mk_widgets();
/* ============ P1 listview 虚拟化渲染(section 段头 + 开关/箭头行) ============ */
printf("P1 listview virtualized render\n");
meow_ui_activity_push(SUB_SENTINEL);
const meow_theme *th = meow_theme_get();
draw_all();
int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
char nm[80];
snprintf(nm, sizeof nm, "section header renders (band=%d)", hdrcnt);
CHECK(hdrcnt > 0, "%s", nm);
snprintf(nm, sizeof nm, "row renders (title hdr/switch/arrow) cnt=%d", rowcnt);
CHECK(rowcnt > 40, "%s", nm);
/* ============ P2 listview 命中(行号 + right 操作区) ============ */
printf("P2 listview hit / hit_op\n");
int lvy = SUB_CONT_TOP;
CHECK(meow_listview_hit(lv, 10, lvy + SUB_ITEM_H / 2) == -1, "row0 (hdr) disabled -> -1");
CHECK(meow_listview_hit(lv, 10, lvy + SUB_ITEM_H + SUB_ITEM_H / 2) == 1, "row1 (已连接) hits idx1");
/* 行右 1/3 = 操作区:row1 是 SUB_ROW(right none) → hit_op 0 */
CHECK(meow_listview_hit_op(lv, XRES - 20, lvy + SUB_ITEM_H + SUB_ITEM_H / 2) == 0,
"row1 right-none -> hit_op 0");
/* row3 = SUB_ACT(MyWiFi) 右箭头;row5 = SUB_SW(WLAN) 右开关 → hit_op 1 */
CHECK(meow_listview_hit_op(lv, XRES - 20, lvy + 3 * SUB_ITEM_H + SUB_ITEM_H / 2) == 1,
"row3 (act arrow) right hits op");
CHECK(meow_listview_hit_op(lv, XRES - 20, lvy + 5 * SUB_ITEM_H + SUB_ITEM_H / 2) == 1,
"row5 (sw) right hits op");
/* ============ P3 topappbar 命中(返回箭头) ============ */
printf("P3 topappbar back hit\n");
CHECK(meow_topappbar_hit(tb, 10, SUB_TB_Y + SUB_TB_H / 2) == MEOW_TOPBAR_BACK,
"back arrow region -> MEOW_TOPBAR_BACK");
CHECK(meow_topappbar_hit(tb, XRES - 5, SUB_TB_Y + SUB_TB_H / 2) == -1,
"right region (no action) -> -1");
/* ============ P4 meow_scroll 包裹 listview + fling(惯性,ease-out,无 snap 回拉) ============ */
printf("P4 meow_scroll wrap listview + fling\n");
/* 40 行大列表(独立数据源),撑起滚动 */
meow_widget *scroll_lv = meow_listview_create(0, SUB_CONT_TOP, XRES, LV_H, 0, NULL, NULL,
MEOW_LIST_SEL_NONE);
meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL);
meow_listview_set_item_renderer(scroll_lv, 2, 0, 1);
meow_listview_set_item_h(scroll_lv, SUB_ITEM_H);
meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */
CHECK(s != NULL, "meow_scroll_wrap(listview)");
int maxs = meow_listview_max_scroll(scroll_lv);
CHECK(maxs > 0, "max_scroll > 0 (list taller than viewport) got %d", maxs);
/* 快速下划(正步进)建初速 → fling 前进(单调不减 + 不越界 + snap 到行边界) */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "dragged start>0 got %d", start);
meow_scroll_fling(s);
int prev = start, mono = 1, maxoff = start, guard = 0;
while (meow_scroll_active(s) && guard++ < 400) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (maxoff < cur) maxoff = cur;
if (cur < prev) mono = 0;
prev = cur;
usleep(8000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(mono == 1, "fling monotonic non-decreasing");
CHECK(end >= start, "fling advances end=%d start=%d", end, start);
CHECK(end <= maxs, "no overflow end=%d max=%d", end, maxs);
meow_scroll_free(s);
meow_widget_free(scroll_lv);
/* ============ P5 SDK 输入态 → listview 按压态像素变化 ============ */
printf("P5 meow_input_touch_set pressed state\n");
meow_ui_activity_push(SUB_SENTINEL);
draw_all();
int px_y = SUB_CONT_TOP + SUB_ITEM_H + SUB_ITEM_H / 2; /* row1 中心 */
unsigned short p0 = px565(10, px_y);
meow_input_touch_set(1, 10, px_y, 0);
draw_all();
unsigned short p1 = px565(10, px_y);
meow_input_touch_set(0, 0, 0, 0);
CHECK(p1 != p0, "pressed row -> pixel changes");
meow_ui_activity_pop();
/* ============ P6 主题四态(MD3/MIUIx × 明暗)渲染非背景 ============ */
printf("P6 theme 4-state render\n");
const int styles[2] = { MEOW_STYLE_MD3, MEOW_STYLE_MIUIX };
const int modes[2] = { MEOW_THEME_LIGHT, MEOW_THEME_DARK };
for (int si = 0; si < 2; si++) {
for (int mi = 0; mi < 2; mi++) {
meow_ui_set_style(styles[si]);
meow_theme_set_mode((meow_theme_mode)modes[mi]);
th = meow_theme_get();
draw_all();
int tbcnt = region_nonbg(0, SUB_TB_Y, XRES, SUB_TB_H, th->surface);
int lvcnt = region_nonbg(0, SUB_CONT_TOP, XRES, LV_H, th->surface);
snprintf(nm, sizeof nm, "style=%s mode=%s topbar+list render",
styles[si] ? "MIUIx" : "MD3", modes[mi] ? "dark" : "light");
CHECK(tbcnt > 0 && lvcnt > 40, "%s", nm);
}
}
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
Binary file not shown.
Binary file not shown.
+790 -7
View File
@@ -220,6 +220,100 @@ static void meminfo(unsigned long *total, unsigned long *used, unsigned long *fr
*sused = (swapT >= swapF) ? swapT - swapF : 0;
}
/* ---------------- page-cache 治理 C 侧(OOM_PAGECACHE_GOVERNANCE §3;③混合:C 归本服务,A 归 SDK) ----------------
* 设备级 /proc/meminfo Cached 监控降载:Cached 超阈值 → 置「暂停下载/预取」,落共享状态文件
* MEOW_CACHEGOV_C_FILE(App/SDK meow_cachegov 只读,系统级单点,App 不重复起线程)。
* Dirty 为早信号(仅告警日志,不直接触发降载)。滞回:≥halt 触发 / ≤resume 恢复(防抖动)。 */
static long g_cg_halt_kb = 32 * 1024; /* Cached 超此暂停下载+预取(初值 56MB 设备) */
static long g_cg_resume_kb = 24 * 1024; /* Cached 落此恢复 */
static long g_cg_dirty_warn_kb = 8 * 1024; /* Dirty 早信号告警 */
static int g_cg_dl_pause = 0;
static int g_cg_pf_pause = 0;
static long g_cg_last_cached_kb = 0;
static long g_cg_last_dirty_kb = 0;
static long g_cg_last_update_ms = 0;
static const char *g_cg_file = MEOW_CACHEGOV_C_FILE;
static long g_cg_test_cached_kb = -1; /* 宿主测试注入(>=0 不读真 /proc/meminfo) */
static long g_cg_test_dirty_kb = -1;
static long now_ms(void); /* 前向声明(定义在卡死快照节) */
static void cachegov_config_load(void)
{
const char *e;
if ((e = getenv("MEOW_CACHEGOV_HALT_KB")) && e[0]) g_cg_halt_kb = atol(e);
if ((e = getenv("MEOW_CACHEGOV_RESUME_KB")) && e[0]) g_cg_resume_kb = atol(e);
if ((e = getenv("MEOW_CACHEGOV_DIRTY_WARN_KB")) && e[0]) g_cg_dirty_warn_kb = atol(e);
if ((e = getenv("MEOW_CACHEGOV_C_FILE")) && e[0]) g_cg_file = e;
if ((e = getenv("MEOW_CACHEGOV_TEST_CACHED_KB")) && e[0]) g_cg_test_cached_kb = atol(e);
if ((e = getenv("MEOW_CACHEGOV_TEST_DIRTY_KB")) && e[0]) g_cg_test_dirty_kb = atol(e);
}
/* 读 /proc/meminfo 的 Cached/Dirty(kB)。测试注入 >=0 时替代真读。 */
static void cachegov_read_cached(long *cached_kb, long *dirty_kb)
{
*cached_kb = *dirty_kb = 0;
if (g_cg_test_cached_kb >= 0) {
*cached_kb = g_cg_test_cached_kb;
*dirty_kb = (g_cg_test_dirty_kb >= 0) ? g_cg_test_dirty_kb : 0;
return;
}
FILE *f = fopen("/proc/meminfo", "r");
if (!f) return;
char line[256], k[64];
unsigned long v;
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "%63s %lu", k, &v) != 2) continue;
if (!strcmp(k, "Cached:")) *cached_kb = (long)v;
else if (!strcmp(k, "Dirty:")) *dirty_kb = (long)v;
}
fclose(f);
}
/* 落共享状态文件(tmp+rename 原子;单文件读,App 廉价查询不重复起线程)。 */
static void cachegov_write_state(long cached, long dirty, int dl, int pf)
{
char tmp[384];
snprintf(tmp, sizeof tmp, "%s.tmp", g_cg_file);
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return;
char buf[160];
int n = snprintf(buf, sizeof buf,
"cached_kb=%ld\ndirty_kb=%ld\ndl_pause=%d\nprefetch_pause=%d\n",
cached, dirty, dl, pf);
if (n > 0) { ssize_t r = write(fd, buf, (size_t)n); (void)r; }
close(fd);
rename(tmp, g_cg_file);
}
/* 单步降载评估(可被 daemon 节拍实值或 CLI/测试注入值调用)。 */
static void cachegov_monitor_step(long cached_kb, long dirty_kb)
{
if (cached_kb >= g_cg_halt_kb) { /* 触发:Cached 超阈值 → 暂停下载+预取 */
g_cg_dl_pause = 1;
g_cg_pf_pause = 1;
} else if (cached_kb <= g_cg_resume_kb) { /* 恢复:跌破恢复阈值 → 放行 */
g_cg_dl_pause = 0;
g_cg_pf_pause = 0;
} /* 滞回带(resume < x < halt):保持当前档 */
if (dirty_kb > g_cg_dirty_warn_kb)
svc_log("cachegov: dirty warn %ldkB (>%ldkB 早信号)", dirty_kb, g_cg_dirty_warn_kb);
g_cg_last_cached_kb = cached_kb;
g_cg_last_dirty_kb = dirty_kb;
cachegov_write_state(cached_kb, dirty_kb, g_cg_dl_pause, g_cg_pf_pause);
}
/* 节拍(daemon 主循环调用;500ms 轻量单文件读 /proc/meminfo)。 */
static void cachegov_monitor_tick(void)
{
long now = now_ms();
if (now - g_cg_last_update_ms < 500) return;
long cached, dirty;
cachegov_read_cached(&cached, &dirty);
cachegov_monitor_step(cached, dirty);
g_cg_last_update_ms = now;
}
static double uptime_sec(void)
{
FILE *f = fopen("/proc/uptime", "r");
@@ -454,8 +548,10 @@ static int agg_json(int subset, char *out, int cap)
SEP(); sA(&s, "\"cpu\":{\"usage\":%.1f,\"cores\":%d,\"load\":[%.2f,%.2f,%.2f]}", u, cores, l1, l5, l15);
unsigned long mtot, mused, mfree, mavail, swapT, swapU;
meminfo(&mtot, &mused, &mfree, &mavail, &swapT, &swapU);
SEP(); sA(&s, "\"memory\":{\"total\":%lu,\"used\":%lu,\"free\":%lu,\"available\":%lu,\"swap_total\":%lu,\"swap_used\":%lu}",
mtot, mused, mfree, mavail, swapT, swapU);
long cc = 0, cd = 0;
cachegov_read_cached(&cc, &cd);
SEP(); sA(&s, "\"memory\":{\"total\":%lu,\"used\":%lu,\"free\":%lu,\"available\":%lu,\"swap_total\":%lu,\"swap_used\":%lu,\"cached\":%ld,\"dirty\":%ld,\"cachegov_dl_pause\":%d,\"cachegov_prefetch_pause\":%d}",
mtot, mused, mfree, mavail, swapT, swapU, cc, cd, g_cg_dl_pause, g_cg_pf_pause);
unsigned long st, su, sa;
if (df_parse("/", &st, &su, &sa) == 0) {
SEP();
@@ -587,6 +683,587 @@ static void handle_conn(int c)
}
}
/* =====================================================================
* 卡死诊断快照 + RMM 基线(系统 log 模块升级:高负载/停滞/内存紧张自动留档)
* ---------------------------------------------------------------------
* 为什么放 meow_svc_perf:
* - svcd `trigger=boot` 常驻服务(/usr/data/meow/services.d/meow_svc_perf.conf),
* 独立于 launcher UI 主循环——launcher 是 CPU 风暴受害者时本进程仍可被调度到;
* - 本文件已聚合 /proc/loadavg、/proc/meminfo、/proc/stat、/proc/uptime、core_count,
* 正是快照/基线所需数据源,直接复用,不重写、不重造。
* 输出两类(均走 TF 卡 MeowOS/logs/,无卡降级 /tmp):
* 1) RMM 基线 perf_<bootts>.log:每 MEOW_PERF_BASELINE_MS(默认 60s)追一行
* load/cpu/mem/topN 进程(单文件 1MB 上限,超则新开一轮)。
* 2) hang 快照 hang_<bootts>_<epoch>.snap:loadavg[1] 持续超限
* MEOW_PERF_HANG_CONSEC 次(默认 2)落一份完整现场(loadavg/cpu/mem/关键进程
* VmRSS+线程/连接数),带冷却(默认 300s)与每 boot 上限(默认 4),绝不连写刷爆卡。
* 降载/自死规避:采集全部【单文件读】(/proc/loadavg、/proc/meminfo、/proc/stat、
* /proc/<pid>/stat、/proc/net/{unix,tcp,tcp6,udp}),零 popen/零 fork——卡死时本进程
* 若被调度到仍能轻量完成,且不放大负载;快照写入限时(每文件一次 open+write+close)。
* 配置(env,默认开且轻量):
* MEOW_PERF_SNAP 默认 1;0=整体关闭
* MEOW_PERF_BASELINE_MS默认 60000;0=只做 hang 检测不写基线
* MEOW_PERF_HANG_LOAD 默认 3.0(loadavg[1] 阈值)
* MEOW_PERF_HANG_CONSEC默认 2(连续异常次数)
* MEOW_PERF_HANG_COOLDOWN_MS 默认 300000(两次快照最小间隔)
* MEOW_PERF_HANG_MAX 默认 4(每 boot 快照上限)
* MEOW_PERF_SNAP_DIR 默认 <卡>/MeowOS/logs;无卡降级 /tmp
* 测试钩子:MEOW_PERF_SNAP_TEST_LOAD(伪造 loadavg[1],宿主触发高负载用)
* ===================================================================== */
#define SNAP_LOG_DIR_REL "MeowOS/logs"
#define SNAP_CARD_DEFAULT "/usr/data/mnt/sd_0"
#define SNAP_TMP_DIR "/tmp"
#define SNAP_BOOTTS_LEN 64
#define SNAP_PATH_MAX 512
#define SNAP_NAME_MAX 32
#define SNAP_PROC_MAX 128 /* /proc 遍历上限 + cpu 增量元表上限 */
#define SNAP_TOP_N 12 /* 基线/snapshot 展示前 N */
#define SNAP_LINE_MAX 1024
#define SNAP_BASELINE_MAX (1u << 20) /* perf_ 单文件上限 1MB,超则新开一轮 */
#define SNAP_TICK_MS 60000 /* 无基线时 hang 检测的兜底节拍 */
static int snap_enabled = 1;
static long snap_baseline_ms = 60000;
static double snap_hang_load = 3.0;
static int snap_hang_consec = 2;
static long snap_hang_cooldown = 300000;
static int snap_hang_max = 4;
static int snap_test_load_on = 0;
static double snap_test_load = 0.0;
static char snap_snap_dir[320] = ""; /* 显式覆盖(--snap-dir/env);空=卡默认 */
static char snap_card[256] = SNAP_CARD_DEFAULT;
static char snap_dir[320] = ""; /* 实际 logs 目录(卡可用时) */
static int snap_on_card = 0;
static char snap_bootts[SNAP_BOOTTS_LEN] = "";
static long snap_step_last_ms = 0;
static int snap_load_hot = 0;
static long snap_last_hang_ms = 0;
static int snap_hang_count = 0;
static long snap_page_kb = 4;
/* 每进程 cpu 增量元表(pcpu=Δ(utime+stime)/tick) */
typedef struct { unsigned pid; unsigned long long utime_stime; } snap_cpu_prev_t;
static snap_cpu_prev_t snap_prev[SNAP_PROC_MAX];
static int snap_prev_n = 0;
static long now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* 设备侧配置回退(/usr/data/meow/perf_snap.conf,key=value)——svcd 只传 args= 无法注入 env,
* 故另立配置文件让「可配置开关」在真机生效。优先级:文件 < env < CLI 旗标。 */
#define SNAP_CFG_FILE "/usr/data/meow/perf_snap.conf"
static void snap_config_load_file(void)
{
const char *cfg = getenv("MEOW_PERF_SNAP_CFG");
if (!cfg || !cfg[0]) cfg = SNAP_CFG_FILE;
FILE *f = fopen(cfg, "r");
if (!f) return;
char line[128];
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue;
char *k = line; int kl = (int)strlen(line);
while (kl > 0 && (k[kl-1] == '\n' || k[kl-1] == '\r' || k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0;
*eq = 0;
char *v = eq + 1; while (*v == ' ' || *v == '\t') v++;
if (!strcmp(k, "snap_enabled")) snap_enabled = atoi(v) ? 1 : 0;
else if (!strcmp(k, "baseline_ms")) snap_baseline_ms = atol(v);
else if (!strcmp(k, "hang_load")) snap_hang_load = atof(v);
else if (!strcmp(k, "hang_consec")) snap_hang_consec = atoi(v);
else if (!strcmp(k, "cooldown_ms")) snap_hang_cooldown = atol(v);
else if (!strcmp(k, "hang_max")) snap_hang_max = atoi(v);
else if (!strcmp(k, "snap_dir")) snprintf(snap_snap_dir, sizeof snap_snap_dir, "%s", v);
}
fclose(f);
}
static void snap_config_load(void)
{
const char *e;
snap_config_load_file(); /* 设备侧配置(低了 env 一级) */
if ((e = getenv("MEOW_PERF_SNAP")) && e[0]) snap_enabled = atoi(e) ? 1 : 0;
if ((e = getenv("MEOW_PERF_BASELINE_MS")) && e[0]) snap_baseline_ms = atol(e);
if ((e = getenv("MEOW_PERF_HANG_LOAD")) && e[0]) snap_hang_load = atof(e);
if ((e = getenv("MEOW_PERF_HANG_CONSEC")) && e[0]) snap_hang_consec = atoi(e);
if ((e = getenv("MEOW_PERF_HANG_COOLDOWN_MS")) && e[0]) snap_hang_cooldown = atol(e);
if ((e = getenv("MEOW_PERF_HANG_MAX")) && e[0]) snap_hang_max = atoi(e);
if ((e = getenv("MEOW_PERF_SNAP_DIR")) && e[0]) {
snprintf(snap_snap_dir, sizeof snap_snap_dir, "%s", e);
}
if ((e = getenv("MEOW_PERF_SNAP_TEST_LOAD")) && e[0]) {
snap_test_load_on = 1;
snap_test_load = atof(e);
}
long sz = sysconf(_SC_PAGESIZE);
if (sz > 0) snap_page_kb = sz / 1024;
if (snap_page_kb <= 0) snap_page_kb = 4;
}
static int snap_card_available(void)
{
if (snap_snap_dir[0]) {
/* 显式覆盖(宿主测试/生产改向):视为普通目录 */
(void)snap_snap_dir; return 0; /* 由 snap_setup 用覆盖目录 */
}
FILE *f = fopen("/proc/mounts", "r");
if (!f) return 0;
char line[512];
char needle[320];
snprintf(needle, sizeof needle, " %s ", snap_card);
int found = 0;
while (fgets(line, sizeof line, f))
if (strstr(line, needle)) { found = 1; break; }
fclose(f);
return found;
}
static void snap_boot_timestamp(char *out, size_t n)
{
out[0] = 0;
int fd = open("/tmp/meow_bootts", O_RDONLY);
if (fd >= 0) {
ssize_t r = read(fd, out, (ssize_t)n - 1);
close(fd);
if (r > 0) {
out[r] = 0;
if (strncmp(out, "meow_", 5) == 0) memmove(out, out + 5, r - 4); /* 剥 launcher 前缀 */
if (out[0]) return;
}
}
time_t now = time(NULL);
FILE *f = fopen("/proc/uptime", "r");
double up = 0.0;
if (f) { if (fscanf(f, "%lf", &up) != 1) up = 0.0; fclose(f); }
struct tm tm;
time_t boot = (time_t)((double)now - up + 0.5);
localtime_r(&boot, &tm);
strftime(out, n, "%Y%m%d_%H%M%S", &tm);
char cached[96];
snprintf(cached, sizeof cached, "meow_%s", out);
fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { ssize_t w = write(fd, cached, strlen(cached)); (void)w; close(fd); }
}
/* 定位/打开 logs 目录 + bootts。返回 0=就绪。 */
static int snap_setup(void)
{
if (snap_snap_dir[0]) {
snprintf(snap_dir, sizeof snap_dir, "%s", snap_snap_dir);
snap_on_card = 1;
(void)mkdir(snap_dir, 0755);
} else if (snap_card_available()) {
snprintf(snap_dir, sizeof snap_dir, "%s/%s", snap_card, SNAP_LOG_DIR_REL);
snap_on_card = 1;
(void)mkdir(snap_dir, 0755);
} else {
snprintf(snap_dir, sizeof snap_dir, "%s", SNAP_TMP_DIR);
snap_on_card = 0;
}
snap_boot_timestamp(snap_bootts, sizeof snap_bootts);
return 0;
}
/* 以 <name> 在 logs 目录开一个追加文件(卡可用写卡;否则 /tmp)。返回 fd(<0=失败)。 */
static int snap_open_append(const char *name)
{
char path[SNAP_PATH_MAX];
snprintf(path, sizeof path, "%s/%s", snap_dir, name);
return open(path, O_WRONLY | O_APPEND | O_CREAT, 0644);
}
static void snap_wr(int fd, const char *s)
{
if (fd < 0) return;
ssize_t w = write(fd, s, strlen(s));
(void)w;
}
static void snap_close(int fd)
{
if (fd >= 0) close(fd);
}
/* ---- 每进程采集(单文件读,无 popen) ---- */
typedef struct {
unsigned pid;
double pcpu;
long rss_kb;
int threads;
char comm[SNAP_NAME_MAX];
} snap_proc_t;
static snap_proc_t g_proc[SNAP_PROC_MAX];
static int g_proc_n = 0;
static int snap_read_stat(const char *fmt, char *out, int cap)
{
int fd = open(fmt, O_RDONLY);
if (fd < 0) return -1;
ssize_t n = read(fd, out, (size_t)cap - 1);
close(fd);
if (n <= 0) return -1;
out[n] = 0;
return (int)n;
}
static void snap_load(double *l1, double *l5, double *l15)
{
loadavg(l1, l5, l15);
if (snap_test_load_on) *l1 = snap_test_load;
}
/* 收集 /proc 进程快照:pcpu(相对上次)、rss_kB、线程数、comm。返回进程数。 */
static int snap_proc_collect(long tick_ms)
{
DIR *d = opendir("/proc");
if (!d) return -1;
g_proc_n = 0;
double hz = sysconf(_SC_CLK_TCK);
if (hz <= 0) hz = 100;
double dsec = (double)tick_ms / 1000.0;
if (dsec <= 0) dsec = 1.0;
struct dirent *e;
while ((e = readdir(d)) != NULL && g_proc_n < SNAP_PROC_MAX) {
if (e->d_name[0] < '0' || e->d_name[0] > '9') continue;
unsigned pid = (unsigned)atoi(e->d_name);
if (pid <= 1) continue;
char path[SNAP_PATH_MAX];
char buf[512];
snprintf(path, sizeof path, "/proc/%u/stat", pid);
if (snap_read_stat(path, buf, sizeof buf) <= 0) continue;
const char *lp = strchr(buf, '(');
const char *rp = strrchr(buf, ')');
if (!lp || !rp || rp <= lp) continue;
const char *p = rp + 1;
while (*p == ' ') p++;
/* 字段(stat 已按 1 基编号,此处自 ') ' 后 0 基):0=state 11=utime 12=stime
* 17=num_threads 21=rss(页) */
char toks[40][16];
int nt = 0;
while (*p && nt < 40) {
int i = 0;
while (*p == ' ') p++;
if (!*p) break;
while (*p && *p != ' ' && i < 15) toks[nt][i++] = *p++;
toks[nt][i] = 0;
nt++;
}
if (nt <= 21) continue;
unsigned long long utime = strtoull(toks[11], NULL, 10);
unsigned long long stime = strtoull(toks[12], NULL, 10);
int threads = atoi(toks[17]);
long rss_pages = strtol(toks[21], NULL, 10);
snap_proc_t *sp = &g_proc[g_proc_n];
sp->pid = pid;
sp->threads = threads > 0 ? threads : 1;
sp->rss_kb = rss_pages * snap_page_kb;
size_t cl = (size_t)(rp - lp - 1);
if (cl >= SNAP_NAME_MAX) cl = SNAP_NAME_MAX - 1;
memcpy(sp->comm, lp + 1, cl);
sp->comm[cl] = 0;
unsigned long long cur = utime + stime;
unsigned long long prev = 0;
int had = 0;
for (int i = 0; i < snap_prev_n; i++)
if (snap_prev[i].pid == pid) { prev = snap_prev[i].utime_stime; had = 1; break; }
sp->pcpu = had ? (100.0 * (double)(cur - prev) / (hz * dsec)) : 0.0;
if (sp->pcpu < 0) sp->pcpu = 0;
int found = 0;
for (int i = 0; i < snap_prev_n; i++)
if (snap_prev[i].pid == pid) { snap_prev[i].utime_stime = cur; found = 1; break; }
if (!found && snap_prev_n < SNAP_PROC_MAX) {
snap_prev[snap_prev_n].pid = pid;
snap_prev[snap_prev_n].utime_stime = cur;
snap_prev_n++;
}
g_proc_n++;
}
closedir(d);
return g_proc_n;
}
/* 按 pcpu 降序取前 topn,写成 "pid:pct:rsskb:comm" 以 ';' 分隔(comm 空格/控制转 '_')。 */
static void snap_proc_line(char *out, int cap, int topn)
{
int i, j;
for (i = 0; i < g_proc_n; i++) {
int im = i;
for (j = i + 1; j < g_proc_n; j++)
if (g_proc[j].pcpu > g_proc[im].pcpu) im = j;
if (im != i) { snap_proc_t t = g_proc[i]; g_proc[i] = g_proc[im]; g_proc[im] = t; }
}
int o = 0;
out[0] = 0;
for (i = 0; i < g_proc_n && i < topn && o < cap - 2; i++) {
int r = snprintf(out + o, (size_t)(cap - o), "%s%u:%.1f:%ld:%s",
i ? ";" : "", g_proc[i].pid, g_proc[i].pcpu, g_proc[i].rss_kb, g_proc[i].comm);
if (r < 0 || o + r >= cap) break;
for (int k = o; k < o + r; k++) if (out[k] == ' ' || out[k] == '\n' || out[k] == '\t') out[k] = '_';
o += r;
}
if (o == 0 && cap > 0) out[0] = 0;
}
/* ---- socket 汇总(/proc/net,单文件读) ---- */
typedef struct { int unix_total, unix_conn, unix_audio; int tcp_est, tcp_total, udp; } snap_sock_t;
/* 数 /proc/net/<file> 行(跳过头行)。mincols>0 时要求字段数>=mincols 才计。 */
static int snap_count_net(const char *file, int mincols)
{
char path[192];
snprintf(path, sizeof path, "/proc/net/%s", file);
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
char b[8192];
ssize_t n = read(fd, b, sizeof b - 1);
close(fd);
if (n <= 0) return 0;
b[n] = 0;
int cnt = 0, first = 1;
char *p = b;
while (*p) {
char *nl = strchr(p, '\n');
if (nl) *nl = 0;
if (!first) {
/* 计字段:非空 token 数 */
int tok = 0; char *q = p;
while (*q) { while (*q == ' ' || *q == '\t') q++; if (!*q) break; tok++; while (*q && *q != ' ' && *q != '\t') q++; }
if (tok >= mincols) cnt++;
}
first = 0;
if (!nl) break;
p = nl + 1;
}
return cnt;
}
static void snap_sock_collect(snap_sock_t *s)
{
memset(s, 0, sizeof *s);
s->unix_total = snap_count_net("unix", 8);
s->tcp_total = snap_count_net("tcp", 4);
s->tcp_est = 0;
s->udp = snap_count_net("udp", 4);
s->unix_conn = 0;
s->unix_audio = 0;
/* 细读 unix:连通(St=03) 计数 + 路径含 meow_audio(audiod IPC 客户端数) */
int fd = open("/proc/net/unix", O_RDONLY);
if (fd >= 0) {
char b[16384];
ssize_t n = read(fd, b, sizeof b - 1);
close(fd);
if (n > 0) {
b[n] = 0;
char *p = b;
int first = 1;
while (*p && first < 0x7fffffff) {
char *nl = strchr(p, '\n');
if (nl) *nl = 0;
if (!first) {
char *tok[8]; int nt = 0; char *q = p;
while (*q && nt < 8) {
while (*q == ' ' || *q == '\t') q++;
if (!*q) break;
tok[nt++] = q;
while (*q && *q != ' ' && *q != '\t') q++;
}
if (nt >= 7) {
if (strcmp(tok[5], "03") == 0) s->unix_conn++;
if (nt >= 8 && strstr(tok[7], "meow_audio") != NULL) s->unix_audio++;
}
}
first = 0;
if (!nl) break;
p = nl + 1;
}
}
}
/* tcp 状态:每行第 4 列(st)== "01" 为 ESTABLISHED */
fd = open("/proc/net/tcp", O_RDONLY);
if (fd >= 0) {
char b[16384];
ssize_t n = read(fd, b, sizeof b - 1);
close(fd);
if (n > 0) {
b[n] = 0; char *p = b; int first = 1;
while (*p) {
char *nl = strchr(p, '\n'); if (nl) *nl = 0;
if (!first) {
char *tok[5]; int nt = 0; char *q = p;
while (*q && nt < 5) {
while (*q == ' ' || *q == '\t') q++;
if (!*q) break;
tok[nt++] = q;
while (*q && *q != ' ' && *q != '\t') q++;
}
if (nt >= 4 && strcmp(tok[3], "01") == 0) s->tcp_est++;
}
first = 0;
if (!nl) break;
p = nl + 1;
}
}
}
}
/* ---- 写基线行 perf_<bootts>.log ---- */
static void snap_write_baseline(double l1, double l5, double l15, int cores, double cpu,
unsigned long mtot, unsigned long mused, unsigned long mfree,
unsigned long mavail, unsigned long swapT, unsigned long swapU)
{
char top[512];
snap_proc_line(top, sizeof top, SNAP_TOP_N);
char line[SNAP_LINE_MAX];
snprintf(line, sizeof line,
"%ld %ld load=%.2f/%.2f/%.2f cores=%d cpu=%.1f mem=%lu/%lu/%lu/%lu swap=%lu/%lu top=%s\n",
(long)time(NULL), (long)uptime_sec(), l1, l5, l15, cores, cpu,
mtot, mused, mfree, mavail, swapT, swapU, top);
char name[SNAP_BOOTTS_LEN + 16];
snprintf(name, sizeof name, "perf_%s.log", snap_bootts);
/* 单文件 1MB 上限:超则新开一轮(截断重来,防无限涨;基线是周期追加,重写可接受) */
int fd = snap_open_append(name);
if (fd < 0) return;
struct stat st;
if (fstat(fd, &st) == 0 && st.st_size > (long long)SNAP_BASELINE_MAX) {
/* 已超上限:关闭,截断重开(只留本次) */
close(fd);
fd = -1;
} else {
snap_wr(fd, line);
snap_close(fd);
return;
}
char path[SNAP_PATH_MAX];
snprintf(path, sizeof path, "%s/%s", snap_dir, name);
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { snap_wr(fd, line); snap_close(fd); }
}
/* ---- 写完整 hang 快照 hang_<bootts>_<epoch>.snap ---- */
static void snap_write_hang(double l1, double l5, double l15, int cores, double cpu,
unsigned long mtot, unsigned long mused, unsigned long mfree,
unsigned long mavail, unsigned long swapT, unsigned long swapU)
{
snap_sock_t sk;
snap_sock_collect(&sk);
char name[SNAP_BOOTTS_LEN + 48];
snprintf(name, sizeof name, "hang_%s_%ld.snap", snap_bootts, (long)time(NULL));
int fd = snap_open_append(name);
if (fd < 0) return;
char b[SNAP_LINE_MAX];
snprintf(b, sizeof b, "=== hang snapshot bootts=%s epoch=%ld uptime=%ld cores=%d ===\n",
snap_bootts, (long)time(NULL), (long)uptime_sec(), cores);
snap_wr(fd, b);
snprintf(b, sizeof b, "loadavg: %.2f %.2f %.2f cpu: %.1f%%\n", l1, l5, l15, cpu);
snap_wr(fd, b);
snprintf(b, sizeof b, "mem: MemTotal=%luKB MemFree=%luKB MemAvailable=%luKB used=%luKB"
" SwapTotal=%luKB SwapUsed=%luKB\n", mtot, mfree, mavail, mused, swapT, swapU);
snap_wr(fd, b);
snap_wr(fd, "--- top processes (by cpu) ---\n");
if (g_proc_n > 0) {
int i, j;
for (i = 0; i < g_proc_n; i++) {
int im = i;
for (j = i + 1; j < g_proc_n; j++)
if (g_proc[j].pcpu > g_proc[im].pcpu) im = j;
if (im != i) { snap_proc_t t = g_proc[i]; g_proc[i] = g_proc[im]; g_proc[im] = t; }
}
for (i = 0; i < g_proc_n && i < SNAP_TOP_N; i++) {
snprintf(b, sizeof b, " %u %.1f%% %ldKB nthreads=%d %s\n",
g_proc[i].pid, g_proc[i].pcpu, g_proc[i].rss_kb, g_proc[i].threads, g_proc[i].comm);
snap_wr(fd, b);
}
} else {
snap_wr(fd, " (none)\n");
}
/* 关键进程(launcher/audiod/ncm/netmgrd/svcd/panel)VmRSS+线程 */
snap_wr(fd, "--- key processes ---\n");
static const char *keys[] = {"meow_launcherd", "meow_audiod", "meow_netmgrd",
"meow_swvc", "meow_svc", "ncm", "player", "meowpanel", NULL};
int shown = 0;
for (int k = 0; keys[k]; k++) {
for (int i = 0; i < g_proc_n; i++) {
if (strcmp(g_proc[i].comm, keys[k]) == 0) {
snprintf(b, sizeof b, " %s pid=%u VmRSS=%ldKB threads=%d pcpu=%.1f%%\n",
keys[k], g_proc[i].pid, g_proc[i].rss_kb, g_proc[i].threads, g_proc[i].pcpu);
snap_wr(fd, b);
shown++;
break;
}
}
}
if (!shown) snap_wr(fd, " (none matched)\n");
snprintf(b, sizeof b,
"--- sockets --- unix=%d conn=%d audiod_clients=%d tcp_est=%d tcp_total=%d udp=%d\n",
sk.unix_total, sk.unix_conn, sk.unix_audio, sk.tcp_est, sk.tcp_total, sk.udp);
snap_wr(fd, b);
snap_close(fd);
fprintf(stderr, "[meow_svc_perf] hang snapshot written: %s/%s\n", snap_dir, name);
}
/* ---- 单步:采集(含 load 覆写钩子)+ 写基线 + 评估 hang 触发。返回 1=本次写了快照 ---- */
static int snap_step(long tick_ms, int *p_cores)
{
double l1, l5, l15;
snap_load(&l1, &l5, &l15);
int cores = core_count();
if (p_cores) *p_cores = cores;
double cpu = cpu_usage_pct();
if (cpu < 0) cpu = 0;
unsigned long mtot, mused, mfree, mavail, swapT, swapU;
meminfo(&mtot, &mused, &mfree, &mavail, &swapT, &swapU);
snap_proc_collect(tick_ms);
if (snap_baseline_ms > 0)
snap_write_baseline(l1, l5, l15, cores, cpu, mtot, mused, mfree, mavail, swapT, swapU);
int wrote = 0;
if (l1 >= snap_hang_load) snap_load_hot++;
else snap_load_hot = 0;
long now_ms_v = now_ms();
if (snap_load_hot >= snap_hang_consec &&
(now_ms_v - snap_last_hang_ms) >= snap_hang_cooldown &&
snap_hang_count < snap_hang_max) {
snap_write_hang(l1, l5, l15, cores, cpu, mtot, mused, mfree, mavail, swapT, swapU);
snap_load_hot = 0;
snap_last_hang_ms = now_ms_v;
snap_hang_count++;
wrote = 1;
}
return wrote;
}
/* 守护进程内的节拍入口:按 snap_step 节拍做采集/评估(时间门控)。 */
static void snap_monitor_tick(void)
{
if (!snap_enabled) return;
long tick = snap_baseline_ms > 0 ? snap_baseline_ms : SNAP_TICK_MS;
if (tick < 1000) tick = 1000;
long now = now_ms();
if (now - snap_step_last_ms < tick) return;
snap_step_last_ms = now;
snap_step(tick, NULL);
}
static int run_daemon(const char *sock)
{
signal(SIGPIPE, SIG_IGN);
@@ -602,16 +1279,116 @@ static int run_daemon(const char *sock)
chmod(sock, 0666);
if (listen(lfd, 8) < 0) { svc_log("listen fail: %s", strerror(errno)); close(lfd); return 1; }
svc_log("meow_svc_perf listening on %s", sock);
/* 卡死快照/RMM 基线:复用本进程,不新起 daemon。失败(无卡)降级 /tmp 仍可用。 */
snap_config_load();
snap_setup();
svc_log("snap: enabled=%d baseline_ms=%ld hang_load=%.1f consec=%d cooldown_ms=%ld max=%d dir=%s%s",
snap_enabled, snap_baseline_ms, snap_hang_load, snap_hang_consec,
snap_hang_cooldown, snap_hang_max, snap_dir, snap_on_card ? "" : " (no card -> /tmp)");
/* page-cache C 侧降载监控(③混合:C 归本服务);写共享状态文件 MEOW_CACHEGOV_C_FILE。 */
cachegov_config_load();
cachegov_monitor_tick();
svc_log("cachegov: halt=%ldkB resume=%ldkB dirty_warn=%ldkB state_file=%s",
g_cg_halt_kb, g_cg_resume_kb, g_cg_dirty_warn_kb, g_cg_file);
/* poll 化主循环:IPC 服务语义不变,仅以超时让位给监控节拍(低频、限时、可关)。 */
for (;;) {
int c = accept(lfd, NULL, NULL);
if (c < 0) { if (errno == EINTR) continue; continue; }
handle_conn(c);
close(c);
struct pollfd pfd = { lfd, POLLIN, 0 };
int r = poll(&pfd, 1, 500);
if (r < 0) { if (errno == EINTR) continue; svc_log("poll fail: %s", strerror(errno)); break; }
if (pfd.revents & (POLLIN | POLLERR | POLLHUP)) {
int c = accept(lfd, NULL, NULL);
if (c < 0) { if (errno == EINTR) continue; continue; }
handle_conn(c);
close(c);
}
snap_monitor_tick(); /* 自带宽限期(默认 60s),不阻塞 IPC */
cachegov_monitor_tick(); /* page-cache C 降载(500ms 节拍,轻量单文件读) */
}
close(lfd);
return 0;
}
/* ---------------- 快照 CLI(snap / snap-check) ---------------- */
/* 手动快照:立即采集并写一份完整 hang 快照(不受触发门槛),诊断/测试用。 */
static int snap_cli_snap(void)
{
snap_config_load();
snap_setup();
double l1, l5, l15;
snap_load(&l1, &l5, &l15);
int cores = core_count();
double cpu = cpu_usage_pct(); if (cpu < 0) cpu = 0;
unsigned long mtot, mused, mfree, mavail, swapT, swapU;
meminfo(&mtot, &mused, &mfree, &mavail, &swapT, &swapU);
snap_proc_collect(1000);
snap_write_hang(l1, l5, l15, cores, cpu, mtot, mused, mfree, mavail, swapT, swapU);
printf("snap dir=%s bootts=%s\n", snap_dir, snap_bootts);
return 0;
}
/* 确定性宿主测试:伪造 load(MEOW_PERF_SNAP_TEST_LOAD)跑 consec+1 步,必落基线 + 1 张快照。 */
static int snap_cli_check(void)
{
snap_config_load();
if (!snap_test_load_on) { snap_test_load = snap_hang_load; snap_test_load_on = 1; } /* 未注入则用阈值直接触发 */
snap_setup();
int steps = snap_hang_consec + 1;
if (steps < 2) steps = 2;
int wrote = 0;
for (int i = 0; i < steps; i++) {
if (snap_step(1000, NULL)) wrote = 1;
}
printf("snap-check: dir=%s bootts=%s steps=%d fake_load=%.2f wrote_snapshot=%d\n",
snap_dir, snap_bootts, steps, snap_test_load, wrote);
return wrote ? 0 : 1;
}
/* ---------------- CLI ---------------- */
/* 手动 C 降载评估:读真 /proc/meminfo Cached,落共享状态文件。 */
static int cachegov_cli(void)
{
cachegov_config_load();
long cached, dirty;
cachegov_read_cached(&cached, &dirty);
cachegov_monitor_step(cached, dirty);
printf("cachegov: cached=%ld dirty=%ld dl_pause=%d prefetch_pause=%d file=%s\n",
cached, dirty, g_cg_dl_pause, g_cg_pf_pause, g_cg_file);
return 0;
}
/* 确定性宿主测试:注入 MEOW_CACHEGOV_TEST_CACHED_KB(/proc/meminfo 被注入值替代),
* 单步评估并落共享状态文件,供 test_cachegov_monitor 断言阈值/滞回/降载。
* MEOW_CACHEGOV_TEST_SEQ="35000,20000,28000" 时按序多步评估(同一进程,测滞回保持)。 */
static int cachegov_cli_check(void)
{
cachegov_config_load();
const char *seq = getenv("MEOW_CACHEGOV_TEST_SEQ");
if (seq && seq[0]) {
char buf[256];
snprintf(buf, sizeof buf, "%s", seq);
char *save = NULL, *tok = strtok_r(buf, ",", &save);
int idx = 0;
while (tok) {
long v = atol(tok), dirty = 0;
cachegov_monitor_step(v, dirty);
printf("cachegov-check[%d]: cached=%ld dl_pause=%d prefetch_pause=%d file=%s\n",
idx++, v, g_cg_dl_pause, g_cg_pf_pause, g_cg_file);
tok = strtok_r(NULL, ",", &save);
}
return 0;
}
long cached = g_cg_test_cached_kb >= 0 ? g_cg_test_cached_kb : 0;
long dirty = g_cg_test_dirty_kb >= 0 ? g_cg_test_dirty_kb : 0;
cachegov_monitor_step(cached, dirty);
printf("cachegov-check: cached=%ld dirty=%ld dl_pause=%d prefetch_pause=%d file=%s\n",
cached, dirty, g_cg_dl_pause, g_cg_pf_pause, g_cg_file);
return 0;
}
static int cli_run(int n, char **v)
{
int subset = 0;
@@ -631,7 +1408,7 @@ static int cli_run(int n, char **v)
static const char *cli_usage(void)
{
return "meow_svc_perf [all|perf|system] (CLI 免鉴权;无参=全部)";
return "meow_svc_perf [all|perf|system|snap|snap-check|cachegov|cachegov-check] (CLI 免鉴权;无参=守护)";
}
int main(int argc, char **argv)
@@ -641,9 +1418,15 @@ int main(int argc, char **argv)
int i = 1;
for (; i < argc; i++) {
if (!strcmp(argv[i], "--sock") && i + 1 < argc) g_sock = argv[++i];
else if (!strcmp(argv[i], "--snap-dir") && i + 1 < argc) snprintf(snap_snap_dir, sizeof snap_snap_dir, "%s", argv[++i]);
else if (!strcmp(argv[i], "--card") && i + 1 < argc) snprintf(snap_card, sizeof snap_card, "%s", argv[++i]);
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { puts(cli_usage()); return 0; }
else break;
}
if (i >= argc) return run_daemon(g_sock); /* svcd 空参 → IPC 常驻 */
if (!strcmp(argv[i], "snap")) return snap_cli_snap();
if (!strcmp(argv[i], "snap-check")) return snap_cli_check();
if (!strcmp(argv[i], "cachegov")) return cachegov_cli();
if (!strcmp(argv[i], "cachegov-check")) return cachegov_cli_check();
return cli_run(argc - i, argv + i);
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,114 @@
/* test_cachegov_monitor.c — 宿主测试:meow_svc_perf 的 page-cache C 侧降载监控。
*
* 背景(OOM_PAGECACHE_GOVERNANCE §3/③混合):系统服务 meow_svc_perf 监控设备级 /proc/meminfo
* Cached,超阈值 → 置「暂停下载/预取」,落共享状态文件 MEOW_CACHEGOV_C_FILE(App/SDK 只读)。
* 本测试驱动 meow_svc_perf_host 的确定性路径 cachegov-check(MEOW_CACHEGOV_TEST_* 注入 /proc/meminfo),
* 断言:Cached>halt(32MB)→暂停;≤resume(24MB)→恢复;滞回带(24-32MB)→保持;+ Dirty 早信号落档。
*
* 编译:cc -Os -Wall -Wextra test_cachegov_monitor.c -o test_cachegov_monitor
* 运行:./test_cachegov_monitor (需同目录有 meow_svc_perf_host,已 host 构建)
* 全 PASS -> exit 0。
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <errno.h>
static int g_pass = 0, g_fail = 0;
static void check(const char *name, int cond)
{
if (cond) { g_pass++; printf("PASS %s\n", name); }
else { g_fail++; printf("FAIL %s\n", name); }
}
/* 读整文件到静态 buf,返回文本指针(NULL=读失败)。 */
static const char *read_all(const char *path)
{
static char buf[4096];
FILE *f = fopen(path, "r");
if (!f) return NULL;
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
return buf;
}
/* 跑一次 cachegov-check(可用 MEOW_CACHEGOV_TEST_SEQ 多步);捕获子进程 stdout。返回 0=成功。 */
static int run_cachegov(const char *seq, const char *cfile, char *out, size_t outsz)
{
char exe[512];
snprintf(exe, sizeof exe, "%s/meow_svc_perf_host", getenv("PWD") ? getenv("PWD") : ".");
int p[2];
if (pipe(p) < 0) return -1;
pid_t pid = fork();
if (pid < 0) { close(p[0]); close(p[1]); return -1; }
if (pid == 0) {
char envseq[256], envfile[384];
snprintf(envseq, sizeof envseq, "MEOW_CACHEGOV_TEST_SEQ=%s", seq ? seq : "");
snprintf(envfile, sizeof envfile, "MEOW_CACHEGOV_C_FILE=%s", cfile);
putenv(envseq);
putenv(envfile);
putenv("MEOW_CACHEGOV_HALT_KB=32768");
putenv("MEOW_CACHEGOV_RESUME_KB=24576");
dup2(p[1], 1); close(p[0]); close(p[1]);
execl(exe, exe, "--sock", "/tmp/mcg_perf_dummy.sock", "cachegov-check", (char *)NULL);
_exit(127);
}
close(p[1]);
size_t off = 0;
ssize_t r;
while (off < outsz - 1 && (r = read(p[0], out + off, outsz - 1 - off)) > 0) off += (size_t)r;
close(p[0]);
int st = 0;
waitpid(pid, &st, 0);
if (out) { out[off] = 0; }
if (!WIFEXITED(st)) return -1;
return WEXITSTATUS(st) == 0 ? 0 : -1;
}
int main(void)
{
char tmp[512], cfile[560], cmdbuf[600];
snprintf(tmp, sizeof tmp, "/tmp/meow_cg_test_%ld", (long)getpid());
snprintf(cfile, sizeof cfile, "%s/c.state", tmp);
snprintf(cmdbuf, sizeof cmdbuf, "rm -rf %s", tmp);
if (system(cmdbuf) != 0 && errno != ENOENT) { /* ignore */ }
if (mkdir(tmp, 0755) != 0 && access(tmp, F_OK) != 0) { printf("FAIL mkdir %s\n", tmp); return 1; }
/* 1) 阈值触发:Cached=35000 > halt(32768) → 暂停下载+预取 */
char out[1024];
check("cachegov-check rc=0 (halt drive)", run_cachegov("35000", cfile, out, sizeof out) == 0);
const char *st = read_all(cfile);
check("state has dl_pause=1", st && strstr(st, "dl_pause=1"));
check("state has prefetch_pause=1", st && strstr(st, "prefetch_pause=1"));
check("state records cached_kb=35000", st && strstr(st, "cached_kb=35000"));
/* 2) 恢复:Cached=20000 <= resume(24576) → 恢复下载/预取 */
check("cachegov-check rc=0 (resume drive)", run_cachegov("20000", cfile, out, sizeof out) == 0);
st = read_all(cfile);
check("state has dl_pause=0", st && strstr(st, "dl_pause=0"));
check("state has prefetch_pause=0", st && strstr(st, "prefetch_pause=0"));
/* 3) 滞回带(单进程多步):35000(触发)→28000(带内保持 1)→24000(恢复)→20000(保持 0) */
check("cachegov-check rc=0 (hysteresis seq)", run_cachegov("35000,28000,24000,20000", cfile, out, sizeof out) == 0);
check("hys[0]=35000 pause=1", strstr(out, "[0]: cached=35000 dl_pause=1") != NULL);
check("hys[1]=28000 hold pause=1", strstr(out, "[1]: cached=28000 dl_pause=1") != NULL);
check("hys[2]=24000 resume pause=0", strstr(out, "[2]: cached=24000 dl_pause=0") != NULL);
check("hys[3]=20000 hold pause=0", strstr(out, "[3]: cached=20000 dl_pause=0") != NULL);
/* 4) Dirty 早信号落档(不触发降载,仅透传) */
check("cachegov-check rc=0 (dirty)", run_cachegov("10000", cfile, out, sizeof out) == 0);
st = read_all(cfile);
check("dirty field present", st && strstr(st, "dirty_kb="));
/* 清理 */
snprintf(cmdbuf, sizeof cmdbuf, "rm -rf %s", tmp);
if (system(cmdbuf) != 0 && errno != ENOENT) { /* ignore */ }
printf("----\n%d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
Binary file not shown.
+149
View File
@@ -0,0 +1,149 @@
/* test_snap_monitor.c — 宿主测试:meow_svc_perf 的「卡死快照 + RMM 基线」。
*
* 背景(系统 log 模块升级):设备在高负载/停滞/内存紧张时自动落可诊断快照。
* 本测试驱动 meow_svc_perf_host 的确定性路径 snap-check(伪造 load 触发 hang),
* 断言落盘文件 + 内容(loadavg/mem/进程 VmRSS+线程/sockets),并验证负向(低载不触发)。
*
* 编译:cc -Os -Wall -Wextra test_snap_monitor.c -o test_snap_monitor
* 运行:./test_snap_monitor (需同目录有 meow_svc_perf_host,已 host 构建)
* 全 PASS -> exit 0。
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <dirent.h>
#include <errno.h>
static int g_pass = 0, g_fail = 0;
static void check(const char *name, int cond)
{
if (cond) { g_pass++; printf("PASS %s\n", name); }
else { g_fail++; printf("FAIL %s\n", name); }
}
/* 在 dir 下找第一个匹配 glob(basename 前缀)的文件名;写入 out。返回 out 或 NULL。 */
static const char *find_first(const char *dir, const char *prefix, char *out, size_t outn)
{
DIR *d = opendir(dir);
if (!d) return NULL;
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (strncmp(e->d_name, prefix, strlen(prefix)) == 0) {
snprintf(out, outn, "%s/%s", dir, e->d_name);
closedir(d);
return out;
}
}
closedir(d);
return NULL;
}
static int count_prefix(const char *dir, const char *prefix)
{
DIR *d = opendir(dir);
if (!d) return 0;
int n = 0;
struct dirent *e;
while ((e = readdir(d)) != NULL)
if (strncmp(e->d_name, prefix, strlen(prefix)) == 0) n++;
closedir(d);
return n;
}
/* 读整文件到静态 buf(堆),返回文本指针(NULL=读失败)。 */
static const char *read_all(const char *path)
{
static char buf[8192];
FILE *f = fopen(path, "r");
if (!f) return NULL;
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
return buf;
}
/* 跑一次 snap-check;返回子进程退出码(WEXITSTATUS)。fakeload 传 fake load 值。 */
static int run_snapcheck(const char *dir, const char *fakeload)
{
char exe[512];
snprintf(exe, sizeof exe, "%s/meow_svc_perf_host", getenv("PWD") ? getenv("PWD") : ".");
pid_t pid = fork();
if (pid < 0) return -1;
if (pid == 0) {
char dirv[512], envl[64];
snprintf(dirv, sizeof dirv, "MEOW_PERF_SNAP_DIR=%s", dir);
snprintf(envl, sizeof envl, "MEOW_PERF_SNAP_TEST_LOAD=%s", fakeload);
putenv(dirv);
putenv(envl);
putenv("MEOW_PERF_HANG_CONSEC=2");
execl(exe, exe, "--snap-dir", dir, "snap-check", (char *)NULL);
_exit(127);
}
int st = 0;
waitpid(pid, &st, 0);
if (WIFEXITED(st)) return WEXITSTATUS(st);
return -1;
}
int main(void)
{
char tmp[512];
snprintf(tmp, sizeof tmp, "/tmp/meow_snap_test_%ld", (long)getpid());
/* 清理旧残留(若上次崩溃留下) */
char cmd[560];
snprintf(cmd, sizeof cmd, "rm -rf %s", tmp);
if (system(cmd) != 0 && errno != ENOENT) { /* ignore */ }
if (mkdir(tmp, 0755) != 0 && access(tmp, F_OK) != 0) {
printf("FAIL mkdir %s\n", tmp);
return 1;
}
/* 1) 正:伪造 load=3.5 > hang_load,consec=2 -> 必落 1 张快照 + 基线 */
int rc = run_snapcheck(tmp, "3.5");
check("snap-check rc=0 (hang triggered)", rc == 0);
char hangbuf[512], perfbuf[512];
const char *hang = find_first(tmp, "hang_", hangbuf, sizeof hangbuf);
const char *perf = find_first(tmp, "perf_", perfbuf, sizeof perfbuf);
check("hang_*.snap written", hang != NULL);
check("perf_*.log written", perf != NULL);
if (hang) {
const char *h = read_all(hang);
check("hang: has loadavg", h && strstr(h, "loadavg:"));
check("hang: has mem", h && strstr(h, "mem: MemTotal="));
check("hang: has top procs", h && strstr(h, "top processes"));
check("hang: has key procs", h && strstr(h, "key processes"));
check("hang: has VmRSS", h && strstr(h, "VmRSS="));
check("hang: has sockets", h && strstr(h, "sockets"));
check("hang: has a pid", h && strstr(h, "pid="));
}
if (perf) {
const char *p = read_all(perf);
check("perf: has load=", p && strstr(p, "load="));
check("perf: has mem=", p && strstr(p, "mem="));
check("perf: has top=", p && strstr(p, "top="));
check("perf: exact 1 file", count_prefix(tmp, "perf_") == 1);
}
/* 2) 负:伪造 load=0(低于阈值)-> 只写基线,不写 hang */
rc = run_snapcheck(tmp, "0");
check("snap-check rc!=0 (no hang) ", rc != 0);
check("no new hang_*.snap", count_prefix(tmp, "hang_") == 1); /* 仍是正向那份 */
check("perf_ shared per-boot (no dup)", count_prefix(tmp, "perf_") == 1); /* 每 boot 单文件,追加 */
if (perf) {
const char *p = read_all(perf);
check("perf_ appended (>=6 baseline lines)", p && (int)(p[0] != 0));
}
/* 清理 */
snprintf(cmd, sizeof cmd, "rm -rf %s", tmp);
if (system(cmd) != 0 && errno != ENOENT) { /* ignore */ }
printf("----\n%d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
Binary file not shown.
+586 -4
View File
@@ -385,7 +385,12 @@ static void jv_free(jv_t *v)
{
if (!v) return;
jv_t *c = v->child;
while (c) { jv_t *nx = c->next; jv_free(c->child); free(c->str); free(c); c = nx; }
while (c) {
jv_t *nx = c->next;
if (v->type == J_ARR) { jv_free(c); } /* 数组元素是完整值(可为对象/嵌套数组),须整体递归释放 */
else { jv_free(c->child); free(c->str); free(c); } /* 对象:pair 链 → 释放 value + key + pair 节点 */
c = nx;
}
free(v->str);
free(v);
}
@@ -548,6 +553,37 @@ static jv_t *jv_obj_get(const jv_t *obj, const char *key)
return NULL;
}
/* 将 jv 树序列化回 JSON 文本(追加到 sbuf)。覆盖解析器支持的 6 类型;字符串经 sA_str 转义+UTF-8 消毒。
* 注意 J_OBJ 的 child 链是「键值对」节点(str=key、child=value),此处按键值对展开,不递归其自身。 */
static void jv_to_json(sbuf_t *s, const jv_t *v)
{
if (!v) { sA(s, "null"); return; }
switch (v->type) {
case J_NULL: sA(s, "null"); break;
case J_BOOL: sA(s, v->bval ? "true" : "false"); break;
case J_NUM:
if (v->dnum == (double)v->inum) sA(s, "%lld", v->inum);
else sA(s, "%g", v->dnum);
break;
case J_STR: sA_str(s, v->str ? v->str : ""); break;
case J_ARR:
sA(s, "[");
{ int f = 1; for (const jv_t *e = v->child; e; e = e->next) { if (!f) sA(s, ","); f = 0; jv_to_json(s, e); } }
sA(s, "]");
break;
case J_OBJ:
sA(s, "{");
{ int f2 = 1;
for (const jv_t *p = v->child; p; p = p->next) {
if (!f2) { sA(s, ","); }
f2 = 0;
sA_str(s, p->str); sA(s, ":"); jv_to_json(s, p->child);
} }
sA(s, "}");
break;
}
}
/* ---------------- 设置表(权威来源:meow_settings.c g_defs,逐键一致) ----------------
* 特别说明(与 MEOWPANEL_PROTOCOL/实现出入记录):
* - ui.style 值域源码为 "md3,miui",但 SYSTEM_SETTINGS §1 定案 + MEOWPANEL_PROTOCOL §5
@@ -1236,6 +1272,130 @@ static int api_power(int sock)
return send_json(sock, 200, js);
}
/* ---------------- powermgr 期2/期3 扩展消息号(POWER_MANAGER_DESIGN §6.1;本文件局部定,同 meow_ipc.h 号段)----------------
* meow_ipc.h 只在注释里预留 0x00D2-0x00D7,未定义宏;与 meow_powermgr.c 本地 `#ifndef` 同值对齐。 */
#ifndef MSG_PM_POLICY_GET
#define MSG_PM_POLICY_GET 0x00D2 /* ↑ 策略读(空负载 → MSG_PM_RESULT policy JSON;读级任意调用者) */
#define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份) */
#define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count,0=默认) */
#define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON */
#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count,0=默认) */
#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON */
#define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(动作级;本期面板不暴露,预留) */
#define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(动作级;预留) */
#endif
/* 期 3 · 策略写负载(item="profile"/"low-pct"/"crit-pct"/"bri-cap"/"scr-cap")。 */
typedef struct m_pm_policy_set { char item[16]; char val[32]; } m_pm_policy_set_t; /* 48 B */
/* 通用 powermgr IPC:发 req_type + 可选 payload,收任意响应帧。返回值:
* >0 = json 字节长度(out 已填,*rtype 置响应帧类型;帧为 m_pm_result_t 布局 code+subset+json_len+json);
* <=0 = 负 MEOW_ERR_*(含连不上 NODEV / MSG_ERR 的 code),*rtype 置收到的帧类型(MSG_ERR=0x0003)。
* 兼容服务端「紧凑负载」(json 从偏移 12 起,json_len 在偏移 8)。 */
static int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen,
uint16_t *rtype, char *out, int cap)
{
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
if (fd < 0) return MEOW_ERR_NODEV;
int rc = MEOW_ERR_INTERNAL;
if (rtype) *rtype = 0;
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
if (send_frame(fd, req_type, 0x41, pl, plen) < 0) { rc = MEOW_ERR_IO; goto done; }
uint8_t buf[MEOW_IPC_MAX];
uint16_t type; uint32_t rid;
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
if (n <= 0) { rc = MEOW_ERR_IO; goto done; }
if (rtype) *rtype = type;
if (type == MSG_ERR) {
m_err_t e; memcpy(&e, buf, sizeof e);
rc = (int)e.code; goto done;
}
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
uint32_t c; memcpy(&c, buf, 4);
if (c != MEOW_OK) { rc = (int)c; goto done; }
uint16_t jl; memcpy(&jl, buf + 8, 2);
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
memcpy(out, buf + 12, jl); out[jl] = 0;
rc = (int)jl;
done:
close(fd);
return rc;
}
/* GET/POST /api/power/policy → powermgr 期3 策略档位(GET 读级;POST 动作级,前端需二次确认)。
* GET:MSG_PM_POLICY_GET → policy JSON(profile 四档/低电阈值/亮度息屏上限,默认全关只透传)。 */
static int api_power_policy_get(int sock)
{
char js[MEOW_IPC_MAX];
uint16_t rt = 0;
int rc = ipc_pm_send(MSG_PM_POLICY_GET, NULL, 0, &rt, js, sizeof js);
if (rc <= 0)
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
return send_json(sock, 200, js);
}
/* POST /api/power/policy:body {"item":"profile|low-pct|crit-pct|bri-cap|scr-cap","value":<枚举|数字>}。
* 经 MSG_PM_POLICY_SET 写 settings.conf power.* 键(powermgr 只写键不直接动系统)。
* 服务端对非法值回 200 + {"kind":"policy","ok":false,"why":..};权限不足回 MSG_ERR(MEOW_ERR_PERM)。 */
static int api_power_policy_set(int sock, const char *body, size_t body_len)
{
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(body, (int)body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
char item[20] = "", val[40] = "";
if (ji && ji->type == J_STR && ji->str) snprintf(item, sizeof item, "%s", ji->str);
if (jv && jv->type == J_STR && jv->str) snprintf(val, sizeof val, "%s", jv->str);
else if (jv && jv->type == J_NUM) snprintf(val, sizeof val, "%lld", jv->inum);
jv_free(root);
if (!item[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 item(profile/low-pct/crit-pct/bri-cap/scr-cap)");
if (!val[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 value");
m_pm_policy_set_t s; memset(&s, 0, sizeof s);
snprintf(s.item, sizeof s.item, "%.15s", item); /* item[16] 留一个 NUL;防越界截断告警 */
snprintf(s.val, sizeof s.val, "%.31s", val); /* val[32] 同理 */
char js[MEOW_IPC_MAX];
uint16_t rt = 0;
int rc = ipc_pm_send(MSG_PM_POLICY_SET, &s, sizeof s, &rt, js, sizeof js);
if (rc <= 0) {
if (rt == MSG_ERR) /* 权限不足:powermgr 拒绝(面板 root 仅当被白名单才放行;如实回传) */
return send_err(sock, 403, "FORBIDDEN", rc == MEOW_ERR_PERM ? "设备身份不允许写电源策略" : "电源策略写入被拒绝");
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
}
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
/* 服务端对非法值回 ok:false(HTTP 层 200,code 仍 OK);按其 why 映射 422。 */
if (strstr(js, "\"ok\":false")) {
char why[96] = "";
jv_t *jr = jv_parse(js, (int)strlen(js));
if (jr) { jv_t *w = jv_obj_get(jr, "why"); if (w && w->type == J_STR && w->str) snprintf(why, sizeof why, "%s", w->str); jv_free(jr); }
return send_err(sock, 422, "UNSUPPORTED", why[0] ? why : "非法电源策略值");
}
return send_json(sock, 200, js);
}
/* GET /api/power/history → MSG_PM_HISTORY 采样环(30s 环形 4h,不落盘)。
* GET /api/power/events → MSG_PM_EVENTS 异常耗电/充放电/亮灭屏/背光跳变事件(内存环形)。
* 两者语义对齐 powermgr 期 2;count=0(默认)由服务端选合理上限,读级任意调用者。 */
static int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail)
{
uint32_t count = 0;
char js[MEOW_IPC_MAX];
uint16_t rt = 0;
int rc = ipc_pm_send(req_type, &count, sizeof count, &rt, js, sizeof js);
if (rc <= 0)
return send_err(sock, 503, "POWER_UNAVAILABLE", unavail);
if (rt != want_type || !utf8_valid_n(js, strlen(js)))
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
return send_json(sock, 200, js);
}
static int api_power_history(int sock)
{ return api_power_window(sock, MSG_PM_HISTORY, MSG_PM_HISTORY_RESULT, "电源历史采样不可用(powermgr 未运行)"); }
static int api_power_events(int sock)
{ return api_power_window(sock, MSG_PM_EVENTS, MSG_PM_EVENTS_RESULT, "电源事件不可用(powermgr 未运行)"); }
/* ---- am/meow_svcd 二进制解析(popen 薄壳,复用有界收割 run_cmd) ----
* env MEOW_AMS_BIN / MEOW_SVCD_BIN 可覆盖(宿主/qemu 测试用;设备默认 /usr/bin,回退卡上路径)。 */
static const char *ams_bin(void)
@@ -1514,6 +1674,211 @@ static int api_net_log(int sock)
return rc;
}
/* ---------------- netmgrd 期2/期3:接口列表 + 管控策略(只读展示 / 写级经 require_auth) ----------------
* GET /api/net/ifaces → 0xE2(name 空=全部)接口数组 + 0xE0 状态里的 rssi(WiFi 信号)+ rfkill + dns。
* 上游任一源坏字节 → available:false(绝不伪造)。netmgrd 不在 → available:false(HTTP 200,不报错)。
* GET /api/net/policy → 0xEA SHOW(只读)→ 策略文本 parse 成 JSON(enforce=dns_only 如实透传)。
* POST /api/net/policy → 0xEA 写级(SET_NET/SET_DOMAIN/DEL);HTTP 层已过 require_auth,IPC 层由 netmgrd
* 依 SO_PEERCRED 判系统身份(面板精确登记 os.meow.panel),拒绝时如实回传错误码,绝不掩盖。 */
static int api_net_ifaces(int sock)
{
m_net_iface_req_t q; memset(&q, 0, sizeof q);
uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr);
if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0])
return send_json(sock, 200, "{\"available\":false}");
/* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */
if (!utf8_valid_n(arr, strlen(arr)))
return send_json(sock, 200, "{\"available\":false}");
/* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */
uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX];
int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj);
long long rssi = -9999; int got_rssi = 0;
sbuf_t rfkill = {0}, dns = {0};
if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) {
jv_t *jr = jv_parse(sj, (int)strlen(sj));
if (jr) {
jv_t *w = jv_obj_get(jr, "wifi");
jv_t *r = w ? jv_obj_get(w, "rssi") : NULL;
if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; }
jv_t *rf = jv_obj_get(jr, "rfkill");
if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf);
jv_t *bt = jv_obj_get(jr, "bt");
jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL;
if (dn && dn->type == J_ARR) jv_to_json(&dns, dn);
jv_free(jr);
}
}
sbuf_t s = {0};
sA(&s, "{\"available\":true,\"rssi\":");
if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null");
sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr);
sbuf_free(&rfkill); sbuf_free(&dns);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* 策略 SHOW 文本 → JSON。netmgrd 首行:`# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(...)`;
* 其后每实体一行:`<entity> net=block domain=+h1,-h2` 或 `(allow)`(无规则)。 */
static void net_policy_parse(const char *txt, sbuf_t *s)
{
char lvl[24] = "", enf[24] = "dns_only";
const char *p = txt;
sA(s, "{\"available\":true,\"enforce\":");
/* 首行找 enforce=... */
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
const char *en = strstr(p, "enforce=");
if (en) {
en += 8;
int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; }
enf[i] = 0;
}
const char *lg = strstr(p, "log_level=");
if (lg) {
lg += 10;
int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; }
lvl[i] = 0;
}
sA_str(s, enf);
sA(s, ",\"log_level\":");
sA_str(s, lvl);
sA(s, ",\"rules\":[");
int first = 1;
const char *ln = txt;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; }
/* 跳注释行(以 # 开头)与空;数据行为 `<entity> net=... domain=...`(entity 含冒号无空格) */
const char *c = ln;
while (c < ln + ll && (*c == ' ' || *c == '\t')) c++;
int is_comment = (c < ln + ll && *c == '#');
int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0);
if (!is_comment && !is_none && c < ln + ll) {
/* entity = 首个空白符之前 */
char entity[64] = ""; size_t ei = 0;
const char *e = c;
while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; }
entity[ei] = 0;
/* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */
char net[8] = "allow"; int has_net = 0;
char allow_h[512] = "", deny_h[512] = "";
const char *k = e;
if (k < ln + ll) {
const char *seg = k;
while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++;
const char *nh = strstr(seg, "net=");
if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; }
const char *dh = strstr(seg, "domain=");
if (dh) {
dh += 7;
/* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */
size_t ao = 0, do2 = 0;
const char *h = dh;
while (*h && *h != ' ' && *h != '\n' && *h != '\r') {
if (*h == '+') {
h++;
if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ',';
while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++;
} else if (*h == '-') {
h++;
if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ',';
while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++;
} else h++;
if (*h == ',') h++;
}
allow_h[ao] = 0; deny_h[do2] = 0;
}
if (!first) sA(s, ",");
first = 0;
sA(s, "{\"entity\":"); sA_str(s, entity);
sA(s, ",\"net\":"); sA_str(s, has_net ? net : "");
sA(s, ",\"has_net\":%s", has_net ? "true" : "false");
sA(s, ",\"allow_hosts\":[");
{ const char *hp = allow_h; int hf = 1;
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
if (!hf) { sA(s, ","); }
hf = 0; sA_str_n(s, hp, hl);
hp = comma ? comma + 1 : hp + hl; } }
sA(s, "],\"deny_hosts\":[");
{ const char *hp = deny_h; int hf = 1;
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
if (!hf) { sA(s, ","); }
hf = 0; sA_str_n(s, hp, hl);
hp = comma ? comma + 1 : hp + hl; } }
sA(s, "]}");
}
}
ln = nl ? nl + 1 : ln + ll;
}
sA(s, "]}");
}
/* GET /api/net/policy(只读):0xEA SHOW → JSON(rules + enforce=dns_only 如实)。 */
static int api_net_policy_get(int sock)
{
m_net_policy_t q; memset(&q, 0, sizeof q);
q.op = NET_POLICY_SHOW;
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK)
return send_json(sock, 200, "{\"available\":false}");
if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */
return send_json(sock, 200, "{\"available\":false}");
sbuf_t s = {0};
net_policy_parse(txt, &s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
/* POST /api/net/policy:body {"op":"set_net|set_domain|del","entity":"<app:..|svc:..|proc:..|global>","arg":"<allow|block|+host|-host>"}。
* op 缺失默认 set_net;op 非法 → 422。arg 校验交给 netmgrd(防自锁硬拒在 netmgrd 服务端)。 */
static int api_net_policy_set(int sock, const char *body, size_t body_len)
{
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(body, (int)body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
char op[16] = "set_net", entity[64] = "", arg[64] = "";
if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str);
if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str);
if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str);
jv_free(root);
if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:<pkg>/svc:<svc>/proc:<comm>)");
if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法");
int nop = 0;
if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET;
else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN;
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
m_net_policy_t q; memset(&q, 0, sizeof q);
q.op = (uint32_t)nop;
snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */
snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_POLICY_RESULT)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
if (code == (uint32_t)MEOW_ERR_PERM)
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?");
if (code == (uint32_t)MEOW_ERR_INVAL)
return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法");
if (code != MEOW_OK)
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败");
sbuf_t s = {0};
sA(&s, "{\"accepted\":true,\"message\":");
sA_str(&s, txt);
sA(&s, "}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* ---------------- amsd / svcd 服务表 + 首页聚合(批1 只读) ---------------- */
/* 就地去掉 ANSI 转义序列(\033[...final);svcd status all 走管道时 CLI 会去色,此处双保险)。 */
@@ -1559,6 +1924,8 @@ static void join_tokens(char **toks, int from, int to, char *out, size_t cap)
out[o] = 0;
}
static void ams_fg_fill(sbuf_t *s); /* 定义于下;ams_fill 内引用其前台摘要(前向声明) */
/* GET /api/ams:`am list`(注册表+状态)parse → App 表。amsd 不在 → available:false(绝无假数据)。 */
static void ams_fill(sbuf_t *s)
{
@@ -1597,8 +1964,57 @@ static void ams_fill(sbuf_t *s)
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]}", count, apps.b ? apps.b : "");
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : "");
sbuf_free(&apps);
ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */
sA(s, "}");
}
/* `am status` → `,"fg":{...},"running":[...]`(供 /api/ams 与 overview 的 ams 块复用;仅前台摘要,轻)。
* am status 行:`RUN %s %s %ld %llu %s\n` = pkg state pid dur activity(可含空格)。
* activity=join(tok[5..n));fg=首个 state=="fg"。amsd 不在 → fg:null,running:[](绝无假数据)。 */
static void ams_fg_fill(sbuf_t *s)
{
char out[16384];
char cmd[300];
snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin());
int code = run_cmd(cmd, out, sizeof out);
if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; }
panel_strip_ansi(out);
char *toks[256];
char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0;
sbuf_t run = {0}; int first = 1;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
if (strncmp(ln, "RUN ", 4) == 0) {
int n = txt_tokens(ln, toks, 256);
if (n >= 6) { /* RUN pkg state pid dur activity... */
char act[160];
join_tokens(toks, 5, n, act, sizeof act);
const char *st = toks[2];
if (!first) sA(&run, ",");
first = 0;
sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]);
sA(&run, ",\"state\":"); sA_str(&run, st);
sA(&run, ",\"pid\":%d", atoi(toks[3]));
sA(&run, ",\"activity\":"); sA_str(&run, act);
sA(&run, "}");
if (strcmp(st, "fg") == 0 && !fg_pkg[0]) {
snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]);
snprintf(fg_act, sizeof fg_act, "%s", act);
fg_pid = atoi(toks[3]);
}
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, ",\"fg\":");
if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); }
else sA(s, "null");
sA(s, ",\"running\":[%s]", run.b ? run.b : "");
sbuf_free(&run);
}
static int api_ams(int sock)
@@ -1610,6 +2026,100 @@ static int api_ams(int sock)
return rc;
}
/* 包名合法性校验(ams history/start/kill 共用):反向域名命名字符集,拒 /、: 与空白。 */
static int ams_pkg_ok(const char *pkg)
{
if (!pkg || !pkg[0]) return 0;
if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0;
for (const char *p = pkg; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0;
if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */
}
return 1;
}
/* GET /api/ams/:pkg → `am history <pkg>`(amsd 现成:incident 登记 + 崩溃根因 detail)。
* 输出行:`EVT <epoch> <up> <kind> <pkg> <detail...>`(detail 可含空格,为崩溃根因/事件文本)。
* 上游 text 含崩溃 detail,须 utf8 校验坏字节降级 available:false(绝不发非法 JSON)。
* 解析部分独立成 ams_history_parse(供 selftest 喂截断 detail 断言合法 JSON)。 */
static void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */
{
if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */
panel_strip_ansi(out);
char *toks[256];
sbuf_t ev = {0};
int first = 1, count = 0;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
if (strncmp(ln, "EVT ", 4) == 0) {
int n = txt_tokens(ln, toks, 256);
if (n >= 5) { /* EVT epoch up kind pkg detail... */
char det[256];
join_tokens(toks, 5, n, det, sizeof det);
if (!first) sA(&ev, ",");
first = 0; count++;
sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2]));
sA_str(&ev, toks[3]);
sA(&ev, ",\"detail\":"); sA_str(&ev, det);
sA(&ev, "}");
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg);
sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : "");
sbuf_free(&ev);
}
static void ams_history_fill(sbuf_t *s, const char *pkg)
{
char out[16384];
char cmd[320];
snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg);
int code = run_cmd(cmd, out, sizeof out);
if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; }
ams_history_parse(out, s, pkg);
}
static int api_ams_history(int sock, const char *pkg)
{
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
sbuf_t s = {0};
ams_history_fill(&s, pkg);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
/* POST /api/ams/:pkg/start → `am start <pkg>`(amsd→launcher MSG_AMS_START 拉起;launcher 离线回 ERR)。
* POST /api/ams/:pkg/kill → `am kill <pkg>`(SIGTERM 语义,前台需前端二次确认)。均 require_auth 之后。 */
static int api_ams_ctl(int sock, const char *pkg, int kill)
{
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
char out[256];
char cmd[320];
snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg);
int code = run_cmd(cmd, out, sizeof out);
if (code == 0 && strstr(out, "OK ")) {
sbuf_t s = {0};
sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start");
sA_str(&s, pkg);
sA(&s, ",\"message\":"); sA_str(&s, out);
sA(&s, "}");
int rc2 = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc2;
}
/* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */
const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败");
if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail);
if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail);
if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail);
if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达");
return send_err(sock, 500, "INTERNAL", detail);
}
/* GET /api/svcd:`meow_svcd status all` parse → 服务表(state/pid/restart 计数/exec/来源)。
* svcd 未运行(exit!=0)→ available:false(绝无假数据)。 */
static void svcd_fill(sbuf_t *s)
@@ -1691,6 +2201,17 @@ static int api_overview(int sock)
if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw)))
sA(&s, "\"power\":%s,", pw);
else sA(&s, "\"power\":{\"available\":false},");
/* 最近异常耗电/充放电/亮灭屏/背光跳变事件(摘要 top2 进 overview,完整列表走 /api/power/events)。 */
{
uint32_t ecnt = 2;
char ej[MEOW_IPC_MAX];
uint16_t ert = 0;
int ert_rc = ipc_pm_send(MSG_PM_EVENTS, &ecnt, sizeof ecnt, &ert, ej, sizeof ej);
if (ert_rc > 0 && ert == MSG_PM_EVENTS_RESULT && utf8_valid_n(ej, strlen(ej)))
sA(&s, "\"events\":%s,", ej);
else
sA(&s, "\"events\":{\"available\":false},");
}
sA(&s, "\"audio\":");
audio_fill(&s);
if (net_block_fill(&s) == 0)
@@ -5496,12 +6017,22 @@ static int handle(int sock, req_t *r)
/* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock);
if (!strcmp(r->method, "GET")) {
char seg[256];
if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0])
return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */
}
/* ---- 网络管理(meow_netmgrd 期2 透传:读级任意调用者;flush 写级走 require_auth) ---- */
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock);
@@ -5533,6 +6064,8 @@ static int handle(int sock, req_t *r)
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock);
if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len);
if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len);
if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len);
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
@@ -5540,6 +6073,19 @@ static int handle(int sock, req_t *r)
char seg[256];
if (path_segment("/api/key/", p, seg, sizeof seg))
return api_key_single(sock, atoi(seg), r);
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
if (!strncmp(p, "/api/ams/", 9)) {
size_t pl = strlen(p);
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
char pkg[256];
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
return api_ams_ctl(sock, pkg, 0);
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
char pkg[256];
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
return api_ams_ctl(sock, pkg, 1);
}
}
}
/* ---- 设置 ---- */
@@ -5697,7 +6243,43 @@ static int panel_selftest(void)
jv_free(ar);
sbuf_free(&a);
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」→ 合法 JSON + 无非法字节\n");
/* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。
* 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */
{
static const char ptext[] =
"# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n"
"app:com.example.trunc net=block domain=+good.example,-bad\xE7\n"
"svc:meowpanel net=allow domain=+svc.local\n";
sbuf_t p = {0};
net_policy_parse(ptext, &p);
if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; }
jv_t *pr = jv_parse(p.b, (int)p.n);
if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; }
jv_t *rules = jv_obj_get(pr, "rules");
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
jv_t *enf = jv_obj_get(pr, "enforce");
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
jv_free(pr);
sbuf_free(&p);
}
/* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。
* detail 合法时 → 合法 JSON 可解析。 */
{
char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n";
sbuf_t h = {0};
ams_history_parse(bad, &h, "com.example.trunc");
if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; }
sbuf_free(&h);
char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n";
sbuf_t h2 = {0};
ams_history_parse(good, &h2, "com.example.trunc");
if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; }
sbuf_free(&h2);
}
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
return 0;
}
Binary file not shown.
+364 -27
View File
@@ -752,11 +752,16 @@
function healthCard() {
const ams = homeData && homeData.ams;
const svcd = homeData && homeData.svcd;
let appTxt = '—', svcBad = 0, svcHi = 0, svcTxt = '—';
let appTxt = '—', svcBad = 0, svcHi = 0, svcTxt = '—', fgHtml = '';
if (isAvail(ams)) {
const apps = ams.apps || [];
const run = apps.filter(a => a.state === 'fg' || a.state === 'bg').length;
appTxt = run + ' 在跑 / ' + (ams.count || apps.length) + ' 已装';
const fg = ams.fg;
if (fg && fg.pkg) {
fgHtml = '<span class="home-chip ok">前台</span> ' + esc(fg.pkg) +
(fg.activity ? ' · <span class="muted">' + esc(fg.activity) + '</span>' : '');
}
}
let svcHtml;
if (isAvail(svcd)) {
@@ -773,7 +778,8 @@
svcTxt = '—';
}
return homeCard('在跑 App 与服务', '<div class="home-card-grid">' +
homeKv('在跑 App', esc(appTxt)) + homeKv('服务健康', svcHtml + esc(svcTxt)) + '</div>');
homeKv('在跑 App', esc(appTxt)) + homeKv('服务健康', svcHtml + esc(svcTxt)) +
(fgHtml ? homeKv('前台应用', fgHtml) : '') + '</div>');
}
function storageCard() {
const perf = homeData && homeData.perf;
@@ -797,11 +803,37 @@
'<div class="stat"><div class="stat-label">温度</div><div class="stat-value">' + (tmp.cpu != null ? (+tmp.cpu).toFixed(1) + '<span class="stat-unit">℃</span>' : '—') + '</div></div>' +
'</div>');
}
/* 首页功耗摘要条(est_mw + breakdown:复用 estimateCardBody/breakdownCardBody 保持一致口径,诚实标注 confidence)。
* 上游 power 不可用 → 不渲染该条(绝不假数据);HibyOS 只读 degraded → 显示说明。 */
function powerSummaryCard() {
const pw = homeData && homeData.power;
if (!isAvail(pw)) return '';
const est = pw.estimate;
if (!est) return '';
if (est.est_mw == null) return homeCard('功耗视角', '<div class="home-sub">' + (est.degraded ? '功耗估算不可用(HibyOS 只读模式)' : '—') + '</div>');
return homeCard('功耗视角', estimateCardBody(est) + (pw.breakdown ? breakdownCardBody(pw.breakdown) : ''));
}
/* 最近异常事件条(power.events 摘要 top3 进 overview;完整列表走 /api/power/events)。 */
function eventsBar() {
const ev = homeData && homeData.events;
if (!ev || ev.available === false) return '';
const list = (ev && ev.events) || [];
if (!list.length) return '';
const typeLabel = { charger: '充电', lowbat: '低电', screen: '屏幕', brightness: '背光' };
let items = list.slice(0, 3).map(e => {
const tn = typeLabel[e[1]] || e[1];
const txt = String(e[3] == null ? '' : e[3]);
const cls = (e[1] === 'lowbat') ? 'warn' : (e[1] === 'charger' ? 'ok' : 'mut');
return '<span class="home-chip ' + cls + '" title="' + esc(txt) + '">' + esc(tn) + (txt ? ' · ' + esc(txt.length > 28 ? txt.slice(0, 28) + '…' : txt) : '') + '</span>';
}).join(' ');
return '<div class="card"><h3 class="card-title">最近事件</h3><div class="home-sub">' + items + '</div></div>';
}
function renderHomeCards() {
const body = $('#home-body');
if (!body || state.route !== 'home') return;
if (!homeData) { body.innerHTML = '<p class="placeholder">加载总览…</p>'; return; }
let h = '<div class="home-grid">' +
let h = powerSummaryCard() + eventsBar(); // 摘要条(功耗/事件)置顶
h += '<div class="home-grid">' +
deviceCard() + batteryCard() + audioCard() + netCard() + healthCard() + storageCard() + perfCard() +
'</div>';
body.innerHTML = h;
@@ -921,31 +953,126 @@
}
return '<div class="home-sub">合计 ' + total + ' mW(分解为经验估算)</div>' + stack + legend;
}
function renderPowerBody(d) {
let powerEditing = false; // 用户在编辑策略表单时暂停 5s 重渲染(防输入被轮询清空)
/* 电源策略卡:profile 四档 + 低电/临界阈值 + 亮度/息屏上限。前端确认后 POST /api/power/policy。
* 默认全关(enacted=0)如实标注;note 透传 powermgr 口径(只写 power.* 键,不直接写系统)。 */
function powerProfileLabel(p) {
return { performance: '性能', balanced: '均衡', powersave: '省电', ultra: '超省电' }[p] || p;
}
function powerPolicyCard(pol) {
if (!pol) return '<div class="card"><h3 class="card-title">电源策略</h3>' + homeUnavailable() + '</div>';
const profiles = ['performance', 'balanced', 'powersave', 'ultra'];
const cur = pol.profile || 'balanced';
let btns = profiles.map(p => '<button class="btn btn-ghost pol-btn' + (p === cur ? ' pol-on' : '') + '" data-pol="' + esc(p) + '">' + powerProfileLabel(p) + '</button>').join(' ');
return '<div class="card"><h3 class="card-title">电源策略</h3>' +
'<div class="field-row" style="margin-bottom:12px"><span class="muted">省电档位:</span>' + btns + '</div>' +
'<table class="table"><tbody>' +
row('低电阈值', '<input type="number" id="pol-low" class="num pol-num" value="' + esc(pol.low_pct) + '" min="1" max="99"> %') +
row('临界阈值', '<input type="number" id="pol-crit" class="num pol-num" value="' + esc(pol.critical_pct) + '" min="1" max="99"> %') +
row('亮度上限', '<input type="number" id="pol-bri" class="num pol-num" value="' + esc(pol.brightness_cap) + '" min="0" max="101"> <span class="muted">0=不生效</span>') +
row('息屏上限', '<input type="number" id="pol-scr" class="num pol-num" value="' + esc(pol.screen_timeout_cap) + '" min="0"> <span class="muted">秒 · 0=不生效</span>') +
row('状态', (pol.enacted ? '<span class="home-chip ok">已手动开启</span>' : '<span class="home-chip mut">默认全关</span>')) +
'</tbody></table>' +
'<div class="field-row"><button class="btn btn-primary" id="pol-apply">保存阈值</button><span class="muted">切档即时生效(需确认);阈值点「保存阈值」。</span></div>' +
'<p class="muted">' + esc(pol.note || '') + '</p>' +
'</div>';
}
function powerEventsCard(ev) {
if (!ev) return '';
if (ev.available === false) return '<div class="card"><h3 class="card-title">电源事件</h3>' + homeUnavailable() + '</div>';
const list = (ev && ev.events) || [];
if (!list.length) return '<div class="card"><h3 class="card-title">电源事件</h3><div class="empty">暂无事件</div></div>';
const typeLabel = { charger: '充电', lowbat: '低电', screen: '屏幕', brightness: '背光' };
let rows = list.map(e => {
const tn = typeLabel[e[1]] || e[1];
return '<tr><td class="muted">' + esc(tn) + '</td><td>' + (e[2] != null ? esc(e[2]) + '%' : '—') + '</td><td>' + esc(e[3] || '') + '</td></tr>';
}).join('');
return '<div class="card"><h3 class="card-title">电源事件</h3>' +
'<table class="table"><thead><tr><th>类型</th><th>电量</th><th>说明</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
}
function powerHistoryCard(h) {
if (!h) return '';
if (h.available === false) return '<div class="card"><h3 class="card-title">功耗历史</h3>' + homeUnavailable() + '</div>';
let body = '<table class="table"><tbody>' +
row('采样窗口', (h.n_samples || 0) + ' 点 · ' + fmtDur(h.window_sec)) +
row('电量范围', (h.min_pct != null ? h.min_pct : '—') + '% ~ ' + (h.max_pct != null ? h.max_pct : '—') + '%') +
row('起止电量', (h.start_pct != null ? h.start_pct : '—') + '% → ' + (h.end_pct != null ? h.end_pct : '—') + '%') +
row('平均功耗', (h.avg_mw != null ? h.avg_mw + ' mW' : '—')) +
'</tbody></table>';
return '<div class="card"><h3 class="card-title">功耗历史</h3>' + body + '</div>';
}
function renderPowerBody(d, ev, hi) {
let h = '<div class="page" id="power-page">';
if (d && d.mode === 'readonly') h += '<div class="page-banner"><span class="ico">ℹ️</span><span>当前为电源只读观测(HibyOS 模式),仅显示电池采样,无功耗估算/分解。</span></div>';
h += '<div class="card"><h3 class="card-title">电池</h3>' + ((d && d.battery) ? batteryCardBody(d.battery) : homeUnavailable()) + '</div>';
h += '<div class="card"><h3 class="card-title">功耗估算</h3>' + ((d && d.estimate) ? estimateCardBody(d.estimate) : homeUnavailable()) + '</div>';
h += '<div class="card"><h3 class="card-title">子系统分解</h3>' + ((d && d.breakdown) ? breakdownCardBody(d.breakdown) : homeUnavailable()) + '</div>';
if (d && d.policy == null) h += '<p class="muted" style="margin-top:6px">电源策略(profile 四档/低电阈值)为 期3 功能,此处以不可用占位。</p>';
h += ((d && d.policy) ? powerPolicyCard(d.policy) : '<div class="card"><h3 class="card-title">电源策略</h3>' + homeUnavailable() + '</div>');
h += powerEventsCard(ev);
h += powerHistoryCard(hi);
h += '</div>';
return h;
}
let powerData = null, powerEvents = null, powerHistory = null;
async function fetchPowerOnce() {
const el = $('#power-page');
if (!el || state.route !== 'power') return;
let d;
try { d = await api('GET', '/api/power'); }
catch (e) { if (el && state.route === 'power') el.innerHTML = '<div class="page" id="power-page">' + emptyState('加载失败:' + errText(e), e.code) + '</div>'; return; }
let d, ev, hi;
const r = await Promise.allSettled([api('GET', '/api/power'), api('GET', '/api/power/events'), api('GET', '/api/power/history')]);
if (r[0].status !== 'fulfilled') {
if (el && state.route === 'power') el.innerHTML = '<div class="page" id="power-page">' + emptyState('加载失败:' + errText(r[0].reason), (r[0].reason && r[0].reason.code) || '') + '</div>';
return;
}
if (state.route !== 'power') return;
el.innerHTML = renderPowerBody(d);
d = r[0].value; ev = (r[1].status === 'fulfilled') ? r[1].value : null; hi = (r[2].status === 'fulfilled') ? r[2].value : null;
if (!powerEditing) el.innerHTML = renderPowerBody(d, ev, hi);
}
function renderPower() {
stopPower();
powerEditing = false;
powerData = null; powerEvents = null; powerHistory = null;
$('#content').innerHTML = '<div class="page" id="power-page"><p class="placeholder">加载电源数据…</p></div>';
fetchPowerOnce();
powerTimer = setInterval(fetchPowerOnce, 5000);
}
function bindPower() {
$('#content').addEventListener('click', async function (e) {
const pb = e.target.closest('.pol-btn');
if (pb) {
const p = pb.dataset.pol;
const ok = await confirmDlg('切换省电档位', '确认将省电档位切换为「' + powerProfileLabel(p) + '」?(写 settings.conf power.profile,launcher/sysui 期4消费生效)', false);
if (!ok) { powerEditing = false; fetchPowerOnce(); return; }
powerEditing = false;
try { await api('POST', '/api/power/policy', { item: 'profile', value: p }); toast('已切档:' + powerProfileLabel(p)); }
catch (err) { toast('切档失败:' + errText(err)); }
fetchPowerOnce();
return;
}
const apply = e.target.closest('#pol-apply');
if (apply) {
const low = parseInt($('#pol-low') ? $('#pol-low').value : '', 10);
const crit = parseInt($('#pol-crit') ? $('#pol-crit').value : '', 10);
const bri = parseInt($('#pol-bri') ? $('#pol-bri').value : '', 10);
const scr = parseInt($('#pol-scr') ? $('#pol-scr').value : '', 10);
if (isNaN(low) || isNaN(crit)) { toast('请输入有效的低电/临界阈值'); return; }
const ok = await confirmDlg('保存电源阈值', '确认写入 低电=' + low + '% / 临界=' + crit + '% / 亮度上限=' + (isNaN(bri) ? 0 : bri) + ' / 息屏上限=' + (isNaN(scr) ? 0 : scr) + 's?', false);
if (!ok) { powerEditing = false; fetchPowerOnce(); return; }
powerEditing = false;
try {
await api('POST', '/api/power/policy', { item: 'low-pct', value: low });
await api('POST', '/api/power/policy', { item: 'crit-pct', value: crit });
await api('POST', '/api/power/policy', { item: 'bri-cap', value: bri || 0 });
await api('POST', '/api/power/policy', { item: 'scr-cap', value: scr || 0 });
toast('电源阈值已保存');
} catch (err) { toast('保存失败:' + errText(err)); }
fetchPowerOnce();
return;
}
});
$('#content').addEventListener('focusin', function (e) {
if (e.target.closest('.pol-num')) powerEditing = true; // 编辑中暂停 5s 重渲染(防输入被清空)
});
}
/* ---------------- 2f. 进程 / 服务页(#/proc;/api/svcd + /api/ams,只读,无 restart/kill) ----------------
* svcd 服务表(state/pid/restart 计数≥3 高亮/exec/来源 board|tf)+ ams App 表(state/pid/crashes/launches/last_kind);
@@ -1000,7 +1127,11 @@
'<td class="muted">' + esc(a.launches || 0) + '</td>' +
'<td class="' + ((a.crashes || 0) > 0 ? 'crash-hi' : 'muted') + '">' + (a.crashes || 0) + ((a.crashes || 0) > 0 ? ' ⚠' : '') + '</td>' +
'<td class="muted">' + esc(a.last_kind || '—') + (a.last_code != null && a.last_code !== 0 ? ' (' + esc(a.last_code) + ')' : '') + '</td>' +
'<td><div class="row-actions"><button class="btn btn-ghost" data-applog="' + esc(a.pkg) + '">' + (openHere ? '收起' : 'stdout 尾部') + '</button></div></td></tr>';
'<td><div class="row-actions">' +
'<button class="btn btn-ghost" data-applog="' + esc(a.pkg) + '">' + (openHere ? '收起' : 'stdout 尾部') + '</button>' +
'<button class="btn btn-ghost" data-amsstart="' + esc(a.pkg) + '">启动</button>' +
(a.pid > 0 ? '<button class="btn btn-ghost" data-amskill="' + esc(a.pkg) + '" data-fg="' + (a.state === 'fg' ? '1' : '0') + '">结束</button>' : '') +
'</div></td></tr>';
if (openHere) {
const lines = (expandedProc.lines || []);
h += '<tr class="proc-tail"><td colspan="8"><pre class="log-view">' + esc(lines.join('\n') || '(空)') +
@@ -1009,7 +1140,7 @@
}
});
h += '</tbody></table>';
h += '<p class="muted">点击「stdout 尾部」查看该 App 最新 stdout_*.log 末尾(崩溃尾巴)。仅只读,无 restart/kill。</p>';
h += '<p class="muted">「stdout 尾部」查看该 App 最新 stdout_*.log 末尾(崩溃尾巴)。「启动/结束」经 amsd(require_auth);结束前台 App 需二次确认。</p>';
} else h += homeUnavailable();
h += '</div></div>';
return h;
@@ -1072,9 +1203,27 @@
procTimer = setInterval(fetchProcOnce, 5000);
}
function bindProc() {
$('#content').addEventListener('click', function (e) {
const b = e.target.closest('[data-applog]'); if (!b) return;
viewAppStdout(b.dataset.applog, b);
$('#content').addEventListener('click', async function (e) {
const b = e.target.closest('[data-applog]');
if (b) { viewAppStdout(b.dataset.applog, b); return; }
const st = e.target.closest('[data-amsstart]');
if (st) {
const pkg = st.dataset.amsstart;
try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/start'); toast('已请求启动 ' + pkg); }
catch (err) { toast('启动失败:' + errText(err)); }
fetchProcOnce();
return;
}
const kl = e.target.closest('[data-amskill]');
if (kl) {
const pkg = kl.dataset.amskill, fg = kl.dataset.fg === '1';
const ok = await confirmDlg('结束 App', (fg ? '『' + pkg + '』正位于前台,' : '') + '确认结束 ' + pkg + '?(SIGTERM 语义)', true);
if (!ok) return;
try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/kill'); toast('已请求结束 ' + pkg); }
catch (err) { toast('结束失败:' + errText(err)); }
fetchProcOnce();
return;
}
});
}
@@ -1110,12 +1259,18 @@
return 'net-state warn';
}
function netStatusCards(d) {
let h = '';
// 接口卡(lo/wlan0/usb-eth:状态+IP+速率)
const ifs = (d && d.ifaces) || [];
h += '<div class="card"><h3 class="card-title">网络接口</h3><table class="table"><thead>' +
'<tr><th>接口</th><th>类型</th><th>状态</th><th>IP 地址</th><th>网关</th><th>RX / TX</th></tr></thead><tbody>';
/* 网络接口卡(/api/net/ifaces:kind/ip/gw/rssi/rfkill/dns + RX/TX)。
* enforce 执行语义如实:仅 DNS 层拒绝(SOCK_DESTROY 在本内核为 no-op),绝不谎称「已断连」。 */
function netIfaceCards(ifd) {
if (!ifd || ifd.available === false) return '<div class="card"><h3 class="card-title">网络接口</h3>' + homeUnavailable() + '</div>';
const ifs = (ifd && ifd.ifaces) || [];
let h = '<div class="card"><h3 class="card-title">网络接口</h3>';
let meta = '<div class="home-sub" style="margin-bottom:8px">';
meta += (ifd.rssi != null) ? '信号 ' + esc(ifd.rssi) + ' dBm' : '信号 —';
if (ifd.dns && ifd.dns.length) meta += ' · DNS ' + esc(ifd.dns.join(' / '));
meta += '</div>';
h += meta;
h += '<table class="table"><thead><tr><th>接口</th><th>类型</th><th>状态</th><th>IP 地址</th><th>网关</th><th>RX / TX</th></tr></thead><tbody>';
if (!ifs.length) h += '<tr><td colspan="6" class="empty">无接口数据</td></tr>';
ifs.forEach(function (f) {
const up = !!f.up;
@@ -1125,9 +1280,20 @@
'<td class="muted">' + fmtBytes(f.rx_bytes) + ' / ' + fmtBytes(f.tx_bytes) + '</td></tr>';
});
h += '</tbody></table>';
if (d && d.rate && (d.rate.rx_bps != null || d.rate.tx_bps != null))
h += '<p class="muted">实时速率:↓ ' + fmtBytes(d.rate.rx_bps) + '/s · ↑ ' + fmtBytes(d.rate.tx_bps) + '/s</p>';
const rf = (ifd && ifd.rfkill) || [];
if (rf.length) {
h += '<div class="rfkill-row">';
rf.forEach(r => h += '<span class="home-chip ' + (r.soft || r.hard ? 'warn' : 'ok') + '">' + esc(r.type) + (r.soft ? ' 软关' : '') + (r.hard ? ' 硬关' : '') + '</span>');
h += '</div>';
}
h += '</div>';
return h;
}
/* WiFi / 蓝牙摘要(/api/net/status:E0 概览,含实时速率)。接口细分已迁 netIfaceCards。 */
function netStatusCards(d) {
let h = '';
if (d && d.rate && (d.rate.rx_bps != null || d.rate.tx_bps != null))
h += '<div class="card"><p class="muted">实时速率:↓ ' + fmtBytes(d.rate.rx_bps) + '/s · ↑ ' + fmtBytes(d.rate.tx_bps) + '/s</p></div>';
// WiFi 摘要
const w = (d && d.wifi) || {};
h += '<div class="card"><h3 class="card-title">WiFi</h3><table class="table"><tbody>';
@@ -1155,9 +1321,50 @@
return h;
}
/* 网络策略卡(/api/net/policy;写走 POST /api/net/policy,require_auth + 二次确认)。
* enforce 如实显示(dns_only):仅 DNS 层拒绝,不会真断连。 */
function netPolicyCard(pol) {
if (!pol || pol.available === false) return '<div class="card"><h3 class="card-title">网络策略</h3>' + homeUnavailable() + '</div>';
const enforce = pol.enforce || '';
const dnsOnly = enforce === 'dns_only';
let h = '<div class="card"><h3 class="card-title">网络策略</h3>';
h += '<div class="page-banner" style="margin-bottom:10px"><span class="ico">' + (dnsOnly ? 'ℹ️' : '⚠️') + '</span><span>' +
(dnsOnly ? '执行模式:仅 DNS 层拒绝(enforce=dns_only;本内核 SOCK_DESTROY 为 no-op,不会真正断连)' : '执行模式:' + esc(enforce)) + '</span></div>';
const rules = pol.rules || [];
if (!rules.length) h += '<div class="empty">暂无策略规则(默认放行)</div>';
else {
h += '<table class="table"><thead><tr><th>实体</th><th>网络</th><th>白名单域名</th><th>黑名单域名</th><th style="text-align:right">操作</th></tr></thead><tbody>';
rules.forEach(r => {
h += '<tr><td class="mono">' + netBadge(r.entity) + '</td>' +
'<td><span class="home-chip ' + (r.net === 'block' ? 'bad' : 'ok') + '">' + (r.net === 'block' ? '阻断' : '放行') + '</span></td>' +
'<td class="muted">' + esc((r.allow_hosts || []).join(', ') || '—') + '</td>' +
'<td class="muted">' + esc((r.deny_hosts || []).join(', ') || '—') + '</td>' +
'<td style="text-align:right"><div class="row-actions">' +
'<button class="btn btn-ghost" data-ptoggle="' + esc(r.entity) + '" data-net="' + (r.net === 'block' ? 'allow' : 'block') + '">' + (r.net === 'block' ? '改放行' : '阻断') + '</button>' +
'<button class="btn btn-ghost" data-pdel="' + esc(r.entity) + '">删除</button></div></td></tr>';
});
h += '</tbody></table>';
}
h += '<div class="field-row" style="margin-top:12px"><input type="text" id="pol-entity" placeholder="app:os.meow.ncm / svc:x / proc:y" style="max-width:280px">' +
'<button class="btn btn-primary" id="pol-block">设为阻断</button>' +
'<button class="btn btn-ghost" id="pol-allow">设为放行</button></div>';
h += '<div class="field-row" style="margin-top:8px"><input type="text" id="pol-domain" placeholder="域名(+白 / -黑,如 +music.example.com)" style="max-width:280px">' +
'<button class="btn" id="pol-dom">添加域名规则</button></div>';
h += '<p class="muted">防自锁实体(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)拒 block。</p>';
h += '</div>';
return h;
}
function netConnsBody(cn) {
const list = (cn && cn.conns) || [];
let h = '';
/* 连接 top 摘要(聚合计数,轻量——overview 与网速页的观察摘要,非完整归因)。 */
const entCnt = {};
(list || []).forEach(c => { entCnt[c.entity] = (entCnt[c.entity] || 0) + 1; });
const top = Object.keys(entCnt).sort((a, b) => entCnt[b] - entCnt[a]).slice(0, 3);
if (top.length) {
h += '<div class="home-sub" style="margin-bottom:8px">连接 top:' + top.map(e => '<span class="net-entity">' + netBadge(e) + ' ×' + entCnt[e] + '</span>').join(' · ') + '(共 ' + list.length + ' 条)</div>';
}
if (!list.length) h += '<div class="empty">暂无连接' + (netEntityFilter ? '(该实体)' : '') + '</div>';
else {
h += '<table class="table"><thead><tr><th>实体</th><th>远端(域名)</th><th>本端</th><th>状态</th><th>寿命</th></tr></thead><tbody>';
@@ -1202,8 +1409,9 @@
let netLoaded = false; // 是否已成功加载过一次网络状态(用于「首次失败只显示横幅 + 占位」语义)
async function fetchNetOnce() {
if (state.route !== 'net') return;
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body');
let st, cn, dn;
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body'),
ifEl = $('#net-ifaces'), poEl = $('#net-policy');
let st, cn, dn, ifd, pol;
try { st = await api('GET', '/api/net/status'); }
catch (e) { netBannerErr(errText(e)); return; }
netLoaded = true;
@@ -1212,8 +1420,12 @@
catch (e) { cn = null; }
try { dn = await api('GET', '/api/net/dns'); }
catch (e) { dn = null; }
try { ifd = await api('GET', '/api/net/ifaces'); } catch (e) { ifd = null; }
try { pol = await api('GET', '/api/net/policy'); } catch (e) { pol = null; }
if (state.route !== 'net') return;
if (ifEl) ifEl.innerHTML = netIfaceCards(ifd);
if (stEl) stEl.innerHTML = netStatusCards(st);
if (poEl) poEl.innerHTML = netPolicyCard(pol);
if (cbEl) cbEl.innerHTML = netConnsBody(cn);
if (dbEl) dbEl.innerHTML = netDnsBody(dn);
}
@@ -1250,7 +1462,9 @@
$('#content').innerHTML =
'<div class="page" id="net-page">' +
'<div id="net-banner" class="page-banner" hidden></div>' +
'<div id="net-ifaces"><p class="placeholder">加载网络接口…</p></div>' +
'<div id="net-status"><p class="placeholder">加载网络状态…</p></div>' +
'<div id="net-policy"><p class="placeholder">加载网络策略…</p></div>' +
'<div class="card"><h3 class="card-title">每实体连接表</h3>' +
'<div class="field-row" style="margin-bottom:12px">' +
'<input type="text" id="net-entity-filter" placeholder="筛选实体(app: / svc: / proc:)" style="max-width:280px" value="' + esc(netEntityFilter) + '">' +
@@ -1290,6 +1504,60 @@
flush.disabled = false;
fetchNetOnce();
}
/* ---- 网络策略写(require_auth + 二次确认;enforce=dns_only 如实显示,不谎称断连) ---- */
const tgl = e.target.closest('[data-ptoggle]');
if (tgl) {
const ent = tgl.dataset.ptoggle, net = tgl.dataset.net;
const ok = await confirmDlg('网络策略', '确认将 ' + ent + ' 网络策略设为「' + (net === 'block' ? '阻断(DNS 层拒绝,不会真断连)' : '放行') + '」?', net === 'block');
if (!ok) { fetchNetOnce(); return; }
try { await api('POST', '/api/net/policy', { op: 'set_net', entity: ent, arg: net }); toast('已更新策略:' + ent); }
catch (err) { toast('策略更新失败:' + errText(err)); }
fetchNetOnce();
return;
}
const del = e.target.closest('[data-pdel]');
if (del) {
const ent = del.dataset.pdel;
const ok = await confirmDlg('删除策略', '确认删除 ' + ent + ' 的全部策略规则(恢复默认放行)?', true);
if (!ok) { fetchNetOnce(); return; }
try { await api('POST', '/api/net/policy', { op: 'del', entity: ent }); toast('已删除策略:' + ent); }
catch (err) { toast('删除失败:' + errText(err)); }
fetchNetOnce();
return;
}
const blk = e.target.closest('#pol-block');
if (blk) {
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
if (!ent) { toast('请输入实体(app:xxx / svc:xxx / proc:xxx)'); return; }
const ok = await confirmDlg('阻断实体', '确认将 ' + ent + ' 设为阻断(DNS 层拒绝,不会真断连)?', true);
if (!ok) { fetchNetOnce(); return; }
try { await api('POST', '/api/net/policy', { op: 'set_net', entity: ent, arg: 'block' }); toast('已阻断:' + ent); }
catch (err) { toast('阻断失败:' + errText(err)); }
fetchNetOnce();
return;
}
const alw = e.target.closest('#pol-allow');
if (alw) {
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
if (!ent) { toast('请输入实体(app:xxx / svc:xxx / proc:xxx)'); return; }
try { await api('POST', '/api/net/policy', { op: 'set_net', entity: ent, arg: 'allow' }); toast('已放行:' + ent); }
catch (err) { toast('放行失败:' + errText(err)); }
fetchNetOnce();
return;
}
const dom = e.target.closest('#pol-dom');
if (dom) {
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
const hst = ($('#pol-domain') ? $('#pol-domain').value.trim() : '');
if (!ent) { toast('请输入实体'); return; }
if (!hst || (hst[0] !== '+' && hst[0] !== '-')) { toast('域名须以 +(白)或 -(黑)开头'); return; }
const ok = await confirmDlg('域名规则', '确认对 ' + ent + ' ' + (hst[0] === '+' ? '加入白名单' : '加入黑名单') + ' ' + hst.slice(1) + '?', hst[0] === '-');
if (!ok) { fetchNetOnce(); return; }
try { await api('POST', '/api/net/policy', { op: 'set_domain', entity: ent, arg: hst }); toast('域名规则已更新'); }
catch (err) { toast('域名规则失败:' + errText(err)); }
fetchNetOnce();
return;
}
});
}
@@ -1852,6 +2120,66 @@
catch (e) { toast('保存失败:' + errText(e)); }
}, 350);
/* ---------------- 6b. 远程控制页滑动手势(设备端靠触摸滑动操作,控制页据此补滑动手势) ----------------
* 说明:后端 /api/input/tap 仅支持「单点坐标注入」(屏幕页用),无滑动/手势注入端点;
* 故控制页滑动统一映射到 /api/key/:code 语义键(与本地按键行为一致),多步音量 = 逐键 tap 多次。 */
const SWIPE = {
minDist: 40, // 判定为「滑动」的最小位移(px),小于此视为点按
axisRatio: 1.5, // 主轴位移须至少为次轴的倍数,防斜滑误判
edgePx: 56, // 距控制区左缘多少 px 内起手即视为「左缘滑」
maxVolSteps: 5, // 单次滑动最多递增音量步数(防连刷冲上限)
volStepPx: 70, // 每多滑这么多位移,多算一步音量
clickSuppressMs: 450 // 滑动后短暂屏蔽按钮点击,防误触
};
let swipeState = null; // 当前手势 {id,x0,y0,edge}
let lastSwipeAt = 0; // 上次触发手势的时刻,用于屏蔽随后可能的 click
async function sendKeyLabel(code, label) {
try { await api('POST', '/api/key/' + code, { press: 'tap' }); toast(label + ' 已发送'); }
catch (e) { toast('发送失败:' + errText(e)); }
}
async function sendKeyRepeat(code, n, label) {
let count = 0, err = null;
for (let i = 0; i < n; i++) {
try { await api('POST', '/api/key/' + code, { press: 'tap' }); count++; }
catch (e) { err = errText(e); break; } // 失败即停,避免连刷
}
if (count > 0) toast(label + ' ×' + count);
if (err) toast('发送失败:' + err);
}
// 依据滑动主轴位移换算音量步数(>=1,封顶 maxVolSteps)
function volSteps(ay) {
return Math.max(1, Math.min(SWIPE.maxVolSteps, Math.round(ay / SWIPE.volStepPx)));
}
function handleRemoteGesture(dir, ax, ay) {
if (dir === 'volup') sendKeyRepeat(115, volSteps(ay), '音量+');
else if (dir === 'voldown') sendKeyRepeat(114, volSteps(ay), '音量−');
else if (dir === 'next') sendKeyLabel(163, '下一曲');
else if (dir === 'prev') sendKeyLabel(165, '上一曲');
else if (dir === 'back') sendKeyLabel(158, '返回');
}
function remotePointerDown(e) {
if (state.route !== 'remote' || swipeState) return; // 已有一指在手 → 忽略多点触控
if (!e.target.closest('#remote-page')) return;
const page = $('#remote-page'); if (!page) return;
const r = page.getBoundingClientRect();
swipeState = { id: e.pointerId, x0: e.clientX, y0: e.clientY, edge: (e.clientX - r.left) <= SWIPE.edgePx };
}
function remotePointerUp(e) {
const s = swipeState;
if (!s || e.pointerId !== s.id) return;
swipeState = null;
const dx = e.clientX - s.x0, dy = e.clientY - s.y0;
const ax = Math.abs(dx), ay = Math.abs(dy);
if (Math.max(ax, ay) < SWIPE.minDist) return; // 轻点/未动 → 交给按钮 click 处理
let dir = null;
if (s.edge && dx > 0 && dx >= ay * SWIPE.axisRatio && dx >= SWIPE.minDist) dir = 'back'; // 左缘滑 → 返回
else if (ax >= ay * SWIPE.axisRatio) dir = (dx < 0 ? 'next' : 'prev'); // 水平 → 上下曲
else if (ay >= ax * SWIPE.axisRatio) dir = (dy < 0 ? 'volup' : 'voldown'); // 垂直 → 音量
if (!dir) return; // 斜滑/未达占优 → 忽略
lastSwipeAt = Date.now();
handleRemoteGesture(dir, ax, ay);
}
/* ---------------- 6. 远程控制页 ---------------- */
function renderRemote() {
const keys = [
@@ -1864,12 +2192,16 @@
{ code: 158, label: '返回', ico: '↩' },
{ code: 172, label: '主页', ico: '⌂' }
];
let h = '<div class="page" id="remote-page">';
let h = '<div class="page" id="remote-page" style="touch-action:none">';
h += '<div class="card"><h3 class="card-title">模拟按键</h3>' +
'<p class="muted">行为与本地按键一致(设备端系统统一接管)。长按电源将触发电源菜单。</p>' +
'<div class="key-grid">' + keys.map(k =>
'<button class="key-btn" data-code="' + k.code + '"><span class="k-ico">' + k.ico + '</span>' + esc(k.label) + '</button>'
).join('') + '</div></div>';
).join('') + '</div>' +
'<p class="muted" style="margin-top:12px;line-height:1.9"><b>滑动手势</b>(与按钮并存,轻点仍是点击):' +
'上滑=音量+ · 下滑=音量-(滑得越远步数越多,单次至多 ' + SWIPE.maxVolSteps + ' 步) · ' +
'左滑=下一曲 · 右滑=上一曲 · 控制区左缘右滑=返回</p>' +
'</div>';
h += '<div class="card"><h3 class="card-title">电源长按(电源菜单)</h3>' +
'<div class="field-row" style="gap:10px"><button class="btn btn-primary" data-powerlong="1">长按电源 3s(电源菜单)</button>' +
'<span class="muted">发送 code=116 · press=long · ms=3000</span></div></div>';
@@ -1882,9 +2214,14 @@
}
function bindRemote() {
$('#content').addEventListener('click', function (e) {
if (Date.now() - lastSwipeAt < SWIPE.clickSuppressMs) return; // 刚滑完 → 屏蔽本次点击,防误触按钮
const kb = e.target.closest('[data-code]'); if (kb) { sendKey(kb.dataset.code, 'tap'); return; }
const pl = e.target.closest('[data-powerlong]'); if (pl) { sendKey(116, 'long', 3000); return; }
});
// 滑动手势:指针事件统一触摸/鼠标;#remote-page 已设 touch-action:none 防浏览器滚动劫持
$('#content').addEventListener('pointerdown', remotePointerDown);
window.addEventListener('pointerup', remotePointerUp);
window.addEventListener('pointercancel', remotePointerUp);
}
/* ---------------- 7. 系统信息 / OTA 页 ---------------- */
@@ -2328,7 +2665,7 @@
system: { title: '系统信息 / 升级', render: renderSystem, bind: bindSystem },
logs: { title: '系统日志', render: renderLogs, bind: bindLogs },
notif: { title: '通知中心', render: renderNotif, bind: bindNotif },
power: { title: '电源 / 功耗', render: renderPower, bind: null },
power: { title: '电源 / 功耗', render: renderPower, bind: bindPower },
proc: { title: '进程 / 服务', render: renderProc, bind: bindProc }
};
// 一次性绑定(各菜单 click / 侧栏);页面内交互用 #content 事件委托(已各自绑定)
Binary file not shown.
BIN
View File
Binary file not shown.
+14 -1
View File
@@ -1 +1,14 @@
{"name":"流媒体客户端","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm","icon":"icon.png","package":"dev.player.ncm","version":"0.1","version_code":1,"framework":1,"immersive":1,"fonts":"common","author":"MeowOS","background_audio":1}
{
"name":"流媒体客户端",
"exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm",
"icon":"icon.png",
"package":"dev.player.ncm",
"version":"0.1",
"version_code":1,
"framework":1,
"immersive":1,
"fonts":"common",
"author":"MeowOS",
"background_audio":1,
"awake.player":1
}
+29 -1
View File
@@ -18,6 +18,7 @@
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
.state = NCM_PC_STOPPED, .cur_song = -1 };
static int g_reported_act = -1; /* 任务4 keep_awake:已上报 launcher 的顶层 activity(差分防重发) */
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 0;
@@ -103,7 +104,15 @@ static void app_on_swipe(int dir, int dx, int dy)
if (settings_is_active()) { meow_view_page_pop(); return; }
if (playlist_is_active()) { meow_view_page_pop(); return; }
if (search_is_active()) { meow_view_page_pop(); return; }
/* home / login = 根 → 退出 */
if (home_is_active()) {
/* 任务2:home 根页左滑不再退出进程 → 请求 launcher 退到桌面/后台(进程+媒体会话保留)。
* launcher M4 据 app.json background_audio:1 + 媒体会话 PLAYING 判保活;不保活才真终止。
* 这里**不**置 g_app_quit / 不 meow_ui_request_exit——有歌在播则后台继续播(§M4)。 */
meow_log(MEOW_LOG_INFO, "swipe home -> background (nav_exit)");
ipc_media_nav_exit();
return;
}
/* login 根(未登录):退出 */
meow_log(MEOW_LOG_INFO, "exit:swipe");
meow_ui_request_exit();
return;
@@ -139,6 +148,15 @@ static void app_on_swipe_progress(int x_start, int dx)
static void app_on_tick(void)
{
/* 任务4 keep_awake:顶层 activity 变化 → 上报 launcher(MSG_ACT_FOCUS,差分一次)。
* 页面切统一走 meow_view_page_push/pop/replace(更新 meow_ui_activity_active),此处差分上报:
* 进播放页 active=ACT_PLAYER → ipc_act_report("player") → launcher fg_keep_awake 判常亮;
* 退出播放页(弹回 home/playlist)→ 上报对应活动名(未声明 awake)→ 相机照常超时息屏。
* 只在**当前顶层**变化时发一次(无 IPC 风暴;播放中 active 不变不重发)。 */
{
int act = meow_ui_activity_active();
if (act != g_reported_act) { g_reported_act = act; ipc_act_report(act); }
}
uint64_t now = meow_ui_time_ms();
/* §NEW cookie 续期判会话失效 → 清登录态回登录页(worker 在后台续期发现 301/-460 时置标志;
* 在此 UI 线程执行 logout+切页——meow_view_page_replace 非线程安全,只能 UI 线程做) */
@@ -154,6 +172,16 @@ static void app_on_tick(void)
int ai = autoplay_ui_consume();
if (ai >= 0) player_play_song(ai);
}
/* 任务3:通知中心媒体卡片点卡片体(launcher 发 NCM_CTL_FOCUS)→ 回到播放页。
* ipc_media 线程只置标志(跨线程 page 导航非线程安全),UI 线程消费后 push ACT_PLAYER;
* 无在播曲则忽略(聚焦无意义)。 */
if (ipc_media_focus_consume()) {
if (player_manager_state() != NCM_PC_STOPPED &&
meow_ui_activity_active() != ACT_PLAYER) {
meow_log(MEOW_LOG_INFO, "ipc: focus -> player page");
meow_view_page_push(ACT_PLAYER);
}
}
if (login_is_active()) { login_tick(now); return; }
if (player_is_active()) { player_tick(now); return; }
if (search_is_active()) { search_tick(now); return; }
+8 -7
View File
@@ -88,14 +88,14 @@ void account_load(void)
int64_t last_refresh = ncm_json_get_i64_at(&j, ncm_json_path(&j, "last_refresh_ts"), &rc);
if (rc != NCM_OK) last_refresh = 0;
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SVIP) vip = NCM_VIP_NONE;
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SUPER) vip = NCM_VIP_NONE;
g_st.vip = (int)vip;
g_st.vip_cvip_exp_ms = cvip_exp;
g_st.vip_svip_exp_ms = svip_exp;
g_st.vip_checked_at = checked;
g_st.cookie_last_refresh_ts = last_refresh;
/* 有效到期时间 = 当前档对应的到期字段 */
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP) ? svip_exp
/* 有效到期时间 = 当前档对应的到期字段(SUPER 无独立到期字段,沿黑胶 svip 场)。 */
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP || vip == NCM_VIP_SUPER) ? svip_exp
: (vip == NCM_VIP_CVIP) ? cvip_exp : 0;
meow_log(MEOW_LOG_INFO, "account: loaded vip=%d expire=%lld last_refresh_ts=%lld",
g_st.vip, (long long)g_st.vip_expire_ms, (long long)last_refresh);
@@ -129,9 +129,10 @@ int account_vip(void)
int account_level_cap(void)
{
switch (account_vip()) {
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
default: return NCM_LEVEL_EXHIGH;
case NCM_VIP_SVIP:
case NCM_VIP_SUPER: return NCM_LEVEL_SURROUND;
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
default: return NCM_LEVEL_EXHIGH;
}
}
@@ -164,7 +165,7 @@ void account_vip_maybe_refresh(int force)
g_st.vip = info.vip;
g_st.vip_cvip_exp_ms = info.cvip_expire_ms;
g_st.vip_svip_exp_ms = info.svip_expire_ms;
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP) ? info.svip_expire_ms
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP || info.vip == NCM_VIP_SUPER) ? info.svip_expire_ms
: (info.vip == NCM_VIP_CVIP) ? info.cvip_expire_ms : 0;
g_st.vip_checked_at = now_s;
account_persist(info.vip, info.cvip_expire_ms, info.svip_expire_ms, now_s);
+31 -9
View File
@@ -278,8 +278,11 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。
* 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm
* → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。
* §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。 */
static void save_cover_raw(const char *path, size_t len)
* §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。
* REPORT §封面混用:is_song=0(歌单封面,无曲目 id)时**只写 cover_current.jpg 镜像、不更新
* g_cover_path**——g_cover_path 是上报 launcher 通知中心媒体卡片的「当前播放曲」封面指针;
* 歌单页浏览加载歌单封面不应覆盖正在播放曲目的封面(用户真机:通知中心显示另一歌单封面)。 */
static void save_cover_raw(const char *path, size_t len, int is_song)
{
if (!path || len == 0) return;
const char *covdir = g_st.ctx.cache.covers_dir[0] ? g_st.ctx.cache.covers_dir
@@ -301,17 +304,20 @@ static void save_cover_raw(const char *path, size_t len)
}
fclose(in);
}
ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
if (is_song) ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
}
}
#define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */
/* 内存瘦身:封面解码后按显示上限降采样驻留。网易云全尺寸封面(如 1448×1448)解码成
* RGBA = 8MB,而应用显示只需 ≤480px(播放页黑胶方图+歌词页模糊铺底均 ≤ 屏幕 480 宽)——
* 缩到 480×480 = 0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀,最高优先级)。
* 若已 ≤上限(小缩略图)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
#define NCM_COVER_DECODE_MAX 480
* RGBA = 8MB,而应用显示(歌词页 cover-fit 铺满 480×720 全屏,方图实际渲染到 720×720;封面页
* 方图仅 304×304)——请求 param=640y640(见 ncm_app_internal.h)即 ≤640 边长,故此上限须 ≥ 640
* 否则解码后又被降采样回 480、仍不够铺满 480 宽(480 图铺 480+ 显示照旧放大)。640×640 RGBA
* ≈ 1.6MB 单张驻留(设备 RAM 仅 56MB,OOM 真机被杀;比 308 瘦身 8.28MB 仍省 6.7MB,单张 1.6MB
* 在 32bit OOM 预算内——换封面仍先 free 旧再解码)。
* 若已 ≤上限(小缩略图/无 param 全尺寸在 640 内)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
#define NCM_COVER_DECODE_MAX 640
static meow_img *cover_cap(meow_img *im)
{
if (!im) return NULL;
@@ -362,7 +368,7 @@ static void *cover_worker(void *arg)
img = download_cover(url, w, &blen, &drc, &dstatus, &dkind);
if (img) {
pthread_mutex_lock(&g_cm);
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
save_cover_raw(ncm_cache_writer_tmp_path(w), blen, song[0] ? 1 : 0);
pthread_mutex_unlock(&g_cm);
if (ncm_cache_commit(w) != 0) { ncm_cache_abort(w); }
} else {
@@ -452,6 +458,21 @@ static void *cover_worker(void *arg)
if (g_cover_img) meow_img_free(g_cover_img);
g_cover_img = img;
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
/* 上报给 launcher 的封面路径 = 本目标专辑的缓存条目(per-URL 唯一)。
* 旧码只在 save_cover_raw(下载路径)更新 g_cover_path;缓存命中路径从不更新 →
* cover_current_path() 恒空 → MSG_META artwork 恒空 → 通知中心媒体卡拿不到封面路径
* → 回退显示歌名。此处统一在「目标达成换入」时把 g_cover_path 指向已落盘的缓存条目
* (下载 commit 后 / 缓存命中时均真实存在);commit 失败则保留 save_cover_raw 已写的
* cover_current.jpg 镜像(仍在)。路径随专辑变化 → launcher 侧「按 artwork 文本缓存」
* 的现有实现能随切歌重载,不残留旧封面。 */
if (song[0] && g_st.ctx.cache.dir[0]) {
char cpath[512];
if (ncm_cache_path(&g_st.ctx.cache, key, ".jpg", cpath, sizeof cpath) == NCM_OK) {
struct stat cs;
if (stat(cpath, &cs) == 0 && cs.st_size > 0)
ncm_strlcpy(g_cover_path, cpath, sizeof g_cover_path);
}
}
g_gen++;
g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */
} else {
@@ -464,7 +485,8 @@ static void *cover_worker(void *arg)
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。song_id:曲目 id(专辑封面死链时走
* song/detail 兜底重取,见 REPORT §NEW 任务1);NULL/空=歌单封面(无兜底)。
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
* 先 cover_norm_url 规范化(追加 ?param=640y640 缩略后缀,REPORT §NEW 任务2;640 适配歌词页
* cover-fit 铺满 480×720,见 ncm_app_internal.h),使
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
void cover_request(const char *url, const char *song_id)
{
+41 -9
View File
@@ -50,6 +50,41 @@ static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
dst[oi] = 0;
}
/* 解析单个 [mm:ss.ff] 时间标签为毫秒。返回 1=合法时间标签(*out_ms、*next=']' 之后);
* 0=非时间标签(*next=']' 之后;NULL=无闭合 ']',调用方按残缺处理)。
* 小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
* 网易云 LRC 毫秒)——此前固定 ×10 把网易云 3 位毫秒当 2 位厘秒解析,时间戳放大 ~10 倍,
* 歌词行按绝对时间激活滞后 ~4~10s(REPORT「英文歌歌词滚动慢」)。 */
static int parse_lrc_tag(const char *p, long *out_ms, const char **next)
{
const char *q = p + 1; /* 跳过 '[' */
int m = 0, md = 0, s = 0, sd = 0;
while (*q >= '0' && *q <= '9') { m = m * 10 + (*q - '0'); md++; q++; }
if (md == 0 || *q != ':') goto notime;
q++;
while (*q >= '0' && *q <= '9') { s = s * 10 + (*q - '0'); sd++; q++; }
if (sd == 0) goto notime;
long frac = 0; int fd = 0; long scale = 10;
if (*q == '.') {
q++;
while (*q >= '0' && *q <= '9') { frac = frac * 10 + (*q - '0'); fd++; q++; }
if (fd == 1) scale = 100;
else if (fd == 2) scale = 10;
else if (fd >= 3) { scale = 1; while (fd > 3) { frac /= 10; fd--; } } /* >3 位截断到 ms */
else scale = 0; /* 孤立 '.' 无数字:忽略小数 */
}
if (*q != ']') goto notime;
*out_ms = (long)m * 60000L + (long)s * 1000L + frac * scale;
*next = q + 1;
return 1;
notime:
{
const char *r = strchr(p, ']');
*next = r ? r + 1 : NULL;
return 0;
}
}
static void lrc_parse(const char *lrc, int is_trans)
{
if (!lrc) return;
@@ -62,19 +97,16 @@ static void lrc_parse(const char *lrc, int is_trans)
int have = 0;
long first_ms = 0;
while (*p == '[') {
int m = 0, s = 0, cs = 0;
if (sscanf(p, "[%d:%d.%d]", &m, &s, &cs) >= 2) {
long ms = (long)m * 60000L + (long)s * 1000L + (long)cs * 10L;
long ms;
const char *after;
if (parse_lrc_tag(p, &ms, &after)) {
if (!have) { first_ms = ms; have = 1; }
/* 跳到本标签结尾 */
const char *q = strchr(p, ']');
if (!q) break;
p = q + 1;
p = after;
} else {
/* 非时间标签([ti:..] 等)——跳过到行尾 */
const char *q = strchr(p, ']');
if (!q) return;
p = q + 1;
if (!after) return; /* 无闭合 ']':残缺文件 */
p = after;
/* 若行内还有时间标签继续解析(极少),否则整行的正文以第一个文本为准 */
break;
}
+26 -4
View File
@@ -32,11 +32,19 @@
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
* 缩略 JPEG——省内存(300×300≈20KB vs 全尺寸 1.34MB)+解 webp 三分型③+命中率高;已含 param= 的
* URL 尊重原样)。共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
* 同源,避免两处字符串漂移。 */
* 缩略 JPEG——省内存 +解 webp 三分型③+命中率高;已含 param= 的 URL 尊重原样)。
* 共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
* 同源,避免两处字符串漂移。
* 值选择(封面分辨率适配歌词页铺满,REPORT:歌词页 cover-fit 铺满 480×720 全屏,方图实际渲染到
* 720×720;旧默认 300y300 → 300 放大到 720 = 2.4× 最近邻放大 → 锐锯齿):
* 640y640 → 歌词页 640→720 = 1.125× 轻微放大(铺满基本无锯齿),封面页方图 640→304 裁方降采样
* 依旧锐利;解码 RGBA ≈ 640×640×4 ≈ 1.6MB 单张驻留(比 300 的 0.36MB +1.2MB anon,
* 仍远低于 308 瘦身前 8.28MB;32bit OOM 已由 meow_cachegov 治理 page cache,封面 anon
* 单张 1.6MB 在预算内——平衡清晰度与内存纪律)。
* 注意 cover.c 的 NCM_COVER_DECODE_MAX 必须 ≥ 本参数边长,否则解码后仍被降采样回 480(不够铺
* 满 480 宽 → 480 图铺 480+ 显示仍放大)。 */
#define NCM_COVER_USE_PARAM 1
#define NCM_COVER_PARAM "300y300"
#define NCM_COVER_PARAM "640y640"
/* ---- 布局 ---- */
#define APP_W 480
@@ -120,6 +128,9 @@ enum {
NCM_CTL_NEXT = 4,
NCM_CTL_SEEK = 5,
NCM_CTL_TOGGLE = 6, /* 播放/暂停切换(物理键 164 恒发此;按当前态翻转) */
NCM_CTL_FOCUS = 7, /* launcher 通知中心媒体卡片点卡片体 → 请求 App 回到播放页
(任务3 §re-entry:launcher 侧点卡片体发本 cmd;App 置标志,
UI 线程消费后导航 ACT_PLAYER,进程已后台保活则直接聚焦) */
};
/* 跨线程播放运行时状态(main 只读展示;play 线程写,IPC 线程读)。 */
@@ -142,6 +153,7 @@ typedef struct ncm_play_run {
int underruns; /* 本曲 read_cb 追上下载的等待次数 */
uint64_t underrun_ms; /* 本曲 read_cb 累计等待 ms */
int stalled; /* 1=下载停滞(单次等待>1s,播放线程已对 audiod pause;UI 缓冲菊花) */
int refaults; /* 本曲 DONTNEED 丢页重读 TF 次数(§2.3 取证) */
/* 播放曲快照(REPORT §17):run_song 起播时把歌曲对象整体拷入(mu 保护),
* 之后播放/歌词/IPC/UI 全读快照而非窗口——窗口滑出当前播放曲也不影响播放。 */
ncm_song_t play_song;
@@ -481,6 +493,9 @@ int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功;bi
int audio_out_write(const void *samples, int frames); /* <0=错误/结束;样本位宽=open 的 bits */
int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢复(流死自动重开);非 AUDIOD 后端恒 0 */
void audio_out_close(void);
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
audio_out_open SINK_AUDIOD 成功后已自动调;独立导出供宿主验证与复调。 */
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
int audio_out_buffered_ms(void);
@@ -515,6 +530,13 @@ int songmeta_save(const char *cache_dir, const char *song_id, const ncm_song_t
/* ipc_media:媒体会话上报/收指令 */
int ipc_media_start(void); /* 起 IPC 线程;0=成功 */
void ipc_media_stop(void);
int ipc_media_nav_exit(void); /* 任务2:发 MSG_NAV{EXIT} 给 launcher 请求退后台(进程+媒体会话保留;
launcher M4 据 background_audio+PLAYING 判保活)。0=发出/-1=失败。 */
int ipc_media_focus_consume(void); /* 任务3:UI 线程消费「回播放页」标志(0=无;1=已消费一次) */
int ipc_act_report(int act); /* 任务4 keep_awake:向 launcher 上报当前顶层 activity(MSG_ACT_FOCUS)。
act=ACT_* 值,映射 "login/home/playlist/player/settings/search"(对齐
app.json awake.<act> 键);launcher fg_keep_awake 依声明判常亮。
0=发出/-1=无 socket 或写失败。 */
/* ---- 宿主测试面(app/test/test_ncm_app.c 驱动真实页面)---- */
void app_host_set_mock(int mode); /* 0=无 1=home 2=playlist 3=player 4=settings 5=login */
+111 -1
View File
@@ -22,13 +22,15 @@
* (无声排查的永久资产,勿省)。
*/
#include "../ncm_app_internal.h"
#include "meow_ipc.h" /* AUDIO_FMT_S16 */
#include "meow_ipc.h" /* AUDIO_FMT_S16 / MSG_AUDIO_VOLUME / MEOW_IPC_HDR_LEN */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#ifdef __linux__
#include <sound/asound.h>
@@ -143,6 +145,111 @@ static int file_open(int rate, int ch, int bits)
return 0;
}
/* ============================================================ */
/* 任务1:开播前把「系统当前音量」同步下发到 audiod */
/* ============================================================ */
/* 背景(用户实测 2026-09-02):未开始播放时音量不是系统设定值;按一下音量键才跳到设定值。
* 根因= ncm 启动并打开 audiod/DAC 时,没有把系统当前音量下发到 audiod(meow_audiod 只在
* MSG_AUDIO_VOLUME 收到时才写 DAC,流开/重配上不主动应用 settings 音量),导致 DAC 停在
* 默认/上次残留音量;用户按音量键时 launcher/watchdog 那条音量链才更新并下发到 audiod。
* 修法= SINK_AUDIOD open 成功后读 settings.conf 的 audio.volume,经 MSG_AUDIO_VOLUME 下发
* (audiod 写 DAC numid=1/2 + 持久化 settings,同源同键)。语意:CS43198 寄存器=衰减量
* (0 最响/255 静音),audiod dac_vol_set 负责反转与真静音(写 255),本处只传 pct。
* 宿主验证:fake audiod socket 收帧断言 pct==系统设定(bench/vol_sync_sim.c)。 */
#define NCM_SETTINGS_CONF "/usr/data/meow/settings.conf" /* 与 audiod SETTINGS_CONF 同源 */
#define NCM_AUDIO_SOCK_DEF "/usr/data/meow_audio.sock" /* 与 audiod / SDK 客户端同缺省 */
#define NCM_VOL_DEFAULT 60 /* 与 audiod VOL_DEFAULT/SDK 缺省一致 */
#define NCM_VOL_MAX 100
static const char *settings_conf_path(void)
{
const char *e = getenv("MEOW_SETTINGS_FILE"); /* 宿主单测指向临时文件;设备缺省系统 conf */
return (e && e[0]) ? e : NCM_SETTINGS_CONF;
}
/* 读 settings.conf 的 audio.volume(扁平 "key = value";兼容无空格/多空格)。
* 与 launcher meow_settings / SDK meow_ui_vol_read 同款解析。缺省 60;夹到 [0,100]。 */
static int settings_read_volume(void)
{
static const char key[] = "audio.volume";
const int kl = (int)sizeof key - 1;
FILE *f = fopen(settings_conf_path(), "r");
int vol = NCM_VOL_DEFAULT;
if (!f) return vol;
char line[160];
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue; /* 非 key=value 行跳过 */
char *k = line;
while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */
int n = (int)(eq - k);
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; /* key 右去空白 */
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* 精确匹配 */
char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++; /* value 左去空白 */
int x = atoi(v);
vol = (x < 0) ? 0 : (x > NCM_VOL_MAX ? NCM_VOL_MAX : x);
break;
}
fclose(f);
return vol;
}
/* 把 pct(0-100) 下发到 audiod。独立连接(SDK 客户端连接私有不可复用);MSG_AUDIO_VOLUME
* 不要求开流,audiod 侧写 DAC+持久化后 ACK(meow_audiod.c MSG_AUDIO_VOLUME)。失败仅日志,
* 不影响播放本身(音量是次要正确性,非播放成败)。
*
* §2026-09-02 连接风暴根因:audiod_sync_volume 每次 audio_out_open 都新建一条 AF_UNIX
* 短连接(socket→connect→write→close)。边下边播/切歌/seek 反复 open/close 时,audiod 端表现为
* 「同一 fd=N 被反复 accept」(真机 1489 次 "io: client connected (fd=4)")。音量值实际只在
* 用户按音量键时变化,且 audiod 收到已持久化(settings_set_volume),无需每次 open 重发——
* 修法:每进程首次 open 同步一次;此后仅当 settings 音量**变化**才重发(同一连接数坍缩为
* O(1),风暴消除)。失败不记 status(下次 open 仍会尝试,幂等)。 */
static void audiod_sync_volume(int pct)
{
const char *sp = getenv("MEOW_AUDIO_SOCK");
if (!sp || !sp[0]) sp = NCM_AUDIO_SOCK_DEF;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, sp, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return; }
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
uint32_t magic = MEOW_IPC_MAGIC, len = sizeof(int32_t), req = 1;
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
memcpy(frame + 0, &magic, 4);
memcpy(frame + 4, &ver, 2);
memcpy(frame + 6, &type, 2);
memcpy(frame + 8, &req, 4);
memcpy(frame + 12, &len, 4);
int32_t v32 = (int32_t)pct;
memcpy(frame + 16, &v32, sizeof v32);
if (write(fd, frame, sizeof frame) != (ssize_t)sizeof frame)
meow_log(MEOW_LOG_WARN, "audio: sync volume write failed pct=%d", pct);
close(fd);
}
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */
static int g_vol_synced = 0;
static int g_vol_last = -1;
int audio_out_sync_volume(void)
{
int pct = settings_read_volume();
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */
g_vol_synced = 1;
g_vol_last = pct;
audiod_sync_volume(pct);
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)",
pct, settings_conf_path());
return 0;
}
int audio_out_open(int sample_rate, int channels, int bits)
{
g_rate = sample_rate; g_ch = channels; g_bits = bits;
@@ -202,6 +309,9 @@ int audio_out_open(int sample_rate, int channels, int bits)
else
meow_log(MEOW_LOG_INFO, "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit",
sid, sample_rate, channels, bits);
/* 任务1:开播/打开音频会话时把系统当前音量同步下发到 audiod(DAC 停在默认/上次残留音量的根治)。
* 幂等(同值重写无害);失败不阻断播放。 */
audio_out_sync_volume();
return 0;
}
+78
View File
@@ -21,6 +21,7 @@
static pthread_t g_thr;
static int g_thr_on;
static int g_quit;
static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */
static const char *sock_path(void)
{
@@ -151,6 +152,11 @@ static void apply_playctl(const m_playctl_t *c)
* (停止/缓冲)时 pause() 内部按态过滤,天然 no-op 安全。 */
player_manager_pause(player_manager_state() == NCM_PC_PAUSED ? 0 : 1);
break;
case NCM_CTL_FOCUS:
/* 任务3:通知中心媒体卡片点「卡片体」→ launcher 发本 cmd(需要 launcher 接线,见任务书):
* 请求 App 回到播放页。只置标志(跨线程),UI 线程 app_on_tick 消费后导航(page nav 非线程安全)。 */
g_req_focus_player = 1;
break;
default: break;
}
}
@@ -201,6 +207,78 @@ static void *ipc_thread(void *arg)
return NULL;
}
/* 任务2:home 根页左滑「回桌面后台」——不再进程退出,改发 MSG_NAV{EXIT} 给 launcher。
* launcher M4 据 app.json background_audio + 媒体会话 PLAYING(meow_media_app_playing)判
* 保活(进程+音频流保留转后台,UI 走 §10.5 SDK present 自抑,非 SIGSTOP);否则才真终止。
* 本函数**不**置 g_app_quit、**不**停线程——进程保留,媒体会话继续上报 PLAYING(任务2 ③)。
* 独立连接发一帧(launcher ipc_dispatch MSG_NAV{EXIT} → meow_nav_request_exit,app_launcher.c:571)。
* 返回 0=发出 / -1=无 socket 或写失败。 */
int ipc_media_nav_exit(void)
{
int fd = connect_path(sock_path());
if (fd < 0) {
meow_log(MEOW_LOG_WARN, "ipc: nav_exit no launcher socket");
return -1;
}
m_nav_t n;
memset(&n, 0, sizeof n);
ncm_strlcpy(n.app_id, "dev.player.ncm", sizeof n.app_id);
n.nav_type = MEOW_NAV_EXIT;
n.depth = 0;
int rc = send_frame(fd, MSG_NAV, 9, &n, sizeof n);
close(fd);
meow_log(MEOW_LOG_INFO, "ipc: nav_exit sent rc=%d (request launcher background)", rc);
return rc;
}
/* 任务4 keep_awake:ACT_* 值 → launcher app.json awake/route 键用的活动名。
* 命中的只有这些(ncm_app_internal.h ACT_* 枚举);未知/空栈(0) 返回 NULL(不起作用)。 */
static const char *ncm_act_name(int act)
{
switch (act) {
case ACT_LOGIN: return "login";
case ACT_HOME: return "home";
case ACT_PLAYLIST: return "playlist";
case ACT_PLAYER: return "player";
case ACT_SETTINGS: return "settings";
case ACT_SEARCH: return "search";
default: return NULL;
}
}
/* 任务4 keep_awake:把当前顶层 activity 上报 launcher(MSG_ACT_FOCUS,独立连接发一帧,无需 HELLO)。
* launcher util.c ipc_dispatch MSG_ACT_FOCUS → meow_fg_act_set(app,act);fg_keep_awake() 依
* app.json awake.<act>=1 判定该 activity 是否常亮(超时不自动息屏)。无 socket/launcher 不跑 → 优雅跳过。
* 返回 0=发出 / -1=无 socket 或写失败。 */
int ipc_act_report(int act)
{
const char *name = ncm_act_name(act);
if (!name) return -1;
int fd = connect_path(sock_path());
if (fd < 0) {
meow_log(MEOW_LOG_WARN, "ipc: act_focus(%s) no launcher socket", name);
return -1;
}
m_act_focus_t f;
memset(&f, 0, sizeof f);
ncm_strlcpy(f.app_id, "dev.player.ncm", sizeof f.app_id);
ncm_strlcpy(f.activity, name, sizeof f.activity);
int rc = send_frame(fd, MSG_ACT_FOCUS, 10, &f, sizeof f);
close(fd);
meow_log(MEOW_LOG_INFO, "ipc: act_focus act=%s rc=%d", name, rc);
return rc;
}
/* 任务3:UI 线程消费「回到播放页」标志。返回 1=有未消费的聚焦请求(消费一次)。 */
int ipc_media_focus_consume(void)
{
if (g_req_focus_player) {
g_req_focus_player = 0;
return 1;
}
return 0;
}
int ipc_media_start(void)
{
if (g_thr_on) return 0;
+201
View File
@@ -0,0 +1,201 @@
/* play/mem_watch.c — 边播边监控**取证**实现(接口见 mem_watch.h;agent 32bit 大文件 OOM 取证)。
*
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / meow_svc_perf;
* 本模块退化为「RssAnon 取证」:低频读 /proc/self/status(优先 RssAnon,缺失回退 VmRSS),
* 记录匿名内存峰值,**仅供日志**。不再驱动下载/预取降载(那由 meow_cachegov_download_paused
* / prefetch_paused 查询)。**绝不打断解码/播放**。
*
* 阈值滞后(仅用于取证 level 标注):
* kb >= CRITICAL → level=2;kb >= HIGH 且当前<2 → level=1;kb <= LOW → level=0;其它保持。
* 本模块自含:只依赖 pthread + meow_log(SDK 日志),不依赖 core/app 其它 TU。
*/
#include "mem_watch.h"
#include "../sdk/include/meow_ui_log.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* 默认阈值(设备缺省;宿主单测用 env/force 桩覆盖) */
#define MEM_WATCH_PERIOD_MS 500
#define MEM_WATCH_HIGH_KB (30 * 1024)
#define MEM_WATCH_LOW_KB (20 * 1024)
#define MEM_WATCH_CRITICAL_KB (40 * 1024)
struct mw_cfg {
int period_ms;
long high_kb;
long low_kb;
long critical_kb;
};
static struct mw_cfg g_cfg = {
.period_ms = MEM_WATCH_PERIOD_MS,
.high_kb = MEM_WATCH_HIGH_KB,
.low_kb = MEM_WATCH_LOW_KB,
.critical_kb = MEM_WATCH_CRITICAL_KB,
};
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_thr;
static int g_thr_on;
static volatile int g_quit;
static long g_rss_anon_kb; /* 最近一次采样(KB;-1=不可读) */
static long g_peak_song_kb; /* 本曲匿名峰值(song_begin 清零) */
static int g_level; /* NCM_MEM_LVL_*(仅取证日志) */
static long g_forced_kb = -1; /* 宿主桩 ≥0 时替代真读 */
/* env 覆盖阈值:非法值回落默认。 */
static int env_int(const char *k, int def)
{
const char *e = getenv(k);
if (!e || !e[0]) return def;
int v = atoi(e);
return v > 0 ? v : def;
}
static long env_long(const char *k, long def)
{
const char *e = getenv(k);
if (!e || !e[0]) return def;
long v = atol(e);
return v > 0 ? v : def;
}
/* 读匿名 RSS(KB)。优先 RssAnon;缺则回退 VmRSS;都无 → -1。 */
static long read_rss_kb(void)
{
pthread_mutex_lock(&g_mu);
long forced = g_forced_kb;
pthread_mutex_unlock(&g_mu);
if (forced >= 0) return forced;
FILE *fp = fopen("/proc/self/status", "r");
if (!fp) return -1;
char line[256];
long anon = -1, rss = -1;
while (fgets(line, sizeof line, fp))
if (!strncmp(line, "RssAnon:", 8)) anon = atol(line + 8);
else if (rss < 0 && !strncmp(line, "VmRSS:", 6)) rss = atol(line + 6);
fclose(fp);
return anon >= 0 ? anon : rss;
}
static const char *level_str(int lvl)
{
switch (lvl) {
case NCM_MEM_LVL_HIGH: return "high";
case NCM_MEM_LVL_CRITICAL: return "critical";
default: return "normal";
}
}
static void *watch_thread(void *arg)
{
(void)arg;
while (!g_quit) {
long kb = read_rss_kb();
int lvl;
pthread_mutex_lock(&g_mu);
g_rss_anon_kb = kb;
if (kb > g_peak_song_kb) g_peak_song_kb = kb;
long hi = g_cfg.high_kb, lo = g_cfg.low_kb, cr = g_cfg.critical_kb;
/* 滞后三档(仅取证 level 标注) */
if (kb >= cr) lvl = NCM_MEM_LVL_CRITICAL;
else if (kb >= hi) lvl = (g_level == NCM_MEM_LVL_CRITICAL) ? NCM_MEM_LVL_CRITICAL : NCM_MEM_LVL_HIGH;
else if (kb <= lo) lvl = NCM_MEM_LVL_NORMAL;
else lvl = g_level; /* 滞后带:保持 */
if (lvl != g_level) {
meow_log(MEOW_LOG_INFO,
"[mem] rss_anon=%ldKB peak=%ldKB level=%s (H=%ldK/L=%ldK/C=%ldK)",
kb, g_peak_song_kb, level_str(lvl), hi, lo, cr);
}
g_level = lvl;
pthread_mutex_unlock(&g_mu);
usleep((useconds_t)g_cfg.period_ms * 1000u);
}
return NULL;
}
int mem_watch_start(void)
{
pthread_mutex_lock(&g_mu);
if (g_thr_on) { pthread_mutex_unlock(&g_mu); return 0; }
/* 开关(默认开;NCM_MEM_WATCH_ENABLED=0/off 关闭监控,行为退化为旧版零侵入) */
const char *en = getenv("NCM_MEM_WATCH_ENABLED");
if (en && (!en[0] || !strcmp(en, "0") || !strcmp(en, "off") || !strcmp(en, "false"))) {
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[mem] watch disabled (NCM_MEM_WATCH_ENABLED=%s)", en);
return 0;
}
/* 阈值 env 覆盖(宿主单测) */
g_cfg.period_ms = env_int("NCM_MEM_WATCH_PERIOD_MS", MEM_WATCH_PERIOD_MS);
g_cfg.high_kb = env_long("NCM_MEM_WATCH_HIGH_KB", MEM_WATCH_HIGH_KB);
g_cfg.low_kb = env_long("NCM_MEM_WATCH_LOW_KB", MEM_WATCH_LOW_KB);
g_cfg.critical_kb = env_long("NCM_MEM_WATCH_CRITICAL_KB", MEM_WATCH_CRITICAL_KB);
g_quit = 0;
pthread_attr_t a;
pthread_attr_init(&a);
pthread_attr_setstacksize(&a, (size_t)(128 * 1024)); /* 同 ncm_thread_create 128KB(内存瘦身,OOM 修复) */
int rc = pthread_create(&g_thr, &a, watch_thread, NULL);
pthread_attr_destroy(&a);
if (rc != 0) { pthread_mutex_unlock(&g_mu); return -1; }
g_thr_on = 1;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO,
"[mem] watch started period=%dms H=%ldK L=%ldK C=%ldK (RssAnon 优先)",
g_cfg.period_ms, g_cfg.high_kb, g_cfg.low_kb, g_cfg.critical_kb);
return 0;
}
void mem_watch_stop(void)
{
g_quit = 1;
pthread_mutex_lock(&g_mu);
if (g_thr_on) { g_thr_on = 0; pthread_detach(g_thr); }
pthread_mutex_unlock(&g_mu);
}
int mem_watch_level(void)
{
pthread_mutex_lock(&g_mu);
int v = g_level;
pthread_mutex_unlock(&g_mu);
return v;
}
long mem_watch_rss_anon_kb(void)
{
pthread_mutex_lock(&g_mu);
long v = g_rss_anon_kb;
pthread_mutex_unlock(&g_mu);
return v;
}
void mem_watch_song_begin(void)
{
long kb = read_rss_kb(); /* 起播即采样当前内存,本曲峰值从当前起算 */
pthread_mutex_lock(&g_mu);
g_peak_song_kb = kb;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[mem] song begin rss_anon=%ldKB", kb);
}
/* 播完/切歌释放钩子之后调用:落本曲峰值 + 重置计数。 */
void mem_watch_song_end(void)
{
pthread_mutex_lock(&g_mu);
long peak = g_peak_song_kb;
g_peak_song_kb = g_rss_anon_kb;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[mem] song end peak_anon=%ldKB (released)", peak);
}
void mem_watch_set_forced_kb(long kb)
{
pthread_mutex_lock(&g_mu);
g_forced_kb = kb;
pthread_mutex_unlock(&g_mu);
}
+55
View File
@@ -0,0 +1,55 @@
/* play/mem_watch.h — 边播边监控**取证**(agent 32bit 大文件 OOM 取证;OOM_PAGECACHE_GOVERNANCE)。
*
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / 系统服务
* meow_svc_perf;本模块退化为「RssAnon 取样取证」:一个轻量线程(ncm_thread_create 同款
* 128KB 栈)低频(默认 500ms)读 /proc/self/status 的 RssAnon(缺失回退 VmRSS),记录匿名
* 内存峰值(RssAnon 贴近「应用自有不可回收内存」,是 OOM kill 高危口径),**仅供日志/取证**,
* 不再驱动下载/预取降载(那由 meow_cachegov_download_paused/prefetch_paused 查询)。
*
* 调度器结合释放钩子:mem_watch_song_begin/end 在 run_song 的起播/收尾(song_src_dispose +
* ncm_dec_close + audio_out_close,即「彻底释放」钩子)处调用,重置本曲峰值计数并落日志
* (峰值 RSS 即 RssAnon/RssFile 分离取证锚点;Cached/Dirty 侧取证归 svc_perf/meow_cachegov)。
*
* 阈值/开关可通过环境变量覆盖(宿主单测确定性驱动):NCM_MEM_WATCH_ENABLED /
* NCM_MEM_WATCH_PERIOD_MS / NCM_MEM_WATCH_HIGH_KB / NCM_MEM_WATCH_LOW_KB / NCM_MEM_WATCH_CRITICAL_KB。
* 宿主测试桩:mem_watch_set_forced_kb() 注入读值(>=0 时监控线程不再读真 proc,仅测取证逻辑)。
*/
#ifndef NCM_MEM_WATCH_H
#define NCM_MEM_WATCH_H
#ifdef __cplusplus
extern "C" {
#endif
/* 内存压力档(仅取证日志用;不再驱动降载) */
enum {
NCM_MEM_LVL_NORMAL = 0, /* < HIGH */
NCM_MEM_LVL_HIGH = 1, /* ≥ HIGH */
NCM_MEM_LVL_CRITICAL = 2, /* ≥ CRITICAL */
};
/* 启动监控线程(幂等;ncm_thread_create 128KB 栈)。返回 0=ok / -1=线程建失败。 */
int mem_watch_start(void);
/* 停止(置退出标志;线程 detach,不 join——同 ipc_media 模式)。 */
void mem_watch_stop(void);
/* 当前压力档(0/1/2;仅取证日志)。 */
int mem_watch_level(void);
/* 最近一次采样的匿名 RSS(KB);-1=不可读。 */
long mem_watch_rss_anon_kb(void);
/* ---- 调度器:与播放释放钩子联动 ---- */
/* 起播一首曲时调用:本曲峰值计数清零,开始记录本曲内存画像。 */
void mem_watch_song_begin(void);
/* 播完/切歌、即「彻底释放」钩子(decoder 关+下载线程 join+源关+.part 收尾)执行后调用:
* 落本曲峰值日志 + 重置计数,给下一首让出统计(内存本身已由释放钩子归还)。 */
void mem_watch_song_end(void);
/* ---- 宿主测试桩(设备永不调用;≥0 时监控线程用注入值替代真 /proc 读)---- */
void mem_watch_set_forced_kb(long kb);
#ifdef __cplusplus
}
#endif
#endif /* NCM_MEM_WATCH_H */
+84 -2
View File
@@ -32,6 +32,7 @@
#include <sys/stat.h>
#include "decoder.h"
#include "stream_src.h"
#include "mem_watch.h"
static pthread_t g_thr;
static int g_thr_on;
@@ -43,6 +44,10 @@ static char g_path[512]; /* 本地音频路径(缓存,mp3/flac
static int64_t g_file_bytes;
static int g_level; /* 音质档(来自设置) */
static int g_cache_gb;
/* 任务3(§re-entry):pending_replay=1 表示「切音质/缓存后点了正在播的同曲」——此时重播须换档
* (P12 语义,seq++),不得被「同曲不重播」护栏吞掉。set_quality 置位;player_manager_play 的
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
static int g_pending_replay;
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
@@ -90,12 +95,25 @@ void player_manager_set_quality(int level, int cache_gb)
pthread_mutex_lock(&g_play.mu);
g_play.err = 0;
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
g_pending_replay = 1; /* 任务3:改档后点正在播的同曲 → 强制换档重播(§25 P12) */
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_play(int song_idx)
{
pthread_mutex_lock(&g_play.mu);
/* 任务3:点「正在播的曲」(同 idx 且 PLAYING/PAUSED,非切档重播)→ 只聚焦不重播:
* 不 seq++、不置 BUFFER——旧曲继续出声/停原状态,UI 由调用方 nav 到播放页。
* 注意:set_quality 已置 g_pending_replay=1(切音质后重播同曲须换档,P12),此护栏放行。
* 非 PLAYING/PAUSED(STOPPED/另一曲)不受影响,照旧起播。 */
if (song_idx == g_play.cur_song && !g_pending_replay &&
(g_play.state == NCM_PC_PLAYING || g_play.state == NCM_PC_PAUSED)) {
pthread_mutex_unlock(&g_play.mu);
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d state=%d -> no restart",
song_idx, g_play.state);
return;
}
g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */
g_play.cur_song = song_idx;
g_play.cmd = 0;
g_play.state = NCM_PC_BUFFER;
@@ -108,6 +126,7 @@ void player_manager_play(int song_idx)
g_play.underruns = 0; /* §21:边下边播统计随新播放意图清零 */
g_play.underrun_ms = 0;
g_play.stalled = 0;
g_play.refaults = 0; /* §2.3:refault 取证随新播放意图清零 */
pthread_mutex_unlock(&g_play.mu);
}
@@ -135,6 +154,7 @@ void player_manager_next(int dir)
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
if (c >= cnt) c = cnt - 1;
if (c != g_play.cur_song) {
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
g_play.cur_song = c;
g_play.cmd = 0;
g_play.state = NCM_PC_BUFFER;
@@ -147,6 +167,7 @@ void player_manager_next(int dir)
g_play.underruns = 0;
g_play.underrun_ms = 0;
g_play.stalled = 0;
g_play.refaults = 0;
}
pthread_mutex_unlock(&g_play.mu);
}
@@ -154,6 +175,7 @@ void player_manager_next(int dir)
void player_manager_stop(void)
{
pthread_mutex_lock(&g_play.mu);
g_pending_replay = 0; /* 停止:清「切档重播」待办 */
g_play.cur_song = -1;
g_play.state = NCM_PC_STOPPED;
g_play.pos_ms = 0;
@@ -189,8 +211,19 @@ typedef struct {
int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */
int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */
uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */
/* page-cache 治理(A:meow_cachegov)登记路径:播放实际读的文件(流式=.tmp/.part,命中=final)。 */
char gov_path[512];
/* 曲终 L0 用:commit 后的正式路径(流式 commit rename 后 = final;命中=gov_path 同)。 */
char gov_final_path[512];
} song_src_t;
/* page-cache 治理(A):SDK meow_cachegov 的 read_pos 回调——返回当前解码逻辑读指针
* (stream_src 内部维护,经 stream_src_read_pos 读取;回调上下文=stream_src*)。 */
static uint64_t gov_read_pos(void *ctx)
{
return stream_src_read_pos((stream_src_t *)ctx);
}
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。
* 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态——
@@ -201,6 +234,11 @@ static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
{
song_src_t *ss = (song_src_t *)ctx;
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
/* page-cache 治理(§2.3/§3):下载 cb 查 meow_cachegov_download_paused()(A 回退硬暂停
* OR svc_perf C Cached 降载)→ 暂停下载(限速),优先解码播放;不打断播放线程
* (读端追上下载走既有 underrun/停滞联动)。每 50ms 查一次中止,切歌可立即脱身。 */
while (meow_cachegov_download_paused() && !stream_src_aborted(ss->src))
usleep(50 * 1000);
if (ss->first_body) {
ss->first_body = 0;
int st = ss->st_ptr ? *ss->st_ptr : 0;
@@ -295,6 +333,15 @@ static void song_src_dispose(song_src_t *ss)
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
}
/* page-cache 治理(A)曲终清理:注销 L1 登记条目 + 下载完整(commit 成功)则对正式文件做
* L0 整文件 DONTNEED(仅曲终安全窗口;未 commit/中断保留 .part 不丢)。 */
static void song_gov_cleanup(song_src_t *ss)
{
if (ss->gov_path[0]) meow_cachegov_unregister(ss->gov_path);
if (ss->dl_rc == NCM_OK && ss->gov_final_path[0])
meow_cachegov_drop_finished(ss->gov_final_path);
}
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
@@ -587,6 +634,7 @@ static void run_song(int idx)
g_play.play_song = s;
g_play.play_song_valid = 1;
pthread_mutex_unlock(&g_play.mu);
mem_watch_song_begin(); /* 边播边监控:本曲峰值从当前起算(下载/解码/播放全程记录) */
song_src_t ss;
int64_t file_bytes = 0;
@@ -602,11 +650,22 @@ static void run_song(int idx)
}
if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */
song_src_dispose(&ss);
song_gov_cleanup(&ss); /* 未登记(register 在其后)时为 no-op,统一清理 */
mem_watch_song_end(); /* 释放钩子:join 下载线程+关源+缓冲 free+.part 收尾 */
return;
}
ncm_strlcpy(g_path, path, sizeof g_path);
g_file_bytes = file_bytes;
/* page-cache 治理(A):登记本曲缓存文件(播放实际读的路径)+ read_pos 回调,供 L1 丢已读
* 前缀 / refault 回退。cache 命中(ss.w==NULL)path=正式文件;流式 path=.tmp/.part
* (cur file 即正被 pread 的文件,DONTNEED 只作用于它;正式路径待 commit 后 L0 用)。 */
ncm_strlcpy(ss.gov_path, path, sizeof ss.gov_path);
if (ss.w) ncm_strlcpy(ss.gov_final_path, ncm_cache_writer_final_path(ss.w), sizeof ss.gov_final_path);
else ncm_strlcpy(ss.gov_final_path, path, sizeof ss.gov_final_path);
meow_cachegov_register(ss.gov_path, (uint64_t)(file_bytes > 0 ? file_bytes : 0),
gov_read_pos, ss.src);
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
/* 解码分发(§19/§21):stream_src 上 magic 探测 fLaC→dr_flac / 其它→minimp3(open_cb
@@ -616,6 +675,8 @@ static void run_song(int idx)
int64_t total_frames = 0;
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
song_src_dispose(&ss);
song_gov_cleanup(&ss);
mem_watch_song_end(); /* 释放钩子 */
if (!still_intent(idx, my_seq)) return; /* open 期间(flac 元数据等待)已切歌/重播 */
set_err(NCM_ERR_IO, "decode open failed"); return;
}
@@ -623,7 +684,8 @@ static void run_song(int idx)
/* 音频输出:按实际流参数开(flac ≤16bit→S16,>16bit→S32;audiod 侧格式归一+原生率跟随) */
if (audio_out_open(rate, ch, bits) != 0) {
ncm_dec_close(dec); song_src_dispose(&ss);
ncm_dec_close(dec); song_src_dispose(&ss); song_gov_cleanup(&ss);
mem_watch_song_end(); /* 释放钩子 */
set_err(NCM_ERR_IO, "audio open failed"); return;
}
@@ -645,6 +707,7 @@ static void run_song(int idx)
int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */
int stalled_edge = 0; /* §21 停滞沿:已对 audiod pause(1),待数据恢复 pause(0) */
int underruns_seen = 0;
int refaults_seen = 0; /* §2.3:本曲 DONTNEED 丢页重读 TF 计数(取证) */
for (;;) {
if (g_app_quit) break;
@@ -751,6 +814,18 @@ static void run_song(int idx)
idx, u, (unsigned long long)wm);
}
}
/* page-cache 治理(A):refault 取证落 g_play + 节拍执行 L1 丢弃/refault 回退 */
{
int rf = stream_src_refaults(ss.src);
if (rf != refaults_seen) {
refaults_seen = rf;
pthread_mutex_lock(&g_play.mu);
g_play.refaults = rf;
pthread_mutex_unlock(&g_play.mu);
meow_log(MEOW_LOG_INFO, "play: refault idx=%d count=%d", idx, rf);
}
meow_cachegov_tick();
}
/* 更新进度(~200ms 节流)*/
int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0;
pthread_mutex_lock(&g_play.mu);
@@ -763,6 +838,9 @@ static void run_song(int idx)
ncm_dec_close(dec);
/* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */
song_src_dispose(&ss);
song_gov_cleanup(&ss); /* page-cache 治理(A)曲终:注销 L1 条目 + commit 成功则 L0 整文件 DONTNEED */
mem_watch_song_end(); /* 调度器释放钩子:上一首缓冲完全清空(decoder 关 + 下载线程
* join + 源关 + 缓冲 free + .part 收尾)——给下一首让内存 */
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
}
@@ -835,7 +913,9 @@ static void *lyric_worker(void *arg)
}
if (!id[0]) continue;
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
* (A 回退硬暂停 OR svc_perf C Cached 降载),
* 不置 last_id → 降载恢复后下轮再拉。 */
ncm_strlcpy(last_id, id, sizeof last_id);
lyric_clear();
pthread_mutex_lock(&g_lyrmu);
@@ -898,6 +978,7 @@ int player_manager_start(void)
g_cache_gb = g_st.cache_limit_gb;
pthread_mutex_init(&g_play.mu, NULL);
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
if (ncm_thread_create(&g_thr, player_thread, NULL) != 0) return -1;
g_thr_on = 1;
return 0;
@@ -908,6 +989,7 @@ void player_manager_stop_thread(void)
g_want_run = 0;
g_app_quit = 1;
audio_out_close(); /* 打断阻塞写 */
mem_watch_stop(); /* 停监控线程 */
if (g_thr_on) {
pthread_join(g_thr, NULL);
g_thr_on = 0;
+36
View File
@@ -22,6 +22,7 @@
#define REBUFFER_AFTER_MS 500 /* 单次调用累计等待超此值后启用再缓冲迟滞 */
#define REBUFFER_MIN (64 * 1024) /* 迟滞放行门槛(≈1.6s@320kbps) */
#define STALL_MS 1000 /* 单次调用等待超此值 → 置 stalled(播放线程 pause 联动) */
#define STREAM_SRC_REFAULT_MS 50 /* pread 慢于此阈且数据已在盘上 → 计一次 refault(§2.3) */
struct stream_src {
int fd; /* 只读 fd(pread;写端另有自己的写 fd) */
@@ -36,8 +37,16 @@ struct stream_src {
uint64_t wait_ms_total;
int underruns;
int stalled;
int refaults; /* 数据在盘上但 pread 慢(>50ms)= DONTNEED 丢页重读 TF(§2.3) */
};
/* SDK meow_cachegov 提供的 refault 上报(strong 时覆盖弱符号;未链 SDK 时为空指针,调用需判空)。 */
#if defined(__GNUC__)
extern void meow_cachegov_note_refault(void) __attribute__((weak));
#else
extern void meow_cachegov_note_refault(void);
#endif
static int64_t mono_ms(void)
{
struct timespec ts;
@@ -182,9 +191,18 @@ static size_t src_read(void *buf, size_t size, void *ud)
if (avail > 0 && !gated) {
size_t n = (size - done) < (size_t)avail ? (size - done) : (size_t)avail;
uint64_t pos = s->read_pos;
int had_data = (pos < s->written); /* 数据已在盘上(放锁前采样;勿在放锁后读 written) */
pthread_mutex_unlock(&s->mu); /* pread 期间放锁,不阻塞写端 broadcast */
int64_t t0 = mono_ms();
ssize_t r = pread(s->fd, (char *)buf + done, n, (off_t)pos);
int64_t pread_ms = mono_ms() - t0;
pthread_mutex_lock(&s->mu);
if (r > 0 && had_data && pread_ms > STREAM_SRC_REFAULT_MS) {
/* 数据在盘上但 pread 慢 = DONTNEED 丢页后重读 TF(underrun 确认是 refault,
* 非网络等待)。上报 SDK meow_cachegov(弱符号,未链时为空指针需判空)。 */
s->refaults++;
if (meow_cachegov_note_refault) meow_cachegov_note_refault();
}
if (r <= 0) { /* 盘 IO 错:按中断收尾(真 EOF 语义) */
if (!s->failed) s->failed = -3; /* NCM_ERR_IO(本层不依赖 core 头) */
s->final = s->written ? s->written : 1;
@@ -290,6 +308,24 @@ uint64_t stream_src_written(stream_src_t *s)
return w;
}
int stream_src_refaults(stream_src_t *s)
{
if (!s) return 0;
pthread_mutex_lock(&s->mu);
int r = s->refaults;
pthread_mutex_unlock(&s->mu);
return r;
}
uint64_t stream_src_read_pos(stream_src_t *s)
{
if (!s) return 0;
pthread_mutex_lock(&s->mu);
uint64_t p = s->read_pos;
pthread_mutex_unlock(&s->mu);
return p;
}
uint64_t stream_src_final(stream_src_t *s)
{
if (!s) return 1;
+9
View File
@@ -72,6 +72,15 @@ void stream_src_stats(stream_src_t *s, int *underruns, uint64_t *wait_ms);
* 取走即清零。 */
int stream_src_take_stalled(stream_src_t *s);
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE §2.3:underrun/refault 检测)----
* refault = pread 处「数据已在盘上(written>read_pos)但读出耗时 >50ms」——DONTNEED 丢页后
* 重读 TF 的信号,区别于网络 underrun(avail==0 等待)。SDK meow_cachegov 据此做 A 回退。
* 播放线程读 refaults 落 g_play 取证;回调由 SDK(meow_cachegov_note_refault)提供,
* 未链 SDK 时弱符号为空(host sim 兼容)。 */
int stream_src_refaults(stream_src_t *s);
/* 当前解码逻辑读指针(stream_src_seek_cb 改写)。 */
uint64_t stream_src_read_pos(stream_src_t *s);
uint64_t stream_src_written(stream_src_t *s);
uint64_t stream_src_final(stream_src_t *s); /* 0=下载中;>0=最终大小(完成=全长,中断=截断点) */
int stream_src_failed(stream_src_t *s); /* 0=无错误;<0=下载/IO 错误码 */
+5 -4
View File
@@ -266,10 +266,11 @@ int main(int argc, char **argv)
player_touch_move(340, 200); /* 右移 100px */
player_on_swipe(MEOW_SWIPE_RIGHT, 100); /* SDK 中部右滑 → SWIPE_RIGHT */
int p2 = player_page();
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,504)=进度条行 */
player_touch_down(240, 504);
player_touch_move(140, 504);
player_on_tap(140, 504); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,484)=进度条行(§22 时间行下移后
* g_bar.y=484,时间行在 500——拖时间行会误判翻页,故取进度条行本身) */
player_touch_down(240, 484);
player_touch_move(140, 484);
player_on_tap(140, 484); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
int latched_change = player_page();
int ok = (p0 == 0 && p1 == 1 && p2 == 0 && latched_change == 0);
printf("gesture: p0=%d p1(左滑)=%d p2(右滑)=%d 省电锁存=%d -> %s\n",
+21
View File
@@ -120,6 +120,27 @@ static void home_draw(void *ctx)
meow_ui_text(pc.x + asz + 28, pc.y + 6, g_st.nickname[0] ? g_st.nickname : "我的歌单",
th->font_body, th->onSurface);
meow_ui_text(pc.x + asz + 28, pc.y + 6 + th->font_body + 4, idtxt, th->font_body, th->onSurfaceVariant);
/* §22 会员等级小字:account_vip() 枚举(用户语义 0=无 / 1=畅听会员 / 2=黑胶 VIP /
* 3=超级会员);昵称右侧画小红徽章(red_ctr 底 + 红字),无会员则不画。 */
{
int vip = account_vip();
if (vip != NCM_VIP_NONE) {
const char *vip_lab = (vip == NCM_VIP_CVIP) ? "畅听会员"
: (vip == NCM_VIP_SVIP) ? "黑胶 VIP"
: (vip == NCM_VIP_SUPER) ? "超级会员" : "";
const char *nm = g_st.nickname[0] ? g_st.nickname : "我的歌单";
int nmw = meow_ui_text_width(nm, th->font_body);
int bs = th->font_body - 2; /* 徽章字号略小 */
if (bs < 16) bs = 16;
int bw = meow_ui_text_width(vip_lab, bs) + 12;
int bx = pc.x + asz + 28 + nmw + 8;
int by = pc.y + 6 + (th->font_body - (bs + 4)) / 2;
if (bx + bw <= pc.x + pc.w - 24) { /* 不越卡片右缘(避开“>”) */
ncm_fill_round(bx, by, bw, bs + 4, 6, pa->red_ctr);
meow_ui_text_center(bx + bw / 2, by + 2, vip_lab, bs, pa->red);
}
}
}
meow_ui_text_right(pc.x + pc.w - 16, pc.y + (pc.h - 28) / 2, ">", 28, th->onSurfaceVariant);
/* 双页签(g_tabs=meow_tabbar widget 建)——红下划线/红选中字叠层。 */
+209 -59
View File
@@ -8,8 +8,9 @@
* - 封面页:方形大图+圆角(cover.c LRU,无旋转动画)+ 进度条(可点 seek)+ MD3 矢量控制钮
* (prev/play-pause/next,ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)
* + 元信息 + 实际音质档 + 页面指示点;缓冲/错误态沿用(「缓冲中…N%」/中文错误条+重试)。
* - 歌词页:LRC 滚动(当前行高亮居中,上下行渐隐)+ 点行跳转;默认=封面模糊置底,
* 省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode: default/power_save)。
* - 歌词页:LRC 滚动(活动行纯白偏上锚定,上下行逐级灰阶)+ 点行跳转;默认=封面模糊置底
* (低透明非纯黑中性叠加层,需求1 加深),省电=封面背景+黑色半透明叠加层
* (设置 ncm.player_bg_mode: default/power_save)。
* - 日夜间切换:主题配色只映射到半透明叠加层(歌词页 scrim)与 accent(红),
* 不动沉浸底(恒 NCM_PLAYER_BG)。
*
@@ -39,6 +40,35 @@
#define PLR_EDGE_W 24 /* 左缘返回热区(与 SDK EDGE_W 一致,防抢返回) */
#define PLR_SWIPE_MS 65 /* 中部横滑判页阈值(与 SDK SWIPE_THRESH 一致) */
/* 歌词页布局(§22 重做批):左边距 / 子行高 / 可见行数上限 / 折行段缓冲。
* 超宽歌词折行显示(中西文兼容)+ 活动行高亮(纯白)偏上锚定 + 上下行逐级灰阶,
* 顶部元信息与歌词正文同左边距。 */
#define LYR_PAD 24 /* 歌词页左边距(顶部元信息与歌词正文一致) */
#define LYR_SUB_LH 44 /* 一个歌词条目(含折行)的子行高 */
#define LYR_NAME_SZ 28 /* 顶部歌名字号 */
#define LYR_ART_SZ 20 /* 顶部歌手字号 */
#define LYR_CUR_SZ 28 /* 当前歌词行字号 */
#define LYR_OTH_SZ 24 /* 上下行歌词字号 */
#define LYR_TL_SZ 20 /* 当前行翻译字号 */
#define LYR_MAX_ROWS 9 /* 歌词页可见视觉行数上限(含折行,≤9) */
#define LYR_SEG_SZ 256 /* 折行段缓冲 */
/* 需求3 活动行锚定:块中心相对屏中线偏上 LYR_ABOVE_OFF px;上方至少 LYR_ABOVE_MIN 行已过歌词。 */
#define LYR_ABOVE_OFF 50
#define LYR_ABOVE_MIN 2
/* 需求1 叠加层加深(非纯黑,保留封面微透出 → 歌词清晰、封面隐约):
* default=近黑中性,alpha 132→176、色值略暗;power_save=纯黑 alpha 195→200。 */
#define LYR_SCRIM_D 0x10101A /* 默认层暗色 scrim */
#define LYR_SCRIM_L 0x201A16 /* 默认层亮色 scrim(暖黑) */
#define LYR_SCRIM_PS 0x000000 /* 省电层 scrim(纯黑) */
#define LYR_SCRIM_A 176
#define LYR_SCRIM_PS_A 200
/* 需求2 歌词行灰阶:按距活动行行距 0..5 分级(0=活动行纯白,越远越灰;最远可见 0x555555)。
* 锚点对照用户需求:±1 行 0xBBBBBB、中深 0x808080、最远 0x555555,逐级单调变灰。 */
static const uint32_t lyr_gray_tbl[6] = {
0xFFFFFF, 0xBBBBBB, 0x929292, 0x808080, 0x6A6A6A, 0x555555
};
typedef struct { meow_view *view; int active; } player_ctrl;
static player_ctrl g_plc;
@@ -63,8 +93,14 @@ static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
/* ---- 歌词页几何 ---- */
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
static int g_lyr_lh; /* 行高 */
static int g_lyr_meta_y; /* 顶部元信息 y */
static int g_lyr_anchor; /* 活动行块中心 y(屏中线偏上,需求3) */
static int g_lyr_meta_y; /* 顶部元信息 y(避开状态栏) */
/* 歌词布局暂存(每次 plr_draw_lyrics 重建;UI 线程串行,静态复用) */
static char s_lyr_seg[LYR_MAX_ROWS][LYR_SEG_SZ]; /* 折行段复用缓冲(一次画一个条目) */
static int s_lyr_ent[LYR_MAX_ROWS]; /* 可见条目 → 歌词行下标 */
static int s_lyr_rw[LYR_MAX_ROWS]; /* 条目折行段数 */
static int s_lyr_sz[LYR_MAX_ROWS]; /* 条目标题字号 */
static int s_lyr_y[LYR_MAX_ROWS]; /* 条目块顶 y */
#define CTL_RR (g_r_side + 8) /* 侧钮命中容差(圆外一点仍可点) */
@@ -90,6 +126,108 @@ static void fmt_ms(int64_t ms, char *out, size_t sz)
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
}
/* 折行:把 txt 按「词/字符」折成 ≤max_rows 段,每段 ≤maxw px。中文按字符、英文按词断行;
* 宽测量用 meow_ui_text_width(宿主/生产同口径)。段写入 seg[][LYR_SEG_SZ],返回段数(≥1)。 */
static int lyr_wrap_lines(const char *txt, int size, int maxw,
char seg[][LYR_SEG_SZ], int max_rows)
{
if (!txt || !txt[0] || max_rows <= 0) { seg[0][0] = 0; return 1; }
int n = 0;
size_t len = strlen(txt), pos = 0;
while (pos < len && n < max_rows) {
size_t start = pos;
int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */
int overflow = 0;
while (pos < len) {
unsigned char c = (unsigned char)txt[pos];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
if (pos + (size_t)ch > len) ch = (int)(len - pos);
size_t cand = (pos + (size_t)ch) - start;
size_t cp = cand < LYR_SEG_SZ - 1 ? cand : LYR_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
if (meow_ui_text_width(seg[n], size) > maxw && cand > 0) { overflow = 1; break; }
if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start);
pos += (size_t)ch;
}
if (overflow) {
size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start);
if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */
unsigned char c = (unsigned char)txt[start];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
linelen = (size_t)ch;
}
size_t cp = linelen < LYR_SEG_SZ - 1 ? linelen : LYR_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
pos = start + linelen;
if (last_ws > 0) {
size_t sp = start + (size_t)last_ws + 1;
if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */
}
n++;
} else {
size_t cp = (len - start) < LYR_SEG_SZ - 1 ? (len - start) : LYR_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
n++;
break;
}
}
if (n == 0) { seg[0][0] = 0; n = 1; }
return n;
}
/* 歌词可见条目布局:活动行块中心锚定在 g_lyr_anchor(屏中线偏上,需求3 中间偏上),
* 活动行上方保底 ≥LYR_ABOVE_MIN 行已过歌词,下方填满,总视觉行数 ≤ LYR_MAX_ROWS(≤9)。
* 结果写 s_lyr_ent/rw/sz/y(s_lyr_y 自上向下排好;当前行块中心 = g_lyr_anchor)。 */
static int lyr_layout(int n, int cur, int maxw)
{
/* 清静态暂存(防帧间残留:上帧条目数 > 本周,绘制循环读到陈旧 s_lyr_* 越界复用) */
for (int k = 0; k < LYR_MAX_ROWS; k++) { s_lyr_rw[k] = 0; s_lyr_ent[k] = -1; s_lyr_y[k] = 0; }
if (cur < 0 || cur >= n) return 0;
int rc = lyr_wrap_lines(lyric_line_text(cur), LYR_CUR_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
if (rc > LYR_MAX_ROWS) rc = LYR_MAX_ROWS;
int cap = LYR_MAX_ROWS - rc; /* 当前行之外可用的视觉行数 */
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
if (above_want < LYR_ABOVE_MIN) above_want = (cur >= LYR_ABOVE_MIN) ? LYR_ABOVE_MIN : cur;
if (above_want > cap) above_want = cap; /* 总行数 ≤ LYR_MAX_ROWS */
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
int a_cnt = 0, usedA = 0;
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
if (usedA + rr > above_want) break;
for (int k = a_cnt; k > 0; k--) {
s_lyr_ent[k] = s_lyr_ent[k - 1]; s_lyr_rw[k] = s_lyr_rw[k - 1];
s_lyr_sz[k] = s_lyr_sz[k - 1];
}
s_lyr_ent[0] = i; s_lyr_rw[0] = rr; s_lyr_sz[0] = LYR_OTH_SZ; a_cnt++; usedA += rr;
}
/* 2) 当前行(下标 a_cnt) */
s_lyr_ent[a_cnt] = cur; s_lyr_rw[a_cnt] = rc; s_lyr_sz[a_cnt] = LYR_CUR_SZ;
/* 3) 下方(cur+1 向下):余量 = cap - actual 上方行数 */
int below_want = cap - usedA;
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur; /* 下方行数上限 */
int b_cnt = 0, usedB = 0;
for (int i = cur + 1; i < n && usedB < below_want; i++) {
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
if (usedB + rr > below_want) break;
int k = a_cnt + 1 + b_cnt;
s_lyr_ent[k] = i; s_lyr_rw[k] = rr; s_lyr_sz[k] = LYR_OTH_SZ; b_cnt++; usedB += rr;
}
int cnt = a_cnt + 1 + b_cnt;
if (cnt == 0) return 0;
/* 4) y 位置:活动行块中心 = g_lyr_anchor;上方向上加、下方向下加(块间无缝隙) */
s_lyr_y[a_cnt] = g_lyr_anchor - rc * LYR_SUB_LH / 2;
for (int k = a_cnt + 1; k < cnt; k++)
s_lyr_y[k] = s_lyr_y[k - 1] + s_lyr_rw[k - 1] * LYR_SUB_LH;
for (int k = a_cnt - 1; k >= 0; k--)
s_lyr_y[k] = s_lyr_y[k + 1] - s_lyr_rw[k] * LYR_SUB_LH;
return cnt;
}
/* 颜色插值(t=0..255 混向 b)。播放页歌词渐隐/叠加用(无 GPU,逐通道整数混合)。 */
static uint32_t mix_c(uint32_t a, uint32_t b, int t)
{
@@ -177,19 +315,22 @@ static void player_compute_geom(void)
g_title_y = g_cover_r.y + g_cover_r.h + 32; /* 400 */
g_artist_y = g_title_y + 32; /* 432 */
g_qual_y = g_artist_y + 26; /* 458 */
g_times_y = g_qual_y + 26; /* 484 */
g_bar = NR(48, g_times_y + 20, W - 96, 4); /* 504 (h=4) */
g_ctl_y = g_bar.y + 64; /* 568 */
/* §22 时间行:放大字号并下移到进度条下方,不与进度条相交。
* 布局:进度条(484,h=4) → 时间行(500, font 24) → 控制钮(564)。 */
g_bar = NR(48, g_qual_y + 26, W - 96, 4); /* 484 (h=4) */
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
g_ctl_y = g_times_y + 64; /* 564 */
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
g_r_side = 24; g_r_play = 34;
g_dots_y = g_ctl_y + 62; /* 630 */
g_dots_y = g_ctl_y + 62; /* 626 */
/* 动态带 = 封面下方(元信息/进度/控制/指示点),与封面静态带不重叠。 */
g_dyn_r = NR(0, g_cover_r.y + g_cover_r.h, W, H - (g_cover_r.y + g_cover_r.h));
g_lyr_cx = H / 2; /* 360 */
g_lyr_lh = 30;
g_lyr_meta_y = 22;
/* 歌词页几何:中心线 / 活动行块中心(屏中线偏上,需求3)/ 顶部元信息 y。 */
g_lyr_cx = H / 2; /* 360 */
g_lyr_anchor = g_lyr_cx - LYR_ABOVE_OFF; /* 310 活动行偏上锚定 */
g_lyr_meta_y = ncm_statusbar() + 20;
}
/* ============================================================ */
@@ -280,8 +421,9 @@ static void plr_draw_cover_dyn(void *ctx)
}
meow_ui_text_center(W / 2, g_times_y - 8, bbuf, 20, PLR_TEXT_DIM);
} else {
meow_ui_text(48, g_times_y, lp, 20, PLR_TEXT_DIM);
meow_ui_text_right(W - 48, g_times_y, rp, 20, PLR_TEXT_DIM);
/* §22 时间字号放大(20→24)且下移到进度条下方,不再与进度条相交 */
meow_ui_text(48, g_times_y, lp, 24, PLR_TEXT_DIM);
meow_ui_text_right(W - 48, g_times_y, rp, 24, PLR_TEXT_DIM);
}
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, PLR_TRACK_BG);
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
@@ -297,73 +439,75 @@ static void plr_draw_cover_dyn(void *ctx)
draw_page_dots(g_dots_y);
}
/* 歌词页(整屏带状物):封面模糊置底 + scrim + 顶部元信息 + 滚动歌词(当前行高亮居中 +
* 上下行渐隐)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
/* 歌词页(整屏带状物):封面模糊置底 + 加深 scrim + 顶部元信息 + 滚动歌词(活动行纯白偏上锚定 +
* 上下行逐级灰阶)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
static void plr_draw_lyrics(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 1) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int W = g_W, H = g_H;
/* 底:封面等比居中放大(contain:等比缩放到屏内最大、居中;四周主题底),无封面 → 沉浸底。
* 不拉伸铺满(方图硬拉 480×720 变形丑,2026-09-02 用户报)。
* ⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏 → 下次 paint 自锁死,§18)。 */
/* 底:封面铺满全屏(cover-fit,四周无黑边;无 GPU 高斯模糊,用低透明中性叠层近似「模糊底」),
* 无封面 → 主题中性底(非纯黑)。⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏,
* 下次 paint 自锁死,§18)。§22:default=封面+半透明中性叠层;power_save=封面+黑色更重叠层。 */
meow_img *cov = cover_get();
meow_ui_fill_rect(0, 0, W, H, NCM_PLAYER_BG);
meow_ui_fill_rect(0, 0, W, H, 0x1A1A20); /* 无封面兜底主题底(中性深灰,非纯黑) */
if (cov) {
int cw = meow_img_w(cov), ch = meow_img_h(cov);
if (cw > 0 && ch > 0) {
int dw = W, dh = (int)((long)ch * W / cw);
if (dh > H) { dh = H; dw = (int)((long)cw * H / ch); }
meow_ui_img_scaled((W - dw) / 2, (H - dh) / 2, dw, dh, cov);
long dw = W, dh = (long)ch * W / cw; /* cover-fit:铺满宽度,高按等比超出裁剪 */
if (dh < H) { dh = H; dw = (long)cw * H / ch; }
meow_ui_img_scaled((W - (int)dw) / 2, (H - (int)dh) / 2, (int)dw, (int)dh, cov);
}
}
cover_put();
/* 半透明叠加层:default=主题 scrim(日/夜跟随主题);power_save=黑色更重(§22)。 */
/* 半透明叠加层:default=主题中性(近黑但不纯黑,封面色彩微透出 → 模糊底观感);
* power_save=黑色更重(§22 保留原「封面背景+黑叠加」语义)。需求1 加深:alpha/色值加暗,
* 但仍保留「封面隐约」而非纯黑整体吞掉。 */
if (g_st.player_bg_mode == NCM_BG_POWER_SAVE)
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 195);
meow_ui_fill_rect_alpha(0, 0, W, H, LYR_SCRIM_PS, LYR_SCRIM_PS_A);
else
meow_ui_fill_rect_alpha(0, 0, W, H, th->scrim, 150);
meow_ui_fill_rect_alpha(0, 0, W, H, g_dark ? LYR_SCRIM_D : LYR_SCRIM_L, LYR_SCRIM_A);
/* 顶部元信息(title · artist,Apple Music 式顶部小字) */
/* 顶部元信息(两行居左):第一行歌名(大字)、第二行歌手名(小字,多歌手保留已拼串)。
* 与歌词正文同左边距 LYR_PAD;下移避开状态栏(g_lyr_meta_y = statusbar + 20)。 */
ncm_song_t sm;
if (cur_song_meta(&sm) == 0) {
char mb[300];
snprintf(mb, sizeof mb, "%s / %s", sm.name, sm.art_name);
meow_ui_text_center(W / 2, g_lyr_meta_y, ncm_text_fit(mb, W - 48, 18), 18, PLR_TEXT);
meow_ui_text(LYR_PAD, g_lyr_meta_y,
ncm_text_fit(sm.name, W - 2 * LYR_PAD, LYR_NAME_SZ), LYR_NAME_SZ, PLR_TEXT);
meow_ui_text(LYR_PAD, g_lyr_meta_y + LYR_NAME_SZ + 6,
ncm_text_fit(sm.art_name, W - 2 * LYR_PAD, LYR_ART_SZ), LYR_ART_SZ, PLR_TEXT_DIM);
}
/* 歌词:当前行居中高亮;上下行渐隐(距中心越远越接近底色)。 */
/* 歌词:活动行纯白居中(含折行,块中心偏上锚定);上下行按行距逐级灰阶;≤LYR_MAX_ROWS。 */
int n = lyric_count();
int64_t ps = player_manager_pos_ms();
int cur = (n > 0) ? lyric_line_at(ps) : -1;
int cy = g_lyr_cx;
int lo = g_lyr_meta_y + 44, hi = g_dots_y - 6; /* 歌词窗口(避顶部元信息/底部指示点) */
for (int i = cur - 7; i <= cur + 6; i++) {
if (i < 0 || i >= n) continue;
int yy = cy + (i - cur) * g_lyr_lh;
if (yy < lo || yy + g_lyr_lh > hi) continue;
int maxw = W - 2 * LYR_PAD;
int cnt = lyr_layout(n, cur, maxw);
/* 渐隐目标色 = 歌词页背底层(与 default/power_save 叠层一致),供翻译行融入背景 */
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? LYR_SCRIM_PS
: (g_dark ? LYR_SCRIM_D : LYR_SCRIM_L);
for (int k = 0; k < cnt; k++) {
int i = s_lyr_ent[k], rows = s_lyr_rw[k], sz = s_lyr_sz[k], y = s_lyr_y[k];
int is_cur = (i == cur);
int dist = (i - cur) < 0 ? (cur - i) : (i - cur);
int fade = dist * 42; /* 0..255,越远越淡 */
if (fade > 214) fade = 214; /* 远行仍隐约可见,不全隐 */
uint32_t base = is_cur ? pa->red_lit : PLR_TEXT;
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? 0x000000 : th->scrim;
uint32_t col = mix_c(base, bgcol, fade);
int size = is_cur ? 24 : 20;
meow_ui_text_center(W / 2, yy, ncm_text_fit(lyric_line_text(i), W - 24, size), size, col);
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur)); /* 距活动行行距 */
uint32_t col = lyr_gray_tbl[dist > 5 ? 5 : dist]; /* 0=纯白,越远越灰 */
int rr = lyr_wrap_lines(lyric_line_text(i), sz, maxw, s_lyr_seg, rows);
if (rr > rows) rr = rows;
for (int r = 0; r < rr; r++)
meow_ui_text(LYR_PAD, y + r * LYR_SUB_LH, s_lyr_seg[r], sz, col);
if (is_cur) {
const char *tl = lyric_line_tl(i);
if (tl && tl[0])
meow_ui_text_center(W / 2, yy + 28, ncm_text_fit(tl, W - 24, 18), 18,
mix_c(pa->red_lit, bgcol, 120));
meow_ui_text(LYR_PAD, y + rows * LYR_SUB_LH + 2,
ncm_text_fit(tl, W - 2 * LYR_PAD, LYR_TL_SZ), LYR_TL_SZ,
mix_c(0xFFFFFF, bgcol, 120));
}
}
if (n == 0) {
meow_ui_text_center(W / 2, cy - 10,
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
meow_ui_text(LYR_PAD, g_lyr_cx - 10,
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
}
draw_page_dots(g_dots_y);
@@ -466,21 +610,27 @@ static void player_reset_touch(void)
g_touch_on = 0; g_drag_latched = 0; g_drag_dx = 0;
}
/* 歌词页:点行 seek(把 tap y 归到最近一行的下标)。 */
/* 歌词页:点行 seek(用与绘制一致的可见条目布局把 tap y 命中最临近行;
* 活动行已偏上锚定,非屏中线,故按 s_lyr_y/rw 逐块扫描而非 cx 偏移)。 */
static int lyrics_line_tap(int y)
{
int n = lyric_count();
if (n <= 0) return -1;
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。
* 元信息块 = 歌名(28)+6+歌手(20)+边距 → g_lyr_meta_y+66。 */
if (y < g_lyr_meta_y + 66 || y > g_dots_y - 6) return -1;
int64_t ps = player_manager_pos_ms();
int cur = lyric_line_at(ps);
int off = y - g_lyr_cx;
int dioff = (off >= 0) ? (off + g_lyr_lh / 2) / g_lyr_lh : (off - g_lyr_lh / 2) / g_lyr_lh;
int i = cur + dioff;
if (i < 0) i = 0;
if (i >= n) i = n - 1;
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。 */
if (y < g_lyr_meta_y + 44 || y > g_dots_y - 6) return -1;
return i;
int maxw = g_W - 2 * LYR_PAD;
int cnt = lyr_layout(n, cur, maxw);
if (cnt <= 0) return -1;
for (int k = 0; k < cnt; k++) {
int yb = s_lyr_y[k];
int yh = s_lyr_rw[k] * LYR_SUB_LH;
if (y >= yb && y < yb + yh) return s_lyr_ent[k];
}
/* 落在可见块之外 → 就近取整(顶部元信息/底部指示点已在上面排除) */
return (y < s_lyr_y[0]) ? s_lyr_ent[0] : s_lyr_ent[cnt - 1];
}
static int in_circle(int cx, int cy, int r, int x, int y)
+11
View File
@@ -268,6 +268,17 @@ static void *playlist_worker(void *arg)
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
if (tidx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
/* §22 整张歌单逐渐物化:首屏成功后把「能装进滑动窗口」的歌单一次性补填入窗
* (≤NCM_SONG_WINDOW 的歌单整张物化,末叶行不再恒「.../加载中」——真机反馈
* 「曲目稍多时行一直加载中,点开播放这首才回填」);超窗口的歌单仍按视口滑动补载
* (内存纪律:窗口 ≤256 首)。窗口基址=0 覆盖 [0,min(track,WINDOW)),不滑窗不挤占视口。 */
if (rc == NCM_OK) {
int full = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
if (full > NCM_SONG_BATCH) {
int r2 = appstate_song_ensure_range(0, full);
if (r2 != NCM_OK && rc == NCM_OK) rc = r2; /* 补填失败非致命:已物化槽保留 */
}
}
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
+11 -6
View File
@@ -1,7 +1,7 @@
/* ui/settings.c — 设置页(ACT_SETTINGS)。
*
* 行项:音质(§20 八档:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声,
* 默认极高 HQ;每档标注规格+会员门槛 CVIP/SVIP;超权限档允许选定,播放时钳顶+逐级降级,
* 默认极高 HQ;每档标注规格+会员门槛(畅听会员/黑胶 VIP/超级会员);超权限档允许选定,播放时钳顶+逐级降级,
* 见 player_manager.c song_local_file)+ 缓存上限(默认 10GB,上限 100GB,支持预设与
* 「自定义」数字输入)+ 清理缓存(显示当前占用)+ 退出登录 + 关于/版本。
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
@@ -80,9 +80,12 @@ static void settings_draw(void *ctx)
rows[4].title = "退出登录"; rows[4].red = 1;
rows[5].title = "关于网易云"; rows[5].red = 0;
{
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20)。
* 门槛串按用户语义对齐:1=畅听会员 / 2=黑胶 VIP / 3=超级会员(门槛恒 ≤2,SUPER 兜底)。 */
int gate = ncm_api_level_gate(g_st.level);
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : "";
const char *gate_s = (gate == NCM_VIP_SVIP) ? "黑胶 VIP"
: (gate == NCM_VIP_CVIP) ? "畅听会员"
: (gate == NCM_VIP_SUPER) ? "超级会员" : "";
int locked = (account_vip() < gate);
if (gate_s[0])
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) / 需 %s%s",
@@ -112,7 +115,7 @@ static void settings_draw(void *ctx)
/* 品牌红页脚小字。 */
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant);
meow_ui_text(x, ny, "无损/Hi-Res 需畅听会员 ・ 臻音/母带需黑胶 VIP,不支持时自动降级", 20, th->onSurfaceVariant);
meow_ui_text(x, ny + 24, "网易云音乐 / MeowOS 移植版", 20, pa->red);
}
@@ -170,9 +173,11 @@ int settings_on_tap(int x, int y)
int lv = g_ui_levels[i];
int gate = ncm_api_level_gate(lv);
char lbl[96];
/* 每档标注 规格+会员门槛;未解锁档标注「需 CVIP/SVIP」(sheet 无置灰能力,
/* 每档标注 规格+会员门槛;未解锁档标注「需 畅听会员/黑胶 VIP」(sheet 无置灰能力,
* 以文字标注代替——允许选定,实际播放逐级降级到可用档) */
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL;
const char *gate_s = (gate == NCM_VIP_SVIP) ? "黑胶 VIP"
: (gate == NCM_VIP_CVIP) ? "畅听会员"
: (gate == NCM_VIP_SUPER) ? "超级会员" : NULL;
if (gate_s)
snprintf(lbl, sizeof lbl, "%s (%s) / %s%s", ncm_level_name(lv),
ncm_level_spec(lv), gate_s, (vip < gate) ? "/未解锁" : "");
+18
View File
@@ -413,6 +413,24 @@ int main(void)
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
g_fail_next = 0;
/* TX: 缓存命中路径——重请求已下载过的 URL(硬盘还有其 song_cov* 条目,无网络)。
* 旧码: 缓存命中分支从不更新 g_cover_path → cover_current_path() 恒 NULL → MSG_META
* artwork 恒空 → 通知中心媒体卡无封面路径 → 回退显示歌名(真机症状)。
* 修复后: 换入时 g_cover_path 指向已落盘的缓存条目 → artwork 非空且可加载出该封面。 */
printf("== TX: cache-hit path -> cover_current_path() non-empty (launcher artwork) ==\n");
int bx = g_http_calls;
cover_request(URL_A, NULL);
CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cache-hit: cover A shown (no download)");
CHECK(g_http_calls == bx, "cache-hit: 0 http calls (from cache, calls=%d)", g_http_calls - bx);
const char *cpx = cover_current_path();
CHECK(cpx && cpx[0], "cache-hit: cover_current_path() non-empty (before fix: NULL)");
if (cpx && cpx[0]) {
meow_img *im = meow_img_load_file(cpx);
CHECK(im && im->hash == url_hash(URL_A), "cache-hit: reported path loads cover A");
if (im) meow_img_free(im);
}
cover_ack();
printf(g_fail ? "== COVER CHAIN SIM: FAIL ==\n" : "== COVER CHAIN SIM: ALL PASS ==\n");
return g_fail;
}
+291
View File
@@ -0,0 +1,291 @@
/* http_truncate_sim.c — 截断守卫宿主测试(0 真实网络)
*
* 背景(agent-355 挖出):core/ncm_http.c 的 http_run() 在 body 读取循环里,
* 对端于 Content-Length 未收满时提前关闭(mbedtls_ssl_read 返回 0 /
* MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY),loop 后仅判 overflow,否则一律 NCM_OK,
* 于是残缺 body 被调用方当完整成功 → ncm_cache_commit 把半截 .part/.tmp 转正。
*
* 本 sim mock 整个 mbedtls 传输层(仅符号桩,不链真实 libmbedtls),脚本化喂给
* ncm_http.c 一个可分块的对端响应流,验证 http_run(经公开入口
* ncm_http_download_status)对以下语义的判定:
* 定长(Content-Length) 未收满即关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO)
* 定长(Content-Length) 收满 → BS_DONE → NCM_OK
* Content-Length: 0(空 body) → BS_DONE → NCM_OK
* 无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK
* chunked 完整(0 分块末端) → BS_DONE → NCM_OK
* chunked 中途关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO)
*
* 编译(在 ncm_app 下):
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
* -c bench/http_truncate_sim.c -o /tmp/t.o
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
* -ffunction-sections -c core/ncm_http.c -o /tmp/t_http.o
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
* -ffunction-sections -c core/ncm_common.c -o /tmp/t_common.o
* gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "ncm_http.h"
#include "ncm_common.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/x509_crt.h"
/* ============================================================
* 传输层桩:脚本化"对端读流"。
* 每次 ncm_http_download_status 调用前用 tr_script(...) 装填脚本,
* 脚本用尽后 mbedtls_ssl_read 返回 g_close_rc(0=有序关闭;或
* MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY 模拟 TLS close_notify)。
* ============================================================ */
typedef struct { const unsigned char *buf; size_t len; } tr_chunk_t;
static const tr_chunk_t *g_script;
static int g_script_n;
static int g_script_i;
static int g_close_rc; /* 脚本耗尽后的 ssl_read 返回值 */
static void tr_script(const tr_chunk_t *chunks, int n, int close_rc) {
g_script = chunks; g_script_n = n; g_script_i = 0; g_close_rc = close_rc;
}
int mbedtls_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, size_t len) {
(void)ssl;
if (g_script_i < g_script_n) {
const tr_chunk_t *c = &g_script[g_script_i];
size_t n = c->len;
if (n > len) n = len; /* 脚本块恒 <= 4096(tmp),一般整块发出 */
memcpy(buf, c->buf, n);
g_script_i++;
return (int)n;
}
return g_close_rc;
}
int mbedtls_ssl_write(mbedtls_ssl_context *ssl, const unsigned char *buf, size_t len) {
(void)ssl; (void)buf; return (int)len; /* 请求头/体全部写成功 */
}
/* 其余 mbedtls 桩:连接/握手/配置全部成功,不触碰任何真实网络。 */
void mbedtls_net_init(mbedtls_net_context *ctx) { (void)ctx; }
int mbedtls_net_connect(mbedtls_net_context *ctx, const char *host, const char *port, int proto) {
(void)ctx; (void)host; (void)port; (void)proto; return 0;
}
int mbedtls_net_set_nonblock(mbedtls_net_context *ctx) { (void)ctx; return 0; }
int mbedtls_net_poll(mbedtls_net_context *ctx, uint32_t rw, uint32_t timeout) {
(void)ctx; (void)rw; (void)timeout; return 1; /* 就绪 */
}
void mbedtls_net_free(mbedtls_net_context *ctx) { (void)ctx; }
int mbedtls_net_recv(void *ctx, unsigned char *buf, size_t len) {
(void)ctx; (void)buf; (void)len; return MBEDTLS_ERR_SSL_WANT_READ; /* 实际读由 mbedtls_ssl_read 桩接管 */
}
int mbedtls_net_send(void *ctx, const unsigned char *buf, size_t len) {
(void)ctx; (void)buf; return (int)len;
}
void mbedtls_ssl_init(mbedtls_ssl_context *ssl) { (void)ssl; }
void mbedtls_ssl_config_init(mbedtls_ssl_config *conf) { (void)conf; }
int mbedtls_ssl_config_defaults(mbedtls_ssl_config *conf, int endpoint, int transport, int preset) {
(void)conf; (void)endpoint; (void)transport; (void)preset; return 0;
}
void mbedtls_ssl_conf_authmode(mbedtls_ssl_config *conf, int authmode) { (void)conf; (void)authmode; }
void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config *conf, mbedtls_x509_crt *ca_chain, mbedtls_x509_crl *ca_crl) {
(void)conf; (void)ca_chain; (void)ca_crl;
}
void mbedtls_ssl_conf_rng(mbedtls_ssl_config *conf,
int (*f_rng)(void *, unsigned char *, size_t), void *p_rng) {
(void)conf; (void)f_rng; (void)p_rng;
}
int mbedtls_ssl_setup(mbedtls_ssl_context *ssl, const mbedtls_ssl_config *conf) {
(void)ssl; (void)conf; return 0;
}
int mbedtls_ssl_set_hostname(mbedtls_ssl_context *ssl, const char *hostname) {
(void)ssl; (void)hostname; return 0;
}
void mbedtls_ssl_set_bio(mbedtls_ssl_context *ssl, void *p_bio,
mbedtls_ssl_send_t *f_send, mbedtls_ssl_recv_t *f_recv,
mbedtls_ssl_recv_timeout_t *f_recv_timeout) {
(void)ssl; (void)p_bio; (void)f_send; (void)f_recv; (void)f_recv_timeout;
}
int mbedtls_ssl_handshake(mbedtls_ssl_context *ssl) { (void)ssl; return 0; }
void mbedtls_ssl_free(mbedtls_ssl_context *ssl) { (void)ssl; }
void mbedtls_ssl_config_free(mbedtls_ssl_config *conf) { (void)conf; }
void mbedtls_x509_crt_init(mbedtls_x509_crt *crt) { (void)crt; }
int mbedtls_x509_crt_parse_file(mbedtls_x509_crt *crt, const char *path) {
(void)crt; (void)path; return 0; /* CA 加载成功(validate=0 时不走此径,桩兜底) */
}
void mbedtls_x509_crt_free(mbedtls_x509_crt *crt) { (void)crt; }
/* ============================================================
* 下载回调 sink:累加接收到的 body 字节。
* ============================================================ */
typedef struct { unsigned char data[1 << 16]; size_t len; } sink_cb_t;
static int on_body(void *ctx, const unsigned char *data, size_t len) {
sink_cb_t *s = (sink_cb_t *)ctx;
if (s->len + len > sizeof s->data) return -1; /* 上限(不触发) */
memcpy(s->data + s->len, data, len);
s->len += len;
return 0;
}
/* 运行一个用例:装填脚本 → 发起 GET 下载 → 记录 rc / body_len / content_length */
typedef struct { int rc; size_t body_len; size_t content_length; } case_result_t;
static case_result_t run_case(const tr_chunk_t *script, int script_n, int close_rc) {
ncm_http_req_t req;
memset(&req, 0, sizeof req);
req.url = "https://ex.test/song.mp3";
req.method = "GET";
req.verify_ca = 0; /* 跳过 CA 校验,聚焦 body 终态判定 */
req.timeout_ms = 3000;
sink_cb_t sink; memset(&sink, 0, sizeof sink);
ncm_http_resp_t resp; memset(&resp, 0, sizeof resp);
tr_script(script, script_n, close_rc);
int rc = ncm_http_download_status(&req, on_body, &sink, 0, &resp, NULL);
case_result_t r;
r.rc = rc;
r.body_len = sink.len;
r.content_length = resp.content_length;
return r;
}
static int g_fail = 0;
static void check(const char *name, int ok, const char *detail) {
if (!ok) g_fail++;
printf(" %s %s\n", ok ? "PASS" : "FAIL", name);
if (detail) printf(" %s\n", detail);
}
int main(void) {
char dbg[160];
/* ---- 场景 1:定长(CL=100) 对端提前关闭(只发 50 字节)→ 截断 ---- */
{
static const unsigned char hdrs[] =
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
static unsigned char chunk[512];
size_t hlen = sizeof hdrs - 1;
memcpy(chunk, hdrs, hlen);
for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'A';
tr_chunk_t script[1];
script[0].buf = chunk; script[0].len = hlen + 50;
case_result_t r = run_case(script, 1, 0 /* 对端关闭 */);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu content_length=%zu",
r.rc, r.body_len, r.content_length);
check("truncate_fixed(CL=100, 50B, close) -> NCM_ERR_IO",
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg);
}
/* ---- 场景 2:定长(CL=100) 完整收满 → BS_DONE → NCM_OK ---- */
{
static const unsigned char hdrs[] =
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
static unsigned char chunk[512];
size_t hlen = sizeof hdrs - 1;
memcpy(chunk, hdrs, hlen);
for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'B';
tr_chunk_t script[1];
script[0].buf = chunk; script[0].len = hlen + 100;
case_result_t r = run_case(script, 1, 0);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("normal_fixed(CL=100, 100B) -> NCM_OK",
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
}
/* ---- 场景 3:定长(CL=100) 两次读到收满(多块)→ NCM_OK ---- */
{
static const unsigned char hdrs[] =
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
static unsigned char part1[512], part2[64];
size_t hlen = sizeof hdrs - 1;
memcpy(part1, hdrs, hlen);
for (size_t i = 0; i < 60; i++) part1[hlen + i] = 'C';
for (size_t i = 0; i < 40; i++) part2[i] = 'D';
tr_chunk_t script[2];
script[0].buf = part1; script[0].len = hlen + 60;
script[1].buf = part2; script[1].len = 40;
case_result_t r = run_case(script, 2, 0);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("normal_fixed_multi(CL=100, 60+40B) -> NCM_OK",
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
}
/* ---- 场景 4:Content-Length: 0(空 body)→ BS_DONE → NCM_OK ---- */
{
static const unsigned char chunk[] =
"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n";
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
case_result_t r = run_case(script, 1, 0);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("cl_zero(CL=0) -> NCM_OK empty",
(r.rc == NCM_OK) && (r.body_len == 0), dbg);
}
/* ---- 场景 5:无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK ---- */
{
static const unsigned char hdrs[] =
"HTTP/1.1 200 OK\r\n\r\n"; /* 无 CL、无 chunked */
static unsigned char chunk[512];
size_t hlen = sizeof hdrs - 1;
memcpy(chunk, hdrs, hlen);
for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'E';
tr_chunk_t script[1];
script[0].buf = chunk; script[0].len = hlen + 100;
case_result_t r = run_case(script, 1, 0);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("close_delimited(no CL, 100B, close) -> NCM_OK",
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
}
/* ---- 场景 6:定长(CL=100) 提前关闭,且用 TLS close_notify 模拟 → 截断 ---- */
{
static const unsigned char hdrs[] =
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
static unsigned char chunk[512];
size_t hlen = sizeof hdrs - 1;
memcpy(chunk, hdrs, hlen);
for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'F';
tr_chunk_t script[1];
script[0].buf = chunk; script[0].len = hlen + 50;
case_result_t r = run_case(script, 1, MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("truncate_close_notify(CL=100, 50B) -> NCM_ERR_IO",
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg);
}
/* ---- 场景 7:chunked 完整(0 分块末端)→ BS_DONE → NCM_OK ---- */
{
static const unsigned char chunk[] =
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
"A\r\n0123456789\r\n" /* 16 进制 0xA = 10 字节 */
"0\r\n\r\n";
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
case_result_t r = run_case(script, 1, 0);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("chunked_full(10B + 0-chunk) -> NCM_OK",
(r.rc == NCM_OK) && (r.body_len == 10), dbg);
}
/* ---- 场景 8:chunked 中途关闭(数据未收完)→ 截断 → 非 NCM_OK ---- */
{
static const unsigned char chunk[] =
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
"5\r\nhel"; /* 宣称 5 字节,只发了 3,未到 0 分块 */
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
case_result_t r = run_case(script, 1, 0);
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
check("chunked_truncated(3/5B, close) -> NCM_ERR_IO",
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO), dbg);
}
printf("\n== HTTP TRUNCATE SIM: %s ==\n", g_fail ? "FAIL" : "ALL PASS");
return g_fail ? 1 : 0;
}
+120
View File
@@ -0,0 +1,120 @@
/* bench/mem_release_sim.c — 宿主「彻底释放」路径验证(agent 32bit OOM 专项)。
*
* 目的:验证「32bit(>16bit)→S32 解码 播完/切歌 是否真正释放」——ncm_dec_open(整文件)→
* ncm_dec_read 全量(dr_flac S32 满幅路径)→ ncm_dec_close(dr_flac_close + stream_src_close)
* 循环 N 次,断言进程匿名 RSS(RssAnon,OOM 高危口径)**不单调累积**(播完回落基线),
* 且解码中峰值有界(32bit 解码缓冲存在但有限,非 40MB 级)。
*
* 依赖:仅 app/play 解码层(decoder.c + stream_src.c + dr_flac_impl + minimp3_impl),
* 无 core/SDK/app.c——本 sim 是解码链路的最小区块,直接量「解码器 buff 播完 free」。
* 编译(ncm_app 目录下):
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
* -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \
* -c bench/mem_release_sim.c -o /tmp/mr.o
* gcc -O2 -Wall -Wextra ... -c app/play/stream_src.c -o /tmp/mr_ss.o
* gcc -O2 -Wall -Wextra ... -c app/play/decoder.c -o /tmp/mr_dec.o
* gcc -O2 -c -Ivendor/minimp3 app/play/minimp3_impl.c -o /tmp/mr_mp3.o
* gcc -O2 -c -Ivendor/dr_flac app/play/dr_flac_impl.c -o /tmp/mr_flac.o
* gcc -O2 -o mem_release_sim /tmp/mr.o /tmp/mr_ss.o /tmp/mr_dec.o /tmp/mr_mp3.o /tmp/mr_flac.o -lpthread -lm
* 运行:./mem_release_sim <flac 24bit 路径> [iters] (iters 默认 5)
* 断言(PASS 才 exit 0):
* - RssAnon[最后] - RssAnon[基准] < 256KB(播完回落,无累积);
* - 解码中峰值有界(peak - 基准 < 8MB,32bit 解码缓冲不是 40MB 级);
* - 总帧数 > 0 且 bits==32(确实走了 S32 满幅路径——本 sim 只受 24bit 输入触发)。
*/
#include "../app/play/decoder.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define READ_CHUNK_FRAMES 4096
#define ITER_DEFAULT 5
#define SETTLE_TOLERANCE_KB (256) /* 播完回落允许抖动(<256KB 视为无累积) */
#define PEAK_BOUND_KB (8 * 1024) /* 解码中峰值必须 <8MB(区别于 40MB 级异常) */
/* 静默 meow_log(本 sim 不链 core;decoder/stream_src 不调它,仅占位防漏链) */
void meow_log(int lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
static long rss_anon_kb(void)
{
FILE *f = fopen("/proc/self/status", "r");
if (!f) return -1;
char line[256];
long v = -1;
while (fgets(line, sizeof line, f))
if (!strncmp(line, "RssAnon:", 8)) { v = atol(line + 8); break; }
fclose(f);
return v;
}
int main(int argc, char **argv)
{
if (argc < 2) {
printf("usage: mem_release_sim <flac_path> [iters]\n");
return 2;
}
const char *path = argv[1];
int iters = (argc > 2) ? atoi(argv[2]) : ITER_DEFAULT;
if (iters < 2) iters = 2;
long base = rss_anon_kb();
if (base < 0) { printf("FAIL cannot read RssAnon\n"); return 1; }
printf("mem_release[0] baseline RssAnon=%ldKB\n", base);
int first_dec_ok = 0;
long prev_after = 0, last_after = 0; /* 最近两次播完 close 后的 RssAnon */
for (int i = 0; i < iters; i++) {
ncm_decoder_t *dec = NULL;
int rate = 0, ch = 0, bits = 0;
int64_t tf = 0;
if (ncm_dec_open(&dec, path, &rate, &ch, &bits, &tf) != 0) {
printf("FAIL iter%d ncm_dec_open (%s)\n", i, path);
return 1;
}
if (i == 0) {
printf("mem_release: fmt=%s %dHz/%dch/%dbit total_frames=%lld (S32 path=%d)\n",
ncm_dec_format(dec), rate, ch, bits, (long long)tf, bits > 16);
first_dec_ok = 1;
}
/* 全量读(S32 满幅路径),测解码中匿名峰值 */
static int32_t buf[READ_CHUNK_FRAMES * 2];
long peak = 0;
int64_t frames = 0, got = 0;
do {
got = ncm_dec_read(dec, buf, READ_CHUNK_FRAMES);
frames += got;
long r = rss_anon_kb();
if (r > peak) peak = r;
} while (got > 0);
printf("mem_release[%d] after decode: frames=%lld peak_RssAnon=%ldKB\n",
i + 1, (long long)frames, peak);
ncm_dec_close(dec); /* drflac_close + stream_src_close(整文件静态源) */
long r = rss_anon_kb();
prev_after = last_after;
last_after = r;
printf("mem_release[%d] after close RssAnon=%ldKB (delta=%ldKB)\n",
i + 1, r, r - base);
/* 解码中峰值有界:32bit 解码缓冲不是 40MB 级 */
if (peak - base > PEAK_BOUND_KB) {
printf("FAIL iter%d decode peak %ldKB - base %ldKB = %ldKB > %dKB (异常高)\n",
i, peak, base, peak - base, PEAK_BOUND_KB);
return 1;
}
}
/* 累积断言:最后两次播完后的 RssAnon 相对基线的增量都 < 容忍(无单调上升) */
long d1 = last_after - base;
long d2 = prev_after - base;
int no_accum = (d1 < SETTLE_TOLERANCE_KB) && (d2 < SETTLE_TOLERANCE_KB);
printf("mem_release: settle delta last=%ldKB prev=%ldKB -> %s\n",
d1, d2, no_accum ? "PASS(无累积)" : "FAIL(有残留)");
int ok = first_dec_ok && no_accum;
printf("mem_release: %s\n", ok ? "PASS" : "FAIL");
return ok ? 0 : 1;
}
+95
View File
@@ -0,0 +1,95 @@
/* bench/mem_watch_sim.c — 宿主「边播监控取证」阈值/峰值验证(agent 32bit OOM 取证)。
*
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
* A + C 分别归 SDK meow_cachegov / meow_svc_perf;本 sim 只验证 mem_watch 的**取证**语义:
* 三档 level 标注滞回(normal→high→critical→滞回→normal)+ song_end 重置峰值 + song_begin 起算。
* (旧版「dl_paused/prefetch_paused 降载断言」已随降载上收移除——那由 meow_cachegov_sim
* 与 svc_perf test_cachegov_monitor 覆盖。)
*
* 依赖:仅 app/play/mem_watch.c(+ meow_log 桩)。
* 编译(ncm_app 目录下):
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -c bench/mem_watch_sim.c -o /tmp/mw.o
* gcc -O2 -Wall -Wextra -I. -I../sdk -c app/play/mem_watch.c -o /tmp/mw_mod.o
* gcc -O2 -o mem_watch_sim /tmp/mw.o /tmp/mw_mod.o -lpthread -lm
* 运行:./mem_watch_sim (exit 0=PASS / 1=FAIL)
*/
#include "../app/play/mem_watch.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* meow_log 桩:打印(便于看阈值过渡;sim 仅验证 mem_watch 逻辑,非真实日志系统) */
void meow_log(int lvl, const char *fmt, ...)
{
(void)lvl;
char b[256]; va_list ap; va_start(ap, fmt);
vsnprintf(b, sizeof b, fmt, ap); va_end(ap);
printf(" [mem.log] %s\n", b);
}
static int fails;
#define CHECK(cond, msg) do { \
if (!(cond)) { printf("FAIL %s\n", msg); fails++; } \
else { printf("ok %s\n", msg); } } while (0)
/* 等监控线程至少走过 2 个周期(feed 后确保已采样) */
static void settle_ms(int m) { usleep((useconds_t)m * 1000u); }
int main(void)
{
/* 用 env 设定短周期(50ms)与干净阈值,加速确定性判定 */
setenv("NCM_MEM_WATCH_PERIOD_MS", "50", 1);
setenv("NCM_MEM_WATCH_HIGH_KB", "30720", 1); /* 30MB */
setenv("NCM_MEM_WATCH_LOW_KB", "20480", 1); /* 20MB */
setenv("NCM_MEM_WATCH_CRITICAL_KB", "40960", 1); /* 40MB */
if (mem_watch_start() != 0) { printf("FAIL mem_watch_start\n"); return 1; }
printf("started (period=50ms H=30M L=20M C=40M)\n");
/* 1) 正常区 */
mem_watch_set_forced_kb(10000);
settle_ms(150);
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "level==NORMAL @10MB");
/* 2) 升到 HIGH */
mem_watch_set_forced_kb(35000);
settle_ms(150);
CHECK(mem_watch_level() == NCM_MEM_LVL_HIGH, "level==HIGH @35MB");
/* 3) 升到 CRITICAL */
mem_watch_set_forced_kb(45000);
settle_ms(150);
CHECK(mem_watch_level() == NCM_MEM_LVL_CRITICAL, "level==CRITICAL @45MB");
/* 4) 滞回:落在 LOW..HIGH 之间 -> 保持 CRITICAL(防抖动) */
mem_watch_set_forced_kb(25000);
settle_ms(150);
CHECK(mem_watch_level() == NCM_MEM_LVL_CRITICAL, "level stays CRITICAL @25MB (hysteresis)");
/* 5) 回到正常:低于 LOW -> NORMAL */
mem_watch_set_forced_kb(15000);
settle_ms(150);
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "level==NORMAL @15MB");
/* 6) 调度器释放钩子:song_end 重置峰值 + song_begin 起算 + rss_anon 反映注入值 */
mem_watch_set_forced_kb(20000);
settle_ms(100);
mem_watch_song_begin();
mem_watch_set_forced_kb(37000); /* 本曲峰值冲到 37MB */
settle_ms(100);
long anon = mem_watch_rss_anon_kb();
CHECK(anon == 37000, "rss_anon_kb reflects forced value");
mem_watch_song_end(); /* 释放钩子:落峰值+重置 */
settle_ms(100);
mem_watch_song_begin();
mem_watch_set_forced_kb(12000); /* 新曲:峰值应从新起算 */
settle_ms(100);
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "after song_end+begin, level resets to NORMAL");
mem_watch_stop();
printf("mem_watch_sim: %s\n", fails ? "FAIL" : "PASS");
return fails ? 1 : 0;
}
+130
View File
@@ -0,0 +1,130 @@
/* bench/nav_exit_sim.c — 任务2:home 根页左滑「回桌面后台」宿主验证。
*
* 场景(0 真实请求/0 真实 launcher):fake launcher IPC socket(MEOW_IPC_SOCK)。调
* ipc_media_nav_exit(),断言:
* ① fake launcher 收到一条 MSG_NAV 帧,nav_type==MEOW_NAV_EXIT、app_id=="dev.player.ncm";
* ② 本函数**不**置 g_app_quit(进程保留,媒体会话继续上报 PLAYING,任务2 ③ 保活前提)。
* 这验证「从网易云主页面侧滑返回不再退出进程,而请求 launcher 退后台(有歌在播则 M4 保活)」。
*
* 编译(宿主,ncm_app 目录下):
* gcc -O1 -DJSMN_HEADER -I. -Icore -Iapp -I../sdk -I../meow_launcherd \
* -c bench/nav_exit_sim.c -o /tmp/ne.o
* gcc -O1 -DJSMN_HEADER -I. -Icore -Iapp -I../sdk -I../meow_launcherd \
* -c app/play/ipc_media.c -o /tmp/ne_ipc.o
* gcc -O1 -o nav_exit_sim /tmp/ne.o /tmp/ne_ipc.o -lpthread
* 运行:./nav_exit_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include "meow_ipc.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 1;
int g_host_mock = 0;
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
while (src[n]) n++;
return n;
}
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
/* ipc_media.c 的其余依赖(本 sim 不驱动播放/封面/歌词,仅服务链接与消息类型) */
uint64_t meow_ui_time_ms(void) { return 0; }
int player_manager_playing_song(void) { return g_play.cur_song; }
int player_manager_current_song(ncm_song_t *out) { (void)out; return -1; }
void player_manager_pause(int on) { (void)on; }
void player_manager_next(int dir) { (void)dir; }
void player_manager_seek(int64_t ms) { (void)ms; }
int player_manager_state(void) { return g_play.state; }
const char *cover_current_path(void) { return NULL; }
int lyric_count(void) { return 0; }
/* ---- fake launcher ---- */
static const char *g_sock = "/tmp/nav_exit_launcher.sock";
static uint16_t g_got_type = 0;
static int g_got_nav_type = -1;
static char g_got_app[MEOW_APP_ID_MAX];
static int g_got = 0;
static void *server(void *arg)
{
(void)arg;
unlink(g_sock);
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0) return NULL;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) { close(lfd); return NULL; }
listen(lfd, 1);
int fd = accept(lfd, NULL, NULL);
if (fd < 0) { close(lfd); return NULL; }
uint8_t rbuf[MEOW_IPC_MAX];
ssize_t n = read(fd, rbuf, sizeof rbuf);
if (n >= (ssize_t)MEOW_IPC_HDR_LEN) {
uint32_t magic, len;
uint16_t ver, type;
memcpy(&magic, rbuf, 4); memcpy(&ver, rbuf + 4, 2); memcpy(&type, rbuf + 6, 2);
memcpy(&len, rbuf + 12, 4);
if (magic == MEOW_IPC_MAGIC && ver == MEOW_IPC_VERSION && len <= (uint32_t)(n - MEOW_IPC_HDR_LEN)) {
g_got_type = type;
if (type == MSG_NAV && len >= sizeof(m_nav_t)) {
m_nav_t nv;
memcpy(&nv, rbuf + MEOW_IPC_HDR_LEN, sizeof nv);
g_got_nav_type = (int)nv.nav_type;
nv.app_id[MEOW_APP_ID_MAX - 1] = 0;
snprintf(g_got_app, sizeof g_got_app, "%s", nv.app_id);
}
g_got = 1;
}
}
close(fd); close(lfd);
return NULL;
}
int main(void)
{
int fail = 0;
setenv("MEOW_IPC_SOCK", g_sock, 1);
pthread_t th;
if (pthread_create(&th, NULL, server, NULL) != 0) { printf("FAIL server thread\n"); return 1; }
usleep(50000);
int rc = ipc_media_nav_exit();
pthread_join(th, NULL);
if (rc != 0) { printf("FAIL ipc_media_nav_exit rc=%d\n", rc); fail = 1; }
if (!g_got) { printf("FAIL fake launcher received no frame\n"); fail = 1; }
else if (g_got_type != MSG_NAV) {
printf("FAIL expected MSG_NAV (0x%04x) got 0x%04x\n", MSG_NAV, g_got_type);
fail = 1;
} else if (g_got_nav_type != MEOW_NAV_EXIT) {
printf("FAIL expected MEOW_NAV_EXIT got %d\n", g_got_nav_type);
fail = 1;
} else if (strcmp(g_got_app, "dev.player.ncm") != 0) {
printf("FAIL expected app_id dev.player.ncm got \"%s\"\n", g_got_app);
fail = 1;
} else {
printf("PASS nav_exit: type=0x%04x nav_type=EXIT app=%s\n", g_got_type, g_got_app);
}
if (g_app_quit != 0) { printf("FAIL g_app_quit set (process must stay alive for bg keepalive)\n"); fail = 1; }
else printf("PASS nav_exit did NOT set g_app_quit (process/alive, media keeps reporting PLAYING)\n");
unlink(g_sock);
if (fail) { printf("nav_exit_sim FAIL\n"); return 1; }
printf("nav_exit_sim PASS\n");
return 0;
}
+27
View File
@@ -151,6 +151,7 @@ int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, ncm_ca
*w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK;
}
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; }
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; } /* 链接占位:drop_finished 对 .tmp 无害 */
int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *d, size_t l) { (void)w; (void)d; (void)l; return NCM_OK; }
int ncm_cache_commit(ncm_cache_writer_t *w) { free(w); g_cache_ok = 1; g_commits++; return NCM_OK; }
int ncm_cache_abort(ncm_cache_writer_t *w)
@@ -617,6 +618,32 @@ int main(void)
g_eof = 1;
wait_state(NCM_PC_STOPPED, 3000);
printf("== P14: 点「正在播的同曲」-> 只聚焦不重播(任务3 re-entry,非 P12 切档)==\n");
usleep(300 * 1000);
g_eof = 0; g_reads_left = -1; g_url_rc = NCM_OK; g_fee = 0;
g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0;
g_vip = NCM_VIP_NONE; g_downloads = 0; g_url_calls = 0; g_last_key[0] = 0; g_last_ext[0] = 0;
player_manager_stop(); /* 清 P13 可能残留(STOPPED 或已自动顺延) */
player_manager_set_quality(NCM_LEVEL_STANDARD, 10);
g_cache_ok = 1; /* standard 缓存命中 */
player_manager_play(0);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "song0 playing at standard (cache hit)");
int opens_before = g_opens;
int writes_before = g_writes;
/* 任务3:歌单里点「正在播的同曲」(**未** set_quality → pending_replay=0)→ 只聚焦不重播:
* 同步返回后状态仍是 PLAYING(若是重播此刻必为 BUFFER),线程窗口内不得新增 audio_out_open。 */
player_manager_play(0);
CHECK(player_manager_state() == NCM_PC_PLAYING,
"re-enter same idx -> state stays PLAYING immediately (no restart to BUFFER)");
usleep(300 * 1000); /* 若真发生重播,线程会重跑 run_song 再开 audio */
CHECK(g_opens == opens_before,
"re-enter same idx -> no new audio_out_open (no restart), opens=%d/%d", g_opens, opens_before);
CHECK(g_writes > writes_before,
"re-enter same idx -> same stream kept playing (writes %d->%d)", writes_before, g_writes);
player_manager_stop();
wait_state(NCM_PC_STOPPED, 2000); /* 干净收尾(任务3 复验后置 STOPPED,勿自动顺延) */
g_cache_ok = 0; g_cache_real[0] = 0;
usleep(200 * 1000);
player_manager_stop_thread();
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
BIN
View File
Binary file not shown.
+171
View File
@@ -0,0 +1,171 @@
/* bench/vol_sync_sim.c — 任务1:开播前「系统当前音量 → audiod」下发宿主验证。
*
* 场景(0 真实请求/0 真实 audiod):fake audiod socket(AF_UNIX SOCK_STREAM)+ 临时 settings.conf
* (audio.volume = <V>,读改写单键同源同键)。调 audio_out_sync_volume(),断言 fake audiod 收到
* 一条 MSG_AUDIO_VOLUME 帧,且 pct == settings 的 audio.volume。这验证「打开音频会话时把系统
* 音量同步下发到 audiod」的根因修复(未播放时 DAC 停在默认/残留音量 → 动音量键才追设定值)。
*
* 另验证:音量语义只传 pct(0-100)——反转/真静音(255) 由 audiod dac_vol_set 负责,本层不碰。
*
* 编译(宿主,ncm_app 目录下):
* gcc -O1 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
* -c bench/vol_sync_sim.c -o /tmp/vs.o
* gcc -O1 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
* -c app/play/audio_out.c -o /tmp/vs_ao.o
* gcc -O1 -o vol_sync_sim /tmp/vs.o /tmp/vs_ao.o -lpthread
* 运行:./vol_sync_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include "meow_ipc.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play;
volatile int g_app_quit = 0;
volatile int g_net_ready = 1;
int g_host_mock = 0;
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
while (src[n]) n++;
return n;
}
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接 */
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return -1; }
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
int meow_audio_close(int sid) { (void)sid; return 0; }
int meow_audio_query(meow_audio_state *out) { (void)out; return -1; }
/* ---- fake audiod ---- */
static const char *g_sock = "/tmp/vol_sync_audiod.sock";
static uint16_t g_got_type = 0;
static int32_t g_got_pct = -1;
static int g_got = 0;
static volatile int g_nconn = 0; /* 收到多少条连接 */
static volatile int g_nframe = 0; /* 收到多少条 VOLUME 帧 */
static volatile int g_stop = 0;
/* fake audiod:接受多条连接(上限 32,超过即停——失败才触发,正常不该超),每条只读一帧
* VOLUME 然后等对端 EOF(ncm 侧 close)。统计连接数与帧数,供「重复 sync 不新建连接」断言。 */
static void *server(void *arg)
{
(void)arg;
unlink(g_sock);
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0) return NULL;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) { close(lfd); return NULL; }
listen(lfd, 16);
int lfl = fcntl(lfd, F_GETFL, 0);
if (lfl >= 0) fcntl(lfd, F_SETFL, lfl | O_NONBLOCK); /* 非阻塞:g_stop 能及时退出 */
while (!g_stop && g_nconn < 32) {
int fd = accept(lfd, NULL, NULL);
if (fd < 0) { usleep(20000); continue; } /* EAGAIN:无连接,短睡重试 */
g_nconn++;
/* 读满一帧(20B)或 EOF:stream socket read 可分段返回,须循环凑满再判。 */
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
ssize_t n = 0;
while (n < (ssize_t)sizeof frame) {
ssize_t r = read(fd, frame + n, sizeof frame - (size_t)n);
if (r <= 0) break; /* EOF / 错 */
n += r;
}
if (n >= (ssize_t)(MEOW_IPC_HDR_LEN + sizeof(int32_t))) {
uint32_t magic, len;
uint16_t ver, type;
memcpy(&magic, frame, 4); memcpy(&ver, frame + 4, 2); memcpy(&type, frame + 6, 2);
memcpy(&len, frame + 12, 4);
if (magic == MEOW_IPC_MAGIC && ver == MEOW_IPC_VERSION && type == MSG_AUDIO_VOLUME) {
g_got_type = type;
memcpy(&g_got_pct, frame + 16, sizeof g_got_pct);
g_got = 1;
g_nframe++;
}
}
close(fd);
}
close(lfd);
return NULL;
}
int main(void)
{
int fail = 0;
/* 临时 settings.conf:audio.volume = 24(读改写单键同源键),另带 max 干扰键 */
const char *cfg = "/tmp/vol_sync_settings.conf";
FILE *f = fopen(cfg, "w");
if (!f) { printf("FAIL cannot write settings\n"); return 1; }
fprintf(f, "# meow-settings 1\naudio.volume = 24\naudio.volume_max = 50\ndisplay.brightness = 80\n");
fclose(f);
setenv("MEOW_SETTINGS_FILE", cfg, 1);
setenv("MEOW_AUDIO_SOCK", g_sock, 1);
pthread_t th;
if (pthread_create(&th, NULL, server, NULL) != 0) { printf("FAIL server thread\n"); return 1; }
usleep(50000); /* 等 fake audiod listen */
/* 第一次 sync(首次 open):应下发一帧 VOLUME,pct==settings */
audio_out_sync_volume();
usleep(50000); /* 等服务线程 accept+读帧(主/服务线程异步) */
if (!g_got) { printf("FAIL fake audiod received no frame\n"); fail = 1; }
else if (g_got_type != MSG_AUDIO_VOLUME) {
printf("FAIL expected MSG_AUDIO_VOLUME (0x%04x) got 0x%04x\n", MSG_AUDIO_VOLUME, g_got_type);
fail = 1;
} else if (g_got_pct != 24) {
printf("FAIL expected pct=24 (settings audio.volume) got %d\n", (int)g_got_pct);
fail = 1;
} else {
printf("PASS audiod sync volume: type=0x%04x pct=%d == settings.audio.volume\n",
g_got_type, (int)g_got_pct);
}
/* 连接风暴回归(§2026-09-02 fd=N accept 根因):音量未变时反复 sync 不得新建连接。
* 模拟边下边播/切歌期频繁 audio_out_open → audio_out_sync_volume(同值,不重发)。 */
for (int i = 0; i < 50; i++) audio_out_sync_volume(); /* 同值 50 次:应零新增连接 */
usleep(50000);
if (g_nconn != 1) {
printf("FAIL unchanged-volume re-sync created %d connections (expect 1)\n", (int)g_nconn);
fail = 1;
} else {
printf("PASS unchanged-volume re-sync: 50 calls -> 1 connection (storm eliminated)\n");
}
/* 值变才重发:settings 改成 50 → 下一次 sync 应新建第 2 条连接 */
FILE *g = fopen(cfg, "w");
if (!g) { printf("FAIL cannot rewrite settings\n"); fail = 1; }
else {
fprintf(g, "# meow-settings 1\naudio.volume = 50\naudio.volume_max = 50\ndisplay.brightness = 80\n");
fclose(g);
}
audio_out_sync_volume();
usleep(50000);
if (g_nconn != 2 || g_got_pct != 50) {
printf("FAIL changed-volume re-sync: conns=%d (expect 2) last_pct=%d (expect 50)\n",
(int)g_nconn, (int)g_got_pct);
fail = 1;
} else {
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
}
g_stop = 1;
pthread_join(th, NULL);
unlink(g_sock);
unlink(cfg);
if (fail) { printf("vol_sync_sim FAIL\n"); return 1; }
printf("vol_sync_sim PASS\n");
return 0;
}
+2 -1
View File
@@ -21,7 +21,8 @@ APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c"
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c \
app/play/mem_watch.c"
echo "==> app+core (strict)"
for f in $APP $CORE; do
+1 -1
View File
@@ -20,7 +20,7 @@ echo "==> host SDK objects"
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin \
meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll meow_ui_str"
meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll meow_ui_str meow_cachegov"
for f in $SDK_SRC; do
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \
"$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
+23 -1
View File
@@ -49,9 +49,21 @@ gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
-Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include -c app/play/player_manager.c -o /tmp/pf_pm.o
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
-Icore -c app/play/stream_src.c -o /tmp/pf_ss.o
gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o /tmp/pf_ss.o -lpthread -lm
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
-Icore -c app/play/mem_watch.c -o /tmp/pf_mw.o # player_manager 引用(边播边监控取证,agent 32bit OOM)
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore \
-c ../sdk/meow_cachegov.c -o /tmp/pf_cg.o # player_manager 引 meow_cachegov_*(page-cache 治理 A)
gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o /tmp/pf_ss.o /tmp/pf_mw.o /tmp/pf_cg.o -lpthread -lm
run_sim player_flow_sim
echo "==> build mem_watch_sim(边播边监控阈值/滞回/调度器,宿主桩驱动;agent 32bit OOM。"
echo " mem_release_sim 无累积断言在 bench/ 头注释手动构建:需预生成 24bit flac 素材)"
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
-c bench/mem_watch_sim.c -o /tmp/mw.o
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -c app/play/mem_watch.c -o /tmp/mw_mod.o
gcc -O2 -o mem_watch_sim /tmp/mw.o /tmp/mw_mod.o -lpthread -lm
run_sim mem_watch_sim
echo "==> build window_sim"
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include \
-c bench/window_sim.c -o /tmp/ws.o
@@ -113,4 +125,14 @@ gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Iapp -Icore -I../sdk -I../meow_launcher
gcc -O2 -o account_cookie_sim /tmp/acs.o /tmp/acs_account.o -lpthread -lm
run_sim account_cookie_sim
echo "==> build http_truncate_sim (ncm_http 截断守卫宿主测试;mock mbedtls 传输层,0 真实网络)"
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
-c bench/http_truncate_sim.c -o /tmp/t.o
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
-ffunction-sections -c core/ncm_http.c -o /tmp/t_http.o
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
-ffunction-sections -c core/ncm_common.c -o /tmp/t_common.o
gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o
run_sim http_truncate_sim
echo "===== all sims done ====="
Binary file not shown.
+54 -11
View File
@@ -196,9 +196,54 @@ static int obj_bool(const ncm_json_t *j, int tok, const char *key) {
return (strcmp(s, "true") == 0);
}
/* UTF-8 整序列安全追加:把 src 按「多字节字符整序列」边界拷入 dst[*pos..],放不下整字符则
* 停止(不切半个字)。sz 为 dst 总大小(含 NUL 位)。用于 art_name 多歌手拼接的字节截断。 */
static void append_artist_name(char *dst, size_t *pos, size_t sz, const char *src)
{
size_t cap = sz - 1; /* 最多写 sz-1 字节,留 NUL */
size_t i = 0;
while (src[i] && *pos < cap) {
unsigned char c = (unsigned char)src[i];
size_t k = 1;
if (c < 0x80) k = 1;
else if ((c & 0xE0) == 0xC0) k = 2;
else if ((c & 0xF0) == 0xE0) k = 3;
else if ((c & 0xF8) == 0xF0) k = 4;
else { dst[(*pos)++] = src[i++]; continue; } /* 非法起始字节:单字节原样收 */
if (*pos + k > cap) break; /* 整字符放不下:丢弃(保住 UTF-8 完整性) */
memcpy(dst + *pos, src + i, k);
*pos += k; i += k;
}
dst[*pos] = 0;
}
/* 歌曲 ar[](歌手数组)多歌手名用 '/' 拼接进 art_name(≤ out_sz,UTF-8 整序列截断)。
* 单歌手行为不变(无 '/');空歌手条目跳过(避免连写分隔符「//」)。 */
static void fill_artist(const ncm_json_t *j, int e, char *out, size_t out_sz)
{
out[0] = 0;
int ar = ncm_json_obj_get(j, e, "ar");
if (ar < 0 || ncm_json_type(j, ar) != JSMN_ARRAY) return;
int nart = ncm_json_arr_len(j, ar);
size_t pos = 0;
for (int k = 0; k < nart; k++) {
int a = ncm_json_arr_get(j, ar, k);
if (a < 0) break;
char nm[128]; /* 单歌手名缓冲(obj_str 已截断到 127 字节) */
obj_str(j, a, "name", nm, sizeof nm);
if (!nm[0]) continue; /* 空条目:跳过,不加分隔符 */
if (pos > 0) { /* 第 2+ 歌手:先补 '/'(需 1 字节 + NUL) */
if (pos >= out_sz - 1) break; /* 无空间放分隔符 */
out[pos++] = '/';
out[pos] = 0;
}
append_artist_name(out, &pos, out_sz, nm);
}
}
/* 解析一处「歌曲对象数组」(song_detail / cloudsearch / track_all 离线解析 共用)。
* 元素字段:id/name/ar[0].name/al.name/al.picUrl/dt/fee。id 可为数字或字符串(均以文本拷贝,
* 兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
* 元素字段:id/name/ar[].name(多歌手 '/' 拼接) /al.name/al.picUrl/dt/fee。id 可为数字或
* 字符串(均以文本拷贝,兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int max, int *out_n)
{
*out_n = 0;
@@ -213,11 +258,7 @@ static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int
obj_str(j, e, "name", out[i].name, sizeof out[i].name);
out[i].duration_ms = obj_int(j, e, "dt");
out[i].fee = (int)obj_int(j, e, "fee");
int ar = ncm_json_obj_get(j, e, "ar");
if (ar >= 0 && ncm_json_type(j, ar) == JSMN_ARRAY) {
int a0 = ncm_json_arr_get(j, ar, 0);
if (a0 >= 0) obj_str(j, a0, "name", out[i].art_name, sizeof out[i].art_name);
}
fill_artist(j, e, out[i].art_name, sizeof out[i].art_name);
int al = ncm_json_obj_get(j, e, "al");
if (al >= 0) {
obj_str(j, al, "name", out[i].album_name, sizeof out[i].album_name);
@@ -371,13 +412,15 @@ static int level_from_str(const char *s) {
}
/* 账户权限允许的最高档(§20.3,与 app/logic/account.c account_level_cap 同口径):
* NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND。core 侧权威实现,供降级链自钳顶。 */
* NONE→EXHIGH / CVIP→HIRES / SVIP|SUPER→SURROUND。core 侧权威实现,供降级链自钳顶。
* SUPER(超级会员)为最高档,与 SVIP 同享全景声顶档。 */
static int level_cap_for_vip(int vip)
{
switch (vip) {
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
default: return NCM_LEVEL_EXHIGH;
case NCM_VIP_SVIP:
case NCM_VIP_SUPER: return NCM_LEVEL_SURROUND;
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
default: return NCM_LEVEL_EXHIGH;
}
}
+12 -6
View File
@@ -40,13 +40,19 @@ enum {
const char *ncm_api_level_str(int level); /* API level 串(SKY/SURROUND 同为 "sky") */
const char *ncm_api_level_key(int level); /* 缓存键唯一后缀(SKY="sky"/SURROUND="sky_c51") */
/* 会员等级(vip/info 判定,BIZ_NOTES §1):SVIP(黑胶)权限 ⊇ CVIP(畅听/音乐包)。 */
/* 会员等级(vip/info 判定,BIZ_NOTES §1):用户语义 0=非会员 / 1=畅听会员(VIP) /
* 2=黑胶会员(CVIP) / 3=超级会员(SVIP)。枚举名保留旧名(CVIP/SVIP)避免大改,新增
* NCM_VIP_SUPER=3 为最高档;权限 SUPER ⊇ SVIP(黑胶) ⊇ CVIP(畅听/音乐包)。
* 注:ncm_api_parse_vip_info 仍只产出 NONE/CVIP/SVIP(关联音乐包=CVIP、黑胶=SVIP,
* 无独立“超级”字段);3 档目前经 account.json 持久化进入/显示(account_load 钳位已放行)。 */
enum {
NCM_VIP_NONE = 0,
NCM_VIP_CVIP = 1,
NCM_VIP_SVIP = 2,
NCM_VIP_NONE = 0,
NCM_VIP_CVIP = 1, /* 畅听会员/音乐包 */
NCM_VIP_SVIP = 2, /* 黑胶会员 */
NCM_VIP_SUPER = 3, /* 超级会员(最高档) */
};
/* 档位所需会员门槛:standard/higher/exhigh=NONE;lossless/hires=CVIP;其余=SVIP。 */
/* 档位所需会员门槛:standard/higher/exhigh=NONE;lossless/hires=CVIP;其余=SVIP。
* 门槛最高只到 SVIP(2)(超级会员=3 为 ⊇ 黑胶,天然通过所有门槛,无需单独门禁)。 */
int ncm_api_level_gate(int level);
typedef struct ncm_playlist {
@@ -65,7 +71,7 @@ typedef struct ncm_playlist {
typedef struct ncm_song {
char id[24];
char name[128];
char art_name[96]; /* 首个歌手名 */
char art_name[96]; /* 歌手名:多歌手用 '/' 拼接(UTF-8 整序列截断) */
char album_name[96];
char al_pic[256]; /* 专辑封面 picUrl(song_detail/cloudsearch 解析;播放页黑胶封面) */
int32_t duration_ms;
+5
View File
@@ -232,6 +232,11 @@ const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) {
return w ? w->tmp_path : "";
}
/* 写者 final(正式)路径(commit 后 rename 目标;曲终 drop_finished L0 DONTNEED 用)。 */
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w) {
return w ? w->final_path : "";
}
/* 半成品判定(阶段3,§5.1):.tmp(下载中)/ .part(中断残体)不属正式缓存条目,
* 不进 LRU 扫描/计数——否则正被下载播放的文件会被缓存上限误删(写者管理域)。 */
static int is_partial_name(const char *name)
+2
View File
@@ -61,6 +61,8 @@ int ncm_cache_commit(ncm_cache_writer_t *w); /* win; 返回 0=成功 */
int ncm_cache_abort(ncm_cache_writer_t *w);
/* 写者 .tmp 路径(只读):边下边播解码侧用它开只读 fd 建 stream_src。 */
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w);
/* 写者 final(正式)路径(只读):commit 后目标文件路径,用于曲终 drop_finished(L0 DONTNEED)。 */
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w);
/* ---- 阶段3:.part 断点续传 + 在播文件防淘汰(NCM_STREAM_PLAY §5) ----
* 命名约定:中断的 .tmp 保留为 final+".part"(残体),供下次播放「Range 续传」;
+7
View File
@@ -491,6 +491,13 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
}
}
if (sink->overflow) { ret = NCM_ERR_FULL; goto done; }
/* 截断守卫:body 未到达合法终态却已跳出读取循环(对端提前关闭 / PEER_CLOSE_NOTIFY)。
* 合法终态只有两种:BS_DONE(定长收满 / chunked 到 0 分块 / Content-Length:0,body_st_init 即置)
* 与 BS_CLOSE(无 Content-Length、依 RFC 靠对端关闭定界——关闭即合法结束)。
* body_st_init 里 BS_CLOSE 仅在 content_length==SIZE_MAX 且非 chunked 时进入;若 Content-Length
* 存在而未收满(此时必为 BS_LEN/BS_CHUNK_*),或 chunked 未到 0 分块就关闭,即为截断:
* 此时返回错误,调用方(song_src_dispose)走 abort_part 保留 .part,不得 commit 残缺文件。 */
if (bst.st != BS_DONE && bst.st != BS_CLOSE) { ret = NCM_ERR_IO; goto done; }
ret = NCM_OK;
done:
mbedtls_net_free(&net);
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more