Merge "Send pointer up input even if there is an exception" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index f5afd88..3166d82 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -71,13 +71,6 @@
 }
 
 flag {
-    name: "enable_launcher_br_metrics"
-    namespace: "launcher"
-    description: "Enables logging of Launcher restore metrics to the Backup & Restore team"
-    bug: "307527314"
-}
-
-flag {
     name: "enable_unfolded_two_pane_picker"
     namespace: "launcher"
     description: "Enables two pane widget picker for unfolded foldables"
@@ -104,3 +97,11 @@
     description: "Enables long-press shortcut to install a copy of an app to Private space"
     bug: "316118005"
 }
+
+flag {
+    name: "enable_launcher_br_metrics_fixed"
+    namespace: "launcher"
+    description: "Enables logging of Launcher restore metrics to the Backup & Restore team"
+    bug: "307527314"
+    is_fixed_read_only: true
+}
diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
index 7209bd8..8dded8f 100644
--- a/quickstep/src/com/android/launcher3/model/AppEventProducer.java
+++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
@@ -191,7 +191,7 @@
     @Nullable
     AppTarget toAppTarget(LauncherAtom.ItemInfo info) {
         int iconInfoType = getIconInfoTypeFromItemInfo(info);
-        UserCache userCache = UserCache.getInstance(mContext);
+        UserCache userCache = UserCache.INSTANCE.get(mContext);
         UserHandle userHandle = userCache.getUserProfiles().stream()
                 .filter(user -> userCache.getUserInfo(user).type == iconInfoType)
                 .findFirst()
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index dab9950..12f1e63 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -181,7 +181,7 @@
                     measuredWidth.toFloat(),
                     measuredHeight.toFloat(),
                     (measuredWidth - arrowWidth) / 2, // arrowOffsetX
-                    0f, // arrowOffsetY
+                    -mArrowOffsetVertical.toFloat(), // arrowOffsetY
                     false, // isPointingUp
                     true, // leftAligned
                     context.getColor(R.color.popup_shade_first),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index f9fc983..491938d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -169,6 +169,7 @@
         mBackgroundRenderer.setBackgroundProgress(mTaskbarBackgroundProgress);
         mBackgroundRenderer.draw(canvas);
         super.dispatchDraw(canvas);
+        mControllerCallbacks.drawDebugUi(canvas);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 3823c5a..3f5402f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -19,8 +19,10 @@
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
 
 import android.content.res.Resources;
+import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.SystemProperties;
 import android.view.ViewTreeObserver;
 
 import com.android.launcher3.DeviceProfile;
@@ -39,6 +41,9 @@
 public class TaskbarDragLayerController implements TaskbarControllers.LoggableTaskbarController,
         TaskbarControllers.BackgroundRendererController {
 
+    private static final boolean DEBUG = SystemProperties.getBoolean(
+            "persist.debug.draw_taskbar_debug_ui", false);
+
     private final TaskbarActivityContext mActivity;
     private final TaskbarDragLayer mTaskbarDragLayer;
     private final int mFolderMargin;
@@ -299,5 +304,15 @@
                     mTaskbarStashViaTouchController,
             };
         }
+
+        /**
+         * Draws debug UI on top of everything in TaskbarDragLayer.
+         */
+        public void drawDebugUi(Canvas canvas) {
+            if (!DEBUG) {
+                return;
+            }
+            mControllers.taskbarInsetsController.drawDebugTouchableRegionBounds(canvas);
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 7ebc18d..633383d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -15,7 +15,11 @@
  */
 package com.android.launcher3.taskbar
 
+import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Insets
+import android.graphics.Paint
+import android.graphics.Rect
 import android.graphics.Region
 import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
 import android.os.Binder
@@ -47,6 +51,7 @@
 import com.android.launcher3.util.DisplayController
 import java.io.PrintWriter
 import kotlin.jvm.optionals.getOrNull
+import kotlin.math.max
 
 /** Handles the insets that Taskbar provides to underlying apps and the IME. */
 class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController {
@@ -58,7 +63,8 @@
 
     /** The bottom insets taskbar provides to the IME when IME is visible. */
     val taskbarHeightForIme: Int = context.resources.getDimensionPixelSize(R.dimen.taskbar_ime_size)
-    private val touchableRegion: Region = Region()
+    // The touchableRegion we will set unless some other state takes precedence.
+    private val defaultTouchableRegion: Region = Region()
     private val insetsOwner: IBinder = Binder()
     private val deviceProfileChangeListener = { _: DeviceProfile ->
         onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -69,6 +75,7 @@
             context,
             this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
         )
+    private val debugTouchableRegion = DebugTouchableRegion()
 
     // Initialized in init.
     private lateinit var controllers: TaskbarControllers
@@ -100,9 +107,9 @@
             }
 
         windowLayoutParams.providedInsets =
-            if (enableTaskbarNoRecreate()) {
+            if (enableTaskbarNoRecreate() && controllers.sharedState != null) {
                 getProvidedInsets(
-                    controllers.sharedState!!.insetsFrameProviders!!,
+                    controllers.sharedState!!.insetsFrameProviders,
                     insetsRoundedCornerFlag
                 )
             } else {
@@ -124,7 +131,7 @@
             } else {
                 0
             }
-        val touchableHeight = Math.max(taskbarTouchableHeight, bubblesTouchableHeight)
+        val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight)
 
         if (
             controllers.bubbleControllers.isPresent &&
@@ -132,14 +139,14 @@
         ) {
             val iconBounds =
                 controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
-            touchableRegion.set(
+            defaultTouchableRegion.set(
                 iconBounds.left,
                 iconBounds.top,
                 iconBounds.right,
                 iconBounds.bottom
             )
         } else {
-            touchableRegion.set(
+            defaultTouchableRegion.set(
                 0,
                 windowLayoutParams.height - touchableHeight,
                 context.deviceProfile.widthPx,
@@ -296,6 +303,8 @@
             context.dragLayer,
             insetsInfo.touchableRegion
         )
+        debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
+
         val bubbleBarVisible =
             controllers.bubbleControllers.isPresent &&
                 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
@@ -303,21 +312,28 @@
         if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "Taskbar is invisible"
         } else if (
             controllers.navbarButtonsViewController.isImeVisible &&
                 controllers.taskbarStashController.isStashed
         ) {
+            // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "Stashed over IME"
         } else if (!controllers.uiController.isTaskbarTouchable) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "Taskbar is not touchable"
         } else if (controllers.taskbarDragController.isSystemDragInProgress) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "System drag is in progress"
         } else if (context.isTaskbarWindowFullscreen) {
             // Intercept entire fullscreen window.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
             insetsIsTouchableRegion = false
+            debugTouchableRegion.lastSetTouchableReason = "Taskbar is fullscreen"
+            context.dragLayer.getBoundsInWindow(debugTouchableRegion.lastSetTouchableBounds, false)
         } else if (
             controllers.taskbarViewController.areIconsVisible() ||
                 context.isNavBarKidsModeActive ||
@@ -346,19 +362,33 @@
                     region.op(bubbleBarBounds, Region.Op.UNION)
                 }
                 insetsInfo.touchableRegion.set(region)
+                debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview"
+                debugTouchableRegion.lastSetTouchableBounds.set(region.bounds)
             } else {
-                insetsInfo.touchableRegion.set(touchableRegion)
+                insetsInfo.touchableRegion.set(defaultTouchableRegion)
+                debugTouchableRegion.lastSetTouchableReason = "Using default touchable region"
+                debugTouchableRegion.lastSetTouchableBounds.set(defaultTouchableRegion.bounds)
             }
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
             insetsIsTouchableRegion = false
         } else {
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason =
+                "Icons are not visible, but other components such as 3 buttons might be"
         }
         context.excludeFromMagnificationRegion(insetsIsTouchableRegion)
     }
 
+    /** Draws the last set touchableRegion as a red rectangle onto the given Canvas. */
+    fun drawDebugTouchableRegionBounds(canvas: Canvas) {
+        val paint = Paint()
+        paint.color = Color.RED
+        paint.style = Paint.Style.STROKE
+        canvas.drawRect(debugTouchableRegion.lastSetTouchableBounds, paint)
+    }
+
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
-        pw.println(prefix + "TaskbarInsetsController:")
+        pw.println("${prefix}TaskbarInsetsController:")
         pw.println("$prefix\twindowHeight=${windowLayoutParams.height}")
         for (provider in windowLayoutParams.providedInsets) {
             pw.print(
@@ -377,5 +407,12 @@
             }
             pw.println()
         }
+        pw.println("$prefix\tlastSetTouchableBounds=${debugTouchableRegion.lastSetTouchableBounds}")
+        pw.println("$prefix\tlastSetTouchableReason=${debugTouchableRegion.lastSetTouchableReason}")
+    }
+
+    class DebugTouchableRegion {
+        val lastSetTouchableBounds = Rect()
+        var lastSetTouchableReason = ""
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index 5b117fc..2f2d636 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -25,6 +25,8 @@
 import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_PINNED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_UNPINNED
 import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAndPopulate
 import java.io.PrintWriter
 
@@ -54,8 +56,10 @@
                 }
                 val animateToValue =
                     if (!launcherPrefs.get(TASKBAR_PINNING)) {
+                        statsLogManager.logger().log(LAUNCHER_TASKBAR_PINNED)
                         PINNING_PERSISTENT
                     } else {
+                        statsLogManager.logger().log(LAUNCHER_TASKBAR_UNPINNED)
                         PINNING_TRANSIENT
                     }
                 taskbarSharedState.taskbarWasPinned = animateToValue == PINNING_TRANSIENT
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index 7113e49..3ebc8ed 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -80,6 +80,9 @@
                 if (android.os.Flags.allowPrivateProfile() && Flags.enablePrivateSpace()) {
                     LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
                     LauncherUserInfo launcherUserInfo = launcherApps.getLauncherUserInfo(user);
+                    if (launcherUserInfo == null) {
+                        continue;
+                    }
                     // UserTypes not supported in Launcher are deemed to be the current
                     // Foreground User.
                     int userType = switch (launcherUserInfo.getUserType()) {
diff --git a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
index 0913fca..27bd03d 100644
--- a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
+++ b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
@@ -5,10 +5,9 @@
 import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType
 import android.app.backup.BackupRestoreEventLogger.BackupRestoreError
 import android.content.Context
-import com.android.launcher3.Flags.enableLauncherBrMetrics
+import com.android.launcher3.Flags.enableLauncherBrMetricsFixed
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
-import com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_BR_METRICS
 
 /**
  * Concrete implementation for wrapper to log Restore event metrics for both success and failure to
@@ -45,7 +44,7 @@
         count: Int,
         @BackupRestoreError error: String?
     ) {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger.logItemsRestoreFailed(dataType, count, error)
         }
     }
@@ -57,7 +56,7 @@
      * @param count the number of data items restored.
      */
     override fun logLauncherItemsRestored(@BackupRestoreDataType dataType: String, count: Int) {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger.logItemsRestored(dataType, count)
         }
     }
@@ -68,7 +67,7 @@
      * @param favoritesId The id of the item type from [Favorites] that was restored.
      */
     override fun logSingleFavoritesItemRestored(favoritesId: Int) {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger.logItemsRestored(favoritesIdToDataType(favoritesId), 1)
         }
     }
