diff --git a/extensions/spotify/build.gradle.kts b/extensions/spotify/build.gradle.kts new file mode 100644 index 000000000..07cce9e23 --- /dev/null +++ b/extensions/spotify/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + compileOnly(project(":extensions:spotify:stub")) +} diff --git a/extensions/spotify/src/main/AndroidManifest.xml b/extensions/spotify/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/spotify/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java new file mode 100644 index 000000000..35be583b7 --- /dev/null +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java @@ -0,0 +1,37 @@ +package app.revanced.extension.spotify.misc; + +import com.spotify.remoteconfig.internal.AccountAttribute; + +import java.util.Map; +import java.util.Objects; + +/** + * @noinspection unused + */ +public final class UnlockPremiumPatch { + private static final Map OVERRIDES = Map.of( + // Disables player and app ads. + "ads", false, + // Works along on-demand, allows playing any song without restriction. + "player-license", "premium", + // Disables shuffle being initially enabled when first playing a playlist. + "shuffle", false, + // Allows playing any song on-demand, without a shuffled order. + "on-demand", true, + // Allows adding songs to queue and removes the smart shuffle mode restriction, + // allowing to pick any of the other modes. + "pick-and-shuffle", false, + // Disables shuffle-mode streaming-rule, which forces songs to be played shuffled + // and breaks the player when other patches are applied. + "streaming-rules", "", + // Enables premium UI in settings and removes the premium button in the nav-bar. + "nft-disabled", "1" + ); + + public static void overrideAttribute(Map attributes) { + for (var entry : OVERRIDES.entrySet()) { + var attribute = Objects.requireNonNull(attributes.get(entry.getKey())); + attribute.value_ = entry.getValue(); + } + } +} diff --git a/extensions/spotify/stub/build.gradle.kts b/extensions/spotify/stub/build.gradle.kts new file mode 100644 index 000000000..a8da923ed --- /dev/null +++ b/extensions/spotify/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id(libs.plugins.android.library.get().pluginId) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} diff --git a/extensions/spotify/stub/src/main/AndroidManifest.xml b/extensions/spotify/stub/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/spotify/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/spotify/stub/src/main/java/com/spotify/remoteconfig/internal/AccountAttribute.java b/extensions/spotify/stub/src/main/java/com/spotify/remoteconfig/internal/AccountAttribute.java new file mode 100644 index 000000000..5b6d0fa18 --- /dev/null +++ b/extensions/spotify/stub/src/main/java/com/spotify/remoteconfig/internal/AccountAttribute.java @@ -0,0 +1,5 @@ +package com.spotify.remoteconfig.internal; + +public final class AccountAttribute { + public Object value_; +} diff --git a/patches/api/patches.api b/patches/api/patches.api index 92106329f..f84f9113e 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -816,6 +816,10 @@ public final class app/revanced/patches/spotify/lite/ondemand/OnDemandPatchKt { public static final fun getOnDemandPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/spotify/misc/UnlockPremiumPatchKt { + public static final fun getUnlockPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt { public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt new file mode 100644 index 000000000..1fe64b8cf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.spotify.misc + +import app.revanced.patcher.fingerprint + +internal val accountAttributeFingerprint = fingerprint { + custom { _, c -> c.endsWith("internal/AccountAttribute;") } +} + +internal val productStateProtoFingerprint = fingerprint { + returns("Ljava/util/Map;") + custom { _, classDef -> + classDef.endsWith("ProductStateProto;") + } +} + +internal val buildQueryParametersFingerprint = fingerprint { + strings("trackRows", "device_type:tablet") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt new file mode 100644 index 000000000..dff32638e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.spotify.misc + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.AccessFlags + +@Suppress("unused") +val unlockPremiumPatch = bytecodePatch( + name = "Unlock Spotify Premium", + description = "Unlock Spotify Premium features. Server-sided features like downloading songs are still locked.", +) { + compatibleWith("com.spotify.music") + + extendWith("extensions/spotify.rve") + + execute { + // Make _value accessible so that it can be overridden in the extension. + accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply { + accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv()) + } + + // Override the attributes map in the getter method. + val attributesMapRegister = 0 + val instantiateUnmodifiableMapIndex = 1 + productStateProtoFingerprint.method.addInstruction( + instantiateUnmodifiableMapIndex, + "invoke-static { v$attributesMapRegister }," + + "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;->overrideAttribute(Ljava/util/Map;)V", + ) + + // Add the query parameter trackRows to show popular tracks in the artist page. + val addQueryParameterIndex = buildQueryParametersFingerprint.stringMatches!!.first().index - 1 + buildQueryParametersFingerprint.method.replaceInstruction(addQueryParameterIndex, "nop") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt deleted file mode 100644 index 761e32206..000000000 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.patches.spotify.navbar - -import app.revanced.patcher.fingerprint -import app.revanced.util.literal -import com.android.tools.smali.dexlib2.AccessFlags - -internal val addNavBarItemFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returns("V") - literal { showBottomNavigationItemsTextId } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt index c191c676b..ddc92bcc4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt @@ -1,50 +1,12 @@ package app.revanced.patches.spotify.navbar -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.patch.resourcePatch -import app.revanced.patches.shared.misc.mapping.get -import app.revanced.patches.shared.misc.mapping.resourceMappingPatch -import app.revanced.patches.shared.misc.mapping.resourceMappings - -internal var showBottomNavigationItemsTextId = -1L - private set -internal var premiumTabId = -1L - private set - -private val premiumNavbarTabResourcePatch = resourcePatch { - dependsOn(resourceMappingPatch) - - execute { - premiumTabId = resourceMappings["id", "premium_tab"] - - showBottomNavigationItemsTextId = resourceMappings[ - "bool", - "show_bottom_navigation_items_text", - ] - } -} +import app.revanced.patches.spotify.misc.unlockPremiumPatch +@Deprecated("Superseded by unlockPremiumPatch", ReplaceWith("unlockPremiumPatch")) @Suppress("unused") val premiumNavbarTabPatch = bytecodePatch( - name = "Premium navbar tab", description = "Hides the premium tab from the navigation bar.", ) { - dependsOn(premiumNavbarTabResourcePatch) - - compatibleWith("com.spotify.music") - - // If the navigation bar item is the premium tab, do not add it. - execute { - addNavBarItemFingerprint.method.addInstructions( - 0, - """ - const v1, $premiumTabId - if-ne p5, v1, :continue - return-void - :continue - nop - """, - ) - } + dependsOn(unlockPremiumPatch) }