Skip to content

13.14.6

Latest

Choose a tag to compare

@github-actions github-actions released this 24 Sep 08:40

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 whole LIKED_PROBE_TTL_MS and 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. at AxionEqViewModel$applyToService$1.invokeSuspend. SavedEQProfile, ParametricEQ, ParametricEQBand and FilterType are persisted through the reified Json.encodeToString/decodeFromString, which resolve the serializer by reflection at runtime — and without @Serializable there 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, and EQProfileJsonTest asserts serializer<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/decodeFromString whose target has no @Serializable — was searched for across app/src/main and core/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. core has 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 — EQProfileRepository lists and reads every eq_profiles/*.json in 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, deleteProfile and setActiveProfile then wrote files from Dispatchers.Main as well. Every file operation now happens on a private Dispatchers.IO scope, mutations wait on a loaded barrier 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 are withContext(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 on WAZE_SUPPORTED. The shim detection in MusicService was refactored to call the same installedBridgePackages, 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 queueMediaItem so a head unit both expands it and plays it, titled from the account's own count (n_song), and returned by onGetItem for 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) walks likedSongs in 50-track pages and stops as soon as the caller has enough, skipping local files, exactly as playlistTracks does; likedSongsCount() costs one page of one item, since the only thing a browsable folder needs to know is whether it would be empty. (Hush)
  • _liked parses as a playlist with no action, and no real playlist can be mistaken for it — pinned in AndroidAutoPlaylistsTest: parse with no action, the shuffle id, a leaf spotify_playlist/_liked/track123 yielding selectedTrackId == track123, and that no base-62 id can satisfy isLiked. (Hush)
  • The phone's own Liked Songs is a different entry — the root LIKED item 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. SpotiFLAutoVerifier now publishes steps (a StateFlow of Step with SOLVING / WAITING / VERIFIED / NEEDS_CHECK), and SpotiFLACVerificationChecklist turns 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 — verifySource joins 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.openInBrowser returned Unit while startActivity throws ActivityNotFoundException on 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 returns Boolean now (runCatching/recoverCatching), and both callers report it: SpotiFLACBrowserVerification and the overlay's browserLaunchFailed state. (Hush)
  • A session the gateway threw away stops reading "Verified" — SpotiFLACSessionRenewer now 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/forgetStep did read-modify-write on _steps.value from 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 use MutableStateFlow.update now. (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 verifyRowControls reads 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 an assemble* dependency, because CI runs no unit tests. (Hush)
  • A probe that reads menus, not just the activity — a Compose DropdownMenu is 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. SemanticsProbeReceiver now 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 N and --ei sample-limit N to size the dump. SemanticsProbeWindowSectionTest guards 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.sh follows — --popup-limit N, and the bash 3.2 array bug that broke the script under set -u when 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), with NewApi fatal and abortOnError true in app and waze-shim; verifyRowControls green. (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.sh connects the app's own MediaBrowser to its own MusicService and 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.

Project Repository
ArchiveTune ArchiveTuneApp/ArchiveTune
Metrolist metrolistgroup/metrolist
Vivi Music vivizzz007/vivi-music
Echo Music EchoMusicApp/Echo-Music
SpotiFLAC spotiflacapp/SpotiFLAC-Mobile
Zemer Cipher ZemerTeam/zemer-cipher

Thank you to the maintainers and contributors of every project listed above.