fix(Spotify - Unlock Spotify Premium): Remove pop up premium ads (#4842)

This commit is contained in:
Nuckyz 2025-05-06 04:35:47 -03:00 committed by GitHub
parent 52af71f68a
commit 00aa2000ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 137 additions and 43 deletions

View file

@ -130,6 +130,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.
*/ */
public static void removeHomeSections(List<Section> sections) { public static void removeHomeSections(List<Section> sections) {
try { try {

View file

@ -2,8 +2,12 @@ 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.patches.spotify.misc.extension.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.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.TypeReference
internal val accountAttributeFingerprint = fingerprint { internal val accountAttributeFingerprint = fingerprint {
custom { _, classDef -> custom { _, classDef ->
@ -15,7 +19,7 @@ internal val accountAttributeFingerprint = fingerprint {
} }
} }
internal val productStateProtoFingerprint = fingerprint { internal val productStateProtoGetMapFingerprint = 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) {
@ -56,16 +60,41 @@ internal val readPlayerOptionOverridesFingerprint = fingerprint {
} }
} }
internal val homeSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
}
internal val protobufListsFingerprint = fingerprint { internal val protobufListsFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
custom { method, _ -> method.name == "emptyProtobufList" } custom { method, _ -> method.name == "emptyProtobufList" }
} }
internal val homeStructureFingerprint = fingerprint { internal val protobufListRemoveFingerprint = fingerprint {
opcodes(Opcode.IGET_OBJECT, Opcode.RETURN_OBJECT) custom { method, _ -> method.name == "remove" }
custom { _, classDef -> classDef.endsWith("homeapi/proto/HomeStructure;") }
} }
internal val homeSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
}
internal val homeStructureGetSectionsFingerprint = fingerprint {
custom { method, classDef ->
classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
} >= 0
}
}
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("Ljava/lang/Object;")
parameters("Ljava/lang/Object;")
custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction {
opcode == Opcode.NEW_INSTANCE && getReference<TypeReference>()?.type?.endsWith(className) == true
} >= 0
}
}
internal const val PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME = "FetchMessageRequest;"
internal val pendragonJsonFetchMessageRequestFingerprint =
reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME)
internal const val PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME = "FetchMessageListRequest;"
internal val pendragonProtoFetchMessageListRequestFingerprint =
reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME)

View file

