Show an explicit camera/gallery choice for photo uploads

Image file inputs went straight to the gallery picker when the camera was
hidden by the system chooser or its permission was missing. They now show a
Tomar foto / Elegir de la galería dialog (capture fields open the camera
directly), and a denied camera permission explains how to enable it in
Settings instead of silently falling back to the picker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
caspar 2026-10-12 14:07:04 -04:00
parent d1b8c34fe1
commit d22af8f256
3 changed files with 71 additions and 16 deletions

View File

@ -89,7 +89,7 @@ All configurable values are in `AppConfig.kt`:
- Permissions are requested at first launch (camera; storage on Android ≤ 9) and again when a feature needs one.
- Downloads: https on allowed hosts via `DownloadManager` (session cookie forwarded); `data:` decoded natively; `blob:` read in-page and returned via `evaluateJavascript` polling (no JS interface). A document-start hook keeps blobs readable if the site revokes the URL immediately. Saved to Downloads, with a toast on completion.
- Uploads: `<input type=file>` image fields offer the camera; `capture` opens it directly. Photos are saved to Pictures/Joykoo.
- Uploads: `<input type=file>` image fields show an in-app "Tomar foto / Elegir de la galería" dialog (the system chooser hid the camera behind the gallery picker); `capture` opens the camera directly. If camera permission is denied, a dialog offers app Settings or the file picker. Photos are saved to Pictures/Joykoo.
- In-page camera (`getUserMedia`) is granted to allowed hosts only, video only.
## Security

View File

@ -1,6 +1,7 @@
package mx.joykoo.scanner
import android.app.Activity
import android.app.AlertDialog
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
@ -15,6 +16,7 @@ import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.provider.Settings
import android.util.Base64
import android.util.Log
import android.webkit.CookieManager
@ -271,31 +273,77 @@ class FileTransfers(
// ── Uploads (<input type=file>) ─────────────────────────────────────
/** Wire to WebChromeClient.onShowFileChooser. */
/**
* Wire to WebChromeClient.onShowFileChooser. Image fields get an explicit
* "Tomar foto / Elegir archivo" choice (or the camera directly for `capture` fields)
* instead of the system chooser, which on many devices opens the gallery picker
* and hides the camera.
*/
fun onShowFileChooser(callback: ValueCallback<Array<Uri>>, params: FileChooserParams): Boolean {
uploadCallback?.onReceiveValue(null)
uploadCallback = callback
val offerCamera = acceptsImages(params.acceptTypes)
val needed = if (offerCamera) Permissions.CAMERA + Permissions.STORAGE else emptyArray()
permissions.withPermissions(needed) { launchChooser(params, offerCamera) }
if (!acceptsImages(params.acceptTypes)) {
launchPicker(params)
return true
}
val needed = Permissions.CAMERA + Permissions.STORAGE
permissions.withPermissions(needed) {
when {
!permissions.allGranted(needed) -> showCameraDenied(params)
params.isCaptureEnabled -> launchCamera(params)
else -> showPhotoSourceDialog(params)
}
}
return true
}
private fun launchChooser(params: FileChooserParams, offerCamera: Boolean) {
// Launching the camera without the CAMERA grant throws, so it is only offered when granted.
val camera = if (offerCamera && permissions.allGranted(Permissions.CAMERA + Permissions.STORAGE))
cameraIntent() else null
val picker = params.createIntent()
val intent = when {
camera != null && params.isCaptureEnabled -> camera
camera != null -> Intent.createChooser(picker, null)
.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(camera))
else -> picker
private fun showPhotoSourceDialog(params: FileChooserParams) {
AlertDialog.Builder(activity)
.setTitle(R.string.upload_title)
.setItems(arrayOf(
activity.getString(R.string.upload_take_photo),
activity.getString(R.string.upload_pick_file),
)) { _, which -> if (which == 0) launchCamera(params) else launchPicker(params) }
.setOnCancelListener { onChooserResult(Activity.RESULT_CANCELED, null) }
.show()
}
/** Camera permission was refused; Android stops prompting after repeated denials,
* so offer the app's settings page as the way to turn it back on. */
private fun showCameraDenied(params: FileChooserParams) {
AlertDialog.Builder(activity)
.setTitle(R.string.camera_denied_title)
.setMessage(R.string.camera_denied_message)
.setPositiveButton(R.string.camera_open_settings) { _, _ ->
onChooserResult(Activity.RESULT_CANCELED, null)
activity.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", activity.packageName, null))
)
}
.setNeutralButton(R.string.upload_pick_file) { _, _ -> launchPicker(params) }
.setOnCancelListener { onChooserResult(Activity.RESULT_CANCELED, null) }
.show()
}
private fun launchCamera(params: FileChooserParams) {
val camera = cameraIntent()
if (camera == null) {
toast(R.string.camera_failed)
launchPicker(params)
return
}
launch(camera)
}
private fun launchPicker(params: FileChooserParams) = launch(params.createIntent())
private fun launch(intent: Intent) {
try {
chooserLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No app to pick files", e)
Log.e(TAG, "No app for ${intent.action}", e)
toast(R.string.camera_failed)
onChooserResult(Activity.RESULT_CANCELED, null)
}
}

View File

@ -29,6 +29,13 @@
<string name="download_done">Guardado en Descargas: %1$s</string>
<string name="download_failed">No se pudo descargar el archivo</string>
<string name="download_permission_denied">Se necesita permiso de almacenamiento para guardar archivos</string>
<string name="upload_title">Agregar foto</string>
<string name="upload_take_photo">Tomar foto</string>
<string name="upload_pick_file">Elegir de la galería o archivos</string>
<string name="camera_failed">No se pudo abrir la cámara</string>
<string name="camera_denied_title">Permiso de cámara</string>
<string name="camera_denied_message">La app no tiene permiso para usar la cámara. Actívelo en Ajustes → Permisos → Cámara y vuelva a intentarlo.</string>
<string name="camera_open_settings">Abrir Ajustes</string>
<string name="status_no_wifi">Sin Wi-Fi</string>
<string name="status_no_internet">sin internet</string>
<string name="status_battery">%1$d%%</string>