mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-04-29 22:24:27 +02:00
fix(Spotify): Remove ads sections from home (#4722)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
parent
eaee621831
commit
0b9a5e7f89
@ -4,6 +4,7 @@ import static java.lang.Boolean.FALSE;
|
|||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
import com.spotify.remoteconfig.internal.AccountAttribute;
|
import com.spotify.remoteconfig.internal.AccountAttribute;
|
||||||
|
import com.spotify.home.evopage.homeapi.proto.Section;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -69,8 +70,13 @@ public final class UnlockPremiumPatch {
|
|||||||
new OverrideAttribute("tablet-free", FALSE, false)
|
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) {
|
public static void overrideAttribute(Map<String, AccountAttribute> attributes) {
|
||||||
try {
|
try {
|
||||||
@ -78,7 +84,7 @@ public final class UnlockPremiumPatch {
|
|||||||
var attribute = attributes.get(override.key);
|
var 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");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
attribute.value_ = override.overrideValue;
|
attribute.value_ = override.overrideValue;
|
||||||
@ -88,4 +94,15 @@ public final class UnlockPremiumPatch {
|
|||||||
Logger.printException(() -> "overrideAttribute failure", ex);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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_;
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
package app.revanced.patches.spotify.misc
|
package app.revanced.patches.spotify.misc
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
import app.revanced.patcher.fingerprint
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
internal val accountAttributeFingerprint = fingerprint {
|
internal val accountAttributeFingerprint = fingerprint {
|
||||||
custom { _, c -> c.endsWith("internal/AccountAttribute;") }
|
custom { _, classDef -> classDef.endsWith("internal/AccountAttribute;") }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val productStateProtoFingerprint = fingerprint {
|
internal val productStateProtoFingerprint = fingerprint {
|
||||||
returns("Ljava/util/Map;")
|
returns("Ljava/util/Map;")
|
||||||
custom { _, classDef ->
|
custom { _, classDef -> classDef.endsWith("ProductStateProto;") }
|
||||||
classDef.endsWith("ProductStateProto;")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val buildQueryParametersFingerprint = fingerprint {
|
internal val buildQueryParametersFingerprint = fingerprint {
|
||||||
@ -21,3 +21,17 @@ internal val contextMenuExperimentsFingerprint = fingerprint {
|
|||||||
parameters("L")
|
parameters("L")
|
||||||
strings("remove_ads_upsell_enabled")
|
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;") }
|
||||||
|
}
|
||||||
|
@ -2,11 +2,18 @@ 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.getInstruction
|
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.extensions.InstructionExtensions.replaceInstruction
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
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.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.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;"
|
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.
|
// Override the attributes map in the getter method.
|
||||||
val attributesMapRegister = 0
|
with(productStateProtoFingerprint.method) {
|
||||||
val instantiateUnmodifiableMapIndex = 1
|
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
|
||||||
productStateProtoFingerprint.method.addInstruction(
|
val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA
|
||||||
instantiateUnmodifiableMapIndex,
|
|
||||||
"invoke-static { v$attributesMapRegister }," +
|
addInstruction(
|
||||||
"$EXTENSION_CLASS_DESCRIPTOR->overrideAttribute(Ljava/util/Map;)V",
|
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.
|
// Add the query parameter trackRows to show popular tracks in the artist page.
|
||||||
val addQueryParameterIndex = buildQueryParametersFingerprint.stringMatches!!.first().index - 1
|
with(buildQueryParametersFingerprint) {
|
||||||
buildQueryParametersFingerprint.method.replaceInstruction(addQueryParameterIndex, "nop")
|
val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow(
|
||||||
|
stringMatches!!.first().index, Opcode.IF_EQZ
|
||||||
|
)
|
||||||
|
method.replaceInstruction(addQueryParameterConditionIndex, "nop")
|
||||||
|
}
|
||||||
|
|
||||||
// Disable the "Spotify Premium" upsell experiment in context menus.
|
// Disable the "Spotify Premium" upsell experiment in context menus.
|
||||||
with(contextMenuExperimentsFingerprint) {
|
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
|
val isUpsellEnabledRegister = method.getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
|
||||||
method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,15 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
|
|||||||
|
|
||||||
// All registers used by an instruction.
|
// All registers used by an instruction.
|
||||||
fun Instruction.getRegistersUsed() = when (this) {
|
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 ThreeRegisterInstruction -> listOf(registerA, registerB, registerC)
|
||||||
is TwoRegisterInstruction -> listOf(registerA, registerB)
|
is TwoRegisterInstruction -> listOf(registerA, registerB)
|
||||||
is OneRegisterInstruction -> listOf(registerA)
|
is OneRegisterInstruction -> listOf(registerA)
|
||||||
@ -62,15 +70,15 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register that is written to by an instruction.
|
// Register that is written to by an instruction.
|
||||||
fun Instruction.getRegisterWritten() = when (this) {
|
fun Instruction.getWriteRegister() : Int {
|
||||||
is ThreeRegisterInstruction -> registerA
|
// Two and three register instructions extend OneRegisterInstruction.
|
||||||
is TwoRegisterInstruction -> registerA
|
if (this is OneRegisterInstruction) return registerA
|
||||||
is OneRegisterInstruction -> registerA
|
throw IllegalStateException("Not a write instruction: $this")
|
||||||
else -> throw IllegalStateException("Not a write instruction: $this")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val writeOpcodes = EnumSet.of(
|
val writeOpcodes = EnumSet.of(
|
||||||
ARRAY_LENGTH,
|
ARRAY_LENGTH,
|
||||||
|
INSTANCE_OF,
|
||||||
NEW_INSTANCE, NEW_ARRAY,
|
NEW_INSTANCE, NEW_ARRAY,
|
||||||
MOVE, MOVE_FROM16, MOVE_16, MOVE_WIDE, MOVE_WIDE_FROM16, MOVE_WIDE_16, MOVE_OBJECT,
|
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,
|
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
|
return freeRegister
|
||||||
}
|
}
|
||||||
if (bestFreeRegisterFound != null) {
|
if (bestFreeRegisterFound != null) {
|
||||||
return bestFreeRegisterFound;
|
return bestFreeRegisterFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Somehow every method register was read from before any register was wrote to.
|
// 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 (instruction.opcode in branchOpcodes) {
|
||||||
if (bestFreeRegisterFound != null) {
|
if (bestFreeRegisterFound != null) {
|
||||||
return bestFreeRegisterFound;
|
return bestFreeRegisterFound
|
||||||
}
|
}
|
||||||
// This method is simple and does not follow branching.
|
// This method is simple and does not follow branching.
|
||||||
throw IllegalArgumentException("Encountered a branch statement before a free register could be found")
|
throw IllegalArgumentException("Encountered a branch statement before a free register could be found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instruction.opcode in writeOpcodes) {
|
if (instruction.opcode in writeOpcodes) {
|
||||||
val writeRegister = instruction.getRegisterWritten()
|
val writeRegister = instruction.getWriteRegister()
|
||||||
|
|
||||||
if (writeRegister !in usedRegisters) {
|
if (writeRegister !in usedRegisters) {
|
||||||
// Verify the register is only used for write and not also as a parameter.
|
// Verify the register is only used for write and not also as a parameter.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user