New launcher icon, version 0.2.0, lint cleanup

Icon: a capsule in a hurry — stadium pill at 45° with motion lines
trailing, «med det samme» as imagery. versionCode 2 / versionName 0.2.0
with a bump-on-release note. Lint: KTX conversions (prefs.edit{},
toUri, createBitmap), mipmap-anydpi-v26 merged into mipmap-anydpi
(minSdk 26 makes the qualifier dead weight), compose-bom 2026.05.01,
Gradle wrapper 9.5.1, and the three deliberate-decision warnings
(BatteryLife/OldTargetApi/NewerVersionAvailable) disabled with
documented reasons. Lint now runs clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-06-11 17:55:06 +02:00
commit 80c8c4788a
9 changed files with 52 additions and 27 deletions

View file

@ -31,8 +31,10 @@ android {
applicationId = "no.naiv.meddetsamme" applicationId = "no.naiv.meddetsamme"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 // Bump both for every release installed on the phone — versionName for
versionName = "0.1.0" // humans, versionCode so dumpsys/install history can tell builds apart.
versionCode = 2
versionName = "0.2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@ -67,6 +69,14 @@ android {
compose = true compose = true
} }
lint {
// Deliberate decisions, not oversights — see CLAUDE.md:
// BatteryLife: decision #4, sideloaded app (Play policy n/a).
// OldTargetApi: targetSdk 35 is pinned by the brief.
// NewerVersionAvailable: Kotlin 2.4 is blocked until KSP supports it.
disable += listOf("BatteryLife", "OldTargetApi", "NewerVersionAvailable")
}
// MigrationTestHelper reads the exported schema JSONs as androidTest assets. // MigrationTestHelper reads the exported schema JSONs as androidTest assets.
sourceSets.getByName("androidTest").assets.srcDir("$projectDir/schemas") sourceSets.getByName("androidTest").assets.srcDir("$projectDir/schemas")
} }

View file

@ -2,9 +2,9 @@ package no.naiv.meddetsamme.alarm
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import androidx.core.net.toUri
/** /**
* Battery-optimisation ("Doze whitelist") exemption brief decision #4: the * Battery-optimisation ("Doze whitelist") exemption brief decision #4: the
@ -24,6 +24,6 @@ object BatteryOptimization {
fun requestExemptionIntent(context: Context): Intent = fun requestExemptionIntent(context: Context): Intent =
Intent( Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${context.packageName}"), "package:${context.packageName}".toUri(),
) )
} }

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import androidx.core.content.edit
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.KeyGenerator import javax.crypto.KeyGenerator
@ -37,22 +38,22 @@ class SettingsStore(context: Context) {
/** Non-secret flags can live in plaintext prefs. */ /** Non-secret flags can live in plaintext prefs. */
var batteryPromptShown: Boolean var batteryPromptShown: Boolean
get() = prefs.getBoolean("battery_prompt_shown", false) get() = prefs.getBoolean("battery_prompt_shown", false)
set(v) = prefs.edit().putBoolean("battery_prompt_shown", v).apply() set(v) = prefs.edit { putBoolean("battery_prompt_shown", v) }
/** FEST dataset URL — not a secret, plaintext pref. */ /** FEST dataset URL — not a secret, plaintext pref. */
var festUrl: String? var festUrl: String?
get() = prefs.getString("fest_url", null) get() = prefs.getString("fest_url", null)
set(v) = prefs.edit().putString("fest_url", v).apply() set(v) = prefs.edit { putString("fest_url", v) }
/** One-time "backup isn't set up" card on the Today screen. */ /** One-time "backup isn't set up" card on the Today screen. */
var backupNudgeDismissed: Boolean var backupNudgeDismissed: Boolean
get() = prefs.getBoolean("backup_nudge_dismissed", false) get() = prefs.getBoolean("backup_nudge_dismissed", false)
set(v) = prefs.edit().putBoolean("backup_nudge_dismissed", v).apply() set(v) = prefs.edit { putBoolean("backup_nudge_dismissed", v) }
/** Daily low-stock/rx check time, minutes after midnight. Default 10:00. */ /** Daily low-stock/rx check time, minutes after midnight. Default 10:00. */
var supplyCheckMinuteOfDay: Int var supplyCheckMinuteOfDay: Int
get() = prefs.getInt("supply_check_minute", 600) get() = prefs.getInt("supply_check_minute", 600)
set(v) = prefs.edit().putInt("supply_check_minute", v.coerceIn(0, 1439)).apply() set(v) = prefs.edit { putInt("supply_check_minute", v.coerceIn(0, 1439)) }
val isBackupConfigured: Boolean val isBackupConfigured: Boolean
get() = !s3Endpoint.isNullOrBlank() && !s3Bucket.isNullOrBlank() && get() = !s3Endpoint.isNullOrBlank() && !s3Bucket.isNullOrBlank() &&

View file

