fix(YouTube - Change form factor): Restore Automotive form factor watch history menu, channel pages, and community posts (#4541)

This commit is contained in:
LisoUseInAIKyrios 2025-03-06 08:26:09 +02:00 committed by GitHub
parent 5bd2e86afe
commit aa5c001968
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 173 additions and 12 deletions

View File

@ -1,9 +1,17 @@
package app.revanced.extension.youtube.patches; package app.revanced.extension.youtube.patches;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import app.revanced.extension.shared.Utils; import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class ChangeFormFactorPatch { public class ChangeFormFactorPatch {
@ -41,14 +49,57 @@ public class ChangeFormFactorPatch {
@Nullable @Nullable
private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType; private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType;
private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
/** /**
* Injection point. * Injection point.
*/ */
public static int getFormFactor(int original) { public static int getFormFactor(int original) {
return FORM_FACTOR_TYPE == null if (FORM_FACTOR_TYPE == null) return original;
? original
: FORM_FACTOR_TYPE; if (USING_AUTOMOTIVE_TYPE) {
// Do not change if the player is opening or is opened,
// otherwise the video description cannot be opened.
PlayerType current = PlayerType.getCurrent();
if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) {
Logger.printDebug(() -> "Using original form factor for player");
return original;
}
if (!NavigationBar.isSearchBarActive()) {
// Automotive type shows error 400 when opening a channel page and using some explore tab.
// This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
// Work around the issue by using the original form factor if not in search and the
// navigation back button is present.
if (NavigationBar.isBackButtonVisible()) {
Logger.printDebug(() -> "Using original form factor, as back button is visible without search present");
return original;
}
// Do not change library tab otherwise watch history is hidden.
// Do this check last since the current navigation button is required.
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
return original;
}
}
}
return FORM_FACTOR_TYPE;
} }
/**
* Injection point.
*/
public static void navigationTabCreated(NavigationButton button, View tabView) {
// On first startup of the app the navigation buttons are fetched and updated.
// If the user immediately opens the 'You' or opens a video, then the call to
// update the navigtation buttons will use the non automotive form factor
// and the explore tab is missing.
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
// For now, always hide the explore tab.
if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) {
tabView.setVisibility(View.GONE);
}
}
} }

View File

@ -3,7 +3,9 @@ package app.revanced.extension.youtube.shared;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE; import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE;
import android.app.Activity; import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.view.View; import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -24,12 +26,22 @@ import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class NavigationBar { public final class NavigationBar {
/**
* Interface to call obfuscated methods in AppCompat Toolbar class.
*/
public interface AppCompatToolbarPatchInterface {
Drawable patch_getNavigationIcon();
}
// //
// Search bar // Search and toolbar.
// //
private static volatile WeakReference<View> searchBarResultsRef = new WeakReference<>(null); private static volatile WeakReference<View> searchBarResultsRef = new WeakReference<>(null);
private static volatile WeakReference<AppCompatToolbarPatchInterface> toolbarResultsRef
= new WeakReference<>(null);
/** /**
* Injection point. * Injection point.
*/ */
@ -37,6 +49,22 @@ public final class NavigationBar {
searchBarResultsRef = new WeakReference<>(searchbarResults); searchBarResultsRef = new WeakReference<>(searchbarResults);
} }
/**
* Injection point.
*/
public static void setToolbar(FrameLayout layout) {
AppCompatToolbarPatchInterface toolbar = Utils.getChildView(layout, false, (view) ->
view instanceof AppCompatToolbarPatchInterface
);
if (toolbar == null) {
Logger.printException(() -> "Could not find navigation toolbar");
return;
}
toolbarResultsRef = new WeakReference<>(toolbar);
}
/** /**
* @return If the search bar is on screen. This includes if the player * @return If the search bar is on screen. This includes if the player
* is on screen and the search results are behind the player (and not visible). * is on screen and the search results are behind the player (and not visible).
@ -47,8 +75,13 @@ public final class NavigationBar {
return searchbarResults != null && searchbarResults.getParent() != null; return searchbarResults != null && searchbarResults.getParent() != null;
} }
public static boolean isBackButtonVisible() {
AppCompatToolbarPatchInterface toolbar = toolbarResultsRef.get();
return toolbar != null && toolbar.patch_getNavigationIcon() != null;
}
// //
// Navigation bar buttons // Navigation bar buttons.
// //
/** /**

View File

@ -6,7 +6,9 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.ListPreference import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.patches.youtube.layout.buttons.navigation.navigationButtonsPatch
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.util.getReference import app.revanced.util.getReference
@ -15,7 +17,7 @@ import com.android.tools.smali.dexlib2.Opcode
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.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.FieldReference
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeFormFactorPatch;" private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeFormFactorPatch;"
@Suppress("unused") @Suppress("unused")
val changeFormFactorPatch = bytecodePatch( val changeFormFactorPatch = bytecodePatch(
@ -26,6 +28,7 @@ val changeFormFactorPatch = bytecodePatch(
sharedExtensionPatch, sharedExtensionPatch,
settingsPatch, settingsPatch,
addResourcesPatch, addResourcesPatch,
navigationButtonsPatch
) )
compatibleWith( compatibleWith(
@ -50,6 +53,8 @@ val changeFormFactorPatch = bytecodePatch(
) )
) )
hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)
createPlayerRequestBodyWithModelFingerprint.method.apply { createPlayerRequestBodyWithModelFingerprint.method.apply {
val formFactorEnumClass = formFactorEnumConstructorFingerprint.originalClassDef.type val formFactorEnumClass = formFactorEnumConstructorFingerprint.originalClassDef.type

View File

@ -16,6 +16,23 @@ internal val actionBarSearchResultsFingerprint = fingerprint {
literal { actionBarSearchResultsViewMicId } literal { actionBarSearchResultsViewMicId }
} }
internal val toolbarLayoutFingerprint = fingerprint {
accessFlags(AccessFlags.PROTECTED, AccessFlags.CONSTRUCTOR)
literal { toolbarContainerId }
}
/**
* Matches to https://android.googlesource.com/platform/frameworks/support/+/9eee6ba/v7/appcompat/src/android/support/v7/widget/Toolbar.java#963
*/
internal val appCompatToolbarBackButtonFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Landroid/graphics/drawable/Drawable;")
parameters()
custom { methodDef, classDef ->
classDef.type == "Landroid/support/v7/widget/Toolbar;"
}
}
/** /**
* Matches to the class found in [pivotBarConstructorFingerprint]. * Matches to the class found in [pivotBarConstructorFingerprint].
*/ */

