Commit Graph
4 Commits
Author SHA1 Message Date
christopherandClaude Sonnet 5 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>
2026-09-17 19:02:48 -04:00
christopherandClaude Sonnet 5 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>
2026-09-17 14:28:42 -04:00
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
NetherwarlordandClaude Sonnet 5 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>
2026-09-16 23:38:23 -04:00