Merge "[dev_option] Move DestopModeFlags out of android.windows.flags" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index c3c42fa..457fdd8 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -65,6 +65,13 @@
 }
 
 flag {
+    name: "enable_taskbar_connected_displays"
+    namespace: "launcher"
+    description: "Enables connected displays in taskbar."
+    bug: "362720616"
+}
+
+flag {
     name: "enable_taskbar_customization"
     namespace: "launcher"
     description: "Enables taskbar customization framework."
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 6af5a30..4014f06 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -237,7 +237,7 @@
             InstanceId instanceId = new InstanceIdSequence().newInstanceId();
             for (ItemInfo info : itemsIdMap) {
                 CollectionInfo parent = getContainer(info, itemsIdMap);
-                StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
+                StatsLogCompatManager.writeSnapshot(info.buildProto(parent, mContext), instanceId);
             }
             additionalSnapshotEvents(instanceId);
             prefs.put(LAST_SNAPSHOT_TIME_MILLIS, now);
@@ -274,7 +274,7 @@
 
                         for (ItemInfo info : itemsIdMap) {
                             CollectionInfo parent = getContainer(info, itemsIdMap);
-                            LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
+                            LauncherAtom.ItemInfo itemInfo = info.buildProto(parent, mContext);
                             Log.d(TAG, itemInfo.toString());
                             StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
                                     instanceId);
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 5b1b68a..09dbeb6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -17,6 +17,8 @@
 
 import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
+import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_APP_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.getTaskbarToHomeDuration;
 import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION;
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
@@ -32,7 +34,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.logging.InstanceId;
@@ -205,11 +206,17 @@
                 isVisible,
                 fromInitOrDestroy,
                 /* startAnimation= */ true,
-                DisplayController.isTransientTaskbar(mLauncher)
-                        ? TRANSIENT_TASKBAR_TRANSITION_DURATION
-                        : (!isVisible
-                                ? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION
-                                : QuickstepTransitionManager.getTaskbarToHomeDuration()));
+                getTaskbarAnimationDuration(isVisible));
+    }
+
+    private int getTaskbarAnimationDuration(boolean isVisible) {
+        if (isVisible && !mLauncher.getPredictiveBackToHomeInProgress()) {
+            return getTaskbarToHomeDuration();
+        } else {
+            return DisplayController.isTransientTaskbar(mLauncher)
+                    ? TRANSIENT_TASKBAR_TRANSITION_DURATION
+                    : TASKBAR_TO_APP_DURATION;
+        }
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index a4fbb25..fc66bec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT;
 import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
@@ -42,6 +43,7 @@
 import android.animation.ObjectAnimator;
 import android.os.SystemClock;
 import android.util.Log;
+import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -64,6 +66,7 @@
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.animation.ViewRootSync;
@@ -682,7 +685,9 @@
             animatorSet.play(iconAlignAnim);
         }
 
-        animatorSet.setInterpolator(EMPHASIZED);
+        Interpolator interpolator = enableScalingRevealHomeAnimation()
+                ? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
+        animatorSet.setInterpolator(interpolator);
 
         if (start) {
             animatorSet.start();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 82d014e..39bf6ac 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1316,6 +1316,10 @@
         mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress);
     }
 
+    public boolean getPredictiveBackToHomeInProgress() {
+        return mIsPredictiveBackToHomeInProgress;
+    }
+
     @Override
     public boolean areDesktopTasksVisible() {
         DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 181cba0..417bb74 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -35,9 +35,6 @@
 import android.provider.Settings.Secure
 import android.text.Html
 import android.util.AttributeSet
-import android.util.Base64
-import android.util.Base64.NO_PADDING
-import android.util.Base64.NO_WRAP
 import android.view.inputmethod.EditorInfo
 import android.widget.TextView
 import android.widget.Toast
@@ -57,9 +54,10 @@
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY
 import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
 import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
+import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
 import com.android.launcher3.R
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfo
@@ -241,7 +239,7 @@
     private fun DebugInfo<Boolean>.getBoolValue() =
         DeviceConfigHelper.prefs.getBoolean(
             this.key,
-            DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, this.key, this.valueInCode)
+            DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, this.key, this.valueInCode),
         )
 
     private fun DebugInfo<Int>.getIntValueAsString() =
