From 86e5c1b45c161a925750b4c14070f1825fbcc0f6 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:37:56 +0900 Subject: [PATCH] feat(YouTube): Add `Change form factor`, Remove `Change layout` patch --- .../extension/shared/settings/Setting.java | 7 +- .../AbstractPreferenceFragment.java | 113 ++++++++----- .../extension/shared/utils/Utils.java | 133 +++++++++++++-- .../AlternativeThumbnailsPatch.java | 15 +- .../components/KeywordContentFilter.java | 15 +- .../general/ChangeFormFactorPatch.java | 151 ++++++++++++++++++ .../patches/general/LayoutSwitchPatch.java | 79 --------- .../extension/youtube/settings/Settings.java | 4 +- .../ReVancedPreferenceFragment.java | 2 +- .../ReVancedSettingsPreference.java | 4 +- .../youtube/shared/NavigationBar.java | 4 + .../extension/youtube/shared/RootView.java | 39 +++++ .../ChangeFormFactorPatch.kt} | 49 +++--- .../Fingerprints.kt | 21 +-- .../navigation/NavigationBarHookPatch.kt | 3 +- .../patches/youtube/utils/patch/PatchList.kt | 6 +- .../youtube/utils/playertype/Fingerprints.kt | 30 ++++ .../utils/playertype/PlayerTypeHookPatch.kt | 54 +++++++ .../utils/resourceid/SharedResourceIdPatch.kt | 6 + .../youtube/settings/host/values/arrays.xml | 26 +-- .../youtube/settings/host/values/strings.xml | 22 ++- .../youtube/settings/xml/revanced_prefs.xml | 8 +- 22 files changed, 579 insertions(+), 212 deletions(-) create mode 100644 extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java delete mode 100644 extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java rename patches/src/main/kotlin/app/revanced/patches/youtube/general/{layoutswitch/LayoutSwitchPatch.kt => formfactor/ChangeFormFactorPatch.kt} (65%) rename patches/src/main/kotlin/app/revanced/patches/youtube/general/{layoutswitch => formfactor}/Fingerprints.kt (71%) diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java index e3a769f67..8d8c567d9 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -24,9 +24,6 @@ import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.StringRef; import app.revanced.extension.shared.utils.Utils; -/** - * @noinspection rawtypes - */ @SuppressWarnings("unused") public abstract class Setting { @@ -128,6 +125,7 @@ public abstract class Setting { /** * @return All settings that have been created, sorted by keys. + * @noinspection Java8ListSort */ @NonNull private static List> allLoadedSettingsSorted() { @@ -171,7 +169,6 @@ public abstract class Setting { /** * Confirmation message to display, if the user tries to change the setting from the default value. - * Currently this works only for Boolean setting types. */ @Nullable public final StringRef userDialogMessage; @@ -271,6 +268,7 @@ public abstract class Setting { *

* This method will be deleted in the future. */ + @SuppressWarnings("rawtypes") public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { if (!oldPrefs.preferences.contains(settingKey)) { return; // Nothing to do. @@ -452,6 +450,7 @@ public abstract class Setting { boolean rebootSettingChanged = false; int numberOfSettingsImported = 0; + //noinspection rawtypes for (Setting setting : SETTINGS) { String key = setting.getImportExportKey(); if (json.has(key)) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java index b2bac3d67..168ef7375 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -12,6 +12,7 @@ import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.preference.SwitchPreference; @@ -21,6 +22,9 @@ import android.widget.ListView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Objects; + +import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.utils.Logger; @@ -48,10 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { - if (str == null) { - return; - } - Setting setting = Setting.getSettingFromPath(str); + Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str)); if (setting == null) { return; } @@ -59,24 +60,23 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { if (pref == null) { return; } + Logger.printDebug(() -> "Preference changed: " + setting.key); + + if (!settingImportInProgress && !showingUserDialogMessage) { + if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) { + // Do not change the setting yet, to allow preserving whatever + // list/text value was previously set if it needs to be reverted. + showSettingUserDialogConfirmation(pref, setting); + return; + } else if (setting.rebootApp) { + showRestartDialog(getContext()); + } + } // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. updatePreference(pref, setting, true, settingImportInProgress); // Update any other preference availability that may now be different. updateUIAvailability(); - - if (settingImportInProgress) { - return; - } - - if (!showingUserDialogMessage) { - if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { - showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting); - } else if (setting.rebootApp) { - showRestartDialog(getActivity()); - } - } - } catch (Exception ex) { Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); } @@ -90,14 +90,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { * so all app specific {@link Setting} instances are loaded before this method returns. */ protected void initialize() { - final int id = getXmlIdentifier("revanced_prefs"); + final int identifier = getXmlIdentifier("revanced_prefs"); + if (identifier == 0) return; + addPreferencesFromResource(identifier); - if (id == 0) return; - addPreferencesFromResource(id); - Utils.sortPreferenceGroups(getPreferenceScreen()); + PreferenceScreen screen = getPreferenceScreen(); + Utils.sortPreferenceGroups(screen); + Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); } - private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { + private void showSettingUserDialogConfirmation(Preference pref, Setting setting) { Utils.verifyOnMainThread(); final var context = getActivity(); @@ -107,12 +109,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { .setTitle(android.R.string.dialog_alert_title) .setMessage(setting.userDialogMessage.toString()) .setPositiveButton(android.R.string.ok, (dialog, id) -> { + // User confirmed, save to the Setting. + updatePreference(pref, setting, true, false); + + // Update availability of other preferences that may be changed. + updateUIAvailability(); + if (setting.rebootApp) { showRestartDialog(context); } }) .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. + // Restore whatever the setting was before the change. + updatePreference(pref, setting, true, true); }) .setOnDismissListener(dialog -> showingUserDialogMessage = false) .setCancelable(false) @@ -123,7 +132,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { * Updates all Preferences values and their availability using the current values in {@link Setting}. */ protected void updateUIToSettingValues() { - updatePreferenceScreen(getPreferenceScreen(), true, true); + updatePreferenceScreen(getPreferenceScreen(), true,true); } /** @@ -133,24 +142,48 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { updatePreferenceScreen(getPreferenceScreen(), false, false); } + /** + * @return If the preference is currently set to the default value of the Setting. + */ + protected boolean prefIsSetToDefault(Preference pref, Setting setting) { + if (pref instanceof SwitchPreference switchPref) { + return switchPref.isChecked() == (Boolean) setting.defaultValue; + } + if (pref instanceof EditTextPreference editPreference) { + return editPreference.getText().equals(setting.defaultValue.toString()); + } + if (pref instanceof ListPreference listPref) { + return listPref.getValue().equals(setting.defaultValue.toString()); + } + + throw new IllegalStateException("Must override method to handle " + + "preference type: " + pref.getClass()); + } + + /** * Syncs all UI Preferences to any {@link Setting} they represent. */ - private void updatePreferenceScreen(@NonNull PreferenceScreen screen, + private void updatePreferenceScreen(@NonNull PreferenceGroup group, boolean syncSettingValue, boolean applySettingToPreference) { // Alternatively this could iterate thru all Settings and check for any matching Preferences, // but there are many more Settings than UI preferences so it's more efficient to only check // the Preferences. - for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) { - Preference pref = screen.getPreference(i); - if (pref instanceof PreferenceScreen preferenceScreen) { - updatePreferenceScreen(preferenceScreen, syncSettingValue, applySettingToPreference); + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + if (pref instanceof PreferenceGroup subGroup) { + updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference); } else if (pref.hasKey()) { String key = pref.getKey(); Setting setting = Setting.getSettingFromPath(key); + if (setting != null) { updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } else if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && (pref instanceof SwitchPreference + || pref instanceof EditTextPreference || pref instanceof ListPreference)) { + // Probably a typo in the patches preference declaration. + Logger.printException(() -> "Preference key has no setting: " + key); } } } @@ -166,26 +199,26 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { protected void syncSettingWithPreference(@NonNull Preference pref, @NonNull Setting setting, boolean applySettingToPreference) { - if (pref instanceof SwitchPreference switchPreference) { + if (pref instanceof SwitchPreference switchPref) { BooleanSetting boolSetting = (BooleanSetting) setting; if (applySettingToPreference) { - switchPreference.setChecked(boolSetting.get()); + switchPref.setChecked(boolSetting.get()); } else { - BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked()); + BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); } - } else if (pref instanceof EditTextPreference editTextPreference) { + } else if (pref instanceof EditTextPreference editPreference) { if (applySettingToPreference) { - editTextPreference.setText(setting.get().toString()); + editPreference.setText(setting.get().toString()); } else { - Setting.privateSetValueFromString(setting, editTextPreference.getText()); + Setting.privateSetValueFromString(setting, editPreference.getText()); } - } else if (pref instanceof ListPreference listPreference) { + } else if (pref instanceof ListPreference listPref) { if (applySettingToPreference) { - listPreference.setValue(setting.get().toString()); + listPref.setValue(setting.get().toString()); } else { - Setting.privateSetValueFromString(setting, listPreference.getValue()); + Setting.privateSetValueFromString(setting, listPref.getValue()); } - updateListPreferenceSummary(listPreference, setting); + updateListPreferenceSummary(listPref, setting); } else { Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref); } @@ -194,7 +227,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { /** * Updates a UI Preference with the {@link Setting} that backs it. * - * @param syncSetting If the UI should be synced {@link Setting} <-> Preference + * @param syncSetting If the UI should be synced {@link Setting} <-> Preference * @param applySettingToPreference If true, then apply {@link Setting} -> Preference. * If false, then apply {@link Setting} <- Preference. */ diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java index 66ffd33cc..8aeafe5dc 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -3,6 +3,8 @@ package app.revanced.extension.shared.utils; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; import android.app.Fragment; import android.content.ClipboardManager; import android.content.Context; @@ -13,6 +15,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.Preference; @@ -43,11 +46,11 @@ import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; -import kotlin.text.Regex; @SuppressWarnings("deprecation") public class Utils { @@ -529,6 +532,81 @@ public class Utils { } } + + /** + * Ignore this class. It must be public to satisfy Android requirements. + */ + public static final class DialogFragmentWrapper extends DialogFragment { + + private Dialog dialog; + @Nullable + private DialogFragmentOnStartAction onStartAction; + + @Override + public void onSaveInstanceState(Bundle outState) { + // Do not call super method to prevent state saving. + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return dialog; + } + + @Override + public void onStart() { + try { + super.onStart(); + + if (onStartAction != null) { + onStartAction.onStart((AlertDialog) getDialog()); + } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex); + } + } + } + + /** + * Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}. + */ + @FunctionalInterface + public interface DialogFragmentOnStartAction { + void onStart(AlertDialog dialog); + } + + public static void showDialog(Activity activity, AlertDialog dialog) { + showDialog(activity, dialog, true, null); + } + + /** + * Utility method to allow showing an AlertDialog on top of other alert dialogs. + * Calling this will always display the dialog on top of all other dialogs + * previously called using this method. + *
+ * Be aware the on start action can be called multiple times for some situations, + * such as the user switching apps without dismissing the dialog then switching back to this app. + *
+ * This method is only useful during app startup and multiple patches may show their own dialog, + * and the most important dialog can be called last (using a delay) so it's always on top. + *
+ * For all other situations it's better to not use this method and + * call {@link AlertDialog#show()} on the dialog. + */ + public static void showDialog(Activity activity, + AlertDialog dialog, + boolean isCancelable, + @Nullable DialogFragmentOnStartAction onStartAction) { + verifyOnMainThread(); + + DialogFragmentWrapper fragment = new DialogFragmentWrapper(); + fragment.dialog = dialog; + fragment.onStartAction = onStartAction; + fragment.setCancelable(isCancelable); + + fragment.show(activity.getFragmentManager(), null); + } + /** * Safe to call from any thread */ @@ -737,14 +815,14 @@ public class Utils { } } - private static final Regex punctuationRegex = new Regex("\\p{P}+"); + private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+"); /** * Strips all punctuation and converts to lower case. A null parameter returns an empty string. */ public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) { if (original == null) return ""; - return punctuationRegex.replace(original, "").toLowerCase(); + return punctuationPattern.matcher(original).replaceAll("").toLowerCase(); } /** @@ -763,8 +841,8 @@ public class Utils { Preference preference = group.getPreference(i); final Sort preferenceSort; - if (preference instanceof PreferenceGroup preferenceGroup) { - sortPreferenceGroups(preferenceGroup); + if (preference instanceof PreferenceGroup subGroup) { + sortPreferenceGroups(subGroup); preferenceSort = groupSort; // Sort value for groups is for it's content, not itself. } else { // Allow individual preferences to set a key sorting. @@ -774,13 +852,16 @@ public class Utils { final String sortValue; switch (preferenceSort) { - case BY_TITLE -> - sortValue = removePunctuationConvertToLowercase(preference.getTitle()); - case BY_KEY -> sortValue = preference.getKey(); - case UNSORTED -> { + case BY_TITLE: + sortValue = removePunctuationConvertToLowercase(preference.getTitle()); + break; + case BY_KEY: + sortValue = preference.getKey(); + break; + case UNSORTED: continue; // Keep original sorting. - } - default -> throw new IllegalStateException(); + default: + throw new IllegalStateException(); } preferences.put(sortValue, preference); @@ -790,7 +871,7 @@ public class Utils { for (Preference pref : preferences.values()) { int order = index++; - // If the preference is a PreferenceScreen or is an intent preference, move to the top. + // Move any screens, intents, and the one off About preference to the top. if (pref instanceof PreferenceScreen || pref.getIntent() != null) { // Arbitrary high number. order -= 1000; @@ -799,4 +880,32 @@ public class Utils { pref.setOrder(order); } } + + /** + * Set all preferences to multiline titles if the device is not using an English variant. + * The English strings are heavily scrutinized and all titles fit on screen + * except 2 or 3 preference strings and those do not affect readability. + *

+ * Allowing multiline for those 2 or 3 English preferences looks weird and out of place, + * and visually it looks better to clip the text and keep all titles 1 line. + */ + public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) { + if (!isSDKAbove(26)) { + return; + } + + String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage(); + if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) { + return; + } + + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + pref.setSingleLineTitle(false); + + if (pref instanceof PreferenceGroup subGroup) { + setPreferenceTitlesToMultiLineIfNeeded(subGroup); + } + } + } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java index aa9750853..f060d2a56 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java @@ -189,14 +189,13 @@ public final class AlternativeThumbnailsPatch { // Unknown tab, treat as the home tab; return homeOption; } - if (selectedNavButton == NavigationButton.HOME) { - return homeOption; - } - if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) { - return subscriptionsOption; - } - // A library tab variant is active. - return libraryOption; + + return switch (selectedNavButton) { + case SUBSCRIPTIONS, NOTIFICATIONS -> subscriptionsOption; + case LIBRARY -> libraryOption; + // Home or explore tab. + default -> homeOption; + }; } /** diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java index bef4712de..9c162d025 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java @@ -556,14 +556,13 @@ public final class KeywordContentFilter extends Filter { if (selectedNavButton == null) { return hideHome; // Unknown tab, treat the same as home. } - if (selectedNavButton == NavigationButton.HOME) { - return hideHome; - } - if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { - return hideSubscriptions; - } - // User is in the Library or Notifications tab. - return false; + + return switch (selectedNavButton) { + case HOME, EXPLORE -> hideHome; + case SUBSCRIPTIONS -> hideSubscriptions; + // User is in the Library or notifications. + default -> false; + }; } private void updateStats(boolean videoWasHidden, @Nullable String keyword) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java new file mode 100644 index 000000000..4fa3779fc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java @@ -0,0 +1,151 @@ +package app.revanced.extension.youtube.patches.general; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.view.View; + +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.PackageUtils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("unused") +public class ChangeFormFactorPatch { + + public enum FormFactor { + /** + * Unmodified, and same as un-patched. + */ + DEFAULT(null, null, null), + /** + *

+         * Some changes include:
+         * - Explore tab is present.
+         * - watch history is missing.
+         * - feed thumbnails fade in.
+         */
+        UNKNOWN(0, null, null),
+        SMALL(1, null, TRUE),
+        SMALL_WIDTH_DP(1, 480, TRUE),
+        LARGE(2, null, FALSE),
+        LARGE_WIDTH_DP(2, 600, FALSE),
+        /**
+         * Cars with 'Google built-in'.
+         * Layout seems identical to {@link #UNKNOWN}
+         * even when using an Android Automotive device.
+         */
+        AUTOMOTIVE(3, null, null),
+        WEARABLE(4, null, null);
+
+        @Nullable
+        final Integer formFactorType;
+
+        @Nullable
+        final Integer widthDp;
+
+        @Nullable
+        final Boolean setMinimumDp;
+
+
+        FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
+            this.formFactorType = formFactorType;
+            this.widthDp = widthDp;
+            this.setMinimumDp = setMinimumDp;
+        }
+
+        private boolean setMinimumDp() {
+            return BooleanUtils.isTrue(setMinimumDp);
+        }
+    }
+
+    private static final FormFactor FORM_FACTOR = Settings.CHANGE_FORM_FACTOR.get();
+    @Nullable
+    private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.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) {
+        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 (!RootView.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 (RootView.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 int getWidthDp(int original) {
+        if (FORM_FACTOR_TYPE == null) return original;
+        Integer widthDp = FORM_FACTOR.widthDp;
+        if (widthDp == null) {
+            return original;
+        }
+        final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
+        if (smallestScreenWidthDp == 0) {
+            return original;
+        }
+        return FORM_FACTOR.setMinimumDp()
+                ? Math.min(smallestScreenWidthDp, widthDp)
+                : Math.max(smallestScreenWidthDp, widthDp);
+    }
+
+    public static boolean phoneLayoutEnabled() {
+        return Objects.equals(FORM_FACTOR.formFactorType, 1);
+    }
+
+    public static boolean tabletLayoutEnabled() {
+        return Objects.equals(FORM_FACTOR.formFactorType, 2);
+    }
+
+    /**
+     * 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/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java
deleted file mode 100644
index 56d343080..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package app.revanced.extension.youtube.patches.general;
-
-import static java.lang.Boolean.FALSE;
-import static java.lang.Boolean.TRUE;
-
-import androidx.annotation.Nullable;
-
-import org.apache.commons.lang3.BooleanUtils;
-
-import java.util.Objects;
-
-import app.revanced.extension.shared.utils.PackageUtils;
-import app.revanced.extension.youtube.settings.Settings;
-
-@SuppressWarnings("unused")
-public final class LayoutSwitchPatch {
-
-    public enum FormFactor {
-        /**
-         * Unmodified type, and same as un-patched.
-         */
-        ORIGINAL(null, null, null),
-        SMALL_FORM_FACTOR(1, null, TRUE),
-        SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE),
-        LARGE_FORM_FACTOR(2, null, FALSE),
-        LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE);
-
-        @Nullable
-        final Integer formFactorType;
-
-        @Nullable
-        final Integer widthDp;
-
-        @Nullable
-        final Boolean setMinimumDp;
-
-        FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
-            this.formFactorType = formFactorType;
-            this.widthDp = widthDp;
-            this.setMinimumDp = setMinimumDp;
-        }
-
-        private boolean setMinimumDp() {
-            return BooleanUtils.isTrue(setMinimumDp);
-        }
-    }
-
-    private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get();
-
-    public static int getFormFactor(int original) {
-        Integer formFactorType = FORM_FACTOR.formFactorType;
-        return formFactorType == null
-                ? original
-                : formFactorType;
-    }
-
-    public static int getWidthDp(int original) {
-        Integer widthDp = FORM_FACTOR.widthDp;
-        if (widthDp == null) {
-            return original;
-        }
-        final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
-        if (smallestScreenWidthDp == 0) {
-            return original;
-        }
-        return FORM_FACTOR.setMinimumDp()
-                ? Math.min(smallestScreenWidthDp, widthDp)
-                : Math.max(smallestScreenWidthDp, widthDp);
-    }
-
-    public static boolean phoneLayoutEnabled() {
-        return Objects.equals(FORM_FACTOR.formFactorType, 1);
-    }
-
-    public static boolean tabletLayoutEnabled() {
-        return Objects.equals(FORM_FACTOR.formFactorType, 2);
-    }
-
-}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 2551ef43d..be1315321 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -32,9 +32,9 @@ import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeT
 import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
 import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
 import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
