Expanded LauncherPrefs APIs to Replace Direct Shared Preference Usage.

LauncherPrefs will contain Launcher's shared preference functionality.
It controls optimizations and classifications such as restorable vs
non-restorable data, bootaware vs non-bootaware data, and configurations
such as default values  so the calling code doesn't need to and our code
base can have a single source of truth for items that are used in
multiple places.

The old APIs remain in place, but are deprecated and will be removed
after all Shared Preference usage has been gated by LauncherPrefs in
future CLs.

Bug: 261635315
Test: Manually tested themed icon, Workspace configuration, and app
install functionality.

Change-Id: I29fd516468bc93fda393062e95be26b6d55c816e
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 5efc45e..3d5c143 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.logging;
 
+import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 import static com.android.launcher3.LauncherPrefs.getDevicePrefs;
 import static com.android.launcher3.LauncherPrefs.getPrefs;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED;
@@ -39,6 +40,7 @@
 import android.util.Xml;
 
 import com.android.launcher3.AutoInstallsLayout;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
@@ -178,7 +180,7 @@
                 logger::log);
 
         SharedPreferences prefs = getPrefs(mContext);
-        logger.log(prefs.getBoolean(KEY_THEMED_ICONS, false)
+        logger.log(LauncherPrefs.get(mContext).get(THEMED_ICONS)
                 ? LAUNCHER_THEMED_ICON_ENABLED
                 : LAUNCHER_THEMED_ICON_DISABLED);
 
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 4965936..3461601 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -18,7 +18,8 @@
 
 import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
 
-import static com.android.launcher3.LauncherPrefs.getDevicePrefs;
+import static com.android.launcher3.LauncherPrefs.ICON_STATE;
+import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 
@@ -55,7 +56,7 @@
 public class LauncherAppState implements SafeCloseable {
 
     public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
-    private static final String KEY_ICON_STATE = "pref_icon_shape_path";
+    public static final String KEY_ICON_STATE = "pref_icon_shape_path";
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -117,10 +118,9 @@
                 observer, MODEL_EXECUTOR.getHandler());
         mOnTerminateCallback.add(iconChangeTracker::close);
         MODEL_EXECUTOR.execute(observer::verifyIconChanged);
-        SharedPreferences prefs = LauncherPrefs.getPrefs(mContext);
-        prefs.registerOnSharedPreferenceChangeListener(observer);
+        LauncherPrefs.get(context).addListener(observer, THEMED_ICONS);
         mOnTerminateCallback.add(
-                () -> prefs.unregisterOnSharedPreferenceChangeListener(observer));
+                () -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS));
 
         InstallSessionTracker installSessionTracker =
                 InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(mModel);
