Moving various launcher binding logic, outside LauncherModel

This brings the BgCallbacks closer to a repository pattern making is easier to switch eventually

Bug: 390572144
Flag: EXEMPT refactor
Test: Updated AsyncBindingTest to use real ModelCalbacks
Change-Id: I9c932b00ea8ac7330473b9c0f5d778453fe7a390
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index b77c43f..7c53360 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -33,7 +33,6 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.views.ArrowTipView;
 import com.android.launcher3.views.Snackbar;
 
@@ -57,7 +56,6 @@
     private HotseatEduDialog mActiveDialog;
 
     private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
-    private IntArray mNewScreens = null;
 
     HotseatEduController(Launcher launcher) {
         mLauncher = launcher;
@@ -96,7 +94,6 @@
         }
         if (pageId == -1) {
             pageId = mLauncher.getModel().getModelDbController().getNewScreenId();
-            mNewScreens = IntArray.wrap(pageId);
         }
         boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout();
         int hotseatItemsNum = mLauncher.getDeviceProfile().numShownHotseatIcons;
@@ -117,18 +114,7 @@
     void moveHotseatItems() {
         mHotseat.removeAllViewsInLayout();
         if (!mNewItems.isEmpty()) {
-            int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
-            ArrayList<ItemInfo> animated = new ArrayList<>();
-            ArrayList<ItemInfo> nonAnimated = new ArrayList<>();
-
-            for (ItemInfo info : mNewItems) {
-                if (info.screenId == lastPage) {
-                    animated.add(info);
-                } else {
-                    nonAnimated.add(info);
-                }
-            }
-            mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
+            mLauncher.bindItemsAdded(mNewItems);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 836c06c..b610f67 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -29,12 +29,12 @@
 import com.android.launcher3.celllayout.CellInfo;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.StringCache;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.taskbar.TaskbarView.TaskbarLayoutParams;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.PackageUserKey;
@@ -42,7 +42,6 @@
 import com.android.quickstep.util.GroupTask;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -68,7 +67,6 @@
     // Used to defer any UI updates during the SUW unstash animation.
     private boolean mDeferUpdatesForSUW;
     private Runnable mDeferredUpdates;
-    private boolean mBindingItems = false;
 
     public TaskbarModelCallbacks(
             TaskbarActivityContext context, TaskbarView container) {
@@ -81,31 +79,25 @@
     }
 
     @Override
-    public void startBinding() {
-        mBindingItems = true;
+    public void bindCompleteModel(IntSparseArrayMap<ItemInfo> itemIdMap,
+            List<FixedContainerItems> extraItems, StringCache stringCache, boolean isBindingSync) {
         mHotseatItems.clear();
         mPredictedItems = Collections.emptyList();
-    }
+        handleItemsAdded(itemIdMap);
 
-    @Override
-    public void finishBindingItems(IntSet pagesBoundFirst) {
-        mBindingItems = false;
+        for (FixedContainerItems item: extraItems) {
+            if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+                mPredictedItems = item.items;
+            } else if (item.containerId == Favorites.CONTAINER_PREDICTION) {
+                mControllers.taskbarAllAppsController.setPredictedApps(item.items);
+            }
+        }
         commitItemsToUI();
     }
 
     @Override
-    public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
-            ArrayList<ItemInfo> addAnimated) {
-        boolean add1 = handleItemsAdded(addNotAnimated);
-        boolean add2 = handleItemsAdded(addAnimated);
-        if (add1 || add2) {
-            commitItemsToUI();
-        }
-    }
-
-    @Override
-    public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
-        if (handleItemsAdded(shortcuts)) {
+    public void bindItemsAdded(List<ItemInfo> items) {
+        if (handleItemsAdded(items)) {
             commitItemsToUI();
         }
     }
@@ -184,10 +176,6 @@
     }
 
     private void commitItemsToUI() {
-        if (mBindingItems) {
-            return;
-        }
-
         ItemInfo[] hotseatItemInfos =
                 new ItemInfo[mContext.getDeviceProfile().numShownHotseatIcons];
         int predictionSize = mPredictedItems.size();
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
index 61d7c77..7be58fb 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -33,6 +33,8 @@
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
 import com.android.launcher3.model.BgDataModel
+import com.android.launcher3.model.BgDataModel.FixedContainerItems
+import com.android.launcher3.model.StringCache
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.TaskItemInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -55,6 +57,7 @@
 import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
 import com.android.launcher3.util.AllModulesForTest
 import com.android.launcher3.util.FakePrefsModule
+import com.android.launcher3.util.IntSparseArrayMap
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.android.launcher3.util.Preconditions.assertNotNull
@@ -566,7 +569,7 @@
         taskbarView.updateItems(hotseatItems, recentAppsController.shownTasks)
         modelCallback.recentAppsController = recentAppsController
         context.baseContext.appComponent.launcherAppState.model.addCallbacksAndLoad(modelCallback)
-        modelCallback.bindItems(hotseatItems.toList(), false)
+        modelCallback.bindItemsAdded(hotseatItems.toList())
         return taskbarView
     }
 
@@ -691,9 +694,16 @@
         var hotseatItems = mutableListOf<WorkspaceItemInfo>()
         var recentAppsController: TaskbarRecentAppsController? = null
 
-        override fun bindItems(shortcuts: List<ItemInfo>, forceAnimateIcons: Boolean) {
+        override fun bindCompleteModel(
+            itemIdMap: IntSparseArrayMap<ItemInfo>,
+            extraItems: MutableList<FixedContainerItems>,
+            stringCache: StringCache,
+            isBindingSync: Boolean,
+        ) = bindItemsAdded(itemIdMap.toList())
+
+        override fun bindItemsAdded(items: List<ItemInfo>) {
             runOnMainSync {
-                shortcuts
+                items
                     .filter { item ->
                         item is WorkspaceItemInfo &&
                             !hotseatItems.any { it.targetPackage == item.targetPackage }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2d77762..0dd3ff2 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -189,7 +189,6 @@
 import com.android.launcher3.dragndrop.LauncherDragController;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
@@ -202,6 +201,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.StringCache;
@@ -230,8 +230,8 @@
 import com.android.launcher3.util.CannedAnimationCoordinator;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ContextTracker;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.KeyboardShortcutsDelegate;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
@@ -285,7 +285,6 @@
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 /**
@@ -372,7 +371,6 @@
 
     private LauncherModel mModel;
     private ModelWriter mModelWriter;
-    private IconCache mIconCache;
     private LauncherAccessibilityDelegate mAccessibilityDelegate;
 
     private PopupDataProvider mPopupDataProvider;
@@ -525,7 +523,6 @@
         initDeviceProfile(idp);
         idp.addOnChangeListener(this);
         mSharedPrefs = LauncherPrefs.getPrefs(this);
-        mIconCache = app.getIconCache();
         mAccessibilityDelegate = createAccessibilityDelegate();
 
         initDragController();
@@ -2200,60 +2197,21 @@
     }
 
     @Override
-    public IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
-        return mModelCallbacks.getPagesToBindSynchronously(orderedScreenIds);
+    public void bindCompleteModelAsync(IntSparseArrayMap<ItemInfo> itemIdMap,
+            List<FixedContainerItems> extraItems, StringCache stringCache, boolean isBindingSync) {
+        mModelCallbacks.bindCompleteModelAsync(itemIdMap, extraItems, stringCache, isBindingSync);
     }
 
     @Override
-    public void startBinding() {
-        mModelCallbacks.startBinding();
+    public void bindItemsAdded(@NonNull List<ItemInfo> items) {
+        mModelCallbacks.bindItemsAdded(items);
     }
 
-    @Override
-    public void bindScreens(IntArray orderedScreenIds) {
-        mModelCallbacks.bindScreens(orderedScreenIds);
-    }
-
-    /**
-     * Remove odd number because they are already included when isTwoPanels and add the pair screen
-     * if not present.
-     */
-    private IntArray filterTwoPanelScreenIds(IntArray orderedScreenIds) {
-        IntSet screenIds = IntSet.wrap(orderedScreenIds);
-        orderedScreenIds.forEach(screenId -> {
-            if (screenId % 2 == 1) {
-                screenIds.remove(screenId);
-                // In case the pair is not added, add it
-                if (!mWorkspace.containsScreenId(screenId - 1)) {
-                    screenIds.add(screenId - 1);
-                }
-            }
-        });
-        return screenIds.getArray();
-    }
-
-    @Override
-    public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
-            ArrayList<ItemInfo> addAnimated) {
-        mModelCallbacks.bindAppsAdded(newScreens, addNotAnimated, addAnimated);
-    }
-
-    /**
-     * Bind the items start-end from the list.
-     *
-     * Implementation of the method from LauncherModel.Callbacks.
-     */
-    @Override
-    public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
-        bindInflatedItems(items.stream()
-                .map(i -> Pair.create(i, getItemInflater().inflateItem(i)))
-                .collect(Collectors.toList()),
-                forceAnimateIcons ? new AnimatorSet() : null);
-    }
-
-    @Override
-    public void bindInflatedItems(List<Pair<ItemInfo, View>> items) {
-        bindInflatedItems(items, null);
+    /** Inflates the binds the provided item using animation */
+    public void inflateAndBindItemWithAnimation(ItemInfo info) {
+        bindInflatedItems(
+                Collections.singletonList(Pair.create(info, getItemInflater().inflateItem(info))),
+                new AnimatorSet());
     }
 
     /**
@@ -2365,6 +2323,9 @@
         return info;
     }
 
+    /** Called when a new LauncherModel data binding is starting */
+    public void startBinding() { }
+
     /**
      * Call back when ModelCallbacks finish binding the Launcher data.
      */
@@ -2379,19 +2340,11 @@
                     .logCardinality(workspaceItemCount)
                     .logEnd(LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC);
         }
-        MAIN_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
+        MAIN_EXECUTOR.getHandler().postAtFrontOfQueue(() ->
             mStartupLatencyLogger
                     .logEnd(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION)
                     .log()
-                    .reset();
-        });
-    }
-
-    @Override
-    public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
-            RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) {
-        mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, onCompleteSignal,
-                workspaceItemCount, isBindSync);
+                    .reset());
         if (mIsColdStartupAfterReboot) {
             Trace.endAsyncSection(COLD_STARTUP_TRACE_METHOD_NAME,
                     COLD_STARTUP_TRACE_COOKIE);
@@ -2404,7 +2357,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void finishBindingItems(IntSet pagesBoundFirst) {
-        mModelCallbacks.finishBindingItems(pagesBoundFirst);
+        TestEventEmitter.sendEvent(TestEvent.WORKSPACE_FINISH_LOADING);
     }
 
     private boolean canAnimatePageChange() {
@@ -3056,7 +3009,6 @@
         return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
     }
 
-    @Override
     @NonNull
     public ItemInflater<Launcher> getItemInflater() {
         return mItemInflater;
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 6d35de5..b3e9905 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -48,7 +48,6 @@
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutRequest
 import com.android.launcher3.util.DaggerSingletonTracker
-import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.PackageUserKey
 import java.io.PrintWriter
@@ -281,9 +280,6 @@
             val bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.isEmpty()
             val callbacksList = if (bindAllCallbacks) callbacks else newCallbacks
             if (callbacksList.isNotEmpty()) {
-                // Clear any pending bind-runnables from the synchronized load process.
-                callbacksList.forEach { MAIN_EXECUTOR.execute(it::clearPendingBinds) }
-
                 val launcherBinder = binderFactory.createBinder(callbacksList)
                 if (bindDirectly) {
                     // Divide the set of loaded items into those that we are binding synchronously,
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index cc5ac49..d0490c7 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -1,25 +1,39 @@
 package com.android.launcher3
 
-import android.annotation.TargetApi
-import android.os.Build
+import android.animation.AnimatorSet
+import android.os.CancellationSignal
 import android.os.Trace
 import android.util.Log
+import android.util.Pair
+import androidx.annotation.AnyThread
 import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.BuildConfig.QSB_ON_FIRST_SCREEN
 import com.android.launcher3.LauncherConstants.TraceEvents
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
 import com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET
 import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
 import com.android.launcher3.allapps.AllAppsStore
 import com.android.launcher3.config.FeatureFlags
-import com.android.launcher3.debug.TestEventEmitter
-import com.android.launcher3.debug.TestEventEmitter.TestEvent
 import com.android.launcher3.model.BgDataModel
+import com.android.launcher3.model.BgDataModel.FixedContainerItems
+import com.android.launcher3.model.ItemInstallQueue
+import com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING
+import com.android.launcher3.model.ModelUtils.WIDGET_FILTER
+import com.android.launcher3.model.ModelUtils.currentScreenContentFilter
 import com.android.launcher3.model.StringCache
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.popup.PopupContainerWithArrow
 import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.IntArray as LIntArray
+import com.android.launcher3.util.IntArray
 import com.android.launcher3.util.IntSet as LIntSet
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.IntSparseArrayMap
 import com.android.launcher3.util.ItemInfoMatcher
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.Preconditions
@@ -28,19 +42,19 @@
 import com.android.launcher3.util.ViewOnDrawExecutor
 import com.android.launcher3.widget.PendingAddWidgetInfo
 import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
 import java.util.function.Predicate
 
-private const val TAG = "ModelCallbacks"
-
 class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks {
 
-    var synchronouslyBoundPages = LIntSet()
-    var pagesToBindSynchronously = LIntSet()
+    private var activeBindTask = AtomicReference(CancellationSignal())
 
+    var synchronouslyBoundPages = IntSet()
+    var pagesToBindSynchronously = IntSet()
     var stringCache: StringCache? = null
 
     var pendingExecutor: ViewOnDrawExecutor? = null
-
     var workspaceLoading = true
 
     /**
@@ -48,7 +62,7 @@
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    override fun startBinding() {
+    private fun startBinding() {
         TraceHelper.INSTANCE.beginSection("startBinding")
         // Floating panels (except the full widget sheet) are associated with individual icons. If
         // we are starting a fresh bind, close all such panels as all the icons are about
@@ -74,11 +88,11 @@
                 " and is setting to ${launcher.deviceProfile.isVerticalBarLayout}",
         )
         launcher.hotseat?.resetLayout(launcher.deviceProfile.isVerticalBarLayout)
+        launcher.startBinding()
         TraceHelper.INSTANCE.endSection()
     }
 
-    @TargetApi(Build.VERSION_CODES.S)
-    override fun onInitialBindComplete(
+    private fun onInitialBindComplete(
         boundPages: LIntSet,
         pendingTasks: RunnableList,
         onCompleteSignal: RunnableList,
@@ -123,7 +137,7 @@
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    override fun finishBindingItems(pagesBoundFirst: LIntSet?) {
+    private fun finishBindingItems(pagesBoundFirst: IntSet?) {
         TraceHelper.INSTANCE.beginSection("finishBindingItems")
         val deviceProfile = launcher.deviceProfile
         launcher.workspace.restoreInstanceStateForRemainingPages()
@@ -137,7 +151,7 @@
         // Since we are just resetting the current page without user interaction,
         // override the previous page so we don't log the page switch.
         launcher.workspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */)
-        pagesToBindSynchronously = LIntSet()
+        pagesToBindSynchronously = IntSet()
 
         // Cache one page worth of icons
         launcher.viewCache.setCacheSize(
@@ -151,14 +165,14 @@
             /*pause=*/ false,
             deviceProfile.isTwoPanels,
         )
-        TestEventEmitter.sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
+        launcher.finishBindingItems(pagesBoundFirst)
     }
 
     /**
      * Clear any pending bind callbacks. This is called when is loader is planning to perform a full
      * rebind from scratch.
      */
-    override fun clearPendingBinds() {
+    fun clearPendingBinds() {
         pendingExecutor?.cancel() ?: return
         pendingExecutor = null
 
@@ -213,7 +227,7 @@
             .filter { workspace.isContainerSupported(it.container) }
             .let {
                 if (it.isNotEmpty()) {
-                    launcher.bindItems(it, false)
+                    bindItems(it, false)
                 }
             }
         workspace.stripEmptyScreens()
@@ -236,7 +250,7 @@
     }
 
     /** Returns the ids of the workspaces to bind. */
-    override fun getPagesToBindSynchronously(orderedScreenIds: LIntArray): LIntSet {
+    private fun getPagesToBindSynchronously(orderedScreenIds: IntArray): IntSet {
         // If workspace binding is still in progress, getCurrentPageScreenIds won't be
         // accurate, and we should use mSynchronouslyBoundPages that's set during initial binding.
         val visibleIds =
@@ -246,7 +260,7 @@
                 else -> synchronouslyBoundPages
             }
         // Launcher IntArray has the same name as Kotlin IntArray
-        val result = LIntSet()
+        val result = IntSet()
         if (visibleIds.isEmpty) {
             return result
         }
@@ -301,7 +315,7 @@
         )
     }
 
-    override fun bindScreens(orderedScreenIds: LIntArray) {
+    private fun bindScreens(orderedScreenIds: LIntArray) {
         launcher.workspace.pageIndicator.setPauseScroll(
             /*pause=*/ true,
             launcher.deviceProfile.isTwoPanels,
@@ -325,26 +339,38 @@
         launcher.workspace.unlockWallpaperFromDefaultPageOnNextLayout()
     }
 
-    override fun bindAppsAdded(
-        newScreens: LIntArray?,
-        addNotAnimated: java.util.ArrayList<ItemInfo?>?,
-        addAnimated: java.util.ArrayList<ItemInfo?>?,
-    ) {
-        // Add the new screens
-        if (newScreens != null) {
-            // newScreens can contain an empty right panel that is already bound, but not known
-            // by BgDataModel.
-            newScreens.removeAllValues(launcher.workspace.mScreenOrder)
-            bindAddScreens(newScreens)
+    override fun bindItemsAdded(items: List<ItemInfo>) {
+        val newScreens = LIntSet()
+        val nonAnimatedItems = mutableListOf<ItemInfo>()
+        val animatedItems = mutableListOf<ItemInfo>()
+        val folderItems = mutableListOf<ItemInfo>()
+        val lastScreen =
+            items
+                .maxByOrNull { if (it.container == CONTAINER_DESKTOP) it.screenId else 0 }
+                ?.screenId ?: 0
+
+        items.forEach {
+            when (it.container) {
+                CONTAINER_HOTSEAT -> nonAnimatedItems.add(it)
+                CONTAINER_DESKTOP -> {
+                    newScreens.add(it.screenId)
+                    if (it.screenId == lastScreen) animatedItems.add(it)
+                    else nonAnimatedItems.add(it)
+                }
+                else -> folderItems.add(it)
+            }
         }
 
+        launcher.workspace.mScreenOrder.forEach { newScreens.remove(it) }
+        if (!newScreens.isEmpty) bindAddScreens(newScreens.array)
+
         // We add the items without animation on non-visible pages, and with
         // animations on the new page (which we will try and snap to).
-        if (!addNotAnimated.isNullOrEmpty()) {
-            launcher.bindItems(addNotAnimated, false)
+        if (nonAnimatedItems.isNotEmpty()) {
+            bindItems(nonAnimatedItems, false)
         }
-        if (!addAnimated.isNullOrEmpty()) {
-            launcher.bindItems(addAnimated, true)
+        if (animatedItems.isNotEmpty()) {
+            bindItems(animatedItems, true)
         }
 
         // Remove the extra empty screen
@@ -398,5 +424,188 @@
         launcher.appsView.updateWorkUI()
     }
 
-    override fun getItemInflater() = launcher.itemInflater
+    /** Bind the items start-end from the list. */
+    @VisibleForTesting
+    fun bindItems(items: List<ItemInfo>, forceAnimateIcons: Boolean) {
+        launcher.bindInflatedItems(
+            items.map { Pair.create(it, launcher.itemInflater.inflateItem(it)) },
+            if (forceAnimateIcons) AnimatorSet() else null,
+        )
+    }
+
+    @AnyThread
+    override fun bindCompleteModelAsync(
+        itemIdMap: IntSparseArrayMap<ItemInfo>,
+        extraItems: List<FixedContainerItems?>,
+        stringCache: StringCache,
+        isBindingSync: Boolean,
+    ) {
+        val taskTracker = CancellationSignal()
+        activeBindTask.getAndSet(taskTracker).cancel()
+
+        val inflater = launcher.itemInflater
+
+        fun executeCallbacksTask(executor: Executor = MAIN_EXECUTOR, task: () -> Unit) {
+            executor.execute {
+                if (taskTracker.isCanceled) {
+                    Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind")
+                } else {
+                    task.invoke()
+                }
+            }
+        }
+
+        // Tries to inflate the items asynchronously and bind. Returns true on success or false if
+        // async-binding is not supported in this case
+        fun inflateAsyncAndBind(items: List<ItemInfo>, executor: Executor) {
+            if (taskTracker.isCanceled) {
+                Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation")
+                return
+            }
+
+            val bindItems = items.map { Pair.create(it, inflater.inflateItem(it, null)) }
+            if (bindItems.isNotEmpty())
+                executeCallbacksTask(executor) { launcher.bindInflatedItems(bindItems, null) }
+        }
+
+        fun bindItemsInChunks(items: List<ItemInfo>, chuckSize: Int, executor: Executor) {
+            // Bind the workspace items
+            val itemCount = items.size
+            var i = 0
+            while (i < itemCount) {
+                val start = i
+                val end = (start + chuckSize).coerceAtMost(itemCount)
+                executeCallbacksTask(executor) { bindItems(items.subList(start, end), false) }
+                i = end
+            }
+        }
+
+        MAIN_EXECUTOR.execute { clearPendingBinds() }
+
+        val orderedScreenIds =
+            IntSet()
+                .apply {
+                    itemIdMap.forEach { if (it.container == CONTAINER_DESKTOP) add(it.screenId) }
+                    if ((QSB_ON_FIRST_SCREEN && !SHOULD_SHOW_FIRST_PAGE_WIDGET) || isEmpty)
+                        add(Workspace.FIRST_SCREEN_ID)
+                }
+                .array
+        val currentScreenIds = getPagesToBindSynchronously(orderedScreenIds)
+
+        fun setupPendingBind(pendingExecutor: Executor) {
+            executeCallbacksTask(pendingExecutor) { launcher.bindStringCache(stringCache) }
+            executeCallbacksTask(pendingExecutor) { finishBindingItems(currentScreenIds) }
+            pendingExecutor.execute {
+                ItemInstallQueue.INSTANCE[launcher].resumeModelPush(FLAG_LOADER_RUNNING)
+            }
+        }
+
+        // Separate the items that are on the current screen, and all the other remaining items
+        val currentWorkspaceItems = ArrayList<ItemInfo>()
+        val otherWorkspaceItems = ArrayList<ItemInfo>()
+        val currentAppWidgets = ArrayList<ItemInfo>()
+        val otherAppWidgets = ArrayList<ItemInfo>()
+
+        val currentScreenCheck = currentScreenContentFilter(currentScreenIds)
+        itemIdMap.forEach { item: ItemInfo ->
+            if (currentScreenCheck.test(item)) {
+                (if (WIDGET_FILTER.test(item)) currentAppWidgets else currentWorkspaceItems).add(
+                    item
+                )
+            } else if (item.container == CONTAINER_DESKTOP) {
+                (if (WIDGET_FILTER.test(item)) otherAppWidgets else otherWorkspaceItems).add(item)
+            }
+        }
+
+        sortWorkspaceItemsSpatially(currentWorkspaceItems)
+        sortWorkspaceItemsSpatially(otherWorkspaceItems)
+
+        // Tell the workspace that we're about to start binding items
+        executeCallbacksTask {
+            clearPendingBinds()
+            startBinding()
+        }
+
+        // Bind workspace screens
+        executeCallbacksTask { bindScreens(orderedScreenIds) }
+
+        // Load items on the current page.
+        if (Flags.enableWorkspaceInflation()) {
+            inflateAsyncAndBind(currentWorkspaceItems, MAIN_EXECUTOR)
+            inflateAsyncAndBind(currentAppWidgets, MAIN_EXECUTOR)
+        } else {
+            bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, MAIN_EXECUTOR)
+            bindItemsInChunks(currentAppWidgets, 1, MAIN_EXECUTOR)
+        }
+        extraItems.forEach {
+            it?.let { executeCallbacksTask { launcher.bindExtraContainerItems(it) } }
+        }
+
+        val pendingTasks = RunnableList()
+        val pendingExecutor = Executor { pendingTasks.add(it) }
+
+        val onCompleteSignal = RunnableList()
+        onCompleteSignal.add { Log.d(TAG, "Calling onCompleteSignal") }
+
+        if (Flags.enableWorkspaceInflation()) {
+            Log.d(TAG, "Starting async inflation")
+            Executors.MODEL_EXECUTOR.execute {
+                inflateAsyncAndBind(otherWorkspaceItems, pendingExecutor)
+                inflateAsyncAndBind(otherAppWidgets, pendingExecutor)
+                setupPendingBind(pendingExecutor)
+
+                // Wait for the async inflation to complete and then notify the completion
+                // signal on UI thread.
+                MAIN_EXECUTOR.execute { onCompleteSignal.executeAllAndDestroy() }
+            }
+        } else {
+            Log.d(TAG, "Starting sync inflation")
+            bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor)
+            bindItemsInChunks(otherAppWidgets, 1, pendingExecutor)
+            setupPendingBind(pendingExecutor)
+            onCompleteSignal.executeAllAndDestroy()
+        }
+
+        val workspaceItemCount = itemIdMap.size()
+        executeCallbacksTask {
+            onInitialBindComplete(
+                currentScreenIds,
+                pendingTasks,
+                onCompleteSignal,
+                workspaceItemCount,
+                isBindingSync,
+            )
+        }
+    }
+
+    /**
+     * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right)
+     */
+    private fun sortWorkspaceItemsSpatially(workspaceItems: MutableList<ItemInfo>) {
+        val idp = launcher.deviceProfile.inv
+        val screenCols = idp.numColumns
+        val screenCellCount = idp.numColumns * idp.numRows
+        workspaceItems.sortWith { lhs: ItemInfo, rhs: ItemInfo ->
+            when {
+                // Between containers, order by hotseat, desktop
+                lhs.container != rhs.container -> lhs.container.compareTo(rhs.container)
+
+                // Within workspace, order by their spatial position in that container
+                lhs.container == CONTAINER_DESKTOP ->
+                    compareValuesBy(lhs, rhs) {
+                        it.screenId * screenCellCount + it.cellY * screenCols + it.cellX
+                    }
+
+                // We currently use the screen id as the rank
+                lhs.container == CONTAINER_HOTSEAT -> lhs.screenId.compareTo(rhs.screenId)
+
+                else -> 0
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "ModelCallbacks"
+        private const val ITEMS_CHUNK: Int = 6 // batch size for the workspace icons
+    }
 }
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index e16a296..fd596bd 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -536,7 +536,7 @@
         // Bind the item in next frame so that if a new workspace page was created,
         // it will get laid out.
         new Handler().post(() -> {
-            mContext.bindItems(Collections.singletonList(item), true);
+            mContext.inflateAndBindItemWithAnimation(item);
             announceConfirmation(R.string.item_moved);
         });
         return true;
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index c91e783..f71d505 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -28,7 +28,6 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -64,7 +63,7 @@
                 mContext.getModelWriter().addItemToDatabase(info,
                         LauncherSettings.Favorites.CONTAINER_DESKTOP,
                         screenId, coordinates[0], coordinates[1]);
-                mContext.bindItems(Collections.singletonList(info), true);
+                mContext.inflateAndBindItemWithAnimation(info);
                 AbstractFloatingView.closeAllOpenViews(mContext);
             }));
             return true;
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index dfba4bb..abcc0cb 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -28,12 +28,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -207,26 +205,7 @@
         }
 
         if (!addedItemsFinal.isEmpty()) {
-            taskController.scheduleCallbackTask(new CallbackTask() {
-                @Override
-                public void execute(@NonNull Callbacks callbacks) {
-                    final ArrayList<ItemInfo> addAnimated = new ArrayList<>();
-                    final ArrayList<ItemInfo> addNotAnimated = new ArrayList<>();
-                    if (!addedItemsFinal.isEmpty()) {
-                        ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1);
-                        int lastScreenId = info.screenId;
-                        for (ItemInfo i : addedItemsFinal) {
-                            if (i.screenId == lastScreenId) {
-                                addAnimated.add(i);
-                            } else {
-                                addNotAnimated.add(i);
-                            }
-                        }
-                    }
-                    callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
-                            addNotAnimated, addAnimated);
-                }
-            });
+            taskController.scheduleCallbackTask(cb -> cb.bindItemsAdded(addedItemsFinal));
         }
     }
 
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 5b95e37..dc4b6f0 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -17,41 +17,24 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
-import static com.android.launcher3.Flags.enableWorkspaceInflation;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
-import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
-import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
-import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.content.Context;
 import android.os.Trace;
 import android.util.Log;
-import android.util.Pair;
-import android.view.View;
 
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
-import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
@@ -61,13 +44,10 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -76,12 +56,10 @@
 public class BaseLauncherBinder {
 
     protected static final String TAG = "LauncherBinder";
-    private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
 
     protected final LooperExecutor mUiExecutor;
 
     private final Context mContext;
-    private final InvariantDeviceProfile mIDP;
     private final LauncherModel mModel;
     protected final BgDataModel mBgDataModel;
     private final AllAppsList mBgAllAppsList;
@@ -93,14 +71,12 @@
     @AssistedInject
     public BaseLauncherBinder(
             @ApplicationContext Context context,
-            InvariantDeviceProfile idp,
             LauncherModel model,
             BgDataModel dataModel,
             AllAppsList allAppsList,
             @Assisted Callbacks[] callbacksList) {
         mUiExecutor = MAIN_EXECUTOR;
         mContext = context;
-        mIDP = idp;
         mModel = model;
         mBgDataModel = dataModel;
         mBgAllAppsList = allAppsList;
@@ -115,24 +91,22 @@
         try {
             // Save a copy of all the bg-thread collections
             IntSparseArrayMap<ItemInfo> itemsIdMap;
-            final IntArray orderedScreenIds = new IntArray();
             ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
-            final int workspaceItemCount;
+            StringCache stringCache;
+
             synchronized (mBgDataModel) {
                 itemsIdMap = mBgDataModel.itemsIdMap.clone();
-                orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
                 mBgDataModel.extraItems.forEach(extraItems::add);
                 if (incrementBindId) {
                     mBgDataModel.lastBindId++;
                     mBgDataModel.lastLoadId = mModel.getLastLoadId();
                 }
                 mMyBindingId = mBgDataModel.lastBindId;
-                workspaceItemCount = mBgDataModel.itemsIdMap.size();
+                stringCache = mBgDataModel.stringCache.clone();
             }
 
             for (Callbacks cb : mCallbacksList) {
-                new UnifiedWorkspaceBinder(cb, itemsIdMap, extraItems, orderedScreenIds)
-                        .bind(isBindSync, workspaceItemCount);
+                cb.bindCompleteModelAsync(itemsIdMap, extraItems, stringCache, isBindSync);
             }
         } finally {
             Trace.endSection();
@@ -190,41 +164,6 @@
         executeCallbacksTask(c -> c.bindSmartspaceWidget(), mUiExecutor);
     }
 
-    /**
-     * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right)
-     */
-    protected void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) {
-        final int screenCols = mIDP.numColumns;
-        final int screenCellCount = mIDP.numColumns * mIDP.numRows;
-        Collections.sort(workspaceItems, (lhs, rhs) -> {
-            if (lhs.container == rhs.container) {
-                // Within containers, order by their spatial position in that container
-                switch (lhs.container) {
-                    case LauncherSettings.Favorites.CONTAINER_DESKTOP: {
-                        int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols
-                                + lhs.cellX);
-                        int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols
-                                + rhs.cellX);
-                        return Integer.compare(lr, rr);
-                    }
-                    case LauncherSettings.Favorites.CONTAINER_HOTSEAT: {
-                        // We currently use the screen id as the rank
-                        return Integer.compare(lhs.screenId, rhs.screenId);
-                    }
-                    default:
-                        if (FeatureFlags.IS_STUDIO_BUILD) {
-                            throw new RuntimeException(
-                                    "Unexpected container type when sorting workspace items.");
-                        }
-                        return 0;
-                }
-            } else {
-                // Between containers, order by hotseat, desktop
-                return Integer.compare(lhs.container, rhs.container);
-            }
-        });
-    }
-
     protected void executeCallbacksTask(CallbackTask task, Executor executor) {
         executor.execute(() -> {
             if (mMyBindingId != mBgDataModel.lastBindId) {
@@ -248,152 +187,6 @@
         }
         return idleLock;
     }
-
-    private class UnifiedWorkspaceBinder {
-
-        private final Callbacks mCallbacks;
-
-        private final IntSparseArrayMap<ItemInfo> mItemIdMap;
-        private final IntArray mOrderedScreenIds;
-        private final ArrayList<FixedContainerItems> mExtraItems;
-
-        UnifiedWorkspaceBinder(
-                Callbacks callbacks,
-                IntSparseArrayMap<ItemInfo> itemIdMap,
-                ArrayList<FixedContainerItems> extraItems,
-                IntArray orderedScreenIds) {
-            mCallbacks = callbacks;
-            mItemIdMap = itemIdMap;
-            mExtraItems = extraItems;
-            mOrderedScreenIds = orderedScreenIds;
-        }
-
-        private void bind(boolean isBindSync, int workspaceItemCount) {
-            final IntSet currentScreenIds =
-                    mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
-            Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks);
-
-            // Separate the items that are on the current screen, and all the other remaining items
-            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
-            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
-            ArrayList<ItemInfo> currentAppWidgets = new ArrayList<>();
-            ArrayList<ItemInfo> otherAppWidgets = new ArrayList<>();
-
-            Predicate<ItemInfo> currentScreenCheck = currentScreenContentFilter(currentScreenIds);
-            mItemIdMap.forEach(item -> {
-                if (currentScreenCheck.test(item)) {
-                    (WIDGET_FILTER.test(item) ? currentAppWidgets : currentWorkspaceItems)
-                            .add(item);
-                } else if (item.container == CONTAINER_DESKTOP) {
-                    (WIDGET_FILTER.test(item) ? otherAppWidgets : otherWorkspaceItems).add(item);
-                }
-            });
-            sortWorkspaceItemsSpatially(currentWorkspaceItems);
-            sortWorkspaceItemsSpatially(otherWorkspaceItems);
-
-            // Tell the workspace that we're about to start binding items
-            executeCallbacksTask(c -> {
-                c.clearPendingBinds();
-                c.startBinding();
-            }, mUiExecutor);
-
-            // Bind workspace screens
-            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
-
-            ItemInflater inflater = mCallbacks.getItemInflater();
-
-            // Load items on the current page.
-            if (enableWorkspaceInflation() && inflater != null) {
-                inflateAsyncAndBind(currentWorkspaceItems, inflater, mUiExecutor);
-                inflateAsyncAndBind(currentAppWidgets, inflater, mUiExecutor);
-            } else {
-                bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
-                bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
-            }
-            mExtraItems.forEach(item ->
-                    executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
-
-            RunnableList pendingTasks = new RunnableList();
-            Executor pendingExecutor = pendingTasks::add;
-
-            RunnableList onCompleteSignal = new RunnableList();
-            onCompleteSignal.add(() -> Log.d(TAG, "Calling onCompleteSignal"));
-
-            if (enableWorkspaceInflation() && inflater != null) {
-                Log.d(TAG, "Starting async inflation");
-                MODEL_EXECUTOR.execute(() ->  {
-                    inflateAsyncAndBind(otherWorkspaceItems, inflater, pendingExecutor);
-                    inflateAsyncAndBind(otherAppWidgets, inflater, pendingExecutor);
-                    setupPendingBind(currentScreenIds, pendingExecutor);
-
-                    // Wait for the async inflation to complete and then notify the completion
-                    // signal on UI thread.
-                    MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy);
-                });
-            } else {
-                Log.d(TAG, "Starting sync inflation");
-                bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
-                bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
-                setupPendingBind(currentScreenIds, pendingExecutor);
-                onCompleteSignal.executeAllAndDestroy();
-            }
-
-            executeCallbacksTask(c -> c.onInitialBindComplete(currentScreenIds, pendingTasks,
-                    onCompleteSignal, workspaceItemCount, isBindSync), mUiExecutor);
-        }
-
-        private void setupPendingBind(
-                IntSet currentScreenIds,
-                Executor pendingExecutor) {
-            StringCache cacheClone = mBgDataModel.stringCache.clone();
-            executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
-
-            executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);
-            pendingExecutor.execute(() -> ItemInstallQueue.INSTANCE.get(mContext)
-                    .resumeModelPush(FLAG_LOADER_RUNNING));
-        }
-
-        /**
-         * Tries to inflate the items asynchronously and bind. Returns true on success or false if
-         * async-binding is not supported in this case.
-         */
-        private void inflateAsyncAndBind(
-                List<ItemInfo> items, @NonNull ItemInflater inflater, Executor executor) {
-            if (mMyBindingId != mBgDataModel.lastBindId) {
-                Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation");
-                return;
-            }
-
-            List<Pair<ItemInfo, View>> bindItems = items.stream()
-                    .map(i -> Pair.create(i, inflater.inflateItem(i, null)))
-                    .collect(Collectors.toList());
-            executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
-        }
-
-        private void bindItemsInChunks(
-                List<ItemInfo> workspaceItems, int chunkCount, Executor executor) {
-            // Bind the workspace items
-            int count = workspaceItems.size();
-            for (int i = 0; i < count; i += chunkCount) {
-                final int start = i;
-                final int chunkSize = (i + chunkCount <= count) ? chunkCount : (count - i);
-                executeCallbacksTask(
-                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                        executor);
-            }
-        }
-
-        protected void executeCallbacksTask(CallbackTask task, Executor executor) {
-            executor.execute(() -> {
-                if (mMyBindingId != mBgDataModel.lastBindId) {
-                    Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
-                    return;
-                }
-                task.execute(mCallbacks);
-            });
-        }
-    }
-
     @AssistedFactory
     public interface BaseLauncherBinderFactory {
         BaseLauncherBinder createBinder(Callbacks[] callbacks);
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 9189eb8..5e0f8a0 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
 import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.mapping;
@@ -35,9 +36,8 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.Pair;
-import android.view.View;
 
+import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -66,9 +66,7 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
-import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.io.PrintWriter;
@@ -439,41 +437,31 @@
         int FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED = 1 << 4;
 
         /**
-         * Returns an IntSet of page ids to bind first, synchronously if possible
-         * or an empty IntSet
-         * @param orderedScreenIds All the page ids to be bound
+         * Does a complete model rebind. The callback can be called on any thread and it is up to
+         * the client to move the executor to appropriate thread
          */
-        @NonNull
-        default IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
-            return new IntSet();
+        @AnyThread
+        default void bindCompleteModelAsync(IntSparseArrayMap<ItemInfo> itemIdMap,
+                List<FixedContainerItems> extraItems, StringCache stringCache,
+                boolean isBindingSync) {
+            MAIN_EXECUTOR.execute(
+                    () -> bindCompleteModel(itemIdMap, extraItems, stringCache, isBindingSync));
         }
 
-        default void clearPendingBinds() { }
-        default void startBinding() { }
+        default void bindCompleteModel(IntSparseArrayMap<ItemInfo> itemIdMap,
+                List<FixedContainerItems> extraItems, StringCache stringCache,
+                boolean isBindingSync) { }
 
-        @Nullable
-        default ItemInflater getItemInflater() {
-            return null;
-        }
-
-        default void bindItems(@NonNull List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
-        /** Alternate method to bind preinflated views */
-        default void bindInflatedItems(@NonNull List<Pair<ItemInfo, View>> items) { }
-
-        default void bindScreens(IntArray orderedScreenIds) { }
-        default void finishBindingItems(IntSet pagesBoundFirst) { }
-        default void bindAppsAdded(IntArray newScreens,
-                ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated) { }
+        default void bindItemsAdded(@NonNull List<ItemInfo> items) { }
+        /** Called when a runtime property of the ItemInfo is updated due to some system event */
+        default void bindItemsUpdated(Set<ItemInfo> updates) { }
+        default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
 
         /**
          * Binds updated incremental download progress
          */
         default void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
 
-        /** Called when a runtime property of the ItemInfo is updated due to some system event */
-        default void bindItemsUpdated(Set<ItemInfo> updates) { }
-        default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
-
         /**
          * Binds the app widgets to the providers that share widgets with the UI.
          */
@@ -481,14 +469,6 @@
 
         default void bindSmartspaceWidget() { }
 
-        /** Called when workspace has been bound. */
-        default void onInitialBindComplete(@NonNull IntSet boundPages,
-                @NonNull RunnableList pendingTasks,
-                @NonNull RunnableList onCompleteSignal,
-                int workspaceItemCount, boolean isBindSync) {
-            pendingTasks.executeAllAndDestroy();
-        }
-
         default void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
 
         /**
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index 01e1730..d591a75 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -242,7 +242,7 @@
         updateItemInfoProps(item, container, screenId, cellX, cellY);
 
         item.id = mModel.getModelDbController().generateNewItemId();
-        notifyOtherCallbacks(c -> c.bindItems(Collections.singletonList(item), false));
+        notifyOtherCallbacks(c -> c.bindItemsAdded(Collections.singletonList(item)));
 
         ModelVerifier verifier = new ModelVerifier();
         final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 54efccd..8c0f837 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -235,11 +235,6 @@
     }
 
     @Override
-    public void startBinding() {
-        mDragController.cancelDrag();
-    }
-
-    @Override
     public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) {
         mPopupDataProvider.setDeepShortcutMap(deepShortcutMap);
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index 6d366db..d19048d 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -61,8 +61,7 @@
         val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)
 
         assertThat(addedItems.size).isEqualTo(1)
-        assertThat(addedItems.first().itemInfo.screenId).isEqualTo(1)
-        assertThat(addedItems.first().isAnimated).isTrue()
+        assertThat(addedItems.first().screenId).isEqualTo(1)
         verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1)
     }
 
@@ -75,8 +74,7 @@
         val addedItems = testAddItems(nonEmptyScreenIds, *itemsToAdd)
 
         assertThat(addedItems.size).isEqualTo(1)
-        assertThat(addedItems.first().itemInfo.screenId).isEqualTo(1)
-        assertThat(addedItems.first().isAnimated).isTrue()
+        assertThat(addedItems.first().screenId).isEqualTo(1)
         verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1)
     }
 
@@ -102,8 +100,7 @@
         val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)
 
         assertThat(addedItems.size).isEqualTo(1)
-        assertThat(addedItems.first().itemInfo.screenId).isEqualTo(2)
-        assertThat(addedItems.first().isAnimated).isTrue()
+        assertThat(addedItems.first().screenId).isEqualTo(2)
         verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 1)
     }
 
@@ -120,14 +117,12 @@
         assertThat(addedItems.size).isEqualTo(3)
 
         // Items that are added to the first screen should not be animated
-        val itemsAddedToFirstScreen = addedItems.filter { it.itemInfo.screenId == 1 }
+        val itemsAddedToFirstScreen = addedItems.filter { it.screenId == 1 }
         assertThat(itemsAddedToFirstScreen.size).isEqualTo(1)
-        assertThat(itemsAddedToFirstScreen.first().isAnimated).isFalse()
 
         // Items that are added to the second screen should be animated
-        val itemsAddedToSecondScreen = addedItems.filter { it.itemInfo.screenId == 2 }
+        val itemsAddedToSecondScreen = addedItems.filter { it.screenId == 2 }
         assertThat(itemsAddedToSecondScreen.size).isEqualTo(2)
-        itemsAddedToSecondScreen.forEach { assertThat(it.isAnimated).isTrue() }
         verifyItemSpaceFinderCall(nonEmptyScreenIds, numberOfExpectedCall = 3)
     }
 
@@ -160,11 +155,11 @@
     private fun testAddItems(
         nonEmptyScreenIds: List<Int>,
         vararg itemsToAdd: WorkspaceItemInfo,
-    ): List<AddedItem> {
+    ): List<ItemInfo> {
         setupWorkspaces(nonEmptyScreenIds)
         val task = newTask(*itemsToAdd)
 
-        val addedItems = mutableListOf<AddedItem>()
+        val addedItems = mutableListOf<ItemInfo>()
 
         runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             mDataModelCallbacks.addedItems.clear()
@@ -187,18 +182,11 @@
             .let { AddWorkspaceItemsTask(it, mWorkspaceItemSpaceFinder) }
 }
 
-private data class AddedItem(val itemInfo: ItemInfo, val isAnimated: Boolean)
-
 private class MyCallbacks : BgDataModel.Callbacks {
 
-    val addedItems = mutableListOf<AddedItem>()
+    val addedItems = mutableListOf<ItemInfo>()
 
-    override fun bindAppsAdded(
-        newScreens: IntArray?,
-        addNotAnimated: ArrayList<ItemInfo>,
-        addAnimated: ArrayList<ItemInfo>,
-    ) {
-        addedItems.addAll(addAnimated.map { AddedItem(it, true) })
-        addedItems.addAll(addNotAnimated.map { AddedItem(it, false) })
+    override fun bindItemsAdded(items: List<ItemInfo>) {
+        addedItems.addAll(items)
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
index 9f55f39..61d96ba 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
@@ -17,45 +17,52 @@
 package com.android.launcher3.model
 
 import android.os.Looper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
-import android.util.Pair
 import android.util.SparseArray
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.Launcher
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherModel
-import com.android.launcher3.model.BgDataModel.Callbacks
+import com.android.launcher3.ModelCallbacks
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.pageindicators.PageIndicatorDots
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
-import com.android.launcher3.util.IntArray
 import com.android.launcher3.util.IntSet
 import com.android.launcher3.util.ItemInflater
 import com.android.launcher3.util.LauncherLayoutBuilder
 import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE
 import com.android.launcher3.util.ModelTestExtensions.loadModelSync
-import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.TestUtil.runOnExecutorSync
 import com.android.launcher3.util.rule.LayoutProviderRule
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Answers
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
-import org.mockito.Spy
 import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.argThat
 import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.isNull
 import org.mockito.kotlin.never
-import org.mockito.kotlin.reset
+import org.mockito.kotlin.spy
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -69,8 +76,13 @@
     @get:Rule val context = SandboxApplication().withModelDependency()
     @get:Rule val layoutProvider = LayoutProviderRule(context)
 
-    @Spy private var callbacks = MyCallbacks()
     @Mock private lateinit var itemInflater: ItemInflater<*>
+    // PageIndicatorDots need to be mocked separately as Workspace uses generics and doesn't define
+    // the actual class of PageIndicator being used
+    @Mock private lateinit var pageIndicatorDots: PageIndicatorDots
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var launcher: Launcher
+
+    private lateinit var callbacks: ModelCallbacks
 
     private val inflationLooper = SparseArray<Looper>()
 
@@ -79,7 +91,6 @@
 
     @Before
     fun setUp() {
-        setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION)
         MockitoAnnotations.initMocks(this)
 
         doAnswer { i ->
@@ -89,6 +100,13 @@
             .whenever(itemInflater)
             .inflateItem(any(), isNull())
 
+        doReturn(itemInflater).whenever(launcher).itemInflater
+        doReturn(InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context))
+            .whenever(launcher)
+            .deviceProfile
+        launcher.workspace.apply { doReturn(pageIndicatorDots).whenever(this).getPageIndicator() }
+        doReturn(context).whenever(launcher).applicationContext
+
         // Set up the workspace with 3 pages of apps
         layoutProvider.setupDefaultLayoutProvider(
             LauncherLayoutBuilder()
@@ -103,30 +121,29 @@
                 .atWorkspace(0, 1, 2)
                 .putApp(TEST_PACKAGE, TEST_PACKAGE)
         )
+        callbacks =
+            spy(ModelCallbacks(launcher).apply { pagesToBindSynchronously = IntSet.wrap(0) })
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION)
     fun test_bind_normally_without_itemInflater() {
         MAIN_EXECUTOR.execute { model.addCallbacksAndLoad(callbacks) }
         waitForLoaderAndTempMainThread()
 
-        verify(callbacks, never()).bindInflatedItems(any())
         verify(callbacks, atLeastOnce()).bindItems(any(), any())
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION)
     fun test_bind_inflates_item_on_background() {
-        callbacks.inflater = itemInflater
         MAIN_EXECUTOR.execute { model.addCallbacksAndLoad(callbacks) }
         waitForLoaderAndTempMainThread()
 
         verify(callbacks, never()).bindItems(any(), any())
-        verify(callbacks, times(1)).bindInflatedItems(argThat { t -> t.size == 2 })
-
-        // Verify remaining items are bound using pendingTasks
-        reset(callbacks)
-        MAIN_EXECUTOR.submit(callbacks.pendingTasks!!::executeAllAndDestroy).get()
-        verify(callbacks, times(1)).bindInflatedItems(argThat { t -> t.size == 3 })
+        // First 2 items were bound and eventually remaining items were bound
+        verify(launcher, times(1)).bindInflatedItems(argThat { size == 2 }, anyOrNull())
+        verify(launcher, times(1)).bindInflatedItems(argThat { size == 3 }, anyOrNull())
 
         // Verify that all items were inflated on the background thread
         assertEquals(5, inflationLooper.size())
@@ -134,31 +151,34 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION)
     fun test_bind_sync_partially_inflates_on_background() {
         model.loadModelSync()
         assertTrue(model.isModelLoaded())
-        callbacks.inflater = itemInflater
 
-        val firstPageBindIds = IntSet()
+        val firstPageBindIds = mutableSetOf<Int>()
+        runOnExecutorSync(MAIN_EXECUTOR) {
+            model.addCallbacksAndLoad(callbacks)
+            verify(callbacks, never()).bindItems(any(), any())
+            verify(launcher, times(1))
+                .bindInflatedItems(
+                    argThat {
+                        firstPageBindIds.addAll(map { it.first.id })
+                        size == 2
+                    },
+                    anyOrNull(),
+                )
 
-        MAIN_EXECUTOR.submit {
-                model.addCallbacksAndLoad(callbacks)
-                verify(callbacks, never()).bindItems(any(), any())
-                verify(callbacks, times(1))
-                    .bindInflatedItems(
-                        argThat { t ->
-                            t.forEach { firstPageBindIds.add(it.first.id) }
-                            t.size == 2
-                        }
-                    )
-
-                // Verify that onInitialBindComplete is called and the binding is not yet complete
-                assertFalse(callbacks.onCompleteSignal!!.isDestroyed)
-            }
-            .get()
+            // Verify that onInitialBindComplete is called and the binding is not yet complete
+            assertNotNull(callbacks.pendingExecutor)
+            clearInvocations(launcher)
+        }
 
         waitForLoaderAndTempMainThread()
-        assertTrue(callbacks.onCompleteSignal!!.isDestroyed)
+
+        // Verify remaining 3 times are bound using pending tasks
+        assertNull(callbacks.pendingExecutor)
+        verify(launcher, times(1)).bindInflatedItems(argThat { t -> t.size == 3 }, anyOrNull())
 
         // Verify that firstPageBindIds are loaded on the main thread and remaining
         // on the background thread.
@@ -168,45 +188,12 @@
                 assertEquals(MAIN_EXECUTOR.looper, inflationLooper.valueAt(i))
             else assertEquals(MODEL_EXECUTOR.looper, inflationLooper.valueAt(i))
         }
-
-        MAIN_EXECUTOR.submit {
-                reset(callbacks)
-                callbacks.pendingTasks!!.executeAllAndDestroy()
-                // Verify remaining 3 times are bound using pending tasks
-                verify(callbacks, times(1)).bindInflatedItems(argThat { t -> t.size == 3 })
-            }
-            .get()
     }
 
     private fun waitForLoaderAndTempMainThread() {
-        MAIN_EXECUTOR.submit {}.get()
-        MODEL_EXECUTOR.submit {}.get()
-        MAIN_EXECUTOR.submit {}.get()
-    }
-
-    class MyCallbacks : Callbacks {
-
-        var inflater: ItemInflater<*>? = null
-        var pendingTasks: RunnableList? = null
-        var onCompleteSignal: RunnableList? = null
-
-        override fun bindItems(shortcuts: MutableList<ItemInfo>, forceAnimateIcons: Boolean) {}
-
-        override fun bindInflatedItems(items: MutableList<Pair<ItemInfo, View>>) {}
-
-        override fun getPagesToBindSynchronously(orderedScreenIds: IntArray?) = IntSet.wrap(0)
-
-        override fun onInitialBindComplete(
-            boundPages: IntSet,
-            pendingTasks: RunnableList,
-            onCompleteSignal: RunnableList,
-            workspaceItemCount: Int,
-            isBindSync: Boolean,
-        ) {
-            this.pendingTasks = pendingTasks
-            this.onCompleteSignal = onCompleteSignal
+        repeat(5) {
+            runOnExecutorSync(MAIN_EXECUTOR) {}
+            runOnExecutorSync(MODEL_EXECUTOR) {}
         }
-
-        override fun getItemInflater() = inflater
     }
 }
diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index 3115899..d64049d 100644
--- a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -32,14 +32,13 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.LayoutProviderRule;
@@ -49,7 +48,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -83,17 +81,16 @@
         Executors.MAIN_EXECUTOR.execute(() -> getModel().addCallbacksAndLoad(cb1));
 
         waitForLoaderAndTempMainThread();
-        cb1.verifySynchronouslyBound(3);
+        cb1.verifyItemsBound(3);
 
         // Add a new callback
         cb1.reset();
         MyCallbacks cb2 = spy(MyCallbacks.class);
-        cb2.mPageToBindSync = IntSet.wrap(2);
         Executors.MAIN_EXECUTOR.execute(() -> getModel().addCallbacksAndLoad(cb2));
 
         waitForLoaderAndTempMainThread();
-        assertFalse(cb1.bindStarted);
-        cb2.verifySynchronouslyBound(3);
+        assertNull(cb1.mItems);
+        cb2.verifyItemsBound(3);
 
         // Remove callbacks
         cb1.reset();
@@ -102,14 +99,14 @@
         // No effect on callbacks when removing an callback
         Executors.MAIN_EXECUTOR.execute(() -> getModel().removeCallbacks(cb2));
         waitForLoaderAndTempMainThread();
-        assertNull(cb1.mPendingTasks);
-        assertNull(cb2.mPendingTasks);
+        assertNull(cb1.mItems);
+        assertNull(cb2.mItems);
 
         // Reloading only loads registered callbacks
         getModel().startLoader();
         waitForLoaderAndTempMainThread();
-        cb1.verifySynchronouslyBound(3);
-        assertNull(cb2.mPendingTasks);
+        cb1.verifyItemsBound(3);
+        assertNull(cb2.mItems);
     }
 
     @Test
@@ -173,30 +170,16 @@
 
     private abstract static class MyCallbacks implements Callbacks {
 
-        final List<ItemInfo> mItems = new ArrayList<>();
-        IntSet mPageToBindSync = IntSet.wrap(0);
-        IntSet mPageBoundSync = new IntSet();
-        RunnableList mPendingTasks;
+        List<ItemInfo> mItems = null;
         AppInfo[] mAppInfos;
-        boolean bindStarted;
 
         MyCallbacks() { }
 
         @Override
-        public void startBinding() {
-            bindStarted = true;
-        }
-
-        @Override
-        public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
-                RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) {
-            mPageBoundSync = boundPages;
-            mPendingTasks = pendingTasks;
-        }
-
-        @Override
-        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
-            mItems.addAll(shortcuts);
+        public void bindCompleteModel(IntSparseArrayMap<ItemInfo> itemIdMap,
+                List<FixedContainerItems> extraItems, StringCache stringCache,
+                boolean isBindingSync) {
+            mItems = itemIdMap.stream().toList();
         }
 
         @Override
@@ -205,29 +188,13 @@
             mAppInfos = apps;
         }
 
-        @Override
-        public IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
-            return mPageToBindSync;
-        }
-
         public void reset() {
-            mItems.clear();
-            mPageBoundSync = new IntSet();
-            mPendingTasks = null;
+            mItems = null;
             mAppInfos = null;
-            bindStarted = false;
         }
 
-        public void verifySynchronouslyBound(int totalItems) {
-            // Verify that the requested page is bound synchronously
-            assertTrue(bindStarted);
-            assertEquals(mPageToBindSync, mPageBoundSync);
-            assertEquals(mItems.size(), 1);
-            assertEquals(IntSet.wrap(mItems.get(0).screenId), mPageBoundSync);
-            assertNotNull(mPendingTasks);
-
-            // Verify that all other pages are bound properly
-            mPendingTasks.executeAllAndDestroy();
+        public void verifyItemsBound(int totalItems) {
+            assertNotNull(mItems);
             assertEquals(mItems.size(), totalItems);
         }
 
@@ -236,9 +203,5 @@
                     .map(ai -> ai.getTargetComponent().getPackageName())
                     .collect(Collectors.toSet());
         }
-
-        public void verifyApps(String... apps) {
-            assertTrue(allApps().containsAll(Arrays.asList(apps)));
-        }
     }
 }