Adding support for async view inflation

Bug: 318539160
Test: atest AsyncBindingTest; atest ItemInflaterTest
Flag: aconfig enable_workspace_inflation DEVELOPMENT
Change-Id: I77a373db7a5805f68f4b8cbfa9b586b5674252de
diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java
index e4aebf6..fb8088c 100644
--- a/src/com/android/launcher3/Alarm.java
+++ b/src/com/android/launcher3/Alarm.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import android.os.Handler;
+import android.os.Looper;
 import android.os.SystemClock;
 
 public class Alarm implements Runnable{
@@ -33,7 +34,11 @@
     private long mLastSetTimeout;
 
     public Alarm() {
-        mHandler = new Handler();
+        this(Looper.myLooper());
+    }
+
+    public Alarm(Looper looper) {
+        mHandler = new Handler(looper);
     }
 
     public void setOnAlarmListener(OnAlarmListener alarmListener) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index edfef5e..1ab6222 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -28,6 +28,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
+import static com.android.launcher3.Flags.enableWorkspaceInflation;
 import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -1485,8 +1486,8 @@
         CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
         if (showPendingWidget) {
             launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-            PendingAppWidgetHostView pendingAppWidgetHostView =
-                    new PendingAppWidgetHostView(this, launcherInfo, appWidgetInfo);
+            PendingAppWidgetHostView pendingAppWidgetHostView = new PendingAppWidgetHostView(
+                    this, mAppWidgetHolder, launcherInfo, appWidgetInfo);
             pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap);
             hostView = pendingAppWidgetHostView;
         } else if (hostView instanceof PendingAppWidgetHostView) {
@@ -2187,17 +2188,23 @@
      */
     @Override
     public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
-        bindItems(items.stream().map(i -> Pair.create(
+        bindInflatedItems(items.stream().map(i -> Pair.create(
                 i, getItemInflater().inflateItem(i, getModelWriter()))).toList(),
                 forceAnimateIcons ? new AnimatorSet() : null);
     }
 
+    @Override
+    public void bindInflatedItems(List<Pair<ItemInfo, View>> items) {
+        bindInflatedItems(items, null);
+    }
+
     /**
      * Bind all the items in the map, ignoring any null views
      *
      * @param boundAnim if non-null, uses it to create and play the bounce animation for added views
      */
-    public void bindItems(List<Pair<ItemInfo, View>> shortcuts, @Nullable AnimatorSet boundAnim) {
+    public void bindInflatedItems(
+            List<Pair<ItemInfo, View>> shortcuts, @Nullable AnimatorSet boundAnim) {
         // Get the list of added items and intersect them with the set of items here
         Workspace<?> workspace = mWorkspace;
         int newItemsScreenId = -1;
@@ -2222,10 +2229,13 @@
                 }
             }
 
-            final View view = e.second;
+            View view = e.second;
             if (view == null) {
                 continue;
             }
+            if (enableWorkspaceInflation() && view instanceof LauncherAppWidgetHostView lv) {
+                view = getAppWidgetHolder().attachViewToHostAndGetAttachedView(lv);
+            }
             workspace.addInScreenFromBind(view, item);
             if (boundAnim != null) {
                 // Animate all the applications up now
@@ -2324,9 +2334,9 @@
 
     @Override
     public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
-            int workspaceItemCount, boolean isBindSync) {
-        mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, workspaceItemCount,
-                isBindSync);
+            RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) {
+        mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, onCompleteSignal,
+                workspaceItemCount, isBindSync);
     }
 
     /**
@@ -3057,6 +3067,7 @@
         return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
     }
 
+    @Override
     public ItemInflater<Launcher> getItemInflater() {
         return mItemInflater;
     }
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 9867556..9b65a31 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -72,6 +72,7 @@
     override fun onInitialBindComplete(
         boundPages: LIntSet,
         pendingTasks: RunnableList,
+        onCompleteSignal: RunnableList,
         workspaceItemCount: Int,
         isBindSync: Boolean
     ) {
@@ -99,7 +100,14 @@
                 }
             }
         pendingExecutor = executor
-        executor.attachTo(launcher)
+
+        if (Flags.enableWorkspaceInflation()) {
+            // Finish the executor as soon as the pending inflation is completed
+            onCompleteSignal.add(executor::markCompleted)
+        } else {
+            // Pending executor is already completed, wait until first draw to run the tasks
+            executor.attachTo(launcher)
+        }
         launcher.bindComplete(workspaceItemCount, isBindSync)
     }
 
@@ -409,4 +417,6 @@
     }
 
     fun getIsFirstPagePinnedItemEnabled(): Boolean = isFirstPagePinnedItemEnabled
+
+    override fun getItemInflater() = launcher.itemInflater
 }
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index a846e68..e861d38 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -440,7 +440,7 @@
             anim.addListener(forEndCallback(
                     () -> view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)));
         }
-        mContext.bindItems(Collections.singletonList(Pair.create(item, view)), anim);
+        mContext.bindInflatedItems(Collections.singletonList(Pair.create(item, view)), anim);
     }
 
     /**
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f013126..ec9c27d 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -40,6 +40,7 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
+import android.os.Looper;
 import android.text.InputType;
 import android.text.Selection;
 import android.text.TextUtils;
@@ -165,10 +166,10 @@
     private static final Rect sTempRect = new Rect();
     private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
 
-    private final Alarm mReorderAlarm = new Alarm();
-    private final Alarm mOnExitAlarm = new Alarm();
-    private final Alarm mOnScrollHintAlarm = new Alarm();
-    final Alarm mScrollPauseAlarm = new Alarm();
+    private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+    private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+    private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+    final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
 
     final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
 
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 284b31e..ee0d5fc 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -32,6 +32,7 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Looper;
 import android.util.AttributeSet;
 import android.util.Property;
 import android.view.LayoutInflater;
@@ -121,7 +122,7 @@
 
     boolean mAnimating = false;
 
-    private Alarm mOpenAlarm = new Alarm();
+    private Alarm mOpenAlarm = new Alarm(Looper.getMainLooper());
 
     private boolean mForceHideDot;
     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index ee66a60..8e73660 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -37,6 +37,7 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.graphics.drawable.Drawable;
+import android.os.Looper;
 import android.os.Process;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -44,6 +45,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -65,7 +67,6 @@
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.widget.WidgetSections;
 import com.android.launcher3.widget.WidgetSections.WidgetSection;
 
@@ -173,9 +174,9 @@
      *
      * @return a request ID that can be used to cancel the request.
      */
+    @AnyThread
     public CancellableTask updateIconInBackground(final ItemInfoUpdateReceiver caller,
             final ItemInfoWithIcon info) {
-        Preconditions.assertUIThread();
         Supplier<ItemInfoWithIcon> task;
         if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
             task = () -> {
@@ -193,13 +194,19 @@
             return mCancelledTask;
         }
 
-        if (mPendingIconRequestCount <= 0) {
-            MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
+        Runnable endRunnable;
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            if (mPendingIconRequestCount <= 0) {
+                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
+            }
+            mPendingIconRequestCount++;
+            endRunnable = this::onIconRequestEnd;
+        } else {
+            endRunnable = () -> { };
         }
-        mPendingIconRequestCount++;
 
         CancellableTask<ItemInfoWithIcon> request = new CancellableTask<>(
-                task, MAIN_EXECUTOR, caller::reapplyItemInfo, this::onIconRequestEnd);
+                task, MAIN_EXECUTOR, caller::reapplyItemInfo, endRunnable);
         Utilities.postAsyncCallback(mWorkerHandler, request);
         return request;
     }
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 9b2344d..fa2a1b0 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -16,20 +16,25 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.Flags.enableWorkspaceInflation;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.os.Process;
 import android.os.Trace;
 import android.util.Log;