@@ -207,12 +207,12 @@
         public void onSystemIconStateChanged(String iconState) {
             IconShape.init(mContext);
             refreshAndReloadLauncher();
-            getDevicePrefs(mContext).edit().putString(KEY_ICON_STATE, iconState).apply();
+            LauncherPrefs.get(mContext).put(ICON_STATE, iconState);
         }
 
         void verifyIconChanged() {
             String iconState = mIconProvider.getSystemIconState();
-            if (!iconState.equals(getDevicePrefs(mContext).getString(KEY_ICON_STATE, ""))) {
+            if (!iconState.equals(LauncherPrefs.get(mContext).get(ICON_STATE))) {
                 onSystemIconStateChanged(iconState);
             }
         }
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 0d6ed04..1fb2dce 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -2,24 +2,255 @@
 
 import android.content.Context
 import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.allapps.WorkProfileManager
+import com.android.launcher3.model.DeviceGridState
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.provider.RestoreDbTask
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.Themes
 
-object LauncherPrefs {
+/**
+ * Use same context for shared preferences, so that we use a single cached instance
+ * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
+ */
+class LauncherPrefs(private val context: Context) {
 
-    @JvmStatic
-    fun getPrefs(context: Context): SharedPreferences {
-        // Use application context for shared preferences, so that we use a single cached instance
-        return context.applicationContext.getSharedPreferences(
-            LauncherFiles.SHARED_PREFERENCES_KEY,
-            Context.MODE_PRIVATE
-        )
+    /**
+     * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
+     * default value type, and will throw an error if the type of the item provided is not a
+     * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`.
+     */
+    @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
+    fun <T : Any> get(item: Item<T>): T {
+        val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
+
+        return when (item.defaultValue::class.java) {
+            String::class.java -> sp.getString(item.sharedPrefKey, item.defaultValue as String)
+            Boolean::class.java,
+            java.lang.Boolean::class.java ->
+                sp.getBoolean(item.sharedPrefKey, item.defaultValue as Boolean)
+            Int::class.java,
+            java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, item.defaultValue as Int)
+            Float::class.java,
+            java.lang.Float::class.java ->
+                sp.getFloat(item.sharedPrefKey, item.defaultValue as Float)
+            Long::class.java,
+            java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, item.defaultValue as Long)
+            Set::class.java -> sp.getStringSet(item.sharedPrefKey, item.defaultValue as Set<String>)
+            else ->
+                throw IllegalArgumentException(
+                    "item type: ${item.defaultValue::class.java}" +
+                        " is not compatible with sharedPref methods"
+                )
+        }
+            as T
     }
 
-    @JvmStatic
-    fun getDevicePrefs(context: Context): SharedPreferences {
-        // Use application context for shared preferences, so that we use a single cached instance
-        return context.applicationContext.getSharedPreferences(
-            LauncherFiles.DEVICE_PREFERENCES_KEY,
-            Context.MODE_PRIVATE
-        )
+    /**
+     * Stores each of the values provided in `SharedPreferences` according to the configuration
+     * contained within the associated items provided. Internally, it uses apply, so the caller
+     * cannot assume that the values that have been put are immediately available for use.
+     *
+     * The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from
+     * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
+     * provided item configurations.
+     */
+    fun put(vararg itemsToValues: Pair<Item<*>, Any>): Unit =
+        prepareToPutValues(itemsToValues).forEach { it.apply() }
+
+    /**
+     * Stores the value provided in `SharedPreferences` according to the item configuration provided
+     * It is asynchronous, so the caller can't assume that the value put is immediately available.
+     */
+    fun <T : Any> put(item: Item<T>, value: T): Unit =
+        context
+            .getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
+            .edit()
+            .putValue(item, value)
+            .apply()
+
+    /**
+     * Synchronously stores all the values provided according to their associated Item
+     * configuration.
+     */
+    fun putSync(vararg itemsToValues: Pair<Item<*>, Any>): Unit =
+        prepareToPutValues(itemsToValues).forEach { it.commit() }
+
+    /**
+     * Update each shared preference file with the item - value pairs provided. This method is
+     * optimized to avoid retrieving the same shared preference file multiple times.
+     *
+     * @return `List<SharedPreferences.Editor>` 1 for each distinct shared preference file among the
+     * items given as part of the itemsToValues parameter
+     */
+    private fun prepareToPutValues(
+        itemsToValues: Array<out Pair<Item<*>, Any>>
+    ): List<SharedPreferences.Editor> =
+        itemsToValues
+            .groupBy { it.first.sharedPrefFile }
+            .map { fileToItemValueList ->
+                context
+                    .getSharedPreferences(fileToItemValueList.key, Context.MODE_PRIVATE)
+                    .edit()
+                    .apply {
+                        fileToItemValueList.value.forEach { itemToValue ->
+                            putValue(itemToValue.first, itemToValue.second)
+                        }
+                    }
+            }
+
+    /**
+     * Handles adding values to `SharedPreferences` regardless of type. This method is especially
+     * helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple
+     * types of Item values.
+     */
+    @Suppress("UNCHECKED_CAST")
+    private fun SharedPreferences.Editor.putValue(
+        item: Item<*>,
+        value: Any
+    ): SharedPreferences.Editor =
+        when (value::class.java) {
+            String::class.java -> putString(item.sharedPrefKey, value as String)
+            Boolean::class.java,
+            java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean)
+            Int::class.java,
+            java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int)
+            Float::class.java,
+            java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float)
+            Long::class.java,
+            java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long)
+            Set::class.java -> putStringSet(item.sharedPrefKey, value as Set<String>)
+            else ->
+                throw IllegalArgumentException(
+                    "item type: " +
+                        "${item.defaultValue!!::class} is not compatible with sharedPref methods"
+                )
+        }
+
+    /**
+     * After calling this method, the listener will be notified of any future updates to the
+     * `SharedPreferences` files associated with the provided list of items. The listener will need
+     * to filter update notifications so they don't activate for non-relevant updates.
+     */
+    fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item<*>) {
+        items
+            .map { it.sharedPrefFile }
+            .distinct()
+            .forEach {
+                context
+                    .getSharedPreferences(it, Context.MODE_PRIVATE)
+                    .registerOnSharedPreferenceChangeListener(listener)
+            }
     }
