Kotlin (unlike Java) supports nested block comments. The doc-comment
on LibraryApi contained the string `/api/*` and `/api/home`-style
paths, which the lexer parsed as opening nested comments:
/**
* Retrofit interface for the server's native /api/* library surface. ← lexer: nested /* opens
...
*/ ← closes the nested one
// outer comment now unclosed; "Unclosed comment" reported at EOF
This compile error is what caused all the "ModuleProcessingStep was
unable to process NetworkModule because LibraryApi could not be
resolved" failures over the last four commits — KSP runs before
compileDebugKotlin and reports the downstream symptom (unresolvable
symbol) before the actual source-level error gets to print.
Rewrote the doc-comment to use `/api/...` and to wrap concrete paths
in backticks; no `/*` substring remains.
The "repos construct their Retrofit interface from shared Retrofit"
pattern from the previous commit stays; it's a sound pattern arrived
at via the wrong reasoning, but defensible on its own merits (fewer
Hilt bindings, locality of reference, easier test override).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "ModuleProcessingStep was unable to process NetworkModule because
LibraryApi could not be resolved" failure under KSP2 + Hilt 2.59.2
turns out to be specific to @Provides returning a hand-written Kotlin
interface that carries no KSP-processed annotations. Hilt's
ModuleProcessingStep resolves the return type through KSP2's API and
gets an ERROR type for source-only interfaces in some configurations
(google/dagger#4303 cluster).
Two source-of-truth interfaces I tested side-by-side:
- AuthSessionDao (@Dao, Room-processed) — @Provides works
- LibraryApi (only @GET Retrofit annotations, no KSP processor) — fails
Workaround that's actually a better pattern: feature repositories
construct their Retrofit interface from the Hilt-injected shared
Retrofit instance. Fewer bindings in the Hilt graph; one Retrofit
interface lives next to its sole consumer.
LibraryApi.kt + wire types remain; LibraryRepository (Phase 5.2) will
hold the `retrofit.create<LibraryApi>()` call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
AppDatabase grew its 12th DAO accessor in slice 9 and tripped the
TooManyFunctions rule. Same shape as the @Dao case from slice 5 —
Room types naturally accumulate one method per entity family. Added
"Database" to the ignoreAnnotated list alongside "Dao".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
AudioCacheIndexDao has 12 methods (default rule threshold is 11) — DAOs
accumulate one method per distinct query and inherently exceed the
default. Scoped via ignoreAnnotated: ["Dao"] rather than raising the
global threshold; the rule still catches non-DAO interfaces that grow
unreasonably wide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
detekt 2.0 removed the top-level `build:` key:
Property 'build' is misspelled or does not exist. Allowed properties:
[comments, complexity, config, console-reports, coroutines,
empty-blocks, exceptions, naming, performance, potential-bugs,
processors, style].
The `build.maxIssues = 0` setting we had moved to the Gradle plugin DSL
(`failOnSeverity` option, defaults to Error). Emptied the YAML; rely on
detekt defaults via `buildUponDefaultConfig = true` until we have
specific rule overrides to write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
1.23.8 still failed on JDK 25 with "25.0.3" — same opaque shape as the
original Gradle 8.10 failure. The 1.23.x line bundles kotlin-compiler-
embeddable 1.9.10 which doesn't actually run on JDK 25 despite the
release note claim.
2.0.0-alpha.3 is explicitly built against Kotlin 2.3.21 + Gradle 9.3.1
+ tested with JDK 25 (per release notes). Alpha is acceptable risk
given there's no stable 2.x and we're already on bleeding-edge AGP 9
+ Kotlin 2.3 elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
detekt 1.23.7 choked on JDK 25 with an opaque "25.0.3" error (same
shape as the original Gradle 8.10 failure). Per the detekt 1.23.8
release notes (Feb 2025), it's the first 1.23.x version tested with
JDK 25. Still built against Kotlin 2.0.21 — fine for our small Phase 1
sources, no exotic Kotlin 2.3 syntax in use yet.
The `tasks.withType<Detekt> { jvmTarget = "17" }` pin from the
previous commit stays as belt-and-braces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
`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>
Hilt 2.52 referenced AGP's old BaseExtension which AGP 9 removed,
causing ktlintCheck to fail at plugin-application time:
Failed to apply plugin 'com.google.dagger.hilt.android'.
> Android BaseExtension not found.
Dagger/Hilt 2.59+ adds AGP 9 support (and mandates it for the Gradle
plugin path). 2.59.2 is the current latest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
M8 phase 1.1: empty multi-project Gradle scaffold (root + :app
placeholder). Version catalog establishes pinned Kotlin/AGP/Compose/
Hilt/Room/Media3/etc. versions for the whole module.
Gradle wrapper (8.10) reused from flutter_client/ — the wrapper jar
is a bootstrap and respects distributionUrl from gradle-wrapper.properties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>