mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix(browser): stop the embedded file pick from killing the app
Completing a pick on either embedded surface crashed the whole app, every
time. parseResult resolved the picked URIs through
`WebChromeClient.FileChooserParams.parseResult`, which is a WebView *static*:
it boots Chromium in whichever process calls it. WebFileChooserActivity — the
main-process chooser host that exists precisely because the `:napplet`
providers are windowless and have no Activity to launch a picker from —
declares no `android:process`, so that call ran in main while `:napplet`
already held the WebView data directory. AwDataDirLock then threw
Using WebView from more than one process at once with the same data
directory is not supported
as a FATAL EXCEPTION on main. Reproduced on a Pixel 8 / Android 17: the
picker opens, the user selects, and on Done the process dies before the page
is ever handed its file.
The two Activity-owning hosts never hit it because they are themselves
`android:process=":napplet"`, where WebView is already initialised — which is
why the full-screen browser picked files correctly throughout. Cancelling did
not hit it either, so "the picker opened" was never enough to catch this.
The URIs are now read off the result Intent directly. The platform
implementation reads exactly the same two fields (ClipData items, else the
data URI, only on RESULT_OK), so behaviour is unchanged for the single-URI,
multi-select and camera shapes; it just no longer drags WebView into a
process that must not have it.
This also plugs a grant leak. releaseGrants runs *inside* parseResult, after
the line that was throwing, so every crashed capture left the camera apps
holding a live write grant on the capture URI that nothing would ever revoke.
Verified on device after the fix:
- embedded pick: no crash, page reads back all 94,976 bytes of the chosen
PNG with its header intact — so a URI granted to the main process is
readable by the WebView in `:napplet` with no re-granting, as designed
- camera capture: 892,681-byte JPEG with EXIF intact (full resolution, so
EXTRA_OUTPUT is doing its job), delivered under the same name as the
granted URI
- grant/revoke: 0 outstanding grants on the capture authority, 1 while the
camera holds it, 0 again once the result is in
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gs2gi3sZfQ7SHrVm2njLMw
This commit is contained in:
co-authored by
Claude Opus 5
parent
6becc6efbe
commit
f11bdb8e49
+22
-1
@@ -162,6 +162,17 @@ object NappletFileChooser {
|
||||
* shape — where the result carries no URI at all and the evidence of a capture is a scratch file
|
||||
* that now has bytes in it. Every capture file this request created and did not return is deleted
|
||||
* here, so a cancelled or unused camera option leaves nothing behind.
|
||||
*
|
||||
* The URIs are read off the Intent here rather than through
|
||||
* [FileChooserParams.parseResult][android.webkit.WebChromeClient.FileChooserParams.parseResult],
|
||||
* which is a WebView *static*: calling it boots Chromium in whatever process calls it. This runs
|
||||
* in the main process too — [com.vitorpamplona.amethyst.napplet.WebFileChooserActivity] is the
|
||||
* chooser host for the embedded surfaces and declares no `android:process` — while `:napplet`
|
||||
* already holds the WebView data directory. That second init throws
|
||||
* `Using WebView from more than one process at once with the same data directory is not
|
||||
* supported` out of `AwDataDirLock` and takes the whole app down on every completed embedded
|
||||
* pick. The platform implementation reads exactly these two fields, so this is behaviour-for-
|
||||
* behaviour identical without dragging WebView into a process that must not have it.
|
||||
*/
|
||||
fun parseResult(
|
||||
context: Context,
|
||||
@@ -169,7 +180,17 @@ object NappletFileChooser {
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
): Array<Uri>? {
|
||||
val picked = FileChooserParams.parseResult(resultCode, data)?.takeIf { it.isNotEmpty() }
|
||||
val picked =
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
val clip = data.clipData
|
||||
if (clip != null && clip.itemCount > 0) {
|
||||
Array(clip.itemCount) { clip.getItemAt(it).uri }
|
||||
} else {
|
||||
data.data?.let { arrayOf(it) }
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}?.takeIf { it.isNotEmpty() }
|
||||
|
||||
// A camera returns no URI at all — it reports success by filling the file we handed it, so an
|
||||
// empty one means it was dismissed. Only consulted when the picker returned nothing of its own.
|
||||
|
||||
Reference in New Issue
Block a user