+
+    /**
+     * Stops the listener from getting notified of any more updates to any of the
+     * `SharedPreferences` files associated with any of the provided list of [Item].
+     */
+    fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item<*>) {
+        // If a listener is not registered to a SharedPreference, unregistering it does nothing
+        items
+            .map { it.sharedPrefFile }
+            .distinct()
+            .forEach {
+                context
+                    .getSharedPreferences(it, Context.MODE_PRIVATE)
+                    .unregisterOnSharedPreferenceChangeListener(listener)
+            }
+    }
+
+    /**
+     * Checks if all the provided [Item] have values stored in their corresponding
+     * `SharedPreferences` files.
+     */
+    fun has(vararg items: Item<*>): Boolean {
+        items
+            .groupBy { it.sharedPrefFile }
+            .forEach { (file, itemsSublist) ->
+                val prefs: SharedPreferences =
+                    context.getSharedPreferences(file, Context.MODE_PRIVATE)
+                if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false
+            }
+        return true
+    }
+
+    /**
+     * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
+     */
+    fun remove(vararg items: Item<*>) = prepareToRemove(items).forEach { it.apply() }
+
+    /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
+    fun removeSync(vararg items: Item<*>) = prepareToRemove(items).forEach { it.commit() }
+
+    /**
+     * Creates `SharedPreferences.Editor` transactions for removing all the provided [Item] values
+     * from their respective `SharedPreferences` files. These returned `Editors` can then be
+     * committed or applied for synchronous or async behavior.
+     */
+    private fun prepareToRemove(items: Array<out Item<*>>): List<SharedPreferences.Editor> =
+        items
+            .groupBy { it.sharedPrefFile }
+            .map { (file, items) ->
+                context.getSharedPreferences(file, Context.MODE_PRIVATE).edit().also { editor ->
+                    items.forEach { item -> editor.remove(item.sharedPrefKey) }
+                }
+            }
+
+    companion object {
+        @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
+
+        @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
+
+        @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "")
+        @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false)
+        @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
+        @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
+        @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "")
+        @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1)
+        @JvmField
+        val DEVICE_TYPE =
+            backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
+        @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "")
+        @JvmField
+        val RESTORE_DEVICE =
+            backedUpItem(RestoreDbTask.RESTORED_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
+        @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
+        @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
+
+        @VisibleForTesting
+        @JvmStatic
+        fun <T> backedUpItem(sharedPrefKey: String, defaultValue: T): Item<T> =
+            Item(sharedPrefKey, LauncherFiles.SHARED_PREFERENCES_KEY, defaultValue)
+
+        @VisibleForTesting
+        @JvmStatic
+        fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): Item<T> =
+            Item(sharedPrefKey, LauncherFiles.DEVICE_PREFERENCES_KEY, defaultValue)
+
+        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
+        @JvmStatic
+        fun getPrefs(context: Context): SharedPreferences {
+            // Use application context for shared preferences, so we use single cached instance
+            return context.applicationContext.getSharedPreferences(
+                LauncherFiles.SHARED_PREFERENCES_KEY,
+                Context.MODE_PRIVATE
+            )
+        }
+
+        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
+        @JvmStatic
+        fun getDevicePrefs(context: Context): SharedPreferences {
+            // Use application context for shared preferences, so we use a single cached instance
+            return context.applicationContext.getSharedPreferences(
+                LauncherFiles.DEVICE_PREFERENCES_KEY,
+                Context.MODE_PRIVATE
+            )
+        }
+    }
+}
+
+data class Item<T>(val sharedPrefKey: String, val sharedPrefFile: String, val defaultValue: T) {
+    fun to(value: T): Pair<Item<T>, T> = Pair(this, value)
 }
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
index 4878077..1c67691 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
@@ -57,7 +57,6 @@
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider;
@@ -161,10 +160,8 @@
                 R.dimen.dynamic_grid_cell_border_spacing);
         mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor);
 
-        mWorkManager = new WorkProfileManager(
-                mActivityContext.getSystemService(UserManager.class),
-                this, LauncherPrefs.getPrefs(mActivityContext),
-                mActivityContext.getStatsLogManager());
+        mWorkManager = new WorkProfileManager(mActivityContext.getSystemService(UserManager.class),
+                this, mActivityContext.getStatsLogManager());
         mAH = Arrays.asList(null, null, null);
         mNavBarScrimPaint = new Paint();
         mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor));
