53d4a73309fe1664cf5e061be48d2dfbb9f7ee43
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a89e81983e |
Phase 5: Offline downloads at selectable bitrates
Adds Room + WorkManager-backed offline downloads: a bitrate picker (Low/Normal/High/Original) on track and album detail screens, a Downloads library screen, and Settings additions for Wi-Fi-only downloads and default quality. Bitrate tiers route through Subsonic's `stream` endpoint with `maxBitRate`; Original uses `download`, which always returns the untranscoded source file. Three real bugs found and fixed during on-device verification: - WorkManager was constructing DownloadWorker with its default reflection-based WorkerFactory instead of HiltWorkerFactory (NoSuchMethodException on the @AssistedInject constructor). The default androidx.startup auto-init ran before Hilt's field injection was guaranteed to have happened. Fixed by disabling the manifest's auto-init provider and calling WorkManager.initialize() manually in DeepwaveApplication.onCreate(), after super.onCreate(). - Crash on every download: WorkManager's own SystemForegroundService declares no foregroundServiceType in its manifest, but the worker requests dataSync at runtime via ForegroundInfo, which API 29+ requires to be a subset of what's manifest-declared. Fixed by manifest-merging that service with foregroundServiceType="dataSync". - Offline playback was completely broken: ResolvingDataSource only rewrites the DataSpec's URI (to file:// for a downloaded track) but always hands it to the same wrapped upstream DataSource to open. OkHttpDataSource can only open http(s) URLs, so the rewritten file:// URI failed with "Malformed URL" and playback silently fell through to the network. Wrapping the signed OkHttpDataSource.Factory in DefaultDataSource.Factory routes by scheme instead - this bug was latent since Phase 4, since LocalTrackFiles was always empty until now and the local-file path was never actually exercised. - Re-downloading a track at a different quality could produce a different file extension (Content-Type-driven), orphaning the previous file on disk with no cleanup path. DownloadWorker now clears any existing files for the track id before writing the new one. Verified on-device: downloads at all four tiers produce distinctly different, correctly-ordered file sizes (Low < Normal < High < Original); a fully downloaded track keeps playing with Wi-Fi and mobile data both disabled; killing and relaunching the app mid-download lets WorkManager resume the interrupted download to a correct, byte-exact final file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
08b3a9a0df |
Phase 3: Subsonic browsing API + real library UI
Replaces Phase 2's sample-data placeholders with real Navidrome browsing: artists, albums, songs, and search, per the Phase 2+ roadmap's Phase 3. - SubsonicApi grows from just ping to getArtists/getArtist/ getAlbumList2/getAlbum/search3, with DTOs split per-domain (ArtistModels/AlbumModels/SongModels/SearchModels) and mapped to plain domain models (data/model) by the new LibraryRepository - Coil wired in for cover art, sharing the same signed OkHttpClient as Retrofit via a Hilt EntryPoint (SubsonicRequestInterceptor signs cover-art requests identically to every other Subsonic call) - Artwork() falls back to the Phase 2 placeholder icon when no cover art id is known - ui/library replaces the Phase 2 placeholders: HomeScreen now shows real Recently Added/Played and a random "Made For You" row, ArtistsScreen is the real Library tab, ArtistDetailScreen and AlbumDetailScreen are new drill-down screens (typed nav routes, ViewModels resolve their id via SavedStateHandle.toRoute()), and SearchScreen does debounced search3 across artists/albums/songs - TrackRow gained a `circular` option so it can double as an artist row in the Library list, not just a track row First real test coverage in the repo: LibraryRepositoryTest (DTO to domain-model mapping against MockWebServer) and SubsonicRequestInterceptorTest (rewritten URL + signed query params). CredentialsStore/ServerRepository tests are deliberately still out of scope - CredentialsStore's Tink/Android Keystore usage isn't testable in a plain JVM unit test without Robolectric, which felt like a bigger side quest than this phase called for. Coil pinned to 3.3.0 rather than the newer 3.6.x latest: newer Coil requires Kotlin 2.4+, which conflicts with this project's Kotlin 2.2.10; 3.3.0 was the last release built against Kotlin 2.2.x. Verified end-to-end against a real Navidrome server on-device: Home rows, artist list/detail, album detail, and search all load real data and real cover art. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a07868b688 |
Phase 2: Spotify/SoundCloud-style theme, nav shell, and component library
Redirects Deepwave from a vanilla Subsonic client toward the Spotify/ SoundCloud-style product the user wants, per the Phase 2+ roadmap. - Dark-first Material3 theme with a fixed aqua accent (no Material You dynamic color), full type scale, rounded-corner shape system - Typed (@Serializable) navigation routes; new bottom-tab shell (Home/Search/Library) with a settings entry point and mini-player placeholder bar, replacing the single placeholder Home screen - Logout moved out of Home into a dedicated Settings screen - Reusable component library (TrackRow, AlbumCard, ArtistCard, ArtworkPlaceholder, BottomNavBar, MiniPlayerBar) for Phase 3 to wire real data into - Strings externalized to strings.xml - Security fix: exclude the Tink keyset and encrypted credentials DataStore from Android auto-backup (backup_rules.xml / data_extraction_rules.xml), previously unexcluded despite allowBackup=true - Fix: Home screen's content column was missing verticalScroll, making it unscrollable once content exceeds the viewport; also added breathing room between each row's header and its cards - Split Login/Settings into stateless content composables + thin ViewModel-wired wrappers, and added @Preview coverage (including interactive previews for MainScreen and BottomNavBar) across every screen and component so the UI can be reviewed in Android Studio's preview pane without running on a device Added material-icons-extended as a pragmatic deviation from the Phase 2 plan (no new deps) since later phases all need icons outside Compose's small default set (play/pause/skip/download/playlist). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
1971d02e2b |
Initial commit: Deepwave Phase 1 - auth scaffolding
Spotify-like Navidrome client scaffold with Hilt DI, a Subsonic API client (token auth, base-URL rewriting), encrypted single-server credential storage (DataStore + Tink/Keystore), and a login/session flow gating Compose Navigation between Login and a placeholder Home screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |