Merge pull request 'fix(android): pairing QR now actually scannable (v0.4.14)' (#93) from companion-qr-scan-fix into main
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m31s

This commit is contained in:
lfg2025 2026-07-17 01:04:08 +00:00
commit e2309cc2ac
4 changed files with 34 additions and 9 deletions

View File

@ -11,8 +11,8 @@ android {
applicationId = "com.archipelago.app" applicationId = "com.archipelago.app"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 17 versionCode = 18
versionName = "0.4.13" versionName = "0.4.14"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View File

@ -239,7 +239,12 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) {
val preview = Preview.Builder().build().also { val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider) 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() val analysis = ImageAnalysis.Builder()
.setTargetResolution(android.util.Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
.also { .also {
@ -268,7 +273,14 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) {
/** ZXing-based QR decoder over the camera's Y (luminance) plane. */ /** ZXing-based QR decoder over the camera's Y (luminance) plane. */
private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAnalysis.Analyzer { private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAnalysis.Analyzer {
private val reader = MultiFormatReader().apply { 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) { override fun analyze(image: ImageProxy) {
@ -284,7 +296,13 @@ private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAna
0, 0, image.width, image.height, 0, 0, image.width, image.height,
false, 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) onDecoded(result.text)
} catch (_: NotFoundException) { } catch (_: NotFoundException) {
// No QR in this frame — keep scanning. // No QR in this frame — keep scanning.

View File

@ -93,7 +93,9 @@
</div> </div>
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4">
<div class="w-[128px] rounded-2xl border border-white/10 bg-white/[0.03] p-1 overflow-hidden"> <!-- Bigger than the download QR: this one is scanned by the
companion app camera, and physical size drives decode. -->
<div class="w-[192px] rounded-2xl border border-white/10 bg-white/[0.03] p-1 overflow-hidden">
<img <img
v-if="pairQrDataUrl" v-if="pairQrDataUrl"
:src="pairQrDataUrl" :src="pairQrDataUrl"
@ -218,9 +220,12 @@ watch(companionIntroRequested, (requested) => {
watch(visible, async (isVisible) => { watch(visible, async (isVisible) => {
if (!isVisible) return 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, { qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, {
width: 112, width: 512,
margin: 1, margin: 2,
errorCorrectionLevel: 'M', errorCorrectionLevel: 'M',
color: { color: {
dark: '#111111', dark: '#111111',
@ -261,9 +266,11 @@ async function showPairScreen() {
step.value = 'pair' step.value = 'pair'
if (!pairQrDataUrl.value) { if (!pairQrDataUrl.value) {
pairingUrl.value = await buildPairingUrl() 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, { pairQrDataUrl.value = await QRCode.toDataURL(pairingUrl.value, {
width: 112, width: 512,
margin: 1, margin: 3,
errorCorrectionLevel: 'M', errorCorrectionLevel: 'M',
color: { color: {
dark: '#111111', dark: '#111111',