@@ -80,7 +79,7 @@
      * @param count number of items that restored.
      */
     override fun logFavoritesItemsRestored(favoritesId: Int, count: Int) {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger.logItemsRestored(favoritesIdToDataType(favoritesId), count)
         }
     }
@@ -95,7 +94,7 @@
         favoritesId: Int,
         @BackupRestoreError error: String?
     ) {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger.logItemsRestoreFailed(favoritesIdToDataType(favoritesId), 1, error)
         }
     }
@@ -112,7 +111,7 @@
         count: Int,
         @BackupRestoreError error: String?
     ) {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger.logItemsRestoreFailed(
                 favoritesIdToDataType(favoritesId),
                 count,
@@ -126,7 +125,7 @@
      * done restoring items for Launcher.
      */
     override fun reportLauncherRestoreResults() {
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             BackupManager(context).reportDelayedRestoreResult(restoreEventLogger)
         }
     }
diff --git a/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java
index d6c1447..d4dd580 100644
--- a/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java
@@ -28,7 +28,6 @@
 
 import android.app.prediction.AppTarget;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
 import android.os.Process;
 import android.os.UserHandle;
@@ -39,13 +38,12 @@
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ActivityContextWrapper;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.launcher3.util.UserIconInfo;
-import com.android.launcher3.util.rule.StaticMockitoRule;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -65,22 +63,24 @@
     private static final UserIconInfo PRIVATE_ICON_INFO =
             new UserIconInfo(PRIVATE_HANDLE, UserIconInfo.TYPE_PRIVATE);
 
-    private Context mContext;
+    private SandboxContext mContext;
     private AppEventProducer mAppEventProducer;
     @Mock
     private UserCache mUserCache;
 
-    @Rule
-    public final StaticMockitoRule mStaticMockitoRule = new StaticMockitoRule(UserCache.class);
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = new ActivityContextWrapper(getApplicationContext());
-        when(UserCache.getInstance(any(Context.class))).thenReturn(mUserCache);
+        mContext = new SandboxContext(getApplicationContext());
+        mContext.putObject(UserCache.INSTANCE, mUserCache);
         mAppEventProducer = new AppEventProducer(mContext, null);
     }
 
+    @After
+    public void tearDown() {
+        mContext.onDestroy();
+    }
+
     @Test
     public void buildAppTarget_containsCorrectUser() {
         when(mUserCache.getUserProfiles())
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
index c9e536a..b165876 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
@@ -17,16 +17,23 @@
 
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 
+import android.graphics.Rect;
+
 import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.Closeable;
+import java.io.IOException;
+
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsPersistentTaskbar extends AbstractTaplTestsTaskbar {
@@ -39,4 +46,28 @@
         // Width check is performed inside TAPL whenever getTaskbar() is called.
         getTaskbar();
     }
+
+    @Test
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
+    public void testThreeButtonsTaskbarBoundsAfterConfigChangeDuringIme() {
+        // Start off in light mode.
+        try (Closeable c = InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .executeShellCommand("cmd uimode night no")) {
+            Rect taskbarBoundsBefore = getTaskbar().getVisibleBounds();
+            startImeTestActivity();
+            // IME should stash the taskbar, which hides icons even in 3 button mode.
+            mLauncher.getLaunchedAppState().assertTaskbarHidden();
+            // Switch to dark mode (any configuration change here would do).
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "cmd uimode night yes").close();
+            // Close IME to check new taskbar bounds.
+            mLauncher.pressBack();
+            Rect taskbarBoundsAfter = getTaskbar().getVisibleBounds();
+            Assert.assertEquals(
+                    "Taskbar bounds are not the same after a configuration change while stashed.",
+                    taskbarBoundsBefore, taskbarBoundsAfter);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index d4bcd24..e015021 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIconProvider;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.ModelLauncherCallbacks;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.InstallSessionTracker;
@@ -101,9 +102,10 @@
             }
         });
 
-        mContext.getSystemService(LauncherApps.class).registerCallback(mModel);
+        ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
+        mContext.getSystemService(LauncherApps.class).registerCallback(callbacks);
         mOnTerminateCallback.add(() ->
-                mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel));
+                mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
 
         SimpleBroadcastReceiver modelChangeReceiver =
                 new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index c81db63..d124746 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,6 +20,7 @@
 
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
+import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE;
 import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE;
 import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE;
 import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
@@ -28,7 +29,6 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
@@ -43,7 +43,6 @@
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.AddWorkspaceItemsTask;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BaseModelUpdateTask;
@@ -55,8 +54,8 @@
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.model.ModelDelegate;
+import com.android.launcher3.model.ModelLauncherCallbacks;
 import com.android.launcher3.model.ModelWriter;
-import com.android.launcher3.model.PackageIncrementalDownloadUpdatedTask;
 import com.android.launcher3.model.PackageInstallStateChangedTask;
 import com.android.launcher3.model.PackageUpdatedTask;
 import com.android.launcher3.model.ReloadStringCacheTask;
@@ -89,7 +88,7 @@
  * LauncherModel object held in a static. Also provide APIs for updating the database state
  * for the Launcher.
  */
-public class LauncherModel extends LauncherApps.Callback implements InstallSessionTracker.Callback {
+public class LauncherModel implements InstallSessionTracker.Callback {
     private static final boolean DEBUG_RECEIVER = false;
 
     static final String TAG = "Launcher.Model";
@@ -168,6 +167,10 @@
         return mModelDbController;
     }
 
+    public ModelLauncherCallbacks newModelCallbacks() {
+        return new ModelLauncherCallbacks(this::enqueueModelUpdateTask);
+    }
+
     /**
      * Adds the provided items to the workspace.
      */
@@ -186,77 +189,6 @@
                 owner);
     }
 
-    @Override
-    public void onPackageChanged(
-            @NonNull final String packageName, @NonNull final UserHandle user) {
-        int op = PackageUpdatedTask.OP_UPDATE;
-        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
-    }
-
-    @Override
-    public void onPackageRemoved(
-            @NonNull final String packageName, @NonNull final UserHandle user) {
-        onPackagesRemoved(user, packageName);
-    }
-
-    public void onPackagesRemoved(
-            @NonNull final UserHandle user, @NonNull final String... packages) {
-        int op = PackageUpdatedTask.OP_REMOVE;
-        FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages));
-        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages));
-    }
-
-    @Override
-    public void onPackageAdded(@NonNull final String packageName, @NonNull final UserHandle user) {
-        int op = PackageUpdatedTask.OP_ADD;
-        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
-    }
-
-    @Override
-    public void onPackagesAvailable(@NonNull final String[] packageNames,
-            @NonNull final UserHandle user, final boolean replacing) {
-        enqueueModelUpdateTask(
-                new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageNames));
-    }
-
-    @Override
-    public void onPackagesUnavailable(@NonNull final String[] packageNames,
-            @NonNull final UserHandle user, final boolean replacing) {
-        if (!replacing) {
-            enqueueModelUpdateTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_UNAVAILABLE, user, packageNames));
-        }
-    }
-
-    @Override
-    public void onPackagesSuspended(
-            @NonNull final String[] packageNames, @NonNull final UserHandle user) {
-        enqueueModelUpdateTask(new PackageUpdatedTask(
-                PackageUpdatedTask.OP_SUSPEND, user, packageNames));
-    }
-
-    @Override
-    public void onPackagesUnsuspended(
-            @NonNull final String[] packageNames, @NonNull final UserHandle user) {
-        enqueueModelUpdateTask(new PackageUpdatedTask(
-                PackageUpdatedTask.OP_UNSUSPEND, user, packageNames));
-    }
-
-    @Override
-    public void onPackageLoadingProgressChanged(@NonNull final String packageName,
-            @NonNull final UserHandle user, final float progress) {
-        if (Utilities.ATLEAST_S) {
-            enqueueModelUpdateTask(new PackageIncrementalDownloadUpdatedTask(
-                    packageName, user, progress));
-        }
-    }
-
-    @Override
-    public void onShortcutsChanged(@NonNull final String packageName,
-            @NonNull final List<ShortcutInfo> shortcuts, @NonNull final UserHandle user) {
-        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
-    }
-
     /**
      * Called when the icon for an app changes, outside of package event
      */
@@ -265,7 +197,7 @@
             @NonNull final UserHandle user) {
         // Update the icon for the calendar package
         Context context = mApp.getContext();
-        onPackageChanged(packageName, user);
+        enqueueModelUpdateTask(new PackageUpdatedTask(OP_UPDATE, user, packageName));
 
         List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
                 .forPackage(packageName).query(ShortcutRequest.PINNED);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 61e853e..3ccd3e1 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -167,8 +167,6 @@
             "Enable the ability to generate monochromatic icons, if it is not provided by the app");
 
     // TODO(Block 8): Clean up flags
-    public static final BooleanFlag ENABLE_LAUNCHER_BR_METRICS = getDebugFlag(305984208,
-            "ENABLE_LAUNCHER_BR_METRICS", TEAMFOOD, "Enable metrics for Launcher restore");
 
     // TODO(Block 9): Clean up flags
     public static final BooleanFlag MULTI_SELECT_EDIT_MODE = getDebugFlag(270709220,
@@ -225,7 +223,7 @@
                 // Task bar pinning and task bar nav bar unification are both dependent on
                 // ENABLE_TASKBAR_NO_RECREATION. We want to turn ENABLE_TASKBAR_NO_RECREATION on
                 // when either of the dependent features is turned on.
-                || ENABLE_TASKBAR_PINNING.get() || ENABLE_TASKBAR_NAVBAR_UNIFICATION;
+                || enableTaskbarPinning() || ENABLE_TASKBAR_NAVBAR_UNIFICATION;
     }
 
     // TODO(Block 16): Clean up flags
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 7b9f6fa..795450a 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -17,18 +17,11 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
-import static com.android.launcher3.Flags.enableSupportForArchiving;
-import static com.android.launcher3.Flags.enableLauncherBrMetrics;
+import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE;
 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.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED;