@@ -265,7 +263,7 @@
         val pluginPermissionApps =
             pm.getPackagesHoldingPermissions(
                     arrayOf(PLUGIN_PERMISSION),
-                    PackageManager.MATCH_DISABLED_COMPONENTS
+                    PackageManager.MATCH_DISABLED_COMPONENTS,
                 )
                 .map { it.packageName }
 
@@ -274,7 +272,7 @@
                 pm.queryIntentServices(
                         Intent(action),
                         PackageManager.MATCH_DISABLED_COMPONENTS or
-                            PackageManager.GET_RESOLVED_FILTER
+                            PackageManager.GET_RESOLVED_FILTER,
                     )
                     .filter { pluginPermissionApps.contains(it.serviceInfo.packageName) }
             }
@@ -316,7 +314,7 @@
                             infoList.forEach {
                                 manager.pluginEnabler.setDisabled(
                                     it.serviceInfo.componentName,
-                                    disabledState
+                                    disabledState,
                                 )
                             }
                             manager.notifyChange(Intent(Intent.ACTION_PACKAGE_CHANGED, pluginUri))
@@ -387,12 +385,12 @@
             addOnboardPref(
                 "All Apps Bounce",
                 HOME_BOUNCE_SEEN.sharedPrefKey,
-                HOME_BOUNCE_COUNT.sharedPrefKey
+                HOME_BOUNCE_COUNT.sharedPrefKey,
             )
             addOnboardPref(
                 "Hybrid Hotseat Education",
                 HOTSEAT_DISCOVERY_TIP_COUNT.sharedPrefKey,
-                HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey
+                HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey,
             )
             addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey)
             addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey)
@@ -470,13 +468,16 @@
                         session.allowPublicAccess()
 
                         session.commit(ORDERED_BG_EXECUTOR) {
-                            val key = Base64.encodeToString(digest, NO_WRAP or NO_PADDING)
-                            Secure.putString(resolver, LAYOUT_DIGEST_KEY, key)
+                            Secure.putString(
+                                resolver,
+                                LAYOUT_PROVIDER_KEY,
+                                createBlobProviderKey(digest),
+                            )
 
                             MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
                             MAIN_EXECUTOR.submit { model.forceReload() }.get()
                             MODEL_EXECUTOR.submit {}.get()
-                            Secure.putString(resolver, LAYOUT_DIGEST_KEY, null)
+                            Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
                         }
                     }
                 }
@@ -512,7 +513,7 @@
                     info.providerName.className,
                     info.spanX,
                     info.spanY,
-                    userType
+                    userType,
                 )
         }
     }
@@ -520,7 +521,7 @@
     private fun createUriPickerIntent(
         action: String,
         executor: Executor,
-        callback: (uri: Uri) -> Unit
+        callback: (uri: Uri) -> Unit,
     ): Intent {
         val pendingIntent =
             PendingIntent(
@@ -532,7 +533,7 @@
                         allowlistToken: IBinder?,
                         finishedReceiver: IIntentReceiver?,
                         requiredPermission: String?,
-                        options: Bundle?
+                        options: Bundle?,
                     ) {
                         intent.data?.let { uri -> executor.execute { callback(uri) } }
                     }
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 1d4160d..2daaaf9 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -386,12 +386,12 @@
                 // and then write to StatsLog.
                 app.getModel().enqueueModelUpdateTask((taskController, dataModel, apps) ->
                         write(event, applyOverwrites(mItemInfo.buildProto(
-                                dataModel.collections.get(mItemInfo.container)))));
+                                dataModel.collections.get(mItemInfo.container), mContext))));
             })) {
                 // Write log on the model thread so that logs do not go out of order
                 // (for eg: drop comes after drag)
                 Executors.MODEL_EXECUTOR.execute(
-                        () -> write(event, applyOverwrites(mItemInfo.buildProto())));
+                        () -> write(event, applyOverwrites(mItemInfo.buildProto(mContext))));
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
index a94d023..2f0a6df 100644
--- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
+++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
@@ -62,7 +62,8 @@
          * Custom interpolator for both the home and wallpaper scaling. Necessary because EMPHASIZED
          * is too aggressive, but EMPHASIZED_DECELERATE is too soft.
          */
