Merge "Import translations. DO NOT MERGE ANYWHERE" into tm-qpr-dev
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/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 9f54f09..edbce10 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,11 +16,14 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -291,7 +294,7 @@
 
     @UiThread
     public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
-        applyFromWorkspaceItem(info, false);
+        applyFromWorkspaceItem(info, null);
     }
 
     /**
@@ -320,10 +323,10 @@
     }
 
     @UiThread
-    public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
+    public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) {
         applyIconAndLabel(info);
         setItemInfo(info);
-        applyLoadingState(promiseStateChanged);
+        applyLoadingState(icon);
         applyDotState(info, false /* animate */);
         setDownloadStateContentDescription(info, info.getProgressLevel());
     }
@@ -710,23 +713,23 @@
      * If this app is installed and downloading incrementally, the progress bar will be updated
      * with the total download progress.
      */
-    public void applyLoadingState(boolean promiseStateChanged) {
+    public void applyLoadingState(PreloadIconDrawable icon) {
         if (getTag() instanceof ItemInfoWithIcon) {
             WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
-            if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE)
-                    != 0) {
-                updateProgressBarUi(info.getProgressLevel() == 100);
-            } else if (info.hasPromiseIconUi() || (info.runtimeStatusFlags
-                        & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
-                updateProgressBarUi(promiseStateChanged);
+            if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0
+                    || info.hasPromiseIconUi()
+                    || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0
+                    || (ENABLE_DOWNLOAD_APP_UX_V2.get() && icon != null)) {
+                updateProgressBarUi(icon);
             }
         }
     }
 
-    private void updateProgressBarUi(boolean maybePerformFinishedAnimation) {
+    private void updateProgressBarUi(PreloadIconDrawable oldIcon) {
+        FastBitmapDrawable originalIcon = mIcon;
         PreloadIconDrawable preloadDrawable = applyProgressLevel();
-        if (preloadDrawable != null && maybePerformFinishedAnimation) {
-            preloadDrawable.maybePerformFinishedAnimation();
+        if (preloadDrawable != null && oldIcon != null) {
+            preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon));
         }
     }
 
