Changelog
Changes in v13.14.5..HEAD:
Highlights
- Liked Songs Reaches the Car — the account's Liked Songs is not a playlist, but it is the folder a Spotify listener reaches for first, and the car's Spotify group did not have it. It now sits at the top of that group inside Home → Mixes and radios, the way the phone's own library orders it, and it is addressed as a playlist (
spotify_playlist/_liked) rather than as a fourth kind of entry — so play, shuffle, browse and the leaf path for one of its tracks are the paths that already existed, with no special case anywhere. The leading underscore is why the id can never collide with a real base-62 playlist id, and a test pins that. The folder is dropped entirely when the account has nothing saved in it, because an entry that opens onto nothing is the same broken promise as a switch that does nothing. (Hush) - A Gateway Blip No Longer Hides the Folder for Five Minutes — the count behind that entry is cached, because a car re-asks for the folder every time the user scrolls back to it and one gateway round trip per flick of the dial is not a design. But the cache could not tell "this account has no Liked Songs" (0) from "this account could not be asked" (
null), so one failed probe was cached for the wholeLIKED_PROBE_TTL_MSand the folder vanished for five minutes — indistinguishable from the feature not existing. An answer the account actually gave is now good for five minutes; a probe that failed is retried after 20 seconds. (Hush) - The Equalizer Crash Is the Serializer That Was Never Generated — the reported release stack was
Serializer for class 'ig9' is not found.atAxionEqViewModel$applyToService$1.invokeSuspend.SavedEQProfile,ParametricEQ,ParametricEQBandandFilterTypeare persisted through the reifiedJson.encodeToString/decodeFromString, which resolve the serializer by reflection at runtime — and without@Serializablethere is no generated serializer to find, so the lookup throws on the main thread the first time a profile is saved or read. All four are annotated now, andEQProfileJsonTestassertsserializer<T>()reflectively for each of them plus a full round trip and a decode of JSON written before the fix, so a profile saved by an older build still loads. (Hush) - A Whole-Bug-Class Sweep, Not One Annotation — the same shape — a reified
encodeToString/decodeFromStringwhose target has no@Serializable— was searched for acrossapp/src/mainandcore/src/main. Every reified call target in the app is annotated:SavedAccount,NewsItem,RecognitionHistoryItem,ThemeExportV1,RemoteCipherConfig,ExtensionRegistry,DownloadedFileIndex,DownloadOriginIndex,SpotiFLACCacheIndex,SpotiFLACMissIndex,SpotiFLACExtensionCheckIndex,SpotiFLACSessionVerdictIndex,CanvasCacheEntry,GeneratedAppIcon.corehas no reified call site at all. This is a class of bug no compiler and no lint check can see, which is why the guard is a test. (Hush) - The Equalizer Stopped Reading the Disk in the Frame That Opened the Menu —
EQProfileRepositorylists and reads everyeq_profiles/*.jsonin its constructor, and the player menu builds it during composition (PlayerMenu), so opening the menu read a directory and parsed its files on the main thread, in that frame;saveProfile,deleteProfileandsetActiveProfilethen wrote files fromDispatchers.Mainas well. Every file operation now happens on a privateDispatchers.IOscope, mutations wait on aloadedbarrier so a profile saved while the initial read is in flight cannot be dropped by the read publishing its older list, and all three mutators arewithContext(Dispatchers.IO). (Hush) - Waze's Bridge Is Told That Hush Is Here — the reported case is Waze opened first: the Bridge comes up on its own, finds no player, and keeps showing a dead panel — and opening Hush afterwards changed nothing, because the only things that ever told a Bridge to re-attach were Waze's own bind and a Reconnect button buried in Hush's settings, which the user has no way to reach while driving. Both directions of that handshake now close: Hush's process start and its playback service coming up broadcast the same reconnect request the button sends, to the same packages, under the same permission (
WazeBridgeAutoReconnect). It is idempotent on the Bridge side (re-bind, re-start the service, ask for a snapshot), fired once per service creation rather than per track, and gated onWAZE_SUPPORTED. The shim detection inMusicServicewas refactored to call the sameinstalledBridgePackages, so "what is a Bridge" has one definition instead of two that would eventually disagree. (Hush)
Android Auto
- The Liked Songs folder is a playable, browsable entry — built with
queueMediaItemso a head unit both expands it and plays it, titled from the account's own count (n_song), and returned byonGetItemfor the folder id alone: a tap on one of its tracks arrives with an action and has to fall through to the leaf branches, which is the one case a folder-shaped item can get wrong. (Hush) - Its tracks come from the Liked Songs endpoint, paged like a playlist's —
SpotifyLibraryRepository.likedSongs(limit)walkslikedSongsin 50-track pages and stops as soon as the caller has enough, skipping local files, exactly asplaylistTracksdoes;likedSongsCount()costs one page of one item, since the only thing a browsable folder needs to know is whether it would be empty. (Hush) _likedparses as a playlist with no action, and no real playlist can be mistaken for it — pinned inAndroidAutoPlaylistsTest: parse with no action, the shuffle id, a leafspotify_playlist/_liked/track123yieldingselectedTrackId == track123, and that no base-62 id can satisfyisLiked. (Hush)- The phone's own Liked Songs is a different entry — the root
LIKEDitem is Hush's local likes; the Spotify one is the account's, and it is the one under Mixes and radios. Two likes, two folders, no ambiguity about which is which. (Hush)
SpotiFLAC
- A run says which source it is on — a verification run works through several sources in sequence and can take a while over each, so "something is happening" is not an answer a user can act on.
SpotiFLAutoVerifiernow publishessteps(aStateFlowofStepwithSOLVING/WAITING/VERIFIED/NEEDS_CHECK), andSpotiFLACVerificationChecklistturns that into the words and colours — a pure object, because the wording is a decision and a decision buried in Compose can only be checked by looking at a phone. It is also the one place two vocabularies have to agree: the line under the buttons counts a run, the rows name its members, and a reader who sees "1 needs a check" above a row saying "Verified" believes neither. (Hush) - Renew and Verify are two buttons again, because they are two jobs — they were merged into one "Verify & renew all" that renewed first and then quietly raised checks for whatever the gateway had turned down, which read as a contradiction: the line above counted how many sources already need a check, and the same button offered to renew them. The gateway rotates a live session; a session it has thrown away can only be replaced by a check, and no amount of renewing produces one. Each button is enabled only when it can do something. (Hush)
- One source's check can be asked for on its own —
verifySourcejoins the same queue with the same permission to open the browser, because asking for a check is the consent a background prewarm does not have — so a car head unit's challenge opens for the user here exactly as it does from the batch. It names itself in the log (settings-verify-one) so a report can tell a deliberate single check from a batch run. (Hush) - A run that cannot open a browser says so —
SpotiFLACChallengeRoute.openInBrowserreturnedUnitwhilestartActivitythrowsActivityNotFoundExceptionon a device with no browser at all, so a head unit with no browser was told the check had been opened when nothing had. It returnsBooleannow (runCatching/recoverCatching), and both callers report it:SpotiFLACBrowserVerificationand the overlay'sbrowserLaunchFailedstate. (Hush) - A session the gateway threw away stops reading "Verified" —
SpotiFLACSessionRenewernow distinguishes a renewal that was answered from one that needs the user, and a gateway-rejected session is dropped rather than kept as a live-looking record (shouldForgetSessionAfterRenewal(result) = result.contactedGateway && result.needsVerification), so the row matches the count the Verify button acts on. (Hush) - Two threads updating the same step list no longer lose one —
SpotiFLAutoVerifier.recordStep/forgetStepdid read-modify-write on_steps.valuefrom the enqueuing caller and from the verifier thread, which is a lost update whenever both touch it (the enqueuing side would erase a step the verifier had just finished). Both useMutableStateFlow.updatenow. (Hush)
Settings and probes
- One control per row — the extension source rows showed "Test", a column of ▲/▼, and an "Update" under the description: five tap targets wide, where a tap meant for one of them landed on another. The row's actions are one overflow (
MoreVert), applied to the source rows, the priority rows and the card's session rows, and the menu is anchored to the row's own bounds (Box) rather than the screen — which is why a menu used to open on the right of the screen instead of under the button that opened it. (Hush) - A build task rejects the shape coming back — a loose control in those rows is a one-word edit no compiler or lint check can see, so
verifyRowControlsreads the settings rows and fails the build when the batch "Verify" or a per-source "Test" reappears beside the row instead of inside its menu. It is wired as anassemble*dependency, because CI runs no unit tests. (Hush) - A probe that reads menus, not just the activity — a Compose
DropdownMenuis not in the activity's view tree: it is a window of its own, so an in-process semantics probe that walked only the decor view reported an open menu as an empty screen — the one question a menu raises, what is in it, unanswered.SemanticsProbeReceivernow scans every window the process owns (WindowManagerGlobal.mViews, the same set the accessibility bridge publishes from) and reports each one's class, size, focus, node count and nodes, with--ei popup-limit Nand--ei sample-limit Nto size the dump.SemanticsProbeWindowSectionTestguards the section, including the two ways a run can look like an answer when it is not: a scan that could not look, and a window that says nothing — neither may print like "no menu is open". (Hush) scripts/semantics-probe.shfollows —--popup-limit N, and the bash 3.2 array bug that broke the script underset -uwhen no extra arguments were given. (Hush)
Housekeeping
- Version bumped to 13.14.6 (versionCode 176). The Waze shims derive their version from the app's, so they move with it. (Hush)
- Unit tests pass (827 tests, both variants, 0 failures — 84 classes), including the new suites for the EQ profile JSON contract (
serializer<T>()for all four types, round trip, legacy decode), the Android Auto playlist codec, the verification-step checklist, the session renewal decision and the wall-clock freshness rule, and the probe's window section. (Hush) - Lint
fossMobileUniversalDebug: 0 errors (943 warnings, 49 hints), withNewApifatal andabortOnErrortrue inappandwaze-shim;verifyRowControlsgreen. (Hush) - Note for testers: no device was attached for this release, so the car tree was checked by reading the code and by the unit tests rather than on a head unit.
scripts/android-auto-browse-smoke.shconnects the app's ownMediaBrowserto its ownMusicServiceand dumps the same ids a head unit sees, and is the first thing to run once a phone is available — with a particular eye on Liked Songs appearing at the top of the Spotify group and opening onto its tracks. (Hush) - The EQ crash was reported from a release build, so the stack's frames carry R8 names (
ig9,AxionEqViewModel$applyToService$1); the frame that was not obfuscated is the one that named the class that had no serializer, and it is the class fixed here. (Hush)
Upstream credits
Hush is built on ArchiveTune and combines features, fixes, and UI from several open-source YouTube Music clients—including Metrolist, Vivi Music, and Echo Music. Those projects are credited below; their licenses and copyright notices are preserved in source.
Thank you to the maintainers and contributors of every project listed above.