Files
reader/android/contract.md
T
hectorqinandClaude Opus 5.5 120827bad6 docs: bring every document in line with the code
Audit of README, docs/, android/README.md, android/contract.md and CLAUDE.md
against the current server, web and Android code: V3 navigation and settings
locations, Docker Hub release images, GitHub clone URL and CI, missing API
endpoints and error codes, architecture of lazy chunks/themes/sources, media
pages and routes, the Android bridge contract, and the new file-management and
member-upload permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:03:45 +00:00

5.9 KiB
Raw Blame History

外壳与 H5 的接口

这份文件是外壳与 H5 之间的全部约定。两边分别用 Kotlin 和 TypeScript 写,互相无法类型检查, 所以以 web/src/android-bridge.d.ts 为准,由 android/scripts/check-shell.sh 双向核对。 改动任何一侧都要同步另一侧和本文件。

1. 启动流程

MainActivity 启动
  → WebHost 配置 WebView,注册 JS 接口与媒体消息通道
  → 加载 https://appassets.androidplatform.net/assets/index.html(+ 快捷方式对应的 #/media/... 片段)
  → H5 检测 window.ReaderAndroid,选择 Android 平台
  → 服务地址:用户已保存的地址 > configuredServerUrl()(构建时 -PdefaultServerUrl)> 留空待填
  → H5 自己完成登录、会话保存(存储层与浏览器相同)和后续所有请求

外壳不读写令牌、不向 localStorage 注入会话,也不 eval 业务脚本。资源来源 https://appassets.androidplatform.net 被 H5 视为无效的服务地址,服务端 CORS 单独放行它。

2. JS 接口

外壳用 addJavascriptInterface 注册两个对象。只有标注 @JavascriptInterface 的方法对页面可见。

window.ReaderAndroid(bridge/ReaderBridge.kt,当前 SHELL_VERSION = 3)

方法 返回 说明
shellVersion() number H5 要求 ≥ MIN_SHELL_VERSION(1),否则按普通浏览器运行
configuredServerUrl() string 构建时预设的服务地址,可为空;d.ts 中为可选方法
deviceLabel() string 写进进度记录的设备名
connectivity() string online / offline,来自系统连接管理器
watchConnectivity(cb) void 见下文「回调」
toast(message) void 原生 Toast
hasNetwork() boolean 当前是否有网络
viewport() string JSON:物理像素视口与密度
cacheUsage() string JSON:webViewBytes、dataBytes、availableBytes
diagnostics() string JSON:外壳版本、设备、系统、应用版本,用于问题报告
renderPage(request) boolean shell ≥ 2。见第 3 节
hidePage() void 移除原生页面,露出 WebView
canOpenDocument(mediaType) boolean 目前总是 false,PDF 留在 WebView
speech() object Kotlin 侧访问器;页面看到的是下面的独立接口

window.ReaderAndroidSpeech(bridge/SpeechBridge.kt,shell ≥ 3)

addJavascriptInterface 只能暴露对象的方法,不能暴露属性,所以朗读接口单独注册; H5 的 promoteSpeechInterface(web/src/core/android-platform.ts)把它挂到 ReaderAndroid.speech,d.ts 中声明为 speech?: SpeechBridge。

方法:available()、init()、onSpeechEvent(cb)、onVoices(cb)、speak(text, utteranceId)、 pause()、stop()、setRate(rate)、setPitch(pitch)、setVolume(volume)、setVoice(voiceId)、shutdown()。 朗读队列与游标在 JS;utteranceId 由 H5 生成,事件原样带回,用来丢弃过期句子的 done。 interruptForMedia() 没有注解,只供 Kotlin 侧在开始播放影音时调用,不属于页面契约。

回调

JS 接口只能传原始类型,所以 Kotlin 端的 watchConnectivity、onSpeechEvent、onVoices 接收的是全局函数名(须是安全标识符),之后用 evaluateJavascript 调用它。H5 包装层把函数注册为 window.__readerConnectivity、__readerSpeechEvent、__readerSpeechVoices 再把名字传过去; 连接状态传字符串,朗读事件与声音列表传 JSON。页面代码按 d.ts 直接传函数即可。

3. 原生整页图片

H5 在 reader-view.ts 中通过 pageHost 询问外壳,仅在页面为 image 模式且读者选择「适应整页」时:

ReaderAndroid.renderPage(JSON.stringify({ sectionId, mode: 'image', mediaType, bytes: <base64>, fit: 'contain' }))
  → true:NativePageView 已覆盖在 WebView 上绘制
  → false:外壳拒绝(模式/适配方式不支持、无法解码、超时 2 秒),H5 在 WebView 中照常绘制

外壳只解码请求里的 bytes(JPEG/PNG/WebP),不按 path 自行取图。离开该页时 H5 调用 hidePage()。 外壳分支用到的模式名必须在 web/src/formats/types.ts 的 RenderMode 中,check-shell.sh 会检查。

4. 影音通道 window.ReaderMedia

media/MediaBridge.kt 用 WebViewCompat.addWebMessageListener 注册,只接受资源来源、只接受主框架, 需要 WebView 支持 WEB_MESSAGE_LISTENER;不支持时不注册,H5 使用网页播放器。H5 发送 JSON 字符串 { action, ... }(web/src/features/media/services/native-audio.ts),外壳通过回复通道回传播放状态。

action:play、showVideo、pause、resume、stop、previous、next、selectQueue、editQueue、 seek、speed(0.5–3.0)、audioTrack、sleep、reconnect、status。播放由 MediaPlaybackService(Media3)执行;视频画面在 MediaVideoActivity 中显示。消息超过约 900 KB 被忽略。

5. 生命周期与系统键

  • onPause 时外壳执行 window.__readerFlush && window.__readerFlush(),H5 借此保存阅读位置。
  • 返回键:WebView 有历史则后退,否则退出。
  • 外壳目前不拦截音量键,也没有「原生翻页器退出后通知 H5」的事件;旧版文档中的 reader:native-page / reader:native-return 事件并不存在。

6. 离线

离线下载与缓存由 H5 负责(与浏览器共用实现),外壳不另建书籍缓存。H5 使用的服务端接口:

GET /api/v1/books/:id/content
  → 整本书,带 ETag,可 Range 续传
  → 目录书(漫画目录)返回 400 DIRECTORY_BOOK;分章书返回 400 CHAPTER_BOOK

GET /api/v1/books/:id/file?path=<manifest files 中列出的 rel_path>
  → 单个文件;path 只能取 manifest 列出的值,其他值得不到别的文件

服务端不可达时客户端读本地缓存并静默降级,不能靠报错弹窗。