-import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_INVALID_LOCATION;
-import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO;
-import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_PROFILE_DELETED;
-import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_SHORTCUT_NOT_FOUND;
-import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_WIDGETS_DISABLED;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL;
 import static com.android.launcher3.config.FeatureFlags.SMARTSPACE_AS_A_WIDGET;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
@@ -37,16 +30,10 @@
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
 
-import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -56,12 +43,10 @@
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
-import android.graphics.Point;
 import android.os.Bundle;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
@@ -69,10 +54,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherPrefs;
@@ -95,13 +79,11 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
@@ -113,9 +95,7 @@
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -123,6 +103,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 
@@ -185,7 +166,7 @@
         mLauncherBinder = launcherBinder;
         mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
         mUserManager = mApp.getContext().getSystemService(UserManager.class);
-        mUserCache = UserCache.getInstance(mApp.getContext());
+        mUserCache = UserCache.INSTANCE.get(mApp.getContext());
         mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
         mIconCache = mApp.getIconCache();
         mUserManagerState = userManagerState;
@@ -234,7 +215,7 @@
         mIsRestoreFromBackup =
                 (Boolean) LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
         LauncherRestoreEventLogger restoreEventLogger = null;
-        if (enableLauncherBrMetrics()) {
+        if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger = LauncherRestoreEventLogger.Companion
                     .newInstance(mApp.getContext());
         }
@@ -247,7 +228,7 @@
             // sanitizeData should not be invoked if the workspace is loaded from a db different
             // from the main db as defined in the invariant device profile.
             // (e.g. both grid preview and minimal device mode uses a different db)
-            if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
+            if (Objects.equals(mApp.getInvariantDeviceProfile().dbFile, mDbName)) {
                 verifyNotStopped();
                 sanitizeFolders(mItemsDeleted);
                 sanitizeWidgetsShortcutsAndPackages();
@@ -452,43 +433,19 @@
             mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME);
             try {
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
-
-                mUserManagerState.init(mUserCache, mUserManager);
-
-                for (UserHandle user : mUserCache.getUserProfiles()) {
-                    long serialNo = mUserCache.getSerialNumberForUser(user);
-                    boolean userUnlocked = mUserManager.isUserUnlocked(user);
-
-                    // We can only query for shortcuts when the user is unlocked.
-                    if (userUnlocked) {
-                        QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
-                                .query(ShortcutRequest.PINNED);
-                        if (pinnedShortcuts.wasSuccess()) {
-                            for (ShortcutInfo shortcut : pinnedShortcuts) {
-                                mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
-                                        shortcut);
-                            }
-                            if (pinnedShortcuts.isEmpty()) {
-                                FileLog.d(TAG, "No pinned shortcuts found for user " + user);
-                            }
-                        } else {
-                            // Shortcut manager can fail due to some race condition when the
-                            // lock state changes too frequently. For the purpose of the loading
-                            // shortcuts, consider the user is still locked.
-                            FileLog.d(TAG, "Shortcut request failed for user "
-                                    + user + ", user may still be locked.");
-                            userUnlocked = false;
-                        }
-                    }
-                    unlockedUsers.put(serialNo, userUnlocked);
-                }
+                queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers);
 
                 List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos = new ArrayList<>();
 
+                WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger,
+                        restoreEventLogger, mUserManagerState, mLauncherApps, mPendingPackages,
+                        mShortcutKeyToPinnedShortcuts, mApp, mIconCache, mBgDataModel,
+                        mWidgetProvidersMap, mIsRestoreFromBackup, installingPkgs, isSdCardReady,
+                        tempPackageKey, widgetHelper, pmHelper, iconRequestInfos, unlockedUsers,
+                        isSafeMode, allDeepShortcuts);
+
                 while (!mStopped && c.moveToNext()) {
-                    processWorkspaceItem(c, memoryLogger, restoreEventLogger, installingPkgs,
-                            isSdCardReady, tempPackageKey, widgetHelper, pmHelper,
-                            iconRequestInfos, unlockedUsers, isSafeMode, allDeepShortcuts);
+                    itemProcessor.processItem();
                 }
                 tryLoadWorkspaceIconsInBulk(iconRequestInfos);
             } finally {
@@ -513,502 +470,85 @@
             // Remove dead items
             mItemsDeleted = c.commitDeleted();
 
-            // Sort the folder items, update ranks, and make sure all preview items are high res.
-            List<FolderGridOrganizer> verifiers =
-                    mApp.getInvariantDeviceProfile().supportedProfiles.stream().map(
-                            FolderGridOrganizer::new).toList();
-            for (FolderInfo folder : mBgDataModel.folders) {
-                Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
-                verifiers.forEach(verifier -> verifier.setFolderInfo(folder));
-                int size = folder.contents.size();
-
-                // Update ranks here to ensure there are no gaps caused by removed folder items.
-                // Ranks are the source of truth for folder items, so cellX and cellY can be
-                // ignored for now. Database will be updated once user manually modifies folder.
-                for (int rank = 0; rank < size; ++rank) {
-                    WorkspaceItemInfo info = folder.contents.get(rank);
-                    // rank is used differently in app pairs, so don't reset
-                    if (folder.itemType != ITEM_TYPE_APP_PAIR) {
-                        info.rank = rank;
-                    }
-
-                    if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION
-                            && verifiers.stream().anyMatch(
-                                verifier -> verifier.isItemInPreview(info.rank))) {
-                        mIconCache.getTitleAndIcon(info, false);
-                    }
-                }
-            }
+            processFolderItems();
 
             c.commitRestoredItems();
         }
     }
 
-    private void processWorkspaceItem(LoaderCursor c,
-            LoaderMemoryLogger memoryLogger,
-            @Nullable LauncherRestoreEventLogger restoreEventLogger,
-            HashMap<PackageUserKey, SessionInfo> installingPkgs,
-            boolean isSdCardReady,
-            PackageUserKey tempPackageKey,
-            WidgetManagerHelper widgetHelper,
-            PackageManagerHelper pmHelper,
-            List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos,
-            LongSparseArray<Boolean> unlockedUsers,
-            boolean isSafeMode,
-            List<ShortcutInfo> allDeepShortcuts) {
+    /**
+     * Initialized the UserManagerState, and determines which users are unlocked. Additionally, if
+     * the user is unlocked, it queries LauncherAppsService for pinned shortcuts and stores the
+     * result in a class variable to be used in other methods while processing workspace items.
+     *
+     * @param context used to query LauncherAppsService
+     * @param unlockedUsers this param is changed, and the updated value is used outside this method
+     */
+    @WorkerThread
+    private void queryPinnedShortcutsForUnlockedUsers(Context context,
+            LongSparseArray<Boolean> unlockedUsers) {
+        mUserManagerState.init(mUserCache, mUserManager);
 
-        try {
-            if (c.user == null) {
-                // User has been deleted, remove the item.
-                c.markDeleted("User of this item has been deleted");
-                if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                    restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                            c.itemType, RESTORE_ERROR_PROFILE_DELETED);
+        for (UserHandle user : mUserCache.getUserProfiles()) {
+            long serialNo = mUserCache.getSerialNumberForUser(user);
+            boolean userUnlocked = mUserManager.isUserUnlocked(user);
+
+            // We can only query for shortcuts when the user is unlocked.
+            if (userUnlocked) {
+                QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
+                        .query(ShortcutRequest.PINNED);
+                if (pinnedShortcuts.wasSuccess()) {
+                    for (ShortcutInfo shortcut : pinnedShortcuts) {
+                        mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
+                                shortcut);
+                    }
+                    if (pinnedShortcuts.isEmpty()) {
+                        FileLog.d(TAG, "No pinned shortcuts found for user " + user);
+                    }
+                } else {
+                    // Shortcut manager can fail due to some race condition when the
+                    // lock state changes too frequently. For the purpose of the loading
+                    // shortcuts, consider the user is still locked.
+                    FileLog.d(TAG, "Shortcut request failed for user "
+                            + user + ", user may still be locked.");
+                    userUnlocked = false;
                 }
-                return;
             }
+            unlockedUsers.put(serialNo, userUnlocked);
+        }
 
-            boolean allowMissingTarget = false;
-            switch (c.itemType) {
-                case Favorites.ITEM_TYPE_APPLICATION:
-                case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                    Intent intent = c.parseIntent();
-                    if (intent == null) {
-                        c.markDeleted("Invalid or null intent");
-                        if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                            restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                    c.itemType, RESTORE_ERROR_MISSING_INFO);
-                        }
-                        return;
-                    }
+    }
 
