Commit Graph

27 Commits

Author SHA1 Message Date
bvandeusen fe878a392c fix(android): split wire types — one declaration per file
Hypothesis for the KSP2 "LibraryApi could not be resolved" failure:
ArtistWire.kt and AlbumWire.kt each declared TWO @Serializable
classes (the Ref and the Detail variant). LibraryApi imports the
Detail variants but the file names match the Ref variants. KSP2's
symbol indexing may key on `className.kt` and fail to surface the
second declaration in a multi-class file.

Splitting per the MatchingDeclarationName convention:
  - ArtistDetailWire.kt (new)
  - AlbumDetailWire.kt (new)
  - ArtistWire.kt / AlbumWire.kt now contain only their namesake type

If this fixes it, the LibraryApi resolution will work without
changing the @Provides signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:57:04 -04:00
bvandeusen ee72459881 fix(android): use retrofit.create<T>() extension in provideLibraryApi
CI hit a KSP/Hilt resolution error on the prior commit:
  ModuleProcessingStep was unable to process 'NetworkModule' because
  'LibraryApi' could not be resolved.

Switching from `retrofit.create(LibraryApi::class.java)` to the Kotlin
extension `retrofit.create()` (with explicit `LibraryApi` return type
annotation). The extension is reified and may sidestep whatever
type-resolution path the previous form tripped under KSP2 + Hilt.

If this also fails, the next step is to split AlbumWire.kt and
ArtistWire.kt so each file has a single top-level declaration —
investigating cross-file symbol-resolution order in KSP2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:03:59 -04:00
bvandeusen a4c20816bc feat(android): LibraryApi Retrofit interface + wire types (M8 phase 5.1)
Mirrors flutter_client/lib/api/endpoints/library.dart 1:1.

Wire types (snake_case @SerialName per server JSON):
  - TrackWire — id/title/album/artist/duration/streamUrl + nullable
    track/disc numbers (fields verified against TrackRef.fromJson in
    flutter_client/lib/models/track.dart)
  - ArtistWire / ArtistDetailWire — the detail shape embeds "albums"
  - AlbumWire / AlbumDetailWire — the detail shape embeds "tracks"

The Detail variants are explicit data classes (rather than a generic
envelope) because the server returns ArtistRef fields PLUS the
embedded array in the same object, which kotlinx.serialization can't
deserialize through a polymorphic envelope.

LibraryApi endpoints:
  - getTrack(id)
  - getArtistDetail(id) — ArtistDetailWire
  - getArtistTracks(id) — bare List<TrackWire> (server emits a bare
    array, NOT enveloped; Retrofit handles it via List return type)
  - getAlbumDetail(id) — AlbumDetailWire
  - shuffleLibrary(limit) — bare List<TrackWire>

Home endpoints (/api/home and /api/home/index) deferred to a future
HomeApi file because they have their own (larger) wire types that
only the Home screen consumes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:56:10 -04:00
bvandeusen 156162e3ac feat(android): port auth_session + promote AuthStore to Room (M8 4.2 slice 10)
Last slice. Promotes the Phase 3.1 in-memory AuthStore placeholder to a
Room-backed single-row auth_session table so session cookie + base URL
survive process death.

Design — hybrid storage:
  - MutableStateFlow is the primary read source so interceptor-thread
    reads stay synchronous (no awaiting a DAO call from inside an
    OkHttp interceptor)
  - Writes update the in-memory state synchronously AND launch a
    write-through coroutine that persists to the DAO
  - init() collects dao.observe() to keep in-memory in sync with
    persisted state on app start + any external DB writes

AuthSessionDao gets partial-update queries (`setSessionCookie` /
`setBaseUrl`) so we don't have to round-trip the full row on every
mutation. First write does an upsert to seed the row.

DatabaseModule grows a @Provides for AuthSessionDao — Hilt can't inject
AppDatabase's abstract DAO accessors directly; each consumer-needed DAO
gets a thin bridge.

AuthCookieInterceptorTest updated: AuthStore now takes (dao, scope)
constructor args. Test uses mockk for the DAO and TestScope with
UnconfinedTestDispatcher so the in-memory state mutations the test
asserts on aren't affected by the asynchronous DAO writes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:21:42 -04:00
bvandeusen e42f2bf525 feat(android): port cached_home_index (M8 4.2 slice 9)
Mirrors flutter_client/lib/cache/db.dart's CachedHomeIndex — per-item
rows that drive the Home screen sections (Recently Added Albums,
Rediscover Albums/Artists, Most Played Tracks, Last Played Artists).

Composite PK (section, position) — exactly one row per slot per
section; sync replaces in-place via upsert. entityType ("album" /
"artist" / "track") dispatches per-tile hydration to the right
per-entity endpoint when the Home screen renders.

DAO surface fits the sync flow:
  - observeBySection (Flow) for the Home composables
  - getBySection (suspend) for one-shot sync reads
  - upsertAll for sync writes
  - deleteBySection for replace-all on a section sync

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:37 -04:00
bvandeusen f3a0c44460 feat(android): port cached_resume_state + fix CacheSource enum (M8 4.2 slice 8)
Two related fixes from re-reading the Drift source for slice 8:

  1. The plan called this slice "last_played" but the Drift table is
     `cached_resume_state` — kept Drift's name for cross-reference
     during the port. Single-row JSON-blob pattern (queue, currentIndex,
     positionMs, source) — ResumeController (Phase 6.5) handles the
     Kotlin-side encode/decode so the schema stays stable across
     resume-shape evolution.

  2. CacheSource enum was incomplete: Task 4.1 ported only 3 of the 5
     Drift variants. Added AUTO_LIKED + AUTO_PLAYLIST (used by the
     auto-cache prefetcher to tag cached files by reason — drives the
     bucket eviction priority order INCIDENTAL > AUTO_PREFETCH >
     AUTO_PLAYLIST > AUTO_LIKED > MANUAL).