+import android.util.Pair;
+import android.view.View;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
@@ -38,6 +43,7 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.PackageUserKey;
@@ -279,8 +285,8 @@
             // 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<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
-            ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+            ArrayList<ItemInfo> currentAppWidgets = new ArrayList<>();
+            ArrayList<ItemInfo> otherAppWidgets = new ArrayList<>();
 
             filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
                     otherWorkspaceItems);
@@ -304,8 +310,8 @@
             executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
 
             // Load items on the current page.
-            bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
-            bindAppWidgets(currentAppWidgets, mUiExecutor);
+            bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
+            bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
             if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mExtraItems.forEach(item ->
                         executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
@@ -313,8 +319,41 @@
 
             RunnableList pendingTasks = new RunnableList();
             Executor pendingExecutor = pendingTasks::add;
-            bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
-            bindAppWidgets(otherAppWidgets, pendingExecutor);
+
+            RunnableList onCompleteSignal = new RunnableList();
+
+            if (enableWorkspaceInflation()) {
+                MODEL_EXECUTOR.execute(() ->  {
+                    setupPendingBind(otherWorkspaceItems, otherAppWidgets, currentScreenIds,
+                            pendingExecutor);
+
+                    // Wait for the async inflation to complete and then notify the completion
+                    // signal on UI thread.
+                    MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy);
+                });
+            } else {
+                setupPendingBind(
+                        otherWorkspaceItems, otherAppWidgets, currentScreenIds, pendingExecutor);
+                onCompleteSignal.executeAllAndDestroy();
+            }
+
+            executeCallbacksTask(
+                    c -> {
+                        if (!enableWorkspaceInflation()) {
+                            MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                        }
+                        c.onInitialBindComplete(currentScreenIds, pendingTasks, onCompleteSignal,
+                                workspaceItemCount, isBindSync);
+                    }, mUiExecutor);
+        }
+
+        private void setupPendingBind(
+                List<ItemInfo> otherWorkspaceItems,
+                List<ItemInfo> otherAppWidgets,
+                IntSet currentScreenIds,
+                Executor pendingExecutor) {
+            bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
+            bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
 
             StringCache cacheClone = mBgDataModel.stringCache.clone();
             executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
@@ -326,38 +365,51 @@
                         ItemInstallQueue.INSTANCE.get(mApp.getContext())
                                 .resumeModelPush(FLAG_LOADER_RUNNING);
                     });
