Commit Graph

19 Commits

Author SHA1 Message Date
bvandeusen 9f0af9c24b feat(android): CI-injected versionName with commit-count iteration
android / Build + lint + test (push) Successful in 4m12s
The APK was shipping with a hardcoded versionName="0.1.0-native"
and versionCode=1 — so the About card never reflected the actual
release, and two same-day re-cuts of the per-day mutable tag
v2026.06.02 looked identical to the update-banner comparator.
Operator wants the iteration restored on the APK side (the docker
tag stays plain per the earlier intentional change).

Scheme:
- Per release: versionName = "${tag}.${commit_count}", e.g.
  "2026.06.02.142", where commit_count = `git rev-list --count HEAD`.
  Monotonic across the project lifetime, deterministic, no manual
  counter to maintain.
- versionCode = commit_count. Monotonic, fits in Int forever (we're
  not hitting 2.1B commits).
- Local / debug / dev builds fall back to versionName="dev" /
  versionCode=1 so the About card reads honestly.

build.gradle.kts:
- defaultConfig reads MINSTREL_VERSION_NAME / MINSTREL_VERSION_CODE
  Gradle properties via project.findProperty with the dev fallbacks.

.gitea/workflows/release.yml:
- android-release: checkout with fetch-depth: 0 (the default shallow
  clone would return 1 for `git rev-list --count HEAD`); new
  Compute release version step exports name + code as step outputs;
  assembleRelease passes them via -P; new job-level outputs propagate
  them to the downstream image-release job.
- image-release: Stage bundled APK + version sidecar pulls the
  computed version_name from needs.android-release.outputs and
  writes it into client/minstrel.apk.version, so the server's
  /api/client/version reports the exact string baked into the APK.
  Without this the sidecar would say "v2026.06.02" while the
  installed APK has "2026.06.02.142" — isVersionNewer would call
  the bundled APK older and the update banner would thrash.

The existing isVersionNewer comparator already handles the
4-component shape ("2026.06.02.142" > "2026.06.02.141"), so no
client-side logic changes are needed.
2026-06-02 14:07:57 -04:00
bvandeusen b83a6a4bdb revert(android): top-nav Library icon back to Lucide.LibraryBig
android / Build + lint + test (push) Has been cancelled
Operator polled another user and reversed the earlier swap to
Material's LibraryMusic. Restore Lucide.LibraryBig and drop the
material-icons-extended Gradle dependency we added for the
intermediate icon, keeping the icon set Lucide-only.
2026-06-02 10:03:15 -04:00
bvandeusen faf2cac0c9 feat(android): use Material Outlined LibraryMusic for top-nav Library
android / Build + lint + test (push) Failing after 2m34s
Lucide has no music-library glyph — Library / LibraryBig /
SquareLibrary all read as a generic books-on-shelf icon without
a label. Operator picked Material's LibraryMusic (the canonical
"books + music note" symbol used by every major music app) as
the recognizable alternative.

Use the Outlined variant: filled icons would clash with the
neighbouring stroked Lucide icons (House, Search, EllipsisVertical),
but Outlined's stroke style matches Lucide closely enough that
the mix is subtle.