-                    int disabledState = mUserManagerState.isUserQuiet(c.serialNumber)
-                            ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
-                    ComponentName cn = intent.getComponent();
-                    String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
+    /**
+     * After all items have been processed and added to the BgDataModel, this method can correctly
+     * rank items inside folders and load the correct miniature preview icons to be shown when the
+     * folder is collapsed.
+     */
+    @WorkerThread
+    private void processFolderItems() {
+        // Sort the folder items, update ranks, and make sure all preview items are high res.
+        List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
+                .stream().map(FolderGridOrganizer::new).toList();
+        for (FolderInfo folder : mBgDataModel.folders) {
+            Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
+            verifiers.forEach(verifier -> verifier.setFolderInfo(folder));
+            int size = folder.contents.size();
 
-                    if (TextUtils.isEmpty(targetPkg)) {
-                        c.markDeleted("Shortcuts can't have null package");
-                        if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                            restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                    c.itemType, RESTORE_ERROR_MISSING_INFO);
-                        }
-                        return;
-                    }
+            // Update ranks here to ensure there are no gaps caused by removed folder items.
+            // Ranks are the source of truth for folder items, so cellX and cellY can be
+            // ignored for now. Database will be updated once user manually modifies folder.
+            for (int rank = 0; rank < size; ++rank) {
+                WorkspaceItemInfo info = folder.contents.get(rank);
+                // rank is used differently in app pairs, so don't reset
+                if (folder.itemType != ITEM_TYPE_APP_PAIR) {
+                    info.rank = rank;
+                }
 
-                    // If there is no target package, it's an implicit intent
-                    // (legacy shortcut) which is always valid
-                    boolean validTarget = TextUtils.isEmpty(targetPkg)
-                            || mLauncherApps.isPackageEnabled(targetPkg, c.user);
-
-                    // If it's a deep shortcut, we'll use pinned shortcuts to restore it
-                    if (cn != null && validTarget && c.itemType
-                            != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                        // If the apk is present and the shortcut points to a specific component.
-
-                        // If the component is already present
-                        if (mLauncherApps.isActivityEnabled(cn, c.user)) {
-                            // no special handling necessary for this item
-                            c.markRestored();
-                            if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                restoreEventLogger.logSingleFavoritesItemRestored(c.itemType);
-                            }
-                        } else {
-                            // Gracefully try to find a fallback activity.
-                            intent = pmHelper.getAppLaunchIntent(targetPkg, c.user);
-                            if (intent != null) {
-                                c.restoreFlag = 0;
-                                c.updater().put(
-                                        Favorites.INTENT,
-                                        intent.toUri(0)).commit();
-                                cn = intent.getComponent();
-                            } else {
-                                c.markDeleted("Intent null, unable to find a launch target");
-                                if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                    restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                            c.itemType, RESTORE_ERROR_MISSING_INFO);
-                                }
-                                return;
-                            }
-                        }
-                    }
-                    // else if cn == null => can't infer much, leave it
-                    // else if !validPkg => could be restored icon or missing sd-card
-
-                    if (!TextUtils.isEmpty(targetPkg) && !validTarget) {
-                        // Points to a valid app (superset of cn != null) but the apk
-                        // is not available.
-
-                        if (c.restoreFlag != 0) {
-                            // Package is not yet available but might be
-                            // installed later.
-                            FileLog.d(TAG, "package not yet restored: " + targetPkg);
-                            tempPackageKey.update(targetPkg, c.user);
-                            if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
-                                // Restore has started once.
-                            } else if (installingPkgs.containsKey(tempPackageKey)) {
-                                // App restore has started. Update the flag
-                                c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
-                                FileLog.d(TAG, "restore started for installing app: " + targetPkg);
-                                c.updater().put(Favorites.RESTORED, c.restoreFlag).commit();
-                            } else {
-                                c.markDeleted("removing app that is not restored and not "
-                                        + "installing. package: " + targetPkg);
-                                if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                    restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                            c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED);
-                                }
-                                return;
-                            }
-                        } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
-                            // Package is present but not available.
-                            disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE;
-                            // Add the icon on the workspace anyway.
-                            allowMissingTarget = true;
-                        } else if (!isSdCardReady) {
-                            // SdCard is not ready yet. Package might get available,
-                            // once it is ready.
-                            Log.d(TAG, "Missing package, will check later: " + targetPkg);
-                            mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
-                            // Add the icon on the workspace anyway.
-                            allowMissingTarget = true;
-                        } else {
-                            // Do not wait for external media load anymore.
-                            c.markDeleted("Invalid package removed: " + targetPkg);
-                            if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                        c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED);
-                            }
-                            return;
-                        }
-                    }
-
-                    if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) {
-                        validTarget = false;
-                    }
-
-                    if (validTarget) {
-                        // The shortcut points to a valid target (either no target
-                        // or something which is ready to be used)
-                        c.markRestored();
-                    }
-
-                    boolean useLowResIcon = !c.isOnWorkspaceOrHotseat();
-
-                    WorkspaceItemInfo info;
-                    if (c.restoreFlag != 0) {
-                        // Already verified above that user is same as default user
-                        info = c.getRestoredItemInfo(intent);
-                    } else if (c.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                        info = c.getAppShortcutInfo(
-                                intent, allowMissingTarget, useLowResIcon, false);
-                    } else if (c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                        ShortcutKey key = ShortcutKey.fromIntent(intent, c.user);
-                        if (unlockedUsers.get(c.serialNumber)) {
-                            ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key);
-                            if (pinnedShortcut == null) {
-                                // The shortcut is no longer valid.
-                                c.markDeleted("Pinned shortcut not found from request."
-                                        + " package=" + key.getPackageName() + ", user=" + c.user);
-                                if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                    restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                            c.itemType, RESTORE_ERROR_SHORTCUT_NOT_FOUND);
-                                }
-                                return;
-                            }
-                            info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext());
-                            // If the pinned deep shortcut is no longer published,
-                            // use the last saved icon instead of the default.
-                            mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon);
-
-                            if (pmHelper.isAppSuspended(
-                                    pinnedShortcut.getPackage(), info.user)) {
-                                info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
-                            }
-                            intent = info.getIntent();
-                            allDeepShortcuts.add(pinnedShortcut);
-                        } else {
-                            // Create a shortcut info in disabled mode for now.
-                            info = c.loadSimpleWorkspaceItem();
-                            info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
-                        }
-                        if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                            restoreEventLogger.logSingleFavoritesItemRestored(c.itemType);
-                        }
-                    } else { // item type == ITEM_TYPE_SHORTCUT
-                        info = c.loadSimpleWorkspaceItem();
-
-                        // Shortcuts are only available on the primary profile
-                        if (!TextUtils.isEmpty(targetPkg)
-                                && pmHelper.isAppSuspended(targetPkg, c.user)) {
-                            disabledState |= FLAG_DISABLED_SUSPENDED;
-                        }
-                        info.options = c.getOptions();
-
-                        // App shortcuts that used to be automatically added to Launcher
-                        // didn't always have the correct intent flags set, so do that here
-                        if (intent.getAction() != null
-                                && intent.getCategories() != null
-                                && intent.getAction().equals(Intent.ACTION_MAIN)
-                                && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
-                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-                        }
-                    }
-
-                    if (info != null) {
-                        if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                            // Skip deep shortcuts; their title and icons have already been
-                            // loaded above.
-                            iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon));
-                        }
-
-                        c.applyCommonProperties(info);
-
-                        info.intent = intent;
-                        info.rank = c.getRank();
-                        info.spanX = 1;
-                        info.spanY = 1;
-                        info.runtimeStatusFlags |= disabledState;
-                        if (isSafeMode && !isSystemApp(mApp.getContext(), intent)) {
-                            info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
-                        }
-                        LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
-                        if (activityInfo != null) {
-                            info.setProgressLevel(
-                                    PackageManagerHelper.getLoadingProgress(activityInfo),
-                                    PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
-                        }
-
-                        if ((c.restoreFlag != 0
-                                || (enableSupportForArchiving()
-                                && activityInfo != null
-                                && activityInfo.getApplicationInfo().isArchived))
-                                && !TextUtils.isEmpty(targetPkg)) {
-                            tempPackageKey.update(targetPkg, c.user);
-                            SessionInfo si = installingPkgs.get(tempPackageKey);
-                            if (si == null) {
-                                info.runtimeStatusFlags
-                                        &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
-                            } else if (activityInfo == null
-                                    // For archived apps, include progress info in case there is
-                                    // a pending install session post restart of device.
-                                    || (enableSupportForArchiving()
-                                    && activityInfo.getApplicationInfo().isArchived)) {
-                                int installProgress = (int) (si.getProgress() * 100);
-
-                                info.setProgressLevel(installProgress,
-                                        PackageInstallInfo.STATUS_INSTALLING);
-                            }
-                        }
-
-                        c.checkAndAddItem(info, mBgDataModel, memoryLogger);
-                    } else {
-                        throw new RuntimeException("Unexpected null WorkspaceItemInfo");
-                    }
-                    break;
-
-                case Favorites.ITEM_TYPE_FOLDER:
-                case Favorites.ITEM_TYPE_APP_PAIR:
-                    FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
-                    c.applyCommonProperties(folderInfo);
-
-                    folderInfo.itemType = c.itemType;
-                    // Do not trim the folder label, as is was set by the user.
-                    folderInfo.title = c.getString(c.mTitleIndex);
-                    folderInfo.spanX = 1;
-                    folderInfo.spanY = 1;
-                    folderInfo.options = c.getOptions();
-
-                    // no special handling required for restored folders
-                    c.markRestored();
-                    if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                        restoreEventLogger.logSingleFavoritesItemRestored(c.itemType);
-                    }
-                    c.checkAndAddItem(folderInfo, mBgDataModel, memoryLogger);
-                    break;
-
-                case Favorites.ITEM_TYPE_APPWIDGET:
-                    if (WidgetsModel.GO_DISABLE_WIDGETS) {
-                        c.markDeleted("Only legacy shortcuts can have null package");
-                        if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                            restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                    c.itemType, RESTORE_ERROR_WIDGETS_DISABLED);
-                        }
-                        return;
-                    }
-                    // Follow through
-                case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                    // Read all Launcher-specific widget details
-                    boolean customWidget = c.itemType
-                            == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
-
-                    int appWidgetId = c.getAppWidgetId();
-                    String savedProvider = c.getAppWidgetProvider();
-                    final ComponentName component;
-
-                    if ((c.getOptions() & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0) {
-                        component  = QsbContainerView.getSearchComponentName(mApp.getContext());
-                        if (component == null) {
-                            c.markDeleted("Discarding SearchWidget without packagename ");
-                            if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                        c.itemType, RESTORE_ERROR_MISSING_INFO);
-                            }
-                            return;
-                        }
-                    } else {
-                        component = ComponentName.unflattenFromString(savedProvider);
-                    }
-                    final boolean isIdValid =
-                            !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
-                    final boolean wasProviderReady =
-                            !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
-
-                    ComponentKey providerKey = new ComponentKey(component, c.user);
-                    if (!mWidgetProvidersMap.containsKey(providerKey)) {
-                        if (customWidget) {
-                            mWidgetProvidersMap.put(providerKey, CustomWidgetManager.INSTANCE
-                                    .get(mApp.getContext()).getWidgetProvider(component));
-                        } else {
-                            mWidgetProvidersMap.put(providerKey,
-                                    widgetHelper.findProvider(component, c.user));
-                        }
-                    }
-                    final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(providerKey);
-
-                    final boolean isProviderReady = isValidProvider(provider);
-                    if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) {
-                        c.markDeleted("Deleting widget that isn't installed anymore: " + provider);
-                        if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                            restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                    c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED);
-                        }
-                    } else {
-                        LauncherAppWidgetInfo appWidgetInfo;
-                        if (isProviderReady) {
-                            appWidgetInfo =
-                                    new LauncherAppWidgetInfo(appWidgetId, provider.provider);
-
-                            // The provider is available. So the widget is either
-                            // available or not available. We do not need to track
-                            // any future restore updates.
-                            int status = c.restoreFlag
-                                    & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
-                                    & ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
-                            if (!wasProviderReady) {
-                                // If provider was not previously ready, update status and UI flag.
-
-                                // Id would be valid only if the widget restore broadcast received.
-                                if (isIdValid) {
-                                    status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-                                }
-                            }
-                            appWidgetInfo.restoreStatus = status;
-                        } else {
-                            Log.v(TAG, "Widget restore pending id=" + c.id
-                                    + " appWidgetId=" + appWidgetId
-                                    + " status=" + c.restoreFlag);
-                            appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component);
-                            appWidgetInfo.restoreStatus = c.restoreFlag;
-
-                            tempPackageKey.update(component.getPackageName(), c.user);
-                            SessionInfo si = installingPkgs.get(tempPackageKey);
-                            Integer installProgress = si == null
-                                    ? null
-                                    : (int) (si.getProgress() * 100);
-
-                            if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) {
-                                // Restore has started once.
-                            } else if (installProgress != null) {
-                                // App restore has started. Update the flag
-                                appWidgetInfo.restoreStatus
-                                        |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
-                            } else if (!isSafeMode) {
-                                c.markDeleted("Unrestored widget removed: " + component);
-                                if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                    restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                            c.itemType, RESTORE_ERROR_APP_NOT_INSTALLED);
-                                }
-                                return;
-                            }
-
-                            appWidgetInfo.installProgress =
-                                    installProgress == null ? 0 : installProgress;
-                        }
-                        if (appWidgetInfo.hasRestoreFlag(
-                                LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
-                            appWidgetInfo.bindOptions = c.parseIntent();
-                        }
-
-                        c.applyCommonProperties(appWidgetInfo);
-                        appWidgetInfo.spanX = c.getSpanX();
-                        appWidgetInfo.spanY = c.getSpanY();
-                        appWidgetInfo.options = c.getOptions();
-                        appWidgetInfo.user = c.user;
-                        appWidgetInfo.sourceContainer = c.getAppWidgetSource();
-
-                        if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
-                            c.markDeleted("Widget has invalid size: "
-                                    + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
-                            if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                        c.itemType, RESTORE_ERROR_INVALID_LOCATION);
-                            }
-                            return;
-                        }
-                        LauncherAppWidgetProviderInfo widgetProviderInfo =
-                                widgetHelper.getLauncherAppWidgetInfo(appWidgetId,
-                                        appWidgetInfo.getTargetComponent());
-                        if (widgetProviderInfo != null
-                                && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
-                                || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
-                            FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent()
-                                    + " minSizes not meet: span=" + appWidgetInfo.spanX
-                                    + "x" + appWidgetInfo.spanY + " minSpan="
-                                    + widgetProviderInfo.minSpanX + "x"
-                                    + widgetProviderInfo.minSpanY);
-                            logWidgetInfo(mApp.getInvariantDeviceProfile(), widgetProviderInfo);
-                        }
-                        if (!c.isOnWorkspaceOrHotseat()) {
-                            c.markDeleted("Widget found where container != CONTAINER_DESKTOP"
-                                    + "nor CONTAINER_HOTSEAT - ignoring!");
-                            if (mIsRestoreFromBackup && restoreEventLogger != null) {
-                                restoreEventLogger.logSingleFavoritesItemRestoreFailed(
-                                        c.itemType, RESTORE_ERROR_INVALID_LOCATION);
-                            }
-                            return;
-                        }
-
-                        if (!customWidget) {
-                            String providerName = appWidgetInfo.providerName.flattenToString();
-                            if (!providerName.equals(savedProvider)
-                                    || (appWidgetInfo.restoreStatus != c.restoreFlag)) {
-                                c.updater()
-                                        .put(Favorites.APPWIDGET_PROVIDER,
-                                                providerName)
-                                        .put(Favorites.RESTORED,
-                                                appWidgetInfo.restoreStatus)
-                                        .commit();
-                            }
-                        }
-
-                        if (appWidgetInfo.restoreStatus
-                                != LauncherAppWidgetInfo.RESTORE_COMPLETED) {
-                            appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
-                                    mApp.getContext(),
-                                    appWidgetInfo.providerName,
-                                    appWidgetInfo.user);
-                            mIconCache.getTitleAndIconForApp(
-                                    appWidgetInfo.pendingItemInfo, false);
-                        }
-
-                        c.checkAndAddItem(appWidgetInfo, mBgDataModel);
-                    }
-                    break;
+                if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION
+                        && verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) {
+                    mIconCache.getTitleAndIcon(info, false);
+                }
             }
