From aa5c001968446e5270c756256724e917009612cd Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:26:09 +0200 Subject: [PATCH] fix(YouTube - Change form factor): Restore Automotive form factor watch history menu, channel pages, and community posts (#4541) --- .../patches/ChangeFormFactorPatch.java | 59 ++++++++++++++++-- .../youtube/shared/NavigationBar.java | 37 ++++++++++- .../formfactor/ChangeFormFactorPatch.kt | 7 ++- .../youtube/misc/navigation/Fingerprints.kt | 17 ++++++ .../misc/navigation/NavigationBarHookPatch.kt | 61 ++++++++++++++++++- .../resources/addresources/values/strings.xml | 4 +- 6 files changed, 173 insertions(+), 12 deletions(-) diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java index a974f322b..706b135af 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java @@ -1,9 +1,17 @@ package app.revanced.extension.youtube.patches; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.view.View; + 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.shared.NavigationBar; +import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public class ChangeFormFactorPatch { @@ -41,14 +49,57 @@ public class ChangeFormFactorPatch { @Nullable 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. */ public static int getFormFactor(int original) { - return FORM_FACTOR_TYPE == null - ? original - : FORM_FACTOR_TYPE; + if (FORM_FACTOR_TYPE == null) return original; + + 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); + } + } } \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java index 6f2deca16..17a509f00 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java @@ -3,7 +3,9 @@ package app.revanced.extension.youtube.shared; import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE; import android.app.Activity; +import android.graphics.drawable.Drawable; import android.view.View; +import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -24,12 +26,22 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") 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 searchBarResultsRef = new WeakReference<>(null); + private static volatile WeakReference toolbarResultsRef + = new WeakReference<>(null); + /** * Injection point. */ @@ -37,6 +49,22 @@ public final class NavigationBar { 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 * 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; } + public static boolean isBackButtonVisible() { + AppCompatToolbarPatchInterface toolbar = toolbarResultsRef.get(); + return toolbar != null && toolbar.patch_getNavigationIcon() != null; + } + // - // Navigation bar buttons + // Navigation bar buttons. // /** diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt index bf800b643..b516e90b5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt @@ -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.addResourcesPatch 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.navigation.hookNavigationButtonCreated import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch 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.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") val changeFormFactorPatch = bytecodePatch( @@ -26,6 +28,7 @@ val changeFormFactorPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, addResourcesPatch, + navigationButtonsPatch ) compatibleWith( @@ -50,6 +53,8 @@ val changeFormFactorPatch = bytecodePatch( ) ) + hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR) + createPlayerRequestBodyWithModelFingerprint.method.apply { val formFactorEnumClass = formFactorEnumConstructorFingerprint.originalClassDef.type diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt index c64cf7594..9df0cda0c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt @@ -16,6 +16,23 @@ internal val actionBarSearchResultsFingerprint = fingerprint { 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]. */ diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt index 902567eb8..d9d7c6db4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -8,6 +8,7 @@ import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch 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.resourceMappingPatch 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.indexOfFirstInstructionReversedOrThrow 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.builder.MutableMethodImplementation 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.OneRegisterInstruction 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.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.util.MethodUtil internal var imageOnlyTabResourceId = -1L @@ -32,6 +37,8 @@ internal var actionBarSearchResultsViewMicId = -1L private set internal var ytFillBellId = -1L private set +internal var toolbarContainerId = -1L + private set private val navigationBarHookResourcePatch = resourcePatch { dependsOn(resourceMappingPatch) @@ -40,6 +47,7 @@ private val navigationBarHookResourcePatch = resourcePatch { imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"] actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"] 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;" internal const val EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR = "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 @@ -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()?.type == + "Lcom/google/android/apps/youtube/app/ui/actionbar/MainCollapsingToolbarLayout;" + } + val register = getInstruction(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 -> navigationBarHookCallbackFingerprint.method.addInstruction( 0, - "invoke-static { p0, p1 }, " + - "$extensionClassDescriptor->navigationTabCreated" + + "invoke-static { p0, p1 }, $extensionClassDescriptor->navigationTabCreated" + "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V", ) } diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 7dd13faa8..08e3b475c 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1132,10 +1132,8 @@ Tablet layout • Community posts are hidden Automotive layout -• Watch history menu is hidden -• Explore tab is restored • Shorts open in the regular player -• Feed is organized by topics and channel" +• Feed is organized by topics and channels" Spoof app version