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 9d5907811..f9371db44 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 @@ -130,6 +130,7 @@ public final class UnlockPremiumPatch { /** * Injection point. Remove ads sections from home. + * Depends on patching protobuffer list remove method. */ public static void removeHomeSections(List
sections) { try { 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 c6fe5fcec..28098f44e 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 @@ -2,8 +2,12 @@ package app.revanced.patches.spotify.misc import app.revanced.patcher.fingerprint 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.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference internal val accountAttributeFingerprint = fingerprint { custom { _, classDef -> @@ -15,7 +19,7 @@ internal val accountAttributeFingerprint = fingerprint { } } -internal val productStateProtoFingerprint = fingerprint { +internal val productStateProtoGetMapFingerprint = fingerprint { returns("Ljava/util/Map;") custom { _, classDef -> 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 { 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;") } +internal val protobufListRemoveFingerprint = fingerprint { + custom { method, _ -> method.name == "remove" } } + +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()?.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()?.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) 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 74eaa578c..90bfc1694 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 @@ -4,21 +4,25 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 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.replaceInstruction -import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.PatchException 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.sharedExtensionPatch import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow 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.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.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference import java.util.logging.Logger private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;" @@ -41,14 +45,18 @@ val unlockPremiumPatch = bytecodePatch( ) execute { - // Make _value accessible so that it can be overridden in the extension. - accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply { - // Add public flag and remove private. - accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv()) + fun MutableClass.publicizeField(fieldName: String) { + fields.first { it.name == fieldName }.apply { + // Add public and remove private flag. + 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. - productStateProtoFingerprint.method.apply { + productStateProtoGetMapFingerprint.method.apply { val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) val attributesMapRegister = getInstruction(getAttributesMapIndex).registerA @@ -61,12 +69,12 @@ val unlockPremiumPatch = bytecodePatch( // Add the query parameter trackRows to show popular tracks in the artist page. - buildQueryParametersFingerprint.apply { - val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow( - stringMatches!!.first().index, Opcode.IF_EQZ + buildQueryParametersFingerprint.method.apply { + val addQueryParameterConditionIndex = indexOfFirstInstructionReversedOrThrow( + buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ ) - method.replaceInstruction(addQueryParameterConditionIndex, "nop") + replaceInstruction(addQueryParameterConditionIndex, "nop") } @@ -105,48 +113,39 @@ val unlockPremiumPatch = bytecodePatch( val shufflingContextCallIndex = indexOfFirstInstructionOrThrow { getReference()?.name == "shufflingContext" } + val boolRegister = getInstruction(shufflingContextCallIndex).registerD - val registerBool = getInstruction(shufflingContextCallIndex).registerD addInstruction( 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. - contextMenuExperimentsFingerprint.apply { - val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow( - stringMatches!!.first().index, Opcode.MOVE_RESULT + contextMenuExperimentsFingerprint.method.apply { + val moveIsEnabledIndex = indexOfFirstInstructionOrThrow( + contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT ) - val isUpsellEnabledRegister = method.getInstruction(moveIsEnabledIndex).registerA + val isUpsellEnabledRegister = getInstruction(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. - 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 protobufListClassDef = with(protobufListsFingerprint.originalMethod) { val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT) - getInstruction(emptyProtobufListGetIndex).getReference()!!.definingClass - } + // Find the protobuffer list class using the definingClass which contains the empty list static value. + val classType = getInstruction(emptyProtobufListGetIndex).getReference()!!.definingClass - val protobufListRemoveFingerprint = fingerprint { - custom { method, classDef -> - method.name == "remove" && classDef.type == protobufListClassName - } + classes.find { it.type == classType } ?: throw PatchException("Could not find protobuffer list class.") } // 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. - protobufListRemoveFingerprint.method.apply { + protobufListRemoveFingerprint.match(protobufListClassDef).method.apply { val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow { val reference = getReference() opcode == Opcode.INVOKE_VIRTUAL && @@ -157,8 +156,12 @@ val unlockPremiumPatch = bytecodePatch( 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. - homeStructureFingerprint.method.apply { + homeStructureGetSectionsFingerprint.method.apply { val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) val sectionsRegister = getInstruction(getSectionsIndex).registerA @@ -168,5 +171,56 @@ val unlockPremiumPatch = bytecodePatch( "$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()?.type?.endsWith(requestClassName) == true + } + + // The index of where the onErrorReturn method is called with the error static value. + val onErrorReturnCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) { + getReference()?.name == "onErrorReturn" + } + val onErrorReturnCallInstruction = getInstruction(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()!!.definingClass + // The index where the request is firstly called, before its result is mapped to other values. + val requestCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) { + getReference()?.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 + ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt index 2836a4872..58606777d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt @@ -60,4 +60,4 @@ val spoofPackageInfoPatch = bytecodePatch( // endregion } } -} \ No newline at end of file +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt index 14149ac7a..1afbcde45 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt @@ -14,4 +14,4 @@ internal val mainActivityOnCreateFingerprint = fingerprint { method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY || classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY) } -} \ No newline at end of file +} diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 7005ed47a..1e711271c 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -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.returnOpcodes 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.iface.Method @@ -169,6 +170,15 @@ internal val Instruction.isBranchInstruction: Boolean internal val Instruction.isReturnInstruction: Boolean 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]. *