Skip to content

Commit f4088ae

Browse files
Nuckyzanddea
authored andcommitted
fix(Spotify - Unlock Spotify Premium): Remove pop up premium ads
1 parent 569731c commit f4088ae

File tree

5 files changed

+141
-54
lines changed

5 files changed

+141
-54
lines changed

extensions/shared/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,15 @@ public static void overrideAttribute(Map<String, /*AccountAttribute*/ Object> at
121121
}
122122

123123
/**
124-
* Injection point. Remove station data from Google assistant URI.
124+
* Injection point. Remove station data from Google Assistant URI.
125125
*/
126126
public static String removeStationString(String spotifyUriOrUrl) {
127127
return spotifyUriOrUrl.replace("spotify:station:", "spotify:");
128128
}
129129

130130
/**
131131
* Injection point. Remove ads sections from home.
132+
* Depends on patching protobuf list remove method.
132133
*/
133134
public static void removeHomeSections(List<Section> sections) {
134135
try {

patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package app.revanced.patches.spotify.misc
22

33
import app.revanced.patcher.fingerprint
44
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
5+
import app.revanced.util.getReference
6+
import app.revanced.util.indexOfFirstInstruction
57
import com.android.tools.smali.dexlib2.AccessFlags
68
import com.android.tools.smali.dexlib2.Opcode
9+
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
10+
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
711

812
internal val accountAttributeFingerprint = fingerprint {
913
custom { _, classDef ->
@@ -15,7 +19,7 @@ internal val accountAttributeFingerprint = fingerprint {
1519
}
1620
}
1721

18-
internal val productStateProtoFingerprint = fingerprint {
22+
internal val productStateProtoGetMapFingerprint = fingerprint {
1923
returns("Ljava/util/Map;")
2024
custom { _, classDef ->
2125
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
@@ -56,16 +60,40 @@ internal val readPlayerOptionOverridesFingerprint = fingerprint {
5660
}
5761
}
5862

63+
internal val protobufListsFingerprint = fingerprint {
64+
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
65+
custom { method, _ -> method.name == "emptyProtobufList" }
66+
}
67+
68+
internal val protobufListRemoveFingerprint = fingerprint {
69+
custom { method, _ -> method.name == "remove" }
70+
}
71+
5972
internal val homeSectionFingerprint = fingerprint {
6073
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
6174
}
6275

63-
internal val protobufListsFingerprint = fingerprint {
64-
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
65-
custom { method, _ -> method.name == "emptyProtobufList" }
76+
internal val homeStructureGetSectionsFingerprint = fingerprint {
77+
custom { method, classDef ->
78+
classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
79+
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
80+
} >= 0
81+
}
6682
}
6783

68-
internal val homeStructureFingerprint = fingerprint {
69-
opcodes(Opcode.IGET_OBJECT, Opcode.RETURN_OBJECT)
70-
custom { _, classDef -> classDef.endsWith("homeapi/proto/HomeStructure;") }
84+
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
85+
returns("Ljava/lang/Object;")
86+
parameters("Ljava/lang/Object;")
87+
custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction {
88+
opcode == Opcode.NEW_INSTANCE && getReference<TypeReference>()?.type?.endsWith(className) == true
89+
} >= 0
90+
}
7191
}
92+
93+
internal const val PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME = "FetchMessageRequest;"
94+
internal val pendragonJsonFetchMessageRequestFingerprint =
95+
reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME)
96+
97+
internal const val PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME = "FetchMessageListRequest;"
98+
internal val pendragonProtoFetchMessageListRequestFingerprint =
99+
reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME)

patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
44
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
55
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
66
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
7+
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
78
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
8-
import app.revanced.patcher.fingerprint
9+
import app.revanced.patcher.patch.PatchException
910
import app.revanced.patcher.patch.bytecodePatch
11+
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
12+
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
1013
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
1114
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
1215
import app.revanced.util.getReference
1316
import app.revanced.util.indexOfFirstInstructionOrThrow
1417
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
15-
import com.android.tools.smali.dexlib2.AccessFlags
18+
import app.revanced.util.toPublicAccessFlags
1619
import com.android.tools.smali.dexlib2.Opcode
1720
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
1821
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
1922
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
2023
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
2124
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
25+
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
2226
import java.util.logging.Logger
2327

2428
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
@@ -33,14 +37,18 @@ val unlockPremiumPatch = bytecodePatch(
3337
dependsOn(sharedExtensionPatch)
3438

3539
execute {
36-
// Make _value accessible so that it can be overridden in the extension.
37-
accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply {
38-
// Add public flag and remove private.
39-
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
40+
fun MutableClass.publicizeField(fieldName: String) {
41+
fields.first { it.name == fieldName }.apply {
42+
// Add public and remove private flag.
43+
accessFlags = accessFlags.toPublicAccessFlags()
44+
}
4045
}
4146

47+
// Make _value accessible so that it can be overridden in the extension.
48+
accountAttributeFingerprint.classDef.publicizeField("value_")
49+
4250
// Override the attributes map in the getter method.
43-
productStateProtoFingerprint.method.apply {
51+
productStateProtoGetMapFingerprint.method.apply {
4452
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
4553
val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA
4654

@@ -53,12 +61,12 @@ val unlockPremiumPatch = bytecodePatch(
5361

5462

5563
// Add the query parameter trackRows to show popular tracks in the artist page.
56-
buildQueryParametersFingerprint.apply {
57-
val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow(
58-
stringMatches!!.first().index, Opcode.IF_EQZ
64+
buildQueryParametersFingerprint.method.apply {
65+
val addQueryParameterConditionIndex = indexOfFirstInstructionReversedOrThrow(
66+
buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ
5967
)
6068

61-
method.replaceInstruction(addQueryParameterConditionIndex, "nop")
69+
replaceInstruction(addQueryParameterConditionIndex, "nop")
6270
}
6371

6472

@@ -96,48 +104,39 @@ val unlockPremiumPatch = bytecodePatch(
96104
val shufflingContextCallIndex = indexOfFirstInstructionOrThrow {
97105
getReference<MethodReference>()?.name == "shufflingContext"
98106
}
107+
val boolRegister = getInstruction<FiveRegisterInstruction>(shufflingContextCallIndex).registerD
99108

100-
val registerBool = getInstruction<FiveRegisterInstruction>(shufflingContextCallIndex).registerD
101109
addInstruction(
102110
shufflingContextCallIndex,
103-
"sget-object v$registerBool, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
111+
"sget-object v$boolRegister, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
104112
)
105113
}
106114

107115

108116
// Disable the "Spotify Premium" upsell experiment in context menus.
109-
contextMenuExperimentsFingerprint.apply {
110-
val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow(
111-
stringMatches!!.first().index, Opcode.MOVE_RESULT
117+
contextMenuExperimentsFingerprint.method.apply {
118+
val moveIsEnabledIndex = indexOfFirstInstructionOrThrow(
119+
contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT
112120
)
113-
val isUpsellEnabledRegister = method.getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
121+
val isUpsellEnabledRegister = getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
114122

115-
method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
123+
replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
116124
}
117125

118126

119-
// Make featureTypeCase_ accessible so we can check the home section type in the extension.
120-
homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply {
121-
// Add public flag and remove private.
122-
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
123-
}
124-
125-
val protobufListClassName = with(protobufListsFingerprint.originalMethod) {
127+
val protobufListClassDef = with(protobufListsFingerprint.originalMethod) {
126128
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
127-
getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass
128-
}
129+
// Find the protobuffer list class using the definingClass which contains the empty list static value.
130+
val classType = getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass
129131

130-
val protobufListRemoveFingerprint = fingerprint {
131-
custom { method, classDef ->
132-
method.name == "remove" && classDef.type == protobufListClassName
133-
}
132+
classes.find { it.type == classType } ?: throw PatchException("Could not find protobuffer list class.")
134133
}
135134

136135
// Need to allow mutation of the list so the home ads sections can be removed.
137136
// Protobuffer list has an 'isMutable' boolean parameter that sets the mutability.
138137
// Forcing that always on breaks unrelated code in strange ways.
139138
// Instead, remove the method call that checks if the list is unmodifiable.
140-
protobufListRemoveFingerprint.method.apply {
139+
protobufListRemoveFingerprint.match(protobufListClassDef).method.apply {
141140
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
142141
val reference = getReference<MethodReference>()
143142
opcode == Opcode.INVOKE_VIRTUAL &&
@@ -148,8 +147,12 @@ val unlockPremiumPatch = bytecodePatch(
148147
removeInstruction(invokeThrowUnmodifiableIndex)
149148
}
150149

150+
151+
// Make featureTypeCase_ accessible so we can check the home section type in the extension.
152+
homeSectionFingerprint.classDef.publicizeField("featureTypeCase_")
153+
151154
// Remove ads sections from home.
152-
homeStructureFingerprint.method.apply {
155+
homeStructureGetSectionsFingerprint.method.apply {
153156
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
154157
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA
155158

@@ -159,5 +162,56 @@ val unlockPremiumPatch = bytecodePatch(
159162
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
160163
)
161164
}
165+
166+
167+
// Replace a fetch request that returns and maps Singles with their static onErrorReturn value.
168+
fun MutableMethod.replaceFetchRequestSingleWithError(requestClassName: String) {
169+
// The index of where the request class is being instantiated.
170+
val requestInstantiationIndex = indexOfFirstInstructionOrThrow {
171+
getReference<TypeReference>()?.type?.endsWith(requestClassName) == true
172+
}
173+
174+
// The index of where the onErrorReturn method is called with the error static value.
175+
val onErrorReturnCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
176+
getReference<MethodReference>()?.name == "onErrorReturn"
177+
}
178+
val onErrorReturnCallInstruction = getInstruction<FiveRegisterInstruction>(onErrorReturnCallIndex)
179+
180+
// The error static value register.
181+
val onErrorReturnValueRegister = onErrorReturnCallInstruction.registerD
182+
183+
// The index where the error static value starts being constructed.
184+
// Because the Singles are mapped, the error static value starts being constructed right after the first
185+
// move-result-object of the map call, before the onErrorReturn method call.
186+
val onErrorReturnValueConstructionIndex =
187+
indexOfFirstInstructionReversedOrThrow(onErrorReturnCallIndex, Opcode.MOVE_RESULT_OBJECT) + 1
188+
189+
val singleClassName = onErrorReturnCallInstruction.getReference<MethodReference>()!!.definingClass
190+
// The index where the request is firstly called, before its result is mapped to other values.
191+
val requestCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
192+
getReference<MethodReference>()?.returnType == singleClassName
193+
}
194+
195+
// Construct a new single with the error static value and return it.
196+
addInstructions(
197+
onErrorReturnCallIndex,
198+
"invoke-static { v$onErrorReturnValueRegister }, " +
199+
"$singleClassName->just(Ljava/lang/Object;)$singleClassName\n" +
200+
"move-result-object v$onErrorReturnValueRegister\n" +
201+
"return-object v$onErrorReturnValueRegister"
202+
)
203+
204+
// Remove every instruction from the request call to right before the error static value construction.
205+
val removeCount = onErrorReturnValueConstructionIndex - requestCallIndex
206+
removeInstructions(requestCallIndex, removeCount)
207+
}
208+
209+
// Remove pendragon (pop up ads) requests and return the errors instead.
210+
pendragonJsonFetchMessageRequestFingerprint.method.replaceFetchRequestSingleWithError(
211+
PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME
212+
)
213+
pendragonProtoFetchMessageListRequestFingerprint.method.replaceFetchRequestSingleWithError(
214+
PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME
215+
)
162216
}
163217
}

patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ val spoofPackageInfoPatch = bytecodePatch(
6060
// endregion
6161
}
6262
}
63-
}
63+
}

patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,7 @@ import com.android.tools.smali.dexlib2.Opcode
2525
import com.android.tools.smali.dexlib2.Opcode.*
2626
import com.android.tools.smali.dexlib2.iface.Method
2727
import com.android.tools.smali.dexlib2.iface.MethodParameter
28-
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
29-
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
30-
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
31-
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
32-
import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction
33-
import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction
34-
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
35-
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
28+
import com.android.tools.smali.dexlib2.iface.instruction.*
3629
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i
3730
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
3831
import com.android.tools.smali.dexlib2.iface.reference.Reference
@@ -41,7 +34,7 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableField
4134
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
4235
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
4336
import com.android.tools.smali.dexlib2.util.MethodUtil
44-
import java.util.EnumSet
37+
import java.util.*
4538

4639
const val REGISTER_TEMPLATE_REPLACEMENT: String = "REGISTER_INDEX"
4740

@@ -137,8 +130,10 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
137130

138131
// Somehow every method register was read from before any register was wrote to.
139132
// In practice this never occurs.
140-
throw IllegalArgumentException("Could not find a free register from startIndex: " +
141-
"$startIndex excluding: $registersToExclude")
133+
throw IllegalArgumentException(
134+
"Could not find a free register from startIndex: " +
135+
"$startIndex excluding: $registersToExclude"
136+
)
142137
}
143138

144139
if (instruction.opcode in branchOpcodes) {
@@ -172,6 +167,15 @@ internal fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude:
172167
throw IllegalStateException()
173168
}
174169

170+
/**
171+
* Adds public [AccessFlags] and removes private and protected flags (if present).
172+
*/
173+
internal fun Int.toPublicAccessFlags(): Int {
174+
return this.or(AccessFlags.PUBLIC.value)
175+
.and(AccessFlags.PROTECTED.value.inv())
176+
.and(AccessFlags.PRIVATE.value.inv())
177+
}
178+
175179
/**
176180
* Find the [MutableMethod] from a given [Method] in a [MutableClass].
177181
*
@@ -876,4 +880,4 @@ infix fun AccessFlags.or(other: AccessFlags) = value or other.value
876880
*
877881
* @param other The [AccessFlags] to perform the operation with.
878882
*/
879-
infix fun AccessFlags.or(other: Int) = value or other
883+
infix fun AccessFlags.or(other: Int) = value or other

0 commit comments

Comments
 (0)