@@ -824,12 +827,12 @@
                 != 0) {
             String percentageString = NumberFormat.getPercentInstance()
                     .format(progressLevel * 0.01);
-            if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+            if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                 setContentDescription(getContext()
                         .getString(
                             R.string.app_installing_title, info.title, percentageString));
             } else if ((info.runtimeStatusFlags
-                    & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
+                    & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
                 setContentDescription(getContext()
                         .getString(
                             R.string.app_downloading_title, info.title, percentageString));
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/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index de47cb5..9001a52 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -28,6 +28,7 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorFilter;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
@@ -41,6 +42,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -53,7 +55,7 @@
 /**
  * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
  */
-public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable {
+public class PreloadIconDrawable extends FastBitmapDrawable {
 
     private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
             new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
@@ -78,16 +80,19 @@
 
     // The smaller the number, the faster the animation would be.
     // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
-    private static final float COMPLETE_ANIM_FRACTION = 0.3f;
+    private static final float COMPLETE_ANIM_FRACTION = 1f;
 
     private static final float SMALL_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get() ? 0.867f : 0.7f;
-    private static final float PROGRESS_STROKE_SCALE = 0.075f;
+    private static final float PROGRESS_STROKE_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get()
+            ? 0.0655f
+            : 0.075f;
+    private static final float PROGRESS_BOUNDS_SCALE = 0.075f;
 
     private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
     private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
 
     private static final int ALPHA_DURATION_MILLIS = 3000;
-    private static final float OVERLAY_ALPHA_RANGE = 127.5f;
+    private static final int OVERLAY_ALPHA_RANGE = 127;
     private static final long WAVE_MOTION_DELAY_FACTOR_MILLIS = 100;
     private static final WeakHashMap<Integer, PorterDuffColorFilter> COLOR_FILTER_MAP =
             new WeakHashMap<>();
@@ -111,19 +116,17 @@
     private final int mSystemBackgroundColor;
     private final boolean mIsDarkMode;
 
-    private int mTrackAlpha;
     private float mTrackLength;
 
     private boolean mRanFinishAnimation;
-
     private final int mRefreshRateMillis;
-    private final AnimatedFloat mIconScale = new AnimatedFloat(this::invalidateSelf);
-    private final AnimatedFloat mOverlayAlpha = new AnimatedFloat(this::updateOverlayAlpha);
-    private boolean mShouldAnimateScaleAndAlpha;
 
     // Progress of the internal state. [0, 1] indicates the fraction of completed progress,
     // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
     private float mInternalStateProgress;
+    // This multiplier is used to animate scale when going from 0 to non-zero and expanding
+    private final Runnable mInvalidateRunnable = this::invalidateSelf;
+    private final AnimatedFloat mIconScaleMultiplier = new AnimatedFloat(mInvalidateRunnable);
 
     private ObjectAnimator mCurrentAnim;
 
@@ -160,10 +163,7 @@
         mRefreshRateMillis = refreshRateMillis;
 
         // If it's a pending app we will animate scale and alpha when it's no longer pending.
-        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && info.getProgressLevel() == 0) {
-            mShouldAnimateScaleAndAlpha = true;
-            mOverlayAlpha.updateValue(127);
-        }
+        mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
 
         setLevel(info.getProgressLevel());
         setIsStartable(info.isAppStartable());
@@ -173,14 +173,17 @@
     protected void onBoundsChange(Rect bounds) {
         super.onBoundsChange(bounds);
 
-        float progressWidth = PROGRESS_STROKE_SCALE * bounds.width();
+
+        float progressWidth = bounds.width() * (ENABLE_DOWNLOAD_APP_UX_V2.get()
+                ? PROGRESS_BOUNDS_SCALE
+                : PROGRESS_STROKE_SCALE);
         mTmpMatrix.setScale(
                 (bounds.width() - 2 * progressWidth) / DEFAULT_PATH_SIZE,
                 (bounds.height() - 2 * progressWidth) / DEFAULT_PATH_SIZE);
         mTmpMatrix.postTranslate(bounds.left + progressWidth, bounds.top + progressWidth);
 
         mShapePath.transform(mTmpMatrix, mScaledTrackPath);
-        mProgressPaint.setStrokeWidth(progressWidth);
+        mProgressPaint.setStrokeWidth(PROGRESS_STROKE_SCALE * bounds.width());
 
         mPathMeasure.setPath(mScaledTrackPath, true);
         mTrackLength = mPathMeasure.getLength();
@@ -195,26 +198,35 @@
             return;
         }
 
-        // Draw background.
-        mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE);
-        mProgressPaint.setColor(mSystemBackgroundColor);
-        canvas.drawPath(mScaledTrackPath, mProgressPaint);
+        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress > 0) {
+            // Draw background.
+            mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+            mProgressPaint.setColor(mSystemBackgroundColor);
+            canvas.drawPath(mScaledTrackPath, mProgressPaint);
+        }
 
-        // Draw track and progress.
-        mProgressPaint.setStyle(Paint.Style.STROKE);
-        mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor);
-        mProgressPaint.setAlpha(TRACK_ALPHA);
-        canvas.drawPath(mScaledTrackPath, mProgressPaint);
-        mProgressPaint.setAlpha(mTrackAlpha);
-        canvas.drawPath(mScaledProgressPath, mProgressPaint);
+        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
+            // Draw track and progress.
+            mProgressPaint.setStyle(Paint.Style.STROKE);
+            mProgressPaint.setColor(mSystemAccentColor);
+            mProgressPaint.setAlpha(TRACK_ALPHA);
+            canvas.drawPath(mScaledTrackPath, mProgressPaint);
+            mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
+            canvas.drawPath(mScaledProgressPath, mProgressPaint);
+        }
 
         int saveCount = canvas.save();
-        canvas.scale(
-                mIconScale.value, mIconScale.value, bounds.exactCenterX(), bounds.exactCenterY());
+        float scale = ENABLE_DOWNLOAD_APP_UX_V2.get()
+                ? 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE)
+                : SMALL_SCALE;
+        canvas.scale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY());
+
+        ColorFilter filter = getOverlayFilter();
+        mPaint.setColorFilter(filter);
         super.drawInternal(canvas, bounds);
         canvas.restoreToCount(saveCount);
 
-        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress == 0) {
+        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && filter != null) {
             reschedule();
         }
     }
@@ -232,7 +244,7 @@
     @Override
     protected boolean onLevelChange(int level) {
         // Run the animation if we have already been bound.
-        updateInternalState(level * 0.01f,  getBounds().width() > 0, false);
+        updateInternalState(level * 0.01f, false, null);
         return true;
     }
 
@@ -240,12 +252,18 @@
      * Runs the finish animation if it is has not been run after last call to
      * {@link #onLevelChange}
      */
-    public void maybePerformFinishedAnimation() {
+    public void maybePerformFinishedAnimation(
+            PreloadIconDrawable oldIcon, Runnable onFinishCallback) {
+
+        if (oldIcon.mInternalStateProgress >= 1) {
+            mInternalStateProgress = oldIcon.mInternalStateProgress;
+        }
+
         // If the drawable was recently initialized, skip the progress animation.
         if (mInternalStateProgress == 0) {
             mInternalStateProgress = 1;
         }
-        updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
+        updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, onFinishCallback);
     }
 
     public boolean hasNotCompleted() {
@@ -260,26 +278,29 @@
         }
     }
 