No data migration needed — schema version is still 1 and we have no
real users yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:35:10 -04:00
bvandeusen 0b1ccd59b1 feat(android): port cached_mutations (M8 4.2 slice 6)
Mirrors flutter_client/lib/cache/db.dart's CachedMutations — the
offline-write queue that MutationQueue.enqueue() inserts into when a
server-write fails with IOException and MutationReplayer.drain() pops
from when connectivity returns (Phase 12.2).

`kind` is a string registered in `MutationKind` (Phase 12.2) so the
replayer can map to the right handler. `payload` is JSON-serialized
args. Unknown kinds get dropped at drain time rather than wedging.

DAO surface tailored to the replayer:
  - observePendingCount: Flow<Int> for the offline-indicator badge
  - getAll: FIFO list for drain (id ASC = oldest first)
  - insert: returns the autoGenerate'd id
  - recordAttempt(id, instant): atomic increment + lastAttemptAt set
  - delete(id) / clear

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:02:02 -04:00
bvandeusen e63034ec9c feat(android): port audio_cache_index (M8 4.2 slice 5)
Mirrors flutter_client/lib/cache/db.dart's AudioCacheIndex — one row
per fully-downloaded audio file. Drives the 2-bucket LRU eviction
policy that Phase 12's AudioCacheEvictionWorker will execute.

DAO surface tailored to the eviction worker:
  - totalBytes / bytesBySource — sum-of-sizeBytes for cap checks
  - evictionCandidates(source) — oldest lastPlayedAt within a source
    bucket, NULL lastPlayedAt sorted first (never-played candidates
    evict before played ones)
  - touchLastPlayed(trackId, instant) — single-column update from
    the player on every "ready+playing" transition
  - bulk delete by track-id list for batch evictions

CacheSource enum (MANUAL / INCIDENTAL / AUTO_PREFETCH) ported in
Task 4.1's TypeConverters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:53:57 -04:00
bvandeusen c18ad19418 feat(android): port cached_quarantine_mine (M8 4.2 slice 4)
Mirrors flutter_client/lib/cache/db.dart's CachedQuarantineMine —
the user's flagged-as-hidden tracks. Server returns the full
denormalized snapshot on /api/me/quarantine; the cache mirrors that
shape so the Quarantine screen renders without a join.

`createdAt` is a server ISO-8601 string (canonical timestamp);
`fetchedAt` is our local sync marker.

DAO covers the three known consumers:
  - observeAll for the Quarantine screen (newest first)
  - observeFlaggedTrackIds for feed-level filtering
  - observeIsHidden(trackId) scalar Flow for per-tile UI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:16:11 -04:00
bvandeusen 4b38623229 feat(android): port cached_playlists + cached_playlist_tracks (M8 4.2 slice 3)
Two related entities mirroring flutter_client/lib/cache/db.dart:
  - CachedPlaylists — one row per playlist; `systemVariant` is null for
    user playlists and "for_you" / "songs_like_artist" / etc. for
    system-generated mixes (used by the add-to-playlist sheet filter)
  - CachedPlaylistTracks — composite-PK join table, `position` carries
    ordering inside a playlist

DAO surfaces split user vs system playlists at the query layer so
ViewModels don't have to filter — observeUserPlaylists/observeSystemPlaylists.
PlaylistTrackDao gets a deleteByPlaylist for the replace-all pattern
after a sync delta lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:10:05 -04:00
bvandeusen 7b55f586ac feat(android): port cached_likes (M8 4.2 slice 2)
Mirrors flutter_client/lib/cache/db.dart's CachedLikes Drift table —
composite PK (userId, entityType, entityId) so the same user can
like a track and its album and its artist independently. entityType
is a plain string ("track" | "album" | "artist") for parity with the
Drift schema and the server wire format.

DAO surface tailored to consumers we know are coming:
  - observeLikedTrackIds(userId): Flow<List<String>> — audio-cache
    eviction reads this set to identify "liked" bucket members
  - observeLikedIdsOfType(userId, entityType): generalized variant
  - observeIsLiked(...): scalar Flow for LikeButton composables
  - upsertAll (sync writes)
  - delete (mutation queue → toggle off)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:03:42 -04:00
bvandeusen 17a3e7dd4f feat(android): port cached_artists / cached_albums / cached_tracks (M8 4.2 slice 1)
Mirrors flutter_client/lib/cache/db.dart's CachedArtists / CachedAlbums /
CachedTracks Drift tables. Library cache foundation — LibraryRepository
(Phase 5.2) reads cache-first through these DAOs and refreshes from
server via the sync controller (Phase 12.4).

Column names follow Kotlin idiom (camelCase) instead of Drift's
snake_case; the schema is internal to the native client and the wire
JSON conversion happens in feature-level mappers.

Each DAO carries:
  - observe* (Flow) for cache-first reads in ViewModels
  - getById/getByIds (suspend) for one-shot lookups
  - upsertAll (suspend, REPLACE) for sync writes
  - deleteByIds (suspend) for sync-driven deletes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:28:54 -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 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