-        private val SCALE_INTERPOLATOR =
+        @JvmField
+        val SCALE_INTERPOLATOR =
             PathInterpolator(
                 Path().apply {
                     moveTo(0f, 0f)
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 87ac193..1d2d161 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -16,8 +16,12 @@
 
 package com.android.launcher3;
 
+import static android.util.Base64.NO_PADDING;
+import static android.util.Base64.NO_WRAP;
+
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.BaseColumns;
+import android.util.Base64;
 
 import androidx.annotation.NonNull;
 
@@ -354,8 +358,17 @@
      * Launcher settings
      */
     public static final class Settings {
-        public static final String LAYOUT_DIGEST_KEY = "launcher3.layout.provider.blob";
+        public static final String LAYOUT_PROVIDER_KEY = "launcher3.layout.provider";
         public static final String LAYOUT_DIGEST_LABEL = "launcher-layout";
         public static final String LAYOUT_DIGEST_TAG = "ignore";
+        public static final String BLOB_KEY_PREFIX = "blob://";
+
+        /**
+         * Creates a key to be used for {@link #LAYOUT_PROVIDER_KEY}
+         * @param digest byte[] representing the message digest for the blob handle
+         */
+        public static String createBlobProviderKey(byte[] digest) {
+            return BLOB_KEY_PREFIX + Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
+        }
     }
 }
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index d39c5de..088277b 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -21,6 +21,7 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.ScreenOnTracker;
+import com.android.launcher3.util.SettingsCache;
 
 import dagger.BindsInstance;
 
@@ -36,6 +37,7 @@
     DaggerSingletonTracker getDaggerSingletonTracker();
     InstallSessionHelper getInstallSessionHelper();
     ScreenOnTracker getScreenOnTracker();
+    SettingsCache getSettingsCache();
 
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index da1a221..5d66d16 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -25,7 +25,8 @@
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
-import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
+import static com.android.launcher3.LauncherSettings.Settings.BLOB_KEY_PREFIX;
+import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
@@ -548,9 +549,15 @@
     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
             LauncherWidgetHolder widgetHolder) {
         ContentResolver cr = mContext.getContentResolver();
-        String blobHandlerDigest = Settings.Secure.getString(cr, LAYOUT_DIGEST_KEY);
-        if (!TextUtils.isEmpty(blobHandlerDigest)) {
+        String systemLayoutProvider = Settings.Secure.getString(cr, LAYOUT_PROVIDER_KEY);
+        if (TextUtils.isEmpty(systemLayoutProvider)) {
+            return null;
+        }
+
+        // Try the blob store first
+        if (systemLayoutProvider.startsWith(BLOB_KEY_PREFIX)) {
             BlobStoreManager blobManager = mContext.getSystemService(BlobStoreManager.class);
+            String blobHandlerDigest = systemLayoutProvider.substring(BLOB_KEY_PREFIX.length());
             try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
                     blobManager.openBlob(BlobHandle.createWithSha256(
                             Base64.decode(blobHandlerDigest, NO_WRAP | NO_PADDING),
@@ -562,25 +569,21 @@
             }
         }
 
-        String authority = Settings.Secure.getString(cr, "launcher3.layout.provider");
-        if (TextUtils.isEmpty(authority)) {
-            return null;
-        }
-
+        // Try contentProvider based provider
         PackageManager pm = mContext.getPackageManager();
-        ProviderInfo pi = pm.resolveContentProvider(authority, 0);
+        ProviderInfo pi = pm.resolveContentProvider(systemLayoutProvider, 0);
         if (pi == null) {
-            Log.e(TAG, "No provider found for authority " + authority);
+            Log.e(TAG, "No provider found for authority " + systemLayoutProvider);
             return null;
         }
-        Uri uri = getLayoutUri(authority, mContext);
+        Uri uri = getLayoutUri(systemLayoutProvider, mContext);
         try (InputStream in = cr.openInputStream(uri)) {
-            Log.d(TAG, "Loading layout from " + authority);
+            Log.d(TAG, "Loading layout from " + systemLayoutProvider);
 
             Resources res = pm.getResourcesForApplication(pi.applicationInfo);
             return getAutoInstallsLayoutFromIS(in, widgetHolder, SourceResources.wrap(res));
         } catch (Exception e) {
-            Log.e(TAG, "Error getting layout stream from: " + authority , e);
+            Log.e(TAG, "Error getting layout stream from: " + systemLayoutProvider , e);
             return null;
         }
     }
diff --git a/src/com/android/launcher3/model/data/AppPairInfo.kt b/src/com/android/launcher3/model/data/AppPairInfo.kt
index 2eb6154..3496c17 100644
--- a/src/com/android/launcher3/model/data/AppPairInfo.kt
+++ b/src/com/android/launcher3/model/data/AppPairInfo.kt
@@ -74,7 +74,7 @@
             (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile().isTablet
         return Pair(
             isTablet || !getFirstApp().isNonResizeable(),
-            isTablet || !getSecondApp().isNonResizeable()
+            isTablet || !getSecondApp().isNonResizeable(),
         )
     }
 
@@ -105,10 +105,10 @@
     }
 
     /** Generates an ItemInfo for logging. */
-    override fun buildProto(cInfo: CollectionInfo?): LauncherAtom.ItemInfo {
+    override fun buildProto(cInfo: CollectionInfo?, context: Context): LauncherAtom.ItemInfo {
         val appPairIcon = LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size)
         appPairIcon.setLabelInfo(title.toString())
-        return getDefaultItemInfoBuilder()
+        return getDefaultItemInfoBuilder(context)
             .setFolderIcon(appPairIcon)
             .setRank(rank)
             .setContainerInfo(getContainerInfo())
diff --git a/src/com/android/launcher3/model/data/CollectionInfo.kt b/src/com/android/launcher3/model/data/CollectionInfo.kt
index 4f5e12f..12ba164 100644
--- a/src/com/android/launcher3/model/data/CollectionInfo.kt
+++ b/src/com/android/launcher3/model/data/CollectionInfo.kt
@@ -17,7 +17,6 @@
 package com.android.launcher3.model.data
 
 import com.android.launcher3.LauncherSettings
-import com.android.launcher3.logger.LauncherAtom
 import com.android.launcher3.util.ContentWriter
 import java.util.function.Predicate
 
@@ -42,9 +41,4 @@
         super.onAddToDatabase(writer)
         writer.put(LauncherSettings.Favorites.TITLE, title)
     }
-
-    /** Returns the collection wrapped as {@link LauncherAtom.ItemInfo} for logging. */
-    override fun buildProto(): LauncherAtom.ItemInfo {
-        return buildProto(null)
-    }
 }
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 18d2b85..f0f2892 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -24,6 +24,8 @@
 import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
 import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
 