View File

@ -8,6 +8,7 @@ import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.misc.mapping.get import app.revanced.patches.shared.misc.mapping.get
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings import app.revanced.patches.shared.misc.mapping.resourceMappings
@ -18,12 +19,16 @@ import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction 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.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
internal var imageOnlyTabResourceId = -1L internal var imageOnlyTabResourceId = -1L
@ -32,6 +37,8 @@ internal var actionBarSearchResultsViewMicId = -1L
private set private set
internal var ytFillBellId = -1L internal var ytFillBellId = -1L
private set private set
internal var toolbarContainerId = -1L
private set
private val navigationBarHookResourcePatch = resourcePatch { private val navigationBarHookResourcePatch = resourcePatch {
dependsOn(resourceMappingPatch) dependsOn(resourceMappingPatch)
@ -40,6 +47,7 @@ private val navigationBarHookResourcePatch = resourcePatch {
imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"] imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"]
actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"] actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"]
ytFillBellId = resourceMappings["drawable", "yt_fill_bell_black_24"] ytFillBellId = resourceMappings["drawable", "yt_fill_bell_black_24"]
toolbarContainerId = resourceMappings["id", "toolbar_container"]
} }
} }
@ -47,6 +55,8 @@ internal const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/shared/NavigationBar;" "Lapp/revanced/extension/youtube/shared/NavigationBar;"
internal const val EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR = internal const val EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR =
"Lapp/revanced/extension/youtube/shared/NavigationBar\$NavigationButton;" "Lapp/revanced/extension/youtube/shared/NavigationBar\$NavigationButton;"
private const val EXTENSION_TOOLBAR_INTERFACE =
"Lapp/revanced/extension/youtube/shared/NavigationBar${'$'}AppCompatToolbarPatchInterface;"
lateinit var hookNavigationButtonCreated: (String) -> Unit lateinit var hookNavigationButtonCreated: (String) -> Unit
@ -143,11 +153,58 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig
) )
} }
// Hook the back button visibility.
toolbarLayoutFingerprint.method.apply {
val index = indexOfFirstInstructionOrThrow {
opcode == Opcode.CHECK_CAST && getReference<TypeReference>()?.type ==
"Lcom/google/android/apps/youtube/app/ui/actionbar/MainCollapsingToolbarLayout;"
}
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(
index + 1,
"invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->setToolbar(Landroid/widget/FrameLayout;)V"
)
}
// Add interface for extensions code to call obfuscated methods.
appCompatToolbarBackButtonFingerprint.let {
it.classDef.apply {
interfaces.add(EXTENSION_TOOLBAR_INTERFACE)
val definingClass = type
val obfuscatedMethodName = it.originalMethod.name
val returnType = "Landroid/graphics/drawable/Drawable;"
methods.add(
ImmutableMethod(
definingClass,
"patch_getNavigationIcon",
listOf(),
returnType,
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
addInstructions(
0,
"""
invoke-virtual { p0 }, $definingClass->$obfuscatedMethodName()$returnType
move-result-object v0
return-object v0
"""
)
}
)
}
}
hookNavigationButtonCreated = { extensionClassDescriptor -> hookNavigationButtonCreated = { extensionClassDescriptor ->
navigationBarHookCallbackFingerprint.method.addInstruction( navigationBarHookCallbackFingerprint.method.addInstruction(
0, 0,
"invoke-static { p0, p1 }, " + "invoke-static { p0, p1 }, $extensionClassDescriptor->navigationTabCreated" +
"$extensionClassDescriptor->navigationTabCreated" +
"(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V", "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V",
) )
} }

View File

@ -1132,10 +1132,8 @@ Tablet layout
• Community posts are hidden • Community posts are hidden
Automotive layout Automotive layout
• Watch history menu is hidden
• Explore tab is restored
• Shorts open in the regular player • Shorts open in the regular player
• Feed is organized by topics and channel"</string> • Feed is organized by topics and channels"</string>
</patch> </patch>
<patch id="layout.spoofappversion.spoofAppVersionPatch"> <patch id="layout.spoofappversion.spoofAppVersionPatch">
<string name="revanced_spoof_app_version_title">Spoof app version</string> <string name="revanced_spoof_app_version_title">Spoof app version</string>