-
-            executeCallbacksTask(
-                    c -> {
-                        MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-                        c.onInitialBindComplete(
-                                currentScreenIds, pendingTasks, workspaceItemCount, isBindSync);
-                    }, mUiExecutor);
         }
 
-        private void bindWorkspaceItems(
-                final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+        /**
+         * Tries to inflate the items asynchronously and bind. Returns true on success or false if
+         * async-binding is not supported in this case.
+         */
+        private boolean inflateAsyncAndBind(List<ItemInfo> items, Executor executor) {
+            if (!enableWorkspaceInflation()) {
+                return false;
+            }
+            ItemInflater inflater = mCallbacks.getItemInflater();
+            if (inflater == null) {
+                return false;
+            }
+
+            if (mMyBindingId != mBgDataModel.lastBindId) {
+                Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation");
+                return true;
+            }
+
+            ModelWriter writer = mApp.getModel()
+                    .getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null);
+            List<Pair<ItemInfo, View>> bindItems = items.stream().map(i ->
+                    Pair.create(i, inflater.inflateItem(i, writer, null))).toList();
+            executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
+            return true;
+        }
+
+        private void bindItemsInChunks(List<ItemInfo> workspaceItems, int chunkCount,
+                Executor executor) {
+            if (inflateAsyncAndBind(workspaceItems, executor)) {
+                return;
+            }
+
             // Bind the workspace items
             int count = workspaceItems.size();
-            for (int i = 0; i < count; i += ITEMS_CHUNK) {
+            for (int i = 0; i < count; i += chunkCount) {
                 final int start = i;
-                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+                final int chunkSize = (i + chunkCount <= count) ? chunkCount : (count - i);
                 executeCallbacksTask(
                         c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
                         executor);
             }
         }
 
-        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
-            // Bind the widgets, one at a time
-            int count = appWidgets.size();
-            for (int i = 0; i < count; i++) {
-                final ItemInfo widget = appWidgets.get(i);
-                executeCallbacksTask(
-                        c -> c.bindItems(Collections.singletonList(widget), false), executor);
-            }
-        }
-
         protected void executeCallbacksTask(CallbackTask task, Executor executor) {
             executor.execute(() -> {
                 if (mMyBindingId != mBgDataModel.lastBindId) {
@@ -430,8 +482,11 @@
             bindAppWidgets(appWidgets);
             executeCallbacksTask(c -> {
                 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-                c.onInitialBindComplete(
-                        mCurrentScreenIds, new RunnableList(), workspaceItemCount, isBindSync);
+
+                RunnableList onCompleteSignal = new RunnableList();
+                onCompleteSignal.executeAllAndDestroy();
+                c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal,
+                        workspaceItemCount, isBindSync);
             }, mUiExecutor);
         }
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 7f0f683..8579d1d 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -33,6 +33,8 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
+import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -54,6 +56,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;
@@ -495,7 +498,15 @@
         default void clearPendingBinds() { }
         default void startBinding() { }
 
-        default void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
+        @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 setIsFirstPagePinnedItemEnabled(boolean isFirstPagePinnedItemEnabled) { }
         default void finishBindingItems(IntSet pagesBoundFirst) { }
@@ -520,7 +531,9 @@
         default void bindSmartspaceWidget() { }
 
         /** Called when workspace has been bound. */
-        default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+        default void onInitialBindComplete(@NonNull IntSet boundPages,
+                @NonNull RunnableList pendingTasks,
+                @NonNull RunnableList onCompleteSignal,
                 int workspaceItemCount, boolean isBindSync) {
             pendingTasks.executeAllAndDestroy();
         }
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index bc51c9b..9e72e28 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -20,7 +20,6 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -37,9 +36,9 @@
      */
     public static <T extends ItemInfo> void filterCurrentWorkspaceItems(
             final IntSet currentScreenIds,
-            ArrayList<T> allWorkspaceItems,
-            ArrayList<T> currentScreenItems,
-            ArrayList<T> otherScreenItems) {
+            List<? extends T> allWorkspaceItems,
+            List<T> currentScreenItems,
+            List<T> otherScreenItems) {
         // Purge any null ItemInfos
         allWorkspaceItems.removeIf(Objects::isNull);
         // Order the set of items by their containers first, this allows use to walk through the
diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt
index 79091ca..cc66af1 100644
--- a/src/com/android/launcher3/util/ItemInflater.kt
+++ b/src/com/android/launcher3/util/ItemInflater.kt
@@ -121,7 +121,7 @@
             }
             val view =
                 if (type == WidgetInflater.TYPE_PENDING || widgetInfo == null)
-                    PendingAppWidgetHostView(context, item, widgetInfo)
+                    PendingAppWidgetHostView(context, widgetHolder, item, widgetInfo)
                 else widgetHolder.createView(item.appWidgetId, widgetInfo)
             prepareAppWidget(view, item)
             return view
diff --git a/src/com/android/launcher3/util/RunnableList.java b/src/com/android/launcher3/util/RunnableList.java
index f6e0c57..2b8bf56 100644
--- a/src/com/android/launcher3/util/RunnableList.java
+++ b/src/com/android/launcher3/util/RunnableList.java
@@ -69,4 +69,11 @@
             }
         }
     }
