fix(mobile): resolve document providers once per selection - #13147
jabrailkhalil wants to merge 5 commits into
Conversation
Signed-off-by: jabrailkhalil <jabrailkhalil@gmail.com>
Signed-off-by: jabrailkhalil <jabrailkhalil@gmail.com>
|
|
|
Thanks for the follow-up — this one is easy to place, because both halves trace straight back to findings in the round-2 maintainer verification of #12126. Template looks good ✓ — every section present, and the Chinese version is a real translation rather than an abridged one. Problem: observed and measured, not theoretical. Finding S4 in that review timed Direction: aligned. Size: not applicable, no core paths. Approach: the scope feels right and the diff is genuinely minimal — no drive-by refactors, no formatting churn, no unrelated edits. Every line serves the stated goal. Two honest observations, neither blocking:
Risk: no elevated risk signals — none of the changed files match the revert-correlated paths. The one thing worth a reviewer's attention is that this implementation differs slightly from the candidate attached to S4, and uses a Kotlin construct that reads ambiguously even though it is correct. Both points are covered in the code review below. Moving on to code review. 🔍 中文说明感谢这个后续 PR——定位很清楚,因为两部分改动都能直接追溯到 #12126 第二轮维护者验证 中的具体结论。 模板完整 ✓ ——各节齐全,中文版是完整翻译,不是缩略版。 问题: 已观测且已实测,不是理论性加固。该评审的 S4 在真机上对同一 authority 下 N 个已授权 fixture URI 计时 方向: 对齐。 规模: 不适用,未触及核心路径。 方案: 范围合理,diff 确实做到了最小——没有顺手重构,没有格式化噪音,没有无关改动,每一行都服务于既定目标。两点如实说明,都不阻塞:
风险: 无升级风险信号——改动文件都不匹配与回滚相关的路径。唯一值得评审人留意的是:本实现与 S4 附带的候选 diff 略有不同,并且用了一个读起来有歧义(但行为正确)的 Kotlin 写法。这两点都在下面的代码审查中说明。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewNo blocking issues. I read the whole of My own proposal before reading the diff was: memoize What I checked, and why I am satisfied:
Two things for the reviewer to weigh, neither blocking:
One gap in coverage I want to state rather than bury: the cached path's own-UID rejection is not directly exercised. The existing Test evidenceUnattended CI run — I did not build or execute anything from this PR. Everything below is the PR's own CI, read through the API, plus static reading of the base tree. One naming trap worth flagging, because it makes the coverage look narrower than it is: the jobs called "Keystore and profiles (API 26/36)" are the device jobs, and The count corroborates that independently of anything in the log body. At the base commit the androidTest source set has exactly 24 That matters for whether the tests carry weight: both new assertions fail on the base code. Without the cache, 100 same-authority documents produce 100 package-manager reads and the two-document case produces 2, while the tests assert 1 and 1. So this is not a suite that passes identically with and without the diff — it pins the change. The permission-count and rejection assertions, by contrast, pass before and after, which is the correct shape for a regression guard. What the tests do not pin is wall-clock latency: they count package-manager accesses, not Binder transactions or milliseconds. The PR says so itself and lists UI latency under "Not validated," which is the right call. The author's local JVM harness numbers (100→1, 100→2, on Windows) are the author's claim and I did not re-run them — but nothing load-bearing rests on them, because CI's device suite asserts the same call counts on real Android at two API levels.
Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The one claim CI cannot settle is the latency benefit, and it is arguably not worth settling: finding S4 already probed it on a real device, reported 55–369 ms at 100 URIs against 35–99 ms with the candidate cache, and explicitly declined to quote a precise saving because host load swamped the spread. If a maintainer does want an A/B on this head, 中文说明代码审查没有阻塞问题。我读的是基线 commit 上 在看 diff 之前我自己的方案是:在 已核实且认可的部分:
两点请评审人自行权衡,都不阻塞:
有一处覆盖缺口我想明说而不是埋起来:缓存命中路径上的 own-UID 拒绝没有被直接覆盖。已有的 测试证据无人值守 CI 运行——我没有构建或执行本 PR 的任何代码。以下内容全部来自通过 API 读取的 PR 自身 CI,加上对基线代码树的静态阅读。 有一个命名陷阱需要指出,因为它让覆盖面看起来比实际窄:名为 "Keystore and profiles (API 26/36)" 的就是设备 job,而 测试数量也独立佐证了这一点,且不依赖日志正文的任何说法。基线 commit 上 androidTest source set 恰好有 24 个 这一点对判断测试是否有分量很关键:两个新断言在基线代码上都会失败。没有缓存时,100 个同 authority 文档会产生 100 次 package manager 读取,两文档场景会产生 2 次,而测试断言的是 1 和 1。所以这不是一个"有没有 diff 都同样通过"的套件——它确实固定住了这次改动。相比之下,权限计数和拒绝断言在改动前后都通过,这对回归防护来说是正确的形态。 测试没有固定的是墙钟延迟:它们统计的是 package manager 访问次数,不是 Binder 事务数或毫秒数。PR 自己也这么说了,并把 UI 延迟列在"未验证"下,这个处理是对的。作者本地 JVM harness 的数字(100→1、100→2,Windows 上)是作者的说法,我没有重跑——但没有任何关键结论依赖它,因为 CI 的设备套件在两个 API 级别的真实 Android 上断言了同样的调用次数。 我拉取数据时 CI 无法定论的只有延迟收益,而这个收益可以说也不值得专门去定论:S4 已经在真机上探测过,报告 100 个 URI 时 55–369 ms、换成候选缓存后 35–99 ms,并明确拒绝给出精确节省值,因为主机负载把差异淹没了。如果维护者确实想在这个 head 上做 A/B, — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — clean across every stage; the only nits are cosmetic and I name them below. Stepping back: this PR does one small thing and does it properly. Six production lines, no behaviour change, and it closes two findings a maintainer wrote down explicitly and left open. My own proposal before reading the diff was identical to what the author wrote, and also to the candidate the maintainer attached to S4 — three independent convergences on the same six lines is a good sign that there is no better shape for this change. I went in expecting to find a security regression, because caching inside a validation loop is exactly where one would hide, and I could not find one. The four per-URI conditions all survive; only the Binder resolution is memoized; the map is a local What I like most is that the evidence ended up stronger than the PR claimed. The body leans on a local Windows JVM harness with controlled Android stubs, which on its own would be thin for a fork PR touching a permission-checking path. It did not have to carry the weight: CI runs My reservations, both non-blocking:
The honest framing on value: the reviewer who measured this called it "a short main-thread stall, nowhere near an ANR." Nobody will notice this change. If the bar were "only merge what users can feel," it would not clear it. But it retires two explicit review findings at close to zero risk, keeps a design document from continuing to misdescribe shipped behaviour, and adds tests that pin both the optimization and its lifetime — which is a legitimate reason to merge, and the kind of follow-up that is easy to promise in review and rare to actually see land. Approving. Because 中文说明Confidence: 4/5 —— 各阶段都干净;仅有的几点都是措辞层面的,下面点名说明。 退一步看整体:这个 PR 只做了一件小事,而且做得规范。6 行生产代码,无行为变化,并且关闭了维护者明确写下、一直未处理的两个结论。我在看 diff 之前自己的方案与作者写的完全一致,也与维护者在 S4 附上的候选 diff 一致——三方独立收敛到同样的 6 行,说明这个改动已经没有更好的形态了。 我原本预期会找到安全回归,因为在校验循环里加缓存正是最容易藏问题的地方,但没找到。四个逐 URI 条件全部保留;被缓存的只有 Binder 解析;map 是局部 我最欣赏的一点是:证据最终比 PR 自己声称的更强。正文依赖的是本地 Windows JVM harness 加受控 Android 桩,单凭这一点,对一个触及权限校验路径的 fork PR 来说是单薄的。但它并不需要承担这个重量:CI 用不带 class 过滤的 我的两点保留意见,均不阻塞:
关于价值的如实定性:实测过这一点的评审人称之为"短暂的主线程停顿,远达不到 ANR"。没有人会注意到这个改动。如果标准是"只合入用户能感知到的东西",它过不了。但它以接近零的风险结清了两个明确的评审结论,阻止一份设计文档继续错误描述已经发布的行为,并补上了同时固定住优化本身与其生命周期的测试——这是合入的正当理由,也正是那种在评审里容易承诺、实际很少看到落地的后续工作。 同意合入。由于 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "agent 6b": none — no check was cut short by the tool ceiling..
Test Plan (not a blocker): ./gradlew — no such file or directory.
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"agent 6b":none — no check was cut short by the tool ceiling.。
Test Plan(非阻断):./gradlew — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.24.7)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "agent 6a": none — I could not execute the Gradle androidTest suite (no device/emulator in this environment), so "tests pass on hardware" is reasoned from the implementat….
Test Plan (not a blocker): ./gradlew — no such file or directory.
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"agent 6a":none — I could not execute the Gradle androidTest suite (no device/emulator in this environment), so "tests pass on hardware" is reasoned from the implementat…。
Test Plan(非阻断):./gradlew — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.24.7)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): "agent 1e": none — all checks above completed within budget (~9 of 42 calls)..
Test Plan (not a blocker): ./gradlew — no such file or directory.
中文说明
未探索到全部深度(达到工具调用预算):"agent 1e":none — all checks above completed within budget (~9 of 42 calls).。
Test Plan(非阻断):./gradlew — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.24.7)
|
I checked the browser smoke log: this job was cancelled at its 30-minute limit, rather than completing with an Android picker assertion failure. Chromium/WebKit installation consumed 15m34s, and the browser smoke started about 22m35s into the job before cancellation during execution. Run/job log. The native picker checks pass on API 26 and API 36, and the latest code review reports no findings. I cannot rerun upstream Actions with the repository permissions available to my account. Please rerun the timed-out browser job when reviewing; I am keeping CI timeout/download changes outside this picker fix. |
Local verification of #13147 — verdict: merge-ready264/264 scripted assertions passed across 6 harness arms (base / head / 4 mutants). Verified head: 中文摘要结论:可以合并。 本地双臂 A/B 验证了核心声明:同一 authority 的 100 个文档,改动前解析 provider 100 次,改动后 1 次(混合双 authority 为 2 次),且每个 URI 的权限检查一次不少(100 次保留)。S3 证明缓存仅存在于单次选择内(连续两次选择 → 2 次解析,而非 1 次)。变异矩阵:无 authority 键的单槽缓存(m1)会放过同 UID 危险 provider 的文档(S11 被错误接受), hoist PackageManager 但不缓存(m2)在混合 authority 下计数不符——两者恰被新增的 mixed-authority 测试钉住,与作者声明一致;m3(去掉 UID 检查)/m4(每 authority 只查一次授权)分别被既有安全性与逐文档授权场景击杀,构成阳性对照。另核实:两份设计文档链接目标均存在且中英对称;head 的 CI 在 API 26/36 模拟器上 29 项设备测试全部通过。未覆盖:本地 Gradle 全量构建与真机/模拟器执行(本机无 Android SDK,已由 CI 覆盖)、第三方 provider 行为、UI 延迟。详细数字见下方表格。 Central claim and A/BClaim: validating a picker result resolves each document-provider authority once per selection while keeping every URI permission check; the cache lives only for one validation call. Method: compiled the real
Evidence: Mutation matrix (vacuity check on the new tests)Four single-point mutants of the head file were compiled and driven through the identical 11 scenarios; each behaves exactly as predicted, and each diverges from head on at least one cell the checked-in test class pins (evidence:
m3 kills prove the harness can fail (it does, on the exact cells), so the green cells are evidence, not a dead harness. This matches the PR's own R1-1 mutation claims and sharpens them: m1 is not merely a count mismatch, it accepts a document from a provider owned by the app's own UID. Corrections to earlier review comments
Other checks
FindingsNone blocking. One informational observation: a hypothetical m2-style regression (hoisting Not covered
MethodologymacOS (arm64), JBR 17.0.10, Kotlin compiler 1.9.20 (embeddable jar, matching the project's pinned Kotlin version). Sources extracted with |
|
@wenshao Thank you for the detailed independent verification and mutation matrix. The mixed-authority case is useful evidence that the cache retains the provider UID boundary as well as reducing lookups. I will keep the verified head |
|
@qewn-code /triage |
|
@wenshao The independently verified head is still 29a21f1; I have not changed it. I noticed the latest triage mention says @qewn-code, so if a rerun was intended, the trigger is @qwen-code /triage. The Android checks passed, while the Web Shell smoke run remains cancelled after its time limit. Could you retry the intended triage trigger and the smoke check on this head when convenient? I will address any confirmed current-head finding. |


What this PR does
Resolve each document-provider authority once while validating a native file-picker result. Keep the cache local to one result and still check the content scheme, authority, provider ownership and read permission for every selected URI.
Why it's needed
The follow-up review of #12126 identified repeated provider-resolution Binder calls for documents from the same authority. Before this change, a selection of 100 documents from one provider performs 100 resolutions. The new code performs one resolution while retaining all 100 permission checks. This addresses the nonblocking provider-cache suggestion separately from the already merged file-selection feature. The Phase 1 design also gains synchronized English/Chinese notices linking to the implemented file-selection design, addressing the stale-documentation finding in the same parent review.
Reviewer Test Plan
How to verify
packages/mobile-shell, run./gradlew :app:assembleDebug :app:assembleDebugAndroidTest :app:testDebugUnitTest :app:lintDebug../gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.qwen.mobileshell.FilePickerDeviceTest.Evidence (Before & After)
The actual production controller was also compiled and executed in a local JVM harness with controlled Android API stubs. Both cache-count assertions fail on upstream
0a5f518b4f73and pass with this change; all nine behavioral/security controls pass before and after.The harness also checks provider ownership/availability changes between requests, denied URI grants, unsafe/unknown/same-UID providers, deduplication and the selection limit. These are controller call-count observations, not Android UI timing measurements. The checked-in Android tests count accesses to the package manager at the controller's provider-resolution boundary and delegate real permission checks to the target context. To verify R1-1, the three actual checked-in test methods were also executed through JUnit on the JVM with controlled Android API stubs: production passes all three; both a one-slot cache without an authority key and a manager-hoisting/no-cache mutation pass the old two tests but fail the new mixed-authority test (expected 2 manager accesses, observed 1). This mutation execution is not Android instrumentation.
Tested on
70256adaa9: API 26: 22 passed / 4 expected skips; API 36: 25 passed / 1 expected skip; all 19 file-picker tests passed on both; CI for the updated test head is pendingEnvironment
Windows, JDK 17, Gradle 8.2.1, Android SDK 34. The instrumentation tests were compiled and packaged, but have not been executed on an Android emulator or physical device locally.
Risk & Scope
Linked Issues
Follow-up to #12126, related to #11704 and #13111. Reviewer evidence: #12126 (comment).
中文说明
本 PR 的改动
验证原生文件选择器结果时,每个文档提供程序 authority 只解析一次。缓存仅存在于本次结果处理中;仍对每个选中的 URI 检查 content scheme、authority、提供程序所有者以及读取权限。
为什么需要
#12126 的后续评审指出,同一 authority 下的多个文档会重复触发提供程序解析的 Binder 调用。改动前,同一个提供程序的 100 个文档会触发 100 次解析;改动后只解析一次,同时保留全部 100 次权限检查。本 PR 单独处理这一非阻塞缓存建议,不扩大已合入文件选择功能的范围。Phase 1 设计文档同时添加同步的中英文说明,链接到已实现的文件选择设计,处理同一次原 PR 评审中的文档过时问题。
评审测试计划
如何验证
packages/mobile-shell运行./gradlew :app:assembleDebug :app:assembleDebugAndroidTest :app:testDebugUnitTest :app:lintDebug。./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.qwen.mobileshell.FilePickerDeviceTest。证据(改动前后)
另用受控 Android API 桩构建本地 JVM harness,编译并运行实际生产控制器。在 upstream
0a5f518b4f73上,两项缓存调用次数断言均失败;改动后均通过。其余九项行为与安全控制在改动前后均通过。Harness 还检查请求之间提供程序所有者或可用性的变化、URI 授权拒绝、危险或未知或同 UID 的提供程序、去重以及数量上限。这些数据是控制器调用次数,不是 Android UI 耗时。提交的 Android 测试统计控制器解析提供程序时对 package manager 的访问,并把实际权限判断委托给目标 context。R1-1 的三个实际测试方法还通过 JUnit 在受控 Android API 桩的 JVM 上执行:生产实现全部通过;无 authority 键的单槽缓存和移出循环但不缓存解析的两种变异均通过旧的两项测试,却在新增混合 authority 测试上失败(应为 2 次访问,实际 1 次)。这一变异验证不是 Android instrumentation。
本地测试环境
70256adaa9的 upstream Android CI:API 26 为 22 通过 / 4 预期跳过;API 36 为 25 通过 / 1 预期跳过;两者均通过全部 19 项 file-picker 测试;新增测试提交的 CI 待运行环境
Windows、JDK 17、Gradle 8.2.1、Android SDK 34。Instrumentation 测试已编译打包,但本地尚未在 Android 模拟器或真机上执行。
风险与范围
关联
#12126 的后续工作,关联 #11704、#13111。评审证据:https://github.com/QwenLM/qwen-code/pull/12126#issuecomment-5848429760。