diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsNavigationBarPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsNavigationBarPatch.kt index 9c99a158c2..76d52e95d3 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsNavigationBarPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsNavigationBarPatch.kt @@ -1,71 +1,83 @@ package app.revanced.patches.youtube.shorts.components import app.revanced.patcher.data.BytecodeContext -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.patch.BytecodePatch -import app.revanced.patches.youtube.shorts.components.fingerprints.BottomNavigationBarFingerprint -import app.revanced.patches.youtube.shorts.components.fingerprints.RenderBottomNavigationBarFingerprint -import app.revanced.patches.youtube.shorts.components.fingerprints.SetPivotBarFingerprint +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patches.youtube.shorts.components.fingerprints.BottomBarContainerHeightFingerprint +import app.revanced.patches.youtube.shorts.components.fingerprints.ReelWatchPagerFingerprint import app.revanced.patches.youtube.utils.integrations.Constants.SHORTS_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.navigation.fingerprints.InitializeButtonsFingerprint +import app.revanced.patches.youtube.utils.navigation.NavigationBarHookPatch +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.BottomBarContainer +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.ReelWatchPlayer +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.fingerprint.MultiMethodFingerprint import app.revanced.util.getReference -import app.revanced.util.getWalkerMethod import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstWideLiteralInstructionValue +import app.revanced.util.injectLiteralInstructionViewCall +import app.revanced.util.patch.MultiMethodBytecodePatch import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference -object ShortsNavigationBarPatch : BytecodePatch( - setOf( - BottomNavigationBarFingerprint, - InitializeButtonsFingerprint, - RenderBottomNavigationBarFingerprint - ) +/** + * Up to YouTube 19.28.42, there are two Methods with almost the same pattern. + * + * In certain YouTube versions, the hook should be done not on the first matching Method, but also on the last matching Method. + * + * 'Multiple fingerprint search' feature is not yet implemented in ReVanced Patcher, + * So I just implement it via [MultiMethodFingerprint]. + * + * Related Issues: + * https://github.com/ReVanced/revanced-patcher/issues/74 + * https://github.com/ReVanced/revanced-patcher/issues/308 + */ +@Patch(dependencies = [NavigationBarHookPatch::class]) +object ShortsNavigationBarPatch : MultiMethodBytecodePatch( + fingerprints = setOf(ReelWatchPagerFingerprint), + multiFingerprints = setOf(BottomBarContainerHeightFingerprint) ) { override fun execute(context: BytecodeContext) { + super.execute(context) - InitializeButtonsFingerprint.resultOrThrow().let { parentResult -> - SetPivotBarFingerprint.also { it.resolve(context, parentResult.classDef) } - .resultOrThrow().let { - it.mutableMethod.apply { - val startIndex = it.scanResult.patternScanResult!!.startIndex - val register = getInstruction(startIndex).registerA + // region patch for set navigation bar height. - addInstruction( - startIndex + 1, - "invoke-static {v$register}, $SHORTS_CLASS_DESCRIPTOR->setNavigationBar(Ljava/lang/Object;)V" - ) - } - } - } - - RenderBottomNavigationBarFingerprint.resultOrThrow().let { - val walkerMethod = - it.getWalkerMethod(context, it.scanResult.patternScanResult!!.endIndex) - - walkerMethod.addInstruction( - 0, - "invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->hideShortsNavigationBar()V" - ) - } - - BottomNavigationBarFingerprint.result?.let { + BottomBarContainerHeightFingerprint.resultOrThrow().forEach { it.mutableMethod.apply { - val targetIndex = indexOfFirstInstructionOrThrow { - getReference()?.name == "findViewById" + val constIndex = indexOfFirstWideLiteralInstructionValue(BottomBarContainer) + + val targetIndex = indexOfFirstInstructionOrThrow(constIndex) { + getReference()?.name == "getHeight" } + 1 - val insertRegister = getInstruction(targetIndex).registerA + + val heightRegister = getInstruction(targetIndex).registerA addInstructions( targetIndex + 1, """ - invoke-static {v$insertRegister}, $SHORTS_CLASS_DESCRIPTOR->hideShortsNavigationBar(Landroid/view/View;)Landroid/view/View; - move-result-object v$insertRegister + invoke-static {v$heightRegister}, $SHORTS_CLASS_DESCRIPTOR->overrideNavigationBarHeight(I)I + move-result v$heightRegister """ ) } } + NavigationBarHookPatch.addBottomBarContainerHook("$SHORTS_CLASS_DESCRIPTOR->setNavigationBar(Landroid/view/View;)V") + + // endregion. + + // region patch for addOnAttachStateChangeListener. + + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $SHORTS_CLASS_DESCRIPTOR->onShortsCreate(Landroid/view/View;)V + """ + + ReelWatchPagerFingerprint.injectLiteralInstructionViewCall( + ReelWatchPlayer, + smaliInstruction + ) + + // endregion. + } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/BottomBarContainerHeightFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/BottomBarContainerHeightFingerprint.kt new file mode 100644 index 0000000000..776252b56b --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/BottomBarContainerHeightFingerprint.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.youtube.shorts.components.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.BottomBarContainer +import app.revanced.util.containsWideLiteralInstructionValue +import app.revanced.util.fingerprint.MultiMethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal object BottomBarContainerHeightFingerprint : MultiMethodFingerprint( + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;", "Landroid/os/Bundle;"), + strings = listOf("r_pfvc"), + customFingerprint = { methodDef, _ -> + methodDef.containsWideLiteralInstructionValue(BottomBarContainer) + }, +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/BottomNavigationBarFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/BottomNavigationBarFingerprint.kt deleted file mode 100644 index c126985dc5..0000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/BottomNavigationBarFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.shorts.components.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object BottomNavigationBarFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Landroid/view/View;", "Landroid/os/Bundle;"), - strings = listOf("r_pfvc", "ReelWatchPaneFragmentViewModelKey") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/ReelWatchPagerFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/ReelWatchPagerFingerprint.kt new file mode 100644 index 0000000000..e4267393c6 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/ReelWatchPagerFingerprint.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.youtube.shorts.components.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.ReelWatchPlayer +import app.revanced.util.fingerprint.LiteralValueFingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal object ReelWatchPagerFingerprint : LiteralValueFingerprint( + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Landroid/view/View;", + literalSupplier = { ReelWatchPlayer } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/RenderBottomNavigationBarFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/RenderBottomNavigationBarFingerprint.kt deleted file mode 100644 index 9dc16d62a9..0000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/RenderBottomNavigationBarFingerprint.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.patches.youtube.shorts.components.fingerprints - -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.Opcode - -internal object RenderBottomNavigationBarFingerprint : MethodFingerprint( - returnType = "Landroid/view/View;", - opcodes = listOf( - Opcode.CONST_STRING, - Opcode.INVOKE_VIRTUAL - ), - strings = listOf("r_pfcv") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/SetPivotBarFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/SetPivotBarFingerprint.kt deleted file mode 100644 index ac218b1551..0000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/shorts/components/fingerprints/SetPivotBarFingerprint.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.revanced.patches.youtube.shorts.components.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object SetPivotBarFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, - parameters = listOf("Z"), - opcodes = listOf( - Opcode.CHECK_CAST, - Opcode.IF_EQZ - ) -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt index e418fc84f0..153c540bf8 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt @@ -95,6 +95,7 @@ object SharedResourceIdPatch : ResourcePatch() { var ReelRightLikeIcon = -1L var ReelTimeBarPlayedColor = -1L var ReelVodTimeStampsContainer = -1L + var ReelWatchPlayer = -1L var RelatedChipCloudMargin = -1L var RightComment = -1L var ScrimOverlay = -1L @@ -204,6 +205,7 @@ object SharedResourceIdPatch : ResourcePatch() { ReelRightLikeIcon = getId(DRAWABLE, "reel_right_like_icon") ReelTimeBarPlayedColor = getId(COLOR, "reel_time_bar_played_color") ReelVodTimeStampsContainer = getId(ID, "reel_vod_timestamps_container") + ReelWatchPlayer = getId(ID, "reel_watch_player") RelatedChipCloudMargin = getId(LAYOUT, "related_chip_cloud_reduced_margins") RightComment = getId(DRAWABLE, "ic_right_comment_32c") ScrimOverlay = getId(ID, "scrim_overlay") diff --git a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 183e3a29cb..46adf69e01 100644 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -16,6 +16,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableClass import app.revanced.patcher.util.proxy.mutableTypes.MutableField import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.MultiMethodFingerprint import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.Method @@ -38,6 +39,8 @@ fun MethodFingerprint.isDeprecated() = fun MethodFingerprint.resultOrThrow() = result ?: throw exception +fun MultiMethodFingerprint.resultOrThrow() = result.ifEmpty { throw exception } + /** * The [PatchException] of failing to resolve a [MethodFingerprint]. * @@ -46,6 +49,9 @@ fun MethodFingerprint.resultOrThrow() = result ?: throw exception val MethodFingerprint.exception get() = PatchException("Failed to resolve ${this.javaClass.simpleName}") +val MultiMethodFingerprint.exception + get() = PatchException("Failed to resolve ${this.javaClass.simpleName}") + fun MethodFingerprint.alsoResolve(context: BytecodeContext, fingerprint: MethodFingerprint) = also { resolve(context, fingerprint.resultOrThrow().classDef) }.resultOrThrow() diff --git a/src/main/kotlin/app/revanced/util/fingerprint/MultiMethodFingerprint.kt b/src/main/kotlin/app/revanced/util/fingerprint/MultiMethodFingerprint.kt new file mode 100644 index 0000000000..71592c695d --- /dev/null +++ b/src/main/kotlin/app/revanced/util/fingerprint/MultiMethodFingerprint.kt @@ -0,0 +1,193 @@ +package app.revanced.util.fingerprint + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.patcher.fingerprint.MethodFingerprintResult +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch +private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult + +/** + * Represents the [MethodFingerprint] for a method. + * @param returnType The return type of the method. + * @param accessFlags The access flags of the method. + * @param parameters The parameters of the method. + * @param opcodes The list of opcodes of the method. + * @param strings A list of strings which a method contains. + * @param customFingerprint A custom condition for this fingerprint. + * A `null` opcode is equals to an unknown opcode. + */ +abstract class MultiMethodFingerprint( + val returnType: String? = null, + val accessFlags: Int? = null, + val parameters: Iterable? = null, + val opcodes: Iterable? = null, + val strings: Iterable? = null, + val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null +) { + /** + * The result of the [MethodFingerprint]. + */ + var result = mutableListOf() + private var resolved = false + + companion object { + /** + * Resolve a list of [MethodFingerprint] against a list of [ClassDef]. + * + * @param classes The classes on which to resolve the [MethodFingerprint] in. + * @param context The [BytecodeContext] to host proxies. + * @return True if the resolution was successful, false otherwise. + */ + fun Iterable.resolve(context: BytecodeContext, classes: Iterable) { + for (fingerprint in this) { // For each fingerprint + if (fingerprint.resolved) continue + for (classDef in classes) // search through all classes for the fingerprint + fingerprint.resolve(context, classDef) + fingerprint.resolved = true + } + } + + /** + * Resolve a [MethodFingerprint] against a [ClassDef]. + * + * @param forClass The class on which to resolve the [MethodFingerprint] in. + * @param context The [BytecodeContext] to host proxies. + * @return True if the resolution was successful, false otherwise. + */ + fun MultiMethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean { + for (method in forClass.methods) + if (this.resolve(context, method, forClass)) + return true + return false + } + + /** + * Resolve a [MethodFingerprint] against a [Method]. + * + * @param method The class on which to resolve the [MethodFingerprint] in. + * @param forClass The class on which to resolve the [MethodFingerprint]. + * @param context The [BytecodeContext] to host proxies. + * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. + */ + fun MultiMethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean { + val methodFingerprint = this + + if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) + return false + + if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) + return false + + fun parametersEqual( + parameters1: Iterable, parameters2: Iterable + ): Boolean { + if (parameters1.count() != parameters2.count()) return false + val iterator1 = parameters1.iterator() + parameters2.forEach { + if (!it.startsWith(iterator1.next())) return false + } + return true + } + + if (methodFingerprint.parameters != null && !parametersEqual( + methodFingerprint.parameters, // TODO: parseParameters() + method.parameterTypes + ) + ) return false + + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) + return false + + val stringsScanResult = if (methodFingerprint.strings != null) { + StringsScanResult( + buildList { + val implementation = method.implementation ?: return false + + val stringsList = methodFingerprint.strings.toMutableList() + + implementation.instructions.forEachIndexed { instructionIndex, instruction -> + if ( + instruction.opcode != Opcode.CONST_STRING && + instruction.opcode != Opcode.CONST_STRING_JUMBO + ) return@forEachIndexed + + val string = ((instruction as ReferenceInstruction).reference as StringReference).string + val index = stringsList.indexOfFirst(string::contains) + if (index == -1) return@forEachIndexed + + add(StringMatch(string, instructionIndex)) + stringsList.removeAt(index) + } + + if (stringsList.isNotEmpty()) return false + } + ) + } else null + + val patternScanResult = if (methodFingerprint.opcodes != null) { + method.implementation?.instructions ?: return false + + method.patternScan(methodFingerprint) ?: return false + } else null + + methodFingerprint.result.add( + MethodFingerprintResult( + method, + forClass, + MethodFingerprintResult.MethodFingerprintScanResult( + patternScanResult, + stringsScanResult + ), + context + ) + ) + + return true + } + + private fun Method.patternScan( + fingerprint: MultiMethodFingerprint + ): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { + val instructions = this.implementation!!.instructions + + val pattern = fingerprint.opcodes!! + val instructionLength = instructions.count() + val patternLength = pattern.count() + + for (index in 0 until instructionLength) { + var patternIndex = 0 + + while (index + patternIndex < instructionLength) { + val originalOpcode = instructions.elementAt(index + patternIndex).opcode + val patternOpcode = pattern.elementAt(patternIndex) + + if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { + // reaching maximum threshold (0) means, + // the pattern does not match to the current instructions + break + } + + if (patternIndex < patternLength - 1) { + // if the entire pattern has not been scanned yet + // continue the scan + patternIndex++ + continue + } + return MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( + index, + index + patternIndex + ) + } + } + + return null + } + } +} diff --git a/src/main/kotlin/app/revanced/util/patch/MultiMethodBytecodePatch.kt b/src/main/kotlin/app/revanced/util/patch/MultiMethodBytecodePatch.kt new file mode 100644 index 0000000000..fa137319a6 --- /dev/null +++ b/src/main/kotlin/app/revanced/util/patch/MultiMethodBytecodePatch.kt @@ -0,0 +1,16 @@ +package app.revanced.util.patch + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.util.fingerprint.MultiMethodFingerprint +import app.revanced.util.fingerprint.MultiMethodFingerprint.Companion.resolve + +abstract class MultiMethodBytecodePatch( + val fingerprints: Set = setOf(), + val multiFingerprints: Set = setOf() +) : BytecodePatch(fingerprints) { + override fun execute(context: BytecodeContext) { + multiFingerprints.resolve(context, context.classes) + } +}