vantage-patches
September 10, 2026 ยท View on GitHub
Vantage-only morphe patches, published as a
single .mpp bundle that pmaxhogan/vantage
stacks on top of whichever upstream bundle a variant already uses.
morphe-cli patch accepts repeated --patches, so this adds patches without
forking anddea, morphe or piko.
One bundle serves every app. Each patch declares its own Compatibility, and a
patch only applies to the app it names, but morphe-cli still offers every patch
in the bundle to every build - so each consuming variant's options file must
explicitly disable the patches meant for other apps, or they apply by default.
Everything here is deliberately small. Anything that belongs upstream should go upstream instead; this repo is for patches that only Vantage wants.
Not to be confused with pmaxhogan/revanced-patches: that one is a fork of anddea's full bundle that only changes a few setting defaults (Shorts off, Playables and AI sections hidden, and so on) and syncs upstream nightly. Changing what an existing upstream patch does by default goes there; a new patch that upstream would never take goes here. Vantage builds with both bundles at once.
Patches
Keep playback on activity destroy (YouTube Music)
Keeps background playback and its media foreground service alive when the system
destroys MusicActivity to reclaim memory. A user-initiated close still stops
playback.
Why the activity dies without the app finishing it: ActivityThread installs a
BinderInternal GC watcher at attach time. After a GC, if activities have
changed and the app's own ART heap sits above 3/4 of Runtime.maxMemory(), it
calls ActivityTaskManager.getService().releaseSomeActivities(mAppThread). The
system then runs WindowProcessController.releaseSomeActivities("low-mem"),
which calls ActivityRecord.destroyImmediately("low-mem") on every activity of
that process that is non-visible, stopped and holds saved state. It is the only
path that destroys a stopped, non-visible activity without finishing it, so
Activity.isFinishing() is false there and true for every user-initiated close.
It is not the only destroy-without-finish in the framework, though: a
configuration-change relaunch also reports isFinishing() false, so the gate
checks isChangingConfigurations() too and suppresses nothing on a rotation.
YouTube Music reacts to that destroy by deactivating its media session, running
MedialibPlayer.stopVideo with STOPPAGE_DIRECTOR_RESET_INTERNALLY, dropping
the foreground service and going cached, even though the player itself lives in
an application-scoped component and had been playing happily with the activity
merely stopped.
The patch opens a short suppression window in MusicActivity.onDestroy when
isFinishing() is false, and swallows exactly two calls while it is open: the
player's stopVideo, and the media session's setActive(false). It also closes
the window in BackgroundPlayerService.onTaskRemoved, so a swipe from recents
landing seconds after a system destroy still stops playback. Everything else
about the destroy proceeds untouched, so the activity is still released and its
memory still reclaimed.
Clone with badge (Claude)
Turns the Claude app into an installable copy with its own package name so
several accounts can be signed in at once. Options: packageName, appLabel,
badgeNumber (1-9) and iconColor (#RRGGBB). The resource half renames the
package, every content-provider authority and custom permission, sets the label,
and swaps the adaptive icon for one on the chosen color with the number stamped
on it. The bytecode half merges extensions/claude and starts a small pill
overlay from Application.onCreate that pins the same number to the top corner
of every screen, so the copies can be told apart while in use.
badgeNumber is an integer on purpose: morphe-cli parses a bare digit passed
with -O as a number and silently ignores it for a string option.
Layout
patches/src/main/kotlin/app/vantage/patches/<app>/... patch declarations (Kotlin)
extensions/<leaf>/ code injected into the app (Java)
One extensions/<leaf> module per app that needs injected code. A module gets
its namespace from defaultNamespace plus its directory name, so
extensions/music becomes app.vantage.extension.music; do not set namespace
in the module itself. Two things every extension module needs and neither is
obvious: no plugins block (the settings plugin already applies AGP, and
declaring com.android.library fails because it applies
com.android.application), and a src/main/AndroidManifest.xml containing just
<manifest/> (AGP fails on the missing input otherwise).
Patches pull their extension in with extendWith("extensions/<leaf>.mpe").
Deliberately not sharedExtensionPatch, which also demands an
extensions/shared.mpe and hooks Application.onCreate - a hook this bundle
does not need and which is untested alongside the upstream bundle's own.
The extension namespace must not be app.morphe.extension.*: this bundle is
merged into the same APK as the anddea bundle, which already ships classes under
that prefix.
Build
export GITHUB_ACTOR=<your github user>
export GITHUB_TOKEN=<a token with read:packages> # the morphe maven repo
./gradlew :patches:buildAndroid
The bundle lands at patches/build/libs/patches-<version>.mpp.
.github/workflows/build-mpp.yml builds it on every push to main and
publishes it as a release asset. The repository variable MPP_PRERELEASE
("true"/"false") gates whether the release is a prerelease; vantage reads
/releases/latest, which skips prereleases.
Consuming it from vantage
config/build.env:
VANTAGE_PATCHES_REPO=pmaxhogan/vantage-patches
VANTAGE_PATCHES_CHANNEL=release
scripts/build.sh resolves the latest release the same way it resolves piko,
then passes a second --patches to the Music variant only.