From b1e80566b8a365abdd7b74817800df5163d46ebe Mon Sep 17 00:00:00 2001 From: inotia00 Date: Thu, 5 Jan 2023 12:11:56 +0900 Subject: [PATCH] add `enable-force-shuffle` patch --- .../MusicPlaybackControlsFingerprint.kt | 20 ++ .../fingerprints/ShuffleClassFingerprint.kt | 29 +++ .../ShuffleClassReferenceFingerprint.kt | 21 +++ .../shuffle/patch/EnforceShufflePatch.kt | 174 ++++++++++++++++++ .../revanced/shared/extensions/Extensions.kt | 12 ++ .../music/settings/host/values/strings.xml | 3 + .../settings/host/xml/settings_headers.xml | 1 + .../host/xml/settings_prefs_compat.xml | 1 + 8 files changed, 261 insertions(+) create mode 100644 src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/MusicPlaybackControlsFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassReferenceFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/music/layout/shuffle/patch/EnforceShufflePatch.kt diff --git a/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/MusicPlaybackControlsFingerprint.kt b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/MusicPlaybackControlsFingerprint.kt new file mode 100644 index 000000000..496e7f3b4 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/MusicPlaybackControlsFingerprint.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.music.layout.shuffle.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Opcode + +object MusicPlaybackControlsFingerprint : MethodFingerprint( + returnType = "V", + access = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z"), + opcodes = listOf( + Opcode.IPUT_BOOLEAN, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { methodDef -> + methodDef.definingClass.endsWith("MusicPlaybackControls;") + } +) diff --git a/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassFingerprint.kt b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassFingerprint.kt new file mode 100644 index 000000000..724ab4a75 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassFingerprint.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.music.layout.shuffle.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import app.revanced.patches.music.misc.resourceid.patch.SharedResourcdIdPatch +import org.jf.dexlib2.iface.instruction.WideLiteralInstruction +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Opcode + +object ShuffleClassFingerprint : MethodFingerprint( + returnType = "V", + access = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IPUT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { methodDef -> + methodDef.name == "" && + methodDef.implementation?.instructions?.any { instruction -> + instruction.opcode.ordinal == Opcode.CONST.ordinal && + (instruction as? WideLiteralInstruction)?.wideLiteral == SharedResourcdIdPatch.disabledIconLabelId + } == true + } +) + diff --git a/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassReferenceFingerprint.kt b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassReferenceFingerprint.kt new file mode 100644 index 000000000..2a4263660 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/fingerprints/ShuffleClassReferenceFingerprint.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.music.layout.shuffle.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Opcode + +object ShuffleClassReferenceFingerprint : MethodFingerprint( + returnType = "V", + access = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf(), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.IGET_OBJECT + ), + strings = listOf("Unknown shuffle mode") +) + diff --git a/src/main/kotlin/app/revanced/patches/music/layout/shuffle/patch/EnforceShufflePatch.kt b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/patch/EnforceShufflePatch.kt new file mode 100644 index 000000000..b847eeaa5 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/music/layout/shuffle/patch/EnforceShufflePatch.kt @@ -0,0 +1,174 @@ +package app.revanced.patches.music.layout.shuffle.patch + +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.* +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.patch.annotations.DependsOn +import app.revanced.patcher.patch.annotations.Patch +import app.revanced.patcher.util.TypeUtil.traverseClassHierarchy +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.util.smali.toInstructions +import app.revanced.patches.music.layout.shuffle.fingerprints.MusicPlaybackControlsFingerprint +import app.revanced.patches.music.layout.shuffle.fingerprints.ShuffleClassFingerprint +import app.revanced.patches.music.layout.shuffle.fingerprints.ShuffleClassReferenceFingerprint +import app.revanced.patches.music.misc.integrations.patch.MusicIntegrationsPatch +import app.revanced.patches.music.misc.resourceid.patch.SharedResourcdIdPatch +import app.revanced.patches.music.misc.settings.patch.MusicSettingsPatch +import app.revanced.shared.annotation.YouTubeMusicCompatibility +import app.revanced.shared.extensions.transformFields +import app.revanced.shared.util.integrations.Constants.MUSIC_SETTINGS_PATH +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.dexbacked.reference.DexBackedMethodReference +import org.jf.dexlib2.iface.instruction.ReferenceInstruction +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.iface.reference.MethodReference +import org.jf.dexlib2.immutable.ImmutableField +import org.jf.dexlib2.immutable.ImmutableMethod +import org.jf.dexlib2.immutable.ImmutableMethodImplementation +import org.jf.dexlib2.immutable.ImmutableMethodParameter + +@Patch +@Name("enable-force-shuffle") +@Description("Enable force shuffle even if another track is played.") +@DependsOn([MusicIntegrationsPatch::class, MusicSettingsPatch::class, SharedResourcdIdPatch::class]) +@YouTubeMusicCompatibility +@Version("0.0.1") +class EnforceShufflePatch : BytecodePatch( + listOf( + MusicPlaybackControlsFingerprint, + ShuffleClassFingerprint, + ShuffleClassReferenceFingerprint + ) +) { + override fun execute(context: BytecodeContext): PatchResult { + + with(ShuffleClassReferenceFingerprint.result!!) { + val startIndex = scanResult.patternScanResult!!.startIndex + val endIndex = scanResult.patternScanResult!!.endIndex + val referenceInstructions = mutableMethod.implementation!!.instructions + + SHUFFLE_CLASS = classDef.type + firstRef = (referenceInstructions.elementAt(startIndex) as ReferenceInstruction).reference as FieldReference + secondRef = (referenceInstructions.elementAt(startIndex + 1) as ReferenceInstruction).reference as DexBackedMethodReference + thirdRef = (referenceInstructions.elementAt(endIndex) as ReferenceInstruction).reference as FieldReference + + referenceInstructions.filter { instruction -> + val fieldReference = (instruction as? ReferenceInstruction)?.reference as? FieldReference + fieldReference?.let { it.type == "Landroid/widget/ImageView;" } == true + }.forEach { instruction -> + fourthRef = (instruction as ReferenceInstruction).reference as FieldReference + } + } + + with(ShuffleClassFingerprint.result!!) { + + val insertIndex = scanResult.patternScanResult!!.endIndex + mutableMethod.addInstruction( + insertIndex, + "sput-object p0, $MUSIC_PLAYBACK_CONTROLS_CLASS_DESCRIPTOR->shuffleclass:$SHUFFLE_CLASS" + ) + + context.traverseClassHierarchy(mutableClass) { + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL + transformFields { + ImmutableField( + definingClass, + name, + type, + AccessFlags.PUBLIC or AccessFlags.PUBLIC, + null, + null, + null + ).toMutable() + } + } + } + + with(MusicPlaybackControlsFingerprint.result!!) { + + val referenceInstructions = mutableMethod.implementation!!.instructions + + fifthRef = (referenceInstructions.elementAt(0) as ReferenceInstruction).reference as FieldReference + sixthRef = (referenceInstructions.elementAt(1) as ReferenceInstruction).reference as DexBackedMethodReference + + mutableClass.staticFields.add( + ImmutableField( + mutableMethod.definingClass, + "shuffleclass", + "$SHUFFLE_CLASS", + AccessFlags.PUBLIC or AccessFlags.STATIC, + null, + null, + null + ).toMutable() + ) + + mutableMethod.addInstructions( + 0, + """ + invoke-virtual {v0, v1}, $MUSIC_PLAYBACK_CONTROLS_CLASS_DESCRIPTOR->buttonHook(Z)V + return-void + """ + ) + + mutableClass.methods.add( + ImmutableMethod( + classDef.type, + "buttonHook", + listOf(ImmutableMethodParameter("Z", null, null)), + "V", + AccessFlags.PUBLIC or AccessFlags.FINAL, + null, + null, + ImmutableMethodImplementation( + 5, """ + invoke-static {}, $MUSIC_SETTINGS_PATH->enableForceShuffle()Z + move-result v0 + if-eqz v0, :cond_0 + new-instance v0, $SHUFFLE_CLASS + sget-object v0, $MUSIC_PLAYBACK_CONTROLS_CLASS_DESCRIPTOR->shuffleclass:$SHUFFLE_CLASS + iget-object v1, v0, $SHUFFLE_CLASS->${firstRef.name}:${firstRef.type} + invoke-interface {v1}, $secondRef + move-result-object v1 + check-cast v1, ${thirdRef.definingClass} + iget-object v1, v1, ${thirdRef.definingClass}->${thirdRef.name}:${thirdRef.type} + invoke-virtual {v1}, ${thirdRef.type}->ordinal()I + move-result v1 + iget-object v2, v0, $SHUFFLE_CLASS->${fourthRef.name}:Landroid/widget/ImageView; + invoke-virtual {v2}, Landroid/widget/ImageView;->performClick()Z + if-eqz v1, :cond_0 + invoke-virtual {v2}, Landroid/widget/ImageView;->performClick()Z + :cond_0 + iput-boolean v4, v3, $MUSIC_PLAYBACK_CONTROLS_CLASS_DESCRIPTOR->${fifthRef.name}:${fifthRef.type} + invoke-virtual {v3}, $sixthRef + return-void + """.toInstructions(), null, null + ) + ).toMutable() + ) + } + + return PatchResultSuccess() + } + + companion object { + const val MUSIC_PLAYBACK_CONTROLS_CLASS_DESCRIPTOR = + "Lcom/google/android/apps/youtube/music/watchpage/MusicPlaybackControls;" + + private lateinit var SHUFFLE_CLASS: String + + private lateinit var firstRef: FieldReference + private lateinit var secondRef: DexBackedMethodReference + private lateinit var thirdRef: FieldReference + private lateinit var fourthRef: FieldReference + + private lateinit var fifthRef: FieldReference + private lateinit var sixthRef: DexBackedMethodReference + } +} diff --git a/src/main/kotlin/app/revanced/shared/extensions/Extensions.kt b/src/main/kotlin/app/revanced/shared/extensions/Extensions.kt index 8fb473b25..4605d7e4f 100644 --- a/src/main/kotlin/app/revanced/shared/extensions/Extensions.kt +++ b/src/main/kotlin/app/revanced/shared/extensions/Extensions.kt @@ -4,6 +4,7 @@ import app.revanced.patcher.extensions.MethodFingerprintExtensions.name import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.patch.PatchResultError import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableField import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.smali.toInstruction import app.revanced.shared.util.integrations.Constants.INTEGRATIONS_PATH @@ -65,6 +66,17 @@ fun MutableClass.transformMethods(transform: MutableMethod.() -> MutableMethod) methods.addAll(transformedMethods) } +/** + * apply a transform to all fields of the class + * + * @param transform the transformation function. original field goes in, transformed field goes out + */ +fun MutableClass.transformFields(transform: MutableField.() -> MutableField) { + val transformedFields = fields.map { it.transform() } + fields.clear() + fields.addAll(transformedFields) +} + internal fun Node.doRecursively(action: (Node) -> Unit) { action(this) for (i in 0 until this.childNodes.length) this.childNodes.item(i).doRecursively(action) diff --git a/src/main/resources/music/settings/host/values/strings.xml b/src/main/resources/music/settings/host/values/strings.xml index d8f3f9dfa..0747e716b 100644 --- a/src/main/resources/music/settings/host/values/strings.xml +++ b/src/main/resources/music/settings/host/values/strings.xml @@ -10,6 +10,9 @@ Enable color match Players Keep player permanently minimized even if another track is played. Enable force minimized player + "Enable force shuffle even if another track is played. +(Not available in Canada)" + Enable force shuffle "Enable 250/251 opus codec when playing audio. (requires an app restart)" Enable opus codec diff --git a/src/main/resources/music/settings/host/xml/settings_headers.xml b/src/main/resources/music/settings/host/xml/settings_headers.xml index b8a90fadb..6a29eca85 100644 --- a/src/main/resources/music/settings/host/xml/settings_headers.xml +++ b/src/main/resources/music/settings/host/xml/settings_headers.xml @@ -13,6 +13,7 @@ + diff --git a/src/main/resources/music/settings/host/xml/settings_prefs_compat.xml b/src/main/resources/music/settings/host/xml/settings_prefs_compat.xml index cf1604387..fc27ac159 100644 --- a/src/main/resources/music/settings/host/xml/settings_prefs_compat.xml +++ b/src/main/resources/music/settings/host/xml/settings_prefs_compat.xml @@ -12,6 +12,7 @@ +