fix(Spotify): Remove ads sections from home (#4722)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
Nuckyz 2025-04-04 09:32:40 -03:00 committed by GitHub
parent eaee621831
commit 0b9a5e7f89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 130 additions and 25 deletions

View File

@ -4,6 +4,7 @@ import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import com.spotify.remoteconfig.internal.AccountAttribute;
import com.spotify.home.evopage.homeapi.proto.Section;
import java.util.List;
import java.util.Map;
@ -69,8 +70,13 @@ public final class UnlockPremiumPatch {
new OverrideAttribute("tablet-free", FALSE, false)
);
private static final List<Integer> REMOVED_HOME_SECTIONS = List.of(
Section.VIDEO_BRAND_AD_FIELD_NUMBER,
Section.IMAGE_BRAND_AD_FIELD_NUMBER
);
/**
* Injection point.
* Override attributes injection point.
*/
public static void overrideAttribute(Map<String, AccountAttribute> attributes) {
try {
@ -78,7 +84,7 @@ public final class UnlockPremiumPatch {
var attribute = attributes.get(override.key);
if (attribute == null) {
if (override.isExpected) {
Logger.printException(() -> "''" + override.key + "' expected but not found");
Logger.printException(() -> "'" + override.key + "' expected but not found");
}
} else {
attribute.value_ = override.overrideValue;
@ -88,4 +94,15 @@ public final class UnlockPremiumPatch {
Logger.printException(() -> "overrideAttribute failure", ex);
}
}
/**
* Remove ads sections from home injection point.
*/
public static void removeHomeSections(List<Section> sections) {
try {
sections.removeIf(section -> REMOVED_HOME_SECTIONS.contains(section.featureTypeCase_));
} catch (Exception ex) {
Logger.printException(() -> "Remove home sections failure", ex);
}
}
}

View File

@ -0,0 +1,7 @@
package com.spotify.home.evopage.homeapi.proto;
public final class Section {
public static final int VIDEO_BRAND_AD_FIELD_NUMBER = 20;
public static final int IMAGE_BRAND_AD_FIELD_NUMBER = 21;
public int featureTypeCase_;
}

View File

@ -1,16 +1,16 @@
package app.revanced.patches.spotify.misc
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val accountAttributeFingerprint = fingerprint {
custom { _, c -> c.endsWith("internal/AccountAttribute;") }
custom { _, classDef -> classDef.endsWith("internal/AccountAttribute;") }
}
internal val productStateProtoFingerprint = fingerprint {
returns("Ljava/util/Map;")
custom { _, classDef ->
classDef.endsWith("ProductStateProto;")
}
custom { _, classDef -> classDef.endsWith("ProductStateProto;") }
}
internal val buildQueryParametersFingerprint = fingerprint {
@ -21,3 +21,17 @@ internal val contextMenuExperimentsFingerprint = fingerprint {
parameters("L")
strings("remove_ads_upsell_enabled")
}
internal val homeSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
}
internal val protobufListsFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
custom { method, _ -> method.name == "emptyProtobufList" }
}
internal val homeStructureFingerprint = fingerprint {
opcodes(Opcode.IGET_OBJECT, Opcode.RETURN_OBJECT)
custom { _, classDef -> classDef.endsWith("homeapi/proto/HomeStructure;") }
}

View File

@ -2,11 +2,18 @@ package app.revanced.patches.spotify.misc
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.util.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
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
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
@ -27,23 +34,75 @@ val unlockPremiumPatch = bytecodePatch(
}
// Override the attributes map in the getter method.
val attributesMapRegister = 0
val instantiateUnmodifiableMapIndex = 1
productStateProtoFingerprint.method.addInstruction(
instantiateUnmodifiableMapIndex,
"invoke-static { v$attributesMapRegister }," +
"$EXTENSION_CLASS_DESCRIPTOR->overrideAttribute(Ljava/util/Map;)V",
)
with(productStateProtoFingerprint.method) {
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA
addInstruction(
getAttributesMapIndex + 1,
"invoke-static { v$attributesMapRegister }, " +
"$EXTENSION_CLASS_DESCRIPTOR->overrideAttribute(Ljava/util/Map;)V"
)
}
// Add the query parameter trackRows to show popular tracks in the artist page.
val addQueryParameterIndex = buildQueryParametersFingerprint.stringMatches!!.first().index - 1
buildQueryParametersFingerprint.method.replaceInstruction(addQueryParameterIndex, "nop")
with(buildQueryParametersFingerprint) {
val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow(
stringMatches!!.first().index, Opcode.IF_EQZ
)
method.replaceInstruction(addQueryParameterConditionIndex, "nop")
}
// Disable the "Spotify Premium" upsell experiment in context menus.
with(contextMenuExperimentsFingerprint) {
val moveIsEnabledIndex = stringMatches!!.first().index + 2
val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow(
stringMatches!!.first().index, Opcode.MOVE_RESULT
)
val isUpsellEnabledRegister = method.getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
}
// Make featureTypeCase_ accessible so we can check the home section type in the extension.
homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply {
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
}
val protobufListClassName = with(protobufListsFingerprint.originalMethod) {
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass
}
val protobufListRemoveFingerprint = fingerprint {
custom { method, classDef ->
method.name == "remove" && classDef.type == protobufListClassName
}
}
// 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.
// Forcing that always on breaks unrelated code in strange ways.
// Instead, remove the method call that checks if the list is unmodifiable.
with(protobufListRemoveFingerprint.method) {
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.returnType == "V" && reference.parameterTypes.isEmpty()
}
// Remove the method call that throws an exception if the list is not mutable.
removeInstruction(invokeThrowUnmodifiableIndex)
}
// Remove ads sections from home.
with(homeStructureFingerprint.method) {
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA
addInstruction(
getSectionsIndex + 1,
"invoke-static { v$sectionsRegister }, " +
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
)
}
}
}