@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import java.time.LocalDate import java.time.LocalDate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -111,14 +112,11 @@ fun FormAvatar(form: MedForm) {
* reading only. Decision #8 bans Felleskatalogen as a DATA source (licensed * reading only. Decision #8 bans Felleskatalogen as a DATA source (licensed
* editorial content, no API); pointing the human at their website is fine. * editorial content, no API); pointing the human at their website is fine.
*/ */
fun felleskatalogenIntent(name: String): android.content.Intent = fun felleskatalogenIntent(name: String): android.content.Intent {
android.content.Intent( val url = "https://www.felleskatalogen.no/medisin/sok?sokord=" +
android.content.Intent.ACTION_VIEW, java.net.URLEncoder.encode(name.trim(), "UTF-8")
android.net.Uri.parse( return android.content.Intent(android.content.Intent.ACTION_VIEW, url.toUri())
"https://www.felleskatalogen.no/medisin/sok?sokord=" + }
java.net.URLEncoder.encode(name.trim(), "UTF-8"),
),
)
/** /**
* State + validation for an [InventoryItem] form shared between the * State + validation for an [InventoryItem] form shared between the

View file

@ -64,7 +64,7 @@ fun SummaryPreviewScreen(nav: Nav) {
renderer.openPage(i).use { page -> renderer.openPage(i).use { page ->
// 2x point size — crisp on phone screens without // 2x point size — crisp on phone screens without
// burning memory on huge bitmaps. // burning memory on huge bitmaps.
val bmp = Bitmap.createBitmap(page.width * 2, page.height * 2, Bitmap.Config.ARGB_8888) val bmp = androidx.core.graphics.createBitmap(page.width * 2, page.height * 2)
bmp.eraseColor(android.graphics.Color.WHITE) bmp.eraseColor(android.graphics.Color.WHITE)
page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
rendered += bmp rendered += bmp

View file

@ -1,21 +1,37 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- A capsule at 45°: two half-pill rounded shapes. Placeholder-quality, but ours. --> <!-- A capsule in a hurry: stadium-shaped pill flying up-right at 45°, with
motion lines trailing — «med det samme» as an icon. Safe zone: center 66
of the 108 viewport. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<group <group
android:rotation="45" android:rotation="-45"
android:pivotX="54" android:pivotX="54"
android:pivotY="54"> android:pivotY="54">
<!-- motion lines trailing behind (left of) the capsule -->
<path <path
android:fillColor="#FFFFFF" android:fillColor="#80FFFFFF"
android:pathData="M42,30 h24 a12,12 0 0 1 12,12 v0 h-48 v0 a12,12 0 0 1 12,-12 z" android:pathData="M18,51.5 h12 a2.5,2.5 0 0 1 0,5 h-12 a2.5,2.5 0 0 1 0,-5 z" />
android:strokeWidth="0" /> <path
android:fillColor="#59FFFFFF"
android:pathData="M14,43.5 h10 a2.5,2.5 0 0 1 0,5 h-10 a2.5,2.5 0 0 1 0,-5 z" />
<path
android:fillColor="#59FFFFFF"
android:pathData="M14,59.5 h10 a2.5,2.5 0 0 1 0,5 h-10 a2.5,2.5 0 0 1 0,-5 z" />
<!-- capsule: full stadium in pale green -->
<path <path
android:fillColor="#A5D6A7" android:fillColor="#A5D6A7"
android:pathData="M30,42 h48 v24 a12,12 0 0 1 -12,12 h-24 a12,12 0 0 1 -12,-12 z" android:pathData="M50,43 h26 a11,11 0 0 1 0,22 h-26 a11,11 0 0 1 0,-22 z" />
android:strokeWidth="0" /> <!-- leading half in white -->
<path
android:fillColor="#FFFFFF"
android:pathData="M63,43 h13 a11,11 0 0 1 0,22 h-13 z" />
<!-- highlight on the leading cap -->
<path
android:fillColor="#33FFFFFF"
android:pathData="M72,46.5 a3,3 0 1 1 0,6 a3,3 0 1 1 0,-6 z" />
</group> </group>
</vector> </vector>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- minSdk 26 = adaptive icons everywhere; no legacy PNG densities needed. --> <!-- minSdk 26 = adaptive icons everywhere (anydpi, no -v26 qualifier needed). -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />

View file

@ -6,7 +6,7 @@
agp = "9.2.1" agp = "9.2.1"
kotlin = "2.3.10" kotlin = "2.3.10"
ksp = "2.3.9" # KSP versioning decoupled from Kotlin since 2.3.0 (no more <kotlin>-<ksp> pairs) ksp = "2.3.9" # KSP versioning decoupled from Kotlin since 2.3.0 (no more <kotlin>-<ksp> pairs)
composeBom = "2026.05.00" composeBom = "2026.05.01"
activityCompose = "1.13.0" activityCompose = "1.13.0"
coreKtx = "1.19.0" coreKtx = "1.19.0"
lifecycle = "2.10.0" lifecycle = "2.10.0"

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME