Files
Deepwave/app/build.gradle.kts
T
christopherandClaude Sonnet 5 cf52bb854e Phase 4: Media3 playback engine
Builds the single MediaSession-backed player that in-app playback,
offline downloads (Phase 5), and Android Auto (Phase 6) will all
share, per the Phase 2+ roadmap's Phase 4.

- DeepwavePlaybackService: ExoPlayer + MediaLibrarySession, built as
  a MediaLibraryService from day one (not the plainer
  MediaSessionService) so Android Auto only has to add browse-tree
  content later, never rebuild the service. Browse tree is a stub
  for now (MediaLibrarySession.Callback's defaults deny browsing).
- LocalOrRemoteDataSource: the entire "prefer a downloaded file over
  streaming" mechanism as a ResolvingDataSource.Resolver, backed by
  an in-memory LocalTrackFiles registry that stays empty until
  Phase 5 populates it from Room - reused unmodified by Android Auto
  later.
- ExoPlayer's OkHttpDataSource shares the same signed OkHttpClient as
  Retrofit/Coil, so SubsonicRequestInterceptor signs stream requests
  identically to every other Subsonic call - streamUrl() builds the
  request URL the same way coverArtUrl() does for Coil.
- PlaybackController: app-facing facade over a MediaController,
  exposing Flow<PlaybackUiState> - same facade convention as
  ServerRepository. NowPlayingScreen (new, full-screen: artwork,
  seek bar, play/pause/skip) and MiniPlayerBar (now real, replacing
  Phase 2/3's static placeholder) both consume it.
- Track clicks in AlbumDetailScreen (queues the whole album from the
  clicked index) and SearchScreen (single-song queue) now actually
  play, via PlaybackController injected into their ViewModels.
- Scrobbling: submission=false when a track starts, submission=true
  for the outgoing track on each transition - a simplified heuristic
  rather than a played-percentage threshold, feeding Navidrome's
  play-count data that Phase 3's getAlbumList2(frequent/recent) rows
  read from.
- Runtime POST_NOTIFICATIONS request added to MainActivity for API
  33+ (declared in the manifest but easy to forget the runtime half
  of - without it the service still plays, but its notification
  never shows).

Split MainScreen into a thin PlayerViewModel-resolving wrapper plus a
previewable MainContent, rather than having MainScreen call
hiltViewModel() directly - preserves the interactive Android Studio
preview from last session, which a direct Hilt dependency would have
broken.

media3 pinned to 1.11.1 (built against Kotlin 2.2.0, matching this
project's 2.2.10 - learned from Phase 3's Coil version conflict to
check this before picking a version this time).

Verified extensively on-device against the real Navidrome server:
real tracks play with correct metadata/artwork, pause/resume and
queue auto-advance work, the real Android MediaSession exposes
correct state (checked independently via `dumpsys media_session`,
not just the app's own UI), background playback survives
foregrounding/backgrounding, and no crashes across the whole session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 04:26:55 -04:00

90 lines
3.0 KiB
Kotlin

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
}
android {
namespace = "com.InfernalAquatics.deepwave"
compileSdk {
version = release(37)
}
defaultConfig {
applicationId = "com.InfernalAquatics.deepwave"
minSdk = 26
targetSdk = 37
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
optimization {
enable = false
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
// Dependency injection
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
// Networking (Subsonic/Navidrome API client)
implementation(libs.retrofit)
implementation(libs.retrofit.kotlinx.serialization.converter)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.kotlinx.serialization.json)
// Encrypted credential storage (DataStore + Tink/Keystore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.tink.android)
// Cover art loading (shares the signed OkHttpClient from NetworkModule)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
// Playback (shares the signed OkHttpClient from NetworkModule)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.media3.datasource.okhttp)
testImplementation(libs.junit)
testImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.mockito.core)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
}