diff --git a/src/com/android/launcher3/allapps/WorkEduCard.java b/src/com/android/launcher3/allapps/WorkEduCard.java
index b3245ee..b4cdc96 100644
--- a/src/com/android/launcher3/allapps/WorkEduCard.java
+++ b/src/com/android/launcher3/allapps/WorkEduCard.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP;
 import static com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.getTabWidth;
 
 import android.content.Context;
@@ -85,8 +86,7 @@
     @Override
     public void onClick(View view) {
         startAnimation(mDismissAnim);
-        LauncherPrefs.getPrefs(getContext()).edit().putInt(WorkProfileManager.KEY_WORK_EDU_STEP,
-                1).apply();
+        LauncherPrefs.get(getContext()).put(WORK_EDU_STEP, 1);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 279f0d3..fa03905 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
 import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.MAIN;
@@ -26,7 +27,6 @@
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
-import android.content.SharedPreferences;
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
@@ -40,6 +40,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
@@ -86,14 +87,12 @@
 
     @WorkProfileState
     private int mCurrentState;
-    private SharedPreferences mPreferences;
 
     public WorkProfileManager(
-            UserManager userManager, BaseAllAppsContainerView<?> allApps, SharedPreferences prefs,
+            UserManager userManager, BaseAllAppsContainerView<?> allApps,
             StatsLogManager statsLogManager) {
         mUserManager = userManager;
         mAllApps = allApps;
-        mPreferences = prefs;
         mMatcher = mAllApps.mPersonalMatcher.negate();
         mStatsLogManager = statsLogManager;
     }
@@ -225,7 +224,7 @@
     }
 
     private boolean isEduSeen() {
-        return mPreferences.getInt(KEY_WORK_EDU_STEP, 0) != 0;
+        return LauncherPrefs.get(mAllApps.getContext()).get(WORK_EDU_STEP) != 0;
     }
 
     private void onWorkFabClicked(View view) {
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index feadafa..9426c22 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -1,8 +1,7 @@
 package com.android.launcher3.graphics;
 
-import static com.android.launcher3.LauncherPrefs.getPrefs;
+import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS;
 import static com.android.launcher3.util.Themes.isThemedIconEnabled;
 
 import android.annotation.TargetApi;
@@ -25,6 +24,7 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Executors;
 
@@ -142,9 +142,8 @@
             }
             case ICON_THEMED:
             case SET_ICON_THEMED: {
-                getPrefs(getContext()).edit()
-                        .putBoolean(KEY_THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE))
-                        .apply();
+                LauncherPrefs.get(getContext())
+                        .put(THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE));
                 return 1;
             }
             default:
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 85d54c0..edc8c1b 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -17,7 +17,10 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.InvariantDeviceProfile.DeviceType;
-import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
+import static com.android.launcher3.LauncherPrefs.DB_FILE;
+import static com.android.launcher3.LauncherPrefs.DEVICE_TYPE;
+import static com.android.launcher3.LauncherPrefs.HOTSEAT_COUNT;
+import static com.android.launcher3.LauncherPrefs.WORKSPACE_SIZE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_2;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_3;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_4;
@@ -25,7 +28,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_6;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.text.TextUtils;
 
 import com.android.launcher3.InvariantDeviceProfile;
