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:
1 parent
99bcc341c1
commit
da2c110429
5 files changed
+56
-13
No files matched your search
@@ -126,4 +126,5 @@ dependencies {
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.zxing.embedded)
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Latest stable versions as of 2026-08-24 (checked against Google Maven /
|
||||
# Maven Central; prereleases deliberately skipped).
|
||||
# Maven Central; prereleases deliberately skipped). zxing-embedded checked
|
||||
# 2026-08-25.
|
||||
[versions]
|
||||
agp = "9.3.2"
|
||||
kotlin = "2.4.10"
|
||||
@@ -7,9 +8,15 @@ compose-multiplatform = "1.11.1"
|
||||
# material3 ships on its own release train, separate from the CMP version.
|
||||
compose-material3 = "1.9.0"
|
||||
androidx-activityCompose = "1.13.0"
|
||||
zxing-embedded = "4.3.0"
|
||||
|
||||
[libraries]
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||
# In-app QR scanner: a ready-made scanning Activity (camera preview, runtime
|
||||
# permission prompt, flashlight toggle) reached through the AndroidX Activity
|
||||
# Result API (ScanContract, added in 4.3.0). Fully offline -- no Play
|
||||
# Services / ML Kit model download involved.
|
||||
zxing-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing-embedded" }
|
||||
# Declared directly rather than through the plugin's `compose.*` accessors,
|
||||
# which are deprecated as of CMP 1.11.
|
||||
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" }
|
||||
|
||||
Reference in new issue
Block a user