@ -4,21 +4,25 @@ 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.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction 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.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.fingerprint 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.MutableMethod
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET 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.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.AccessFlags 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.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
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import java.util.logging.Logger import java.util.logging.Logger
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;" private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
@ -41,14 +45,18 @@ val unlockPremiumPatch = bytecodePatch(
) )
execute { execute {
// Make _value accessible so that it can be overridden in the extension. fun MutableClass.publicizeField(fieldName: String) {
accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply { fields.first { it.name == fieldName }.apply {
// Add public flag and remove private. // Add public and remove private flag.
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv()) accessFlags = accessFlags.toPublicAccessFlags()
}
} }
// Make _value accessible so that it can be overridden in the extension.
accountAttributeFingerprint.classDef.publicizeField("value_")
// Override the attributes map in the getter method. // Override the attributes map in the getter method.
productStateProtoFingerprint.method.apply { productStateProtoGetMapFingerprint.method.apply {
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA
@ -61,12 +69,12 @@ val unlockPremiumPatch = bytecodePatch(
// Add the query parameter trackRows to show popular tracks in the artist page. // Add the query parameter trackRows to show popular tracks in the artist page.
buildQueryParametersFingerprint.apply { buildQueryParametersFingerprint.method.apply {
val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow( val addQueryParameterConditionIndex = indexOfFirstInstructionReversedOrThrow(
stringMatches!!.first().index, Opcode.IF_EQZ buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ
) )
method.replaceInstruction(addQueryParameterConditionIndex, "nop") replaceInstruction(addQueryParameterConditionIndex, "nop")
} }
@ -105,48 +113,39 @@ val unlockPremiumPatch = bytecodePatch(
val shufflingContextCallIndex = indexOfFirstInstructionOrThrow { val shufflingContextCallIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.name == "shufflingContext" getReference<MethodReference>()?.name == "shufflingContext"
} }
val boolRegister = getInstruction<FiveRegisterInstruction>(shufflingContextCallIndex).registerD
val registerBool = getInstruction<FiveRegisterInstruction>(shufflingContextCallIndex).registerD
addInstruction( addInstruction(
shufflingContextCallIndex, shufflingContextCallIndex,
"sget-object v$registerBool, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;" "sget-object v$boolRegister, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
) )
} }
// Disable the "Spotify Premium" upsell experiment in context menus. // Disable the "Spotify Premium" upsell experiment in context menus.
contextMenuExperimentsFingerprint.apply { contextMenuExperimentsFingerprint.method.apply {
val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow( val moveIsEnabledIndex = indexOfFirstInstructionOrThrow(
stringMatches!!.first().index, Opcode.MOVE_RESULT contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT
) )
val isUpsellEnabledRegister = method.getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA val isUpsellEnabledRegister = getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0") replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
} }
// Make featureTypeCase_ accessible so we can check the home section type in the extension. val protobufListClassDef = with(protobufListsFingerprint.originalMethod) {
homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply {
// Add public flag and remove private.
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
}
val protobufListClassName = with(protobufListsFingerprint.originalMethod) {
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT) val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass // Find the protobuffer list class using the definingClass which contains the empty list static value.
} val classType = getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass
val protobufListRemoveFingerprint = fingerprint { classes.find { it.type == classType } ?: throw PatchException("Could not find protobuffer list class.")
custom { method, classDef ->
method.name == "remove" && classDef.type == protobufListClassName
}
} }
// Need to allow mutation of the list so the home ads sections can be removed. // Need to allow mutation of the list so the home ads sections can be removed.
// Protobuffer list has an 'isMutable' boolean parameter that sets the mutability. // Protobuffer list has an 'isMutable' boolean parameter that sets the mutability.
// Forcing that always on breaks unrelated code in strange ways. // Forcing that always on breaks unrelated code in strange ways.
// Instead, remove the method call that checks if the list is unmodifiable. // Instead, remove the method call that checks if the list is unmodifiable.
protobufListRemoveFingerprint.method.apply { protobufListRemoveFingerprint.match(protobufListClassDef).method.apply {
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow { val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>() val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
@ -157,8 +156,12 @@ val unlockPremiumPatch = bytecodePatch(
removeInstruction(invokeThrowUnmodifiableIndex) removeInstruction(invokeThrowUnmodifiableIndex)
} }
// Make featureTypeCase_ accessible so we can check the home section type in the extension.
homeSectionFingerprint.classDef.publicizeField("featureTypeCase_")
// Remove ads sections from home. // Remove ads sections from home.
homeStructureFingerprint.method.apply { homeStructureGetSectionsFingerprint.method.apply {
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA
@ -168,5 +171,56 @@ val unlockPremiumPatch = bytecodePatch(
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V" "$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
) )
} }
// Replace a fetch request that returns and maps Singles with their static onErrorReturn value.
fun MutableMethod.replaceFetchRequestSingleWithError(requestClassName: String) {
// The index of where the request class is being instantiated.
val requestInstantiationIndex = indexOfFirstInstructionOrThrow {
getReference<TypeReference>()?.type?.endsWith(requestClassName) == true
}
// The index of where the onErrorReturn method is called with the error static value.
val onErrorReturnCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
getReference<MethodReference>()?.name == "onErrorReturn"
}
val onErrorReturnCallInstruction = getInstruction<FiveRegisterInstruction>(onErrorReturnCallIndex)
// The error static value register.
val onErrorReturnValueRegister = onErrorReturnCallInstruction.registerD
// The index where the error static value starts being constructed.
// Because the Singles are mapped, the error static value starts being constructed right after the first
// move-result-object of the map call, before the onErrorReturn method call.
val onErrorReturnValueConstructionIndex =
indexOfFirstInstructionReversedOrThrow(onErrorReturnCallIndex, Opcode.MOVE_RESULT_OBJECT) + 1
val singleClassName = onErrorReturnCallInstruction.getReference<MethodReference>()!!.definingClass
// The index where the request is firstly called, before its result is mapped to other values.
val requestCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
getReference<MethodReference>()?.returnType == singleClassName
}
// Construct a new single with the error static value and return it.
addInstructions(
onErrorReturnCallIndex,
"invoke-static { v$onErrorReturnValueRegister }, " +
"$singleClassName->just(Ljava/lang/Object;)$singleClassName\n" +
"move-result-object v$onErrorReturnValueRegister\n" +
"return-object v$onErrorReturnValueRegister"
)
// Remove every instruction from the request call to right before the error static value construction.
val removeCount = onErrorReturnValueConstructionIndex - requestCallIndex
removeInstructions(requestCallIndex, removeCount)
}
// Remove pendragon (pop up ads) requests and return the errors instead.
pendragonJsonFetchMessageRequestFingerprint.method.replaceFetchRequestSingleWithError(
PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME
)
pendragonProtoFetchMessageListRequestFingerprint.method.replaceFetchRequestSingleWithError(
PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME
)
} }
} }

View file

@ -60,4 +60,4 @@ val spoofPackageInfoPatch = bytecodePatch(
// endregion // endregion
} }
} }
} }

View file

@ -14,4 +14,4 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY) || classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
} }
} }

View file

@ -18,6 +18,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.util.InstructionUtils.Companion.branchOpcodes import app.revanced.util.InstructionUtils.Companion.branchOpcodes
import app.revanced.util.InstructionUtils.Companion.returnOpcodes import app.revanced.util.InstructionUtils.Companion.returnOpcodes
import app.revanced.util.InstructionUtils.Companion.writeOpcodes import app.revanced.util.InstructionUtils.Companion.writeOpcodes
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.Opcode.* import com.android.tools.smali.dexlib2.Opcode.*
import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.Method
@ -169,6 +170,15 @@ internal val Instruction.isBranchInstruction: Boolean
internal val Instruction.isReturnInstruction: Boolean internal val Instruction.isReturnInstruction: Boolean
get() = this.opcode in returnOpcodes get() = this.opcode in returnOpcodes
/**
* Adds public [AccessFlags] and removes private and protected flags (if present).
*/
internal fun Int.toPublicAccessFlags() : Int {
return this.or(AccessFlags.PUBLIC.value)
.and(AccessFlags.PROTECTED.value.inv())
.and(AccessFlags.PRIVATE.value.inv())
}
/** /**
* Find the [MutableMethod] from a given [Method] in a [MutableClass]. * Find the [MutableMethod] from a given [Method] in a [MutableClass].
* *