-        } catch (Exception e) {
-            Log.e(TAG, "Desktop items loading interrupted", e);
         }
     }
 
@@ -1206,52 +746,6 @@
                 && (provider.provider.getPackageName() != null);
     }
 
-    @SuppressLint("NewApi") // Already added API check.
-    private static void logWidgetInfo(InvariantDeviceProfile idp,
-            LauncherAppWidgetProviderInfo widgetProviderInfo) {
-        Point cellSize = new Point();
-        for (DeviceProfile deviceProfile : idp.supportedProfiles) {
-            deviceProfile.getCellSize(cellSize);
-            FileLog.d(TAG, "DeviceProfile available width: " + deviceProfile.availableWidthPx
-                    + ", available height: " + deviceProfile.availableHeightPx
-                    + ", cellLayoutBorderSpacePx Horizontal: "
-                    + deviceProfile.cellLayoutBorderSpacePx.x
-                    + ", cellLayoutBorderSpacePx Vertical: "
-                    + deviceProfile.cellLayoutBorderSpacePx.y
-                    + ", cellSize: " + cellSize);
-        }
-
-        StringBuilder widgetDimension = new StringBuilder();
-        widgetDimension.append("Widget dimensions:\n")
-                .append("minResizeWidth: ")
-                .append(widgetProviderInfo.minResizeWidth)
-                .append("\n")
-                .append("minResizeHeight: ")
-                .append(widgetProviderInfo.minResizeHeight)
-                .append("\n")
-                .append("defaultWidth: ")
-                .append(widgetProviderInfo.minWidth)
-                .append("\n")
-                .append("defaultHeight: ")
-                .append(widgetProviderInfo.minHeight)
-                .append("\n");
-        if (Utilities.ATLEAST_S) {
-            widgetDimension.append("targetCellWidth: ")
-                    .append(widgetProviderInfo.targetCellWidth)
-                    .append("\n")
-                    .append("targetCellHeight: ")
-                    .append(widgetProviderInfo.targetCellHeight)
-                    .append("\n")
-                    .append("maxResizeWidth: ")
-                    .append(widgetProviderInfo.maxResizeWidth)
-                    .append("\n")
-                    .append("maxResizeHeight: ")
-                    .append(widgetProviderInfo.maxResizeHeight)
-                    .append("\n");
-        }
-        FileLog.d(TAG, widgetDimension.toString());
-    }
-
     private static void logASplit(String label) {
         if (DEBUG) {
             Log.d(TAG, label);
diff --git a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
new file mode 100644
index 0000000..b12b2bc
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.model
+
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.text.TextUtils
+import com.android.launcher3.LauncherModel.ModelUpdateTask
+import com.android.launcher3.logging.FileLog
+import com.android.launcher3.model.PackageUpdatedTask.OP_ADD
+import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE
+import com.android.launcher3.model.PackageUpdatedTask.OP_SUSPEND
+import com.android.launcher3.model.PackageUpdatedTask.OP_UNAVAILABLE
+import com.android.launcher3.model.PackageUpdatedTask.OP_UNSUSPEND
+import com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE
+import java.util.function.Consumer
+
+/**
+ * Implementation of {@link LauncherApps#Callbacks} which converts various events to corresponding
+ * model tasks
+ */
+class ModelLauncherCallbacks(private var taskExecutor: Consumer<ModelUpdateTask>) :
+    LauncherApps.Callback() {
+
+    override fun onPackageAdded(packageName: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_ADD, user, packageName))
+    }
+
+    override fun onPackageChanged(packageName: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, packageName))
+    }
+
+    override fun onPackageLoadingProgressChanged(
+        packageName: String,
+        user: UserHandle,
+        progress: Float
+    ) {
+        taskExecutor.accept(PackageIncrementalDownloadUpdatedTask(packageName, user, progress))
+    }
+
+    override fun onPackageRemoved(packageName: String, user: UserHandle) {
+        FileLog.d(TAG, "package removed received $packageName")
+        taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, packageName))
+    }
+
+    override fun onPackagesAvailable(
+        vararg packageNames: String,
+        user: UserHandle,
+        replacing: Boolean
+    ) {
+        taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, *packageNames))
+    }
+
+    override fun onPackagesSuspended(vararg packageNames: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_SUSPEND, user, *packageNames))
+    }
+
+    override fun onPackagesUnavailable(
+        packageNames: Array<String>,
+        user: UserHandle,
+        replacing: Boolean
+    ) {
+        if (!replacing) {
+            taskExecutor.accept(PackageUpdatedTask(OP_UNAVAILABLE, user, *packageNames))
+        }
+    }
+
+    override fun onPackagesUnsuspended(vararg packageNames: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_UNSUSPEND, user, *packageNames))
+    }
+
+    override fun onShortcutsChanged(
+        packageName: String,
+        shortcuts: MutableList<ShortcutInfo>,
+        user: UserHandle
+    ) {
+        taskExecutor.accept(ShortcutsChangedTask(packageName, shortcuts, user, true))
+    }
+
+    fun onPackagesRemoved(user: UserHandle, packages: List<String>) {
+        FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages))
+        taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, *packages.toTypedArray()))
+    }
+
+    companion object {
+        private const val TAG = "LauncherAppsCallbackImpl"
+    }
+}
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 76a87ed..2457a42 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -52,7 +52,8 @@
                 ApplicationInfo ai = app.getContext()
                         .getPackageManager().getApplicationInfo(mInstallInfo.packageName, 0);
                 if (InstantAppResolver.newInstance(app.getContext()).isInstantApp(ai)) {
-                    app.getModel().onPackageAdded(ai.packageName, mInstallInfo.user);
+                    app.getModel().newModelCallbacks()
+                            .onPackageAdded(ai.packageName, mInstallInfo.user);
                 }
             } catch (PackageManager.NameNotFoundException e) {
                 // Ignore
diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
index 3798575..8cfa3aa 100644
--- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java
+++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
@@ -67,11 +67,10 @@
                 }
             }
             if (!packagesRemoved.isEmpty()) {
-                mModel.onPackagesRemoved(user,
-                        packagesRemoved.toArray(new String[packagesRemoved.size()]));
+                mModel.newModelCallbacks().onPackagesRemoved(user, packagesRemoved);
             }
             if (!packagesUnavailable.isEmpty()) {
-                mModel.onPackagesUnavailable(
+                mModel.newModelCallbacks().onPackagesUnavailable(
                         packagesUnavailable.toArray(new String[packagesUnavailable.size()]),
                         user, false);
             }
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
new file mode 100644
index 0000000..5389d38
--- /dev/null
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -0,0 +1,672 @@
+/*
+ * Copyright (C) 2023 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.model
+
+import android.annotation.SuppressLint
+import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.ShortcutInfo
+import android.graphics.Point
+import android.text.TextUtils
+import android.util.Log
+import android.util.LongSparseArray
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.Utilities
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.logging.FileLog
+import com.android.launcher3.model.data.IconRequestInfo
+import com.android.launcher3.model.data.ItemInfoWithIcon
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.PackageInstallInfo
+import com.android.launcher3.qsb.QsbContainerView
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.WidgetManagerHelper
+import com.android.launcher3.widget.custom.CustomWidgetManager
+
+/**
+ * This items is used by LoaderTask to process items that have been loaded from the Launcher's DB.
+ * This data, stored in the Favorites table, needs to be processed in order to be shown on the Home
+ * Page.
+ *
+ * This class processes each of those items: App Shortcuts, Widgets, Folders, etc., one at a time.
+ */
+class WorkspaceItemProcessor(
+    private val c: LoaderCursor,
+    private val memoryLogger: LoaderMemoryLogger?,
+    private val restoreEventLogger: LauncherRestoreEventLogger?,
+    private val userManagerState: UserManagerState,
+    private val launcherApps: LauncherApps,
+    private val pendingPackages: MutableSet<PackageUserKey>,
+    private val shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo>,
+    private val app: LauncherAppState,
+    private val iconCache: IconCache,
+    private val bgDataModel: BgDataModel,
+    private val widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?>,
+    private val isRestoreFromBackup: Boolean,
+    private val installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo>,
+    private val isSdCardReady: Boolean,
+    private val tempPackageKey: PackageUserKey,
+    private val widgetHelper: WidgetManagerHelper,
+    private val pmHelper: PackageManagerHelper,
+    private val iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>>,
+    private val unlockedUsers: LongSparseArray<Boolean>,
+    private val isSafeMode: Boolean,
+    private val allDeepShortcuts: MutableList<ShortcutInfo>
+) {
+    /**
+     * This is the entry point for processing 1 workspace item. This method is like the midfielder
+     * that delegates the actual processing to either processAppShortcut, processFolder, or
+     * processWidget depending on what type of item is being processed.
+     *
+     * All the parameters are expected to be shared between many repeated calls of this method, one
+     * for each workspace item.
+     */
+    fun processItem() {
+        try {
+            if (c.user == null) {
+                // User has been deleted, remove the item.
+                c.markDeleted("User has been deleted")
+                if (isRestoreFromBackup) {
+                    restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                        c.itemType,
+                        LauncherRestoreEventLogger.RESTORE_ERROR_PROFILE_DELETED
+                    )
+                }
+                return
+            }
+            when (c.itemType) {
+                Favorites.ITEM_TYPE_APPLICATION,
+                Favorites.ITEM_TYPE_DEEP_SHORTCUT -> processAppOrDeepShortcut()
+                Favorites.ITEM_TYPE_FOLDER,
+                Favorites.ITEM_TYPE_APP_PAIR -> processFolderOrAppPair()
+                Favorites.ITEM_TYPE_APPWIDGET,
+                Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> processWidget()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Desktop items loading interrupted", e)
+        }
+    }
+
+    /**
+     * This method verifies that an app shortcut should be shown on the home screen, updates the
+     * database accordingly, formats the data in such a way that it is ready to be added to the data
+     * model, and then adds it to the launcher’s data model.
+     *
+     * In this method, verification means that an an app shortcut database entry is required to:
+     * Have a Launch Intent. This is how the app component symbolized by the shortcut is launched.
+     * Have a Package Name. Not be in a funky “Restoring, but never actually restored” state. Not
+     * have null or missing ShortcutInfos or ItemInfos in other data models.
+     *
+     * If any of the above are found to be true, the database entry is deleted, and not shown on the
+     * user’s home screen. When an app is verified, it is marked as restored, meaning that the app
+     * is viable to show on the home screen.
+     *
+     * In order to accommodate different types and versions of App Shortcuts, different properties
+     * and flags are set on the ItemInfo objects that are added to the data model. For example,
+     * icons that are not a part of the workspace or hotseat are marked as using low resolution icon
+     * bitmaps. Currently suspended app icons are marked as such. Installing packages are also
+     * marked as such. Lastly, after applying common properties to the ItemInfo, it is added to the
+     * data model to be bound to the launcher’s data model.
+     */
+    @SuppressLint("NewApi")
+    @VisibleForTesting
+    fun processAppOrDeepShortcut() {
+        var allowMissingTarget = false
+        var intent = c.parseIntent()
+        if (intent == null) {
+            c.markDeleted("Invalid or null intent")
+            if (isRestoreFromBackup) {
+                restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                    c.itemType,
+                    LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO
+                )
+            }
+            return
+        }
+        var disabledState =
+            if (userManagerState.isUserQuiet(c.serialNumber))
+                WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER
+            else 0
+        var cn = intent.component
+        val targetPkg = if (cn == null) intent.getPackage() else cn.packageName
+        if (TextUtils.isEmpty(targetPkg)) {
+            c.markDeleted("Shortcuts can't have null package")
+            if (isRestoreFromBackup) {
+                restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                    c.itemType,
+                    LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO
+                )
+            }
+            return
+        }
+
+        // If there is no target package, it's an implicit intent
+        // (legacy shortcut) which is always valid
+        var validTarget =
+            (TextUtils.isEmpty(targetPkg) || launcherApps.isPackageEnabled(targetPkg, c.user))
+
+        // If it's a deep shortcut, we'll use pinned shortcuts to restore it
+        if (cn != null && validTarget && (c.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
+            // If the apk is present and the shortcut points to a specific component.
+
+            // If the component is already present
+            if (launcherApps.isActivityEnabled(cn, c.user)) {
+                // no special handling necessary for this item
+                c.markRestored()
+                if (isRestoreFromBackup) {
+                    restoreEventLogger?.logSingleFavoritesItemRestored(c.itemType)
+                }
+            } else {
+                // Gracefully try to find a fallback activity.
+                intent = pmHelper.getAppLaunchIntent(targetPkg, c.user)
+                if (intent != null) {
+                    c.restoreFlag = 0
+                    c.updater().put(Favorites.INTENT, intent.toUri(0)).commit()
+                } else {
+                    c.markDeleted("Intent null, unable to find a launch target")
+                    if (isRestoreFromBackup) {
+                        restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                            c.itemType,
+                            LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO
+                        )
+                    }
+                    return
+                }
+            }
+        }
+        // else if cn == null => can't infer much, leave it
+        // else if !validPkg => could be restored icon or missing sd-card
+        when {
+            !TextUtils.isEmpty(targetPkg) && !validTarget -> {
+                // Points to a valid app (superset of cn != null) but the apk
+                // is not available.
+                when {
+                    c.restoreFlag != 0 -> {
+                        // Package is not yet available but might be
+                        // installed later.
+                        FileLog.d(TAG, "package not yet restored: $targetPkg")
+                        tempPackageKey.update(targetPkg, c.user)
+                        when {
+                            c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
+                                // Restore has started once.
+                            }
+                            installingPkgs.containsKey(tempPackageKey) -> {
+                                // App restore has started. Update the flag
+                                c.restoreFlag =
+                                    c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
+                                FileLog.d(TAG, "restore started for installing app: $targetPkg")
+                                c.updater().put(Favorites.RESTORED, c.restoreFlag).commit()
+                            }
+                            else -> {
+                                c.markDeleted(
+                                    "removing app that is not restored and not installing. package: $targetPkg"
+                                )
+                                if (isRestoreFromBackup) {
+                                    restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                                        c.itemType,
+                                        LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED
+                                    )
+                                }
+                                return
+                            }
+                        }
+                    }
+                    pmHelper.isAppOnSdcard(targetPkg!!, c.user) -> {
+                        // Package is present but not available.
+                        disabledState =
+                            disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE
+                        // Add the icon on the workspace anyway.
+                        allowMissingTarget = true
+                    }
+                    !isSdCardReady -> {
+                        // SdCard is not ready yet. Package might get available,
+                        // once it is ready.
+                        Log.d(TAG, "Missing package, will check later: $targetPkg")
+                        pendingPackages.add(PackageUserKey(targetPkg, c.user))
+                        // Add the icon on the workspace anyway.
+                        allowMissingTarget = true
+                    }
+                    else -> {
+                        // Do not wait for external media load anymore.
+                        c.markDeleted("Invalid package removed: $targetPkg")
+                        if (isRestoreFromBackup) {
+                            restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                                c.itemType,
+                                LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED
+                            )
+                        }
+                        return
+                    }
+                }
+            }
+        }
+        if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) {
+            validTarget = false
+        }
+        if (validTarget) {
+            // The shortcut points to a valid target (either no target
+            // or something which is ready to be used)
+            c.markRestored()
+        }
+        val useLowResIcon = !c.isOnWorkspaceOrHotseat
+        val info: WorkspaceItemInfo?
+        when {
+            c.restoreFlag != 0 -> {
+                // Already verified above that user is same as default user
+                info = c.getRestoredItemInfo(intent)
+            }
+            c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
+                info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
+            c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT -> {
+                val key = ShortcutKey.fromIntent(intent, c.user)
+                if (unlockedUsers[c.serialNumber]) {
+                    val pinnedShortcut = shortcutKeyToPinnedShortcuts[key]
+                    if (pinnedShortcut == null) {
+                        // The shortcut is no longer valid.
+                        c.markDeleted(
+                            "Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}"
+                        )
+                        if (isRestoreFromBackup) {
+                            restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                                c.itemType,
+                                LauncherRestoreEventLogger.RESTORE_ERROR_SHORTCUT_NOT_FOUND
+                            )
+                        }
+                        return
+                    }
+                    info = WorkspaceItemInfo(pinnedShortcut, app.context)
+                    // If the pinned deep shortcut is no longer published,
+                    // use the last saved icon instead of the default.
+                    iconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon)
+                    if (pmHelper.isAppSuspended(pinnedShortcut.getPackage(), info.user)) {
+                        info.runtimeStatusFlags =
+                            info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
+                    }
+                    intent = info.getIntent()
+                    allDeepShortcuts.add(pinnedShortcut)
+                } else {
+                    // Create a shortcut info in disabled mode for now.
+                    info = c.loadSimpleWorkspaceItem()
+                    info.runtimeStatusFlags =
+                        info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER
+                }
+                if (isRestoreFromBackup) {
+                    restoreEventLogger?.logSingleFavoritesItemRestored(c.itemType)
+                }
+            }
+            else -> { // item type == ITEM_TYPE_SHORTCUT
+                info = c.loadSimpleWorkspaceItem()
+
+                // Shortcuts are only available on the primary profile
+                if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg!!, c.user)) {
+                    disabledState = disabledState or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
+                }
+                info.options = c.options
+
+                // App shortcuts that used to be automatically added to Launcher
+                // didn't always have the correct intent flags set, so do that here
+                if (
+                    intent.action != null &&
+                        intent.categories != null &&
+                        intent.action == Intent.ACTION_MAIN &&
+                        intent.categories.contains(Intent.CATEGORY_LAUNCHER)
+                ) {
+                    intent.addFlags(
+                        Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                    )
+                }
+            }
+        }
+        if (info != null) {
+            if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                // Skip deep shortcuts; their title and icons have already been
+                // loaded above.
+                iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon))
+            }
+            c.applyCommonProperties(info)
+            info.intent = intent
+            info.rank = c.rank
+            info.spanX = 1
+            info.spanY = 1
+            info.runtimeStatusFlags = info.runtimeStatusFlags or disabledState
+            if (isSafeMode && !PackageManagerHelper.isSystemApp(app.context, intent)) {
+                info.runtimeStatusFlags =
+                    info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE
+            }
+            val activityInfo = c.launcherActivityInfo
+            if (activityInfo != null) {
+                info.setProgressLevel(
+                    PackageManagerHelper.getLoadingProgress(activityInfo),
+                    PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING
+                )
+            }
+            if (
+                (c.restoreFlag != 0 ||
+                    Flags.enableSupportForArchiving() &&
+                        activityInfo != null &&
+                        activityInfo.applicationInfo.isArchived) && !TextUtils.isEmpty(targetPkg)
+            ) {
+                tempPackageKey.update(targetPkg, c.user)
+                val si = installingPkgs[tempPackageKey]
+                if (si == null) {
+                    info.runtimeStatusFlags =
+                        info.runtimeStatusFlags and
+                            ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE.inv()
+                } else if (
+                    activityInfo ==
+                        null // For archived apps, include progress info in case there is
+                    // a pending install session post restart of device.
+                    ||
+                        (Flags.enableSupportForArchiving() &&
+                            activityInfo.applicationInfo.isArchived)
+                ) {
+                    val installProgress = (si.getProgress() * 100).toInt()
+                    info.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING)
+                }
+            }
+            c.checkAndAddItem(info, bgDataModel, memoryLogger)
+        } else {
+            throw RuntimeException("Unexpected null WorkspaceItemInfo")
+        }
+    }
+
+    /**
+     * Loads the folder information from the database and formats it into a FolderInfo. Some of the
+     * processing for folder content items is done in LoaderTask after all the items in the
+     * workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel.
+     */
+    @VisibleForTesting
+    fun processFolderOrAppPair() {
+        val folderInfo =
+            bgDataModel.findOrMakeFolder(c.id).apply {
+                c.applyCommonProperties(this)
+                itemType = c.itemType
+                // Do not trim the folder label, as is was set by the user.
+                title = c.getString(c.mTitleIndex)
+                spanX = 1
+                spanY = 1
+                options = c.options
+            }
+
+        // no special handling required for restored folders
+        c.markRestored()
+        if (isRestoreFromBackup) {
+            restoreEventLogger?.logSingleFavoritesItemRestored(c.itemType)
+        }
+        c.checkAndAddItem(folderInfo, bgDataModel, memoryLogger)
+    }
+
+    /**
+     * This method, similar to processAppShortcut above, verifies that a widget should be shown on
+     * the home screen, updates the database accordingly, formats the data in such a way that it is
+     * ready to be added to the data model, and then adds it to the launcher’s data model.
+     *
+     * It verifies that: Widgets are not disabled due to the Launcher variety being of the `Go`
+     * type. Search Widgets have a package name. The app behind the widget is still installed on the
+     * device. The app behind the widget is not in a funky “Restoring, but never actually restored”
+     * state. The widget has a valid size. The widget is in the workspace or the hotseat. If any of
+     * the above are found to be true, the database entry is deleted, and the widget is not shown on
+     * the user’s home screen. When a widget is verified, it is marked as restored, meaning that the
+     * widget is viable to show on the home screen.
+     *
+     * Common properties are applied to the Widget’s Info object, and other information as well
+     * depending on the type of widget. Custom widgets are treated differently than non-custom
+     * widgets, installing / restoring widgets are treated differently, etc.
+     */
+    @VisibleForTesting
+    fun processWidget() {
+        if (WidgetsModel.GO_DISABLE_WIDGETS && c.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
+            c.markDeleted("Only legacy shortcuts can have null package")
+            if (isRestoreFromBackup) {
+                restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                    c.itemType,
+                    LauncherRestoreEventLogger.RESTORE_ERROR_WIDGETS_DISABLED
+                )
+            }
+            return
+        }
+
+        // Read all Launcher-specific widget details
+        val customWidget = (c.itemType == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET)
+        val appWidgetId = c.appWidgetId
+        val savedProvider = c.appWidgetProvider
+        val component: ComponentName?
+        if (c.options and LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET != 0) {
+            component = QsbContainerView.getSearchComponentName(app.context)
+            if (component == null) {
+                c.markDeleted("Discarding SearchWidget without package name")
+                if (isRestoreFromBackup) {
+                    restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                        c.itemType,
+                        LauncherRestoreEventLogger.RESTORE_ERROR_MISSING_INFO
+                    )
+                }
+                return
+            }
+        } else {
+            component = ComponentName.unflattenFromString(savedProvider)
+        }
+        val isIdValid = !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)
+        val wasProviderReady = !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+        val providerKey = ComponentKey(component, c.user)
+        if (!widgetProvidersMap.containsKey(providerKey)) {
+            if (customWidget) {
+                widgetProvidersMap[providerKey] =
+                    CustomWidgetManager.INSTANCE[app.context].getWidgetProvider(component)
+            } else {
+                widgetProvidersMap[providerKey] = widgetHelper.findProvider(component, c.user)
+            }
+        }
+        val provider = widgetProvidersMap[providerKey]
+        val isProviderReady = LoaderTask.isValidProvider(provider)
+        if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) {
+            c.markDeleted("Deleting widget that isn't installed anymore: $provider")
+            if (isRestoreFromBackup) {
+                restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                    c.itemType,
+                    LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED
+                )
+            }
+        } else {
+            val appWidgetInfo: LauncherAppWidgetInfo
+            if (isProviderReady) {
+                appWidgetInfo = LauncherAppWidgetInfo(appWidgetId, provider!!.provider)
+
+                // The provider is available. So the widget is either
+                // available or not available. We do not need to track
+                // any future restore updates.
+                var status =
+                    (c.restoreFlag and
+                        LauncherAppWidgetInfo.FLAG_RESTORE_STARTED.inv() and
+                        LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY.inv())
+                if (!wasProviderReady) {
+                    // If provider was not previously ready, update status and UI flag.
+
+                    // Id would be valid only if the widget restore broadcast received.
+                    if (isIdValid) {
+                        status = status or LauncherAppWidgetInfo.FLAG_UI_NOT_READY
+                    }
+                }
+                appWidgetInfo.restoreStatus = status
+            } else {
+                Log.v(
+                    TAG,
+                    "Widget restore pending id=${c.id} appWidgetId=$appWidgetId status=${c.restoreFlag}"
+                )
+                appWidgetInfo = LauncherAppWidgetInfo(appWidgetId, component)
+                appWidgetInfo.restoreStatus = c.restoreFlag
+                tempPackageKey.update(component!!.packageName, c.user)
+                val si = installingPkgs[tempPackageKey]
+                val installProgress = if (si == null) null else (si.getProgress() * 100).toInt()
+                when {
+                    c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) -> {
+                        // Restore has started once.
+                    }
+                    installProgress != null -> {
+                        // App restore has started. Update the flag
+                        appWidgetInfo.restoreStatus =
+                            appWidgetInfo.restoreStatus or
+                                LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
+                    }
+                    !isSafeMode -> {
+                        c.markDeleted("Unrestored widget removed: $component")
+                        if (isRestoreFromBackup) {
+                            restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                                c.itemType,
+                                LauncherRestoreEventLogger.RESTORE_ERROR_APP_NOT_INSTALLED
+                            )
+                        }
+                        return
+                    }
+                }
+                appWidgetInfo.installProgress = installProgress ?: 0
+            }
+            if (appWidgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
+                appWidgetInfo.bindOptions = c.parseIntent()
+            }
+            c.applyCommonProperties(appWidgetInfo)
+            appWidgetInfo.spanX = c.spanX
+            appWidgetInfo.spanY = c.spanY
+            appWidgetInfo.options = c.options
+            appWidgetInfo.user = c.user
+            appWidgetInfo.sourceContainer = c.appWidgetSource
+            if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
+                c.markDeleted(
+                    "Widget has invalid size: ${appWidgetInfo.spanX}x${appWidgetInfo.spanY}"
+                )
+                if (isRestoreFromBackup) {
+                    restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                        c.itemType,
+                        LauncherRestoreEventLogger.RESTORE_ERROR_INVALID_LOCATION
+                    )
+                }
+                return
+            }
+            val widgetProviderInfo =
+                widgetHelper.getLauncherAppWidgetInfo(appWidgetId, appWidgetInfo.targetComponent)
+            if (
+                widgetProviderInfo != null &&
+                    (appWidgetInfo.spanX < widgetProviderInfo.minSpanX ||
+                        appWidgetInfo.spanY < widgetProviderInfo.minSpanY)
+            ) {
+                FileLog.d(
+                    TAG,
+                    "Widget ${widgetProviderInfo.component} minSizes not meet:" +
+                        " span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY}" +
+                        " minSpan=${widgetProviderInfo.minSpanX}x${widgetProviderInfo.minSpanY}"
+                )
+                logWidgetInfo(app.invariantDeviceProfile, widgetProviderInfo)
+            }
+            if (!c.isOnWorkspaceOrHotseat) {
+                c.markDeleted(
+                    "Widget found where container != CONTAINER_DESKTOP" +
+                        " nor CONTAINER_HOTSEAT - ignoring!"
+                )
+                if (isRestoreFromBackup) {
+                    restoreEventLogger?.logSingleFavoritesItemRestoreFailed(
+                        c.itemType,
+                        LauncherRestoreEventLogger.RESTORE_ERROR_INVALID_LOCATION
+                    )
+                }
+                return
+            }
+            if (!customWidget) {
+                val providerName = appWidgetInfo.providerName.flattenToString()
+                if (providerName != savedProvider || appWidgetInfo.restoreStatus != c.restoreFlag) {
+                    c.updater()
+                        .put(Favorites.APPWIDGET_PROVIDER, providerName)
+                        .put(Favorites.RESTORED, appWidgetInfo.restoreStatus)
+                        .commit()
+                }
+            }
+            if (appWidgetInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) {
+                appWidgetInfo.pendingItemInfo =
+                    WidgetsModel.newPendingItemInfo(
+                        app.context,
+                        appWidgetInfo.providerName,
+                        appWidgetInfo.user
+                    )
+                iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false)
+            }
+            c.checkAndAddItem(appWidgetInfo, bgDataModel)
+        }
+    }
+
+    companion object {
+        private val TAG = WorkspaceItemProcessor::class.java.simpleName
+        private fun logWidgetInfo(
+            idp: InvariantDeviceProfile,
+            widgetProviderInfo: LauncherAppWidgetProviderInfo
+        ) {
+            val cellSize = Point()
+            for (deviceProfile in idp.supportedProfiles) {
+                deviceProfile.getCellSize(cellSize)
+                FileLog.d(
+                    TAG,
+                    "DeviceProfile available width: ${deviceProfile.availableWidthPx}," +
+                        " available height: ${deviceProfile.availableHeightPx}," +
+                        " cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," +
+                        " cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," +
+                        " cellSize: $cellSize"
+                )
+            }
+            val widgetDimension = StringBuilder()
+            widgetDimension
+                .append("Widget dimensions:\n")
+                .append("minResizeWidth: ")
+                .append(widgetProviderInfo.minResizeWidth)
+                .append("\n")
+                .append("minResizeHeight: ")
+                .append(widgetProviderInfo.minResizeHeight)
+                .append("\n")
+                .append("defaultWidth: ")
+                .append(widgetProviderInfo.minWidth)
+                .append("\n")
+                .append("defaultHeight: ")
+                .append(widgetProviderInfo.minHeight)
+                .append("\n")
+            if (Utilities.ATLEAST_S) {
+                widgetDimension
+                    .append("targetCellWidth: ")
+                    .append(widgetProviderInfo.targetCellWidth)
+                    .append("\n")
+                    .append("targetCellHeight: ")
+                    .append(widgetProviderInfo.targetCellHeight)
+                    .append("\n")
+                    .append("maxResizeWidth: ")
+                    .append(widgetProviderInfo.maxResizeWidth)
+                    .append("\n")
+                    .append("maxResizeHeight: ")
+                    .append(widgetProviderInfo.maxResizeHeight)
+                    .append("\n")
+            }
+            FileLog.d(TAG, widgetDimension.toString())
+        }
+    }
+}
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index a969c8f..9750d25 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -18,7 +18,7 @@
 
 import static android.os.Process.myUserHandle;
 
