diff --git a/extensions/spotify/build.gradle.kts b/extensions/spotify/build.gradle.kts index 4a5ce935d..f4da3ba6f 100644 --- a/extensions/spotify/build.gradle.kts +++ b/extensions/spotify/build.gradle.kts @@ -10,7 +10,7 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } } diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java new file mode 100644 index 000000000..f3003bb31 --- /dev/null +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java @@ -0,0 +1,51 @@ +package app.revanced.extension.spotify.layout.hide.createbutton; + +import java.util.List; + +import app.revanced.extension.shared.Utils; + +@SuppressWarnings("unused") +public final class HideCreateButtonPatch { + + /** + * A list of ids of resources which contain the Create button title. + */ + private static final List CREATE_BUTTON_TITLE_RES_ID_LIST = List.of( + Integer.toString(Utils.getResourceIdentifier("navigationbar_musicappitems_create_title", "string")) + ); + + /** + * The old id of the resource which contained the Create button title. Used in older versions of the app. + */ + private static final int OLD_CREATE_BUTTON_TITLE_RES_ID = + Utils.getResourceIdentifier("bottom_navigation_bar_create_tab_title", "string"); + + /** + * Injection point. This method is called on every navigation bar item to check whether it is the Create button. + * If the navigation bar item is the Create button, it returns null to erase it. + * The method fingerprint used to patch ensures we can safely return null here. + */ + public static Object returnNullIfIsCreateButton(Object navigationBarItem) { + if (navigationBarItem == null) { + return null; + } + + String stringifiedNavigationBarItem = navigationBarItem.toString(); + boolean isCreateButton = CREATE_BUTTON_TITLE_RES_ID_LIST.stream() + .anyMatch(stringifiedNavigationBarItem::contains); + + if (isCreateButton) { + return null; + } + + return navigationBarItem; + } + + /** + * Injection point. Called in older versions of the app. Returns whether the old navigation bar item is the old + * Create button. + */ + public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) { + return oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_TITLE_RES_ID; + } +} 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 f9371db44..f01fee831 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 @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Objects; import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; @SuppressWarnings("unused") public final class UnlockPremiumPatch { @@ -22,15 +23,15 @@ public final class UnlockPremiumPatch { private static final boolean IS_SPOTIFY_LEGACY_APP_TARGET; static { - boolean legacy; + boolean isLegacy; try { Class.forName(SPOTIFY_MAIN_ACTIVITY_LEGACY); - legacy = true; + isLegacy = true; } catch (ClassNotFoundException ex) { - legacy = false; + isLegacy = false; } - IS_SPOTIFY_LEGACY_APP_TARGET = legacy; + IS_SPOTIFY_LEGACY_APP_TARGET = isLegacy; } private static class OverrideAttribute { @@ -61,11 +62,12 @@ public final class UnlockPremiumPatch { } } - private static final List OVERRIDES = List.of( + private static final List PREMIUM_OVERRIDES = List.of( // Disables player and app ads. new OverrideAttribute("ads", FALSE), // Works along on-demand, allows playing any song without restriction. new OverrideAttribute("player-license", "premium"), + new OverrideAttribute("player-license-v2", "premium", !IS_SPOTIFY_LEGACY_APP_TARGET), // Disables shuffle being initially enabled when first playing a playlist. new OverrideAttribute("shuffle", FALSE), // Allows playing any song on-demand, without a shuffled order. @@ -91,18 +93,46 @@ public final class UnlockPremiumPatch { new OverrideAttribute("tablet-free", FALSE, false) ); + /** + * A list of home sections feature types ids which should be removed. These ids match the ones from the protobuf + * response which delivers home sections. + */ private static final List REMOVED_HOME_SECTIONS = List.of( Section.VIDEO_BRAND_AD_FIELD_NUMBER, Section.IMAGE_BRAND_AD_FIELD_NUMBER ); + /** + * A list of lists which contain strings that match whether a context menu item should be filtered out. + * The main approach used is matching context menu items by the id of their text resource. + */ + private static final List> FILTERED_CONTEXT_MENU_ITEMS_BY_STRINGS = List.of( + // "Listen to music ad-free" upsell on playlists. + List.of(getResourceIdentifier("context_menu_remove_ads")), + // "Listen to music ad-free" upsell on albums. + List.of(getResourceIdentifier("playlist_entity_reinventfree_adsfree_context_menu_item")), + // "Start a Jam" context menu item, but only filtered if the user does not have premium and the item is + // being used as a Premium upsell (ad). + List.of( + getResourceIdentifier("group_session_context_menu_start"), + "isPremiumUpsell=true" + ) + ); + + /** + * Utility method for returning resources ids as strings. + */ + private static String getResourceIdentifier(String resourceIdentifierName) { + return Integer.toString(Utils.getResourceIdentifier(resourceIdentifierName, "id")); + } + /** * Injection point. Override account attributes. */ - public static void overrideAttribute(Map attributes) { + public static void overrideAttributes(Map attributes) { try { - for (var override : OVERRIDES) { - var attribute = attributes.get(override.key); + for (OverrideAttribute override : PREMIUM_OVERRIDES) { + Object attribute = attributes.get(override.key); if (attribute == null) { if (override.isExpected) { Logger.printException(() -> "'" + override.key + "' expected but not found"); @@ -117,12 +147,12 @@ public final class UnlockPremiumPatch { } } } catch (Exception ex) { - Logger.printException(() -> "overrideAttribute failure", ex); + Logger.printException(() -> "overrideAttributes failure", ex); } } /** - * Injection point. Remove station data from Google assistant URI. + * Injection point. Remove station data from Google Assistant URI. */ public static String removeStationString(String spotifyUriOrUrl) { return spotifyUriOrUrl.replace("spotify:station:", "spotify:"); @@ -130,7 +160,7 @@ public final class UnlockPremiumPatch { /** * Injection point. Remove ads sections from home. - * Depends on patching protobuffer list remove method. + * Depends on patching abstract protobuf list ensureIsMutable method. */ public static void removeHomeSections(List
sections) { try { @@ -139,4 +169,17 @@ public final class UnlockPremiumPatch { Logger.printException(() -> "Remove home sections failure", ex); } } + + /** + * Injection point. Returns whether the context menu item is a Premium ad. + */ + public static boolean isFilteredContextMenuItem(Object contextMenuItem) { + if (contextMenuItem == null) { + return false; + } + + String stringifiedContextMenuItem = contextMenuItem.toString(); + return FILTERED_CONTEXT_MENU_ITEMS_BY_STRINGS.stream() + .anyMatch(filters -> filters.stream().allMatch(stringifiedContextMenuItem::contains)); + } } diff --git a/extensions/spotify/stub/build.gradle.kts b/extensions/spotify/stub/build.gradle.kts index a8da923ed..61a9e204a 100644 --- a/extensions/spotify/stub/build.gradle.kts +++ b/extensions/spotify/stub/build.gradle.kts @@ -7,11 +7,11 @@ android { compileSdk = 34 defaultConfig { - minSdk = 26 + minSdk = 24 } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } } diff --git a/patches/api/patches.api b/patches/api/patches.api index 83e6c7afe..233b3ad9f 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -873,6 +873,10 @@ public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSync public static final fun getEnableOfflineSync ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatchKt { + public static final fun getHideCreateButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/spotify/layout/theme/CustomThemePatchKt { public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/Fingerprints.kt new file mode 100644 index 000000000..5d555b187 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.spotify.layout.hide.createbutton + +import app.revanced.patcher.fingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val navigationBarItemSetClassFingerprint = fingerprint { + strings("NavigationBarItemSet(") +} + +internal val navigationBarItemSetConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + // Make sure the method checks whether navigation bar items are null before adding them. + // If this is not true, then we cannot patch the method and potentially transform the parameters into null. + opcodes(Opcode.IF_EQZ, Opcode.INVOKE_VIRTUAL) + custom { method, _ -> + method.indexOfFirstInstruction { + getReference()?.name == "add" + } >= 0 + } +} + +internal val oldNavigationBarAddItemFingerprint = fingerprint { + strings("Bottom navigation tabs exceeds maximum of 5 tabs") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt new file mode 100644 index 000000000..9685f0463 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt @@ -0,0 +1,110 @@ +package app.revanced.patches.spotify.layout.hide.createbutton + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import java.util.logging.Logger + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch;" + +@Suppress("unused") +val hideCreateButtonPatch = bytecodePatch( + name = "Hide Create button", + description = "Hides the \"Create\" button in the navigation bar." +) { + compatibleWith("com.spotify.music") + + dependsOn(sharedExtensionPatch) + + execute { + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + Logger.getLogger(this::class.java.name).warning( + "Create button does not exist in legacy app target. No changes applied." + ) + return@execute + } + + val oldNavigationBarAddItemMethod = oldNavigationBarAddItemFingerprint.originalMethodOrNull + // Only throw the fingerprint error when oldNavigationBarAddItemMethod does not exist. + val navigationBarItemSetClassDef = if (oldNavigationBarAddItemMethod == null) { + navigationBarItemSetClassFingerprint.originalClassDef + } else { + navigationBarItemSetClassFingerprint.originalClassDefOrNull + } + + if (navigationBarItemSetClassDef != null) { + // Main patch for newest and most versions. + // The NavigationBarItemSet constructor accepts multiple parameters which represent each navigation bar item. + // Each item is manually checked whether it is not null and then added to a LinkedHashSet. + // Since the order of the items can differ, we are required to check every parameter to see whether it is the + // Create button. So, for every parameter passed to the method, invoke our extension method and overwrite it + // to null in case it is the Create button. + navigationBarItemSetConstructorFingerprint.match(navigationBarItemSetClassDef).method.apply { + // Add 1 to the index because the first parameter register is `this`. + val parameterTypesWithRegister = parameterTypes.mapIndexed { index, parameterType -> + parameterType to (index + 1) + } + + val returnNullIfIsCreateButtonDescriptor = + "$EXTENSION_CLASS_DESCRIPTOR->returnNullIfIsCreateButton(Ljava/lang/Object;)Ljava/lang/Object;" + + parameterTypesWithRegister.reversed().forEach { (parameterType, parameterRegister) -> + addInstructions( + 0, + """ + invoke-static { p$parameterRegister }, $returnNullIfIsCreateButtonDescriptor + move-result-object p$parameterRegister + check-cast p$parameterRegister, $parameterType + """ + ) + } + } + } + + if (oldNavigationBarAddItemMethod != null) { + // In case an older version of the app is being patched, hook the old method which adds navigation bar items. + // Return null early if the navigation bar item title resource id is old Create button title resource id. + oldNavigationBarAddItemFingerprint.methodOrNull?.apply { + val getNavigationBarItemTitleStringIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.definingClass == "Landroid/content/res/Resources;" && reference.name == "getString" + } + // This register is a parameter register, so it can be used at the start of the method when adding + // the new instructions. + val oldNavigationBarItemTitleResIdRegister = + getInstruction(getNavigationBarItemTitleStringIndex).registerD + + // The instruction where the normal method logic starts. + val firstInstruction = getInstruction(0) + + val isOldCreateButtonDescriptor = + "$EXTENSION_CLASS_DESCRIPTOR->isOldCreateButton(I)Z" + + addInstructionsWithLabels( + 0, + """ + invoke-static { v$oldNavigationBarItemTitleResIdRegister }, $isOldCreateButtonDescriptor + move-result v0 + + # If this navigation bar item is not the Create button, jump to the normal method logic. + if-eqz v0, :normal-method-logic + + # Return null early because this method return value is a BottomNavigationItemView. + const/4 v0, 0 + return-object v0 + """, + ExternalLabel("normal-method-logic", firstInstruction) + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt index da0d8482d..67a5d65a7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt @@ -8,8 +8,8 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.stringOption import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.util.* import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode 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 708ec7e77..a797763a0 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 @@ -1,15 +1,18 @@ package app.revanced.patches.spotify.misc import app.revanced.patcher.fingerprint -import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstruction import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.TypeReference -internal val accountAttributeFingerprint = fingerprint { +context(BytecodePatchContext) +internal val accountAttributeFingerprint get() = fingerprint { custom { _, classDef -> classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) { "Lcom/spotify/useraccount/v1/AccountAttribute;" @@ -19,7 +22,8 @@ internal val accountAttributeFingerprint = fingerprint { } } -internal val productStateProtoGetMapFingerprint = fingerprint { +context(BytecodePatchContext) +internal val productStateProtoGetMapFingerprint get() = fingerprint { returns("Ljava/util/Map;") custom { _, classDef -> classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) { @@ -34,9 +38,22 @@ internal val buildQueryParametersFingerprint = fingerprint { strings("trackRows", "device_type:tablet") } -internal val contextMenuExperimentsFingerprint = fingerprint { +internal val contextMenuViewModelClassFingerprint = fingerprint { + strings("ContextMenuViewModel(header=") +} + +internal val contextMenuViewModelAddItemFingerprint = fingerprint { parameters("L") - strings("remove_ads_upsell_enabled") + returns("V") + custom { method, _ -> + method.indexOfFirstInstruction { + getReference()?.name == "add" + } >= 0 + } +} + +internal val getViewModelFingerprint = fingerprint { + custom { method, _ -> method.name == "getViewModel" } } internal val contextFromJsonFingerprint = fingerprint { @@ -47,15 +64,15 @@ internal val contextFromJsonFingerprint = fingerprint { Opcode.MOVE_RESULT_OBJECT, Opcode.INVOKE_STATIC ) - custom { methodDef, classDef -> - methodDef.name == "fromJson" && + custom { method, classDef -> + method.name == "fromJson" && classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;") } } internal val readPlayerOptionOverridesFingerprint = fingerprint { - custom { methodDef, classDef -> - methodDef.name == "readPlayerOptionOverrides" && + custom { method, classDef -> + method.name == "readPlayerOptionOverrides" && classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;") } } @@ -91,7 +108,8 @@ internal val homeStructureGetSectionsFingerprint = fingerprint { internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint { returns("Ljava/lang/Object;") parameters("Ljava/lang/Object;") - custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction { + custom { method, _ -> + method.name == "apply" && method.indexOfFirstInstruction { opcode == Opcode.NEW_INSTANCE && getReference()?.type?.endsWith(className) == true } >= 0 } 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 3e16751ab..d32ce24b3 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 @@ -2,20 +2,21 @@ 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.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableClass import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.util.* import app.revanced.util.toPublicAccessFlags 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 import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -60,7 +61,7 @@ val unlockPremiumPatch = bytecodePatch( addInstruction( getAttributesMapIndex + 1, "invoke-static { v$attributesMapRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->overrideAttribute(Ljava/util/Map;)V" + "$EXTENSION_CLASS_DESCRIPTOR->overrideAttributes(Ljava/util/Map;)V" ) } @@ -71,7 +72,7 @@ val unlockPremiumPatch = bytecodePatch( buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ ) - replaceInstruction(addQueryParameterConditionIndex, "nop") + removeInstruction(addQueryParameterConditionIndex) } @@ -119,14 +120,42 @@ val unlockPremiumPatch = bytecodePatch( } - // Disable the "Spotify Premium" upsell experiment in context menus. - contextMenuExperimentsFingerprint.method.apply { - val moveIsEnabledIndex = indexOfFirstInstructionOrThrow( - contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT - ) - val isUpsellEnabledRegister = getInstruction(moveIsEnabledIndex).registerA + val contextMenuViewModelClassDef = contextMenuViewModelClassFingerprint.originalClassDef - replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0") + // Hook the method which adds context menu items and return before adding if the item is a Premium ad. + contextMenuViewModelAddItemFingerprint.match(contextMenuViewModelClassDef).method.apply { + val contextMenuItemClassType = parameterTypes.first() + val contextMenuItemClassDef = classes.find { + it.type == contextMenuItemClassType + } ?: throw PatchException("Could not find context menu item class.") + + // The class returned by ContextMenuItem->getViewModel, which represents the actual context menu item. + val viewModelClassType = getViewModelFingerprint.match(contextMenuItemClassDef).originalMethod.returnType + + // The instruction where the normal method logic starts. + val firstInstruction = getInstruction(0) + + val isFilteredContextMenuItemDescriptor = + "$EXTENSION_CLASS_DESCRIPTOR->isFilteredContextMenuItem(Ljava/lang/Object;)Z" + + addInstructionsWithLabels( + 0, + """ + # The first parameter is the context menu item being added. + # Invoke getViewModel to get the actual context menu item. + invoke-interface { p1 }, $contextMenuItemClassType->getViewModel()$viewModelClassType + move-result-object v0 + + # Check if this context menu item should be filtered out. + invoke-static { v0 }, $isFilteredContextMenuItemDescriptor + move-result v0 + + # If this context menu item should not be filtered out, jump to the normal method logic. + if-eqz v0, :normal-method-logic + return-void + """, + ExternalLabel("normal-method-logic", firstInstruction) + ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt index 438fe49df..048228871 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt @@ -1,21 +1,5 @@ package app.revanced.patches.spotify.misc.extension -import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.shared.misc.extension.sharedExtensionPatch -import app.revanced.patches.spotify.shared.SPOTIFY_MAIN_ACTIVITY_LEGACY -/** - * If patching a legacy 8.x target. This may also be set if patching slightly older/newer app targets, - * but the only legacy target of interest is 8.6.98.900 as it's the last version that - * supports Spotify integration on Kenwood/Pioneer car stereos. - */ -internal var IS_SPOTIFY_LEGACY_APP_TARGET = false - -val sharedExtensionPatch = bytecodePatch { - dependsOn(sharedExtensionPatch("spotify", mainActivityOnCreateHook)) - - execute { - IS_SPOTIFY_LEGACY_APP_TARGET = mainActivityOnCreateHook.fingerprint - .originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY - } -} +val sharedExtensionPatch = sharedExtensionPatch("spotify", mainActivityOnCreateHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt index 3d60abf9b..a2d65561d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt @@ -1,41 +1,41 @@ -package app.revanced.patches.spotify.misc.privacy - -import app.revanced.patcher.fingerprint -import app.revanced.util.literal -import com.android.tools.smali.dexlib2.AccessFlags - -internal val shareCopyUrlFingerprint = fingerprint { - returns("Ljava/lang/Object;") - parameters("Ljava/lang/Object;") - strings("clipboard", "Spotify Link") - custom { method, _ -> - method.name == "invokeSuspend" - } -} - -internal val shareCopyUrlLegacyFingerprint = fingerprint { - returns("Ljava/lang/Object;") - parameters("Ljava/lang/Object;") - strings("clipboard", "createNewSession failed") - custom { method, _ -> - method.name == "apply" - } -} - -internal val formatAndroidShareSheetUrlFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) - returns("Ljava/lang/String;") - parameters("L", "Ljava/lang/String;") - literal { - '\n'.code.toLong() - } -} - -internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC) - returns("Ljava/lang/String;") - parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;") - literal { - '\n'.code.toLong() - } -} +package app.revanced.patches.spotify.misc.privacy + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags + +internal val shareCopyUrlFingerprint = fingerprint { + returns("Ljava/lang/Object;") + parameters("Ljava/lang/Object;") + strings("clipboard", "Spotify Link") + custom { method, _ -> + method.name == "invokeSuspend" + } +} + +internal val shareCopyUrlLegacyFingerprint = fingerprint { + returns("Ljava/lang/Object;") + parameters("Ljava/lang/Object;") + strings("clipboard", "createNewSession failed") + custom { method, _ -> + method.name == "apply" + } +} + +internal val formatAndroidShareSheetUrlFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Ljava/lang/String;") + parameters("L", "Ljava/lang/String;") + literal { + '\n'.code.toLong() + } +} + +internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Ljava/lang/String;") + parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;") + literal { + '\n'.code.toLong() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt index 8df4c7720..7fe59394d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt @@ -1,70 +1,70 @@ -package app.revanced.patches.spotify.misc.privacy - -import app.revanced.patcher.Fingerprint -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -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.getReference -import app.revanced.util.indexOfFirstInstructionOrThrow -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference - -private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;" - -@Suppress("unused") -val sanitizeSharingLinksPatch = bytecodePatch( - name = "Sanitize sharing links", - description = "Removes the tracking query parameters from links before they are shared.", -) { - compatibleWith("com.spotify.music") - - dependsOn(sharedExtensionPatch) - - execute { - val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" + - "sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;" - - val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) { - shareCopyUrlLegacyFingerprint - } else { - shareCopyUrlFingerprint - } - - copyFingerprint.method.apply { - val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow { - getReference()?.name == "newPlainText" - } - val register = getInstruction(newPlainTextInvokeIndex).registerD - - addInstructions( - newPlainTextInvokeIndex, - """ - invoke-static { v$register }, $extensionMethodDescriptor - move-result-object v$register - """ - ) - } - - // Android native share sheet is used for all other quick share types (X, WhatsApp, etc). - val shareUrlParameter : String - val shareSheetFingerprint : Fingerprint - if (IS_SPOTIFY_LEGACY_APP_TARGET) { - shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint - shareUrlParameter = "p2" - } else { - shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint - shareUrlParameter = "p1" - } - - shareSheetFingerprint.method.addInstructions( - 0, - """ - invoke-static { $shareUrlParameter }, $extensionMethodDescriptor - move-result-object $shareUrlParameter - """ - ) - } -} +package app.revanced.patches.spotify.misc.privacy + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;" + +@Suppress("unused") +val sanitizeSharingLinksPatch = bytecodePatch( + name = "Sanitize sharing links", + description = "Removes the tracking query parameters from links before they are shared.", +) { + compatibleWith("com.spotify.music") + + dependsOn(sharedExtensionPatch) + + execute { + val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" + + "sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;" + + val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) { + shareCopyUrlLegacyFingerprint + } else { + shareCopyUrlFingerprint + } + + copyFingerprint.method.apply { + val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "newPlainText" + } + val urlRegister = getInstruction(newPlainTextInvokeIndex).registerD + + addInstructions( + newPlainTextInvokeIndex, + """ + invoke-static { v$urlRegister }, $extensionMethodDescriptor + move-result-object v$urlRegister + """ + ) + } + + // Android native share sheet is used for all other quick share types (X, WhatsApp, etc). + val shareUrlParameter : String + val shareSheetFingerprint : Fingerprint + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint + shareUrlParameter = "p2" + } else { + shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint + shareUrlParameter = "p1" + } + + shareSheetFingerprint.method.addInstructions( + 0, + """ + invoke-static { $shareUrlParameter }, $extensionMethodDescriptor + move-result-object $shareUrlParameter + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt index ad40f24e2..c84b43f71 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt @@ -1,7 +1,9 @@ package app.revanced.patches.spotify.misc.widgets import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.util.returnEarly +import java.util.logging.Logger @Suppress("unused") val fixThirdPartyLaunchersWidgets = bytecodePatch( @@ -11,6 +13,14 @@ val fixThirdPartyLaunchersWidgets = bytecodePatch( compatibleWith("com.spotify.music") execute { + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + // The permission check does not exist in legacy versions. + Logger.getLogger(this::class.java.name).warning( + "Legacy app target does not have any third party launcher restrictions. No changes applied." + ) + return@execute + } + // Only system app launchers are granted the BIND_APPWIDGET permission. // Override the method that checks for it to always return true, as this permission is not actually required // for the widgets to work. diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt index 1afbcde45..b107fd267 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt @@ -1,6 +1,8 @@ package app.revanced.patches.spotify.shared import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patches.spotify.misc.extension.mainActivityOnCreateHook private const val SPOTIFY_MAIN_ACTIVITY = "Lcom/spotify/music/SpotifyMainActivity;" @@ -15,3 +17,18 @@ internal val mainActivityOnCreateFingerprint = fingerprint { || classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY) } } + +private var isLegacyAppTarget: Boolean? = null + +/** + * If patching a legacy 8.x target. This may also be set if patching slightly older/newer app targets, + * but the only legacy target of interest is 8.6.98.900 as it's the last version that + * supports Spotify integration on Kenwood/Pioneer car stereos. + */ +context(BytecodePatchContext) +internal val IS_SPOTIFY_LEGACY_APP_TARGET get(): Boolean { + if (isLegacyAppTarget == null) { + isLegacyAppTarget = mainActivityOnCreateHook.fingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY + } + return isLegacyAppTarget!! +}