Case study · 2026

HakaiMusic — a music client that keeps playing when the stream URL does not resolve

A native Android music client for YouTube audio: search, queue, offline downloads, personalised rows, and optional cloud sync. Kotlin, Jetpack Compose, Media3, and Room.

Signing out ends the account session only. The downloads, playlists and listening history stay on the device.

Most of the music people actually listen to already lives on YouTube. What surrounds it is the problem: ads between tracks, a player that stops the moment the screen locks, and an account you have to hold just to keep a playlist. HakaiMusic is a native Android client for that same audio, with no ads, no analytics, and no account required to use any of it.

Two engines behind one media session

ExoPlayer is tried first for every item, playing a stream URL resolved lazily through an internal scheme. When the source reports that no usable URL exists, playback swaps to a second engine that drives the real YouTube player inside a headless WebView, implemented as a Media3 player so the media session cannot tell the two apart. Lock-screen controls, the notification, and Bluetooth buttons keep working across the swap.

The swap is not a precaution. On current YouTube the web player response is cipher-only, measured on a real device, so a YouTube track is served by the WebView engine while ExoPlayer handles downloads and local files. One session, two very different paths underneath it.

The device is the source of truth

Library, queue, history and downloads live in Room and never require a network round trip to read. Signing in is optional and adds sync; signing out ends the session and nothing else. After a sign-out on the test device, five downloads, one playlist, fifty-nine listening events and thirty-four history rows were all still there, along with the audio files on disk.

Sync that assumes the connection will drop

Every syncable row carries a timestamp that is only written after the Firestore batch actually commits, so a row that never reached the server is visibly unsynced rather than silently lost. With Wi-Fi and mobile data off, a change was recorded and sat in the queue while the app stayed fully usable; when the network came back, the sync ran and the row cleared. Document ids are stable client-generated ids, so repeating a sync produces no duplicates.

The pull path was proven the blunt way: the playlist was deleted straight out of the local database, the app restarted, and it came back from the server with the same id, title and membership.

Recommendations you are allowed to refuse

The home rows are built from what has actually been played, and every recommended track carries a "not interested" action that removes it from the row and stays gone after a restart, with an undo. The action appears only on recommendation rows, never in your own library or search results, and rejecting a track does not interrupt what is currently playing.

What is not verified yet

Sync has never run against a genuine second handset. The restore was proven by deleting local rows and letting the server refill them, which is the same code path a second device takes, but that is an argument, not an observation. The telephony branch of audio focus is also untested on hardware, because a real incoming call cannot be scripted and will not be faked. Both are written down in the project docs the same way, with the inferences kept separate from the measurements.