View File

@ -53,7 +53,15 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
// All registers used by an instruction.
fun Instruction.getRegistersUsed() = when (this) {
is FiveRegisterInstruction -> listOf(registerC, registerD, registerE, registerF, registerG)
is FiveRegisterInstruction -> {
when (registerCount) {
1 -> listOf(registerC)
2 -> listOf(registerC, registerD)
3 -> listOf(registerC, registerD, registerE)
4 -> listOf(registerC, registerD, registerE, registerF)
else -> listOf(registerC, registerD, registerE, registerF, registerG)
}
}
is ThreeRegisterInstruction -> listOf(registerA, registerB, registerC)
is TwoRegisterInstruction -> listOf(registerA, registerB)
is OneRegisterInstruction -> listOf(registerA)
@ -62,15 +70,15 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
}
// Register that is written to by an instruction.
fun Instruction.getRegisterWritten() = when (this) {
is ThreeRegisterInstruction -> registerA
is TwoRegisterInstruction -> registerA
is OneRegisterInstruction -> registerA
else -> throw IllegalStateException("Not a write instruction: $this")
fun Instruction.getWriteRegister() : Int {
// Two and three register instructions extend OneRegisterInstruction.
if (this is OneRegisterInstruction) return registerA
throw IllegalStateException("Not a write instruction: $this")
}
val writeOpcodes = EnumSet.of(
ARRAY_LENGTH,
INSTANCE_OF,
NEW_INSTANCE, NEW_ARRAY,
MOVE, MOVE_FROM16, MOVE_16, MOVE_WIDE, MOVE_WIDE_FROM16, MOVE_WIDE_16, MOVE_OBJECT,
MOVE_OBJECT_FROM16, MOVE_OBJECT_16, MOVE_RESULT, MOVE_RESULT_WIDE, MOVE_RESULT_OBJECT, MOVE_EXCEPTION,
@ -140,7 +148,7 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
return freeRegister
}
if (bestFreeRegisterFound != null) {
return bestFreeRegisterFound;
return bestFreeRegisterFound
}
// Somehow every method register was read from before any register was wrote to.
@ -151,14 +159,14 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
if (instruction.opcode in branchOpcodes) {
if (bestFreeRegisterFound != null) {
return bestFreeRegisterFound;
return bestFreeRegisterFound
}
// This method is simple and does not follow branching.
throw IllegalArgumentException("Encountered a branch statement before a free register could be found")
}
if (instruction.opcode in writeOpcodes) {
val writeRegister = instruction.getRegisterWritten()
val writeRegister = instruction.getWriteRegister()
if (writeRegister !in usedRegisters) {
// Verify the register is only used for write and not also as a parameter.