feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)

脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
AskaEth committed 2026-11-04 20:02:28 +08:00
1 parent e87ccdf061
commit 7c7d891570
59 files changed
+5305 -590

No files matched your search

+4 -1
View File
@@ -537,7 +537,10 @@ int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 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 成功后已自动调;独立导出供宿主验证与复调。 */
每音频会话(新 audiod 流 sid)首同步一次 + 值变才重发——切歌/新建
会话时 DAC 音量不残留默认响度(修「切歌音量骤增」);同会话内不
重复建连(不做 fd 风暴)。audio_out_open SINK_AUDIOD 成功后已自动调;
独立导出供宿主验证与复调。 */
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
int audio_out_buffered_ms(void);
+17 -9
View File
@@ -233,20 +233,27 @@ static void audiod_sync_volume(int 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;
/* 会话身份 = audiod 流 sid(g_sid):切歌/新建音频会话 = 每次 audio_out_open 都向 audiod 开一条
* 新流、audiod 分配新 sid(单调不重用)。此前用进程级 g_vol_synced「首次同步一次」——切歌(新会话)
* 时音量不再被重发到 audiod,DAC 落在会话建立/重配后的默认/上次残留值(衰减量 0=最响),造成
* 「切歌音量骤增,按一下音量键才回落到设定值」(用户真机 2026-09-05)。audiod 只在 reconfigure
* 且参数变化时才 re-apply 音量(_audio_mark_reconfig_needed 同参数跳过),本会话重发是权威兜底。
* 修法:**每会话首同步一次**——新 sid(新会话)必重发一次当前音量;同会话内仅值变才重发。
* 每会话恰 1 条短连接,不回到「每次 open 都建连」的 fd 风暴(§2026-09-02 根因,vol_sync_sim 60 次
* 同值→1 连接)。音量值取自 settings.conf(audiod 权威同源同键),反转/真静音(255) 由 audiod
* dac_vol_set 负责,本层只传 pct(0-100)。 */
static int g_vol_synced_sid = -1; /* 上次音量同步针对的 audiod 流 sid;-1=从未(必发) */
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;
if (g_vol_synced_sid == g_sid && pct == g_vol_last) return 0; /* 本会话已同步且值未变:零新建连接 */
g_vol_synced_sid = g_sid;
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());
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s, sid=%d)",
pct, settings_conf_path(), g_sid);
return 0;
}
@@ -379,6 +386,7 @@ int audio_out_pause(int paused)
}
g_sid = sid;
meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid);
audio_out_sync_volume(); /* 新会话(新 sid):音量权威兜底重发一次(DAC 复位后不残留默认响度) */
return 0;
}
+38 -6
View File
@@ -1032,10 +1032,18 @@ static int g_lyr_thr_on;
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
/* 歌词拉取失败后的重试退避(ms,指数翻倍封顶 OS):掉线瞬间失败的请求**不**写死「无歌词」,
* 交回退避重试;网络恢复后下一轮到点即重拉成功(真机症状:掉线一会儿再重连后恒「暂无歌词」)。 */
#define LYRIC_RETRY_BASE_MS 1000
#define LYRIC_RETRY_MAX_MS (30 * 1000)
static void *lyric_worker(void *arg)
{
(void)arg;
char last_id[24] = "";
int last_ok = 1; /* last_id 对应结果是否成功(成功不重拉;失败记退避待重试) */
int retry_delay = 0; /* 失败后逐次翻倍的退避基值(ms) */
int64_t retry_at = 0; /* 单调时刻:下次重试;0=无待重试 */
while (!g_app_quit) {
usleep(300 * 1000);
char id[24];
@@ -1051,14 +1059,28 @@ static void *lyric_worker(void *arg)
g_lyr_state = 0;
pthread_mutex_unlock(&g_lyrmu);
last_id[0] = 0;
last_ok = 1;
retry_delay = 0;
retry_at = 0;
continue;
}
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);
int is_new = (strcmp(id, last_id) != 0);
if (is_new) {
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);
last_ok = 0;
retry_delay = 0;
retry_at = 0;
} else {
if (last_ok) continue; /* 同一曲已成功拉完(含 NCM_OK 空词=真无词),不重拉 */
if (meow_cachegov_prefetch_paused()) continue;
if (retry_at > ncm_monotonic_ms()) continue; /* 失败退避未到 */
/* 到点重试:保留 retry_delay 以便失败时继续翻倍 */
}
lyric_clear();
pthread_mutex_lock(&g_lyrmu);
g_lyr_state = 1; /* 加载中 */
@@ -1068,22 +1090,32 @@ static void *lyric_worker(void *arg)
char *tl = (char *)malloc(32 * 1024);
if (!lrc || !tl) {
free(lrc); free(tl);
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
retry_at = ncm_monotonic_ms() + retry_delay;
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
continue;
}
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
if (rc == NCM_OK) {
last_ok = 1; /* 真正结果:空词(NCM_OK 无词也算「已拉完」) */
retry_delay = 0;
retry_at = 0;
lyric_parse(lrc, tl);
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
} else {
/* 拉取失败(网络/HTTP/会话):不写死「无词」,交回退避重试;UI 呈「加载中」而非「暂无歌词」 */
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
retry_at = ncm_monotonic_ms() + retry_delay;
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
}
free(lrc); free(tl);
pthread_mutex_lock(&g_lyrmu);
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */
g_lyr_state = last_ok ? 2 : 0; /* 2=已拉完(可 0 行=无词);0=失败待重试(UI 呈加载中) */
pthread_mutex_unlock(&g_lyrmu);
}
return NULL;
+64 -30
View File
@@ -26,8 +26,12 @@ static volatile int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
* §17 滑动窗口:worker 两种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化首段;
* ②滑窗补载:物化视口区间 [g_ens_lo,g_ens_hi)(ensure_range,盘缓存优先)。 */
* §17 滑动窗口:worker 三种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化第 0 页
* (≤NCM_SONG_BATCH 首,一次一页——不再首屏连拉多批补满窗口,大歌单阻塞久且触发风控);
* ②顺序下一页(g_ens_page=1):仅当用户滑到已分页底部(余量 ≤NCM_PL_PAGE_EDGE 首)时拉一页,
* 成功推进 g_paged_hi,拉完该歌单全部曲目即止;③回滚补物化:已分页区间内被窗口挤出的
* 失效槽按视口 ±24 补(盘缓存命中为主,不占网络分页额度)。 */
#define NCM_PL_PAGE_EDGE 20 /* 懒加载触发余量(首):视口底距已分页底部 ≤20 首才拉下一页 */
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_plthr;
static int g_plthr_on;
@@ -37,9 +41,11 @@ static volatile int g_plloading;
static volatile int g_plloaded_idx;
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
static volatile int g_ens_lo, g_ens_hi; /* 补载/下一页目标区间(全局下标) */
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
static volatile int g_ens_page; /* 1=顺序下一页任务(成功推进 g_paged_hi) */
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
static volatile int g_paged_hi; /* 已分页高水位:从顶部连续物化到的全局下标(下一页起点) */
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
@@ -239,12 +245,15 @@ static void pl_draw_footer(void *ctx)
ncm_draw_footer(r_cont, foot);
}
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。 */
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。
* 拉到数据后必须让列表项重新绑定/重绘:src2 每帧 live 取行(占位行 ↔ 已物化歌名/歌手),
* 失效列表带 + 整帧重画把刚物化的歌名/歌手填进各项(修「拉完曲目行仍 ... 加载中」)。 */
static void pl_apply(int reset_scroll)
{
if (g_list) {
meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
const ncm_playlist_t *pcur = pl_cur();
if (g_tb && pcur) {
@@ -261,30 +270,24 @@ static void *playlist_worker(void *arg)
(void)arg;
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
ncm_playlist_t tpl;
int tsearch, tidx, tgen, first, lo, hi;
int tsearch, tidx, tgen, first, lo, hi, page;
pthread_mutex_lock(&g_plmu);
tpl = g_tgt.pl; tsearch = g_tgt.is_search;
tidx = g_tgt.idx; tgen = g_tgt.gen;
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi;
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi; page = g_ens_page;
pthread_mutex_unlock(&g_plmu);
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
int rc;
if (g_host_mock) rc = 0;
else if (first) {
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
/* 首屏:快照式加载(detail 全量 trackIds + 物化第 0 页)。ctx 校验代次/目标,过期自动丢弃。
* §懒加载:首屏只拉一页(≤NCM_SONG_BATCH 首,一次仅一批)——不再 ensure_range 补满整个
* 滑动窗口(旧版首屏连拉 ≤3 批:5k+ 大歌单「我喜欢的音乐」长时间卡住不出列表,连续
* 请求还触发风控);后续页由 pl_maybe_ensure 在用户滑到已分页底部时逐页懒加载。 */
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; /* 补填失败非致命:已物化槽保留 */
}
}
if (rc == NCM_OK)
g_paged_hi = (g_st.track_ids_count < NCM_SONG_BATCH) ? g_st.track_ids_count : NCM_SONG_BATCH;
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
@@ -292,8 +295,10 @@ static void *playlist_worker(void *arg)
"playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d",
tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc);
} else {
/* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */
/* 补载:物化目标区间(顺序下一页 / 回滚补物化;盘缓存优先,批网格补载)。
* 顺序下一页成功 → 已分页高水位推进到 hi;部分失败不推进(占位行保留,滑到底部重试本页)。 */
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
if (rc == NCM_OK && page && hi > g_paged_hi) g_paged_hi = hi;
}
pthread_mutex_lock(&g_plmu);
if (first) {
@@ -324,7 +329,25 @@ static void *playlist_worker(void *arg)
return NULL;
}
/* 视口驱动滑窗补载:视口 ±24 行有未物化槽位 → 发 ensure worker(已在载/失败态/宿主 mock 不发)。 */
/* 发补载 worker(单飞;调用方先判 g_plthr_on)。page=1 为顺序下一页任务。 */
static void pl_ensure_post(int lo, int hi, int page)
{
g_ens_lo = lo; g_ens_hi = hi;
g_ens_first = 0;
g_ens_page = page;
g_plloading = 1;
g_plthr_on = 1;
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
}
}
/* 懒加载分页触发(Bug 修①:旧版视口 ±24 触发 + 首屏连拉多批 → 大歌单连续拉多页、阻塞 + 风控):
* ① 顺序下一页:仅在视口底滑到「已分页底部」(余量 ≤NCM_PL_PAGE_EDGE 首)时拉下一页,
* 一次只拉一页(≤NCM_SONG_BATCH);g_paged_hi 达 track_ids_count = 全部拉完,停止。
* 远离已分页底部(快速猛滑到深处占位区)不拉——防连续拉多页触发风控。
* ② 回滚补物化:视口 ±24 落在「已分页区间内」的失效槽(窗口挤出)按旧逻辑补——
* 盘缓存(songmeta)命中为主,不消耗网络分页额度;区间外(未分页)一律不预拉。 */
static void pl_maybe_ensure(void)
{
if (g_plthr_on || g_host_mock) return;
@@ -334,23 +357,32 @@ static void pl_maybe_ensure(void)
int scroll = meow_listview_scroll(g_list);
int first = scroll / APP_ROW_H;
int last = (scroll + r_list.h) / APP_ROW_H + 1;
if (last > total) last = total;
int paged = g_paged_hi;
if (paged > total) paged = total; /* 防御:索引重建后高水位越界 */
/* ① 顺序下一页(懒加载) */
if (paged < total) {
int margin = paged - last; /* 视口底距已分页底部余量(首;负=已越过) */
if (margin <= NCM_PL_PAGE_EDGE && margin >= -NCM_PL_PAGE_EDGE) {
int hi = paged + NCM_SONG_BATCH;
if (hi > total) hi = total;
pl_ensure_post(paged, hi, 1);
return;
}
}
/* ② 回滚补物化(仅已分页区间内) */
int lo = first - 24, hi = last + 24;
if (lo < 0) lo = 0;
if (hi > total) hi = total;
if (hi > paged) hi = paged;
if (lo >= hi) return;
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */
g_ens_lo = lo; g_ens_hi = hi;
g_ens_first = 0;
g_plloading = 1;
g_plthr_on = 1;
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
}
pl_ensure_post(lo, hi, 0);
}
void playlist_refresh(void)
{
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; pl_apply(1); return; }
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; g_paged_hi = g_st.track_ids_count; pl_apply(1); return; }
if (g_plthr_on) return;
g_load_err = 0;
g_ens_err = 0;
@@ -373,7 +405,9 @@ void playlist_refresh(void)
if (!appstate_playlist_window_is(g_tgt.pl.id))
appstate_playlist_reset();
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
g_ens_first = 1; /* 首屏:重建索引 + 物化第 0 页 */
g_ens_page = 0;
g_paged_hi = 0; /* 分页高水位归零:首屏成功后由 worker 重标 */
g_pl_stale = 0;
g_plloading = 1;
g_plthr_on = 1;