-    private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
+    private void updateInternalState(
+            float finalProgress, boolean isFinish, Runnable onFinishCallback) {
         if (mCurrentAnim != null) {
             mCurrentAnim.cancel();
             mCurrentAnim = null;
         }
 
-        if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
-            return;
-        }
-        if (finalProgress < mInternalStateProgress) {
-            shouldAnimate = false;
-        }
-        if (!shouldAnimate || mRanFinishAnimation) {
+        boolean animateProgress =
+                finalProgress >= mInternalStateProgress && getBounds().width() > 0;
+        if (!animateProgress || mRanFinishAnimation) {
             setInternalProgress(finalProgress);
+            if (isFinish && onFinishCallback != null) {
+                onFinishCallback.run();
+            }
         } else {
             mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
             mCurrentAnim.setDuration(
                     (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
             mCurrentAnim.setInterpolator(LINEAR);
             if (isFinish) {
+                if (onFinishCallback != null) {
+                    mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback));
+                }
                 mCurrentAnim.addListener(new AnimatorListenerAdapter() {
                     @Override
                     public void onAnimationEnd(Animator animation) {
@@ -297,62 +318,38 @@
      *     - icon with pending motion
      *     - progress track is not visible
      *     - progress bar is not visible
-     *   for progress < 1
+     *   for progress < 1:
      *     - icon without pending motion
      *     - progress track is visible
      *     - progress bar is visible. Progress bar is drawn as a fraction of
      *       {@link #mScaledTrackPath}.
      *       @see PathMeasure#getSegment(float, float, Path, boolean)
-     *   for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION)
-     *     - we calculate fraction of progress in the above range
-     *     - progress track is drawn with alpha based on fraction
-     *     - progress bar is drawn at 100% with alpha based on fraction
-     *     - icon is scaled up based on fraction and is drawn in enabled state
-     *   for progress >= (1 + COMPLETE_ANIM_FRACTION)
-     *     - only icon is drawn in normal state
+     *   for progress > 1:
+     *     - scale the icon back to full size
      */
     private void setInternalProgress(float progress) {
         // Animate scale and alpha from pending to downloading state.
-        if (ENABLE_DOWNLOAD_APP_UX_V2.get()
-                && mShouldAnimateScaleAndAlpha && mInternalStateProgress == 0 && progress > 0) {
-            Animator iconScaleAnimator = mIconScale.animateToValue(SMALL_SCALE);
+        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && progress > 0 && mInternalStateProgress == 0) {
+            // Progress is changing for the first time, animate the icon scale
+            Animator iconScaleAnimator = mIconScaleMultiplier.animateToValue(1);
             iconScaleAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
             iconScaleAnimator.setInterpolator(EMPHASIZED);
             iconScaleAnimator.start();
-
-            Animator overlayAlphaAnimator = mOverlayAlpha.animateToValue(0);
-            overlayAlphaAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
-            overlayAlphaAnimator.setInterpolator(EMPHASIZED);
-            overlayAlphaAnimator.start();
         }
 
         mInternalStateProgress = progress;
         if (progress <= 0) {
-            mIconScale.updateValue(ENABLE_DOWNLOAD_APP_UX_V2.get() ? 1 : SMALL_SCALE);
-            mScaledTrackPath.reset();
-            mTrackAlpha = MAX_PAINT_ALPHA;
-        } else if (progress < 1) {
-            mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
-            if (ENABLE_DOWNLOAD_APP_UX_V2.get()) {
-                mPathMeasure.getSegment(0, mTrackLength, mScaledTrackPath, true);
+            if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
+                mScaledTrackPath.reset();
             }
-
-            if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || !mShouldAnimateScaleAndAlpha) {
-                mIconScale.updateValue(SMALL_SCALE);
-            }
-            mTrackAlpha = MAX_PAINT_ALPHA;
+            mIconScaleMultiplier.updateValue(0);
         } else {
-            setIsDisabled(mItem.isDisabled());
-            mScaledTrackPath.set(mScaledProgressPath);
-            float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
-
-            if (fraction >= 1) {
-                // Animation has completed
-                mIconScale.updateValue(1);
-                mTrackAlpha = 0;
-            } else {
-                mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
-                mIconScale.updateValue(SMALL_SCALE + (1 - SMALL_SCALE) * fraction);
+            mPathMeasure.getSegment(
+                    0, Math.min(progress, 1) * mTrackLength, mScaledProgressPath, true);
+            if (progress > 1 && ENABLE_DOWNLOAD_APP_UX_V2.get()) {
+                // map the scale back to original value
+                mIconScaleMultiplier.updateValue(Utilities.mapBoundToRange(
+                        progress - 1, 0, COMPLETE_ANIM_FRACTION, 1, 0, EMPHASIZED));
             }
         }
         invalidateSelf();
@@ -393,71 +390,48 @@
     }
 
     @Override
-    public void run() {
-        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
-            return;
-        }
-        if (!applyPendingIconOverlay()) {
-            reschedule();
-        }
-    }
-
-    @Override
     public boolean setVisible(boolean visible, boolean restart) {
-        boolean result = super.setVisible(visible, restart);
-        if (visible) {
-            reschedule();
-        } else {
-            unscheduleSelf(this);
+        if (!visible) {
+            unscheduleSelf(mInvalidateRunnable);
         }
-        return result;
+        return super.setVisible(visible, restart);
     }
 
     private void reschedule() {
-        unscheduleSelf(this);
-
+        unscheduleSelf(mInvalidateRunnable);
         if (!isVisible()) {
             return;
         }
-
         final long upTime = SystemClock.uptimeMillis();
-        scheduleSelf(this, upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
+        scheduleSelf(mInvalidateRunnable,
+                upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
     }
 
-
     /**
-     * Apply an overlay on the pending icon with cascading motion based on its position.
-     * Returns {@code true} if the icon alpha is updated, so that we re-draw.
+     * Returns a color filter to be used as an overlay on the pending icon with cascading motion
+     * based on its position.
      */
-    private boolean applyPendingIconOverlay() {
+    private ColorFilter getOverlayFilter() {
+        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
+            // If the download has started, we do no need to animate
+            return null;
+        }
         long waveMotionDelay = (mItem.cellX * WAVE_MOTION_DELAY_FACTOR_MILLIS)
                 + (mItem.cellY * WAVE_MOTION_DELAY_FACTOR_MILLIS);
         long time = SystemClock.uptimeMillis();
-        float newAlpha = Utilities.mapBoundToRange(
-                (float) (time + waveMotionDelay) % ALPHA_DURATION_MILLIS,
+        int alpha = (int) Utilities.mapBoundToRange(
+                (int) ((time + waveMotionDelay) % ALPHA_DURATION_MILLIS),
                 0,
                 ALPHA_DURATION_MILLIS,
                 0,
-                MAX_PAINT_ALPHA,
+                OVERLAY_ALPHA_RANGE * 2,
                 LINEAR);
-        if (newAlpha > OVERLAY_ALPHA_RANGE) {
-            newAlpha = (OVERLAY_ALPHA_RANGE - (newAlpha % OVERLAY_ALPHA_RANGE));
+        if (alpha > OVERLAY_ALPHA_RANGE) {
+            alpha = (OVERLAY_ALPHA_RANGE - (alpha % OVERLAY_ALPHA_RANGE));
         }
-
-        boolean invalidate = false;
-        if ((int) mOverlayAlpha.value != newAlpha) {
-            mOverlayAlpha.updateValue(newAlpha);
-            invalidate = true;
-        }
-        return invalidate;
-    }
-
-    private void updateOverlayAlpha() {
         int overlayColor = mIsDarkMode ? 0 : 255;
-        int currArgb =
-                Color.argb((int) mOverlayAlpha.value, overlayColor, overlayColor, overlayColor);
-        mPaint.setColorFilter(COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY));
-        invalidateSelf();
+        int currArgb = Color.argb(alpha, overlayColor, overlayColor, overlayColor);
+        return COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY);
     }
 
     protected static class PreloadIconConstantState extends FastBitmapConstantState {
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/LauncherBindableItemsContainer.java b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
index a4cb30a..f73940b 100644
--- a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
+++ b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
@@ -50,7 +50,12 @@
                 Drawable oldIcon = shortcut.getIcon();
                 boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
                         && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
-                shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
+                shortcut.applyFromWorkspaceItem(
+                        si,
+                        si.isPromise() != oldPromiseState
+                                && oldIcon instanceof PreloadIconDrawable
+                                ? (PreloadIconDrawable) oldIcon
+                                : null);
             } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
                 ((FolderIcon) v).updatePreviewItems(updates::contains);
             }
@@ -74,7 +79,7 @@
         ItemOperator op = (info, v) -> {
             if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
                     && updates.contains(info)) {
-                ((BubbleTextView) v).applyLoadingState(false /* promiseStateChanged */);
+                ((BubbleTextView) v).applyLoadingState(null);
             } else if (v instanceof PendingAppWidgetHostView
                     && info instanceof LauncherAppWidgetInfo
                     && updates.contains(info)) {
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,