Skip to content

feat(android): complete the app — parity fixes, library pagination and local release signing - #10

Merged
Nxssie merged 17 commits into
mainfrom
feat/android-app
Sep 11, 2026
Merged

Nxssie merged 17 commits into
mainfrom
feat/android-app

Conversation

@Nxssie

@Nxssie Nxssie commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Everything since #9, in atomic commits:

Playback

  • Level normalisation to a common target (measured per track, cached, applied on decoded PCM), replacing the desktop AGC.
  • Shuffling a queue now keeps the tapped track absolute instead of landing on a random one.
  • Playback survives leaving the app: the engine and the foreground service no longer get released on isFinishing, and the service stops with its notification instead of outliving it.

Navigation

  • The media widget opens Now Playing (session card and notification body), over the running instance, and past the login overlay.
  • Tab, platform, back stack and theme survive a recreate/process death; the theme is shared with the desktop.
  • Tapping the player bar on Now Playing opens the queue.

Library completeness

  • SoundCloud: liked tracks, own and saved playlists follow next_href (they stopped at the first page, e.g. 50 of 1.7k likes).
  • YouTube: liked songs, own playlists and subscriptions follow nextPageToken; the videos.list enrichment is chunked and bounded-concurrent.

Release tooling (local, no CI)

  • Release signing from environment variables, git-derived versionCode/versionName, APK named wren-<version>.apk, mise tasks, and a manual mise run android:release that refuses dirty/untagged/unpushed states and verifies the signature before uploading.

- Shuffling replaced the whole list before the index was applied, so the
  tapped track pointed at whatever landed on that position; only the rest
  of the queue is shuffled now.
- Masters from SoundCloud and YouTube Music sit 6-8 dB apart, so tracks from
  different sources sounded like different apps; both players now share one
  target (-14 LUFS) and one estimator.
- Gain is fixed for the whole track and looked up from a per-track cache, so
  nothing pumps mid-song and a quiet passage is never mistaken for a quiet
  master. It is applied on decoded PCM (media3 AudioProcessor on Android, the
  existing sample loop on desktop) and measured pre-gain, so replays do not
  compound their own correction.
- Replaces the desktop AGC, whose smoothed RMS gain could not tell either case
  apart and drifted for seconds at the start of every track.
- A track plays at its native level the first time and is normalised from the
  next play on, which is what keeping the gain static costs.
- Both the session card (lock screen, quick settings) and the notification body
  opened MainActivity bare, so the app always landed on Search; the content
  intent now carries an action that the app turns into a Now Playing request.
- singleTop keeps the tap inside the running instance, and the request is
  pushed onto the tab history when the app is already open (back returns to
  where the user was) or becomes the entry tab on a cold start.
- Releasing the engine from onDestroy was meant as teardown, but isFinishing
  is true for a plain back press, so leaving the app released the player and
  stopped the foreground service it had just started.
- The overlays are drawn above the tabs, so switching the tab behind one left
  the user on the SoundCloud WebView with the request silently consumed.
- Drops the back handler over that overlay: SoundCloudLoginScreen's own is
  composed later, so it always won, and it cancels to the same place.
- Tab, platform and the back stack were plain `remember`, so a system-initiated
  recreate (dark mode, locale, font scale) or a restore after process death
  dropped the user on Search with the queue still playing.
- The theme now lives in a config file: it is a user preference, and it was
  resetting on every cold start.
…aying

- Tapping the bar there switched to the tab the user was already on, so the tap
  went nowhere; the queue sheet is the only thing left to reveal.
- The store was Android-only while the desktop kept the state in memory, so
  the same choice had two implementations and only one of them survived a
  restart. Both apps already share AppDirs.
- Nothing stopped the service after the release-on-finish was removed, so a
  paused notification could be swiped away leaving a foreground service with
  no notification to control: unstoppable for the user, and against the
  platform's foreground-service rules. Dismissal now stops the service, and a
  sticky restart with no player never enters the foreground.
- Liked tracks, own playlists and saved playlists all made a single call
  (`limit=50` by default) and dropped `next_href`, so a library came back
  truncated at the first page — a 1.7k-likes account saw 50.
- One cursor walk now serves all four listings, and it keeps what it has when
  a page fails instead of replacing a partial list with an empty one.
- Every library listing (liked songs, own playlists, subscriptions) took the
  first Data API page and ignored `nextPageToken`, so all three capped at 50
  items regardless of how much was actually there.
- The `videos.list` enrichment that adds durations is chunked by the API's
  50-id limit and runs a few chunks at a time, since walking a large liked
  list one request at a time makes the library screen crawl.
- Channel lookups for saved playlists are chunked too: past 50 of them the
  single request was rejected and every owner silently went missing.
- The release variant had no signing config, so the only installable APK was
  the debug one. Credentials now come from WREN_KEYSTORE* variables, which
  keeps them out of the repo and lets another machine build unsigned instead
  of failing.
- versionCode was a literal 1, so an installed build could never be replaced
  by a later one without uninstalling; it now counts commits, and versionName
  is the `git describe` string, so a device build names its own revision.
- Both are overridable from the environment for a tagged release, and a
  missing git checkout falls back instead of failing the build.
- `mise run android:apk` wraps the signing variables and the release task, and
  a project tool pin keeps Gradle on the Java 21 the toolchain asks for.
- AGP's `app-release.apk` says nothing about which build it is, so a file on a
  phone could not be traced back to a revision; it is now `wren-<versionName>.apk`
  from the same string the installed app reports.
- An unsigned build keeps AGP's `-unsigned` marker, decided from the signing
  variables rather than the variant (which reports a signing config either way):
  an APK that cannot be installed should not look like one that can.
- Attaching the APK to the tag's GitHub release had to be done by hand or not
  at all; `mise run android:release` does it without wiring anything into CI,
  so which builds become public stays a decision rather than a push side effect.
- Refuses a dirty tree or an untagged commit (the APK is named from git, so
  there would be nothing to trace it to) and verifies the CN=wren signature
  before uploading, since a release page is not the place to find that out.
- gh would create the missing tag at origin's HEAD, publishing an APK under a
  revision it was not built from; the remote tag must already point at this
  commit, so a release never becomes the thing that creates its own tag.
@Nxssie
Nxssie merged commit 529c065 into main Sep 11, 2026
5 checks passed
@Nxssie
Nxssie deleted the feat/android-app branch September 11, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant