From dde5042b129894fdf9de1991fadb705c51e202bb Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 28 Aug 2026 04:00:57 -0400 Subject: [PATCH] Run the linter the app build already had, and fix what it found `./gradlew :androidApp:lintDebug` had apparently never been run. It reported 11 errors, 8 warnings and 3 hints, and one of the errors was a crash: UsageScreen formats its reset countdown with java.time -- OffsetDateTime and Duration, both API 26 -- while minSdk is 24 and core library desugaring was off. On 24 and 25 that is a NoClassDefFoundError, and the `catch (_: Exception)` around the code does not stop an Error, so the usage screen would have died rather than degraded. Core library desugaring is now on, with desugar_jdk_libs 2.1.5. Verified by looking in the built APK rather than trusting the flag: it now carries Lj$/time/Duration and Lj$/time/OffsetDateTime, the backported classes the call sites are rewritten against. The rest: the two KTX suggestions taken (SharedPreferences.edit's block form, which cannot forget its apply(), and String.toUri), the three autoboxing hints taken (mutableIntStateOf/mutableLongStateOf), and androidx.core:core-ktx declared at 1.19.0 rather than inherited through activity-compose, since this code now calls its extensions directly. Two are suppressed with their reasons, both scoped to the one element. DiscouragedApi on the scanner's screenOrientation, which is not a pin but the removal of the library's landscape lock. MissingApplicationIcon, because there is no icon yet and that is a decision to make later, not an oversight -- an app with no icon is obvious to anyone who opens a launcher, so the warning tells nobody here anything. 0 errors now. The 4 warnings left are one thing: Compose Multiplatform 1.12.0 is out and this is on 1.11.1. That is an upgrade to decide on, not a defect, so it is left for its own change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw --- app/androidApp/build.gradle.kts | 10 ++++++++ app/androidApp/src/main/AndroidManifest.xml | 23 ++++++++++++++++--- .../main/kotlin/com/example/aiapp/AppRoot.kt | 3 ++- .../kotlin/com/example/aiapp/MainActivity.kt | 3 ++- .../kotlin/com/example/aiapp/ServerConfig.kt | 12 +++++----- .../kotlin/com/example/aiapp/SessionScreen.kt | 3 ++- .../com/example/aiapp/SettingsScreen.kt | 3 ++- app/gradle/libs.versions.toml | 10 ++++++++ 8 files changed, 54 insertions(+), 13 deletions(-) diff --git a/app/androidApp/build.gradle.kts b/app/androidApp/build.gradle.kts index abb9dc9..6f9b853 100644 --- a/app/androidApp/build.gradle.kts +++ b/app/androidApp/build.gradle.kts @@ -106,6 +106,11 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 + // minSdk is 24 and UsageScreen formats its countdown with + // java.time, which the platform only has from 26. Without this it + // is a NoClassDefFoundError on 24 and 25 -- an Error, so the + // catch around that code does not stop it. + isCoreLibraryDesugaringEnabled = true } } @@ -121,10 +126,15 @@ androidComponents { } dependencies { + // Not a library this code calls: it is what `isCoreLibraryDesugaring + // Enabled` above rewrites java.time against, so API 24 and 25 have it. + coreLibraryDesugaring(libs.desugar.jdk.libs) + implementation(libs.compose.runtime) implementation(libs.compose.foundation) implementation(libs.compose.material3) implementation(libs.compose.ui) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core.ktx) implementation(libs.zxing.embedded) } diff --git a/app/androidApp/src/main/AndroidManifest.xml b/app/androidApp/src/main/AndroidManifest.xml index a334a2e..a0aa264 100644 --- a/app/androidApp/src/main/AndroidManifest.xml +++ b/app/androidApp/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + + android:theme="@android:style/Theme.Material.Light.NoActionBar" + tools:ignore="MissingApplicationIcon"> @@ -46,12 +54,21 @@ code being scanned is usually on a monitor in front of someone holding the phone upright. zxing_CaptureTheme is the library's own fullscreen theme, which is all the activity needs. --> + + android:windowSoftInputMode="stateAlwaysHidden" + tools:ignore="DiscouragedApi" /> diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt index f94e1fd..b8b867e 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt @@ -3,6 +3,7 @@ package com.example.aiapp import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -32,7 +33,7 @@ fun AppRoot(settingsVersion: Int) { var screen by remember { mutableStateOf(Screen.SessionList) } // Bumped whenever another screen changes something the list shows, so // returning to it refetches instead of showing a stale list. - var reloadToken by remember { mutableStateOf(0) } + var reloadToken by remember { mutableIntStateOf(0) } val current = settings if (current == null) { diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/MainActivity.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/MainActivity.kt index f7dc524..e73f82d 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/MainActivity.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -24,7 +25,7 @@ import androidx.core.view.WindowCompat class MainActivity : ComponentActivity() { // Bumped whenever enrollment lands via an aiapp:// intent so the // composition below re-reads the stored settings. - private var settingsVersion by mutableStateOf(0) + private var settingsVersion by mutableIntStateOf(0) // Registered up front since permission launchers must be registered // before the activity reaches STARTED. diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/ServerConfig.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/ServerConfig.kt index a012f41..15f6fb0 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/ServerConfig.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/ServerConfig.kt @@ -2,6 +2,7 @@ package com.example.aiapp import android.content.Context import android.net.Uri +import androidx.core.content.edit import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 @@ -37,12 +38,11 @@ fun loadServerSettings(context: Context): ServerSettings? { } fun saveServerSettings(context: Context, settings: ServerSettings) { - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - .edit() - .putString(KEY_HOST, settings.host) - .putInt(KEY_PORT, settings.port) - .putString(KEY_TOKEN, seal(settings.token)) - .apply() + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putString(KEY_HOST, settings.host) + putInt(KEY_PORT, settings.port) + putString(KEY_TOKEN, seal(settings.token)) + } } /** diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt index e92e4e8..a1c0285 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -124,7 +125,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: () val scope = rememberCoroutineScope() var items by remember { mutableStateOf(listOf()) } var status by remember { mutableStateOf(summary.status) } - var totalTokens by remember { mutableStateOf(0L) } + var totalTokens by remember { mutableLongStateOf(0L) } var streamError by remember { mutableStateOf(null) } var actionError by remember { mutableStateOf(null) } var input by remember { mutableStateOf("") } 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 88ef4c3..6cf1ccb 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SettingsScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SettingsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment 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 @@ -56,7 +57,7 @@ fun SettingsScreen( // 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)) + val settings = parseEnrollmentUri(contents.toUri()) if (settings == null) { error = "Not a valid enrollment code" } else { diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index b344074..6a90864 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -8,15 +8,25 @@ 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" +# Declared rather than inherited because this +# code now calls its extensions directly (SharedPreferences.edit, String +# .toUri), and a transitive it merely inherited could change under it. +androidx-core-ktx = "1.19.0" zxing-embedded = "4.3.0" +# Backports java.time (and more) to API 24, which UsageScreen needs: its +# reset countdown is OffsetDateTime/Duration, both API 26. Checked +# 2026-08-28 against Google Maven. +desugar-jdk-libs = "2.1.5" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } # 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" } +desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar-jdk-libs" } # 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" }