diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/ShortsTextComponentParentFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/ShortsTextComponentParentFingerprint.kt index 7179eb3ac..27e529824 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/ShortsTextComponentParentFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/ShortsTextComponentParentFingerprint.kt @@ -10,11 +10,12 @@ object ShortsTextComponentParentFingerprint : MethodFingerprint( access = AccessFlags.PROTECTED or AccessFlags.FINAL, parameters = listOf("L", "L"), opcodes = listOf( - Opcode.IF_EQZ, - Opcode.CONST_4, - Opcode.IF_EQ, - Opcode.CONST_4, - Opcode.IF_EQ, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.GOTO, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, Opcode.RETURN_VOID, Opcode.IGET_OBJECT, Opcode.CHECK_CAST, diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextComponentAtomicReferenceFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextComponentAtomicReferenceFingerprint.kt new file mode 100644 index 000000000..c555cb827 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextComponentAtomicReferenceFingerprint.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.youtube.layout.returnyoutubedislike.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 + +/** + * Resolves against the same method that [TextComponentContextFingerprint] resolves to. + */ +object TextComponentAtomicReferenceFingerprint : MethodFingerprint( + returnType = "L", + access = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.MOVE_OBJECT, // available unused register + Opcode.MOVE_OBJECT_FROM16, + Opcode.MOVE_OBJECT_FROM16, + Opcode.MOVE_FROM16, + Opcode.INVOKE_VIRTUAL, // CharSequence atomic reference + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.MOVE_OBJECT, // CharSequence reference, and control flow label. Insert code here. + Opcode.INVOKE_VIRTUAL_RANGE, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL_RANGE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.GOTO, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL_RANGE, + Opcode.MOVE_RESULT_OBJECT, + ) +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextComponentContextFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextComponentContextFingerprint.kt new file mode 100644 index 000000000..1a23e7046 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextComponentContextFingerprint.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.youtube.layout.returnyoutubedislike.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 + +/** + * Resolves against the same method that [TextComponentContextFingerprint] resolves to. + */ +object TextComponentContextFingerprint : MethodFingerprint( + returnType = "L", + access = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET_OBJECT, // conversion context field name + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_BOOLEAN, + Opcode.IGET, + Opcode.IGET, + Opcode.IGET, + ) +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextReferenceFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextReferenceFingerprint.kt deleted file mode 100644 index 763aef752..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextReferenceFingerprint.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.revanced.patches.youtube.layout.returnyoutubedislike.fingerprints - - -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import org.jf.dexlib2.Opcode - -object TextReferenceFingerprint : MethodFingerprint( - opcodes = listOf( - Opcode.INVOKE_STATIC_RANGE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_DIRECT, - Opcode.INVOKE_VIRTUAL - ) -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextReferenceParamFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextReferenceParamFingerprint.kt deleted file mode 100644 index f81420fcb..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/fingerprints/TextReferenceParamFingerprint.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.patches.youtube.layout.returnyoutubedislike.fingerprints - -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import org.jf.dexlib2.Opcode - -object TextReferenceParamFingerprint : MethodFingerprint( - opcodes = listOf( - Opcode.MOVE_OBJECT, - Opcode.MOVE_OBJECT_FROM16 // the first occurrence of this instruction uses the register for the text object - ) -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/patch/ReturnYouTubeDislikePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/patch/ReturnYouTubeDislikePatch.kt index b483afa1d..3796ee62b 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/patch/ReturnYouTubeDislikePatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/patch/ReturnYouTubeDislikePatch.kt @@ -9,6 +9,7 @@ import app.revanced.patcher.data.toMethodWalker import app.revanced.patcher.extensions.MethodFingerprintExtensions.name import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.instruction +import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve import app.revanced.patcher.patch.BytecodePatch @@ -25,8 +26,8 @@ import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch import app.revanced.patches.youtube.misc.playertype.patch.PlayerTypeHookPatch import app.revanced.patches.youtube.misc.video.videoid.patch.VideoIdPatch import org.jf.dexlib2.builder.instruction.BuilderInstruction35c -import org.jf.dexlib2.iface.instruction.FiveRegisterInstruction import org.jf.dexlib2.iface.instruction.OneRegisterInstruction +import org.jf.dexlib2.iface.instruction.ReferenceInstruction import org.jf.dexlib2.iface.instruction.TwoRegisterInstruction @Patch @@ -52,13 +53,13 @@ class ReturnYouTubeDislikePatch : BytecodePatch( ) ) { override fun execute(context: BytecodeContext): PatchResult { - // region Inject newVideoLoaded event handler + // region Inject newVideoLoaded event handler to update dislikes when a new video is loaded. VideoIdPatch.injectCall("$INTEGRATIONS_PATCH_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V") // endregion - // region Hook interaction + // region Hook like/dislike/remove like button clicks to send votes to the API. listOf( LikeFingerprint.toPatch(Vote.LIKE), @@ -78,39 +79,56 @@ class ReturnYouTubeDislikePatch : BytecodePatch( // endregion - // region Hook components + // region Hook creation of Spans and the cached lookup of them. - TextReferenceFingerprint.also { + // Alternatively the hook can be made at the creation of Spans in TextComponentSpec, + // And it works in all situations except it fails to update the Span when the user dislikes, + // since the underlying (likes only) text did not change. + // This hook handles all situations, as it's where the created Spans are stored and later reused. + TextComponentContextFingerprint.also { it.resolve( context, TextComponentConstructorFingerprint.result!!.classDef ) - }.result?.let { result -> - val moveTextRefParamInstructionIndex = TextReferenceParamFingerprint.also { - if (!TextReferenceParamFingerprint.resolve(context, result.method, result.classDef)) - return TextReferenceParamFingerprint.toErrorResult() - }.result!!.scanResult.patternScanResult!!.endIndex + }.result?.also { result -> + if (!TextComponentAtomicReferenceFingerprint.resolve(context, result.method, result.classDef)) + throw TextComponentAtomicReferenceFingerprint.toErrorResult() + }?.let { textComponentContextFingerprintResult -> + val conversionContextIndex = textComponentContextFingerprintResult + .scanResult.patternScanResult!!.startIndex + val atomicReferenceStartIndex = TextComponentAtomicReferenceFingerprint.result!! + .scanResult.patternScanResult!!.startIndex - result.mutableMethod.apply { - val insertIndex = result.scanResult.patternScanResult!!.endIndex + textComponentContextFingerprintResult.mutableMethod.apply { + // Get the conversion context obfuscated field name, and the registers for the AtomicReference and CharSequence + val conversionContextFieldName = + (instruction(conversionContextIndex) as ReferenceInstruction).reference.toString() + val contextRegister = // any free register + (instruction(atomicReferenceStartIndex) as TwoRegisterInstruction).registerB + val atomicReferenceRegister = + (instruction(atomicReferenceStartIndex + 4) as BuilderInstruction35c).registerC - val atomicReferenceInstruction = (instruction(insertIndex - 1) as FiveRegisterInstruction) - val conversionContextParam = atomicReferenceInstruction.registerC - val textRefParam = (instruction(moveTextRefParamInstructionIndex) as TwoRegisterInstruction).registerA - - // Overwritten after injected code, which is why it can be used. - val clobberRegister = atomicReferenceInstruction.registerD + val insertIndex = atomicReferenceStartIndex + 7 + val moveCharSequenceInstruction = instruction(insertIndex) as TwoRegisterInstruction + val charSequenceRegister = moveCharSequenceInstruction.registerB + // Insert as first instructions at the control flow label. + // Must replace the existing instruction to preserve the label, and then insert the remaining instructions. + replaceInstruction(insertIndex, "move-object/from16 v$contextRegister, p0") addInstructions( - insertIndex, - """ - # required instruction, otherwise register might be out of range - move-object/from16 v$clobberRegister, v$textRefParam - invoke-static {v$clobberRegister, v$conversionContextParam}, $ON_COMPONENT_CREATED_DESCRIPTOR + insertIndex + 1, """ + iget-object v$contextRegister, v$contextRegister, $conversionContextFieldName # copy obfuscated context field into free register + invoke-static {v$contextRegister, v$atomicReferenceRegister, v$charSequenceRegister}, $INTEGRATIONS_PATCH_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/util/concurrent/atomic/AtomicReference;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$charSequenceRegister + move-object v${moveCharSequenceInstruction.registerA}, v${moveCharSequenceInstruction.registerB} # original instruction at the insertion point """ ) } - } ?: return TextReferenceFingerprint.toErrorResult() + } ?: return TextComponentContextFingerprint.toErrorResult() + + // endregion + + // region Hook for Short videos. ShortsTextComponentParentFingerprint.result?.let { context @@ -123,22 +141,23 @@ class ReturnYouTubeDislikePatch : BytecodePatch( return PatchResultError("Method signature did not match: $this $parameterTypes") val insertIndex = implementation!!.instructions.size - 1 - val spannedParameterRegister = (instruction(insertIndex) as OneRegisterInstruction).registerA val parameter = (instruction(insertIndex - 2) as BuilderInstruction35c).reference if (!parameter.toString().endsWith("Landroid/text/Spanned;")) return PatchResultError("Method signature parameter did not match: $parameter") - addInstructions( - insertIndex, - """ - invoke-static {v$spannedParameterRegister}, $INTEGRATIONS_PATCH_CLASS_DESCRIPTOR->onShortsComponentCreated(Landroid/text/Spanned;)Landroid/text/Spanned; - move-result-object v$spannedParameterRegister - """ - ) + insertShorts(insertIndex, spannedParameterRegister) } } + + // Additional hook, called after user dislikes. + with(it.mutableMethod) { + val insertIndex = it.scanResult.patternScanResult!!.startIndex + 2 + val overwriteRegister = (implementation!!.instructions.elementAt(insertIndex - 1) + as OneRegisterInstruction).registerA + insertShorts(insertIndex, overwriteRegister) + } } ?: return ShortsTextComponentParentFingerprint.toErrorResult() // endregion @@ -150,17 +169,21 @@ class ReturnYouTubeDislikePatch : BytecodePatch( const val INTEGRATIONS_PATCH_CLASS_DESCRIPTOR = "Lapp/revanced/integrations/patches/ReturnYouTubeDislikePatch;" - const val ON_COMPONENT_CREATED_DESCRIPTOR = - "$INTEGRATIONS_PATCH_CLASS_DESCRIPTOR->onComponentCreated(Ljava/lang/Object;Ljava/util/concurrent/atomic/AtomicReference;)V" - private fun MethodFingerprint.toPatch(voteKind: Vote) = VotePatch(this, voteKind) - } + private data class VotePatch(val fingerprint: MethodFingerprint, val voteKind: Vote) + private enum class Vote(val value: Int) { + LIKE(1), + DISLIKE(-1), + REMOVE_LIKE(0) + } - private data class VotePatch(val fingerprint: MethodFingerprint, val voteKind: Vote) - - private enum class Vote(val value: Int) { - LIKE(1), - DISLIKE(-1), - REMOVE_LIKE(0) + private fun MutableMethod.insertShorts(index: Int, register: Int) { + addInstructions( + index, """ + invoke-static {v$register}, $INTEGRATIONS_PATCH_CLASS_DESCRIPTOR->onShortsComponentCreated(Landroid/text/Spanned;)Landroid/text/Spanned; + move-result-object v$register + """ + ) + } } }