From ce5385b28ebd54e0a2fe1be81c77ab2b6ddab4e1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 3 Jun 2025 04:15:52 -0300 Subject: [PATCH] feat(Spotify): Add `Hide Create button` patch (#5062) --- extensions/spotify/build.gradle.kts | 4 +- .../createbutton/HideCreateButtonPatch.java | 51 +++++++ extensions/spotify/stub/build.gradle.kts | 6 +- patches/api/patches.api | 4 + .../layout/hide/createbutton/Fingerprints.kt | 28 ++++ .../createbutton/HideCreateButtonPatch.kt | 110 ++++++++++++++ .../spotify/layout/theme/CustomThemePatch.kt | 2 +- .../patches/spotify/misc/Fingerprints.kt | 38 +++-- .../spotify/misc/extension/ExtensionPatch.kt | 18 +-- .../spotify/misc/privacy/Fingerprints.kt | 82 +++++----- .../misc/privacy/SanitizeSharingLinksPatch.kt | 140 +++++++++--------- .../widgets/FixThirdPartyLaunchersWidgets.kt | 10 ++ .../patches/spotify/shared/Fingerprints.kt | 17 +++ 13 files changed, 366 insertions(+), 144 deletions(-) create mode 100644 extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt 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/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/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!! +}