@@ -58,11 +60,11 @@
     }
 
     public DeviceGridState(Context context) {
-        SharedPreferences prefs = LauncherPrefs.getPrefs(context);
-        mGridSizeString = prefs.getString(KEY_WORKSPACE_SIZE, "");
-        mNumHotseat = prefs.getInt(KEY_HOTSEAT_COUNT, -1);
-        mDeviceType = prefs.getInt(KEY_DEVICE_TYPE, TYPE_PHONE);
-        mDbFile = prefs.getString(KEY_DB_FILE, "");
+        LauncherPrefs lp = LauncherPrefs.get(context);
+        mGridSizeString = lp.get(WORKSPACE_SIZE);
+        mNumHotseat = lp.get(HOTSEAT_COUNT);
+        mDeviceType = lp.get(DEVICE_TYPE);
+        mDbFile = lp.get(DB_FILE);
     }
 
     /**
@@ -90,12 +92,11 @@
      * Stores the device state to shared preferences
      */
     public void writeToPrefs(Context context) {
-        LauncherPrefs.getPrefs(context).edit()
-                .putString(KEY_WORKSPACE_SIZE, mGridSizeString)
-                .putInt(KEY_HOTSEAT_COUNT, mNumHotseat)
-                .putInt(KEY_DEVICE_TYPE, mDeviceType)
-                .putString(KEY_DB_FILE, mDbFile)
-                .apply();
+        LauncherPrefs.get(context).put(
+                WORKSPACE_SIZE.to(mGridSizeString),
+                HOTSEAT_COUNT.to(mNumHotseat),
+                DEVICE_TYPE.to(mDeviceType),
+                DB_FILE.to(mDbFile));
     }
 
     /**
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 150bca4..db23566 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.pm;
 
-import static com.android.launcher3.LauncherPrefs.getPrefs;
-
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -34,6 +32,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.SessionCommitReceiver;
 import com.android.launcher3.Utilities;
@@ -65,7 +64,7 @@
     // Set<String> of session ids of promise icons that have been added to the home screen
     // as FLAG_PROMISE_NEW_INSTALLS.
     @NonNull
-    protected static final String PROMISE_ICON_IDS = "promise_icon_ids";
+    public static final String PROMISE_ICON_IDS = "promise_icon_ids";
 
     private static final boolean DEBUG = false;
 
@@ -102,7 +101,7 @@
             return mPromiseIconIds;
         }
         mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
-                getPrefs(mAppContext).getString(PROMISE_ICON_IDS, "")));
+                LauncherPrefs.get(mAppContext).get(LauncherPrefs.PROMISE_ICON_IDS)));
 
         IntArray existingIds = new IntArray();
         for (SessionInfo info : getActiveSessions().values()) {
@@ -146,9 +145,8 @@
     }
 
     private void updatePromiseIconPrefs() {
-        getPrefs(mAppContext).edit()
-                .putString(PROMISE_ICON_IDS, getPromiseIconIds().getArray().toConcatString())
-                .apply();
+        LauncherPrefs.get(mAppContext).put(LauncherPrefs.PROMISE_ICON_IDS,
+                getPromiseIconIds().getArray().toConcatString());
     }
 
     @Nullable
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 5e97b2d..2a452be 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -17,13 +17,14 @@
 package com.android.launcher3.provider;
 
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
-import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
+import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS;
+import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS;
+import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
 import android.app.backup.BackupManager;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.UserHandle;
@@ -62,13 +63,13 @@
 public class RestoreDbTask {
 
     private static final String TAG = "RestoreDbTask";
-    private static final String RESTORED_DEVICE_TYPE = "restored_task_pending";
+    public static final String RESTORED_DEVICE_TYPE = "restored_task_pending";
 
     private static final String INFO_COLUMN_NAME = "name";
     private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
 
-    private static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
-    private static final String APPWIDGET_IDS = "appwidget_ids";
+    public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
+    public static final String APPWIDGET_IDS = "appwidget_ids";
 
     /**
      * Tries to restore the backup DB if needed
@@ -87,7 +88,7 @@
 
         // Set is pending to false irrespective of the result, so that it doesn't get
         // executed again.
-        LauncherPrefs.getPrefs(context).edit().remove(RESTORED_DEVICE_TYPE).commit();
+        LauncherPrefs.get(context).removeSync(RESTORE_DEVICE);
 
         idp.reinitializeAfterRestore(context);
     }
@@ -240,8 +241,7 @@
         }
 
         // If restored from a single display backup, remove gaps between screenIds
-        if (LauncherPrefs.getPrefs(context).getInt(RESTORED_DEVICE_TYPE, TYPE_PHONE)
-                != TYPE_MULTI_DISPLAY) {
+        if (LauncherPrefs.get(context).get(RESTORE_DEVICE) != TYPE_MULTI_DISPLAY) {
             removeScreenIdGaps(db);
         }
 
@@ -339,7 +339,7 @@
     }
 
     public static boolean isPending(Context context) {
-        return LauncherPrefs.getPrefs(context).contains(RESTORED_DEVICE_TYPE);
+        return LauncherPrefs.get(context).has(RESTORE_DEVICE);
     }
 
     /**
@@ -347,34 +347,31 @@
      */
     public static void setPending(Context context) {
         FileLog.d(TAG, "Restore data received through full backup ");
-        LauncherPrefs.getPrefs(context).edit()
-                .putInt(RESTORED_DEVICE_TYPE, new DeviceGridState(context).getDeviceType())
-                .commit();
+        LauncherPrefs.get(context)
+                .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType()));
     }
 
     private void restoreAppWidgetIdsIfExists(Context context) {
-        SharedPreferences prefs = LauncherPrefs.getPrefs(context);
-        if (prefs.contains(APPWIDGET_OLD_IDS) && prefs.contains(APPWIDGET_IDS)) {
+        LauncherPrefs lp = LauncherPrefs.get(context);
+        if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) {
             LauncherWidgetHolder holder = LauncherWidgetHolder.newInstance(context);
             AppWidgetsRestoredReceiver.restoreAppWidgetIds(context,
-                    IntArray.fromConcatString(prefs.getString(APPWIDGET_OLD_IDS, "")).toArray(),
-                    IntArray.fromConcatString(prefs.getString(APPWIDGET_IDS, "")).toArray(),
+                    IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(),
+                    IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
                     holder);
             holder.destroy();
         } else {
             FileLog.d(TAG, "No app widget ids to restore.");
         }
 
-        prefs.edit().remove(APPWIDGET_OLD_IDS)
-                .remove(APPWIDGET_IDS).apply();
+        lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS);
     }
 
     public static void setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds,
             @NonNull int[] newIds) {
-        LauncherPrefs.getPrefs(context).edit()
-                .putString(APPWIDGET_OLD_IDS, IntArray.wrap(oldIds).toConcatString())
-                .putString(APPWIDGET_IDS, IntArray.wrap(newIds).toConcatString())
-                .commit();
+        LauncherPrefs.get(context).putSync(
+                OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()),
+                APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString()));
     }
 
 }
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index 585bea9..5526839 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -19,6 +19,8 @@
 import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_TEXT;
 import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_THEME;
 
