fix(YouTube - Change form factor): Explore button sometimes shows in Automotive layout

Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
This commit is contained in:
oSumAtrIX 2026-03-19 20:09:30 +01:00
parent 9fa641ed81
commit 733f3bb2cd
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
12 changed files with 222 additions and 115 deletions

View file

@ -215,6 +215,7 @@ val hideAdsPatch = bytecodePatch(
setOf(
Endpoint.BROWSE,
Endpoint.SEARCH,
Endpoint.NEXT,
).forEach { endpoint ->
addOSNameHook(
endpoint,

View file

@ -1,9 +1,18 @@
package app.revanced.patches.youtube.ad.video
import app.revanced.patcher.gettingFirstMethodDeclaratively
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType
internal val BytecodePatchContext.loadVideoAdsMethod by gettingFirstMethodDeclaratively(
"TriggerBundle doesn't have the required metadata specified by the trigger ",
"Ping migration no associated ping bindings for activated trigger: ",
)
internal val BytecodePatchContext.playerBytesAdLayoutMethod by gettingFirstMethodDeclaratively(
"Bootstrapped layout construction resulted in non PlayerBytesLayout. PlayerAds count: "
) {
returnType("V")
parameterTypes("L")
}

View file

@ -1,16 +1,20 @@
package app.revanced.patches.youtube.ad.video
import app.revanced.patcher.extensions.ExternalLabel
import app.revanced.patcher.extensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.contexthook.Endpoint
import app.revanced.patches.youtube.misc.contexthook.addOSNameHook
import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/VideoAdsPatch;"
@Suppress("ObjectPropertyName")
val videoAdsPatch = bytecodePatch(
name = "Video ads",
@ -20,6 +24,7 @@ val videoAdsPatch = bytecodePatch(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
hookClientContextPatch
)
compatibleWith(
@ -41,15 +46,33 @@ val videoAdsPatch = bytecodePatch(
SwitchPreference("revanced_hide_video_ads"),
)
loadVideoAdsMethod.addInstructionsWithLabels(
0,
"""
invoke-static { }, Lapp/revanced/extension/youtube/patches/VideoAdsPatch;->shouldShowAds()Z
move-result v0
if-nez v0, :show_video_ads
return-void
""",
ExternalLabel("show_video_ads", loadVideoAdsMethod.getInstruction(0)),
)
setOf(
loadVideoAdsMethod,
playerBytesAdLayoutMethod,
).forEach { method ->
method.addInstructionsWithLabels(
0,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->hideVideoAds()Z
move-result v0
if-eqz v0, :show_video_ads
return-void
:show_video_ads
nop
"""
)
}
setOf(
Endpoint.GET_WATCH,
Endpoint.PLAYER,
Endpoint.REEL,
).forEach { endpoint ->
addOSNameHook(
endpoint,
"$EXTENSION_CLASS_DESCRIPTOR->hideVideoAds(Ljava/lang/String;)Ljava/lang/String;",
)
}
}
}

View file

@ -7,6 +7,9 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.patches.youtube.misc.contexthook.Endpoint
import app.revanced.patches.youtube.misc.contexthook.addClientFormFactorHook
import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
@ -27,6 +30,7 @@ val changeFormFactorPatch = bytecodePatch(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
hookClientContextPatch,
navigationBarHookPatch
)
@ -49,8 +53,6 @@ val changeFormFactorPatch = bytecodePatch(
ListPreference("revanced_change_form_factor"),
)
hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)
val formFactorEnumConstructorClass = formFactorEnumConstructorMethod.definingClass
val createPlayerRequestBodyWithModelMatch = firstMethodComposite {
@ -71,11 +73,23 @@ val changeFormFactorPatch = bytecodePatch(
addInstructions(
index + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getFormFactor(I)I
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getUniversalFormFactor(I)I
move-result v$register
""",
)
}
}
setOf(
Endpoint.GET_WATCH,
Endpoint.NEXT,
Endpoint.GUIDE,
Endpoint.REEL,
).forEach { endpoint ->
addClientFormFactorHook(
endpoint,
"$EXTENSION_CLASS_DESCRIPTOR->replaceBrokenFormFactor(I)I",
)
}
}
}

View file

@ -1,15 +1,21 @@
package app.revanced.patches.youtube.misc.contexthook
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.accessFlags
import app.revanced.patcher.allOf
import app.revanced.patcher.classDef
import app.revanced.patcher.composingFirstMethod
import app.revanced.patcher.extensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.fieldReference
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.methodReference
import app.revanced.patcher.field
import app.revanced.patcher.firstMethodComposite
import app.revanced.patcher.firstMethodDeclaratively
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.method
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch
@ -26,8 +32,9 @@ import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import java.lang.ref.WeakReference
private lateinit var browseIdField: FieldReference
private lateinit var clientFormFactorField: FieldReference
private lateinit var clientInfoField: FieldReference
private lateinit var clientVersionField: FieldReference
private lateinit var messageLiteBuilderField: FieldReference
@ -41,9 +48,15 @@ enum class Endpoint(
BROWSE(
BytecodePatchContext::browseEndpointParentMethod::get
),
GET_WATCH(
BytecodePatchContext::getWatchEndpointConstructorPrimaryMethod::get,
BytecodePatchContext::getWatchEndpointConstructorSecondaryMethod::get,
),
GUIDE(
BytecodePatchContext::guideEndpointConstructorMethod::get
),
NEXT(BytecodePatchContext::nextEndpointParentMethod::get),
PLAYER(BytecodePatchContext::playerEndpointParentMethod::get),
REEL(
BytecodePatchContext::reelCreateItemsEndpointConstructorMethod::get,
BytecodePatchContext::reelItemWatchEndpointConstructorMethod::get,
@ -90,12 +103,8 @@ val hookClientContextPatch = bytecodePatch(
}
}
browseEndpointParentMethod.immutableClassDef.browseEndpointConstructorMethodMatch.let {
it.method.apply {
val browseIdIndex = it[-1]
browseIdField =
getInstruction<ReferenceInstruction>(browseIdIndex).fieldReference!!
}
clientFormFactorField = getSetClientFormFactorMethodMatch().let {
it.method.getInstruction<ReferenceInstruction>(it[0]).fieldReference!!
}
}
@ -140,7 +149,7 @@ val hookClientContextPatch = bytecodePatch(
}
)
it.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index ->
it.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index ->
it.addInstructionsAtControlFlowLabel(
index,
"invoke-direct/range { p0 .. p0 }, ${it.definingClass}->$helperMethodName()V"
@ -152,19 +161,22 @@ val hookClientContextPatch = bytecodePatch(
}
}
fun addClientFormFactorHook(endPoint: Endpoint, descriptor: String) {
endPoint.instructions += """
iget v2, v1, $clientFormFactorField
invoke-static { v2 }, $descriptor
move-result v2
iput v2, v1, $clientFormFactorField
"""
}
fun addClientVersionHook(endPoint: Endpoint, descriptor: String) {
endPoint.instructions += if (endPoint == Endpoint.BROWSE) """
iget-object v3, p0, $browseIdField
iget-object v2, v1, $clientVersionField
invoke-static { v3, v2 }, $descriptor
move-result-object v2
iput-object v2, v1, $clientVersionField
""" else """
endPoint.instructions += """
iget-object v2, v1, $clientVersionField
invoke-static { v2 }, $descriptor
move-result-object v2
iput-object v2, v1, $clientVersionField
"""
"""
}
fun addOSNameHook(endPoint: Endpoint, descriptor: String) {
@ -173,5 +185,5 @@ fun addOSNameHook(endPoint: Endpoint, descriptor: String) {
invoke-static { v2 }, $descriptor
move-result-object v2
iput-object v2, v1, $osNameField
"""
"""
}

View file

@ -1,6 +1,7 @@
package app.revanced.patches.youtube.misc.contexthook
import app.revanced.patcher.ClassDefComposing
import app.revanced.patcher.CompositeMatch
import app.revanced.patcher.accessFlags
import app.revanced.patcher.after
import app.revanced.patcher.afterAtMost
@ -9,8 +10,11 @@ import app.revanced.patcher.composingFirstMethod
import app.revanced.patcher.custom
import app.revanced.patcher.extensions.methodReference
import app.revanced.patcher.field
import app.revanced.patcher.firstImmutableMethodDeclaratively
import app.revanced.patcher.firstMethodComposite
import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively
import app.revanced.patcher.gettingFirstMethodDeclaratively
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.method
@ -22,7 +26,6 @@ import com.android.tools.smali.dexlib2.AccessFlags
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.google.common.io.ByteArrayDataOutput
internal const val CLIENT_INFO_CLASS_DESCRIPTOR =
$$"Lcom/google/protos/youtube/api/innertube/InnertubeContext$ClientInfo;"
@ -86,25 +89,33 @@ internal val BytecodePatchContext.buildDummyClientContextBodyMethodMatch by comp
)
}
internal val ClassDef.browseEndpointConstructorMethodMatch by ClassDefComposing.composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
returnType("V")
var methodDefiningClass = ""
custom {
methodDefiningClass = this.definingClass
true
internal fun BytecodePatchContext.getSetClientFormFactorMethodMatch(): CompositeMatch {
val clientFormFactorEnumConstructorMethod = firstImmutableMethodDeclaratively(
"UNKNOWN_FORM_FACTOR",
"SMALL_FORM_FACTOR",
"LARGE_FORM_FACTOR",
"AUTOMOTIVE_FORM_FACTOR",
"WEARABLE_FORM_FACTOR",
) {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
}.immutableClassDef.firstMethodComposite {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returnType("L")
parameterTypes("I")
}
instructions(
""(),
after(
allOf(
Opcode.IPUT_OBJECT(),
field { definingClass == methodDefiningClass && type == "Ljava/lang/String;" }
)
),
)
return firstMethodComposite {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
parameterTypes("L")
instructions(
allOf(Opcode.IGET(), field { type == "I" && definingClass == CLIENT_INFO_CLASS_DESCRIPTOR }),
method { this == clientFormFactorEnumConstructorMethod }
)
}
}
internal val BytecodePatchContext.browseEndpointParentMethod by gettingFirstImmutableMethodDeclaratively(
@ -113,6 +124,22 @@ internal val BytecodePatchContext.browseEndpointParentMethod by gettingFirstImmu
returnType("Ljava/lang/String;")
}
internal val BytecodePatchContext.getWatchEndpointConstructorPrimaryMethod by gettingFirstMethodDeclaratively(
"get_watch"
) {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
returnType("V")
custom { immutableClassDef.fields.any { it.type == "Ljava/util/function/Consumer;" } }
}
internal val BytecodePatchContext.getWatchEndpointConstructorSecondaryMethod by gettingFirstMethodDeclaratively(
"get_watch"
) {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
returnType("V")
custom { immutableClassDef.fields.none { it.type == "Ljava/util/function/Consumer;" } }
}
internal val BytecodePatchContext.guideEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively(
"guide"
) {
@ -120,6 +147,18 @@ internal val BytecodePatchContext.guideEndpointConstructorMethod by gettingFirst
returnType("V")
}
internal val BytecodePatchContext.nextEndpointParentMethod by gettingFirstImmutableMethodDeclaratively(
"watchNextType"
) {
returnType("Ljava/lang/String;")
}
internal val BytecodePatchContext.playerEndpointParentMethod by gettingFirstImmutableMethodDeclaratively(
"dataExpiredForSeconds"
) {
returnType("Ljava/lang/String;")
}
internal val BytecodePatchContext.reelCreateItemsEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively(
"reel/create_reel_items"
) {

View file

@ -1428,10 +1428,16 @@ Ready to submit?"</string>
Tablet layout
• Community posts are hidden
• Playback in feeds setting is not available
• Remix button and Sound button are not available in Shorts
• Video action bar is not collapsed
Automotive layout
• Feed is organized by topics and channels
• Playback in feeds setting is not available
• Remix button and Sound button are not available in Shorts
• Shorts open in the regular player
• Feed is organized by topics and channels"</string>
Video action bar is not collapsed"</string>
</patch>
<patch id="layout.spoofappversion.spoofAppVersionPatch">
<string name="revanced_spoof_app_version_title">Spoof app version</string>