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:
parent
d1b8c34fe1
commit
d22af8f256
@ -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.
|
- 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.
|
- 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.
|
- In-page camera (`getUserMedia`) is granted to allowed hosts only, video only.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package mx.joykoo.scanner
|
package mx.joykoo.scanner
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@ -15,6 +16,7 @@ import android.os.Environment
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
@ -271,31 +273,77 @@ class FileTransfers(
|
|||||||
|
|
||||||
// ── Uploads (<input type=file>) ─────────────────────────────────────
|
// ── 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 {
|
fun onShowFileChooser(callback: ValueCallback<Array<Uri>>, params: FileChooserParams): Boolean {
|
||||||
uploadCallback?.onReceiveValue(null)
|
uploadCallback?.onReceiveValue(null)
|
||||||
uploadCallback = callback
|
uploadCallback = callback
|
||||||
val offerCamera = acceptsImages(params.acceptTypes)
|
if (!acceptsImages(params.acceptTypes)) {
|
||||||
val needed = if (offerCamera) Permissions.CAMERA + Permissions.STORAGE else emptyArray()
|
launchPicker(params)
|
||||||
permissions.withPermissions(needed) { launchChooser(params, offerCamera) }
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchChooser(params: FileChooserParams, offerCamera: Boolean) {
|
private fun showPhotoSourceDialog(params: FileChooserParams) {
|
||||||
// Launching the camera without the CAMERA grant throws, so it is only offered when granted.
|
AlertDialog.Builder(activity)
|
||||||
val camera = if (offerCamera && permissions.allGranted(Permissions.CAMERA + Permissions.STORAGE))
|
.setTitle(R.string.upload_title)
|
||||||
cameraIntent() else null
|
.setItems(arrayOf(
|
||||||
val picker = params.createIntent()
|
activity.getString(R.string.upload_take_photo),
|
||||||
val intent = when {
|
activity.getString(R.string.upload_pick_file),
|
||||||
camera != null && params.isCaptureEnabled -> camera
|
)) { _, which -> if (which == 0) launchCamera(params) else launchPicker(params) }
|
||||||
camera != null -> Intent.createChooser(picker, null)
|
.setOnCancelListener { onChooserResult(Activity.RESULT_CANCELED, null) }
|
||||||
.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(camera))
|
.show()
|
||||||
else -> picker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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 {
|
try {
|
||||||
chooserLauncher.launch(intent)
|
chooserLauncher.launch(intent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} 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)
|
onChooserResult(Activity.RESULT_CANCELED, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,13 @@
|
|||||||
<string name="download_done">Guardado en Descargas: %1$s</string>
|
<string name="download_done">Guardado en Descargas: %1$s</string>
|
||||||
<string name="download_failed">No se pudo descargar el archivo</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="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_wifi">Sin Wi-Fi</string>
|
||||||
<string name="status_no_internet">sin internet</string>
|
<string name="status_no_internet">sin internet</string>
|
||||||
<string name="status_battery">%1$d%%</string>
|
<string name="status_battery">%1$d%%</string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user