diff --git a/extensions/music/build.gradle.kts b/extensions/music/build.gradle.kts
new file mode 100644
index 000000000..f3c06ad73
--- /dev/null
+++ b/extensions/music/build.gradle.kts
@@ -0,0 +1 @@
+// Do not remove. Necessary for the extension plugin to be applied to the project.
diff --git a/extensions/music/src/main/AndroidManifest.xml b/extensions/music/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/music/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java
new file mode 100644
index 000000000..05c14bb30
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java
@@ -0,0 +1,27 @@
+package app.revanced.extension.music.spoof;
+
+/**
+ * @noinspection unused
+ */
+public class SpoofClientPatch {
+ private static final int CLIENT_TYPE_ID = 26;
+ private static final String CLIENT_VERSION = "6.21";
+ private static final String DEVICE_MODEL = "iPhone16,2";
+ private static final String OS_VERSION = "17.7.2.21H221";
+
+ public static int getClientId() {
+ return CLIENT_TYPE_ID;
+ }
+
+ public static String getClientVersion() {
+ return CLIENT_VERSION;
+ }
+
+ public static String getClientModel() {
+ return DEVICE_MODEL;
+ }
+
+ public static String getOsVersion() {
+ return OS_VERSION;
+ }
+}
\ No newline at end of file
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 53a61711c..c76a2d38f 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -324,8 +324,12 @@ public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatchKt {
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatchKt {
- public static final fun getSpoofVideoStreamsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+public final class app/revanced/patches/music/misc/spoof/SpoofClientPatchKt {
+ public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatchKt {
+ public static final fun getUserAgentClientSpoofPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/myexpenses/misc/pro/UnlockProPatchKt {
@@ -766,6 +770,10 @@ public final class app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch
public static synthetic fun spoofVideoStreamsPatch$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatchKt {
+ public static final fun userAgentClientSpoofPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatchKt {
public static final fun getRemoveFileSizeLimitPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt
index 5b25a006a..9351b600e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt
@@ -4,5 +4,6 @@ import app.revanced.patches.music.misc.extension.hooks.applicationInitHook
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch(
+ "music",
applicationInitHook,
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt
index 0fa223b23..2cb49fd5b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt
@@ -4,7 +4,7 @@ import app.revanced.patcher.patch.Option
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME
-import app.revanced.patches.music.misc.spoof.spoofVideoStreamsPatch
+import app.revanced.patches.music.misc.spoof.spoofClientPatch
import app.revanced.patches.shared.castContextFetchFingerprint
import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch
import app.revanced.patches.shared.primeMethodFingerprint
@@ -21,7 +21,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch(
extensionPatch = sharedExtensionPatch,
gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch,
) {
- dependsOn(spoofVideoStreamsPatch)
+ dependsOn(spoofClientPatch)
compatibleWith(MUSIC_PACKAGE_NAME)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt
new file mode 100644
index 000000000..abf19cc95
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt
@@ -0,0 +1,39 @@
+package app.revanced.patches.music.misc.spoof
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val playerRequestConstructorFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
+ strings("player")
+}
+
+/**
+ * Matches using the class found in [playerRequestConstructorFingerprint].
+ */
+internal val createPlayerRequestBodyFingerprint = fingerprint {
+ parameters("L")
+ returns("V")
+ opcodes(
+ Opcode.CHECK_CAST,
+ Opcode.IGET,
+ Opcode.AND_INT_LIT16,
+ )
+ strings("ms")
+}
+
+/**
+ * Used to get a reference to other clientInfo fields.
+ */
+internal val setClientInfoFieldsFingerprint = fingerprint {
+ returns("L")
+ strings("Google Inc.")
+}
+
+/**
+ * Used to get a reference to the clientInfo and clientInfo.clientVersion field.
+ */
+internal val setClientInfoClientVersionFingerprint = fingerprint {
+ strings("10.29")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt
new file mode 100644
index 000000000..a8f8933ac
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt
@@ -0,0 +1,105 @@
+package app.revanced.patches.music.misc.spoof
+
+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.instructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
+
+internal const val EXTENSION_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/music/spoof/SpoofClientPatch;"
+
+// TODO: Replace this patch with spoofVideoStreamsPatch once possible.
+val spoofClientPatch = bytecodePatch(
+ name = "Spoof client",
+ description = "Spoofs the client to fix playback.",
+) {
+ compatibleWith("com.google.android.apps.youtube.music")
+
+ dependsOn(
+ sharedExtensionPatch,
+ // TODO: Add settingsPatch
+ userAgentClientSpoofPatch,
+ )
+
+ execute {
+ val playerRequestClass = playerRequestConstructorFingerprint.classDef
+
+ val createPlayerRequestBodyMatch = createPlayerRequestBodyFingerprint.match(playerRequestClass)
+
+ val clientInfoContainerClass = createPlayerRequestBodyMatch.method
+ .getInstruction(createPlayerRequestBodyMatch.patternMatch!!.startIndex)
+ .getReference()!!.type
+
+ val clientInfoField = setClientInfoClientVersionFingerprint.method.instructions.first {
+ it.opcode == Opcode.IPUT_OBJECT && it.getReference()!!.definingClass == clientInfoContainerClass
+ }.getReference()!!
+
+ val setClientInfoFieldInstructions = setClientInfoFieldsFingerprint.method.instructions.filter {
+ (it.opcode == Opcode.IPUT_OBJECT || it.opcode == Opcode.IPUT) &&
+ it.getReference()!!.definingClass == clientInfoField.type
+ }.map { it.getReference()!! }
+
+ // Offsets are known for the fields in the clientInfo object.
+ val clientIdField = setClientInfoFieldInstructions[0]
+ val clientModelField = setClientInfoFieldInstructions[5]
+ val osVersionField = setClientInfoFieldInstructions[7]
+ val clientVersionField = setClientInfoClientVersionFingerprint.method
+ .getInstruction(setClientInfoClientVersionFingerprint.stringMatches!!.first().index + 1)
+ .getReference()
+
+ // Helper method to spoof the client info.
+ val spoofClientInfoMethod = ImmutableMethod(
+ playerRequestClass.type,
+ "spoofClientInfo",
+ listOf(ImmutableMethodParameter(clientInfoContainerClass, null, null)),
+ "V",
+ AccessFlags.PRIVATE.value or AccessFlags.STATIC.value,
+ null,
+ null,
+ MutableMethodImplementation(3),
+ ).toMutable().also(playerRequestClass.methods::add).apply {
+ addInstructions(
+ """
+ iget-object v0, p0, $clientInfoField
+
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getClientId()I
+ move-result v1
+ iput v1, v0, $clientIdField
+
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getClientModel()Ljava/lang/String;
+ move-result-object v1
+ iput-object v1, v0, $clientModelField
+
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion()Ljava/lang/String;
+ move-result-object v1
+ iput-object v1, v0, $clientVersionField
+
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion()Ljava/lang/String;
+ move-result-object v1
+ iput-object v1, v0, $osVersionField
+
+ return-void
+ """,
+ )
+ }
+
+ createPlayerRequestBodyMatch.method.apply {
+ val checkCastIndex = createPlayerRequestBodyMatch.patternMatch!!.startIndex
+ val clientInfoContainerRegister = getInstruction(checkCastIndex).registerA
+
+ addInstruction(checkCastIndex + 1, "invoke-static {v$clientInfoContainerRegister}, $spoofClientInfoMethod")
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
deleted file mode 100644
index 21eb32156..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package app.revanced.patches.music.misc.spoof
-
-import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch
-
-val spoofVideoStreamsPatch = spoofVideoStreamsPatch({
- compatibleWith("com.google.android.apps.youtube.music")
-})
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt
new file mode 100644
index 000000000..4afee1080
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt
@@ -0,0 +1,5 @@
+package app.revanced.patches.music.misc.spoof
+
+import app.revanced.patches.shared.misc.spoof.userAgentClientSpoofPatch
+
+val userAgentClientSpoofPatch = userAgentClientSpoofPatch("com.google.android.apps.youtube.music")
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt
new file mode 100644
index 000000000..f6ca942ad
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt
@@ -0,0 +1,81 @@
+package app.revanced.patches.shared.misc.spoof
+
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patches.all.misc.transformation.IMethodCall
+import app.revanced.patches.all.misc.transformation.filterMapInstruction35c
+import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.StringReference
+
+private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE =
+ "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;"
+
+fun userAgentClientSpoofPatch(originalPackageName: String) = transformInstructionsPatch(
+ filterMap = { classDef, _, instruction, instructionIndex ->
+ filterMapInstruction35c(
+ "Lapp/revanced/extension",
+ classDef,
+ instruction,
+ instructionIndex,
+ )
+ },
+ transform = transform@{ mutableMethod, entry ->
+ val (_, _, instructionIndex) = entry
+
+ // Replace the result of context.getPackageName(), if it is used in a user agent string.
+ mutableMethod.apply {
+ // After context.getPackageName() the result is moved to a register.
+ val targetRegister = (
+ getInstruction(instructionIndex + 1)
+ as? OneRegisterInstruction ?: return@transform
+ ).registerA
+
+ // IndexOutOfBoundsException is technically possible here,
+ // but no such occurrences are present in the app.
+ val referee = getInstruction(instructionIndex + 2).getReference()?.toString()
+
+ // Only replace string builder usage.
+ if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) {
+ return@transform
+ }
+
+ // Do not change the package name in methods that use resources, or for methods that use GmsCore.
+ // Changing these package names will result in playback limitations,
+ // particularly Android VR background audio only playback.
+ val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction {
+ val reference = getReference()
+ opcode == Opcode.CONST_STRING &&
+ (reference?.string == "android.resource://" || reference?.string == "gcore_")
+ }
+ if (resourceOrGmsStringInstructionIndex >= 0) {
+ return@transform
+ }
+
+ // Overwrite the result of context.getPackageName() with the original package name.
+ replaceInstruction(
+ instructionIndex + 1,
+ "const-string v$targetRegister, \"$originalPackageName\"",
+ )
+ }
+ },
+)
+
+@Suppress("unused")
+private enum class MethodCall(
+ override val definedClassName: String,
+ override val methodName: String,
+ override val methodParams: Array,
+ override val returnType: String,
+) : IMethodCall {
+ GetPackageName(
+ "Landroid/content/Context;",
+ "getPackageName",
+ emptyArray(),
+ "Ljava/lang/String;",
+ ),
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt
index d9b659613..f881b24d1 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt
@@ -1,82 +1,5 @@
package app.revanced.patches.youtube.misc.spoof
-import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
-import app.revanced.patches.all.misc.transformation.IMethodCall
-import app.revanced.patches.all.misc.transformation.filterMapInstruction35c
-import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
-import app.revanced.util.getReference
-import app.revanced.util.indexOfFirstInstruction
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-import com.android.tools.smali.dexlib2.iface.reference.StringReference
+import app.revanced.patches.shared.misc.spoof.userAgentClientSpoofPatch
-private const val ORIGINAL_PACKAGE_NAME = "com.google.android.youtube"
-private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE =
- "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;"
-
-val userAgentClientSpoofPatch = transformInstructionsPatch(
- filterMap = { classDef, _, instruction, instructionIndex ->
- filterMapInstruction35c(
- "Lapp/revanced/extension",
- classDef,
- instruction,
- instructionIndex,
- )
- },
- transform = transform@{ mutableMethod, entry ->
- val (_, _, instructionIndex) = entry
-
- // Replace the result of context.getPackageName(), if it is used in a user agent string.
- mutableMethod.apply {
- // After context.getPackageName() the result is moved to a register.
- val targetRegister = (
- getInstruction(instructionIndex + 1)
- as? OneRegisterInstruction ?: return@transform
- ).registerA
-
- // IndexOutOfBoundsException is technically possible here,
- // but no such occurrences are present in the app.
- val referee = getInstruction(instructionIndex + 2).getReference()?.toString()
-
- // Only replace string builder usage.
- if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) {
- return@transform
- }
-
- // Do not change the package name in methods that use resources, or for methods that use GmsCore.
- // Changing these package names will result in playback limitations,
- // particularly Android VR background audio only playback.
- val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction {
- val reference = getReference()
- opcode == Opcode.CONST_STRING &&
- (reference?.string == "android.resource://" || reference?.string == "gcore_")
- }
- if (resourceOrGmsStringInstructionIndex >= 0) {
- return@transform
- }
-
- // Overwrite the result of context.getPackageName() with the original package name.
- replaceInstruction(
- instructionIndex + 1,
- "const-string v$targetRegister, \"$ORIGINAL_PACKAGE_NAME\"",
- )
- }
- },
-)
-
-@Suppress("unused")
-private enum class MethodCall(
- override val definedClassName: String,
- override val methodName: String,
- override val methodParams: Array,
- override val returnType: String,
-) : IMethodCall {
- GetPackageName(
- "Landroid/content/Context;",
- "getPackageName",
- emptyArray(),
- "Ljava/lang/String;",
- ),
-}
+val userAgentClientSpoofPatch = userAgentClientSpoofPatch("com.google.android.youtube")