视觉按最新原型,业务按已确认PRD。现有代码为满足设计而改造。执行见按设计实施与任务卡;当前代码与验证范围见消费者功能页。
原生离线下载独立复审
本页目录
已确认的问题及复现P2:重复 clearOwner 的旧 poll 删除重新进入该 owner 后的新目录P2:旧 DownloadHelper 回调移除同 UUID 的新 preparation,使 Promise 永不完成P2:owner 契约最初拒绝 JS 允许的 HTTP IPv6 loopback验证边界:ready 不等于 decoder 或许可证证明已复核的行为与已修复的问题运行证据审核者:子 Agent(gpt-6.1-sol,reasoning_effort=max)。独立复核 Android/JS,由主 Agent 负责修改和最终验收。
范围:只读审查 root 实现的 JukuOfflineStore.java、JukuOfflineModule.java、JukuOfflineService.java、ReactVideoPackage.kt、ReactExoplayerView.java 离线分支、两个 Android manifest、JukuMediaClient.java 来源策略、src/playback/nativeOffline.ts 及对应夹具。审核者没有编辑上述文件。iOS 的实现者源码/SDK 检查见原生离线下载接入,不算独立 iOS 完整审查。
结论:本次独立复核确认 3 个 P2,root 已修复,独立正向回归全部通过;限定上述队列机器范围,没有剩余已确认 P1/P2。当前是没有 issuer 接入的有限原生队列 PoC。消费者 src/queries/playback.ts 仍抛出 503 offline_authorization_required;原生 request metadata、owner 与 expiresAt 不构成许可证、设备 attestation 或受保护内容离线授权。
已确认的问题及复现
P2:重复 clearOwner 的旧 poll 删除重新进入该 owner 后的新目录
位置:JukuOfflineStore.java:251 的 clearOwner() / waitRemoved()。原实现为每次 clearOwner 创建独立异步 poll;poll 持有旧资源 ID,却在结束时查找该 owner 当前的目录映射,并清除 clearing:<owner>。
实际复现:完成原 MP4 下载;第一次 clear 发起删除、暂未完成;排空真实 Media3 删除工作但不推进已排队的 100ms poll;第二次 clear 看到 index 已空而立即返回成功,清掉锁;同 owner 重新 configure 并 enqueue,实际 SimpleCache 创建新 UUID 目录;推进第一条旧 poll,它删除新目录却报告清理成功。没有改写 manager/index/cache,只有媒体 transport 是本地字节夹具。
证据:offline-android-adversarial.log 的修复前 duplicateClearErasesAReenteredOwnersNewDirectory 断言缺陷通过。root 使用每 owner 的 completion 列表合并到同一次 waitRemoved;失败保留持久 clearing 标志,完成前禁止 owner 重激活。修复后独立夹具 offline-android-review/com/brentvatne/exoplayer/JukuOfflineAdversarialReviewTest.java 的 duplicateClearPreservesAReenteredOwnersNewDirectory 通过,验证两条 Promise 均等待清理终态,重进后的真实新目录/cache 字节在后续 poll 时仍存在。状态:已修复并独立验证。
P2:旧 DownloadHelper 回调移除同 UUID 的新 preparation,使 Promise 永不完成
位置:JukuOfflineStore.java:218 的 onPrepared / :232 的 onPrepareError。原实现先执行 preparing.remove(id),再比较是否为当前 pending;旧事件也因此会移除新条目。
实际复现:enqueue MP4(id,A),在真实 helper 的主线程 onPrepared 分发前切换 owner B,旧 request 正常报 owner changed;随后 B 使用同 UUID enqueue;排空主线程,旧 helper 的真实回调先移除 B 的 pending,B 的回调也直接返回。B 的 completion 未 settle,真实 DownloadIndex 无此任务。检查已安装 Media3 1.8 的真实 bytecode:DownloadHelper.release() 不取消已 post 的 callback,因此这是可达竞态。没有使用反射生成 callback 或用替代 helper。
证据:offline-android-adversarial-callback.log 的修复前 stalePreparedCallbackDropsReplacementAssetPromise 断言缺陷通过;offline-android-downloadhelper-bytecode.txt。root 两个回调都改为先用 preparing.get(id) == pending 确认身份,再 remove。修复后独立夹具 stalePreparedCallbackPreservesReplacementAssetPromise 通过,B 的 Promise 正常完成,真实下载最终 completed,native playback 返回 owner B。onPrepareError 使用同一保护已源码复核,本次没有单独合成 error callback。状态:已修复并独立验证实际 onPrepared 竞态。
P2:owner 契约最初拒绝 JS 允许的 HTTP IPv6 loopback
downloadOwner 接受 http://[::1]:8080;原生 scope 最初只接受 localhost/127.0.0.1。仅加入 ::1 分支仍不满足本次 SDK 28 Uri 解析行为,独立正向夹具抛出 offline_owner_invalid。root 将 owner identity 解析改用 java.net.URI,接受 ::1/[::1];源媒体仍用 Android Uri 与原有严格网络策略。修复后独立 allowedIPv6OwnerCanConfigureAndClear 通过,root 另有三种 loopback owner 回归。这个修复只影响 owner metadata,没有放宽媒体 URL/DNS 规则。状态:已修复并独立验证 configure/clear 契约。
验证边界:ready 不等于 decoder 或许可证证明
实际 HTML200 负夹具完整进入当前 MP4 队列与 cache,native playback snapshot 为 ready;真实 Media3 Mp4Extractor.sniff 对这些缓存字节返回 false。证据是独立夹具 html200CompletesQueueButActualMp4ExtractorRejectsIt,修复前检查通过。当前 ready 表示 Media3 STATE_COMPLETED,不能写成“MP4 可解码已验证”或“受保护视频许可证有效”。消费者入口未开放,MP4 metadata/decoder 验证、provider/license/设备执行验收属于尚未完成的 O00 范围;本复核不把该边界另计为有限队列机器的 P1/P2。
已复核的行为与已修复的问题
- 原生 module 普通 Promise 参数与 JS adapter 匹配;guest 使用
configureOwner('', ''),expiry 统一毫秒。snapshot 不暴露 source URL/headers;JS 检查 owner、UUID、有效期与精确dramivio-offline://asset/<UUID>,native player 再从 DownloadIndex 的 UUID 找 owned completed asset。 - Android 离线 source 使用真实 Media3 DownloadHelper 创建 MediaSource,CacheDataSource 的 upstream 和 write sink 均为空;缓存缺失不能退回来源网络。owner/expiry/clearing 同时在 open/read 检查。已有真实 manager/index/cache 测试覆盖本地 MP4、HLS manifest/segment/AES key、冷启动、暂停恢复、换号、到期与物理移除;不等于真机播放器/后台完整验收。
- source heads 只有 Referer/User-Agent;独立 OkHttp client 使用
NO_COOKIES、没有网站认证 interceptor/shared API jar、DNS 公网地址限制及每次 network redirect 目的检查。只是复核真实源码链路,本次 native transport 夹具不证明实际 HTTPS/redirect/DNS 请求结果。 - root 在复核期间修复 modern AGP 使用
AndroidManifestNew.xml导致下载服务未注册的问题:当前两份 manifest 都含非导出的 DownloadService 与受 BIND_JOB_SERVICE 保护的 PlatformScheduler service;真实 merged manifest 夹具已经覆盖,不能把之前 Config.NONE 的 compile 结果当注册证明。 - root 先前修复同 URL 不同 asset 删除互相擦 cache 的问题:CacheKeyFactory 添加实际 asset UUID 前缀,配套真实 cache 删除保留另一个 asset 的测试。持久 clearing preference 在清理失败/中断时保持锁定。并发多次 clear 的终态竞态由上面的独立复现另行验证。
运行证据
独立 reviewer fixtures 使用 Robolectric 4.17 / Android SDK 28、实际 Media3 1.8、SQLite DownloadIndex、SimpleCache 和 DownloadHelper;重用 author 的本地 transport 与生成媒体资源。额外 sourceSet 仅由 offline-android-review.init.gradle 指向 work/,未修改仓库源代码或正式夹具。没有下载上游媒体、连接生产 API 或发布。
offline-android-js-adapter-review.log:独立重跑 7 个 JS adapter 测试全部通过。初始 reviewer native run:并发 clear 与 HTML200 检查通过,IPv6 owner 正向检查失败;单独旧 callback 检查通过。
offline-android-adversarial-fixed.log:修复后的独立 4 项全部通过,Gradle exit 0:IPv6 owner configure/clear、重复 clear 后新目录/cache 保留、旧 prepared 回调后新 request 完成、HTML200 实际 extractor 拒绝。root 作者夹具另有 offline-android-robolectric.log 的 15 项通过;这是作者运行的更广功能验证,与上述独立执行分开记录。
真机系统调度/foreground service、进程终止/重启、磁盘低空间/损坏、真实 decoder 播放、网络侧 DNS/redirect 隔离与真实 offline license/device attestation 均不由这些 fixtures 证明。没有发现已确认 P1;本次确认的 P2 均由 root 修复并通过上述独立复核。