+import android.content.Context;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -245,13 +247,13 @@
 
     @NonNull
     @Override
-    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo) {
+    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo, Context context) {
         FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
                 .setCardinality(getContents().size());
         if (LabelState.SUGGESTED.equals(getLabelState())) {
             folderIcon.setLabelInfo(title.toString());
         }
-        return getDefaultItemInfoBuilder()
+        return getDefaultItemInfoBuilder(context)
                 .setFolderIcon(folderIcon)
                 .setRank(rank)
                 .addItemAttributes(getLabelState().mLogAttribute)
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index b706d24..c22a8a5 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -36,6 +36,7 @@
 
 import android.content.ComponentName;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Process;
@@ -352,16 +353,16 @@
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
     @NonNull
-    public LauncherAtom.ItemInfo buildProto() {
-        return buildProto(null);
+    public LauncherAtom.ItemInfo buildProto(Context context) {
+        return buildProto(null, context);
     }
 
     /**
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
     @NonNull
-    public LauncherAtom.ItemInfo buildProto(@Nullable final CollectionInfo cInfo) {
-        LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder();
+    public LauncherAtom.ItemInfo buildProto(@Nullable final CollectionInfo cInfo, Context context) {
+        LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder(context);
         Optional<ComponentName> nullableComponent = Optional.ofNullable(getTargetComponent());
         switch (itemType) {
             case ITEM_TYPE_APPLICATION:
@@ -434,10 +435,10 @@
     }
 
     @NonNull
-    protected LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder() {
+    protected LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder(Context context) {
         LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
-        SettingsCache.INSTANCE.executeIfCreated(cache ->
-                itemBuilder.setIsKidsMode(cache.getValue(NAV_BAR_KIDS_MODE, 0)));
+        itemBuilder.setIsKidsMode(
+                SettingsCache.INSTANCE.get(context).getValue(NAV_BAR_KIDS_MODE, 0));
         UserCache.INSTANCE.executeIfCreated(cache ->
                 itemBuilder.setUserType(getUserType(cache.getUserInfo(user))));
         itemBuilder.setRank(rank);
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index 361f09d..7569ed5 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -24,6 +24,7 @@
 
 import android.appwidget.AppWidgetHostView;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.os.Process;
@@ -270,8 +271,9 @@
 
     @NonNull
     @Override
-    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) {
-        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo);
+    public LauncherAtom.ItemInfo buildProto(
+            @Nullable CollectionInfo collectionInfo, Context context) {
+        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo, context);
         return info.toBuilder()
                 .setWidget(info.getWidget().toBuilder().setWidgetFeatures(widgetFeatures))
                 .addItemAttributes(getAttribute(sourceContainer))
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index a7d5c13..9a70298 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -50,7 +50,7 @@
 
     public T get(Context context) {
         Context app = context.getApplicationContext();
-        if (app instanceof SandboxApplication sc) {
+        if (app instanceof ObjectSandbox sc) {
             return sc.getObject(this);
         }
 
@@ -100,7 +100,8 @@
         T get(Context context);
     }
 
-    public interface SandboxApplication {
+    /** Sandbox for isolating {@link MainThreadInitializedObject} instances from Launcher. */
+    public interface ObjectSandbox {
 
         /**
          * Find a cached object from mObjectMap if we have already created one. If not, generate
@@ -116,7 +117,7 @@
         <T extends SafeCloseable> void putObject(MainThreadInitializedObject<T> object, T value);
 
         /**
-         * Returns whether this context should cleanup all objects when its destroyed or leave it
+         * Returns whether this sandbox should cleanup all objects when its destroyed or leave it
          * to the GC.
          * These objects can have listeners attached to the system server and mey not be able to get
          * GCed themselves when running on a device.
@@ -137,7 +138,7 @@
      * Abstract Context which allows custom implementations for
      * {@link MainThreadInitializedObject} providers
      */
-    public static class SandboxContext extends LauncherApplication implements SandboxApplication {
+    public static class SandboxContext extends LauncherApplication implements ObjectSandbox {
 
         private static final String TAG = "SandboxContext";
 
@@ -159,8 +160,8 @@
 
         @Override
         public boolean shouldCleanUpOnDestroy() {
-            return (getBaseContext().getApplicationContext() instanceof SandboxApplication sa)
-                    ? sa.shouldCleanUpOnDestroy() : true;
+            return (getBaseContext().getApplicationContext() instanceof ObjectSandbox os)
+                    ? os.shouldCleanUpOnDestroy() : true;
         }
 
         public void onDestroy() {
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index cd6701d..a52f75b 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -25,14 +25,21 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.Looper;
 import android.provider.Settings;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import javax.inject.Inject;
+
 /**
  * ContentObserver over Settings keys that also has a caching layer.
  * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and
@@ -47,6 +54,7 @@
  *
  * Cache will also be updated if a key queried is missing (even if it has no listeners registered).
  */
+@LauncherAppSingleton
 public class SettingsCache extends ContentObserver implements SafeCloseable {
 
     /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
@@ -79,12 +87,14 @@
     /**
      * Singleton instance
      */
-    public static MainThreadInitializedObject<SettingsCache> INSTANCE =
-            new MainThreadInitializedObject<>(SettingsCache::new);
+    public static final DaggerSingletonObject<SettingsCache> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getSettingsCache);
 
-    private SettingsCache(Context context) {
-        super(new Handler());
+    @Inject
+    SettingsCache(@ApplicationContext final Context context, DaggerSingletonTracker tracker) {
+        super(new Handler(Looper.getMainLooper()));
         mResolver = context.getContentResolver();
+        ExecutorUtil.executeSyncOnMainOrFail(() -> tracker.addCloseable(this));
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index a916252..23ab0fb 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -82,8 +82,9 @@
 
     @NonNull
     @Override
-    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) {
-        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo);
+    public LauncherAtom.ItemInfo buildProto(
+            @Nullable CollectionInfo collectionInfo, Context context) {
+        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo, context);
         return info.toBuilder()
                 .addItemAttributes(LauncherAppWidgetInfo.getAttribute(sourceContainer))
                 .build();
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
index 748d376..09b9a3b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -31,6 +31,7 @@
 
 import android.content.ContentProvider;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
@@ -250,15 +251,16 @@
         private final File mDbDir;
 
         public SandboxModelContext() {
-            super(ApplicationProvider.getApplicationContext());
+            this(ApplicationProvider.getApplicationContext());
+        }
+
+        public SandboxModelContext(Context context) {
+            super(context);
 
             // System settings cache content provider. Ensure that they are statically initialized
-            Settings.Secure.getString(
-                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
-            Settings.System.getString(
-                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
-            Settings.Global.getString(
-                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
+            Settings.Secure.getString(context.getContentResolver(), "test");
+            Settings.System.getString(context.getContentResolver(), "test");
+            Settings.Global.getString(context.getContentResolver(), "test");
 
             mPm = spy(getBaseContext().getPackageManager());
             mDbDir = new File(getCacheDir(), UUID.randomUUID().toString());
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
new file mode 100644
index 0000000..4f9b8c7
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.Context
+import android.content.ContextParams
+import android.content.ContextWrapper
+import android.content.pm.ApplicationInfo
+import android.content.res.Configuration
+import android.os.Bundle
+import android.os.IBinder
+import android.os.UserHandle
+import android.view.Display
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
+import org.junit.Rule
+import org.junit.rules.ExternalResource
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Sandbox application where created [Context] instances are still sandboxed within it.
+ *
+ * Tests can declare this application as a [Rule], so that it is set up and destroyed automatically.
+ * Alternatively, they can call [init] and [onDestroy] directly. Either way, these need to be called
+ * for it to work and avoid leaks from created singletons.
+ *
+ * The create [Context] APIs construct a `ContextImpl`, which resets the application to the true
+ * application, thus leaving the sandbox. This implementation wraps the created contexts to
+ * propagate this application (see [SandboxApplicationWrapper]).
+ */
+class SandboxApplication private constructor(private val base: SandboxApplicationWrapper) :
+    SandboxModelContext(base), TestRule {
+
+    constructor(
+        base: Context = ApplicationProvider.getApplicationContext()
+    ) : this(SandboxApplicationWrapper(base))
+
+    /**
+     * Initializes the sandbox application propagation logic.
+     *
+     * This function either needs to be called manually or automatically through using [Rule].
+     */
+    fun init() {
+        base.app = this@SandboxApplication
+    }
+
+    /** Returns `this` if [init] was called, otherwise crashes the test. */
+    override fun getApplicationContext(): Context = base.applicationContext
+
+    override fun shouldCleanUpOnDestroy(): Boolean {
+        // Defer to the true application to decide whether to clean up. For instance, we do not want
+        // to cleanup under Robolectric.
+        val app = ApplicationProvider.getApplicationContext<Context>()
+        return if (app is ObjectSandbox) app.shouldCleanUpOnDestroy() else true
+    }
+
+    override fun apply(statement: Statement, description: Description): Statement {
+        return object : ExternalResource() {
+                override fun before() {
+                    base.app = this@SandboxApplication
+                }
+
+                override fun after() = onDestroy()
+            }
+            .apply(statement, description)
+    }
+}
+
+private class SandboxApplicationWrapper(base: Context, var app: Context? = null) :
+    ContextWrapper(base) {
+
+    override fun getApplicationContext(): Context {
+        return checkNotNull(app) { "SandboxApplication accessed before #init() was called." }
+    }
+
+    override fun createPackageContext(packageName: String?, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createPackageContext(packageName, flags), app)
+    }
+
+    override fun createPackageContextAsUser(
+        packageName: String,
+        flags: Int,
+        user: UserHandle,
+    ): Context {
+        return SandboxApplicationWrapper(
+            super.createPackageContextAsUser(packageName, flags, user),
+            app,
+        )
+    }
+
+    override fun createContextAsUser(user: UserHandle, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createContextAsUser(user, flags), app)
+    }
+
+    override fun createApplicationContext(application: ApplicationInfo?, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createApplicationContext(application, flags), app)
+    }
+
+    override fun createContextForSdkInSandbox(sdkInfo: ApplicationInfo, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createContextForSdkInSandbox(sdkInfo, flags), app)
+    }
+
+    override fun createContextForSplit(splitName: String?): Context {
+        return SandboxApplicationWrapper(super.createContextForSplit(splitName), app)
+    }
+
+    override fun createConfigurationContext(overrideConfiguration: Configuration): Context {
+        return SandboxApplicationWrapper(
+            super.createConfigurationContext(overrideConfiguration),
+            app,
+        )
+    }
+
+    override fun createDisplayContext(display: Display): Context {
+        return SandboxApplicationWrapper(super.createDisplayContext(display), app)
+    }
+
+    override fun createDeviceContext(deviceId: Int): Context {
+        return SandboxApplicationWrapper(super.createDeviceContext(deviceId), app)
+    }
+
+    override fun createWindowContext(type: Int, options: Bundle?): Context {
+        return SandboxApplicationWrapper(super.createWindowContext(type, options), app)
+    }
+
+    override fun createWindowContext(display: Display, type: Int, options: Bundle?): Context {
+        return SandboxApplicationWrapper(super.createWindowContext(display, type, options), app)
+    }
+
+    override fun createContext(contextParams: ContextParams): Context {
+        return SandboxApplicationWrapper(super.createContext(contextParams), app)
+    }
+
+    override fun createAttributionContext(attributionTag: String?): Context {
+        return SandboxApplicationWrapper(super.createAttributionContext(attributionTag), app)
+    }
+
+    override fun createCredentialProtectedStorageContext(): Context {
+        return SandboxApplicationWrapper(super.createCredentialProtectedStorageContext(), app)
+    }
+
+    override fun createDeviceProtectedStorageContext(): Context {
+        return SandboxApplicationWrapper(super.createDeviceProtectedStorageContext(), app)
+    }
+
+    override fun createTokenContext(token: IBinder, display: Display): Context {
+        return SandboxApplicationWrapper(super.createTokenContext(token, display), app)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
new file mode 100644
index 0000000..d87a406
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class SandboxApplicationTest {
+    @get:Rule val app = SandboxApplication()
+
+    private val display: Display
+        get() {
+            return checkNotNull(app.getSystemService(DisplayManager::class.java))
+                .getDisplay(DEFAULT_DISPLAY)
+        }
+
+    @Test
+    fun testCreateDisplayContext_isSandboxed() {
+        val displayContext = app.createDisplayContext(display)
+        assertThat(displayContext.applicationContext).isEqualTo(app)
+    }
+
+    @Test
+    fun testCreateWindowContext_fromSandboxedDisplayContext_isSandboxed() {
+        val displayContext = app.createDisplayContext(display)
+        val nestedContext = displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+        assertThat(nestedContext.applicationContext).isEqualTo(app)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun testGetApplicationContext_beforeManualInit_throwsException() {
+        val manualApp = SandboxApplication()
+        assertThat(manualApp.applicationContext).isEqualTo(manualApp)
+    }
+
+    @Test
+    fun testGetApplicationContext_afterManualInit_isApplication() {
+        SandboxApplication().run {
+            init()
+            assertThat(applicationContext).isEqualTo(this)
+            onDestroy()
+        }
+    }
+
+    @Test
+    fun testGetObject_objectCreatesDisplayContext_isSandboxed() {
+        class TestSingleton(context: Context) : SafeCloseable {
+            override fun close() = Unit
+
+            val displayContext = context.createDisplayContext(display)
+        }
+
+        val displayContext = MainThreadInitializedObject { TestSingleton(it) }[app].displayContext
+        assertThat(displayContext.applicationContext).isEqualTo(app)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
index 64035da..ce682f1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
@@ -15,14 +15,12 @@
  */
 package com.android.launcher3.util;
 
-import static android.util.Base64.NO_PADDING;
-import static android.util.Base64.NO_WRAP;
-
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
+import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY;
+import static com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey;
 
 import static org.junit.Assert.assertTrue;
 
@@ -42,7 +40,6 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.system.OsConstants;
-import android.util.Base64;
 import android.util.Log;
 
 import androidx.test.uiautomator.UiDevice;
@@ -169,13 +166,12 @@
             session.commit(AsyncTask.THREAD_POOL_EXECUTOR, i -> wait.countDown());
         }
 
-        String key = Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
-
         grantWriteSecurePermission();
-        Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, key);
+        Settings.Secure.putString(
+                context.getContentResolver(), LAYOUT_PROVIDER_KEY, createBlobProviderKey(digest));
         wait.await();
         return () ->
-            Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, null);
+            Settings.Secure.putString(context.getContentResolver(), LAYOUT_PROVIDER_KEY, null);
     }
 
     /**