+
+    /**
+     * Returns true if the list has been destroyed
+     */
+    public boolean isDestroyed() {
+        return mDestroyed;
+    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index b1c477c..40c3984 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -54,6 +54,9 @@
     @Nullable
     private final IntConsumer mAppWidgetRemovedCallback;
 
+    @Nullable
+    private ListenableHostView mViewToRecycle;
+
     public LauncherAppWidgetHost(@NonNull Context context,
             @Nullable IntConsumer appWidgetRemovedCallback,
             List<ProviderChangedListener> providerChangeListeners) {
@@ -73,11 +76,21 @@
         }
     }
 
+    /**
+     * Sets the view to be recycled for the next widget creation.
+     */
+    public void recycleViewForNextCreation(ListenableHostView viewToRecycle) {
+        mViewToRecycle = viewToRecycle;
+    }
+
     @Override
     @NonNull
     public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
             AppWidgetProviderInfo appWidget) {
-        return new ListenableHostView(context);
+        ListenableHostView result =
+                mViewToRecycle != null ? mViewToRecycle : new ListenableHostView(context);
+        mViewToRecycle = null;
+        return result;
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index e77ec12..2259e3c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -40,12 +40,12 @@
 
 import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.Flags;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
 
 /**
@@ -72,7 +72,7 @@
 
     private final Rect mTempRect = new Rect();
     private final CheckLongPressHelper mLongPressHelper;
-    protected final Launcher mLauncher;
+    protected final ActivityContext mActivityContext;
 
     // Maintain the color manager.
     private final LocalColorExtractor mColorExtractor;
@@ -94,15 +94,15 @@
 
     public LauncherAppWidgetHostView(Context context) {
         super(context);
-        mLauncher = Launcher.getLauncher(context);
+        mActivityContext = ActivityContext.lookupContext(context);
         mLongPressHelper = new CheckLongPressHelper(this, this);
-        setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
+        setAccessibilityDelegate(mActivityContext.getAccessibilityDelegate());
         setBackgroundResource(R.drawable.widget_internal_focus_bg);
         if (Flags.enableFocusOutline()) {
             setDefaultFocusHighlightEnabled(false);
         }
 
-        if (Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
+        if (Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
         }
         mColorExtractor = new LocalColorExtractor(); // no-op
@@ -120,8 +120,7 @@
     @Override
     public boolean onLongClick(View view) {
         if (mIsScrollable) {
-            DragLayer dragLayer = mLauncher.getDragLayer();
-            dragLayer.requestDisallowInterceptTouchEvent(false);
+            mActivityContext.getDragLayer().requestDisallowInterceptTouchEvent(false);
         }
         view.performLongClick();
         return true;
@@ -218,7 +217,7 @@
 
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            DragLayer dragLayer = mLauncher.getDragLayer();
+            BaseDragLayer dragLayer = mActivityContext.getDragLayer();
             if (mIsScrollable) {
                 dragLayer.requestDisallowInterceptTouchEvent(true);
             }
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 23127b3..15bd6ed 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -17,7 +17,9 @@
 
 import static android.app.Activity.RESULT_CANCELED;
 
+import static com.android.launcher3.Flags.enableWorkspaceInflation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
 
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
@@ -27,6 +29,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.Looper;
 import android.util.SparseArray;
 import android.widget.Toast;
 
@@ -310,7 +313,9 @@
     }
 
     /**
-     * Create a view for the specified app widget
+     * Create a view for the specified app widget. When calling this method from a background
+     * thread, the returned view will not receive ongoing updates. The caller needs to reattach
+     * the view using {@link #attachViewToHostAndGetAttachedView} on UIThread
      *
      * @param appWidgetId The ID of the widget
      * @param appWidget   The {@link LauncherAppWidgetProviderInfo} of the widget
@@ -327,7 +332,55 @@
         }
 
         LauncherAppWidgetHostView view = createViewInternal(appWidgetId, appWidget);
-        mViews.put(appWidgetId, view);
+        // Do not update mViews on a background thread call, as the holder is not thread safe.
+        if (!enableWorkspaceInflation() || Looper.myLooper() == Looper.getMainLooper()) {
+            mViews.put(appWidgetId, view);
+        }
+        return view;
+    }
+
+    /**
+     * Attaches an already inflated view to the host. If the view can't be attached, creates
+     * and attaches a new view.
+     * @return the final attached view
+     */
+    @NonNull
+    public final AppWidgetHostView attachViewToHostAndGetAttachedView(
+            @NonNull LauncherAppWidgetHostView view) {
+        if (mViews.get(view.getAppWidgetId()) != view) {
+            view = recycleExistingView(view);
+            mViews.put(view.getAppWidgetId(), view);
+        }
+        return view;
+    }
+
+    /**
+     * Recycling logic:
+     *   1) If the final view should be a pendingView
+     *          if the provided view is also a pendingView, return itself
+     *          otherwise discard provided view and return a new pending view
+     *   2) If the recycled view is a pendingView, discard it and return a new view
+     *   3) Use the same for as creating a new view, but used the provided view in the host instead
+     *      of creating a new view. This ensures that all the host callbacks are properly attached
+     *      as a result of using the same flow.
+     */
+    protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) {
+        if ((mFlags & FLAG_LISTENING) == 0) {
+            if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) {
+                return view;
+            } else {
+                return new PendingAppWidgetHostView(mContext, this, view.getAppWidgetId(),
+                        fromProviderInfo(mContext, view.getAppWidgetInfo()));
+            }
+        }
+        LauncherAppWidgetHost host = (LauncherAppWidgetHost) mWidgetHost;
+        if (view instanceof ListenableHostView lhv) {
+            host.recycleViewForNextCreation(lhv);
+        }
+
+        view = createViewInternal(
+                view.getAppWidgetId(), fromProviderInfo(mContext, view.getAppWidgetInfo()));
+        host.recycleViewForNextCreation(null);
         return view;
     }
 
