Scan the enrollment QR in-app instead of relying on the camera app

Not every phone's camera app hands a scanned aiapp:// URI off to the app
reliably, which made enrollment an annoying multi-step process. The
Settings screen now scans the QR itself via zxing-android-embedded's
ScanContract (offline, no Play Services/ML Kit download) and feeds the
decoded URI through the existing parseEnrollmentUri. The aiapp://enroll
deep link stays as a fallback for cameras that do redirect.

Verified on the emulator: Scan QR code launches the scanner, prompts for
camera permission, shows a live preview, and backing out returns to
Settings cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Sonnet 5 committed 2026-08-25 16:25:14 -04:00
1 parent 99bcc341c1
commit da2c110429
5 files changed
+56 -13

No files matched your search

+6 -2
View File
@@ -27,8 +27,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Enrollment: the server prints its aiapp://enroll QR to the
terminal; the phone's camera app opens the URI here, so no
camera code or QR library is needed in this app. -->
terminal. This intent filter is the fallback path for a
camera app that redirects a scanned aiapp:// URI here
directly; the Settings screen's own "Scan QR code" button
(zxing-android-embedded) is the primary path and needs no
filter, since it decodes the QR itself and hands the URI
to parseEnrollmentUri in-process. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -1,5 +1,6 @@
package com.example.aiapp
import android.net.Uri
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -21,12 +23,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.activity.compose.rememberLauncherForActivityResult
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
/**
* Server address and token. The normal path is scanning the server's
* terminal QR (which lands in MainActivity and never shows this screen);
* these fields are the fallback for typing the same three values by hand.
* [onBack] is null on first run, when there is nothing to go back to.
* Server address and token. The normal path is the "Scan QR code" button
* below, which decodes the server's terminal QR itself; these fields are
* the fallback for typing the same three values by hand. [onBack] is null
* on first run, when there is nothing to go back to.
*/
@Composable
fun SettingsScreen(
@@ -42,6 +48,19 @@ fun SettingsScreen(
var token by remember { mutableStateOf("") }
var error by remember { mutableStateOf<String?>(null) }
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result: ScanIntentResult ->
// Null contents means the user backed out of the scanner -- not an
// error, so nothing to report.
val contents = result.contents ?: return@rememberLauncherForActivityResult
val settings = parseEnrollmentUri(Uri.parse(contents))
if (settings == null) {
error = "Not a valid enrollment code"
} else {
saveServerSettings(context, settings)
onSaved(settings)
}
}
Column(Modifier.fillMaxSize().padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text(
@@ -55,13 +74,19 @@ fun SettingsScreen(
}
Spacer(Modifier.height(8.dp))
Text(
"The easy way: run ai-server on the backend and scan the QR it prints " +
"with the phone's camera. Or type the same values here.",
"The easy way: run ai-server on the backend and scan the QR it prints. " +
"Or type the same values here.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(16.dp))
OutlinedButton(
onClick = { scanLauncher.launch(ScanOptions().setDesiredBarcodeFormats(ScanOptions.QR_CODE)) },
modifier = Modifier.fillMaxWidth(),
) { Text("Scan QR code") }
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = host,
onValueChange = { host = it },