From 6df9afe6846284028b4a2edebcec75f8d32ad987 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 17 Jul 2026 02:03:11 +0100 Subject: [PATCH] fix(android): pairing QR now actually scannable Camera decode failed because every lever was at its minimum: the web modal generated the QR at 112px (~2.5px per module before upscaling), the analysis stream was CameraX's 640x480 default, and ZXing ran without TRY_HARDER. Generate the QR at 512px (displayed 192px with a real quiet zone), analyze at 1280x720, decode with TRY_HARDER plus an inverted-luminance fallback. v0.4.14 (versionCode 18). Co-Authored-By: Claude Fable 5 --- Android/app/build.gradle.kts | 4 ++-- .../app/ui/components/QrScannerOverlay.kt | 22 +++++++++++++++++-- .../src/components/CompanionIntroOverlay.vue | 17 +++++++++----- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index a4562f4c..844b42e1 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 = 17 - versionName = "0.4.13" + versionCode = 18 + versionName = "0.4.14" 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 2c9eaacc..96b57e53 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 @@ -239,7 +239,12 @@ private 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. + @Suppress("DEPRECATION") val analysis = ImageAnalysis.Builder() + .setTargetResolution(android.util.Size(1280, 720)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { @@ -268,7 +273,14 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) { /** ZXing-based QR decoder over the camera's Y (luminance) plane. */ private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAnalysis.Analyzer { private val reader = MultiFormatReader().apply { - setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))) + setHints( + mapOf( + DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE), + // Screen-displayed QRs come with moiré, glare, and soft focus at + // close range — the exhaustive search is worth the milliseconds. + DecodeHintType.TRY_HARDER to true, + ) + ) } override fun analyze(image: ImageProxy) { @@ -284,7 +296,13 @@ private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAna 0, 0, image.width, image.height, false, ) - val result = reader.decodeWithState(BinaryBitmap(HybridBinarizer(source))) + val result = try { + reader.decodeWithState(BinaryBitmap(HybridBinarizer(source))) + } catch (_: NotFoundException) { + // Dark-themed pages can render light-on-dark QRs — retry inverted. + reader.reset() + reader.decodeWithState(BinaryBitmap(HybridBinarizer(source.invert()))) + } onDecoded(result.text) } catch (_: NotFoundException) { // No QR in this frame — keep scanning. diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index b059c14a..da2e9337 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -93,7 +93,9 @@
-
+ +
{ watch(visible, async (isVisible) => { if (!isVisible) return + // Generate large and let CSS scale down — at 112px source a ~45-module QR + // is 2.5px/module, which camera decoders (the companion app included) + // routinely fail on. 512px keeps every module crisp. qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, { - width: 112, - margin: 1, + width: 512, + margin: 2, errorCorrectionLevel: 'M', color: { dark: '#111111', @@ -261,9 +266,11 @@ async function showPairScreen() { step.value = 'pair' if (!pairQrDataUrl.value) { pairingUrl.value = await buildPairingUrl() + // Large source + a real quiet zone; this QR is scanned by the companion + // app's camera, so give it every advantage (see download QR note above). pairQrDataUrl.value = await QRCode.toDataURL(pairingUrl.value, { - width: 112, - margin: 1, + width: 512, + margin: 3, errorCorrectionLevel: 'M', color: { dark: '#111111',