Commit Graph

15 Commits

Author SHA1 Message Date
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 53f69fb1f5 feat(android): NetworkModule + AuthCookieInterceptor + AuthStore placeholder
M8 phase 3.1. Wires the single shared OkHttp + Retrofit instance the
whole app uses (per-endpoint Retrofit interfaces land in feature
modules). Audio HTTP via ExoPlayer's OkHttpDataSource.Factory will
reuse this same client — single auth/connection-pool surface.

  - AuthStore: in-memory MutableStateFlow placeholder. Task 4.2
    promotes it to a Room-backed single-row table for process-death
    persistence; public API stays identical.
  - AuthCookieInterceptor: attaches Cookie on outbound, captures
    Set-Cookie on successful responses (login flow), clears the store
    on 401 (logout signal).
  - ServerBaseUrl: value class to type-safely DI the base URL.
  - NetworkModule: Hilt-provided OkHttp + Retrofit + HttpLogging.

First task with unit tests — AuthCookieInterceptorTest uses MockWebServer
to verify all three interceptor branches plus a no-Set-Cookie-no-overwrite
case. Will tell us if the JUnit 5 + okhttp-mockwebserver test stack is
plumbed correctly through Gradle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:00:41 -04:00
bvandeusen 25ae70c5bd fix(android): clear detekt findings — rename files, wrap long lines, allow Composable PascalCase
Seven findings from the first real detekt run:

  - LocalActionColors.kt / Tokens.kt: MatchingDeclarationName flagged
    that the file names don't match the single top-level declaration.
    Renamed to ActionColors.kt and FabledSwordTokens.kt (`git mv`).
  - Typography.kt: three Font(...) calls exceeded the default 120-char
    line length. Wrapped each named-arg list onto its own line.
  - MainActivity.kt + MinstrelTheme.kt: FunctionNaming flagged App() /
    MinstrelTheme() for not starting lowercase — these are
    @Composable functions and PascalCase is the Compose convention.
    Added a config override to the detekt YAML:

      naming:
        FunctionNaming:
          ignoreAnnotated: ['Composable']

    Matches every mainstream Compose codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:33:35 -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 a948a71fa5 feat(android): Hilt application + AppModule (Json + ApplicationScope)
M8 phase 1.3. Plants the Hilt entrypoint so the rest of the modules
(NetworkModule, DatabaseModule, PlayerModule) can land in subsequent
phases. WorkerFactory wired so HiltWorker can be used directly later.

Restores @AndroidEntryPoint on MainActivity (deferred from 1.2 since
Hilt KSP errors without an annotated Application class).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 21:58:30 -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