feat(Spotify): Add Hide Create button
patch (#5062)
This commit is contained in:
@ -10,7 +10,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class UnlockPremiumPatch {
|
public final class UnlockPremiumPatch {
|
||||||
@ -22,15 +23,15 @@ public final class UnlockPremiumPatch {
|
|||||||
private static final boolean IS_SPOTIFY_LEGACY_APP_TARGET;
|
private static final boolean IS_SPOTIFY_LEGACY_APP_TARGET;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
boolean legacy;
|
boolean isLegacy;
|
||||||
try {
|
try {
|
||||||
Class.forName(SPOTIFY_MAIN_ACTIVITY_LEGACY);
|
Class.forName(SPOTIFY_MAIN_ACTIVITY_LEGACY);
|
||||||
legacy = true;
|
isLegacy = true;
|
||||||
} catch (ClassNotFoundException ex) {
|
} catch (ClassNotFoundException ex) {
|
||||||
legacy = false;
|
isLegacy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IS_SPOTIFY_LEGACY_APP_TARGET = legacy;
|
IS_SPOTIFY_LEGACY_APP_TARGET = isLegacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class OverrideAttribute {
|
private static class OverrideAttribute {
|
||||||
@ -61,11 +62,12 @@ public final class UnlockPremiumPatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final List<OverrideAttribute> OVERRIDES = List.of(
|
private static final List<OverrideAttribute> PREMIUM_OVERRIDES = List.of(
|
||||||
// Disables player and app ads.
|
// Disables player and app ads.
|
||||||
new OverrideAttribute("ads", FALSE),
|
new OverrideAttribute("ads", FALSE),
|
||||||
// Works along on-demand, allows playing any song without restriction.
|
// Works along on-demand, allows playing any song without restriction.
|
||||||
new OverrideAttribute("player-license", "premium"),
|
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.
|
// Disables shuffle being initially enabled when first playing a playlist.
|
||||||
new OverrideAttribute("shuffle", FALSE),
|
new OverrideAttribute("shuffle", FALSE),
|
||||||
// Allows playing any song on-demand, without a shuffled order.
|
// Allows playing any song on-demand, without a shuffled order.
|
||||||
@ -91,18 +93,46 @@ public final class UnlockPremiumPatch {
|
|||||||
new OverrideAttribute("tablet-free", FALSE, false)
|
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<Integer> REMOVED_HOME_SECTIONS = List.of(
|
private static final List<Integer> REMOVED_HOME_SECTIONS = List.of(
|
||||||
Section.VIDEO_BRAND_AD_FIELD_NUMBER,
|
Section.VIDEO_BRAND_AD_FIELD_NUMBER,
|
||||||
Section.IMAGE_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<List<String>> 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.
|
* Injection point. Override account attributes.
|
||||||
*/
|
*/
|
||||||
public static void overrideAttribute(Map<String, /*AccountAttribute*/ Object> attributes) {
|
public static void overrideAttributes(Map<String, /*AccountAttribute*/ Object> attributes) {
|
||||||
try {
|
try {
|
||||||
for (var override : OVERRIDES) {
|
for (OverrideAttribute override : PREMIUM_OVERRIDES) {
|
||||||
var attribute = attributes.get(override.key);
|
Object attribute = attributes.get(override.key);
|
||||||
if (attribute == null) {
|
if (attribute == null) {
|
||||||
if (override.isExpected) {
|
if (override.isExpected) {
|
||||||
Logger.printException(() -> "'" + override.key + "' expected but not found");
|
Logger.printException(() -> "'" + override.key + "' expected but not found");
|
||||||
@ -117,12 +147,12 @@ public final class UnlockPremiumPatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} 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) {
|
public static String removeStationString(String spotifyUriOrUrl) {
|
||||||
return spotifyUriOrUrl.replace("spotify:station:", "spotify:");
|
return spotifyUriOrUrl.replace("spotify:station:", "spotify:");
|
||||||
@ -130,7 +160,7 @@ public final class UnlockPremiumPatch {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point. Remove ads sections from home.
|
* 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<Section> sections) {
|
public static void removeHomeSections(List<Section> sections) {
|
||||||
try {
|
try {
|
||||||
@ -139,4 +169,17 @@ public final class UnlockPremiumPatch {
|
|||||||
Logger.printException(() -> "Remove home sections failure", ex);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,11 @@ android {
|
|||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -873,6 +873,10 @@ public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSync
|
|||||||
public static final fun getEnableOfflineSync ()Lapp/revanced/patcher/patch/BytecodePatch;
|
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 final class app/revanced/patches/spotify/layout/theme/CustomThemePatchKt {
|
||||||
public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
}
|
}
|
||||||
|
@ -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<MethodReference>()?.name == "add"
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val oldNavigationBarAddItemFingerprint = fingerprint {
|
||||||
|
strings("Bottom navigation tabs exceeds maximum of 5 tabs")
|
||||||
|
}
|
@ -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<MethodReference>()
|
||||||
|
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<FiveRegisterInstruction>(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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,8 @@ import app.revanced.patcher.patch.bytecodePatch
|
|||||||
import app.revanced.patcher.patch.resourcePatch
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
import app.revanced.patcher.patch.stringOption
|
import app.revanced.patcher.patch.stringOption
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
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.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
import app.revanced.util.*
|
import app.revanced.util.*
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
package app.revanced.patches.spotify.misc
|
package app.revanced.patches.spotify.misc
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
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.getReference
|
||||||
import app.revanced.util.indexOfFirstInstruction
|
import app.revanced.util.indexOfFirstInstruction
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
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
|
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||||
|
|
||||||
internal val accountAttributeFingerprint = fingerprint {
|
context(BytecodePatchContext)
|
||||||
|
internal val accountAttributeFingerprint get() = fingerprint {
|
||||||
custom { _, classDef ->
|
custom { _, classDef ->
|
||||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
"Lcom/spotify/useraccount/v1/AccountAttribute;"
|
"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;")
|
returns("Ljava/util/Map;")
|
||||||
custom { _, classDef ->
|
custom { _, classDef ->
|
||||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
@ -34,9 +38,22 @@ internal val buildQueryParametersFingerprint = fingerprint {
|
|||||||
strings("trackRows", "device_type:tablet")
|
strings("trackRows", "device_type:tablet")
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val contextMenuExperimentsFingerprint = fingerprint {
|
internal val contextMenuViewModelClassFingerprint = fingerprint {
|
||||||
|
strings("ContextMenuViewModel(header=")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val contextMenuViewModelAddItemFingerprint = fingerprint {
|
||||||
parameters("L")
|
parameters("L")
|
||||||
strings("remove_ads_upsell_enabled")
|
returns("V")
|
||||||
|
custom { method, _ ->
|
||||||
|
method.indexOfFirstInstruction {
|
||||||
|
getReference<MethodReference>()?.name == "add"
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val getViewModelFingerprint = fingerprint {
|
||||||
|
custom { method, _ -> method.name == "getViewModel" }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val contextFromJsonFingerprint = fingerprint {
|
internal val contextFromJsonFingerprint = fingerprint {
|
||||||
@ -47,15 +64,15 @@ internal val contextFromJsonFingerprint = fingerprint {
|
|||||||
Opcode.MOVE_RESULT_OBJECT,
|
Opcode.MOVE_RESULT_OBJECT,
|
||||||
Opcode.INVOKE_STATIC
|
Opcode.INVOKE_STATIC
|
||||||
)
|
)
|
||||||
custom { methodDef, classDef ->
|
custom { method, classDef ->
|
||||||
methodDef.name == "fromJson" &&
|
method.name == "fromJson" &&
|
||||||
classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
|
classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val readPlayerOptionOverridesFingerprint = fingerprint {
|
internal val readPlayerOptionOverridesFingerprint = fingerprint {
|
||||||
custom { methodDef, classDef ->
|
custom { method, classDef ->
|
||||||
methodDef.name == "readPlayerOptionOverrides" &&
|
method.name == "readPlayerOptionOverrides" &&
|
||||||
classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
|
classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,7 +108,8 @@ internal val homeStructureGetSectionsFingerprint = fingerprint {
|
|||||||
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
|
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
|
||||||
returns("Ljava/lang/Object;")
|
returns("Ljava/lang/Object;")
|
||||||
parameters("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<TypeReference>()?.type?.endsWith(className) == true
|
opcode == Opcode.NEW_INSTANCE && getReference<TypeReference>()?.type?.endsWith(className) == true
|
||||||
} >= 0
|
} >= 0
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,21 @@ package app.revanced.patches.spotify.misc
|
|||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
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.getInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
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.PatchException
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
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.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
import app.revanced.util.*
|
import app.revanced.util.*
|
||||||
import app.revanced.util.toPublicAccessFlags
|
import app.revanced.util.toPublicAccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
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.instruction.TwoRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
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.MethodReference
|
||||||
@ -60,7 +61,7 @@ val unlockPremiumPatch = bytecodePatch(
|
|||||||
addInstruction(
|
addInstruction(
|
||||||
getAttributesMapIndex + 1,
|
getAttributesMapIndex + 1,
|
||||||
"invoke-static { v$attributesMapRegister }, " +
|
"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
|
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.
|
val contextMenuViewModelClassDef = contextMenuViewModelClassFingerprint.originalClassDef
|
||||||
contextMenuExperimentsFingerprint.method.apply {
|
|
||||||
val moveIsEnabledIndex = indexOfFirstInstructionOrThrow(
|
|
||||||
contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT
|
|
||||||
)
|
|
||||||
val isUpsellEnabledRegister = getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
|
|
||||||
|
|
||||||
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,21 +1,5 @@
|
|||||||
package app.revanced.patches.spotify.misc.extension
|
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.shared.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.spotify.shared.SPOTIFY_MAIN_ACTIVITY_LEGACY
|
|
||||||
|
|
||||||
/**
|
val sharedExtensionPatch = sharedExtensionPatch("spotify", mainActivityOnCreateHook)
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,41 +1,41 @@
|
|||||||
package app.revanced.patches.spotify.misc.privacy
|
package app.revanced.patches.spotify.misc.privacy
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
import app.revanced.patcher.fingerprint
|
||||||
import app.revanced.util.literal
|
import app.revanced.util.literal
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
|
||||||
internal val shareCopyUrlFingerprint = fingerprint {
|
internal val shareCopyUrlFingerprint = fingerprint {
|
||||||
returns("Ljava/lang/Object;")
|
returns("Ljava/lang/Object;")
|
||||||
parameters("Ljava/lang/Object;")
|
parameters("Ljava/lang/Object;")
|
||||||
strings("clipboard", "Spotify Link")
|
strings("clipboard", "Spotify Link")
|
||||||
custom { method, _ ->
|
custom { method, _ ->
|
||||||
method.name == "invokeSuspend"
|
method.name == "invokeSuspend"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val shareCopyUrlLegacyFingerprint = fingerprint {
|
internal val shareCopyUrlLegacyFingerprint = fingerprint {
|
||||||
returns("Ljava/lang/Object;")
|
returns("Ljava/lang/Object;")
|
||||||
parameters("Ljava/lang/Object;")
|
parameters("Ljava/lang/Object;")
|
||||||
strings("clipboard", "createNewSession failed")
|
strings("clipboard", "createNewSession failed")
|
||||||
custom { method, _ ->
|
custom { method, _ ->
|
||||||
method.name == "apply"
|
method.name == "apply"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
|
internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||||
returns("Ljava/lang/String;")
|
returns("Ljava/lang/String;")
|
||||||
parameters("L", "Ljava/lang/String;")
|
parameters("L", "Ljava/lang/String;")
|
||||||
literal {
|
literal {
|
||||||
'\n'.code.toLong()
|
'\n'.code.toLong()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
|
internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PUBLIC)
|
accessFlags(AccessFlags.PUBLIC)
|
||||||
returns("Ljava/lang/String;")
|
returns("Ljava/lang/String;")
|
||||||
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
|
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
|
||||||
literal {
|
literal {
|
||||||
'\n'.code.toLong()
|
'\n'.code.toLong()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,70 +1,70 @@
|
|||||||
package app.revanced.patches.spotify.misc.privacy
|
package app.revanced.patches.spotify.misc.privacy
|
||||||
|
|
||||||
import app.revanced.patcher.Fingerprint
|
import app.revanced.patcher.Fingerprint
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
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.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.getReference
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
"Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
|
"Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val sanitizeSharingLinksPatch = bytecodePatch(
|
val sanitizeSharingLinksPatch = bytecodePatch(
|
||||||
name = "Sanitize sharing links",
|
name = "Sanitize sharing links",
|
||||||
description = "Removes the tracking query parameters from links before they are shared.",
|
description = "Removes the tracking query parameters from links before they are shared.",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.spotify.music")
|
compatibleWith("com.spotify.music")
|
||||||
|
|
||||||
dependsOn(sharedExtensionPatch)
|
dependsOn(sharedExtensionPatch)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
|
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||||
"sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
|
"sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
|
||||||
|
|
||||||
val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
shareCopyUrlLegacyFingerprint
|
shareCopyUrlLegacyFingerprint
|
||||||
} else {
|
} else {
|
||||||
shareCopyUrlFingerprint
|
shareCopyUrlFingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
copyFingerprint.method.apply {
|
copyFingerprint.method.apply {
|
||||||
val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
|
val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
|
||||||
getReference<MethodReference>()?.name == "newPlainText"
|
getReference<MethodReference>()?.name == "newPlainText"
|
||||||
}
|
}
|
||||||
val register = getInstruction<FiveRegisterInstruction>(newPlainTextInvokeIndex).registerD
|
val urlRegister = getInstruction<FiveRegisterInstruction>(newPlainTextInvokeIndex).registerD
|
||||||
|
|
||||||
addInstructions(
|
addInstructions(
|
||||||
newPlainTextInvokeIndex,
|
newPlainTextInvokeIndex,
|
||||||
"""
|
"""
|
||||||
invoke-static { v$register }, $extensionMethodDescriptor
|
invoke-static { v$urlRegister }, $extensionMethodDescriptor
|
||||||
move-result-object v$register
|
move-result-object v$urlRegister
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
|
// Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
|
||||||
val shareUrlParameter : String
|
val shareUrlParameter : String
|
||||||
val shareSheetFingerprint : Fingerprint
|
val shareSheetFingerprint : Fingerprint
|
||||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
|
shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
|
||||||
shareUrlParameter = "p2"
|
shareUrlParameter = "p2"
|
||||||
} else {
|
} else {
|
||||||
shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
|
shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
|
||||||
shareUrlParameter = "p1"
|
shareUrlParameter = "p1"
|
||||||
}
|
}
|
||||||
|
|
||||||
shareSheetFingerprint.method.addInstructions(
|
shareSheetFingerprint.method.addInstructions(
|
||||||
0,
|
0,
|
||||||
"""
|
"""
|
||||||
invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
|
invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
|
||||||
move-result-object $shareUrlParameter
|
move-result-object $shareUrlParameter
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package app.revanced.patches.spotify.misc.widgets
|
package app.revanced.patches.spotify.misc.widgets
|
||||||
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
import app.revanced.util.returnEarly
|
import app.revanced.util.returnEarly
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val fixThirdPartyLaunchersWidgets = bytecodePatch(
|
val fixThirdPartyLaunchersWidgets = bytecodePatch(
|
||||||
@ -11,6 +13,14 @@ val fixThirdPartyLaunchersWidgets = bytecodePatch(
|
|||||||
compatibleWith("com.spotify.music")
|
compatibleWith("com.spotify.music")
|
||||||
|
|
||||||
execute {
|
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.
|
// 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
|
// Override the method that checks for it to always return true, as this permission is not actually required
|
||||||
// for the widgets to work.
|
// for the widgets to work.
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package app.revanced.patches.spotify.shared
|
package app.revanced.patches.spotify.shared
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
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;"
|
private const val SPOTIFY_MAIN_ACTIVITY = "Lcom/spotify/music/SpotifyMainActivity;"
|
||||||
|
|
||||||
@ -15,3 +17,18 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
|
|||||||
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
|
|| 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!!
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user