diff --git a/CLAUDE.md b/CLAUDE.md
index 034e18b..d2a2c02 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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: `` image fields offer the camera; `capture` opens it directly. Photos are saved to Pictures/Joykoo.
+- Uploads: `` 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
diff --git a/app/src/main/java/mx/joykoo/scanner/FileTransfers.kt b/app/src/main/java/mx/joykoo/scanner/FileTransfers.kt
index 9c07064..b8962d7 100644
--- a/app/src/main/java/mx/joykoo/scanner/FileTransfers.kt
+++ b/app/src/main/java/mx/joykoo/scanner/FileTransfers.kt
@@ -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 () ─────────────────────────────────────
- /** 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>, 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)
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 96518d1..26a6d51 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,6 +29,13 @@
Guardado en Descargas: %1$sNo se pudo descargar el archivoSe necesita permiso de almacenamiento para guardar archivos
+ Agregar foto
+ Tomar foto
+ Elegir de la galería o archivos
+ No se pudo abrir la cámara
+ Permiso de cámara
+ La app no tiene permiso para usar la cámara. Actívelo en Ajustes → Permisos → Cámara y vuelva a intentarlo.
+ Abrir AjustesSin Wi-Fisin internet%1$d%%