+import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch.FormFactor;
 import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch;
 import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage;
-import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
 import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
 import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode;
 import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
@@ -154,7 +154,7 @@ public class Settings extends BaseSettings {
     public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
     public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
 
-    public static final EnumSetting CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true);
+    public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
     public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
     public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
     public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index d5abb90e7..5eab38ba7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -75,7 +75,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
     private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
         try {
             if (str == null) return;
-            Setting setting = Setting.getSettingFromPath(str);
+            Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
 
             if (setting == null) return;
 
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
index 43ee95719..c3c22520b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
@@ -8,7 +8,7 @@ import android.preference.Preference;
 import android.preference.SwitchPreference;
 
 import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch;
+import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
 import app.revanced.extension.youtube.patches.utils.PatchStatus;
 import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
 import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
@@ -82,7 +82,7 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
      */
     private static void TabletLayoutLinks() {
         final boolean isTablet = ExtendedUtils.isTablet() &&
-                !LayoutSwitchPatch.phoneLayoutEnabled();
+                !ChangeFormFactorPatch.phoneLayoutEnabled();
 
         enableDisablePreferences(
                 isTablet,
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
index 3c65e1d0c..469e8aa9b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
@@ -223,6 +223,10 @@ public final class NavigationBar {
          * This tab will never be in a selected state, even if the create video UI is on screen.
          */
         CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
+        /**
+         * Only shown to automotive layout.
+         */
+        EXPLORE("TAB_EXPLORE"),
         SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
         /**
          * Notifications tab.  Only present when
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
index 1a6d97edd..3d18b32b2 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
@@ -2,9 +2,48 @@ package app.revanced.extension.youtube.shared;
 
 import static app.revanced.extension.youtube.patches.components.RelatedVideoFilter.isActionBarVisible;
 
+import android.graphics.drawable.Drawable;
+import android.widget.FrameLayout;
+
+import java.lang.ref.WeakReference;
+
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.Utils;
+
 @SuppressWarnings("unused")
 public final class RootView {
 
+    /**
+     * Interface to call obfuscated methods in AppCompat Toolbar class.
+     */
+    public interface AppCompatToolbarPatchInterface {
+        Drawable patch_getToolbarIcon();
+    }
+
+    private static volatile WeakReference toolbarResultsRef
+            = new WeakReference<>(null);
+
+    /**
+     * 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 toolbar");
+            return;
+        }
+
+        toolbarResultsRef = new WeakReference<>(toolbar);
+    }
+
+    public static boolean isBackButtonVisible() {
+        AppCompatToolbarPatchInterface toolbar = toolbarResultsRef.get();
+        return toolbar != null && toolbar.patch_getToolbarIcon() != null;
+    }
+
     /**
      * @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).
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatch.kt
similarity index 65%
rename from patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt
rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatch.kt
index 26ec6af6d..4b89c7782 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatch.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.youtube.general.layoutswitch
+package app.revanced.patches.youtube.general.formfactor
 
 import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
@@ -6,30 +6,37 @@ import app.revanced.patcher.patch.bytecodePatch
 import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
 import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH
-import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LAYOUT
+import app.revanced.patches.youtube.utils.navigation.hookNavigationButtonCreated
+import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch
+import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_FORM_FACTOR
+import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
 import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
 import app.revanced.patches.youtube.utils.settings.settingsPatch
 import app.revanced.util.fingerprint.definingClassOrThrow
+import app.revanced.util.fingerprint.matchOrThrow
 import app.revanced.util.fingerprint.methodOrThrow
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstructionOrThrow
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.reference.FieldReference
 
 private const val EXTENSION_CLASS_DESCRIPTOR =
-    "$GENERAL_PATH/LayoutSwitchPatch;"
+    "$GENERAL_PATH/ChangeFormFactorPatch;"
 
 @Suppress("unused")
-val layoutSwitchPatch = bytecodePatch(
-    CHANGE_LAYOUT.title,
-    CHANGE_LAYOUT.summary,
+val changeFormFactorPatch = bytecodePatch(
+    CHANGE_FORM_FACTOR.title,
+    CHANGE_FORM_FACTOR.summary,
 ) {
     compatibleWith(COMPATIBLE_PACKAGE)
 
-    dependsOn(settingsPatch)
+    dependsOn(
+        settingsPatch,
+        playerTypeHookPatch,
+        navigationBarHookPatch,
+    )
 
     execute {
 
@@ -53,27 +60,31 @@ val layoutSwitchPatch = bytecodePatch(
             )
         }
 
-        layoutSwitchFingerprint.methodOrThrow().apply {
-            val index = indexOfFirstInstructionReversedOrThrow(Opcode.IF_NEZ)
-            val register = getInstruction(index).registerA
+        widthDpUIFingerprint.matchOrThrow().let {
+            it.method.apply {
+                val index = it.patternMatch!!.startIndex
+                val register = getInstruction(index).registerA
 
-            addInstructions(
-                index, """
-                    invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I
-                    move-result v$register
-                    """
-            )
+                addInstructions(
+                    index, """
+                        invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I
+                        move-result v$register
+                        """
+                )
+            }
         }
 
+        hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)
+
         // region add settings
 
         addPreference(
             arrayOf(
                 "PREFERENCE_SCREEN: GENERAL",
                 "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS",
-                "SETTINGS: CHANGE_LAYOUT"
+                "SETTINGS: CHANGE_FORM_FACTOR"
             ),
-            CHANGE_LAYOUT
+            CHANGE_FORM_FACTOR
         )
 
         // endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/Fingerprints.kt
similarity index 71%
rename from patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt
rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/Fingerprints.kt
index f5d8e5b4b..569d6aed2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/Fingerprints.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.youtube.general.layoutswitch
+package app.revanced.patches.youtube.general.formfactor
 
 import app.revanced.util.fingerprint.legacyFingerprint
 import app.revanced.util.or
@@ -11,20 +11,16 @@ internal val formFactorEnumConstructorFingerprint = legacyFingerprint(
     strings = listOf(
         "UNKNOWN_FORM_FACTOR",
         "SMALL_FORM_FACTOR",
-        "LARGE_FORM_FACTOR"
+        "LARGE_FORM_FACTOR",
+        "AUTOMOTIVE_FORM_FACTOR",
     )
 )
 
-internal val layoutSwitchFingerprint = legacyFingerprint(
-    name = "layoutSwitchFingerprint",
+internal val widthDpUIFingerprint = legacyFingerprint(
+    name = "widthDpUIFingerprint",
     returnType = "I",
     accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
-    parameters = listOf("L"),
     opcodes = listOf(
-        Opcode.INVOKE_VIRTUAL,
-        Opcode.MOVE_RESULT_OBJECT,
-        Opcode.INVOKE_STATIC,
-        Opcode.MOVE_RESULT,
         Opcode.IF_NEZ,
         Opcode.CONST_4,
         Opcode.RETURN,
@@ -41,6 +37,11 @@ internal val layoutSwitchFingerprint = legacyFingerprint(
         Opcode.CONST_4,
         Opcode.RETURN,
         Opcode.CONST_4,
-        Opcode.RETURN
+        Opcode.RETURN,
+    ),
+    literals = listOf(
+        480L,
+        600L,
+        720L
     )
 )
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt
index 27a267ce9..5d351f608 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt
@@ -109,8 +109,7 @@ val navigationBarHookPatch = bytecodePatch(
         hookNavigationButtonCreated = { extensionClassDescriptor ->
             navigationBarHookCallbackFingerprint.methodOrThrow().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/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
index d56f8fb1d..c8dca6a6f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
@@ -21,9 +21,9 @@ internal enum class PatchList(
         "Bypass URL redirects",
         "Adds an option to bypass URL redirects and open the original URL directly."
     ),
-    CHANGE_LAYOUT(
-        "Change layout",
-        "Adds an option to change the dp in order to use a tablet or phone layout."
+    CHANGE_FORM_FACTOR(
+        "Change form factor",
+        "Adds an option to change the UI appearance to a phone, tablet, or automotive device."
     ),
     CHANGE_LIVE_RING_CLICK_ACTION(
         "Change live ring click action",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt
index f7208bf7b..3a1f528b5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt
@@ -1,6 +1,7 @@
 package app.revanced.patches.youtube.utils.playertype
 
 import app.revanced.patches.youtube.utils.resourceid.reelWatchPlayer
+import app.revanced.patches.youtube.utils.resourceid.toolbarContainerId
 import app.revanced.util.fingerprint.legacyFingerprint
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstruction
@@ -9,6 +10,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.Method
 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
 
 internal val browseIdClassFingerprint = legacyFingerprint(
     name = "browseIdClassFingerprint",
@@ -61,6 +63,34 @@ internal val searchQueryClassFingerprint = legacyFingerprint(
     }
 )
 
+internal val toolbarLayoutFingerprint = legacyFingerprint(
+    name = "toolbarLayoutFingerprint",
+    literals = listOf(toolbarContainerId),
+    customFingerprint = { method, _ ->
+        method.name == "" &&
+                indexOfMainCollapsingToolbarLayoutInstruction(method) >= 0
+    }
+)
+
+internal fun indexOfMainCollapsingToolbarLayoutInstruction(method: Method) =
+    method.indexOfFirstInstruction {
+        opcode == Opcode.CHECK_CAST &&
+                getReference()?.type == "Lcom/google/android/apps/youtube/app/ui/actionbar/MainCollapsingToolbarLayout;"
+    }
+
+/**
+ * Matches to https://android.googlesource.com/platform/frameworks/support/+/9eee6ba/v7/appcompat/src/android/support/v7/widget/Toolbar.java#963
+ */
+internal val appCompatToolbarBackButtonFingerprint = legacyFingerprint(
+    name = "appCompatToolbarBackButtonFingerprint",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+    returnType = "Landroid/graphics/drawable/Drawable;",
+    parameters = emptyList(),
+    customFingerprint =  { _, classDef ->
+        classDef.type == "Landroid/support/v7/widget/Toolbar;"
+    },
+)
+
 internal val videoStateFingerprint = legacyFingerprint(
     name = "videoStateFingerprint",
     returnType = "V",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt
index 3bcb4e63f..6fe33c7d2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt
@@ -5,6 +5,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
 import app.revanced.patcher.patch.PatchException
 import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
 import app.revanced.patches.shared.litho.addLithoFilter
 import app.revanced.patches.shared.litho.lithoFilterPatch
 import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
@@ -21,10 +22,13 @@ import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstructionOrThrow
 import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
 import app.revanced.util.indexOfFirstStringInstructionOrThrow
+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.OneRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
 import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
 
 private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR =
     "$UTILS_PATH/PlayerTypeHookPatch;"
@@ -32,6 +36,9 @@ private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR =
 private const val EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR =
     "$SHARED_PATH/RootView;"
 
+private const val EXTENSION_ROOT_VIEW_TOOLBAR_INTERFACE =
+    "$SHARED_PATH/RootView${'$'}AppCompatToolbarPatchInterface;"
+
 private const val FILTER_CLASS_DESCRIPTOR =
     "$COMPONENTS_PATH/RelatedVideoFilter;"
 
@@ -165,6 +172,53 @@ val playerTypeHookPatch = bytecodePatch(
 
         // endregion
 
+        // region patch for hook back button visibility
+
+        toolbarLayoutFingerprint.methodOrThrow().apply {
+            val index = indexOfMainCollapsingToolbarLayoutInstruction(this)
+            val register = getInstruction(index).registerA
+
+            addInstruction(
+                index + 1,
+                "invoke-static { v$register }, $EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR->setToolbar(Landroid/widget/FrameLayout;)V"
+            )
+        }
+
+        // Add interface for extensions code to call obfuscated methods.
+        appCompatToolbarBackButtonFingerprint.matchOrThrow().let {
+            it.classDef.apply {
+                interfaces.add(EXTENSION_ROOT_VIEW_TOOLBAR_INTERFACE)
+
+                val definingClass = type
+                val obfuscatedMethodName = it.originalMethod.name
+                val returnType = "Landroid/graphics/drawable/Drawable;"
+
+                methods.add(
+                    ImmutableMethod(
+                        definingClass,
+                        "patch_getToolbarIcon",
+                        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
+                             """
+                        )
+                    }
+                )
+            }
+        }
+
+        // endregion
+
         addLithoFilter(FILTER_CLASS_DESCRIPTOR)
     }
 }
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt
index 654130e91..6bf586431 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt
@@ -213,6 +213,8 @@ var tapBloomView = -1L
     private set
 var titleAnchor = -1L
     private set
+var toolbarContainerId = -1L
+    private set
 var toolTipContentView = -1L
     private set
 var totalTime = -1L
@@ -656,6 +658,10 @@ internal val sharedResourceIdPatch = resourcePatch(
             ID,
             "title_anchor"
         ]
+        toolbarContainerId = resourceMappings[
+            ID,
+            "toolbar_container"
+        ]
         toolTipContentView = resourceMappings[
             LAYOUT,
             "tooltip_content_view"
diff --git a/patches/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml
index c3dd9f126..9ca518f23 100644
--- a/patches/src/main/resources/youtube/settings/host/values/arrays.xml
+++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml
@@ -22,19 +22,21 @@
         MIDDLE
         END
     
-    
-        @string/revanced_change_layout_entry_1
-        @string/revanced_change_layout_entry_2
-        @string/revanced_change_layout_entry_3
-        @string/revanced_change_layout_entry_4
-        @string/revanced_change_layout_entry_5
+    
+        @string/revanced_change_form_factor_entry_1
+        @string/revanced_change_form_factor_entry_2
+        @string/revanced_change_form_factor_entry_3
+        @string/revanced_change_form_factor_entry_4
+        @string/revanced_change_form_factor_entry_5
+        @string/revanced_change_form_factor_entry_6
     
-    
-        ORIGINAL
-        SMALL_FORM_FACTOR
-        SMALL_FORM_FACTOR_WIDTH_DP
-        LARGE_FORM_FACTOR
-        LARGE_FORM_FACTOR_WIDTH_DP
+    
+        DEFAULT
+        SMALL
+        SMALL_WIDTH_DP
+        LARGE
+        LARGE_WIDTH_DP
+        AUTOMOTIVE
     
     
         @string/revanced_change_start_page_entry_default
diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml
index e239b6781..2344a75f2 100644
--- a/patches/src/main/resources/youtube/settings/host/values/strings.xml
+++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml
@@ -439,17 +439,27 @@ Limitation: Back button on the toolbar may not work."
     "Removes the viewer discretion dialog.
 This does not bypass the age restriction. It just accepts it automatically."
 
-    Change layout
-    Original
-    Phone
-    Phone (Max 480 dp)
-    Tablet
-    Tablet (Min 600 dp)
     Change live ring click action
     "Channel opens when the live ring is clicked.
 
 Limitation: When the Shorts live stream is opened in regular player due to the 'Open Shorts in regular player' setting, channel does not open."
     Live stream opens when the live ring is clicked.
+    Layout form factor
+    Default
+    Phone
+    Phone (Max 480 dp)
+    Tablet
+    Tablet (Min 600 dp)
+    Automotive
+    "Changes include:
+
+Tablet layout
+• Community posts are hidden.
+
+Automotive layout
+• Shorts open in the regular player.
+• Feed is organized by topics and channels.
+• Video description cannot be opened when 'Spoof streaming data' is turned off."
     Spoof app version
     Version spoofed
     Version not spoofed
diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
index be91eb852..136f93c31 100644
--- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
+++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
@@ -269,12 +269,12 @@
         
 
-        
-
         
 
+        
+