Adds the compose-material-icons-extended dependency (version
pinned by compose-bom). R8 strips unused icons in release builds
so the APK cost is just the ones we actually reference.
2026-06-02 09:40:49 -04:00
bvandeusen 01fdd2f380 feat(android): NowPlaying dominant-color gradient backdrop (audit v2 #17, slice 1)
Adds androidx.palette dependency and a rememberDominantColor()
helper that pulls the cover bitmap via Coil's singleton loader
(shares the cache with the on-screen AsyncImage), runs Palette
extraction on Dispatchers.Default, and animates the resulting
color with animateColorAsState so track changes tween smoothly.

NowPlayingScreen wraps the body in a Box with a vertical gradient
(0% dominant @ 55%α → 45% dominant @ 18%α → 100% scheme.background)
and lets the Scaffold's containerColor go transparent so the
gradient shows through. Falls back to a near-transparent gradient
on bitmap-load failure so the screen never sits on flat black.

Hero transition (MiniPlayer → NowPlaying cover) and cover
preload still pending — those land in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:08:00 -04:00
bvandeusen a42a4df736 fix(android): silence JVM + Kotlin annotation-target warnings
Two cleanups around the noise at the top of every CI log:

* CI workflow: JAVA_TOOL_OPTIONS=--enable-native-access=ALL-UNNAMED
  at the workflow env level so both build + release jobs apply it to
  the launcher JVM (not just the daemon). The launcher is the one
  loading native-platform.jar via System.load.

* Kotlin compiler: -Xannotation-default-target=param-property in
  kotlin.compilerOptions.freeCompilerArgs. Opts every @Inject /
  @ApplicationContext / @ApplicationScope constructor-parameter
  annotation into the future Kotlin 2.3 behavior (apply to both
  param AND property), clearing the 11 warnings on AuthController /
  AuthStore / MutationReplayer / SyncController / EventsStream /
  LiveEventsDispatcher / PlayEventsReporter / PlayerController /
  PlayerFactory / ResumeController.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:51:36 -04:00
bvandeusen 4580f950e3 feat(android): PlayEventsReporter — wire mobile plays to server
Observes PlayerController.uiState and runs the (current track,
playing) state machine. Fires play_started on track-begin, then
on close emits play_ended (within 3s of duration) or play_skipped
through the live EventsApi. Failures and offline-start plays fall
through to the MutationQueue PLAY_OFFLINE kind for durable replay.
App-background (ProcessLifecycleOwner onStop) closes the current
play via the offline path so a process kill mid-listen still
records a play.

Wires into MinstrelApplication via @Inject so the singleton
constructs at app start (same pattern as MutationReplayer /
SyncController / ResumeController). client_id is a stable
device-install UUID resolved through AuthStore (added in 415200d).

Adds androidx.lifecycle:lifecycle-process dependency for the
ProcessLifecycleOwner background-event hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:04:23 -04:00
bvandeusen e934da30a1 feat(android): Library Compose screens + Coil OkHttp sharing + Lucide (M8 phase 5.4)
First user-visible UI. LibraryScreen renders the LibraryViewModel
UiState into horizontally-scrolling Artist / Album rows; Loading
shows a centered spinner, Empty / Error fall back to shared widgets.

Files:
  - library/ui/LibraryScreen.kt — top-level screen, hiltViewModel
    + collectAsStateWithLifecycle, exhaustive when(state)
  - library/widgets/ArtistCard.kt — circular cover + name beneath
  - library/widgets/AlbumCard.kt — 144dp square cover + title +
    artist beneath, matches Flutter spec (~176dp tile width)
  - shared/widgets/EmptyState.kt — generic empty-state widget
    (Lucide Inbox by default), used by Library + reusable for
    Quarantine / search etc.
  - shared/widgets/ErrorRetry.kt — error message + retry button
    (uses LocalActionColors.primary = Moss per design system rule)

Audit-deferred items now triggered:
  - MinstrelApplication implements SingletonImageLoader.Factory and
    wires OkHttpNetworkFetcherFactory(callFactory = { okHttpClient })
    so Coil cover-art requests reuse the shared auth-bearing OkHttp
  - Lucide icons via com.composables:icons-lucide-android:2.2.1 for
    placeholder / decorative iconography

MainActivity now renders LibraryScreen inside a Scaffold (not the
"phase 1" text placeholder). Nav-graph wiring deferred to Phase 5.5
— onArtistClick / onAlbumClick are no-op for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:38:08 -04:00
bvandeusen a10079e8ef feat(android): Room foundation — AppDatabase + TypeConverters + Gradle plugin
M8 phase 4.1. Pre-flight research (per the feedback_m8_preflight_research
rule) found two needed adjustments to the original plan:

  1. Room 2.6.1 -> 2.8.4. Room 2.6.1 (Oct 2023) predates Kotlin 2.0
     mainstream; Room 2.7+ explicitly supports Kotlin 2.0+ and KSP2.
     Room 2.8.4 is current stable; minSdk 23 (we're at 26) and AGP 8.4+
     (we're on 9), both compatible.
  2. Use the androidx.room Gradle plugin's `room { schemaDirectory(...) }`
     block instead of the legacy `ksp { arg("room.schemaLocation", ...) }`
     pattern. Cleaner schema-export plumbing in Room 2.7+. Audit-deferred
     item; trigger condition (first Room entity) met here.

Files:
  - cache/db/AppDatabase.kt — @Database stub, schemaVersion 1
  - cache/db/TypeConverters.kt — Instant <-> Long, CacheSource enum
  - cache/db/DatabaseModule.kt — Hilt-provided AppDatabase singleton
  - cache/db/entities/SyncMetadataEntity.kt — pulled forward from
    Task 4.2 slice 7 to satisfy Room's "needs >=1 entity" compile check;
    its consumer (SyncController) lands in Phase 12.4
  - cache/db/dao/SyncMetadataDao.kt — minimal observe/get/upsert
  - libs.versions.toml — Room 2.8.4, androidx-room plugin alias
  - app/build.gradle.kts — apply androidx.room plugin, add room {}
    block, drop ksp room.schemaLocation arg

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:22:11 -04:00
bvandeusen 4cbbdbfef9 chore(android): audit-response wave — JUnit launcher, compileSdk 36, CC warn, image :36, Gradle cache
Fixes uncovered by the 2026-05-22 build-config audit + the Gradle 9 +
JUnit Platform launcher requirement that just surfaced in CI:

  - testRuntimeOnly junit-platform-launcher: Gradle 9 no longer auto-
    injects it; tests fail with "Failed to load JUnit Platform" without
    an explicit dep.
  - compileSdk + targetSdk 35 -> 36: AGP 9 defaults to 36 and warns on
    lower values; CI was also auto-downloading build-tools 36 at
    runtime (now pre-installed in ci-android:36).
  - container.image bumped to ci-android:36 to match.
  - configuration-cache.problems=warn in gradle.properties: detekt 2.0-
    alpha + ktlint Gradle plugin have CC compat holes; warn rather
    than fail.
  - androidTest dep parity: kotlin("test") + kotlinx-coroutines-test
    added (was on testImplementation only).
  - CI: actions/cache@v4 for ~/.gradle/{caches,wrapper} + ~/.kotlin,
    keyed on gradle-wrapper.properties + libs.versions.toml + the
    *.gradle.kts files. Saves ~3 min per run after warm-up.

Deferred (with trigger conditions, will land when needed): Hilt
testing artifacts + HiltTestRunner (first @HiltAndroidTest), Room
Gradle plugin + schemaDirectory (Phase 4.2), Coil 3 ImageLoader
factory sharing OkHttp (Phase 5.4), MainDispatcherRule test utility
(Phase 5.3), JUnit-5/4 split for instrumented (Phase 5+),
NetworkSecurityConfig (pre-cutover Phase 14).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:41:00 -04:00
bvandeusen 41af466621 fix(android): add kotlin-test test dep for assertEquals/assertNull
AuthCookieInterceptorTest imports `kotlin.test.assertEquals` /
`kotlin.test.assertNull` which weren't resolving without the explicit
kotlin-test dep. `kotlin("test")` is sourced from the applied Kotlin
plugin (built-in via AGP 9), so no version pin needed.

Hilt + KSP code generation worked correctly in the prior run —
hiltAggregateDepsDebug succeeded; the failure was purely test-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:06:53 -04:00
bvandeusen a68eb1c533 fix(android): migrate detekt block + task types to 2.0 DSL
Three breaking changes I missed when bumping to 2.0.0-alpha.3:

  1. Gradle plugin id changed: io.gitlab.arturbosch.detekt -> dev.detekt
  2. Task FQN changed: io.gitlab.arturbosch.detekt.Detekt
     -> dev.detekt.gradle.Detekt
     (same for DetektCreateBaselineTask)
  3. jvmTarget is now a Property API (.set("17")) instead of var assignment

Also dropped `autoCorrect` from the detekt {} block — it's not in the
2.0 options list per the official getting-started docs.

Per the 2.0 release notes: "the workaround of disabling the new DSL
and built-in Kotlin via gradle.properties for AGP 9.x projects is no
longer required" — so our existing AGP 9 + built-in-Kotlin setup is
expected to work cleanly with detekt 2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:13:36 -04:00
bvandeusen 8c51fe0bbe fix(android): pin detekt jvm-target to 17
detekt 1.23.7 bundles kotlin-compiler-embeddable 1.9.10 whose
--jvm-target validator only accepts up to 22. Detekt auto-detected
the runner's JDK 25 and choked. Pin to 17 (matches our
compileOptions.targetCompatibility + kotlin.compilerOptions.jvmTarget).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 07:58:07 -04:00
bvandeusen 627810aee6 style(android): ktlint multiline-expression-wrapping on build.gradle.kts
Two assignments had a multi-line RHS sitting on the same line as the
`=`. ktlint's multiline-expression-wrapping rule requires the
multi-line expression to start on a new line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 07:55:39 -04:00
bvandeusen 6fb2ff2b9c chore(android): bump Kotlin 2.2 -> 2.3 + KSP 2.0 -> 2.3 for AGP 9 built-in Kotlin
The previous attempt opted out of AGP 9's built-in Kotlin (via
android.builtInKotlin=false + explicit kotlin-android plugin) because
the message from Gradle suggested it. But Kotlin 2.2.21's
kotlin-android plugin can't cast AGP 9's new ApplicationExtension to
the removed BaseExtension:

  class ApplicationExtensionImpl$AgpDecorated_Decorated
  cannot be cast to class com.android.build.gradle.BaseExtension

That suggestion is for projects with an older Kotlin toolchain. The
real fix:

  - Kotlin 2.3.21 (latest stable; first line where kotlin-android also
    supports AGP 9, but more importantly the built-in path works)
  - KSP 2.3.8 — KSP PR #2674 (merged Oct 2025) added AGP 9 built-in
    Kotlin support. KSP 1.x and pre-2.3 don't work with built-in Kotlin.
  - Re-drop the kotlin-android plugin from both build.gradle.kts files;
    AGP 9 enables built-in Kotlin by default and KSP 2.3 cooperates.
  - Remove android.builtInKotlin=false from gradle.properties.

compose-compiler plugin tracks the Kotlin version via version.ref, so
no separate bump there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:04:21 -04:00
bvandeusen 4071894217 fix(android): opt out of AGP 9 built-in Kotlin — KSP incompatible
AGP 9 enables built-in Kotlin by default (`android.builtInKotlin=true`),
which we initially adopted by dropping the `kotlin-android` plugin
alias. But KSP isn't compatible with built-in Kotlin yet — Gradle
errors out with:

  > KSP is not compatible with Android Gradle Plugin's built-in Kotlin.
  > Please disable by adding android.builtInKotlin=false to gradle.properties
  > and apply kotlin("android") plugin

Restored the explicit kotlin-android plugin (root + :app) and added
`android.builtInKotlin=false` to gradle.properties. Revisit when KSP
gains built-in-Kotlin support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:57:49 -04:00
bvandeusen 6dae4b452f fix(android): replace deprecated kotlinOptions with kotlin{compilerOptions}
`kotlinOptions { jvmTarget = "17" }` inside the `android { }` block was
removed in newer Kotlin tooling; replaced with the modern top-level
`kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }` form.

Required after the Kotlin 2.2 + AGP 9 bump; the old DSL was tolerated
through AGP 8.7 + Kotlin 2.0 but not through AGP 9's built-in Kotlin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:55:43 -04:00
bvandeusen d8989aa95c chore(android): bump toolchain to Gradle 9.1 + AGP 9.0.1 + Kotlin 2.2.21 for JDK 25
Original 8.x toolchain choked on the ci-android image's JDK 25 with an
opaque "25.0.3" error in `ktlintCheck`; Gradle 8.10's JDK compat matrix
caps at 23. Modern chain:

  - Gradle 9.1.0 (first to support JDK 25)
  - AGP 9.0.1 (requires Gradle 9.1+, requires Kotlin 2.2.10+)
  - Kotlin 2.2.21 / KSP 2.2.21-2.0.5 (latest 2.2.x line)
  - Compose BOM 2026.05.01 (current; pulls ui-text-google-fonts at the
    BOM-managed version, so the explicit pin was dropped)

AGP 9.0 breaking changes that affected us:
  - `kotlin-android` plugin no longer needed — AGP 9 auto-enables via
    `android.builtInKotlin=true` default. Removed alias from both the
    root build.gradle.kts and :app/build.gradle.kts.
  - `applicationVariants` API removed; we don't use it.
  - Other defaults flipped (useAndroidx, uniquePackageNames, etc.) but
    we already set them explicitly or weren't relying on the old defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:45:37 -04:00
bvandeusen f2f6fa06a2 feat(android): FabledSword theme — Material 3 + Google Fonts
M8 phase 1.4. Mirrors flutter_client/lib/theme/. Source of truth for hex
values is flutter_client/shared/fabledsword.tokens.json (manual sync until
cross-language codegen lands; ports the dark-surface + flat cohort).

Material 3 ColorScheme takes accent as primary; action colors
(Moss/Bronze/Oxblood) live in LocalActionColors as semantic roles per the
project_design_system rule "NEVER use accent for action buttons".

Typography uses androidx.compose.ui.text.googlefonts to fetch Fraunces /
Inter / JetBrains Mono at runtime via Play Services Fonts — matches the
Flutter client's `google_fonts` package (no bundled .ttf files in either
tree). Weights restricted to 400/500. Fraunces is reserved for ≥18sp
display/headline slots per the design-system rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:18:15 -04:00
bvandeusen 34e54f29e9 feat(android): :app module skeleton — Compose Activity + manifest
M8 phase 1.2. AndroidManifest declares FGS mediaPlayback +
POST_NOTIFICATIONS permissions ahead of the player phase. Activity
hosts a single Compose Scaffold for now; nav graph lands in phase 5.

Launcher icons reused from flutter_client/ (same applicationId means
same brand at cutover). MinstrelApplication referenced in manifest
but the class itself lands in Task 1.3 — manifest class names are
resolved at install time, not build time, so the intermediate commit
still builds.

@AndroidEntryPoint deferred to Task 1.3 alongside @HiltAndroidApp on
MinstrelApplication (Hilt KSP errors without an annotated Application).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 21:17:40 -04:00