diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index f7fbdf60..cd8bc3bf 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.archipelago.app" minSdk = 26 targetSdk = 35 - versionCode = 41 - versionName = "0.5.21" + versionCode = 42 + versionName = "0.5.22" vectorDrawables { useSupportLibrary = true diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt index e6d531e3..61a7940a 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt @@ -238,6 +238,7 @@ internal fun CameraQrPreview(onDecoded: (String) -> Unit) { val mainExecutor = ContextCompat.getMainExecutor(context) val providerFuture = ProcessCameraProvider.getInstance(context) var provider: ProcessCameraProvider? = null + val focusScheduler = Executors.newSingleThreadScheduledExecutor() providerFuture.addListener({ val p = providerFuture.get() @@ -245,12 +246,15 @@ internal fun CameraQrPreview(onDecoded: (String) -> Unit) { val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } - // CameraX's analysis default is 640x480 — too few pixels per module - // to decode a modal-sized QR at arm's length. 1280x720 more than - // doubles the pixel density at negligible analysis cost. + // Dense Lightning-invoice QRs need BOTH enough pixels per module and + // sharp focus. 1280x720 + a far-focused camera (e.g. Pixel 9a's main + // lens, which won't focus close) left dense invoices undecodable + // while sparse address QRs still read — the "scanner doesn't pick up + // invoices" report. 1920x1080 roughly doubles module resolution so a + // QR held at the camera's actual focus distance still resolves. @Suppress("DEPRECATION") val analysis = ImageAnalysis.Builder() - .setTargetResolution(android.util.Size(1280, 720)) + .setTargetResolution(android.util.Size(1920, 1080)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { @@ -261,13 +265,27 @@ internal fun CameraQrPreview(onDecoded: (String) -> Unit) { } try { p.unbindAll() - p.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis) + val cam = p.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis) + // Force a centre autofocus on a repeating tick. A hand-held QR is + // a static scene, so continuous-AF often never retriggers and the + // lens sits at its resting (far) focus — fatal for dense codes. + // A normalized centre point works before the view is measured. + val point = androidx.camera.core.SurfaceOrientedMeteringPointFactory(1f, 1f) + .createPoint(0.5f, 0.5f) + val focusAction = androidx.camera.core.FocusMeteringAction.Builder( + point, + androidx.camera.core.FocusMeteringAction.FLAG_AF, + ).disableAutoCancel().build() + focusScheduler.scheduleWithFixedDelay({ + runCatching { cam.cameraControl.startFocusAndMetering(focusAction) } + }, 0, 2, java.util.concurrent.TimeUnit.SECONDS) } catch (_: Exception) { // Camera unavailable — the user can dismiss and enter details manually. } }, mainExecutor) onDispose { + focusScheduler.shutdownNow() provider?.unbindAll() analysisExecutor.shutdown() } diff --git a/neode-ui/public/packages/archipelago-companion.apk b/neode-ui/public/packages/archipelago-companion.apk index 36a9f06b..9bc05e51 100644 Binary files a/neode-ui/public/packages/archipelago-companion.apk and b/neode-ui/public/packages/archipelago-companion.apk differ