diff --git a/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/TabletMiniPlayerPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/TabletMiniPlayerPatch.kt index f1aa8542f..3897272fc 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/TabletMiniPlayerPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/TabletMiniPlayerPatch.kt @@ -3,24 +3,35 @@ package app.revanced.patches.youtube.general.tabletminiplayer import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.MiniPlayerDimensionsCalculatorFingerprint import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.MiniPlayerOverrideFingerprint import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.MiniPlayerOverrideNoContextFingerprint import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.MiniPlayerResponseModelSizeCheckFingerprint import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.ModernMiniPlayerConfigFingerprint +import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.ModernMiniPlayerConstructorFingerprint import app.revanced.patches.youtube.utils.integrations.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.integrations.Constants.GENERAL_CLASS_DESCRIPTOR import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.YtOutlinePiPWhite +import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.YtOutlineXWhite import app.revanced.patches.youtube.utils.settings.SettingsPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.getReference import app.revanced.util.getStringInstructionIndex import app.revanced.util.getTargetIndex import app.revanced.util.getTargetIndexReversed import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.literalInstructionHook import app.revanced.util.patch.BaseBytecodePatch import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference @Suppress("unused") object TabletMiniPlayerPatch : BaseBytecodePatch( @@ -35,7 +46,8 @@ object TabletMiniPlayerPatch : BaseBytecodePatch( MiniPlayerDimensionsCalculatorFingerprint, MiniPlayerResponseModelSizeCheckFingerprint, MiniPlayerOverrideFingerprint, - ModernMiniPlayerConfigFingerprint + ModernMiniPlayerConfigFingerprint, + ModernMiniPlayerConstructorFingerprint ) ) { override fun execute(context: BytecodeContext) { @@ -57,6 +69,35 @@ object TabletMiniPlayerPatch : BaseBytecodePatch( hook(it.scanResult.patternScanResult!!.endIndex) } } + + ModernMiniPlayerConstructorFingerprint.resultOrThrow().let { + it.mutableClass.methods.forEach { mutableMethod -> + mutableMethod.hookModernMiniPlayer() + } + } + + // In ModernMiniPlayer, the drawables of the close button and expand button are reversed. + // OnClickListener appears to be applied normally, so this appears to be a bug in YouTube. + // To solve this, swap the drawables of the close and expand buttons. + // This Drawable will be used in multiple Classes, so instead of using LiteralValueFingerprint to patch only specific methods, + // Apply the patch to all methods where literals are used. + mapOf( + YtOutlineXWhite to "replaceCloseButtonDrawableId", + YtOutlinePiPWhite to "replaceExpandButtonDrawableId" + ).forEach { (literal, methodName) -> + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $GENERAL_CLASS_DESCRIPTOR->$methodName(I)I + move-result v$REGISTER_TEMPLATE_REPLACEMENT + """ + + context.literalInstructionHook(literal, smaliInstruction) + } + + SettingsPatch.addPreference( + arrayOf( + "SETTINGS: ENABLE_MODERN_MINI_PLAYER" + ) + ) } else { MiniPlayerOverrideFingerprint.resultOrThrow().let { it.mutableMethod.apply { @@ -89,14 +130,46 @@ object TabletMiniPlayerPatch : BaseBytecodePatch( SettingsPatch.updatePatchStatus(this) } - private fun MutableMethod.hook(index: Int) { + private fun MutableMethod.hook(index: Int) = + hook(index, "enableTabletMiniPlayer") + + private fun MutableMethod.hook( + index: Int, + methodName: String + ) { val register = getInstruction(index).registerA addInstructions( index, """ - invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->enableTabletMiniPlayer(Z)Z + invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->$methodName(Z)Z move-result v$register """ ) } + + private fun MutableMethod.hookModernMiniPlayer() { + if (returnType == "Z") { + hook(getTargetIndexReversed(Opcode.RETURN), "enableModernMiniPlayer") + hook(getTargetIndex(Opcode.RETURN), "enableModernMiniPlayer") + } + + val iPutIndex = indexOfFirstInstruction { + this.opcode == Opcode.IPUT + && this.getReference()?.type == "I" + } + + if (iPutIndex < 0) return + + val targetReference = getInstruction(iPutIndex).reference + val targetInstruction = getInstruction(iPutIndex) + + addInstructions( + iPutIndex + 1, """ + invoke-static {v${targetInstruction.registerA}}, $GENERAL_CLASS_DESCRIPTOR->enableModernMiniPlayer(I)I + move-result v${targetInstruction.registerA} + iput v${targetInstruction.registerA}, v${targetInstruction.registerB}, $targetReference + """ + ) + removeInstruction(iPutIndex) + } } diff --git a/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/fingerprints/ModernMiniPlayerConstructorFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/fingerprints/ModernMiniPlayerConstructorFingerprint.kt new file mode 100644 index 000000000..51a32ff56 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/general/tabletminiplayer/fingerprints/ModernMiniPlayerConstructorFingerprint.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.youtube.general.tabletminiplayer.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.util.fingerprint.LiteralValueFingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal object ModernMiniPlayerConstructorFingerprint : LiteralValueFingerprint( + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literalSupplier = { 45623000 } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt index 73209d52f..54f427a5e 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt @@ -38,8 +38,8 @@ import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.YtWor import app.revanced.patches.youtube.utils.settings.SettingsPatch import app.revanced.patches.youtube.utils.settings.SettingsPatch.contexts import app.revanced.patches.youtube.utils.toolbar.ToolBarHookPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT import app.revanced.util.doRecursively -import app.revanced.util.findMutableMethodOf import app.revanced.util.getTargetIndex import app.revanced.util.getTargetIndexWithMethodReferenceName import app.revanced.util.getTargetIndexWithReference @@ -47,6 +47,7 @@ import app.revanced.util.getTargetIndexWithReferenceReversed import app.revanced.util.getWalkerMethod import app.revanced.util.getWideLiteralInstructionIndex import app.revanced.util.literalInstructionBooleanHook +import app.revanced.util.literalInstructionHook import app.revanced.util.patch.BaseBytecodePatch import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.Opcode @@ -54,7 +55,6 @@ import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.util.MethodUtil import org.w3c.dom.Element @@ -97,7 +97,17 @@ object ToolBarComponentsPatch : BaseBytecodePatch( // region patch for change YouTube header // Invoke YouTube's header attribute into integrations. - replaceHeaderAttributeId(context) + val smaliInstruction = """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->getHeaderAttributeId()I + move-result v$REGISTER_TEMPLATE_REPLACEMENT + """ + + arrayOf( + YtPremiumWordMarkHeader, + YtWordMarkHeader + ).forEach { literal -> + context.literalInstructionHook(literal, smaliInstruction) + } // YouTube's headers have the form of AttributeSet, which is decoded from YouTube's built-in classes. val attributeResolverMethod = AttributeResolverFingerprint.resultOrThrow().mutableMethod @@ -404,36 +414,6 @@ object ToolBarComponentsPatch : BaseBytecodePatch( SettingsPatch.updatePatchStatus(this) } - private fun replaceHeaderAttributeId(context: BytecodeContext) { - val headerAttributeIdArray = arrayOf(YtPremiumWordMarkHeader, YtWordMarkHeader) - - context.classes.forEach { classDef -> - classDef.methods.forEach { method -> - method.implementation.apply { - this?.instructions?.forEachIndexed { index, instruction -> - if (instruction.opcode != Opcode.CONST) - return@forEachIndexed - if (headerAttributeIdArray.indexOf((instruction as Instruction31i).wideLiteral) < 0) - return@forEachIndexed - - (instructions.elementAt(index)).apply { - val register = (this as OneRegisterInstruction).registerA - context.proxy(classDef) - .mutableClass - .findMutableMethodOf(method) - .addInstructions( - index + 1, """ - invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->getHeaderAttributeId()I - move-result v$register - """ - ) - } - } - } - } - } - } - private fun MutableMethod.injectSearchBarHook( insertIndex: Int, descriptor: String 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 05d4db236..cddb71037 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 @@ -91,7 +91,9 @@ object SharedResourceIdPatch : ResourcePatch() { var VideoQualityBottomSheet = -1L var VoiceSearch = -1L var YouTubeControlsOverlaySubtitleButton = -1L + var YtOutlinePiPWhite = -1L var YtOutlineVideoCamera = -1L + var YtOutlineXWhite = -1L var YtPremiumWordMarkHeader = -1L var YtWordMarkHeader = -1L @@ -173,7 +175,9 @@ object SharedResourceIdPatch : ResourcePatch() { VideoQualityBottomSheet = getId(LAYOUT, "video_quality_bottom_sheet_list_fragment_title") VoiceSearch = getId(ID, "voice_search") YouTubeControlsOverlaySubtitleButton = getId(LAYOUT, "youtube_controls_overlay_subtitle_button") + YtOutlinePiPWhite = getId(DRAWABLE, "yt_outline_picture_in_picture_white_24") YtOutlineVideoCamera = getId(DRAWABLE, "yt_outline_video_camera_black_24") + YtOutlineXWhite = getId(DRAWABLE, "yt_outline_x_white_24") YtPremiumWordMarkHeader = getId(ATTR, "ytPremiumWordmarkHeader") YtWordMarkHeader = getId(ATTR, "ytWordmarkHeader") diff --git a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 752c6a2d7..ae5d60e6c 100644 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -24,12 +24,15 @@ import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.immutable.ImmutableField import com.android.tools.smali.dexlib2.util.MethodUtil +const val REGISTER_TEMPLATE_REPLACEMENT: String = "REGISTER_INDEX" + fun MethodFingerprint.resultOrThrow() = result ?: throw exception /** @@ -136,7 +139,7 @@ fun MethodFingerprint.literalInstructionBooleanHook( } } -fun MethodFingerprint.literalInstructionViewHook( +fun MethodFingerprint.literalInstructionHook( literal: Long, descriptor: String ) { @@ -152,6 +155,37 @@ fun MethodFingerprint.literalInstructionViewHook( } } +fun BytecodeContext.literalInstructionHook( + literal: Long, + smaliInstruction: String +) { + val context = this + context.classes.forEach { classDef -> + classDef.methods.forEach { method -> + method.implementation.apply { + this?.instructions?.forEachIndexed { _, instruction -> + if (instruction.opcode != Opcode.CONST) + return@forEachIndexed + if ((instruction as Instruction31i).wideLiteral != literal) + return@forEachIndexed + + context.proxy(classDef) + .mutableClass + .findMutableMethodOf(method).apply { + val index = getWideLiteralInstructionIndex(literal) + val register = (instruction as OneRegisterInstruction).registerA.toString() + + addInstructions( + index + 1, + smaliInstruction.replace(REGISTER_TEMPLATE_REPLACEMENT, register) + ) + } + } + } + } + } +} + /** * Find the index of the first wide literal instruction with the given value. * diff --git a/src/main/resources/youtube/settings/host/values/strings.xml b/src/main/resources/youtube/settings/host/values/strings.xml index 2dd02892e..bda452b63 100644 --- a/src/main/resources/youtube/settings/host/values/strings.xml +++ b/src/main/resources/youtube/settings/host/values/strings.xml @@ -277,6 +277,10 @@ Limitations: Enable tablet mini player Tablet mini player is enabled. Tablet mini player is disabled. + Enable modern mini player + Modern mini player is enabled. + Modern mini player is disabled. + Hide floating microphone button Floating microphone button is hidden. Floating microphone button is shown. diff --git a/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 93d98dd20..44c157a87 100644 --- a/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -171,6 +171,8 @@ +