AskaEth
ae4e4e53f6
feat(share): 接入系统分享入口 —— 分享文本 / 图片进会话(未登录先暂存,登录后落地)
主人 2026-10-04 提出的「分享到 App」的客户端半边。触发面就是系统分享菜单的
ACTION_SEND / ACTION_SEND_MULTIPLE;发送侧**完全复用既有路径**(短文本 WS、
长文本与图片 REST、大图先 /upload),不另起一套。
- Manifest:MainActivity 加 SEND / SEND_MULTIPLE 的 intent-filter,MIME 只声明
text/plain 与 image/*。刻意不声明 */*:对话路径上今天没有「文件」这个表示
(协同 02/F1 三缺),声明 */* 等于给出一个点进去必然失败的入口;收窄之后非图片
文件干脆不进分享菜单。另加 launchMode=singleTop 与 label(分享菜单里显示 Cyrene,
图标沿用应用启动图标,不另设一份会漂移的来源)。
- MainActivity:冷启动(onCreate)与热启动(onNewIntent + setIntent)两条路都接。
冷启动只在 savedInstanceState==null 时接 intent —— Activity 因旋转 / 深色切换重建
时系统会把同一个 intent 再送一遍,照单全收就会把同一条分享发第二次。
- 新增 domain/share(纯逻辑,可单测):
- ShareRules.extract:文本 / 单图 / 多图 / 空内容 / 只有 MIME 没有内容 /
EXTRA_STREAM 缺席时回落到 ClipData;去重与空白条目在**截断之前**处理
(重复项既不该挤掉真图,也不该被算成「截掉了」)。一次最多接 4 张,超出的
条数如实计数,不静默丢。正文一个字都不加(来源标记走独立 RoleSystem 消息 +
SenderName,拼进正文即违反红线,见 02/F4)。
- ShareRules.cacheFileName:文件名只由本端 token + 序号 + MIME 白名单扩展名构成,
外部 URI 的路径 / 显示名绝不参与(纯函数里连本端 token 都不信,非法字符折成 _)。
- PendingShareHolder:待落地内容的队列,take() 是原子出队 —— 一条内容只能被消费
一次(消费它的地方都会被重建:重组、切会话、Activity 重建),同时不把先后两条
挤在同一窗口的那次分享丢掉。
- 新增 app/ShareIntake:进程级单例(不塞 AppContainer,避免碰别的并行改动)。
图片在收到的那一刻就拷进 cacheDir 并换成**本端 file://**(content:// 的读授权活不过
登录 / 切页,拷不动的那几张如实计入「未发送」);进程启动清一次缓存目录
(那份副本只对产生它的进程有意义),运行期间不回收 —— 发送失败回滚到输入框之后
仍要读它。
- ChatViewModel:sendShared 与输入框的 send 共用 emitAndDeliver(同一套乐观气泡 /
幂等键 / 回滚),差别只有「不动用户草稿」(分享不是用户按的发送键)。一条消息只能
带一张图 ⇒ N 张图 = N 条消息,第一条带分享的正文;顺序在同一个协程里串起来。
长文本不截断,由既有 ChatSendRouting 判据改走 REST(WS 帧 4096 字节是断连不是报错)。
- ChatScreen:仅当**当前会话**的组合存在时认领(已访问过的会话 VM 都还活着,UI 层
认证才不会让旧会话抢走),落地前先等 WS 连接、上限 3 秒(冷启动必然撞上握手;超时
也照发,让「没网」退化成内容回到输入框这种看得见的结果,而不是停在看不见的地方)。
截断 / 读不到 / 空内容用 shareNotice 如实告知(8 秒后自动收走),与 error 分开。
- 测试 +23(ShareExtractionTest 18 / PendingShareHolderTest 5):文本、单图、多图、
空、只有 MIME、ClipData 兜底与去重、截断计数、超大文本不截断且落 REST、
文件名不可穿越、原子出队(并发 64 取 64 条不重不漏)。assembleDebug 绿,
testDebugUnitTest 353 → 376 全绿(failures/errors 皆 0)。
2026-10-04 18:05:27 +08:00
..
2026-10-04 18:05:27 +08:00
2026-09-08 21:48:40 +08:00
2026-09-08 20:40:58 +08:00