feat(YouTube/Enable tablet mini player): add Enable modern mini player settings

This commit is contained in:
inotia00 2024-04-29 19:34:41 +09:00
parent 9aecec0df8
commit f66ac24f55
7 changed files with 144 additions and 37 deletions

View File

@ -3,24 +3,35 @@ package app.revanced.patches.youtube.general.tabletminiplayer
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 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.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.MiniPlayerDimensionsCalculatorFingerprint 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.MiniPlayerOverrideFingerprint
import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.MiniPlayerOverrideNoContextFingerprint 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.MiniPlayerResponseModelSizeCheckFingerprint
import app.revanced.patches.youtube.general.tabletminiplayer.fingerprints.ModernMiniPlayerConfigFingerprint 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.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.integrations.Constants.GENERAL_CLASS_DESCRIPTOR 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
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.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.getStringInstructionIndex
import app.revanced.util.getTargetIndex import app.revanced.util.getTargetIndex
import app.revanced.util.getTargetIndexReversed import app.revanced.util.getTargetIndexReversed
import app.revanced.util.getWalkerMethod 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.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction 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") @Suppress("unused")
object TabletMiniPlayerPatch : BaseBytecodePatch( object TabletMiniPlayerPatch : BaseBytecodePatch(
@ -35,7 +46,8 @@ object TabletMiniPlayerPatch : BaseBytecodePatch(
MiniPlayerDimensionsCalculatorFingerprint, MiniPlayerDimensionsCalculatorFingerprint,
MiniPlayerResponseModelSizeCheckFingerprint, MiniPlayerResponseModelSizeCheckFingerprint,
MiniPlayerOverrideFingerprint, MiniPlayerOverrideFingerprint,
ModernMiniPlayerConfigFingerprint ModernMiniPlayerConfigFingerprint,
ModernMiniPlayerConstructorFingerprint
) )
) { ) {
override fun execute(context: BytecodeContext) { override fun execute(context: BytecodeContext) {
@ -57,6 +69,35 @@ object TabletMiniPlayerPatch : BaseBytecodePatch(
hook(it.scanResult.patternScanResult!!.endIndex) 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 { } else {
MiniPlayerOverrideFingerprint.resultOrThrow().let { MiniPlayerOverrideFingerprint.resultOrThrow().let {
it.mutableMethod.apply { it.mutableMethod.apply {
@ -89,14 +130,46 @@ object TabletMiniPlayerPatch : BaseBytecodePatch(
SettingsPatch.updatePatchStatus(this) 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<OneRegisterInstruction>(index).registerA val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions( addInstructions(
index, """ 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 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<FieldReference>()?.type == "I"
}
if (iPutIndex < 0) return
val targetReference = getInstruction<ReferenceInstruction>(iPutIndex).reference
val targetInstruction = getInstruction<TwoRegisterInstruction>(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)
}
} }

View File

@ -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 }
)

View File

@ -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
import app.revanced.patches.youtube.utils.settings.SettingsPatch.contexts import app.revanced.patches.youtube.utils.settings.SettingsPatch.contexts
import app.revanced.patches.youtube.utils.toolbar.ToolBarHookPatch import app.revanced.patches.youtube.utils.toolbar.ToolBarHookPatch
import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT
import app.revanced.util.doRecursively import app.revanced.util.doRecursively
import app.revanced.util.findMutableMethodOf
import app.revanced.util.getTargetIndex import app.revanced.util.getTargetIndex
import app.revanced.util.getTargetIndexWithMethodReferenceName import app.revanced.util.getTargetIndexWithMethodReferenceName
import app.revanced.util.getTargetIndexWithReference import app.revanced.util.getTargetIndexWithReference
@ -47,6 +47,7 @@ import app.revanced.util.getTargetIndexWithReferenceReversed
import app.revanced.util.getWalkerMethod import app.revanced.util.getWalkerMethod
import app.revanced.util.getWideLiteralInstructionIndex import app.revanced.util.getWideLiteralInstructionIndex
import app.revanced.util.literalInstructionBooleanHook import app.revanced.util.literalInstructionBooleanHook
import app.revanced.util.literalInstructionHook
import app.revanced.util.patch.BaseBytecodePatch import app.revanced.util.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.Opcode 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.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction 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.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.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
import org.w3c.dom.Element import org.w3c.dom.Element
@ -97,7 +97,17 @@ object ToolBarComponentsPatch : BaseBytecodePatch(
// region patch for change YouTube header // region patch for change YouTube header
// Invoke YouTube's header attribute into integrations. // 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. // YouTube's headers have the form of AttributeSet, which is decoded from YouTube's built-in classes.
val attributeResolverMethod = AttributeResolverFingerprint.resultOrThrow().mutableMethod val attributeResolverMethod = AttributeResolverFingerprint.resultOrThrow().mutableMethod
@ -404,36 +414,6 @@ object ToolBarComponentsPatch : BaseBytecodePatch(
SettingsPatch.updatePatchStatus(this) 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( private fun MutableMethod.injectSearchBarHook(
insertIndex: Int, insertIndex: Int,
descriptor: String descriptor: String

View File

@ -91,7 +91,9 @@ object SharedResourceIdPatch : ResourcePatch() {
var VideoQualityBottomSheet = -1L var VideoQualityBottomSheet = -1L
var VoiceSearch = -1L var VoiceSearch = -1L
var YouTubeControlsOverlaySubtitleButton = -1L var YouTubeControlsOverlaySubtitleButton = -1L
var YtOutlinePiPWhite = -1L
var YtOutlineVideoCamera = -1L var YtOutlineVideoCamera = -1L
var YtOutlineXWhite = -1L
var YtPremiumWordMarkHeader = -1L var YtPremiumWordMarkHeader = -1L
var YtWordMarkHeader = -1L var YtWordMarkHeader = -1L
@ -173,7 +175,9 @@ object SharedResourceIdPatch : ResourcePatch() {
VideoQualityBottomSheet = getId(LAYOUT, "video_quality_bottom_sheet_list_fragment_title") VideoQualityBottomSheet = getId(LAYOUT, "video_quality_bottom_sheet_list_fragment_title")
VoiceSearch = getId(ID, "voice_search") VoiceSearch = getId(ID, "voice_search")
YouTubeControlsOverlaySubtitleButton = getId(LAYOUT, "youtube_controls_overlay_subtitle_button") 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") YtOutlineVideoCamera = getId(DRAWABLE, "yt_outline_video_camera_black_24")
YtOutlineXWhite = getId(DRAWABLE, "yt_outline_x_white_24")
YtPremiumWordMarkHeader = getId(ATTR, "ytPremiumWordmarkHeader") YtPremiumWordMarkHeader = getId(ATTR, "ytPremiumWordmarkHeader")
YtWordMarkHeader = getId(ATTR, "ytWordmarkHeader") YtWordMarkHeader = getId(ATTR, "ytWordmarkHeader")

View File

@ -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.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction 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.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.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.iface.reference.Reference
import com.android.tools.smali.dexlib2.immutable.ImmutableField import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
const val REGISTER_TEMPLATE_REPLACEMENT: String = "REGISTER_INDEX"
fun MethodFingerprint.resultOrThrow() = result ?: throw exception fun MethodFingerprint.resultOrThrow() = result ?: throw exception
/** /**
@ -136,7 +139,7 @@ fun MethodFingerprint.literalInstructionBooleanHook(
} }
} }
fun MethodFingerprint.literalInstructionViewHook( fun MethodFingerprint.literalInstructionHook(
literal: Long, literal: Long,
descriptor: String 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. * Find the index of the first wide literal instruction with the given value.
* *

View File

@ -277,6 +277,10 @@ Limitations:
<string name="revanced_enable_tablet_mini_player_title">Enable tablet mini player</string> <string name="revanced_enable_tablet_mini_player_title">Enable tablet mini player</string>
<string name="revanced_enable_tablet_mini_player_summary_on">Tablet mini player is enabled.</string> <string name="revanced_enable_tablet_mini_player_summary_on">Tablet mini player is enabled.</string>
<string name="revanced_enable_tablet_mini_player_summary_off">Tablet mini player is disabled.</string> <string name="revanced_enable_tablet_mini_player_summary_off">Tablet mini player is disabled.</string>
<string name="revanced_enable_modern_mini_player_title">Enable modern mini player</string>
<string name="revanced_enable_modern_mini_player_summary_on">Modern mini player is enabled.</string>
<string name="revanced_enable_modern_mini_player_summary_off">Modern mini player is disabled.</string>
<string name="revanced_hide_floating_microphone_title">Hide floating microphone button</string> <string name="revanced_hide_floating_microphone_title">Hide floating microphone button</string>
<string name="revanced_hide_floating_microphone_summary_on">Floating microphone button is hidden.</string> <string name="revanced_hide_floating_microphone_summary_on">Floating microphone button is hidden.</string>
<string name="revanced_hide_floating_microphone_summary_off">Floating microphone button is shown.</string> <string name="revanced_hide_floating_microphone_summary_off">Floating microphone button is shown.</string>

View File

@ -171,6 +171,8 @@
<!-- SETTINGS: ENABLE_TABLET_MINI_PLAYER <!-- SETTINGS: ENABLE_TABLET_MINI_PLAYER
<SwitchPreference android:title="@string/revanced_enable_tablet_mini_player_title" android:key="revanced_enable_tablet_mini_player" android:defaultValue="false" android:summaryOn="@string/revanced_enable_tablet_mini_player_summary_on" android:summaryOff="@string/revanced_enable_tablet_mini_player_summary_off" />SETTINGS: ENABLE_TABLET_MINI_PLAYER --> <SwitchPreference android:title="@string/revanced_enable_tablet_mini_player_title" android:key="revanced_enable_tablet_mini_player" android:defaultValue="false" android:summaryOn="@string/revanced_enable_tablet_mini_player_summary_on" android:summaryOff="@string/revanced_enable_tablet_mini_player_summary_off" />SETTINGS: ENABLE_TABLET_MINI_PLAYER -->
<!-- SETTINGS: ENABLE_MODERN_MINI_PLAYER
<SwitchPreference android:title="@string/revanced_enable_modern_mini_player_title" android:key="revanced_enable_modern_mini_player" android:defaultValue="false" android:summaryOn="@string/revanced_enable_modern_mini_player_summary_on" android:summaryOff="@string/revanced_enable_modern_mini_player_summary_off" />SETTINGS: ENABLE_MODERN_MINI_PLAYER -->
<!-- SETTINGS: HIDE_LAYOUT_COMPONENTS <!-- SETTINGS: HIDE_LAYOUT_COMPONENTS
<SwitchPreference android:title="@string/revanced_hide_floating_microphone_title" android:key="revanced_hide_floating_microphone" android:defaultValue="true" android:summaryOn="@string/revanced_hide_floating_microphone_summary_on" android:summaryOff="@string/revanced_hide_floating_microphone_summary_off" /> <SwitchPreference android:title="@string/revanced_hide_floating_microphone_title" android:key="revanced_hide_floating_microphone" android:defaultValue="true" android:summaryOn="@string/revanced_hide_floating_microphone_summary_on" android:summaryOff="@string/revanced_hide_floating_microphone_summary_off" />