diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index 1eec39e..9acfc1f 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -2197,3 +2197,80 @@ infinity maximum height constraints, which is disallowed. - HEAD = **`3d8bc61`** = `origin/android`;worktree / 分支 / 构建锁**全清**;主仓唯一脏 = `devdocs/02`(后端在途 + 我的登记与四问)。 - 模拟器装的**是最新包**(含 W58~W62);App 已重启并连回**真后端**;`adb reverse`:`tcp:80 → 10942`(真后端)、`tcp:10942 → 10942`。 - 假后端 18080 进程**仍在跑**(带语音夹具;在后台任务里,到点会被系统收)。 + +## ★★ 锚点补记四十一 · 通知横幅 + 免打扰开关 + W63~W66 四批落地(2026-10-07 下午) + +### 1. 主人报的问题:「我在其他 APP 时没看到横幅」 + +**根因**:「她主动说的话」那条渠道的重要级是 **`IMPORTANCE_DEFAULT`(3)** —— 而**横幅(heads-up)要求 `HIGH`(4)**。代码注释里当初是**刻意**选 DEFAULT 的(「HIGH 会弹横幅盖住用户正在做的事,而这句话并没有急到那个程度」)⇒ 主人明确要横幅,即**口径变更**。 + +**修法(关键约束)**:🔴 **渠道的重要级一旦创建就改不了** —— 只有**用户**能在系统设置里改,App 改不动。所以光改常量对已建过渠道的设备**无效**,必须**换一个新渠道 id**: +- `PROACTIVE_CHANNEL_ID` = **`proactive_v2`**(`IMPORTANCE_HIGH`); +- 新增 `LEGACY_PROACTIVE_CHANNEL_ID = "proactive"`,建渠道时 `deleteNotificationChannel` 掉(重要级改不动,留着只会让系统设置里多一条永不出现的条目); +- `.setPriority(PRIORITY_HIGH)`(管 Android 7.x 及更早,那边还没渠道)。 + +**真机验收(`emulator-5554` + 假后端)**:注入一条 `proactive_message`,屏幕顶部弹出横幅「昔涟 · 现在 / 横幅验收:她主动说了一句」;`dumpsys notification` 显示 `channel=proactive_v2 importance=4` ✓ + +### 2. 主人要的「免打扰」开关(W66,`0e2df11` → 合并 `3b9936e`) + +需求原话:「**开启时弹通知不弹横幅,关闭时两者都弹,默认都弹**」。 + +**为什么只能是「两条渠道 + 切 id」**:同上 —— 重要级改不了 ⇒ 运行时无法开关横幅。**不能**用 `setSilent(true)`(那是完全静音,语义不是「只关横幅」)。 + +| | 弹横幅那档(`quiet=false`,**默认**) | 免打扰那档(`quiet=true`) | +|---|---|---| +| 渠道 id | `proactive_v2` | `proactive_quiet` | +| 重要级 | `IMPORTANCE_HIGH`(4) ⇒ 横幅 | `IMPORTANCE_DEFAULT`(3) ⇒ 提示音、**不弹横幅** | +| 渠道名 | 她主动说的话 | 她主动说的话(免打扰:只在通知栏) | + +- 判据纯函数 `proactiveChannelSpec(quiet)`(id/名/重要级/描述**成对给全**,防「换了 id 没换重要级」)+ `proactiveChannelId(quiet)`;`quiet` **刻意不给默认值**(让编译器拦住「忘了传」)。 +- **惰性创建**:只 ensure「这一次要用的那一条」(两条都建 ⇒ 从没开过免打扰的用户会平白多一条没见过的渠道);**另一条不删**(用户可能对它调过设置);只删那条再也发不到的 legacy `proactive`。 +- 设置项存进既有的 `AppPrefs`(键 `proactive_quiet`,**默认 false**),UI 在设置页新增的「**通知**」段(`PreferenceCard` 外包 `SwitchPreference`),文案写明「**不是静音**」。发通知那一刻现读热副本 ⇒ **开关一改,下一条就生效**(不必重启)。 +- ⚠️ 如实登记的毫秒级窗口:冷启动首读盘未回时读到默认 `false`,代价封顶是「多看到一次横幅」,不丢通知。 +- 全量 **123 suites / 1481 tests**(+2 suite / +8 tests)。 + +**真机验收**:设置页点开该开关(副文案从「**关(默认)**:她主…」变成「**已开:她主动找你说话只在通知栏提…**」);回后台再注入一条 ⇒ **屏幕顶部没有横幅**(截图:桌面干净),`dumpsys` 显示那条走的是 `channel=proactive_quiet importance=3`,通知栏里条目仍在、有提示音 ✓ + +### 3. W63:解析器去二次方(`0e834fc` → 合并 `3b9936e`) + +审查只归因了一处,子代理**找出两处**(都在 `domain/parse/ChatContentParser.kt`): +1. `hasContentAfter` 每个落单 `

