diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SettingsScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SettingsScreen.kt index e99820c..88ef4c3 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SettingsScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SettingsScreen.kt @@ -1,5 +1,7 @@ package com.example.aiapp +import android.Manifest +import android.content.pm.PackageManager import android.net.Uri import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -24,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import com.google.zxing.client.android.Intents import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanIntentResult @@ -62,6 +65,17 @@ fun SettingsScreen( } } + val requestCamera = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + scanLauncher.launch(enrollmentScanOptions()) + } else { + error = "Scanning needs the camera. Grant it in the system settings, " + + "or type the host, port and token in below." + } + } + Column(Modifier.fillMaxSize().padding(16.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Text( @@ -84,23 +98,21 @@ fun SettingsScreen( OutlinedButton( onClick = { - scanLauncher.launch( - ScanOptions() - .setDesiredBarcodeFormats(ScanOptions.QR_CODE) - .setCaptureActivity(EnrollmentScanActivity::class.java) - // Follow the phone, not the library's landscape pin. - .setOrientationLocked(false) - // Decode both polarities: ZXing otherwise looks only for a - // dark code on a light ground, and ai-server's QR is block - // characters in the terminal's foreground colour -- so on a - // dark-themed terminal it comes out as a photographic - // negative that the scanner silently never matches. Which - // way round it renders is the terminal's business, not - // something this app should depend on. MIXED_SCAN alternates - // normal and inverted frames, so it costs half the frame - // rate at each polarity and nothing else. - .addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN) - ) + // Hold the camera permission before the scanner starts. + // Letting its activity ask on our behalf is what the + // library does by default, and it opens the camera without + // waiting for the answer: the first-ever scan comes up as + // a live preview with "Sorry, the Android camera + // encountered a problem" over it, and works on the second + // try. Nothing is wrong with the camera, so nothing should + // say there is. + if (context.checkSelfPermission(Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED + ) { + scanLauncher.launch(enrollmentScanOptions()) + } else { + requestCamera.launch(Manifest.permission.CAMERA) + } }, modifier = Modifier.fillMaxWidth(), ) { Text("Scan QR code") } @@ -152,3 +164,24 @@ fun SettingsScreen( }) { Text("Save") } } } + +/** + * How the enrollment QR is scanned, in one place because two callers reach + * it -- straight from the button when the camera permission is already + * held, and from the permission result when it has just been granted. + * + * MIXED_SCAN is the load-bearing part: ZXing otherwise looks only for a + * dark code on a light ground, and ai-server's QR is block characters in + * the terminal's foreground colour, so on a dark-themed terminal it comes + * out as a photographic negative the scanner silently never matches. Which + * way round it renders is the terminal's business, not something this app + * should depend on. The mixed decoder alternates normal and inverted + * frames, costing half the frame rate at each polarity and nothing else. + */ +private fun enrollmentScanOptions(): ScanOptions = + ScanOptions() + .setDesiredBarcodeFormats(ScanOptions.QR_CODE) + .setCaptureActivity(EnrollmentScanActivity::class.java) + // Follow the phone, not the library's landscape pin. + .setOrientationLocked(false) + .addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)