@@ -338,8 +391,15 @@
             // Since the launcher hasn't started listening to widget updates, we can't simply call
             // host.createView here because the later will make a binder call to retrieve
             // RemoteViews from system process.
-            return new PendingAppWidgetHostView(mContext, appWidgetId, appWidget);
+            return new PendingAppWidgetHostView(mContext, this, appWidgetId, appWidget);
         } else {
+            if (enableWorkspaceInflation() && Looper.myLooper() != Looper.getMainLooper()) {
+                // Widget is being inflated a background thread, just create and
+                // return a placeholder view
+                ListenableHostView hostView = new ListenableHostView(mContext);
+                hostView.setAppWidget(appWidgetId, appWidget);
+                return hostView;
+            }
             try {
                 return (LauncherAppWidgetHostView) mWidgetHost.createView(
                         mContext, appWidgetId, appWidget);
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index adf85c7..86400ba 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -50,6 +50,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.FastBitmapDrawable;
@@ -65,7 +66,7 @@
 public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
         implements OnClickListener, ItemInfoUpdateReceiver {
     private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5;
-    private static final float MIN_SATUNATION = 0.7f;
+    private static final float MIN_SATURATION = 0.7f;
 
     private static final int FLAG_DRAW_SETTINGS = 1;
     private static final int FLAG_DRAW_ICON = 2;
@@ -75,6 +76,7 @@
 
     private final Rect mRect = new Rect();
 
+    private final LauncherWidgetHolder mWidgetHolder;
     private final LauncherAppWidgetProviderInfo mAppwidget;
     private final LauncherAppWidgetInfo mInfo;
     private final int mStartState;
@@ -90,6 +92,7 @@
     private Drawable mSettingIconDrawable;
 
     private boolean mDrawableSizeChanged;
+    private boolean mIsDeferredWidget;
 
     private final TextPaint mPaint;
 
@@ -98,13 +101,13 @@
 
     @Nullable private Bitmap mPreviewBitmap;
 
-    public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
-            @Nullable LauncherAppWidgetProviderInfo appWidget) {
-        this(context, info, appWidget,
+    public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder,
+            LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) {
+        this(context, widgetHolder, info, appWidget,
                 context.getResources().getText(R.string.gadget_complete_setup_text));
 
         super.updateAppWidget(null);
-        setOnClickListener(mLauncher.getItemOnClickListener());
+        setOnClickListener(mActivityContext.getItemOnClickListener());
 
         if (info.pendingItemInfo == null) {
             info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(),
@@ -117,14 +120,16 @@
     }
 
     public PendingAppWidgetHostView(
-            Context context, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
-        this(context, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
+            Context context, LauncherWidgetHolder widgetHolder,
+            int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
+        this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
                 appWidget, appWidget.label);
         getBackground().mutate().setAlpha(DEFERRED_ALPHA);
 
         mCenterDrawable = new ColorDrawable(Color.TRANSPARENT);
         mDragFlags = FLAG_DRAW_LABEL;
         mDrawableSizeChanged = true;
+        mIsDeferredWidget = true;
     }
 
     /** Set {@link Bitmap} of widget preview. */
@@ -136,10 +141,11 @@
         invalidate();
     }
 
-    private PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
+    private PendingAppWidgetHostView(Context context,
+            LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info,
             LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
         super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
-
+        mWidgetHolder = widgetHolder;
         mAppwidget = appwidget;
         mInfo = info;
         mStartState = info.restoreStatus;
@@ -148,9 +154,12 @@
 
         mPaint = new TextPaint();
         mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
-        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
-                mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
+        mPaint.setTextSize(TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_PX,
+                mActivityContext.getDeviceProfile().iconTextSizePx,
+                getResources().getDisplayMetrics()));
         mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
+
         setWillNotDraw(false);
         setBackgroundResource(R.drawable.pending_widget_bg);
     }
@@ -161,6 +170,11 @@
     }
 
     @Override
+    public int getAppWidgetId() {
+        return mInfo.appWidgetId;
+    }
+
+    @Override
     public void updateAppWidget(RemoteViews remoteViews) {
         checkIfRestored();
     }
@@ -172,6 +186,10 @@
         }
     }
 