-import static com.android.launcher3.Flags.enableLauncherBrMetrics;
+import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS;
 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
@@ -30,7 +30,6 @@
 import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_PROFILE_NOT_RESTORED;
 import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_WIDGETS_DISABLED;
 import static com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RESTORE_ERROR_WIDGET_REMOVED;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_BR_METRICS;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
 
@@ -199,7 +198,7 @@
         Arrays.fill(args, "?");
         final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
         logFavoritesTable(db, "items to delete from unrestored profiles:", where, profileIds);
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             reportUnrestoredProfiles(db, where, profileIds, restoreEventLogger);
         }
         int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
@@ -361,7 +360,7 @@
         DeviceGridState deviceGridState = new DeviceGridState(context);
         FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState);
         LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType()));
-        if (enableLauncherBrMetrics() || ENABLE_LAUNCHER_BR_METRICS.get()) {
+        if (enableLauncherBrMetricsFixed()) {
             LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
         }
     }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index c3ab08c..54c9324 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Outline;
 import android.os.Process;
 import android.util.AttributeSet;
@@ -124,6 +125,9 @@
     }
 
     @Override
+    protected void onConfigurationChanged(Configuration newConfig) {}
+
+    @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
         if (changed && mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index def27b8..dbca9d1 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -1,6 +1,5 @@
 package com.android.launcher3.model
 
-import android.content.Context
 import android.os.UserHandle
 import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -11,16 +10,20 @@
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherModel
 import com.android.launcher3.LauncherModel.LoaderTransaction
+import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.icons.cache.CachingLogic
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
+import com.android.launcher3.pm.InstallSessionHelper
 import com.android.launcher3.pm.UserCache
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.LooperIdleLock
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.UserIconInfo
-import com.android.launcher3.util.rule.StaticMockitoRule
 import com.google.common.truth.Truth
 import java.util.concurrent.CountDownLatch
+import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -39,6 +42,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class LoaderTaskTest {
+    private lateinit var context: SandboxContext
     @Mock private lateinit var app: LauncherAppState
     @Mock private lateinit var bgAllAppsList: AllAppsList
     @Mock private lateinit var modelDelegate: ModelDelegate
@@ -52,21 +56,28 @@
 
     @Spy private var userManagerState: UserManagerState? = UserManagerState()
 
-    @get:Rule(order = 0) val staticMockitoRule = StaticMockitoRule(UserCache::class.java)
-    @get:Rule(order = 1)
-    val setFlagsRule = SetFlagsRule().apply { initAllFlagsToReleaseConfigDefault() }
+    @get:Rule val setFlagsRule = SetFlagsRule().apply { initAllFlagsToReleaseConfigDefault() }
 
     @Before
     fun setup() {
-        val context = InstrumentationRegistry.getInstrumentation().targetContext
+        MockitoAnnotations.initMocks(this)
+
+        context =
+            SandboxContext(
+                InstrumentationRegistry.getInstrumentation().targetContext,
+                InstallSessionHelper.INSTANCE,
+                LauncherPrefs.INSTANCE,
+                ItemInstallQueue.INSTANCE,
+                PluginManagerWrapper.INSTANCE
+            )
         val idp =
-            InvariantDeviceProfile.INSTANCE[context].apply {
+            InvariantDeviceProfile().apply {
                 numRows = 5
                 numColumns = 6
                 numDatabaseHotseatIcons = 5
             }
+        context.putObject(InvariantDeviceProfile.INSTANCE, idp)
 
-        MockitoAnnotations.initMocks(this)
         `when`(app.context).thenReturn(context)
         `when`(app.model).thenReturn(launcherModel)
         `when`(launcherModel.beginLoader(any(LoaderTask::class.java))).thenReturn(transaction)
@@ -77,7 +88,12 @@
         `when`(launcherBinder.newIdleLock(any(LoaderTask::class.java))).thenReturn(idleLock)
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
         `when`(iconCache.updateHandler).thenReturn(iconCacheUpdateHandler)
-        `when`(UserCache.getInstance(any(Context::class.java))).thenReturn(userCache)
+        context.putObject(UserCache.INSTANCE, userCache)
+    }
+
+    @After
+    fun tearDown() {
+        context.onDestroy()
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index 3dfd6b4..25a4c4e 100644
--- a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -73,6 +73,10 @@
         TestUtil.uninstallDummyApp();
     }
 
+    private ModelLauncherCallbacks getCallbacks() {
+        return mModelHelper.getModel().newModelCallbacks();
+    }
+
     @Test
     public void testTwoCallbacks_loadedTogether() throws Exception {
         setupWorkspacePages(3);
@@ -127,14 +131,14 @@
 
         // Install package 1
         TestUtil.installDummyApp();
-        mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+        getCallbacks().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
         waitForLoaderAndTempMainThread();
         assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
         assertTrue(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
 
         // Uninstall package 2
         TestUtil.uninstallDummyApp();
-        mModelHelper.getModel().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+        getCallbacks().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
         waitForLoaderAndTempMainThread();
         assertFalse(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
         assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
@@ -142,7 +146,7 @@
         // Unregister a callback and verify updates no longer received
         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2));
         TestUtil.installDummyApp();
-        mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+        getCallbacks().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
         waitForLoaderAndTempMainThread();
 
         // cb2 didn't get the update
diff --git a/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java b/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java
deleted file mode 100644
index 6b91474..0000000
--- a/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2022 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.rule;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-
-import com.android.dx.mockito.inline.extended.StaticMockitoSession;
-import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
-
-import org.junit.rules.MethodRule;
-import org.junit.runners.model.FrameworkMethod;
-import org.junit.runners.model.Statement;
-import org.mockito.junit.MockitoRule;
-import org.mockito.quality.Strictness;
-
-/**
- * Similar to {@link MockitoRule}, but uses {@link StaticMockitoSession}, which allows mocking
- * static methods.
- */
-public class StaticMockitoRule implements MethodRule {
-    private Class<?>[] mClasses;
-
-    public StaticMockitoRule(Class<?>... classes) {
-        mClasses = classes;
-    }
-
-    @Override
-    public Statement apply(Statement base, FrameworkMethod method, Object target) {
-        return new Statement() {
-            public void evaluate() throws Throwable {
-                StaticMockitoSessionBuilder builder =
-                        mockitoSession()
-                                .name(target.getClass().getSimpleName() + "." + method.getName())
-                                .initMocks(target)
-                                .strictness(Strictness.STRICT_STUBS);
-
-                for (Class<?> clazz : mClasses) {
-                    builder.mockStatic(clazz);
-                }
-
-                StaticMockitoSession session = builder.startMocking();
-                Throwable testFailure = evaluateSafely(base);
-                session.finishMocking(testFailure);
-                if (testFailure != null) {
-                    throw testFailure;
-                }
-            }
-
-            private Throwable evaluateSafely(Statement base) {
-                try {
-                    base.evaluate();
-                    return null;
-                } catch (Throwable throwable) {
-                    return throwable;
-                }
-            }
-        };
-    }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index a202c53..e6315f3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -153,7 +153,7 @@
         return By.clazz(TextView.class).text("");
     }
 
-    private Rect getVisibleBounds() {
+    public Rect getVisibleBounds() {
         return mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID).getVisibleBounds();
     }