53d4a73309fe1664cf5e061be48d2dfbb9f7ee43
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
53d4a73309 |
Phase 6.5: Android Auto (phone-projection)
Adds a real browse tree to DeepwavePlaybackService, replacing the Phase 4 stub callback: root -> Playlists/Artists/Albums/Downloaded -> drill-down -> tracks. Built entirely on Phase 3/5/6's existing repositories (LibraryRepository, PlaylistRepository, DownloadRepository) - no new Subsonic calls. A logged-out root shows a single "log in on your phone first" placeholder instead of the four categories, since Auto shares the same process/session as the phone app rather than having its own login flow. Also wires voice/typed search (onSearch/onGetSearchResult) through the existing search3-backed LibraryRepository.search(), and onAddMediaItems to rebuild a streamable URI when the car hands a browsed item back for playback - a MediaItem's URI doesn't survive the trip across into Android Auto's process, only its metadata does, so this reconstructs it from the media id rather than re-fetching anything. No androidx.car.app dependency: that library targets navigation/POI apps, not media - Auto's media category is driven entirely by MediaLibraryService + media3-session + the automotive_app_desc.xml manifest declaration added here. Verified: full app rebuild, Hilt's DI graph resolves with BrowseTree injected into DeepwavePlaybackService, all unit tests pass, and in-app playback on the phone itself still works correctly through the new session callback (confirmed via dumpsys media_session and logcat, no crashes) - the regression risk of swapping the stub callback for a real one. Every onGetLibraryRoot/onGetChildren/ onAddMediaItems/onSearch/onGetSearchResult override was verified by the Kotlin compiler to correctly match Media3 1.11.1's actual MediaLibrarySession.Callback signatures. Not verified: the actual browse tree over a live Android Auto connection. This device has no car or head unit to connect to, and the Desktop Head Unit tool isn't installed on this machine (Google no longer ships it through the standard SDK Manager - it's a separate download). Recommend testing with the actual DHU tool or a real car/head unit before relying on this in the car. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |