From 14223f40b5ca48f35bbecfd849dff20dfd309d92 Mon Sep 17 00:00:00 2001
From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Date: Fri, 14 Apr 2023 05:50:16 +0400
Subject: [PATCH] feat(youtube): user selectable default video speed and
 quality (#354)

Co-authored-by: johnconner122 <107796137+johnconner122@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
---
 .../quality/RememberVideoQualityPatch.java    | 233 ++++++++++--------
 .../playback/speed/CustomVideoSpeedPatch.java |  13 +-
 .../speed/RememberPlaybackSpeedPatch.java     |  42 +++-
 .../integrations/settings/SettingsEnum.java   |  60 ++++-
 .../settings/SharedPrefCategory.java          |  74 ++++--
 .../ReVancedSettingsFragment.java             |  68 +++--
 6 files changed, 321 insertions(+), 169 deletions(-)

diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java
index 695b52ac..df85317d 100644
--- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java
+++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java
@@ -1,139 +1,168 @@
 package app.revanced.integrations.patches.playback.quality;
 
-import android.content.Context;
+import static app.revanced.integrations.utils.ReVancedUtils.NetworkType;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.List;
 
 import app.revanced.integrations.settings.SettingsEnum;
-import app.revanced.integrations.settings.SharedPrefCategory;
 import app.revanced.integrations.utils.LogHelper;
 import app.revanced.integrations.utils.ReVancedUtils;
-import app.revanced.integrations.utils.ReVancedUtils.NetworkType;
 
 public class RememberVideoQualityPatch {
-    public static int selectedQuality1 = -2;
-    private static Boolean newVideo = false;
-    private static Boolean userChangedQuality = false;
+    private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
+    private static final SettingsEnum wifiQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_WIFI;
+    private static final SettingsEnum mobileQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_MOBILE;
 
-    public static void changeDefaultQuality(int defaultQuality) {
-        Context context = ReVancedUtils.getContext();
+    private static boolean qualityNeedsUpdating;
+    @Nullable
+    private static String currentVideoId;
 
-        var networkType = ReVancedUtils.getNetworkType();
+    /**
+     * If the user selected a new quality from the flyout menu,
+     * and {@link SettingsEnum#VIDEO_QUALITY_REMEMBER_LAST_SELECTED} is enabled.
+     */
+    private static boolean userChangedDefaultQuality;
 
+    /**
+     * Index of the video quality chosen by the user from the flyout menu.
+     */
+    private static int userSelectedQualityIndex;
+
+    /**
+     * The available qualities of the current video in human readable form: [1080, 720, 480]
+     */
+    @Nullable
+    private static List<Integer> videoQualities;
+
+    private static void changeDefaultQuality(int defaultQuality) {
+        NetworkType networkType = ReVancedUtils.getNetworkType();
         if (networkType == NetworkType.NONE) {
-            ReVancedUtils.showToastShort("No internet connection.");
-        } else {
-            var preferenceKey = "wifi_quality";
-            var networkTypeMessage = "WIFI";
-
-            if (networkType == NetworkType.MOBILE) {
-                networkTypeMessage = "mobile";
-                preferenceKey = "mobile_quality";
-            }
-
-            SharedPrefCategory.REVANCED_PREFS.saveString(preferenceKey, String.valueOf(defaultQuality));
-            ReVancedUtils.showToastShort("Changing default " + networkTypeMessage + " quality to: " + defaultQuality);
+            ReVancedUtils.showToastShort("No internet connection");
+            return;
         }
-
-        userChangedQuality = false;
+        String networkTypeMessage;
+        if (networkType == NetworkType.MOBILE) {
+            mobileQualitySetting.saveValue(defaultQuality);
+            networkTypeMessage = "mobile";
+        } else {
+            wifiQualitySetting.saveValue(defaultQuality);
+            networkTypeMessage = "Wi-Fi";
+        }
+        ReVancedUtils.showToastShort("Changed default " + networkTypeMessage
+                + " quality to: " + defaultQuality +"p");
     }
 
-    public static int setVideoQuality(Object[] qualities, int quality, Object qInterface, String qIndexMethod) {
-        Field[] fields;
-
-        if (!(newVideo || userChangedQuality) || qInterface == null) {
-            return quality;
-        }
-
-        Class<?> intType = Integer.TYPE;
-        ArrayList<Integer> iStreamQualities = new ArrayList<>();
+    /**
+     * Injection point.
+     *
+     * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
+     * @param originalQualityIndex quality index to use, as chosen by YouTube
+     */
+    public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) {
         try {
-            for (Object streamQuality : qualities) {
-                for (Field field : streamQuality.getClass().getFields()) {
-                    if (field.getType().isAssignableFrom(intType)) {  // converts quality index to actual readable resolution
-                        int value = field.getInt(streamQuality);
-                        if (field.getName().length() <= 2) {
-                            iStreamQualities.add(value);
+            if (!(qualityNeedsUpdating || userChangedDefaultQuality) || qInterface == null) {
+                return originalQualityIndex;
+            }
+            qualityNeedsUpdating = false;
+
+            final int preferredQuality;
+            if (ReVancedUtils.getNetworkType() == NetworkType.MOBILE) {
+                preferredQuality = mobileQualitySetting.getInt();
+            } else {
+                preferredQuality = wifiQualitySetting.getInt();
+            }
+            if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
+                return originalQualityIndex; // nothing to do
+            }
+
+            if (videoQualities == null || videoQualities.size() != qualities.length) {
+                videoQualities = new ArrayList<>(qualities.length);
+                for (Object streamQuality : qualities) {
+                    for (Field field : streamQuality.getClass().getFields()) {
+                        if (field.getType().isAssignableFrom(Integer.TYPE)
+                                && field.getName().length() <= 2) {
+                            videoQualities.add(field.getInt(streamQuality));
                         }
                     }
                 }
+                LogHelper.printDebug(() -> "VideoId: " + currentVideoId + " videoQualities: " + videoQualities);
             }
-        } catch (Exception ignored) {
-        }
-        Collections.sort(iStreamQualities);
-        int index = 0;
-        if (userChangedQuality) {
-            for (int convertedQuality : iStreamQualities) {
-                int selectedQuality2 = qualities.length - selectedQuality1 + 1;
-                index++;
-                if (selectedQuality2 == index) {
-                    final int indexToLog = index; // must be final for lambda
-                    LogHelper.printDebug(() -> "Quality index is: " + indexToLog + " and corresponding value is: " + convertedQuality);
-                    changeDefaultQuality(convertedQuality);
-                    return selectedQuality2;
+
+            if (userChangedDefaultQuality) {
+                userChangedDefaultQuality = false;
+                final int quality = videoQualities.get(userSelectedQualityIndex);
+                LogHelper.printDebug(() -> "User changed default quality to: " + quality);
+                changeDefaultQuality(quality);
+                return userSelectedQualityIndex;
+            }
+
+            // find the highest quality that is equal to or less than the preferred
+            int qualityToUse = videoQualities.get(0); // first element is automatic mode
+            int qualityIndexToUse = 0;
+            int i = 0;
+            for (Integer quality : videoQualities) {
+                if (quality <= preferredQuality && qualityToUse < quality)  {
+                    qualityToUse = quality;
+                    qualityIndexToUse = i;
                 }
+                i++;
+            }
+            if (qualityIndexToUse == originalQualityIndex) {
+                LogHelper.printDebug(() -> "Video is already preferred quality: " + preferredQuality);
+                return originalQualityIndex;
             }
-        }
-        newVideo = false;
-        final int qualityToLog = quality;
-        LogHelper.printDebug(() -> "Quality: " + qualityToLog);
-        Context context = ReVancedUtils.getContext();
-        if (context == null) {
-            LogHelper.printException(() -> "Context is null or settings not initialized, returning quality: " + qualityToLog);
-            return quality;
-        }
-        var networkType = ReVancedUtils.getNetworkType();
-        if (networkType == NetworkType.NONE) {
-            LogHelper.printDebug(() -> "No Internet connection!");
-            return quality;
-        } else {
-            var preferenceKey = "wifi_quality";
-            if (networkType == NetworkType.MOBILE) preferenceKey = "mobile_quality";
 
-            int preferredQuality = SharedPrefCategory.REVANCED_PREFS.getInt(preferenceKey, -2);
-            if (preferredQuality == -2) return quality;
+            final int qualityToUseLog = qualityToUse;
+            LogHelper.printDebug(() -> "Quality changed from: "
+                    + videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog);
 
-            for (int streamQuality2 : iStreamQualities) {
-                final int indexToLog = index;
-                LogHelper.printDebug(() -> "Quality at index " + indexToLog + ": " + streamQuality2);
-                index++;
-            }
-            for (Integer iStreamQuality : iStreamQualities) {
-                int streamQuality3 = iStreamQuality;
-                if (streamQuality3 <= preferredQuality) {
-                    quality = streamQuality3;
-                }
-            }
-            if (quality == -2) return quality;
-
-            int qualityIndex = iStreamQualities.indexOf(quality);
-            final int qualityToLog2 = quality;
-            LogHelper.printDebug(() -> "Index of quality " + qualityToLog2 + " is " + qualityIndex);
-            try {
-                Class<?> cl = qInterface.getClass();
-                Method m = cl.getMethod(qIndexMethod, Integer.TYPE);
-                LogHelper.printDebug(() -> "Method is: " + qIndexMethod);
-                m.invoke(qInterface, iStreamQualities.get(qualityIndex));
-                LogHelper.printDebug(() -> "Quality changed to: " + qualityIndex);
-                return qualityIndex;
-            } catch (Exception ex) {
-                LogHelper.printException(() -> "Failed to set quality", ex);
-                return qualityIndex;
-            }
+            Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE);
+            m.invoke(qInterface, qualityToUse);
+            return qualityIndexToUse;
+        } catch (Exception ex) {
+            LogHelper.printException(() -> "Failed to set quality", ex);
+            return originalQualityIndex;
         }
     }
 
+    /**
+     * Injection point.
+     */
     public static void userChangedQuality(int selectedQuality) {
-        if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return;
+        if (!SettingsEnum.VIDEO_QUALITY_REMEMBER_LAST_SELECTED.getBoolean()) return;
 
-        selectedQuality1 = selectedQuality;
-        userChangedQuality = true;
+        userSelectedQualityIndex = selectedQuality;
+        userChangedDefaultQuality = true;
     }
 
-    public static void newVideoStarted(String videoId) {
-        newVideo = true;
+    /**
+     * Injection point.
+     */
+    public static void newVideoStarted(@NonNull String videoId) {
+        // The same videoId can be passed in multiple times for a single video playback.
+        // Such as closing and opening the app, and sometimes when turning off/on the device screen.
+        //
+        // Known limitation, if:
+        // 1. a default video quality exists, and remember quality is turned off
+        // 2. user opens a video
+        // 3. user changes the video quality
+        // 4. user turns off then on the device screen (or does anything else that triggers the video id hook)
+        // result: the video quality of the current video will revert back to the saved default
+        //
+        // qualityNeedsUpdating could be set only when the videoId changes
+        // but then if the user closes and re-opens the same video the default video quality will not be applied.
+        LogHelper.printDebug(() -> "newVideoStarted: " + videoId);
+        qualityNeedsUpdating = true;
+
+        if (!videoId.equals(currentVideoId)) {
+            currentVideoId = videoId;
+            videoQualities = null;
+        }
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java
index 663fe6b1..6692dc85 100644
--- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java
+++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java
@@ -1,9 +1,12 @@
 package app.revanced.integrations.patches.playback.speed;
 
 public class CustomVideoSpeedPatch {
-    // Values are useless as they are being overridden by the respective patch.
-    // This generates a .array segment in Dalvik bytecode
-    // which the patch utilizes to store the video speeds in, only
-    // if it has two or more default values.
-    public static final float[] videoSpeeds = { 0, 0 };
+    /**
+     * Default playback speeds offered by YouTube.
+     * Values are also used by {@link RememberPlaybackSpeedPatch}.
+     *
+     * If custom video speed is applied,
+     * then this array is overwritten by the patch with custom speeds
+     */
+    public static final float[] videoSpeeds = {0.25f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
 }
diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java
index 5b4a9460..b3475944 100644
--- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java
+++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java
@@ -1,5 +1,7 @@
 package app.revanced.integrations.patches.playback.speed;
 
+import android.preference.ListPreference;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -9,6 +11,11 @@ import app.revanced.integrations.utils.ReVancedUtils;
 
 public final class RememberPlaybackSpeedPatch {
 
+    /**
+     * PreferenceList entries and values, of all available playback speeds.
+     */
+    private static String[] preferenceListEntries, preferenceListEntryValues;
+
     @Nullable
     private static String currentVideoId;
 
@@ -21,7 +28,7 @@ public final class RememberPlaybackSpeedPatch {
             return;
         }
         currentVideoId = videoId;
-        VideoInformation.overridePlaybackSpeed(SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat());
+        VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat());
     }
 
     /**
@@ -31,13 +38,9 @@ public final class RememberPlaybackSpeedPatch {
      * @param playbackSpeed The playback speed the user selected
      */
     public static void userSelectedPlaybackSpeed(float playbackSpeed) {
-        if (SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean()) {
-            SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.saveValue(playbackSpeed);
-
-            // TODO: extract these strings into localized file
-            ReVancedUtils.showToastLong("Remembering playback speed: " + playbackSpeed + "x");
-        } else if (playbackSpeed != (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.defaultValue) {
-            ReVancedUtils.showToastLong("Applying playback speed: " + playbackSpeed + "x");
+        if (SettingsEnum.PLAYBACK_SPEED_REMEMBER_LAST_SELECTED.getBoolean()) {
+            SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed);
+            ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x");
         }
     }
 
@@ -48,4 +51,27 @@ public final class RememberPlaybackSpeedPatch {
     public static float getPlaybackSpeedOverride() {
         return VideoInformation.getCurrentPlaybackSpeed();
     }
+
+    /**
+     * Initialize a settings preference list.
+     *
+     * Normally this is done during patching by creating a static xml preference list,
+     * but the available playback speeds differ depending if {@link CustomVideoSpeedPatch} is applied or not.
+     */
+    public static void initializeListPreference(ListPreference preference) {
+        if (preferenceListEntries == null) {
+            float[] videoSpeeds = CustomVideoSpeedPatch.videoSpeeds;
+            preferenceListEntries = new String[videoSpeeds.length];
+            preferenceListEntryValues = new String[videoSpeeds.length];
+            int i = 0;
+            for (float speed : videoSpeeds) {
+                String speedString = String.valueOf(speed);
+                preferenceListEntries[i] = speedString + "x";
+                preferenceListEntryValues[i] = speedString;
+                i++;
+            }
+        }
+        preference.setEntries(preferenceListEntries);
+        preference.setEntryValues(preferenceListEntryValues);
+    }
 }
diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java
index 8819bd6e..2ddad19d 100644
--- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java
+++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java
@@ -29,9 +29,11 @@ public enum SettingsEnum {
 
     // Video settings
     OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", BOOLEAN, TRUE),
-    REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE),
-    REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE),
-    REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE("revanced_remember_playback_speed_last_selected_value", FLOAT, 1.0f),
+    VIDEO_QUALITY_REMEMBER_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE),
+    VIDEO_QUALITY_DEFAULT_WIFI("revanced_default_video_quality_wifi", INTEGER, -2),
+    VIDEO_QUALITY_DEFAULT_MOBILE("revanced_default_video_quality_mobile", INTEGER, -2),
+    PLAYBACK_SPEED_REMEMBER_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE),
+    PLAYBACK_SPEED_DEFAULT("revanced_default_playback_speed", FLOAT, 1.0f),
 
     // TODO: Unused currently
     // Whitelist settings
@@ -295,13 +297,13 @@ public enum SettingsEnum {
                 value = sharedPref.getBoolean(path, (boolean) defaultValue);
                 break;
             case INTEGER:
-                value = sharedPref.getInt(path, (Integer) defaultValue);
+                value = sharedPref.getIntegerString(path, (Integer) defaultValue);
                 break;
             case LONG:
-                value = sharedPref.getLong(path, (Long) defaultValue);
+                value = sharedPref.getLongString(path, (Long) defaultValue);
                 break;
             case FLOAT:
-                value = sharedPref.getFloat(path, (Float) defaultValue);
+                value = sharedPref.getFloatString(path, (Float) defaultValue);
                 break;
             case STRING:
                 value = sharedPref.getString(path, (String) defaultValue);
@@ -316,9 +318,37 @@ public enum SettingsEnum {
      *
      * This intentionally is a static method, to deter accidental usage
      * when {@link #saveValue(Object)} was intended.
+     *
+     * This method is only to be used by the Settings preference code.
      */
-    public static void setValue(@NonNull SettingsEnum setting, @NonNull Object newValue) {
-        setting.value = Objects.requireNonNull(newValue);
+    public static void setValue(@NonNull SettingsEnum setting, @NonNull String newValue) {
+        Objects.requireNonNull(newValue);
+        switch (setting.returnType) {
+            case BOOLEAN:
+                setting.value = Boolean.valueOf(newValue);
+                break;
+            case INTEGER:
+                setting.value = Integer.valueOf(newValue);
+                break;
+            case LONG:
+                setting.value = Long.valueOf(newValue);
+                break;
+            case FLOAT:
+                setting.value = Float.valueOf(newValue);
+                break;
+            case STRING:
+                setting.value = newValue;
+                break;
+            default:
+                throw new IllegalStateException(setting.name());
+        }
+    }
+    /**
+     * This method is only to be used by the Settings preference code.
+     */
+    public static void setValue(@NonNull SettingsEnum setting, @NonNull Boolean newValue) {
+        Objects.requireNonNull(newValue);
+        setting.value = newValue;
     }
 
     /**
@@ -331,13 +361,13 @@ public enum SettingsEnum {
                 sharedPref.saveBoolean(path, (boolean) newValue);
                 break;
             case INTEGER:
-                sharedPref.saveInt(path, (int) newValue);
+                sharedPref.saveIntegerString(path, (Integer) newValue);
                 break;
             case LONG:
-                sharedPref.saveLong(path, (long) newValue);
+                sharedPref.saveLongString(path, (Long) newValue);
                 break;
             case FLOAT:
-                sharedPref.saveFloat(path, (float) newValue);
+                sharedPref.saveFloatString(path, (Float) newValue);
                 break;
             case STRING:
                 sharedPref.saveString(path, (String) newValue);
@@ -384,6 +414,14 @@ public enum SettingsEnum {
         return (String) value;
     }
 
+    /**
+     * @return the value of this setting as as generic object type.
+     */
+    @NonNull
+    public Object getObjectValue() {
+        return value;
+    }
+
     public enum ReturnType {
         BOOLEAN,
         INTEGER,
diff --git a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java
index 59413da3..4bf180ed 100644
--- a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java
+++ b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java
@@ -4,11 +4,21 @@ import android.content.Context;
 import android.content.SharedPreferences;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.util.Objects;
 
 import app.revanced.integrations.utils.ReVancedUtils;
 
+/**
+ * Shared categories, and helper methods.
+ *
+ * The various save methods store numbers as Strings,
+ * which is required if using {@link android.preference.PreferenceFragment}.
+ *
+ * If saved numbers will not be used with a preference fragment,
+ * then store the primitive numbers using {@link #preferences}.
+ */
 public enum SharedPrefCategory {
     YOUTUBE("youtube"),
     RETURN_YOUTUBE_DISLIKE("ryd"),
@@ -25,27 +35,41 @@ public enum SharedPrefCategory {
         preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE);
     }
 
-    public void saveString(@NonNull String key, @NonNull String value) {
-        Objects.requireNonNull(value);
-        preferences.edit().putString(key, value).apply();
+    private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
+        preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
     }
 
     public void saveBoolean(@NonNull String key, boolean value) {
         preferences.edit().putBoolean(key, value).apply();
     }
 
-    public void saveInt(@NonNull String key, int value) {
-        preferences.edit().putInt(key, value).apply();
+    /**
+     * @param value a NULL parameter removes the value from the preferences
+     */
+    public void saveIntegerString(@NonNull String key, @Nullable Integer value) {
+        saveObjectAsString(key, value);
     }
 
-    public void saveLong(@NonNull String key, long value) {
-        preferences.edit().putLong(key, value).apply();
+    /**
+     * @param value a NULL parameter removes the value from the preferences
+     */
+    public void saveLongString(@NonNull String key, @Nullable Long value) {
+        saveObjectAsString(key, value);
     }
 
-    public void saveFloat(@NonNull String key, float value) {
-        preferences.edit().putFloat(key, value).apply();
+    /**
+     * @param value a NULL parameter removes the value from the preferences
+     */
+    public void saveFloatString(@NonNull String key, @Nullable Float value) {
+        saveObjectAsString(key, value);
     }
 
+    /**
+     * @param value a NULL parameter removes the value from the preferences
+     */
+    public void saveString(@NonNull String key, @Nullable String value) {
+        saveObjectAsString(key, value);
+    }
 
     @NonNull
     public String getString(@NonNull String key, @NonNull String _default) {
@@ -53,42 +77,50 @@ public enum SharedPrefCategory {
         return preferences.getString(key, _default);
     }
 
+
     public boolean getBoolean(@NonNull String key, boolean _default) {
         return preferences.getBoolean(key, _default);
     }
 
-    // region Hack, required for PreferencesFragments to function correctly.  unknown why required
-
     @NonNull
-    public Integer getInt(@NonNull String key, @NonNull Integer _default) {
+    public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) {
         try {
-            return Integer.valueOf(preferences.getString(key, _default.toString()));
+            String value = preferences.getString(key, null);
+            if (value != null) {
+                return Integer.valueOf(value);
+            }
+            return _default;
         } catch (ClassCastException ex) {
-            return preferences.getInt(key, _default);
+            return preferences.getInt(key, _default); // old data, previously stored as primitive
         }
     }
 
     @NonNull
-    public Long getLong(@NonNull String key, @NonNull Long _default) {
+    public Long getLongString(@NonNull String key, @NonNull Long _default) {
         try {
-            return Long.valueOf(preferences.getString(key, _default.toString()));
+            String value = preferences.getString(key, null);
+            if (value != null) {
+                return Long.valueOf(value);
+            }
+            return _default;
         } catch (ClassCastException ex) {
             return preferences.getLong(key, _default);
         }
     }
 
     @NonNull
-    public Float getFloat(@NonNull String key, @NonNull Float _default) {
+    public Float getFloatString(@NonNull String key, @NonNull Float _default) {
         try {
-            return Float.valueOf(preferences.getString(key, _default.toString()));
+            String value = preferences.getString(key, null);
+            if (value != null) {
+                return Float.valueOf(value);
+            }
+            return _default;
         } catch (ClassCastException ex) {
             return preferences.getFloat(key, _default);
         }
     }
 
-    // endregion
-
-
     @NonNull
     @Override
     public String toString() {
diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java
index 8862f88e..16b427d2 100644
--- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java
+++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java
@@ -13,6 +13,7 @@ import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Process;
 import android.preference.EditTextPreference;
+import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceManager;
@@ -23,12 +24,18 @@ import androidx.annotation.Nullable;
 
 import com.google.android.apps.youtube.app.application.Shell_HomeActivity;
 
+import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
 import app.revanced.integrations.settings.SettingsEnum;
 import app.revanced.integrations.settings.SharedPrefCategory;
 import app.revanced.integrations.utils.LogHelper;
 import app.revanced.integrations.utils.ReVancedUtils;
 
 public class ReVancedSettingsFragment extends PreferenceFragment {
+    /**
+     * Used to prevent showing reboot dialog, if user cancels a setting user dialog.
+     */
+    private boolean showingUserDialogMessage;
+
     SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
         try {
             SettingsEnum setting = SettingsEnum.settingFromPath(str);
@@ -43,32 +50,22 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
                 SettingsEnum.setValue(setting, switchPref.isChecked());
             } else if (pref instanceof EditTextPreference) {
                 String editText = ((EditTextPreference) pref).getText();
-                Object value;
-                switch (setting.returnType) {
-                    case INTEGER:
-                        value = Integer.parseInt(editText);
-                        break;
-                    case LONG:
-                        value = Long.parseLong(editText);
-                        break;
-                    case FLOAT:
-                        value = Float.parseFloat(editText);
-                        break;
-                    case STRING:
-                        value = editText;
-                        break;
-                    default:
-                        throw new IllegalStateException(setting.toString());
-                }
-                SettingsEnum.setValue(setting, value);
+                SettingsEnum.setValue(setting, editText);
+            } else if (pref instanceof ListPreference) {
+                ListPreference listPref = (ListPreference) pref;
+                SettingsEnum.setValue(setting, listPref.getValue());
+                updateListPreferenceSummary((ListPreference) pref, setting);
             } else {
                 LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref);
+                return;
             }
 
-            if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
-                showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting);
-            } else if (setting.rebootApp) {
-                rebootDialog(getActivity());
+            if (!showingUserDialogMessage) {
+                if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
+                    showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting);
+                } else if (setting.rebootApp) {
+                    rebootDialog(getActivity());
+                }
             }
 
             enableDisablePreferences();
@@ -88,6 +85,20 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
 
             enableDisablePreferences();
 
+            // if the preference was included, then initialize it based on the available playback speed
+            Preference defaultSpeedPreference = findPreference(SettingsEnum.PLAYBACK_SPEED_DEFAULT.path);
+            if (defaultSpeedPreference instanceof ListPreference) {
+                RememberPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
+            }
+
+            // set the summary text for any ListPreferences
+            for (SettingsEnum setting : SettingsEnum.values()) {
+                Preference preference = findPreference(setting.path);
+                if (preference instanceof ListPreference) {
+                    updateListPreferenceSummary((ListPreference) preference, setting);
+                }
+            }
+
             preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
         } catch (Exception ex) {
             LogHelper.printException(() -> "onActivityCreated() error", ex);
@@ -109,6 +120,13 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
         }
     }
 
+    private void updateListPreferenceSummary(ListPreference listPreference, SettingsEnum setting) {
+        final int entryIndex = listPreference.findIndexOfValue(setting.getObjectValue().toString());
+        if (entryIndex >= 0) {
+            listPreference.setSummary(listPreference.getEntries()[entryIndex]);
+        }
+    }
+
     private void reboot(@NonNull Activity activity) {
         final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
         PendingIntent intent = PendingIntent.getActivity(activity, 0,
@@ -126,10 +144,12 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
                     reboot(activity);
                 })
                 .setNegativeButton(negativeButton,  null)
+                .setCancelable(false)
                 .show();
     }
 
     private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) {
+        showingUserDialogMessage = true;
         new AlertDialog.Builder(activity)
                 .setTitle(str("revanced_settings_confirm_user_dialog_title"))
                 .setMessage(setting.userDialogMessage.toString())
@@ -143,6 +163,10 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
                     SettingsEnum.setValue(setting, defaultBooleanValue);
                     switchPref.setChecked(defaultBooleanValue);
                 })
+                .setOnDismissListener(dialog -> {
+                    showingUserDialogMessage = false;
+                })
+                .setCancelable(false)
                 .show();
     }
 }