diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index 8ce4a9fa76..78852d3700 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -153,7 +153,6 @@ private static List> allLoadedSettingsSorted() { /** * 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; @@ -244,6 +243,7 @@ public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @N * * 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. @@ -419,6 +419,7 @@ public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNu 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/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java index 902b95897e..c7b570a855 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -42,7 +42,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { - Setting setting = Setting.getSettingFromPath(str); + Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str)); if (setting == null) { return; } @@ -52,23 +52,21 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { } Logger.printDebug(() -> "Preference changed: " + setting.key); - // 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); + 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(); } catch (Exception ex) { Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); } @@ -92,7 +90,7 @@ protected void initialize() { Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); } - private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { + private void showSettingUserDialogConfirmation(Preference pref, Setting setting) { Utils.verifyOnMainThread(); final var context = getContext(); @@ -104,12 +102,19 @@ private void showSettingUserDialogConfirmation(SwitchPreference switchPref, Bool .setTitle(confirmDialogTitle) .setMessage(Objects.requireNonNull(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; @@ -132,6 +137,24 @@ protected void updateUIAvailability() { 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. */ @@ -170,23 +193,20 @@ private void updatePreferenceScreen(@NonNull PreferenceScreen screen, protected void syncSettingWithPreference(@NonNull Preference pref, @NonNull Setting setting, boolean applySettingToPreference) { - if (pref instanceof SwitchPreference) { - SwitchPreference switchPref = (SwitchPreference) pref; + if (pref instanceof SwitchPreference switchPref) { BooleanSetting boolSetting = (BooleanSetting) setting; if (applySettingToPreference) { switchPref.setChecked(boolSetting.get()); } else { BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); } - } else if (pref instanceof EditTextPreference) { - EditTextPreference editPreference = (EditTextPreference) pref; + } else if (pref instanceof EditTextPreference editPreference) { if (applySettingToPreference) { editPreference.setText(setting.get().toString()); } else { Setting.privateSetValueFromString(setting, editPreference.getText()); } - } else if (pref instanceof ListPreference) { - ListPreference listPref = (ListPreference) pref; + } else if (pref instanceof ListPreference listPref) { if (applySettingToPreference) { listPref.setValue(setting.get().toString()); } else { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java index 92be08433e..c670a79de6 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java @@ -176,14 +176,13 @@ private static ThumbnailOption optionSettingForCurrentNavigation() { // 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/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java new file mode 100644 index 0000000000..a974f322bc --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java @@ -0,0 +1,54 @@ +package app.revanced.extension.youtube.patches; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ChangeFormFactorPatch { + + public enum FormFactor { + /** + * Unmodified, and same as un-patched. + */ + DEFAULT(null), + /** + *
+         * Some changes include:
+         * - Explore tab is present.
+         * - watch history is missing.
+         * - feed thumbnails fade in.
+         */
+        UNKNOWN(0),
+        SMALL(1),
+        LARGE(2),
+        /**
+         * Cars with 'Google built-in'.
+         * Layout seems identical to {@link #UNKNOWN}
+         * even when using an Android Automotive device.
+         */
+        AUTOMOTIVE(3),
+        WEARABLE(4);
+
+        @Nullable
+        final Integer formFactorType;
+
+        FormFactor(@Nullable Integer formFactorType) {
+            this.formFactorType = formFactorType;
+        }
+    }
+
+    @Nullable
+    private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType;
+
+    /**
+     * Injection point.
+     */
+    public static int getFormFactor(int original) {
+        return FORM_FACTOR_TYPE == null
+                ? original
+                : FORM_FACTOR_TYPE;
+    }
+
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/TabletLayoutPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/TabletLayoutPatch.java
deleted file mode 100644
index f2ae035980..0000000000
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/TabletLayoutPatch.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package app.revanced.extension.youtube.patches;
-
-import app.revanced.extension.youtube.settings.Settings;
-
-@SuppressWarnings("unused")
-public final class TabletLayoutPatch {
-
-    private static final boolean TABLET_LAYOUT_ENABLED = Settings.TABLET_LAYOUT.get();
-
-    /**
-     * Injection point.
-     */
-    public static boolean getTabletLayoutEnabled() {
-        return TABLET_LAYOUT_ENABLED;
-    }
-}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java
index b451fd282f..ff55d18a9f 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java
@@ -528,14 +528,13 @@ private boolean hideKeywordSettingIsActive() {
         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/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
index ac8218c417..3a57fb8361 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
@@ -366,7 +366,7 @@ private static boolean shouldHideShortsFeedItems() {
         }
 
         return switch (selectedNavButton) {
-            case HOME -> hideHome;
+            case HOME, EXPLORE -> hideHome;
             case SUBSCRIPTIONS -> hideSubscriptions;
             case LIBRARY -> hideHistory;
             default -> false;
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 841c4fb639..93515648ac 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -7,6 +7,7 @@
 import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
 import static app.revanced.extension.shared.settings.Setting.parent;
 import static app.revanced.extension.shared.settings.Setting.parentsAny;
+import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor;
 import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage;
 import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode;
 import static app.revanced.extension.youtube.patches.ForceOriginalAudioPatch.ForceOriginalAudioAvailability;
@@ -202,12 +203,12 @@ public class Settings extends BaseSettings {
     public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE);
 
     // General layout
+    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 BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
     public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
     public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
             "revanced_remove_viewer_discretion_dialog_user_dialog_message");
     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 BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message");
     public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true);
     public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true);
     public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", IS_19_17_OR_GREATER ? "19.26.42" : "17.33.42", true, parent(SPOOF_APP_VERSION));
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 3a24567956..a302a59baf 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,12 +3,15 @@
 import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE;
 
 import android.app.Activity;
+import android.os.Build;
 import android.view.View;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 
 import java.lang.ref.WeakReference;
 import java.util.Arrays;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.WeakHashMap;
@@ -242,6 +245,30 @@ private static void navigationTabCreatedCallback(NavigationButton button, View t
         // Code is added during patching.
     }
 
+    /**
+     * Use the bundled non cairo filled icon instead of a custom icon.
+     * Use the old non cairo filled icon, which is almost identical to
+     * the what would be the filled cairo icon.
+     */
+    private static final int fillBellCairoBlack = Utils.getResourceIdentifier(
+            "yt_fill_bell_black_24", "drawable");
+
+    /**
+     * Injection point.
+     * Fixes missing drawable.
+     */
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) {
+        if (fillBellCairoBlack != 0) {
+            // Show a popup informing this fix is no longer needed to those who might care.
+            if (BaseSettings.DEBUG.get() && enumMap.containsKey(tabActivityCairo)) {
+                Logger.printException(() -> "YouTube fixed the cairo notification icons");
+            }
+            enumMap.putIfAbsent(tabActivityCairo, fillBellCairoBlack);
+        }
+    }
+
     public enum NavigationButton {
         HOME("PIVOT_HOME", "TAB_HOME_CAIRO"),
         SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"),
@@ -250,6 +277,10 @@ public enum NavigationButton {
          * 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/patches/api/patches.api b/patches/api/patches.api
index e8e7cfc70d..92ca7f9469 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1101,6 +1101,10 @@ public final class app/revanced/patches/youtube/layout/buttons/overlay/HidePlaye
 	public static final fun getHidePlayerOverlayButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
+public final class app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatchKt {
+	public static final fun getChangeFormFactorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
 public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatchKt {
 	public static final fun getHideEndscreenCardsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
@@ -1230,7 +1234,6 @@ public final class app/revanced/patches/youtube/layout/startupshortsreset/Disabl
 }
 
 public final class app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatchKt {
-	public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
 	public static final fun getEnableTabletLayoutPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
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
new file mode 100644
index 0000000000..b7621b02f4
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt
@@ -0,0 +1,75 @@
+package app.revanced.patches.youtube.layout.formfactor
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.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.ListPreference
+import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
+import app.revanced.patches.youtube.misc.settings.settingsPatch
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+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;"
+
+@Suppress("unused")
+val changeFormFactorPatch = bytecodePatch(
+    name = "Change form factor",
+    description = "Adds an option to change the UI appearance to a phone, tablet, or automotive device.",
+) {
+    dependsOn(
+        sharedExtensionPatch,
+        settingsPatch,
+        addResourcesPatch,
+    )
+
+    compatibleWith(
+        "com.google.android.youtube"(
+            "18.38.44",
+            "18.49.37",
+            "19.16.39",
+            "19.25.37",
+            "19.34.42",
+            "19.43.41",
+            "19.45.38",
+            "19.46.42",
+            "19.47.53",
+        ),
+    )
+
+    execute {
+        addResources("youtube", "layout.formfactor.changeFormFactorPatch")
+
+        PreferenceScreen.GENERAL_LAYOUT.addPreferences(
+            ListPreference(
+                "revanced_change_form_factor",
+                summaryKey = null,
+            )
+        )
+
+        createPlayerRequestBodyWithModelFingerprint.method.apply {
+            val formFactorEnumClass = formFactorEnumConstructorFingerprint.originalClassDef.type
+
+            val index = indexOfFirstInstructionOrThrow {
+                val reference = getReference()
+                opcode == Opcode.IGET &&
+                        reference?.definingClass == formFactorEnumClass &&
+                        reference.type == "I"
+            }
+            val register = getInstruction(index).registerA
+
+            addInstructions(
+                index + 1,
+                """
+                    invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getFormFactor(I)I
+                    move-result v$register
+                """
+            )
+        }
+    }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/Fingerprints.kt
new file mode 100644
index 0000000000..d1f1535ebf
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/Fingerprints.kt
@@ -0,0 +1,49 @@
+package app.revanced.patches.youtube.layout.formfactor
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
+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.FieldReference
+
+internal val formFactorEnumConstructorFingerprint = fingerprint {
+    accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
+    strings(
+        "UNKNOWN_FORM_FACTOR",
+        "SMALL_FORM_FACTOR",
+        "LARGE_FORM_FACTOR",
+        "AUTOMOTIVE_FORM_FACTOR"
+    )
+}
+
+internal val createPlayerRequestBodyWithModelFingerprint = fingerprint {
+    accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
+    returns("L")
+    parameters()
+    opcodes(Opcode.OR_INT_LIT16)
+    custom { method, _ ->
+        method.indexOfModelInstruction() >= 0 &&
+                method.indexOfReleaseInstruction() >= 0
+    }
+}
+
+private fun Method.indexOfModelInstruction() =
+    indexOfFirstInstruction {
+        val reference = getReference()
+
+        reference?.definingClass == "Landroid/os/Build;" &&
+                reference.name == "MODEL" &&
+                reference.type == "Ljava/lang/String;"
+    }
+
+internal fun Method.indexOfReleaseInstruction(): Int =
+    indexOfFirstInstruction {
+        val reference = getReference()
+
+        reference?.definingClass == "Landroid/os/Build${'$'}VERSION;" &&
+                reference.name == "RELEASE" &&
+                reference.type == "Ljava/lang/String;"
+    }
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt
index ac460dada5..c04f5e99c7 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt
@@ -1,66 +1,9 @@
 package app.revanced.patches.youtube.layout.tablet
 
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
-import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.instructions
 import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patcher.util.smali.ExternalLabel
-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.extension.sharedExtensionPatch
-import app.revanced.patches.youtube.misc.settings.PreferenceScreen
-import app.revanced.patches.youtube.misc.settings.settingsPatch
+import app.revanced.patches.youtube.layout.formfactor.changeFormFactorPatch
 
-const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/TabletLayoutPatch;"
-
-val enableTabletLayoutPatch = bytecodePatch(
-    name = "Enable tablet layout",
-    description = "Adds an option to enable tablet layout.",
-) {
-    dependsOn(
-        sharedExtensionPatch,
-        settingsPatch,
-        addResourcesPatch,
-    )
-
-    compatibleWith(
-        "com.google.android.youtube"(
-            "18.38.44",
-            "18.49.37",
-            "19.16.39",
-            "19.25.37",
-            "19.34.42",
-            "19.43.41",
-            "19.45.38",
-            "19.46.42",
-            "19.47.53",
-        ),
-    )
-
-    execute {
-        addResources("youtube", "layout.tablet.enableTabletLayoutPatch")
-
-        PreferenceScreen.GENERAL_LAYOUT.addPreferences(
-            SwitchPreference("revanced_tablet_layout"),
-        )
-
-        getFormFactorFingerprint.method.apply {
-            val returnIsLargeFormFactorIndex = instructions.lastIndex - 4
-            val returnIsLargeFormFactorLabel = getInstruction(returnIsLargeFormFactorIndex)
-
-            addInstructionsWithLabels(
-                0,
-                """
-                      invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getTabletLayoutEnabled()Z
-                      move-result v0
-                      if-nez v0, :is_large_form_factor
-                """,
-                ExternalLabel(
-                    "is_large_form_factor",
-                    returnIsLargeFormFactorLabel,
-                ),
-            )
-        }
-    }
-}
+@Deprecated("Use 'Change form factor' instead.")
+val enableTabletLayoutPatch = bytecodePatch {
+    dependsOn(changeFormFactorPatch)
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/Fingerprints.kt
deleted file mode 100644
index 30667a85bf..0000000000
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/Fingerprints.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package app.revanced.patches.youtube.layout.tablet
-
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.AccessFlags
-import app.revanced.patcher.fingerprint
-
-internal val getFormFactorFingerprint = fingerprint {
-    accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
-    returns("L")
-    parameters("Landroid/content/Context;", "Ljava/util/List;")
-    opcodes(
-        Opcode.SGET_OBJECT,
-        Opcode.INVOKE_VIRTUAL,
-        Opcode.MOVE_RESULT_OBJECT,
-        Opcode.INVOKE_VIRTUAL,
-        Opcode.MOVE_RESULT,
-        Opcode.IF_EQZ,
-        Opcode.SGET_OBJECT,
-        Opcode.RETURN_OBJECT,
-        Opcode.INVOKE_STATIC,
-        Opcode.MOVE_RESULT_OBJECT,
-        Opcode.RETURN_OBJECT,
-    )
-    strings("")
-}
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 0f5ade30a6..c64cf75940 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
@@ -105,3 +105,15 @@ internal val pivotBarConstructorFingerprint = fingerprint {
     accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
     strings("com.google.android.apps.youtube.app.endpoint.flags")
 }
+
+internal val imageEnumConstructorFingerprint = fingerprint {
+    accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
+    strings("TAB_ACTIVITY_CAIRO")
+}
+
+internal val setEnumMapFingerprint = fingerprint {
+    accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
+    literal {
+        ytFillBellId
+    }
+}
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 503eb91d41..902567eb89 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
@@ -1,6 +1,7 @@
 package app.revanced.patches.youtube.misc.navigation
 
 import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
 import app.revanced.patcher.extensions.InstructionExtensions.instructions
 import app.revanced.patcher.patch.PatchException
@@ -12,13 +13,16 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
 import app.revanced.patches.shared.misc.mapping.resourceMappings
 import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
 import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
+import app.revanced.patches.youtube.misc.playservice.is_19_35_or_greater
 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.Opcode
 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.util.MethodUtil
 
@@ -26,6 +30,8 @@ internal var imageOnlyTabResourceId = -1L
     private set
 internal var actionBarSearchResultsViewMicId = -1L
     private set
+internal var ytFillBellId = -1L
+    private set
 
 private val navigationBarHookResourcePatch = resourcePatch {
     dependsOn(resourceMappingPatch)
@@ -33,6 +39,7 @@ private val navigationBarHookResourcePatch = resourcePatch {
     execute {
         imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"]
         actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"]
+        ytFillBellId = resourceMappings["drawable", "yt_fill_bell_black_24"]
     }
 }
 
@@ -144,6 +151,36 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig
                     "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V",
             )
         }
+
+        // Fix YT bug of notification tab missing the filled icon.
+        if (is_19_35_or_greater) {
+            val cairoNotificationEnumReference = with(imageEnumConstructorFingerprint) {
+                val stringIndex = stringMatches!!.first().index
+                val cairoNotificationEnumIndex = method.indexOfFirstInstructionOrThrow(stringIndex) {
+                    opcode == Opcode.SPUT_OBJECT
+                }
+                method.getInstruction(cairoNotificationEnumIndex).reference
+            }
+
+            setEnumMapFingerprint.method.apply {
+                val enumMapIndex = indexOfFirstInstructionReversedOrThrow {
+                    val reference = getReference()
+                    opcode == Opcode.INVOKE_VIRTUAL &&
+                            reference?.definingClass == "Ljava/util/EnumMap;" &&
+                            reference.name == "put" &&
+                            reference.parameterTypes.firstOrNull() == "Ljava/lang/Enum;"
+                }
+                val instruction = getInstruction(enumMapIndex)
+
+                addInstructions(
+                    enumMapIndex + 1,
+                    """
+                        sget-object v${instruction.registerD}, $cairoNotificationEnumReference
+                        invoke-static { v${instruction.registerC}, v${instruction.registerD} }, $EXTENSION_CLASS_DESCRIPTOR->setCairoNotificationFilledIcon(Ljava/util/EnumMap;Ljava/lang/Enum;)V
+                    """
+                )
+            }
+        }
     }
 }
 
diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml
index 4a0d6463d9..670b043b40 100644
--- a/patches/src/main/resources/addresources/values/arrays.xml
+++ b/patches/src/main/resources/addresources/values/arrays.xml
@@ -8,7 +8,7 @@
                 iOS TV
             
             
-                
+                
                 ANDROID_VR
                 ANDROID_VR_NO_AUTH
                 ANDROID_UNPLUGGED
@@ -148,6 +148,21 @@
                 17.33.42
             
         
+        
+            
+                @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
+            
+            
+                
+                DEFAULT
+                SMALL
+                LARGE
+                AUTOMOTIVE
+            
+        
         
             
                 @string/revanced_exit_fullscreen_entry_1
@@ -174,7 +189,7 @@
                 @string/revanced_miniplayer_type_entry_6
             
             
-                
+                
                 DISABLED
                 DEFAULT
                 MINIMAL
@@ -192,7 +207,7 @@
                 @string/revanced_miniplayer_type_entry_6
             
             
-                
+                
                 DEFAULT
                 MINIMAL
                 TABLET
@@ -221,7 +236,7 @@
                 @string/revanced_change_start_page_entry_browse
             
             
-                
+                
                 DEFAULT
                 
                 SEARCH
@@ -248,7 +263,7 @@
                 @string/revanced_shorts_player_type_regular_player
             
             
-                
+                
                 SHORTS_PLAYER
                 REGULAR_PLAYER
             
@@ -272,7 +287,7 @@
                 @string/revanced_alt_thumbnail_options_entry_4
             
             
-                
+                
                 ORIGINAL
                 DEARROW
                 DEARROW_STILL_IMAGES
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index d2e07dd35d..6e4bceb3b9 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -1029,6 +1029,23 @@ Ready to submit?"
             sponsor.ajay.app
             Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms
         
+        
+            Layout form factor
+            Default
+            Phone
+            Tablet
+            Automotive
+            "Changes include:
+
+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"
+        
         
             Spoof app version
             Version spoofed
@@ -1086,12 +1103,6 @@ If later turned off, it is recommended to clear the app data to prevent UI bugs.
             Shorts background play will autoplay
             Shorts background play will repeat
         
-        
-            Enable tablet layout
-            Tablet layout is enabled
-            Tablet layout is disabled
-            Community posts do not show up on tablet layouts
-        x
         
             Miniplayer
             Change the style of the in app minimized player