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 index 38283542f..49dfffaec 100644 --- 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 @@ -94,7 +94,7 @@ public final class UnlockPremiumPatch { ); /** - * Override attributes injection point. + * Injection point. Override account attributes. */ public static void overrideAttribute(Map attributes) { try { @@ -119,7 +119,14 @@ public final class UnlockPremiumPatch { } /** - * Remove ads sections from home injection point. + * Injection point. Remove station data from Google assistant URI. + */ + public static String removeStationString(String spotifyUriOrUrl) { + return spotifyUriOrUrl.replace("spotify:station:", "spotify:"); + } + + /** + * Injection point. Remove ads sections from home. */ public static void removeHomeSections(List
sections) { try { 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 index 29f472a5d..c6fe5fcec 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt @@ -35,6 +35,27 @@ internal val contextMenuExperimentsFingerprint = fingerprint { strings("remove_ads_upsell_enabled") } +internal val contextFromJsonFingerprint = fingerprint { + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC + ) + custom { methodDef, classDef -> + methodDef.name == "fromJson" && + classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;") + } +} + +internal val readPlayerOptionOverridesFingerprint = fingerprint { + custom { methodDef, classDef -> + methodDef.name == "readPlayerOptionOverrides" && + classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;") + } +} + internal val homeSectionFingerprint = fingerprint { custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") } } 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 index 183bbb908..bb4e62a11 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt @@ -1,6 +1,7 @@ package app.revanced.patches.spotify.misc import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction @@ -8,9 +9,12 @@ import app.revanced.patcher.fingerprint import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch -import app.revanced.util.* +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference @@ -47,6 +51,7 @@ val unlockPremiumPatch = bytecodePatch( ) } + // Add the query parameter trackRows to show popular tracks in the artist page. with(buildQueryParametersFingerprint) { val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow( @@ -55,12 +60,50 @@ val unlockPremiumPatch = bytecodePatch( method.replaceInstruction(addQueryParameterConditionIndex, "nop") } + if (IS_SPOTIFY_LEGACY_APP_TARGET) { - return@execute Logger.getLogger(this::class.java.name).info( + return@execute Logger.getLogger(this::class.java.name).warning( "Patching a legacy Spotify version. Patch functionality may be limited." ) } + + // Enable choosing a specific song/artist via Google Assistant. + contextFromJsonFingerprint.method.apply { + val insertIndex = contextFromJsonFingerprint.patternMatch!!.startIndex + // Both the URI and URL need to be modified. + val registerUrl = getInstruction(insertIndex).registerC + val registerUri = getInstruction(insertIndex + 2).registerD + + val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" + + "removeStationString(Ljava/lang/String;)Ljava/lang/String;" + + addInstructions( + insertIndex, + """ + invoke-static { v$registerUrl }, $extensionMethodDescriptor + move-result-object v$registerUrl + invoke-static { v$registerUri }, $extensionMethodDescriptor + move-result-object v$registerUri + """ + ) + } + + + // Disable forced shuffle when asking for an album/playlist via Google Assistant. + readPlayerOptionOverridesFingerprint.method.apply { + val shufflingContextCallIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "shufflingContext" + } + + val registerBool = getInstruction(shufflingContextCallIndex).registerD + addInstruction( + shufflingContextCallIndex, + "sget-object v$registerBool, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;" + ) + } + + // Disable the "Spotify Premium" upsell experiment in context menus. with(contextMenuExperimentsFingerprint) { val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow( @@ -70,6 +113,7 @@ val unlockPremiumPatch = bytecodePatch( method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0") } + // Make featureTypeCase_ accessible so we can check the home section type in the extension. homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply { accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())