diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java index 1410b308e..e58afe575 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java @@ -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 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 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
sections) { + try { + sections.removeIf(section -> REMOVED_HOME_SECTIONS.contains(section.featureTypeCase_)); + } catch (Exception ex) { + Logger.printException(() -> "Remove home sections failure", ex); + } + } } diff --git a/extensions/spotify/stub/src/main/java/com/spotify/home/evopage/homeapi/proto/Section.java b/extensions/spotify/stub/src/main/java/com/spotify/home/evopage/homeapi/proto/Section.java new file mode 100644 index 000000000..cc7da5f71 --- /dev/null +++ b/extensions/spotify/stub/src/main/java/com/spotify/home/evopage/homeapi/proto/Section.java @@ -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_; +} 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 876d390f2..b23f405f4 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,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;") } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt index b81699fdb..eee224f17 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt @@ -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(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(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()!!.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() + 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(getSectionsIndex).registerA + + addInstruction( + getSectionsIndex + 1, + "invoke-static { v$sectionsRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V" + ) + } } } diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index c32a0c072..4d3826865 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -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.