` 现扫到串尾(约占 23s); +2. **`tokenize` 里 `P_BLOCK_RE.findAll(raw)` 的懒惰回溯**(约占 8.2s)—— 只修第 1 处时,20000 个落单 `

` 仍是 **8308ms**。 + +**它还推翻了我给的建议方案**:我建议「记下最后一个有效内容的下标」,但答案**对起点不单调**(`"

"` 上 `hasContentAfter(0)=false` 而 `(1)=true` —— 起点落在标签内部也算有正文)⇒ 改成**逐位置答案表**(`ContentAfterQuery`,惰性建表,常规消息一个字节都不建)+ 对 `tokenize` 做「先切掉最后一个 `

` 之后」的前缀优化。 + +**实测(JVM)**:`parse("

".repeat(N))`:**2000 → 5ms**(原 854)、**8000 → 12ms**(原 4254)、**20000 → 17ms**(原 26405)。`ChatContentParserTest` 套件从 31.5s 降到 0.15s。全量 **121 suites / 1473 tests**(+2)。 +**留了一处同类平方(如实登记、未修)**:`"

"`(``、末尾只有一个 `

`)仍走 `P_BLOCK_RE` 最坏路径,**改前改后一样**(20000 → 7.2s)。治它要手写线性匹配,会牵动气泡切分核心语义,超出「纯性能、语义不变」的范围 ⇒ **要不要另开切片等主人定**。 + +### 4. W64:分享回落链(`4fe5509` → 合并 `3b9936e`) + +修「只有 `EXTRA_SUBJECT`/`EXTRA_TITLE` 的分享**静默**」:**回落链** `EXTRA_TEXT` → `EXTRA_SUBJECT` → `EXTRA_TITLE`(取第一个非空白);三者都空/读不到 ⇒ 走**既有**「读不进来」路径(不再静默);「连值都没有」(误触分享)照旧静默。 +- 来源如实登记(`SharedContent.textSource`),用户看到一句「**这段文字来自对方的分享标题(对方没有提供正文)**」—— 走认领那一刻的**柔和横幅**,**绝不进正文**(有单测钉住 `shareConfirmBody` 逐字等于正文)。 +- 防御式读取**自己核过 SDK 源码**:`BaseBundle.getCharSequence` 自己 catch `ClassCastException` 回 null;会抛的是内部的 `unparcel()` → `BadParcelableException` ⇒ 用「读原始值 + `as?` + `runCatching`」两条都挡。 +- **子代理自己在设备上跑过** 4 条 fixture(只有 SUBJECT / 只有 TITLE / `--ei TEXT 42`(有值但类型读不出,今天=毫无反应)/ TEXT+SUBJECT 同时给 ⇒ TEXT 赢且不出提示),并截图确认。 +- 全量 **123 suites / 1494 tests**(+2 suite / +23 tests)。 +- ⚠️ 两条可选改进(**等主人点头**):① 把来源提示画进**面板内部**(Miuix `WindowDialog` 有 `summary` 参数,现调用点没传 ⇒ 加一行即可);② 让「读不进来」那句在**面板一开**就说(现在是发送时才说)。 + +### 5. W65:分享预览与气泡一致(`cdd58fa` → 合并 `3b9936e`) + +让预览**走同一套 `parse()`**(多段成块、``/`` 变灰斜体),做到「**所见即所发**」;**超窗判据重做**为「内容自然高(`onSizeChanged` 挂在 `verticalScroll` 里面读到的、不受这一窗上限影响的高)vs 窗口高(量到的行高 × 3)」,**两边都是这一帧真实排出的像素**(旧判据「行数 > 3」在多块之后覆盖不住窗口的量)。 +- **逐个核过每个 `verticalScroll` 前面的有限约束**(这条必须有,见锚点三十八):外层是 `sharePreviewMaxHeight(textExpanded)`(150/240dp 字面量);正文窗收起态是 `shareConfirmWindowCap(cap)`(唯一出口、恒有限),展开态**上限与滚动一起去掉** ⇒ 「无界高度 + 滚动」两种状态都不成立。 +- 推翻了 W61 那条钉住「预览 trim、气泡解析」分工的断言(口径变更,改成新口径并写明理由),其余断言未放松。 +- ⚠️ **待主人真机核对**(子代理只跑了单测):① 含 `

` 的分享 → 预览里**看不到尖括号**且与气泡字一致;② `

甲

乙

丙

` → 预览**三块分开**;③ `看着窗外` → 预览是灰斜体一行;④ 短分享**没有**「展开」;⑤ 面板打开/展开/收起三下**不崩**。 +- 它也如实登记:`onSizeChanged` 挂在 scroll 里面能读到「自然高度」这条是**按 Compose 内部实现推的**,若推错,症状是「展开」控件永不出现(②④两条真机现象就是在核它)。 + +### 6. ★ 国产 ROM 上「横幅」还需要什么(回答主人的第二个问题) + +**AOSP 层面(我们已经做对了)**:横幅的门槛就是**渠道重要级 = HIGH**(+ 7.x 的 `setPriority`、`CATEGORY_MESSAGE` 已在)。**不需要**「额外声明消息类型」—— Android 8+ 的**渠道本身就是那个声明**。 + +**国产 ROM 层面(App 做不到,只能引导用户去系统里开)**: +1. **该 App 的「横幅 / 悬浮通知」开关** —— MIUI/HyperOS、ColorOS 等在**渠道设置**里另有一个独立的横幅开关,默认可能关着; +2. **自启动 / 后台运行白名单 + 省电策略「无限制」** —— 这条管的不是横幅,是**进程能不能活着**(进程没了就收不到帧,也就没有通知); +3. **「后台弹出界面」权限**(MIUI/ColorOS 有)—— 主要影响从后台**弹 Activity**,对横幅通常不必要,但有些 ROM 会连带拦截。 + +⚠️ **本条与「推送」的关系(值得主人知道)**:上面全是「**进程还活着**」前提下的横幅。要做到「App 被划掉/被系统杀掉之后**仍然**收到通知」,**必须接厂商推送通道**(小米/华为/OPPO/vivo 各自的 Push,且**服务端也要接**)—— 那是另一个量级的工程(主人此前说「推送我继续研究」)。 + +### 7. 环境 +- HEAD = **`3b9936e`** = `origin/android`;worktree / 分支 / 构建锁**全清**;主仓唯一脏 = `devdocs/02`(后端在途 + 我的登记与四问)。 +- 全量真跑:**125 suites / 1509 tests / 0 failures / 0 errors**。 +- 模拟器装的**是最新包**(含 W58~W66);晚些已把 `tcp:80` 切回真后端。假后端 18080(带语音夹具 + `/__control/frame`)在后台任务里跑着。