+import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
+
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
 import android.content.Context;
@@ -74,7 +76,7 @@
      * Returns true if workspace icon theming is enabled
      */
     public static boolean isThemedIconEnabled(Context context) {
-        return LauncherPrefs.getPrefs(context).getBoolean(KEY_THEMED_ICONS, false);
+        return LauncherPrefs.get(context).get(THEMED_ICONS);
     }
 
     public static String getDefaultBodyFont(Context context) {
diff --git a/tests/src/com/android/launcher3/LauncherPrefsTest.kt b/tests/src/com/android/launcher3/LauncherPrefsTest.kt
new file mode 100644
index 0000000..151abf1
--- /dev/null
+++ b/tests/src/com/android/launcher3/LauncherPrefsTest.kt
@@ -0,0 +1,148 @@
+package com.android.launcher3
+
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_BOOLEAN_ITEM = LauncherPrefs.nonRestorableItem("1", false)
+private val TEST_STRING_ITEM = LauncherPrefs.nonRestorableItem("2", "( ͡❛ ͜ʖ ͡❛)")
+private val TEST_INT_ITEM = LauncherPrefs.nonRestorableItem("3", -1)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LauncherPrefsTest {
+
+    private val launcherPrefs by lazy {
+        LauncherPrefs.get(InstrumentationRegistry.getInstrumentation().targetContext).apply {
+            remove(TEST_BOOLEAN_ITEM, TEST_STRING_ITEM, TEST_INT_ITEM)
+        }
+    }
+
+    @Test
+    fun has_keyMissingFromLauncherPrefs_returnsFalse() {
+        assertThat(launcherPrefs.has(TEST_BOOLEAN_ITEM)).isFalse()
+    }
+
+    @Test
+    fun has_keyPresentInLauncherPrefs_returnsTrue() {
+        with(launcherPrefs) {
+            putSync(TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue))
+            assertThat(has(TEST_BOOLEAN_ITEM)).isTrue()
+            remove(TEST_BOOLEAN_ITEM)
+        }
+    }
+
+    @Test
+    fun addListener_listeningForStringItemUpdates_isCorrectlyNotifiedOfUpdates() {
+        val latch = CountDownLatch(1)
+        val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() }
+
+        with(launcherPrefs) {
+            putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue))
+            addListener(listener, TEST_STRING_ITEM)
+            putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc"))
+
+            assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue()
+            remove(TEST_STRING_ITEM)
+        }
+    }
+
+    @Test
+    fun removeListener_previouslyListeningForStringItemUpdates_isNoLongerNotifiedOfUpdates() {
+        val latch = CountDownLatch(1)
+        val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() }
+
+        with(launcherPrefs) {
+            addListener(listener, TEST_STRING_ITEM)
+            removeListener(listener, TEST_STRING_ITEM)
+            putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "hello."))
+
+            // latch will be still be 1 (and await will return false) if the listener was not called
+            assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse()
+            remove(TEST_STRING_ITEM)
+        }
+    }
+
+    @Test
+    fun addListenerAndRemoveListener_forMultipleItems_bothWorkProperly() {
+        var latch = CountDownLatch(3)
+        val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() }
+
+        with(launcherPrefs) {
+            addListener(listener, TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM)
+            putSync(
+                TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue + 123),
+                TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc"),
+                TEST_BOOLEAN_ITEM.to(!TEST_BOOLEAN_ITEM.defaultValue)
+            )
+            assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue()
+
+            removeListener(listener, TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM)
+            latch = CountDownLatch(1)
+            putSync(
+                TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue),
+                TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue),
+                TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue)
+            )
+            remove(TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM)
+
+            assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse()
+        }
+    }
+
+    @Test
+    fun get_booleanItemNotInLauncherprefs_returnsDefaultValue() {
+        assertThat(launcherPrefs.get(TEST_BOOLEAN_ITEM)).isEqualTo(TEST_BOOLEAN_ITEM.defaultValue)
+    }
+
+    @Test
+    fun get_stringItemNotInLauncherPrefs_returnsDefaultValue() {
+        assertThat(launcherPrefs.get(TEST_STRING_ITEM)).isEqualTo(TEST_STRING_ITEM.defaultValue)
+    }
+
+    @Test
+    fun get_intItemNotInLauncherprefs_returnsDefaultValue() {
+        assertThat(launcherPrefs.get(TEST_INT_ITEM)).isEqualTo(TEST_INT_ITEM.defaultValue)
+    }
+
+    @Test
+    fun put_storesItemInLauncherPrefs_successfully() {
+        val notDefaultValue = !TEST_BOOLEAN_ITEM.defaultValue
+
+        with(launcherPrefs) {
+            putSync(TEST_BOOLEAN_ITEM.to(notDefaultValue))
+            assertThat(get(TEST_BOOLEAN_ITEM)).isEqualTo(notDefaultValue)
+            remove(TEST_BOOLEAN_ITEM)
+        }
+    }
+
+    @Test
+    fun put_storesListOfItemsInLauncherPrefs_successfully() {
+        with(launcherPrefs) {
+            putSync(
+                TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue),
+                TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue),
+                TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue)
+            )
+            assertThat(has(TEST_BOOLEAN_ITEM, TEST_INT_ITEM, TEST_STRING_ITEM)).isTrue()
+            remove(TEST_STRING_ITEM, TEST_INT_ITEM, TEST_BOOLEAN_ITEM)
+        }
+    }
+
+    @Test
+    fun remove_deletesItemFromLauncherPrefs_successfully() {
+        val notDefaultValue = !TEST_BOOLEAN_ITEM.defaultValue
+
+        with(launcherPrefs) {
+            putSync(TEST_BOOLEAN_ITEM.to(notDefaultValue))
+            remove(TEST_BOOLEAN_ITEM)
+            assertThat(get(TEST_BOOLEAN_ITEM)).isEqualTo(TEST_BOOLEAN_ITEM.defaultValue)
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index dcc8ec7..a85fa3a 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -24,7 +24,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.LauncherFiles
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
 import com.android.launcher3.LauncherSettings.Favorites.*
 import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
@@ -754,11 +755,7 @@
             .edit()
             .putBoolean(FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.key, true)
             .commit()
-        context
-            .getSharedPreferences(LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
-            .edit()
-            .putString(DeviceGridState.KEY_WORKSPACE_SIZE, srcGridSize)
-            .commit()
+        LauncherPrefs.get(context).putSync(WORKSPACE_SIZE.to(srcGridSize))
         FeatureFlags.initialize(context)
     }
 
diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 93bf312..caec301 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.AllAppsList;
@@ -506,7 +507,7 @@
 
         SanboxModelContext() {
             super(ApplicationProvider.getApplicationContext(),
-                    UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
+                    UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE,
                     LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                     DisplayController.INSTANCE, CustomWidgetManager.INSTANCE,
                     SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE,