Take ktfmt's defaults for the Kotlin half

The server half went to rustfmt's defaults earlier today; this is the same
move for the app, and the same reasoning. Kotlin ships no formatter with
the Gradle build, so the question was which to adopt: ktfmt is Kotlin-org
owned now (it moved from facebook/ktfmt), is a formatter rather than a
configurable linter, and has essentially nothing to tune -- which is what
rule 27 is asking for. ktlint's .editorconfig surface is the thing that
rule warns against, and detekt is static analysis, whose job Android Lint
already does here.

One setting, and it is a choice between the tool's own two styles rather
than a tuning: kotlinLangStyle() is the 4-space one, which is what this
code already was. The 2-space default would have reindented every file to
say nothing.

  ./gradlew :androidApp:ktfmtFormat   to apply
  ./gradlew :androidApp:ktfmtCheck    to verify

Formatting only. The one thing worth checking by hand was the generated
PEM constant, since a leading newline there costs Android's
CertificateFactory its preamble sniff and fails at runtime nowhere near
the cause: ktfmt moved `.trimMargin()` onto its own line and left the
template alone, and the regenerated constant still starts at the opening
quotes.

Verified after: ktfmtCheck, compileDebugKotlin and lintDebug all pass, and
the APK builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-28 04:03:14 -04:00
1 parent dde5042b12
commit 0c13090d70
16 files changed
+585 -496

No files matched your search

@@ -2,7 +2,8 @@ package com.example.aiapp
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -26,18 +27,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
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
import com.journeyapps.barcodescanner.ScanOptions
/**
* 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.
* 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(
@@ -53,29 +51,30 @@ 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(contents.toUri())
if (settings == null) {
error = "Not a valid enrollment code"
} else {
saveServerSettings(context, settings)
onSaved(settings)
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(contents.toUri())
if (settings == null) {
error = "Not a valid enrollment code"
} else {
saveServerSettings(context, settings)
onSaved(settings)
}
}
}
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."
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()) {
@@ -107,8 +106,9 @@ fun SettingsScreen(
// 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
if (
context.checkSelfPermission(Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
) {
scanLauncher.launch(enrollmentScanOptions())
} else {
@@ -116,7 +116,9 @@ fun SettingsScreen(
}
},
modifier = Modifier.fillMaxWidth(),
) { Text("Scan QR code") }
) {
Text("Scan QR code")
}
Spacer(Modifier.height(16.dp))
OutlinedTextField(
@@ -149,35 +151,39 @@ fun SettingsScreen(
Spacer(Modifier.height(8.dp))
}
Button(onClick = {
val portNumber = port.trim().toIntOrNull()
val effectiveToken = token.trim().ifEmpty { existing?.token ?: "" }
when {
host.isBlank() -> error = "Host is required"
portNumber == null || portNumber !in 1..65535 -> error = "Port must be 1-65535"
effectiveToken.isEmpty() -> error = "Token is required -- scan the server's QR or paste it"
else -> {
val settings = ServerSettings(host.trim(), portNumber, effectiveToken)
saveServerSettings(context, settings)
onSaved(settings)
Button(
onClick = {
val portNumber = port.trim().toIntOrNull()
val effectiveToken = token.trim().ifEmpty { existing?.token ?: "" }
when {
host.isBlank() -> error = "Host is required"
portNumber == null || portNumber !in 1..65535 -> error = "Port must be 1-65535"
effectiveToken.isEmpty() ->
error = "Token is required -- scan the server's QR or paste it"
else -> {
val settings = ServerSettings(host.trim(), portNumber, effectiveToken)
saveServerSettings(context, settings)
onSaved(settings)
}
}
}
}) { Text("Save") }
) {
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.
* 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.
* 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()