+    public boolean isDeferredWidget() {
+        return mIsDeferredWidget;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
@@ -184,8 +202,8 @@
             if (mOnDetachCleanup != null) {
                 mOnDetachCleanup.close();
             }
-            mOnDetachCleanup = mLauncher.getAppWidgetHolder()
-                    .addOnUpdateListener(mInfo.appWidgetId, mAppwidget, this::checkIfRestored);
+            mOnDetachCleanup = mWidgetHolder.addOnUpdateListener(
+                    mInfo.appWidgetId, mAppwidget, this::checkIfRestored);
             checkIfRestored();
         }
     }
@@ -211,11 +229,13 @@
             // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
             return;
         }
-        // Remove and rebind the current widget (which was inflated in the wrong
-        // orientation), but don't delete it from the database
-        mLauncher.removeItem(this, info, false  /* deleteFromDb */,
-                "widget removed because of configuration change");
-        mLauncher.bindAppWidget(info);
+        if (mActivityContext instanceof Launcher launcher) {
+            // Remove and rebind the current widget (which was inflated in the wrong
+            // orientation), but don't delete it from the database
+            launcher.removeItem(this, info, false  /* deleteFromDb */,
+                    "widget removed because of configuration change");
+            launcher.bindAppWidget(info);
+        }
     }
 
     @Override
@@ -303,7 +323,7 @@
         // Make the dominant color bright.
         float[] hsv = new float[3];
         Color.colorToHSV(dominantColor, hsv);
-        hsv[1] = Math.min(hsv[1], MIN_SATUNATION);
+        hsv[1] = Math.min(hsv[1], MIN_SATURATION);
         hsv[2] = 1;
         mSettingIconDrawable.setColorFilter(Color.HSVToColor(hsv),  PorterDuff.Mode.SRC_IN);
     }
@@ -344,7 +364,7 @@
     }
 
     private void updateDrawableBounds() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
         int paddingTop = getPaddingTop();
         int paddingBottom = getPaddingBottom();
         int paddingLeft = getPaddingLeft();