mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Completes the file-input support: a page that accepts photos or video can now reach the camera, not only files already on the device. `accept="image/*"` on a mobile browser means "take one or pick one"; until now Amethyst could only do the second half, which is the wrong half for the common case of uploading a photo. How it decides, mirroring a mobile browser: a bare file input offers stills and video, an image-only accept offers just the camera, a document accept offers neither. Resolved by FileChooserAccept.captureMedia, pure and unit-tested. Unlike the type filter this does NOT widen on an extension the platform cannot name — widening there would put a camera in front of a page that never asked for one. Permission handling is the part worth reading. ACTION_IMAGE_CAPTURE throws SecurityException for an app that declares CAMERA without holding it, and Amethyst declares it, so the grant has to exist before the chooser is built. When the page set `capture` the permission is requested first — the user tapped a control whose entire purpose is to take a photo. Without `capture` the camera is offered only if permission is already held, so opening a document upload never raises a camera prompt out of nowhere. A denial is not a failure: the picker still opens, minus the camera. A camera needs somewhere to put a full-resolution shot (EXTRA_OUTPUT; without one it returns a thumbnail, useless as an upload), so each option gets an empty scratch file in cacheDir behind its own FileProvider — a dedicated one with its own authority and paths file, exposing a single subdirectory rather than the everything the app's general-purpose provider exposes. It needs its own subclass because the manifest merger keys providers by android:name and would otherwise collide with the app's. A chooser entry supplied via EXTRA_INITIAL_INTENTS is started by the system, not by us, and the URI grant flags on it are not reliably carried across that hop, so every resolved camera package is granted write access up front — none of them can be ruled out before the user chooses. That grant is taken back the moment the outcome is known, for the kept capture as well as the discarded ones, revoked per package rather than per URI so it cannot clip this app's own read of its own provider. Unfilled scratch files are deleted immediately; a kept one cannot be (the page may not read it until the form is submitted) and is swept on a later request instead. The three Activity-owning surfaces — the full-screen browser, the full-screen napplet/nSite sandbox, and the main-process host that serves both embedded surfaces — now share one WebFileChooserLauncher, so filtering, multi-select, capture and the permission flow cannot drift between them. The embedded providers pass the input's `capture` flag across the existing Messenger contract rather than having the main process re-derive it. Every path still ends in exactly one call to the page's filePathCallback, including a denied permission, a dismissed camera, and a device with no camera app at all. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FxfdHeR9Ry4qALXHT5Sf1Q
36 lines
1.4 KiB
XML
36 lines
1.4 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
|
<!--
|
|
Android 11+ package visibility. Without these the sandbox cannot resolve the camera apps, which
|
|
it must do to name them in grantUriPermission before handing over the capture file. Launching an
|
|
implicit intent is unaffected; only querying is.
|
|
-->
|
|
<queries>
|
|
<intent>
|
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
|
</intent>
|
|
<intent>
|
|
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
|
</intent>
|
|
</queries>
|
|
|
|
<application>
|
|
<!--
|
|
Hands a camera app the single empty file a WebView capture will be written to. Its own
|
|
provider with its own authority rather than the app's general-purpose one, so the exposed
|
|
surface is one cache subdirectory instead of all of cache/ and external files.
|
|
-->
|
|
<provider
|
|
android:name="com.vitorpamplona.amethyst.napplethost.NappletCaptureFileProvider"
|
|
android:authorities="${applicationId}.napplethost.captures"
|
|
android:exported="false"
|
|
android:grantUriPermissions="true">
|
|
<meta-data
|
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
android:resource="@xml/napplet_capture_paths" />
|
|
</provider>
|
|
</application>
|
|
|
|
</manifest>
|