Merge "Refactor: Use AppChipStatus enum instead of isExpanded boolean" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 517bd6d..1bce9b3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -59,6 +59,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <action android:name="android.intent.action.SHOW_WORK_APPS" />
+                <action android:name="android.intent.action.ALL_APPS" />
                 <category android:name="android.intent.category.HOME" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.MONKEY"/>
diff --git a/go/AndroidManifest-launcher.xml b/go/AndroidManifest-launcher.xml
index 2223036..bef7180 100644
--- a/go/AndroidManifest-launcher.xml
+++ b/go/AndroidManifest-launcher.xml
@@ -57,6 +57,7 @@
             android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.ALL_APPS" />
                 <category android:name="android.intent.category.HOME" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.MONKEY"/>
diff --git a/proguard.flags b/proguard.flags
index da00c00..c0a0042 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,6 +1,12 @@
--keep,allowshrinking,allowoptimization class com.android.launcher3.** {
-  *;
-}
+
+-keep,allowshrinking,allowoptimization class com.android.launcher3.** {*;}
+-keepclasseswithmembernames class com.android.launcher3.** {*;}
+
+-keep,allowshrinking,allowoptimization class com.android.systemui.shared.** {*;}
+-keepclasseswithmembernames class com.android.systemui.shared.** {*;}
+
+-keep,allowshrinking,allowoptimization class com.android.quickstep.** {*;}
+-keepclasseswithmembernames class com.android.quickstep.** {*;}
 
 # The support library contains references to newer platform versions.
 # Don't warn about those in case this app is linking against an older
@@ -49,14 +55,6 @@
 # Ignore warnings for hidden utility classes referenced from the shared lib
 -dontwarn com.android.internal.util.**
 
-################ Do not optimize recents lib #############
--keep class com.android.systemui.shared.** {
-  *;
-}
-
--keep class com.android.quickstep.** {
-  *;
-}
 
 -keep class com.android.internal.protolog.** {
   *;
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 80d8154..d6aa886 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -57,6 +57,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <action android:name="android.intent.action.SHOW_WORK_APPS" />
+                <action android:name="android.intent.action.ALL_APPS" />
                 <category android:name="android.intent.category.HOME" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.MONKEY"/>
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 1ce28cf..49cee0f 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -23,7 +23,6 @@
 
     <string name="stats_log_manager_class" translatable="false">com.android.quickstep.logging.StatsLogCompatManager</string>
     <string name="test_information_handler_class" translatable="false">com.android.quickstep.QuickstepTestInformationHandler</string>
-    <string name="widget_holder_factory_class" translatable="false">com.android.launcher3.uioverrides.QuickstepWidgetHolder$QuickstepHolderFactory</string>
     <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
     <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
     <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
diff --git a/quickstep/src/com/android/launcher3/dagger/Modules.kt b/quickstep/src/com/android/launcher3/dagger/Modules.kt
index 52be413..7671a82 100644
--- a/quickstep/src/com/android/launcher3/dagger/Modules.kt
+++ b/quickstep/src/com/android/launcher3/dagger/Modules.kt
@@ -16,11 +16,13 @@
 
 package com.android.launcher3.dagger
 
+import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepWidgetHolderFactory
 import com.android.launcher3.uioverrides.SystemApiWrapper
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl
 import com.android.launcher3.util.ApiWrapper
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory
 import com.android.quickstep.util.GestureExclusionManager
 import com.android.quickstep.util.SystemWindowManagerProxy
 import dagger.Binds
@@ -40,6 +42,13 @@
 }
 
 @Module
+abstract class WidgetModule {
+
+    @Binds
+    abstract fun bindWidgetHolderFactory(factor: QuickstepWidgetHolderFactory): WidgetHolderFactory
+}
+
+@Module
 abstract class PluginManagerWrapperModule {
     @Binds
     abstract fun bindPluginManagerWrapper(impl: PluginManagerWrapperImpl): PluginManagerWrapper
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 810fa6f..1ac2d7c 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -40,7 +40,8 @@
 import com.android.quickstep.fallback.RecentsState
 import com.android.wm.shell.desktopmode.DisplayDeskState
 import com.android.wm.shell.desktopmode.IDesktopTaskListener.Stub
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useRoundedCorners
 import java.io.PrintWriter
 import java.lang.ref.WeakReference
 import javax.inject.Inject
@@ -89,8 +90,7 @@
     private val taskbarDesktopModeListeners: MutableSet<TaskbarDesktopModeListener> = HashSet()
 
     // This simply indicates that user is currently in desktop mode or not.
-    var isInDesktopMode = false
-        private set
+    @Deprecated("Does not work with multi-desks") private var isInDesktopModeDeprecated = false
 
     // to track if any pending notification to be done.
     var isNotifyingDesktopVisibilityPending = false
@@ -104,12 +104,16 @@
      * Number of visible desktop windows in desktop mode. This can be > 0 when user goes to overview
      * from desktop window mode.
      */
-    var visibleDesktopTasksCount: Int = 0
+    @Deprecated("Does not work with multi-desks")
+    var visibleDesktopTasksCountDeprecated: Int = 0
         /**
          * Sets the number of desktop windows that are visible and updates launcher visibility based
          * on it.
          */
         set(visibleTasksCount) {
+            if (enableMultipleDesktops(context)) {
+                return
+            }
             if (DEBUG) {
                 Log.d(
                     TAG,
@@ -121,11 +125,11 @@
             }
 
             if (visibleTasksCount != field) {
-                if (visibleDesktopTasksCount == 0 && visibleTasksCount == 1) {
-                    isInDesktopMode = true
+                if (visibleDesktopTasksCountDeprecated == 0 && visibleTasksCount == 1) {
+                    isInDesktopModeDeprecated = true
                 }
-                if (visibleDesktopTasksCount == 1 && visibleTasksCount == 0) {
-                    isInDesktopMode = false
+                if (visibleDesktopTasksCountDeprecated == 1 && visibleTasksCount == 0) {
+                    isInDesktopModeDeprecated = false
                 }
                 val wasVisible = field > 0
                 val isVisible = visibleTasksCount > 0
@@ -171,7 +175,7 @@
     private var desktopTaskListener: DesktopTaskListenerImpl?
 
     init {
-        desktopTaskListener = DesktopTaskListenerImpl(this, context.displayId)
+        desktopTaskListener = DesktopTaskListenerImpl(this, context, context.displayId)
         systemUiProxy.setDesktopTaskListener(desktopTaskListener)
 
         lifecycleTracker.addCloseable {
@@ -185,7 +189,7 @@
      * [INACTIVE_DESK_ID] if no desk is currently active or the multiple desks feature is disabled.
      */
     fun getActiveDeskId(displayId: Int): Int {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             // When the multiple desks feature is disabled, callers should not rely on the concept
             // of a desk ID.
             return INACTIVE_DESK_ID
@@ -196,8 +200,8 @@
 
     /** Returns whether a desk is currently active on the display with the given [displayId]. */
     fun isInDesktopMode(displayId: Int): Boolean {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
-            return isInDesktopMode
+        if (!enableMultipleDesktops(context)) {
+            return isInDesktopModeDeprecated
         }
 
         val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
@@ -213,7 +217,7 @@
      * Overview is not active.
      */
     fun isInDesktopModeAndNotInOverview(displayId: Int): Boolean {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             return areDesktopTasksVisibleAndNotInOverview()
         }
 
@@ -225,7 +229,7 @@
 
     /** Whether desktop tasks are visible in desktop mode. */
     private fun areDesktopTasksVisibleAndNotInOverview(): Boolean {
-        val desktopTasksVisible: Boolean = visibleDesktopTasksCount > 0
+        val desktopTasksVisible: Boolean = visibleDesktopTasksCountDeprecated > 0
         if (DEBUG) {
             Log.d(
                 TAG,
@@ -309,7 +313,7 @@
             inOverviewState = overviewStateEnabled
             val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview()
 
-            if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+            if (!enableMultipleDesktops(context)) {
                 if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
                     notifyIsInDesktopModeChanged(DEFAULT_DISPLAY, areDesktopTasksVisibleNow)
                 }
@@ -504,7 +508,7 @@
         displayDeskStates: Array<DisplayDeskState>,
         canCreateDesks: Boolean,
     ) {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             return
         }
 
@@ -527,7 +531,7 @@
             ?: null.also { Slog.e(TAG, "Expected non-null desk config for display: $displayId") }
 
     private fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             return
         }
 
@@ -535,7 +539,7 @@
     }
 
     private fun onDeskAdded(displayId: Int, deskId: Int) {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             return
         }
 
@@ -549,7 +553,7 @@
     }
 
     private fun onDeskRemoved(displayId: Int, deskId: Int) {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             return
         }
 
@@ -566,7 +570,7 @@
     }
 
     private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
-        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+        if (!enableMultipleDesktops(context)) {
             return
         }
 
@@ -626,7 +630,7 @@
         pw.println(prefix + "DesktopVisibilityController:")
 
         pw.println("$prefix\tdesktopVisibilityListeners=$desktopVisibilityListeners")
-        pw.println("$prefix\tvisibleDesktopTasksCount=$visibleDesktopTasksCount")
+        pw.println("$prefix\tvisibleDesktopTasksCount=$visibleDesktopTasksCountDeprecated")
         pw.println("$prefix\tinOverviewState=$inOverviewState")
         pw.println("$prefix\tbackgroundStateEnabled=$backgroundStateEnabled")
         pw.println("$prefix\tgestureInProgress=$gestureInProgress")
@@ -640,6 +644,7 @@
      */
     private class DesktopTaskListenerImpl(
         controller: DesktopVisibilityController,
+        @ApplicationContext private val context: Context,
         private val displayId: Int,
     ) : Stub() {
         private val controller = WeakReference(controller)
@@ -660,7 +665,7 @@
                     if (DEBUG) {
                         Log.d(TAG, "desktop visible tasks count changed=$visibleTasksCount")
                     }
-                    visibleDesktopTasksCount = visibleTasksCount
+                    visibleDesktopTasksCountDeprecated = visibleTasksCount
                 }
             }
         }
@@ -670,7 +675,7 @@
         }
 
         override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
-            if (!DesktopModeStatus.useRoundedCorners()) return
+            if (!useRoundedCorners()) return
             MAIN_EXECUTOR.execute {
                 controller.get()?.apply {
                     Log.d(
@@ -683,7 +688,10 @@
             }
         }
 
+        // TODO: b/402496827 - The multi-desks backend needs to be updated to call this API only
+        //  once, not between desk switches.
         override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) {
+            val controller = controller.get() ?: return
             MAIN_EXECUTOR.execute {
                 Log.d(
                     TAG,
@@ -691,15 +699,19 @@
                         "duration= " +
                         transitionDuration),
                 )
-                val controller = controller.get()
-                if (controller != null && !controller.isInDesktopMode) {
-                    controller.isInDesktopMode = true
+                if (enableMultipleDesktops(context)) {
+                    controller.notifyTaskbarDesktopModeListenersForEntry(transitionDuration)
+                } else if (!controller.isInDesktopModeDeprecated) {
+                    controller.isInDesktopModeDeprecated = true
                     controller.notifyTaskbarDesktopModeListenersForEntry(transitionDuration)
                 }
             }
         }
 
+        // TODO: b/402496827 - The multi-desks backend needs to be updated to call this API only
+        //  once, not between desk switches.
         override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {
+            val controller = controller.get() ?: return
             MAIN_EXECUTOR.execute {
                 Log.d(
                     TAG,
@@ -707,9 +719,10 @@
                         "duration= " +
                         transitionDuration),
                 )
-                val controller = controller.get()
-                if (controller != null && controller.isInDesktopMode) {
-                    controller.isInDesktopMode = false
+                if (enableMultipleDesktops(context)) {
+                    controller.notifyTaskbarDesktopModeListenersForExit(transitionDuration)
+                } else if (controller.isInDesktopModeDeprecated) {
+                    controller.isInDesktopModeDeprecated = false
                     controller.notifyTaskbarDesktopModeListenersForExit(transitionDuration)
                 }
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
index aa3feb7..b82e6ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
@@ -23,7 +23,7 @@
 
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.BaseContext;
-import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.SystemUiProxy;
 
@@ -33,21 +33,49 @@
         implements SystemShortcut.BubbleActivityStarter {
 
     protected final LayoutInflater mLayoutInflater;
-    private final boolean mIsPrimaryDisplay;
 
     public BaseTaskbarContext(Context windowContext, boolean isPrimaryDisplay) {
         super(windowContext, Themes.getActivityThemeRes(windowContext));
         mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
-        mIsPrimaryDisplay = isPrimaryDisplay;
     }
 
-    public boolean isTransientTaskbar() {
-        return DisplayController.isTransientTaskbar(this) && mIsPrimaryDisplay;
-    }
+    /**
+     * Returns whether taskbar is transient or persistent. External displays will be persistent.
+     *
+     * @return {@code true} if transient, {@code false} if persistent.
+     */
+    public abstract boolean isTransientTaskbar();
 
-    public boolean isPrimaryDisplay() {
-        return mIsPrimaryDisplay;
-    }
+    /**
+     * Returns whether the taskbar is pinned in gesture navigation mode.
+     */
+    public abstract boolean isPinnedTaskbar();
+
+    /**
+     * Returns the current navigation mode. External displays will be in THREE_BUTTONS mode.
+     */
+    public abstract NavigationMode getNavigationMode();
+
+    /**
+     * Returns whether the taskbar is in desktop mode.
+     */
+    public abstract boolean isInDesktopMode();
+
+    /**
+     * Returns whether the taskbar is forced to be pinned when home is visible.
+     */
+    public abstract  boolean showLockedTaskbarOnHome();
+
+    /**
+     * Returns whether desktop taskbar (pinned taskbar that shows desktop tasks) is to be used on
+     * the display because the display is a freeform display.
+     */
+    public abstract  boolean showDesktopTaskbarForFreeformDisplay();
+
+    /**
+     * Returns whether the taskbar is displayed on primary or external display.
+     */
+    public abstract boolean isPrimaryDisplay();
 
     @Override
     public final LayoutInflater getLayoutInflater() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 6e210e3..913035a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -223,7 +223,8 @@
         if (!Flags.predictiveBackToHomePolish()) {
             shouldOverrideToFastAnimation |= mLauncher.getPredictiveBackToHomeInProgress();
         }
-        boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mLauncher);
+        boolean isPinnedTaskbar =
+                mControllers.taskbarActivityContext.isPinnedTaskbar();
         if (isVisible || isPinnedTaskbar) {
             return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinnedTaskbar);
         } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 018903e..66887d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -417,9 +417,41 @@
         dispatchDeviceProfileChanged();
     }
 
-    /** Returns whether current taskbar is transient. */
+    @Override
     public boolean isTransientTaskbar() {
-        return super.isTransientTaskbar() && !isPhoneMode();
+        return DisplayController.isTransientTaskbar(this) && mIsPrimaryDisplay && !isPhoneMode();
+    }
+
+    @Override
+    public boolean isPinnedTaskbar() {
+        return DisplayController.isPinnedTaskbar(this);
+    }
+
+    @Override
+    public NavigationMode getNavigationMode() {
+        return isPrimaryDisplay() ? DisplayController.getNavigationMode(this)
+                : NavigationMode.THREE_BUTTONS;
+    }
+
+    @Override
+    public boolean isInDesktopMode() {
+        return mControllers != null
+                && mControllers.taskbarDesktopModeController.isInDesktopMode(getDisplayId());
+    }
+
+    @Override
+    public boolean showLockedTaskbarOnHome() {
+        return DisplayController.showLockedTaskbarOnHome(this);
+    }
+
+    @Override
+    public boolean showDesktopTaskbarForFreeformDisplay() {
+        return DisplayController.showDesktopTaskbarForFreeformDisplay(this);
+    }
+
+    @Override
+    public boolean isPrimaryDisplay() {
+        return mIsPrimaryDisplay;
     }
 
     /**
@@ -455,9 +487,7 @@
                     .setIsTransientTaskbar(true)
                     .build();
         }
-        mNavMode = (DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
-                && !mIsPrimaryDisplay) ? NavigationMode.THREE_BUTTONS
-                : DisplayController.getNavigationMode(this);
+        mNavMode = getNavigationMode();
     }
 
     /** Called when the visibility of the bubble bar changed. */
@@ -666,8 +696,7 @@
      */
     public WindowManager.LayoutParams createDefaultWindowLayoutParams(int type, String title) {
         int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                | WindowManager.LayoutParams.FLAG_SLIPPERY
-                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+                | WindowManager.LayoutParams.FLAG_SLIPPERY;
         boolean watchOutside = isTransientTaskbar() || isThreeButtonNav();
         if (watchOutside && !isRunningInTestHarness()) {
             windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
@@ -1426,11 +1455,6 @@
         mControllers.uiController.startSplitSelection(splitSelectSource);
     }
 
-    boolean isInDesktopMode() {
-        return mControllers != null
-                && mControllers.taskbarDesktopModeController.isInDesktopMode(getDisplayId());
-    }
-
     protected void onTaskbarIconClicked(View view) {
         TaskbarUIController taskbarUIController = mControllers.uiController;
         RecentsView recents = taskbarUIController.getRecentsView();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
index ca8e4ca..e96e67d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
@@ -20,7 +20,6 @@
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.statehandlers.DesktopVisibilityController.TaskbarDesktopModeListener
 import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS
-import com.android.launcher3.util.DisplayController
 
 /** Handles Taskbar in Desktop Windowing mode. */
 class TaskbarDesktopModeController(
@@ -30,9 +29,6 @@
     private lateinit var taskbarControllers: TaskbarControllers
     private lateinit var taskbarSharedState: TaskbarSharedState
 
-    val isInDesktopMode: Boolean
-        get() = desktopVisibilityController.isInDesktopMode
-
     fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) {
         taskbarControllers = controllers
         taskbarSharedState = sharedState
@@ -52,9 +48,10 @@
     }
 
     fun shouldShowDesktopTasksInTaskbar(): Boolean {
+        val activityContext = taskbarControllers.taskbarActivityContext
         return isInDesktopMode(context.displayId) ||
-            DisplayController.showDesktopTaskbarForFreeformDisplay(context) ||
-            (DisplayController.showLockedTaskbarOnHome(context) &&
+            activityContext.showDesktopTaskbarForFreeformDisplay() ||
+            (activityContext.showLockedTaskbarOnHome() &&
                 taskbarControllers.taskbarStashController.isOnHome)
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 7a23006..038e374 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -44,7 +44,6 @@
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
 import com.android.launcher3.util.ResourceBasedOverride
@@ -319,7 +318,7 @@
     fun maybeShowSearchEdu() {
         if (
             !enableTaskbarPinning() ||
-                !DisplayController.isPinnedTaskbar(activityContext) ||
+                !activityContext.isPinnedTaskbar ||
                 !isTooltipEnabled ||
                 !shouldShowSearchEdu ||
                 userHasSeenSearchEdu ||
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index b7000db..76489e0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -19,6 +19,7 @@
 import static android.view.MotionEvent.ACTION_HOVER_EXIT;
 import static android.view.View.ALPHA;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
 import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
 
@@ -98,20 +99,18 @@
 
     @Override
     public boolean onHover(View v, MotionEvent event) {
-        boolean isFolderOpen = AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER);
         // If hover leaves a taskbar icon animate the tooltip closed.
         if (event.getAction() == ACTION_HOVER_EXIT) {
             mHoverToolTipView.close(/* animate= */ false);
             mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false);
-        } else if (!isFolderOpen && event.getAction() == ACTION_HOVER_ENTER) {
-            // Do not reveal if any floating views such as folders or edu pop-ups are open.
-            revealHoverToolTip();
+        } else if (event.getAction() == ACTION_HOVER_ENTER) {
+            maybeRevealHoverToolTip();
             mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
         }
         return false;
     }
 
-    private void revealHoverToolTip() {
+    private void maybeRevealHoverToolTip() {
         if (mHoverView == null || mToolTipText == null) {
             return;
         }
@@ -122,6 +121,12 @@
         if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
             return;
         }
+        // Do not reveal if floating views such as folders or app pop-ups are open,
+        // as these views will overlap and not look great.
+        if (AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER | TYPE_ACTION_POPUP)) {
+            return;
+        }
+
         Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
         mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
                 mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 3af2ab6..f342fa5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -51,7 +51,6 @@
 import com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.launcher3.testing.shared.ResourceUtils
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Executors
 import java.io.PrintWriter
 import kotlin.jvm.optionals.getOrNull
@@ -150,7 +149,7 @@
         if (
             taskbarStashController.isInApp ||
                 controllers.uiController.isInOverviewUi ||
-                DisplayController.showLockedTaskbarOnHome(context)
+                context.showLockedTaskbarOnHome()
         ) {
             // only add the taskbar touch region if not on home
             val bottom = windowLayoutParams.height
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index b510e7e..98415d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -40,7 +40,6 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.content.Context;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.animation.Interpolator;
@@ -61,7 +60,6 @@
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
@@ -225,11 +223,9 @@
                     updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true);
                     if (!mShouldDelayLauncherStateAnim) {
                         if (toState == LauncherState.NORMAL) {
+                            TaskbarActivityContext activity = mControllers.taskbarActivityContext;
                             boolean isPinnedTaskbarAndNotInDesktopMode =
-                                    DisplayController.isPinnedTaskbar(
-                                            mControllers.taskbarActivityContext)
-                                            && !DisplayController.isInDesktopMode(
-                                            mControllers.taskbarActivityContext);
+                                    !activity.isInDesktopMode() && activity.isPinnedTaskbar();
                             applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
                                     isPinnedTaskbarAndNotInDesktopMode));
                         } else {
@@ -475,8 +471,8 @@
         final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat();
         final float toAlignment = isIconAlignedWithHotseat ? 1 : 0;
         boolean handleOpenFloatingViews = false;
-        boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(
-                mControllers.taskbarActivityContext);
+        boolean isPinnedTaskbar =
+                mControllers.taskbarActivityContext.isPinnedTaskbar();
         if (DEBUG) {
             Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher
                     + ", mLauncherState: " + mLauncherState
@@ -590,7 +586,8 @@
         float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
         AnimatedFloat taskbarBgOffset =
                 mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset();
-        boolean showTaskbar = shouldShowTaskbar(mLauncher, isInLauncher, isInOverview);
+        boolean showTaskbar = shouldShowTaskbar(mControllers.taskbarActivityContext, isInLauncher,
+                isInOverview);
         float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f;
         float taskbarBgOffsetStart = showTaskbar ? 1f : 0f;
 
@@ -727,13 +724,13 @@
         return animatorSet;
     }
 
-    private static boolean shouldShowTaskbar(Context context, boolean isInLauncher,
-            boolean isInOverview) {
-        if (DisplayController.showDesktopTaskbarForFreeformDisplay(context)) {
+    private static boolean shouldShowTaskbar(TaskbarActivityContext activityContext,
+            boolean isInLauncher, boolean isInOverview) {
+        if (activityContext.showDesktopTaskbarForFreeformDisplay()) {
             return true;
         }
 
-        if (DisplayController.showLockedTaskbarOnHome(context) && isInLauncher) {
+        if (activityContext.showLockedTaskbarOnHome() && isInLauncher) {
             return true;
         }
         return !isInLauncher || isInOverview;
@@ -788,11 +785,11 @@
      * This refers to the intended state - a transition to this state might be in progress.
      */
     public boolean isTaskbarAlignedWithHotseat() {
-        if (DisplayController.showDesktopTaskbarForFreeformDisplay(mLauncher)) {
+        if (mControllers.taskbarActivityContext.showDesktopTaskbarForFreeformDisplay()) {
             return false;
         }
 
-        if (DisplayController.showLockedTaskbarOnHome(mLauncher) && isInLauncher()) {
+        if (mControllers.taskbarActivityContext.showLockedTaskbarOnHome() && isInLauncher()) {
             return false;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index b5e271d..1521715 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -227,7 +227,7 @@
                     }
                     // Only Handles Special Exit Cases for Desktop Mode Taskbar Recreation.
                     if (taskbarActivityContext != null
-                            && !DisplayController.showLockedTaskbarOnHome(context)) {
+                            && !taskbarActivityContext.showLockedTaskbarOnHome()) {
                         recreateTaskbars();
                     }
                 } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 1a6cd60..7f240bd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -83,7 +83,7 @@
     // Initialized in init.
     private TaskbarControllers mControllers;
     private boolean mAllowInitialSplitSelection;
-    private AppInfo[] mAppInfosList;
+    private AppInfo[] mAppInfosList = AppInfo.EMPTY_ARRAY;
     // Saves the ItemInfos in the hotseat without the predicted items.
     private SparseArray<ItemInfo> mHotseatInfosList;
     private ManageWindowsTaskbarShortcut<BaseTaskbarContext> mManageWindowsTaskbarShortcut;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index eaf00b6..e597148 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -66,7 +66,6 @@
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.SystemUiFlagUtils;
@@ -307,8 +306,8 @@
         // TODO(b/390665752): Feature to "lock" pinned taskbar to home screen will be superseded by
         //     pinning, in other launcher states, at which point this variable can be removed.
         mInAppStateAffectsDesktopTasksVisibilityInTaskbar =
-                !DisplayController.showDesktopTaskbarForFreeformDisplay(mActivity)
-                        && DisplayController.showLockedTaskbarOnHome(mActivity);
+                !mActivity.showDesktopTaskbarForFreeformDisplay()
+                        && mActivity.showLockedTaskbarOnHome();
 
         mTaskbarBackgroundDuration = activity.getResources().getInteger(
                 R.integer.taskbar_background_duration);
@@ -413,7 +412,7 @@
      * Returns how long the stash/unstash animation should play.
      */
     public long getStashDuration() {
-        if (DisplayController.isPinnedTaskbar(mActivity)) {
+        if (mActivity.isPinnedTaskbar()) {
             return PINNED_TASKBAR_TRANSITION_DURATION;
         }
         return mActivity.isTransientTaskbar() ? TRANSIENT_TASKBAR_STASH_DURATION
@@ -1186,7 +1185,7 @@
         }
 
         // Do not stash if pinned taskbar, hardware keyboard is attached and no IME is docked
-        if (mActivity.isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity)
+        if (mActivity.isHardwareKeyboard() && mActivity.isPinnedTaskbar()
                 && !mActivity.isImeDocked()) {
             return false;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index dcb9fbf..d0886e0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -36,7 +36,6 @@
 
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
-import com.android.launcher3.util.DisplayController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
@@ -67,8 +66,8 @@
         InteractionJankMonitorWrapper.begin(v, Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS,
                 /* tag= */ "TASKBAR_BUTTON");
         mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP);
-        if (DisplayController.showLockedTaskbarOnHome(mActivity)
-                || DisplayController.showDesktopTaskbarForFreeformDisplay(mActivity)) {
+        if (mActivity.showLockedTaskbarOnHome()
+                || mActivity.showDesktopTaskbarForFreeformDisplay()) {
             // If the taskbar can be shown on the home screen, use mAllAppsToggler to toggle all
             // apps, which will toggle the launcher activity all apps when on home screen.
             // TODO(b/395913143): Reconsider this if a gap in taskbar all apps functionality that
@@ -248,8 +247,7 @@
 
         /** Returns true if the taskbar pinning popup view was shown for {@code event}. */
         private boolean maybeShowPinningView(@NonNull MotionEvent event) {
-            if (!DisplayController.isPinnedTaskbar(mActivity) || mTaskbarView.isEventOverAnyItem(
-                    event)) {
+            if (!mActivity.isPinnedTaskbar() || mTaskbarView.isEventOverAnyItem(event)) {
                 return false;
             }
             mControllers.taskbarPinningController.showPinningView(mTaskbarView, event.getRawX());
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 6786aed..c5b97e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -19,6 +19,7 @@
 import static android.animation.LayoutTransition.CHANGE_APPEARING;
 import static android.animation.LayoutTransition.CHANGE_DISAPPEARING;
 import static android.animation.LayoutTransition.DISAPPEARING;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.DesktopModeFlags.ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
@@ -86,7 +87,6 @@
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
 import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.MultiPropertyFactory;
@@ -747,7 +747,8 @@
     }
 
     private boolean shouldUpdateIconContentDescription(BubbleTextView btv) {
-        boolean isInDesktopMode = mControllers.taskbarDesktopModeController.isInDesktopMode();
+        boolean isInDesktopMode = mControllers.taskbarDesktopModeController.isInDesktopMode(
+                DEFAULT_DISPLAY);
         boolean isAllAppsButton = btv instanceof TaskbarAllAppsButtonContainer;
         boolean isDividerButton = btv instanceof TaskbarDividerContainer;
         return isInDesktopMode && !isAllAppsButton && !isDividerButton;
@@ -930,7 +931,9 @@
     private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
         PendingAnimation setter = new PendingAnimation(100);
         // icon alignment not needed for pinned taskbar.
-        if (DisplayController.isPinnedTaskbar(mActivity)) return setter.createPlaybackController();
+        if (mActivity.isPinnedTaskbar()) {
+            return setter.createPlaybackController();
+        }
         mOnControllerPreCreateCallback.run();
         DeviceProfile taskbarDp = mActivity.getDeviceProfile();
         Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
index dd91d17..8574b89 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView;
 import com.android.launcher3.taskbar.allapps.TaskbarSearchSessionController;
+import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
 
 /**
@@ -142,6 +143,41 @@
     }
 
     @Override
+    public boolean isTransientTaskbar() {
+        return mTaskbarContext.isTransientTaskbar();
+    }
+
+    @Override
+    public boolean isPinnedTaskbar() {
+        return mTaskbarContext.isPinnedTaskbar();
+    }
+
+    @Override
+    public NavigationMode getNavigationMode() {
+        return mTaskbarContext.getNavigationMode();
+    }
+
+    @Override
+    public boolean isInDesktopMode() {
+        return mTaskbarContext.isInDesktopMode();
+    }
+
+    @Override
+    public boolean showLockedTaskbarOnHome() {
+        return mTaskbarContext.showLockedTaskbarOnHome();
+    }
+
+    @Override
+    public boolean showDesktopTaskbarForFreeformDisplay() {
+        return mTaskbarContext.showDesktopTaskbarForFreeformDisplay();
+    }
+
+    @Override
+    public boolean isPrimaryDisplay() {
+        return mTaskbarContext.isPrimaryDisplay();
+    }
+
+    @Override
     public void onDragStart() {}
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
index 675d55b..de20f77 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
@@ -200,7 +200,7 @@
     private LayoutParams createLayoutParams() {
         LayoutParams layoutParams = new LayoutParams(
                 TYPE_APPLICATION_OVERLAY,
-                LayoutParams.FLAG_SPLIT_TOUCH,
+                /* flags = */ 0,
                 PixelFormat.TRANSLUCENT);
         layoutParams.setTitle(WINDOW_TITLE);
         layoutParams.gravity = Gravity.BOTTOM;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java
deleted file mode 100644
index 45813ce..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
-
-import android.appwidget.AppWidgetHost;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.Context;
-import android.os.Looper;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.LauncherWidgetHolder;
-
-import java.util.function.IntConsumer;
-
-/**
- * {@link AppWidgetHost} that is used to receive the changes to the widgets without
- * storing any {@code Activity} info like that of the launcher.
- */
-final class QuickstepAppWidgetHost extends AppWidgetHost {
-    private final @NonNull Context mContext;
-    private final @NonNull IntConsumer mAppWidgetRemovedCallback;
-    private final @NonNull LauncherWidgetHolder.ProviderChangedListener mProvidersChangedListener;
-
-    QuickstepAppWidgetHost(@NonNull Context context, @NonNull IntConsumer appWidgetRemovedCallback,
-            @NonNull LauncherWidgetHolder.ProviderChangedListener listener,
-            @NonNull Looper looper) {
-        super(context, APPWIDGET_HOST_ID, null, looper);
-        mContext = context;
-        mAppWidgetRemovedCallback = appWidgetRemovedCallback;
-        mProvidersChangedListener = listener;
-    }
-
-    @Override
-    protected void onProvidersChanged() {
-        mProvidersChangedListener.notifyWidgetProvidersChanged();
-    }
-
-    @Override
-    public void onAppWidgetRemoved(int appWidgetId) {
-        // Route the call via model thread, in case it comes while a loader-bind is in progress
-        Executors.MODEL_EXECUTOR.execute(
-                () -> Executors.MAIN_EXECUTOR.execute(
-                        () -> mAppWidgetRemovedCallback.accept(appWidgetId)));
-    }
-
-    @Override
-    protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) {
-        LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
-                mContext, appWidget);
-        super.onProviderChanged(appWidgetId, info);
-        // The super method updates the dimensions of the providerInfo. Update the
-        // launcher spans accordingly.
-        info.initSpans(mContext, LauncherAppState.getIDP(mContext));
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHostProvider.kt b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHostProvider.kt
new file mode 100644
index 0000000..1387cb7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHostProvider.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides
+
+import android.app.ActivityThread
+import android.content.Context
+import android.content.ContextWrapper
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.util.LooperExecutor
+import com.android.launcher3.widget.LauncherWidgetHolder
+import com.android.launcher3.widget.ListenableAppWidgetHost
+
+object QuickstepAppWidgetHostProvider {
+
+    /** Static widget host which is always listening and is lazily created */
+    @JvmStatic
+    val staticQuickstepHost: ListenableAppWidgetHost by lazy {
+        ListenableAppWidgetHost(
+                LooperContext(
+                    ActivityThread.currentApplication(),
+                    ListenableAppWidgetHost.widgetHolderExecutor,
+                ),
+                LauncherWidgetHolder.APPWIDGET_HOST_ID,
+            )
+            .apply { if (BuildConfig.WIDGETS_ENABLED) startListening() }
+    }
+
+    private class LooperContext(ctx: Context, val executor: LooperExecutor) : ContextWrapper(ctx) {
+
+        override fun getMainLooper() = executor.looper
+
+        override fun getMainExecutor() = executor
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 26a1322..2f61eab 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -34,8 +34,11 @@
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
+import java.util.function.Consumer;
+
 /** Provides a Quickstep specific animation when launching an activity from an app widget. */
-class QuickstepInteractionHandler implements RemoteViews.InteractionHandler {
+class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
+        Consumer<LauncherAppWidgetHostView> {
 
     private static final String TAG = "QuickstepInteractionHandler";
 
@@ -45,6 +48,11 @@
         mLauncher = launcher;
     }
 
+    @Override
+    public void accept(LauncherAppWidgetHostView host) {
+        host.setInteractionHandler(this);
+    }
+
     @SuppressWarnings("NewApi")
     @Override
     public boolean onInteraction(View view, PendingIntent pendingIntent,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 806b8ab..605fd31 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -143,7 +143,6 @@
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory;
 import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController;
@@ -172,7 +171,6 @@
 import com.android.launcher3.util.StartActivityParams;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.FloatingIconView;
-import com.android.launcher3.widget.LauncherWidgetHolder;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
@@ -298,6 +296,7 @@
 
     @Override
     protected void setupViews() {
+        getAppWidgetHolder().setOnViewCreationCallback(new QuickstepInteractionHandler(this));
         super.setupViews();
 
         mActionsView = findViewById(R.id.overview_actions_view);
@@ -726,15 +725,6 @@
     }
 
     @Override
-    protected LauncherWidgetHolder createAppWidgetHolder() {
-        final QuickstepHolderFactory factory =
-                (QuickstepHolderFactory) LauncherWidgetHolder.HolderFactory.newFactory(this);
-        return factory.newInstance(this,
-                appWidgetId -> getWorkspace().removeWidget(appWidgetId),
-                new QuickstepInteractionHandler(this));
-    }
-
-    @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         if (savedInstanceState != null) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
index 56fc4d1..c7eedb0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
@@ -16,43 +16,41 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
+import static com.android.launcher3.uioverrides.QuickstepAppWidgetHostProvider.getStaticQuickstepHost;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.widget.ListenableAppWidgetHost.getWidgetHolderExecutor;
 
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
-import android.util.Log;
 import android.util.SparseArray;
 import android.widget.RemoteViews;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
 
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
-import java.util.ArrayList;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.util.Collections;
-import java.util.List;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.function.BiConsumer;
-import java.util.function.IntConsumer;
 
 /**
  * {@link LauncherWidgetHolder} that puts the app widget host in the background
  */
 public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
 
-    private static final String TAG = "QuickstepWidgetHolder";
-
     private static final UpdateKey<AppWidgetProviderInfo> KEY_PROVIDER_UPDATE =
             AppWidgetHostView::onUpdateProviderInfo;
     private static final UpdateKey<RemoteViews> KEY_VIEWS_UPDATE =
@@ -60,51 +58,17 @@
     private static final UpdateKey<Integer> KEY_VIEW_DATA_CHANGED =
             AppWidgetHostView::onViewDataChanged;
 
-    private static final List<QuickstepWidgetHolder> sHolders = new ArrayList<>();
     private static final SparseArray<QuickstepWidgetHolderListener> sListeners =
             new SparseArray<>();
 
-    private static AppWidgetHost sWidgetHost = null;
-
     private final UpdateHandler mUpdateHandler = this::onWidgetUpdate;
-    private final @Nullable RemoteViews.InteractionHandler mInteractionHandler;
-
-    private final @NonNull IntConsumer mAppWidgetRemovedCallback;
 
     // Map to all pending updated keyed with appWidgetId;
     private final SparseArray<PendingUpdate> mPendingUpdateMap = new SparseArray<>();
 
-    private QuickstepWidgetHolder(@NonNull Context context,
-            @Nullable IntConsumer appWidgetRemovedCallback,
-            @Nullable RemoteViews.InteractionHandler interactionHandler) {
-        super(context, appWidgetRemovedCallback);
-        mAppWidgetRemovedCallback = appWidgetRemovedCallback != null ? appWidgetRemovedCallback
-                : i -> {};
-        mInteractionHandler = interactionHandler;
-        MAIN_EXECUTOR.execute(() -> sHolders.add(this));
-    }
-
-    @Override
-    @NonNull
-    protected AppWidgetHost createHost(@NonNull Context context,
-            @Nullable IntConsumer appWidgetRemovedCallback) {
-        if (sWidgetHost == null) {
-            sWidgetHost = new QuickstepAppWidgetHost(context.getApplicationContext(),
-                    i -> MAIN_EXECUTOR.execute(() ->
-                            sHolders.forEach(h -> h.mAppWidgetRemovedCallback.accept(i))),
-                    () -> MAIN_EXECUTOR.execute(() ->
-                            sHolders.forEach(h ->
-                                    // Listeners might remove themselves from the list during the
-                                    // iteration. Creating a copy of the list to avoid exceptions
-                                    // for concurrent modification.
-                                    new ArrayList<>(h.mProviderChangedListeners).forEach(
-                                    ProviderChangedListener::notifyWidgetProvidersChanged))),
-                    getWidgetHolderExecutor().getLooper());
-            if (WIDGETS_ENABLED) {
-                sWidgetHost.startListening();
-            }
-        }
-        return sWidgetHost;
+    @AssistedInject
+    public QuickstepWidgetHolder(@Assisted("UI_CONTEXT") @NonNull Context context) {
+        super(context, getStaticQuickstepHost());
     }
 
     @Override
@@ -168,21 +132,6 @@
         sListeners.remove(appWidgetId);
     }
 
-    /**
-     * Called when the launcher is destroyed
-     */
-    @Override
-    public void destroy() {
-        try {
-            MAIN_EXECUTOR.submit(() -> {
-                clearViews();
-                sHolders.remove(this);
-            }).get();
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to remove self from holder list", e);
-        }
-    }
-
     @Override
     protected boolean shouldListen(int flags) {
         return (flags & (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED))
@@ -199,7 +148,7 @@
         }
 
         getWidgetHolderExecutor().execute(() -> {
-            sWidgetHost.setAppWidgetHidden();
+            mWidgetHost.setAppWidgetHidden();
             setListeningFlag(false);
         });
     }
@@ -217,7 +166,7 @@
         };
         QuickstepWidgetHolderListener holderListener = getHolderListener(appWidgetId);
         holderListener.addHolder(handler);
-        return () -> holderListener.mListeningHolders.remove(handler);
+        return () -> holderListener.removeHolder(handler);
     }
 
     /**
@@ -239,7 +188,6 @@
     protected LauncherAppWidgetHostView createViewInternal(
             int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
         LauncherAppWidgetHostView widgetView = new LauncherAppWidgetHostView(mContext);
-        widgetView.setInteractionHandler(mInteractionHandler);
         widgetView.setAppWidget(appWidgetId, appWidget);
         widgetView.updateAppWidget(getHolderListener(appWidgetId).addHolder(mUpdateHandler));
         return widgetView;
@@ -249,7 +197,7 @@
         QuickstepWidgetHolderListener listener = sListeners.get(appWidgetId);
         if (listener == null) {
             listener = new QuickstepWidgetHolderListener(appWidgetId);
-            sWidgetHost.setListener(appWidgetId, listener);
+            getStaticQuickstepHost().setListener(appWidgetId, listener);
             sListeners.put(appWidgetId, listener);
         }
         return listener;
@@ -262,7 +210,7 @@
     public void clearViews() {
         mViews.clear();
         for (int i = sListeners.size() - 1; i >= 0; i--) {
-            sListeners.valueAt(i).mListeningHolders.remove(mUpdateHandler);
+            sListeners.valueAt(i).removeHolder(mUpdateHandler);
         }
     }
 
@@ -289,13 +237,15 @@
             mWidgetId = widgetId;
         }
 
-        @UiThread
-        @Nullable
         public RemoteViews addHolder(@NonNull UpdateHandler holder) {
-            mListeningHolders.add(holder);
+            MAIN_EXECUTOR.execute(() -> mListeningHolders.add(holder));
             return mRemoteViews;
         }
 
+        public void removeHolder(@NonNull UpdateHandler holder) {
+            MAIN_EXECUTOR.execute(() -> mListeningHolders.remove(holder));
+        }
+
         @Override
         @AnyThread
         public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) {
@@ -322,44 +272,13 @@
         }
     }
 
-    /**
-     * {@code HolderFactory} subclass that takes an interaction handler as one of the parameters
-     * when creating a new instance.
-     */
-    public static class QuickstepHolderFactory extends HolderFactory {
 
-        @SuppressWarnings("unused")
-        public QuickstepHolderFactory(Context context) { }
+    /** A factory that generates new instances of {@code LauncherWidgetHolder} */
+    @AssistedFactory
+    public interface QuickstepWidgetHolderFactory extends WidgetHolderFactory {
 
         @Override
-        public LauncherWidgetHolder newInstance(@NonNull Context context,
-                @Nullable IntConsumer appWidgetRemovedCallback) {
-            return newInstance(context, appWidgetRemovedCallback, null);
-        }
-
-        /**
-         * @param context The context of the caller
-         * @param appWidgetRemovedCallback The callback that is called when widgets are removed
-         * @param interactionHandler The interaction handler when the widgets are clicked
-         * @return A new {@link LauncherWidgetHolder} instance
-         */
-        public LauncherWidgetHolder newInstance(@NonNull Context context,
-                @Nullable IntConsumer appWidgetRemovedCallback,
-                @Nullable RemoteViews.InteractionHandler interactionHandler) {
-
-            if (!FeatureFlags.ENABLE_WIDGET_HOST_IN_BACKGROUND.get()) {
-                return new LauncherWidgetHolder(context, appWidgetRemovedCallback) {
-                    @Override
-                    protected AppWidgetHost createHost(Context context,
-                            @Nullable IntConsumer appWidgetRemovedCallback) {
-                        AppWidgetHost host = super.createHost(context, appWidgetRemovedCallback);
-                        host.setInteractionHandler(interactionHandler);
-                        return host;
-                    }
-                };
-            }
-            return new QuickstepWidgetHolder(context, appWidgetRemovedCallback, interactionHandler);
-        }
+        QuickstepWidgetHolder newInstance(@Assisted("UI_CONTEXT") @NonNull Context context);
     }
 
     private interface UpdateKey<T> extends BiConsumer<AppWidgetHostView, T> { }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
index 23dc81d..c9f791c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -21,6 +21,7 @@
 import com.android.app.animation.Interpolators.INSTANT
 import com.android.app.animation.Interpolators.LINEAR
 import com.android.launcher3.Flags.enableDesktopExplodedView
+import com.android.launcher3.Flags.enableGridOnlyOverview
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.LauncherState
 import com.android.launcher3.anim.AnimatedFloat
@@ -143,7 +144,11 @@
             recentsView,
             TASK_MODALNESS,
             toState.overviewModalness,
-            config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR),
+            config.getInterpolator(
+                ANIM_OVERVIEW_MODAL,
+                if (enableGridOnlyOverview() && !toState.isRecentsViewVisible) FINAL_FRAME
+                else LINEAR,
+            ),
         )
 
         val fromState = launcher.stateManager.state
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 0c0b4fd..ae82f82 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -15,11 +15,11 @@
  */
 package com.android.launcher3.uioverrides.states;
 
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 
 import android.graphics.Rect;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.views.ActivityContext;
@@ -50,6 +50,9 @@
 
     @Override
     public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        if (enableGridOnlyOverview()) {
+            return super.getOverviewScaleAndOffset(launcher);
+        }
         return getOverviewScaleAndOffsetForModalState(launcher.getOverviewPanel());
     }
 
@@ -65,7 +68,7 @@
 
     @Override
     public boolean isTaskbarStashed(Launcher launcher) {
-        if (Flags.enableGridOnlyOverview()) {
+        if (enableGridOnlyOverview()) {
             return true;
         }
         return super.isTaskbarStashed(launcher);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 454a307..76eb138 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -63,6 +63,7 @@
     private var hasDismissThresholdHapticRun = false
     private var initialDisplacement: Float = 0f
     private var recentsScaleAnimation: SpringAnimation? = null
+    private var isBlockedDuringDismissal = false
 
     private fun canInterceptTouch(ev: MotionEvent): Boolean =
         when {
@@ -137,6 +138,7 @@
     }
 
     override fun onDragStart(start: Boolean, startDisplacement: Float) {
+        if (isBlockedDuringDismissal) return
         val taskBeingDragged = taskBeingDragged ?: return
 
         initialDisplacement =
@@ -149,6 +151,7 @@
     }
 
     override fun onDrag(displacement: Float): Boolean {
+        if (isBlockedDuringDismissal) return true
         val taskBeingDragged = taskBeingDragged ?: return false
         val currentDisplacement = displacement + initialDisplacement
         val boundedDisplacement =
@@ -204,6 +207,7 @@
     }
 
     override fun onDragEnd(velocity: Float) {
+        if (isBlockedDuringDismissal) return
         val taskBeingDragged = taskBeingDragged ?: return
 
         val currentDisplacement =
@@ -234,6 +238,7 @@
                         if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f
                     )
                 }
+        isBlockedDuringDismissal = true
         recentsScaleAnimation =
             recentsView.animateRecentsScale(RECENTS_SCALE_DEFAULT).addEndListener { _, _, _, _ ->
                 recentsScaleAnimation = null
@@ -246,6 +251,7 @@
         taskBeingDragged?.translationZ = 0f
         taskBeingDragged = null
         springAnimation = null
+        isBlockedDuringDismissal = false
     }
 
     private fun getRecentsScale(dismissFraction: Float): Float {
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index f96bbcb..943f543 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -68,8 +68,10 @@
      * running tasks
      */
     public RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy) {
+        // TODO: b/403344864 Make sure init with correct number of RemoteTargetHandle with
+        //  multi-desks feature enabled as well.
         int visibleTasksCount = DesktopVisibilityController.INSTANCE.get(context)
-                .getVisibleDesktopTasksCount();
+                .getVisibleDesktopTasksCountDeprecated();
         if (visibleTasksCount > 0) {
             // Allocate +1 to account for a new task added to the desktop mode
             int numHandles = visibleTasksCount + 1;
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 554cea2..7db1813 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -19,6 +19,7 @@
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Flags.enableDesktopExplodedView;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -118,7 +119,9 @@
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
         setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
-                config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
+                config.getInterpolator(ANIM_OVERVIEW_MODAL,
+                        enableGridOnlyOverview() && !state.isRecentsViewVisible() ? FINAL_FRAME
+                                : LINEAR));
         setter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, state.isFullScreen() ? 1 : 0, LINEAR);
         boolean showAsGrid =
                 state.displayOverviewTasksAsGrid(mRecentsViewContainer.getDeviceProfile());
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 29b6626..695c77c 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -17,6 +17,7 @@
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
 import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
@@ -252,7 +253,14 @@
     @Override
     public void onStateTransitionStart(RecentsState toState) {
         setOverviewStateEnabled(true);
-        setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+        if (enableGridOnlyOverview()) {
+            if (toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
+                setOverviewGridEnabled(true);
+            }
+        } else {
+            setOverviewGridEnabled(
+                    toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+        }
         setOverviewFullscreenEnabled(toState.isFullScreen());
         if (toState == MODAL_TASK) {
             setOverviewSelectEnabled(true);
@@ -271,6 +279,11 @@
     @Override
     public void onStateTransitionComplete(RecentsState finalState) {
         DesktopVisibilityController.INSTANCE.get(mContainer).onLauncherStateChanged(finalState);
+        if (enableGridOnlyOverview()) {
+            if (!finalState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
+                setOverviewGridEnabled(false);
+            }
+        }
         if (!finalState.isRecentsViewVisible()) {
             // Clean-up logic that occurs when recents is no longer in use/visible.
             reset();
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index 2c1a4eb..00aa109 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.Flags.enableDesktopExplodedView;
 import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
 import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
@@ -197,6 +198,9 @@
 
         @Override
         public float[] getOverviewScaleAndOffset(RecentsViewContainer container) {
+            if (enableGridOnlyOverview()) {
+                return super.getOverviewScaleAndOffset(container);
+            }
             return getOverviewScaleAndOffsetForModalState(container.getOverviewPanel());
         }
     }
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 553a620..00ef13b 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -31,7 +31,7 @@
 import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
 import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper.PreviewPositionHelperFactory
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
@@ -203,7 +203,7 @@
                     GetThumbnailPositionUseCase(
                         deviceProfileRepository = inject(scopeId),
                         rotationStateRepository = inject(scopeId),
-                        previewPositionHelper = PreviewPositionHelper(),
+                        previewPositionHelperFactory = PreviewPositionHelperFactory(),
                     )
                 OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
                 else -> {
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
index 8501382..e83d9f0 100644
--- a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
@@ -27,7 +27,7 @@
 class GetThumbnailPositionUseCase(
     private val deviceProfileRepository: RecentsDeviceProfileRepository,
     private val rotationStateRepository: RecentsRotationStateRepository,
-    private val previewPositionHelper: PreviewPositionHelper,
+    private val previewPositionHelperFactory: PreviewPositionHelper.PreviewPositionHelperFactory,
 ) {
     operator fun invoke(
         thumbnailData: ThumbnailData?,
@@ -38,6 +38,7 @@
         val thumbnail =
             thumbnailData?.thumbnail ?: return ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
 
+        val previewPositionHelper = previewPositionHelperFactory.create()
         previewPositionHelper.updateThumbnailMatrix(
             Rect(0, 0, thumbnail.width, thumbnail.height),
             thumbnailData,
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 74de2ac..c1282b9 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -18,6 +18,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
 import static com.android.launcher3.LauncherState.ADD_DESK_BUTTON;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -151,7 +152,14 @@
     public void onStateTransitionStart(LauncherState toState) {
         setOverviewStateEnabled(toState.isRecentsViewVisible);
 
-        setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+        if (enableGridOnlyOverview()) {
+            if (toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
+                setOverviewGridEnabled(true);
+            }
+        } else {
+            setOverviewGridEnabled(
+                    toState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+        }
         setOverviewFullscreenEnabled(toState.getOverviewFullscreenProgress() == 1);
         if (toState == OVERVIEW_MODAL_TASK) {
             setOverviewSelectEnabled(true);
@@ -170,6 +178,11 @@
     @Override
     public void onStateTransitionComplete(LauncherState finalState) {
         DesktopVisibilityController.INSTANCE.get(mContainer).onLauncherStateChanged(finalState);
+        if (enableGridOnlyOverview()) {
+            if (!finalState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
+                setOverviewGridEnabled(false);
+            }
+        }
 
         if (!finalState.isRecentsViewVisible) {
             // Clean-up logic that occurs when recents is no longer in use/visible.
diff --git a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
index d39b528..db18394 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
@@ -82,6 +82,7 @@
                             runTaskGridReflowSpringAnimation(
                                 draggedTaskView,
                                 getDismissedTaskGapForReflow(draggedTaskView),
+                                onEndRunnable,
                             )
                         } else {
                             recentsView.dismissTaskView(
@@ -89,11 +90,12 @@
                                 /* animateTaskView = */ false,
                                 /* removeTask = */ true,
                             )
+                            onEndRunnable()
                         }
                     } else {
                         recentsView.onDismissAnimationEnds()
+                        onEndRunnable()
                     }
-                    onEndRunnable()
                 }
         if (!isDismissing) {
             addNeighborSettlingSpringAnimations(
@@ -295,7 +297,7 @@
         val maxDismissSettlingVelocity =
             recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
         MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
-            .playToken(
+            ?.playToken(
                 MSDLToken.CANCEL,
                 InteractionProperties.DynamicVibrationScale(
                     boundToRange(abs(velocity) / maxDismissSettlingVelocity, 0f, 1f),
@@ -339,6 +341,7 @@
     private fun runTaskGridReflowSpringAnimation(
         dismissedTaskView: TaskView,
         dismissedTaskGap: Float,
+        onEndRunnable: () -> Unit,
     ) {
         // Empty spring animation exists for conditional start, and to drive neighboring springs.
         val springAnimationDriver =
@@ -427,12 +430,17 @@
                 driverProgressThreshold = dismissedTaskGap,
                 isSpringDirectionVertical = false,
             )
+        } else {
+            springAnimationDriver.addEndListener { _, _, _, _ ->
+                // Play the same haptic as when neighbors spring into place.
+                MSDLPlayerWrapper.INSTANCE.get(recentsView.context)?.playToken(MSDLToken.CANCEL)
+            }
         }
 
         // Start animations and remove the dismissed task at the end, dismiss immediately if no
         // neighboring tasks exist.
         val runGridEndAnimationAndRelayout = {
-            recentsView.expressiveDismissTaskView(dismissedTaskView)
+            recentsView.expressiveDismissTaskView(dismissedTaskView, onEndRunnable)
         }
         springAnimationDriver?.apply {
             addEndListener { _, _, _, _ -> runGridEndAnimationAndRelayout() }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8c7fe26..aa25738 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -275,7 +275,7 @@
         HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener, DesktopVisibilityListener {
 
-    private static final String TAG = "RecentsView";
+    protected static final String TAG = "RecentsView";
     private static final boolean DEBUG = false;
 
     public static final FloatProperty<RecentsView<?, ?>> CONTENT_ALPHA =
@@ -534,7 +534,6 @@
     protected final Rect mLastComputedTaskSize = new Rect();
     protected final Rect mLastComputedGridSize = new Rect();
     protected final Rect mLastComputedGridTaskSize = new Rect();
-    private TaskView mSelectedTask = null;
     // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
     @Nullable
     protected Float mLastComputedTaskStartPushOutDistance = null;
@@ -2418,21 +2417,28 @@
     }
 
     /**
-     * Sets the last TaskView selected.
+     * Returns the currently selected TaskView in Select mode.
+     */
+    @Nullable
+    public TaskView getSelectedTaskView() {
+        return mUtils.getSelectedTaskView();
+    }
+
+    /**
+     * Sets the selected TaskView in Select mode.
      */
     public void setSelectedTask(int lastSelectedTaskId) {
-        mSelectedTask = getTaskViewByTaskId(lastSelectedTaskId);
+        mUtils.setSelectedTaskView(getTaskViewByTaskId(lastSelectedTaskId));
     }
 
     /**
      * Returns the bounds of the task selected to enter modal state.
      */
     public Rect getSelectedTaskBounds() {
-        if (mSelectedTask == null) {
-            return enableGridOnlyOverview() && mContainer.getDeviceProfile().isTablet
-                    ? mLastComputedGridTaskSize : mLastComputedTaskSize;
+        if (getSelectedTaskView() == null) {
+            return mLastComputedTaskSize;
         }
-        return getTaskBounds(mSelectedTask);
+        return getTaskBounds(getSelectedTaskView());
     }
 
     /**
@@ -2448,7 +2454,7 @@
         return deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f;
     }
 
-    private Rect getTaskBounds(TaskView taskView) {
+    protected Rect getTaskBounds(TaskView taskView) {
         int selectedPage = indexOfChild(taskView);
         int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
         int selectedPageScroll = getScrollForPage(selectedPage);
@@ -3937,7 +3943,7 @@
                 newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
             }
         }
-        if (lastGridTaskView != null && (lastGridTaskView.isVisibleToUser() || (
+        if (lastGridTaskView != null && (isTaskViewVisible(lastGridTaskView) || (
                 isExpressiveDismiss && lastGridTaskView == dismissedTaskView))) {
             // After dismissal, animate translation of the remaining tasks to fill any gap left
             // between the end of the grid and the clear all button. Only animate if the clear
@@ -4734,11 +4740,12 @@
         runDismissAnimation(pa);
     }
 
-    protected void expressiveDismissTaskView(TaskView taskView) {
+    protected void expressiveDismissTaskView(TaskView taskView, Function0<Unit> onEndRunnable) {
         PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION);
         createTaskDismissAnimation(pa, taskView, false /* animateTaskView */, true /* removeTask */,
                 DISMISS_TASK_DURATION, false /* dismissingForSplitSelection*/,
                 true /* isExpressiveDismiss */);
+        pa.addEndListener((success) -> onEndRunnable.invoke());
         runDismissAnimation(pa);
     }
 
@@ -5011,15 +5018,8 @@
     }
 
     private void updatePivots() {
-        if (mOverviewSelectEnabled) {
-            if (enableGridOnlyOverview()) {
-                getModalTaskSize(mTempRect);
-                Rect selectedTaskPosition = getSelectedTaskBounds();
-                Utilities.getPivotsForScalingRectToRect(mTempRect, selectedTaskPosition,
-                        mTempPointF);
-            } else {
-                mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
-            }
+        if (mOverviewSelectEnabled && !enableGridOnlyOverview()) {
+            mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
         } else {
             mTempRect.set(mLastComputedTaskSize);
             getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
@@ -5065,7 +5065,7 @@
                 && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile())
                 && mTaskModalness > 0;
         if (shouldCalculateOffsetForAllTasks) {
-            modalMidpoint = indexOfChild(mSelectedTask);
+            modalMidpoint = indexOfChild(getSelectedTaskView());
         }
 
         float midpointOffsetSize = 0;
@@ -5273,7 +5273,7 @@
      */
     private float getVerticalOffsetSize(TaskView taskView, float offsetProgress) {
         if (offsetProgress == 0 || !(showAsGrid() && enableGridOnlyOverview())
-                || mSelectedTask == null) {
+                || getSelectedTaskView() == null) {
             // Don't bother calculating everything below if we won't offset vertically.
             return 0;
         }
@@ -5281,7 +5281,7 @@
         // First, get the position of the task relative to the top row.
         Rect taskPosition = getTaskBounds(taskView);
 
-        boolean isSelectedTaskTopRow = mTopRowIdSet.contains(mSelectedTask.getTaskViewId());
+        boolean isSelectedTaskTopRow = mTopRowIdSet.contains(getSelectedTaskView().getTaskViewId());
         boolean isChildTopRow = mTopRowIdSet.contains(taskView.getTaskViewId());
         // Whether the task should be shifted to the top.
         boolean isTopShift = !isSelectedTaskTopRow && isChildTopRow;
@@ -5335,8 +5335,8 @@
      * Resets the visuals when exit modal state.
      */
     public void resetModalVisuals() {
-        if (mSelectedTask != null) {
-            mSelectedTask.taskContainers.forEach(
+        if (getSelectedTaskView() != null) {
+            getSelectedTaskView().taskContainers.forEach(
                     taskContainer -> taskContainer.getOverlay().resetModalVisuals());
         }
     }
@@ -6717,8 +6717,14 @@
     private void setTaskModalness(float modalness) {
         mTaskModalness = modalness;
         updatePageOffsets();
-        if (mSelectedTask != null) {
-            mSelectedTask.setModalness(modalness);
+        if (getSelectedTaskView() != null) {
+            if (enableGridOnlyOverview()) {
+                for (TaskView taskView : getTaskViews()) {
+                    taskView.setModalness(modalness);
+                }
+            } else {
+                getSelectedTaskView().setModalness(modalness);
+            }
         } else if (getCurrentPageTaskView() != null) {
             getCurrentPageTaskView().setModalness(modalness);
         }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 9c35913..51a5d0f 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.views
 
+import android.graphics.PointF
 import android.graphics.Rect
 import android.util.FloatProperty
 import android.view.KeyEvent
@@ -25,9 +26,11 @@
 import androidx.core.view.children
 import com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU
 import com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType
+import com.android.launcher3.Flags.enableGridOnlyOverview
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
+import com.android.launcher3.Utilities.getPivotsForScalingRectToRect
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID
 import com.android.launcher3.util.IntArray
@@ -38,6 +41,7 @@
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
 import java.util.function.BiConsumer
+import kotlin.math.min
 import kotlin.reflect.KMutableProperty1
 
 /**
@@ -393,6 +397,47 @@
             taskViews.filterIsInstance<DesktopTaskView>().forEach { it.explodeProgress = field }
         }
 
+    var selectedTaskView: TaskView? = null
+        set(newValue) {
+            val oldValue = field
+            field = newValue
+            if (oldValue != newValue) {
+                onSelectedTaskViewUpdated(oldValue, newValue)
+            }
+        }
+
+    private fun onSelectedTaskViewUpdated(
+        oldSelectedTaskView: TaskView?,
+        newSelectedTaskView: TaskView?,
+    ) {
+        if (!enableGridOnlyOverview()) return
+        with(recentsView) {
+            oldSelectedTaskView?.modalScale = 1f
+            oldSelectedTaskView?.modalPivot = null
+
+            if (newSelectedTaskView == null) return
+
+            val modalTaskBounds = mTempRect
+            getModalTaskSize(modalTaskBounds)
+            val selectedTaskBounds = getTaskBounds(newSelectedTaskView)
+
+            // Map bounds to selectedTaskView's coordinate system.
+            modalTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top)
+            selectedTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top)
+
+            val modalScale =
+                min(
+                    (modalTaskBounds.height().toFloat() / selectedTaskBounds.height()),
+                    (modalTaskBounds.width().toFloat() / selectedTaskBounds.width()),
+                )
+            val modalPivot = PointF()
+            getPivotsForScalingRectToRect(modalTaskBounds, selectedTaskBounds, modalPivot)
+
+            newSelectedTaskView.modalScale = modalScale
+            newSelectedTaskView.modalPivot = modalPivot
+        }
+    }
+
     companion object {
         class RecentsViewFloatProperty(
             private val utilsProperty: KMutableProperty1<RecentsViewUtils, Float>
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index f244298..e6ef708 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -33,6 +33,7 @@
 import android.view.Display
 import android.view.MotionEvent
 import android.view.View
+import android.view.View.OnClickListener
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.view.accessibility.AccessibilityNodeInfo
@@ -148,6 +149,9 @@
     val isRunningTask: Boolean
         get() = this === recentsView?.runningTaskView
 
+    private val isSelectedTask: Boolean
+        get() = this === recentsView?.selectedTaskView
+
     open val displayId: Int
         get() = taskContainers.firstOrNull()?.task.displayId
 
@@ -338,6 +342,12 @@
             onModalnessUpdated(field)
         }
 
+    var modalPivot: PointF? = null
+        set(value) {
+            field = value
+            updatePivots()
+        }
+
     var splitSplashAlpha = 0f
         set(value) {
             field = value
@@ -362,6 +372,12 @@
             applyScale()
         }
 
+    var modalScale = 1f
+        set(value) {
+            field = value
+            applyScale()
+        }
+
     private var dismissTranslationX = 0f
         set(value) {
             field = value
@@ -447,9 +463,10 @@
         }
 
     private val taskViewAlpha = MultiValueAlpha(this, Alpha.entries.size)
-    protected var stableAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.STABLE)
-    var attachAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.ATTACH)
-    var splitAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.SPLIT)
+    protected var stableAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Stable)
+    var attachAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Attach)
+    var splitAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Split)
+    private var modalAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Modal)
 
     protected var shouldShowScreenshot = false
         get() = !isRunningTask || field
@@ -629,14 +646,7 @@
 
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
         super.onLayout(changed, left, top, right, bottom)
-        val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-        if (container.deviceProfile.isTablet) {
-            pivotX = (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat()
-            pivotY = thumbnailTopMargin.toFloat()
-        } else {
-            pivotX = (right - left) * 0.5f
-            pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f
-        }
+        updatePivots()
         systemGestureExclusionRects =
             SYSTEM_GESTURE_EXCLUSION_RECT.onEach {
                 it.right = width
@@ -647,6 +657,24 @@
         }
     }
 
+    private fun updatePivots() {
+        val modalPivot = modalPivot
+        if (modalPivot != null) {
+            pivotX = modalPivot.x
+            pivotY = modalPivot.y
+        } else {
+            val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
+            if (container.deviceProfile.isTablet) {
+                pivotX =
+                    (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat()
+                pivotY = thumbnailTopMargin.toFloat()
+            } else {
+                pivotX = (right - left) * 0.5f
+                pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f
+            }
+        }
+    }
+
     override fun onRecycle() {
         resetPersistentViewTransforms()
 
@@ -654,6 +682,9 @@
         attachAlpha = 1f
         splitAlpha = 1f
         splitSplashAlpha = 0f
+        modalAlpha = 1f
+        modalScale = 1f
+        modalPivot = null
         taskThumbnailSplashAlpha = 0f
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
@@ -1287,7 +1318,7 @@
                 targets.apps,
                 targets.wallpapers,
                 targets.nonApps,
-                true /* launcherClosing */,
+                true, /* launcherClosing */
                 recentsView.stateManager,
                 recentsView,
                 recentsView.depthController,
@@ -1723,7 +1754,7 @@
     fun getSizeAdjustment(fullscreenEnabled: Boolean) = if (fullscreenEnabled) nonGridScale else 1f
 
     private fun applyScale() {
-        val scale = persistentScale * dismissScale
+        val scale = persistentScale * dismissScale * Utilities.mapRange(modalness, 1f, modalScale)
         scaleX = scale
         scaleY = scale
         updateFullscreenParams()
@@ -1785,9 +1816,13 @@
     private fun onModalnessUpdated(modalness: Float) {
         isClickable = modalness == 0f
         taskContainers.forEach {
-            it.iconView.setModalAlpha(1 - modalness)
+            it.iconView.setModalAlpha(1f - modalness)
             it.digitalWellBeingToast?.bannerOffsetPercentage = modalness
         }
+        if (enableGridOnlyOverview()) {
+            modalAlpha = if (isSelectedTask) 1f else (1f - modalness)
+            applyScale()
+        }
     }
 
     fun resetPersistentViewTransforms() {
@@ -1843,9 +1878,10 @@
         private const val TAG = "TaskView"
 
         private enum class Alpha {
-            STABLE,
-            ATTACH,
-            SPLIT,
+            Stable,
+            Attach,
+            Split,
+            Modal,
         }
 
         private enum class SettledProgress {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt
new file mode 100644
index 0000000..d2b9fcf
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_EXIT
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.R
+import com.android.launcher3.apppairs.AppPairIcon
+import com.android.launcher3.folder.FolderIcon
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatAppPairsItem
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatFolderItem
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+class TaskbarHoverToolTipControllerTest {
+
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
+    @InjectController lateinit var popupController: TaskbarPopupController
+
+    private val taskbarContext: TaskbarActivityContext by taskbarUnitTestRule::activityContext
+
+    private lateinit var taskbarView: TaskbarView
+    private lateinit var iconView: BubbleTextView
+    private lateinit var appPairIcon: AppPairIcon
+    private lateinit var folderIcon: FolderIcon
+
+    private val isHoverToolTipOpen: Boolean
+        get() {
+            // TaskbarHoverToolTip uses ArrowTipView which is type TYPE_ON_BOARD_POPUP.
+            return AbstractFloatingView.hasOpenView(
+                taskbarContext,
+                AbstractFloatingView.TYPE_ON_BOARD_POPUP,
+            )
+        }
+
+    @Before
+    fun setup() {
+        runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) }
+
+        val hotseatItems =
+            arrayOf(
+                createHotseatWorkspaceItem(),
+                createHotseatAppPairsItem(),
+                createHotseatFolderItem(),
+            )
+        runOnMainSync {
+            taskbarView.updateItems(hotseatItems, emptyList())
+            iconView =
+                taskbarView.iconViews.filterIsInstance<BubbleTextView>().first {
+                    it.tag is WorkspaceItemInfo
+                }
+            appPairIcon = taskbarView.iconViews.filterIsInstance<AppPairIcon>().first()
+            folderIcon = taskbarView.iconViews.filterIsInstance<FolderIcon>().first()
+        }
+    }
+
+    @Test
+    fun onHover_hoverEnterIcon_revealToolTip_hoverExitIcon_closeToolTip() {
+        runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) }
+        assertThat(isHoverToolTipOpen).isTrue()
+        assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue()
+        runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_EXIT) }
+        assertThat(isHoverToolTipOpen).isFalse()
+        assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse()
+    }
+
+    @Test
+    fun onHover_hoverEnterFolderIcon_revealToolTip_hoverExitFolderIcon_closeToolTip() {
+        runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_ENTER) }
+        assertThat(isHoverToolTipOpen).isTrue()
+        assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue()
+        runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_EXIT) }
+        assertThat(isHoverToolTipOpen).isFalse()
+        assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse()
+    }
+
+    @Test
+    fun onHover_hoverEnterAppPair_revealToolTip_hoverExitAppPair_closeToolTip() {
+        runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_ENTER) }
+        assertThat(isHoverToolTipOpen).isTrue()
+        assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue()
+        runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_EXIT) }
+        assertThat(isHoverToolTipOpen).isFalse()
+        assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse()
+    }
+
+    @Test
+    fun onHover_hoverEnterIconAlignedWithHotseat_noToolTip() {
+        taskbarContext.setUIController(
+            object : TaskbarUIController() {
+                override fun isIconAlignedWithHotseat(): Boolean = true
+            }
+        )
+
+        runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) }
+        assertThat(isHoverToolTipOpen).isFalse()
+    }
+
+    @Test
+    fun onHover_hoverEnterFolderOpen_noToolTip() {
+        runOnMainSync {
+            folderIcon.folder.animateOpen()
+            iconView.dispatchGenericMotionEvent(HOVER_ENTER)
+        }
+        assertThat(isHoverToolTipOpen).isFalse()
+    }
+
+    @Test
+    fun onHover_hoverEnterPopupOpen_noToolTip() {
+        runOnMainSync {
+            popupController.showForIcon(iconView)
+            iconView.dispatchGenericMotionEvent(HOVER_ENTER)
+        }
+        assertThat(isHoverToolTipOpen).isFalse()
+    }
+
+    companion object {
+        private val HOVER_EXIT = MotionEvent.obtain(0, 0, ACTION_HOVER_EXIT, 0f, 0f, 0)
+        private val HOVER_ENTER = MotionEvent.obtain(0, 0, ACTION_HOVER_ENTER, 0f, 0f, 0)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 8758d7c..334d8ab 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -960,7 +960,7 @@
     private fun setInDesktopMode(inDesktopMode: Boolean) {
         whenever(taskbarControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar())
             .thenReturn(inDesktopMode)
-        whenever(taskbarControllers.taskbarDesktopModeController.isInDesktopMode)
+        whenever(taskbarControllers.taskbarDesktopModeController.isInDesktopMode(DEFAULT_DISPLAY))
             .thenReturn(inDesktopMode)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
index e52aacf..92abbba 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
@@ -18,8 +18,13 @@
 
 import android.content.ComponentName
 import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Bitmap.createBitmap
 import android.os.Process
+import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.AppPairInfo
+import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
@@ -53,7 +58,27 @@
         return WorkspaceItemInfo(
                 AppInfo(TEST_COMPONENT, "Test App $id", Process.myUserHandle(), Intent())
             )
-            .apply { this.id = id }
+            .apply {
+                this.id = id
+                // Create a placeholder icon so that the test  doesn't try to load a high-res icon.
+                this.bitmap = BitmapInfo.fromBitmap(createBitmap(1, 1, Bitmap.Config.ALPHA_8))
+            }
+    }
+
+    fun createHotseatAppPairsItem(): AppPairInfo {
+        return AppPairInfo().apply {
+            add(createHotseatWorkspaceItem(1))
+            add(createHotseatWorkspaceItem(2))
+        }
+    }
+
+    fun createHotseatFolderItem(): FolderInfo {
+        return FolderInfo().apply {
+            title = "Test Folder"
+            add(createHotseatWorkspaceItem(1))
+            add(createHotseatWorkspaceItem(2))
+            add(createHotseatWorkspaceItem(3))
+        }
     }
 
     /** Creates a list of fake recent tasks. */
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index 2dacf69..19c8824 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -28,6 +28,7 @@
 import com.android.launcher3.taskbar.TaskbarControllers
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
+import com.android.launcher3.taskbar.TaskbarUIController
 import com.android.launcher3.taskbar.bubbles.BubbleControllers
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
@@ -117,6 +118,8 @@
                                 super.recreateTaskbars()
                                 if (currentActivityContext != null) {
                                     injectControllers()
+                                    // TODO(b/346394875): we should test a non-default uiController.
+                                    activityContext.setUIController(TaskbarUIController.DEFAULT)
                                     controllerInjectionCallback.invoke()
                                 }
                             }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
index a253280..7646e69 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -25,11 +25,14 @@
 import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper.PreviewPositionHelperFactory
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -38,15 +41,21 @@
 class GetThumbnailPositionUseCaseTest {
     private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
     private val rotationStateRepository = FakeRecentsRotationStateRepository()
+    private val previewPositionHelperFactoryMock = mock<PreviewPositionHelperFactory>()
     private val previewPositionHelper = mock<PreviewPositionHelper>()
 
     private val systemUnderTest =
         GetThumbnailPositionUseCase(
-            deviceProfileRepository,
-            rotationStateRepository,
-            previewPositionHelper,
+            deviceProfileRepository = deviceProfileRepository,
+            rotationStateRepository = rotationStateRepository,
+            previewPositionHelperFactory = previewPositionHelperFactoryMock,
         )
 
+    @Before
+    fun setUp() {
+        whenever(previewPositionHelperFactoryMock.create()).thenReturn(previewPositionHelper)
+    }
+
     @Test
     fun nullThumbnailData_returnsIdentityMatrix() = runTest {
         val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
@@ -96,6 +105,25 @@
             )
     }
 
+    @Test
+    fun multipleInvocations_usesPreviewPositionHelperFactoryEachTime() = runTest {
+        whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
+
+        val sut =
+            GetThumbnailPositionUseCase(
+                deviceProfileRepository = deviceProfileRepository,
+                rotationStateRepository = rotationStateRepository,
+                previewPositionHelperFactory = previewPositionHelperFactoryMock,
+            )
+        verify(previewPositionHelperFactoryMock, times(0)).create()
+
+        sut.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, /* isRtl= */ true)
+        sut.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, /* isRtl= */ false)
+
+        // Each invocation of use case should use a fresh position helper acquired by the factory.
+        verify(previewPositionHelperFactoryMock, times(2)).create()
+    }
+
     private companion object {
         const val THUMBNAIL_WIDTH = 100
         const val THUMBNAIL_HEIGHT = 200
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
deleted file mode 100644
index 3f7c85c..0000000
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.taskbar;
-
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
-import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.Display;
-import android.view.MotionEvent;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.apppairs.AppPairIcon;
-import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.util.ActivityContextWrapper;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.stubbing.Answer;
-
-/**
- * Tests for TaskbarHoverToolTipController.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
-
-    private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
-    private TestableLooper mTestableLooper;
-
-    @Mock private TaskbarView mTaskbarView;
-    @Mock private MotionEvent mMotionEvent;
-    @Mock private BubbleTextView mHoverBubbleTextView;
-    @Mock private FolderIcon mHoverFolderIcon;
-    @Mock private AppPairIcon mAppPairIcon;
-    @Mock private Display mDisplay;
-    @Mock private TaskbarDragLayer mTaskbarDragLayer;
-    private Folder mSpyFolderView;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-
-        Context context = getApplicationContext();
-
-        doAnswer((Answer<Object>) invocation -> context.getSystemService(
-                (String) invocation.getArgument(0)))
-                .when(taskbarActivityContext).getSystemService(anyString());
-        when(taskbarActivityContext.getResources()).thenReturn(context.getResources());
-        when(taskbarActivityContext.getApplicationInfo()).thenReturn(
-                context.getApplicationInfo());
-        when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
-        when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
-        when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
-        when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false);
-
-        when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
-        mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
-        when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView);
-        doReturn(false).when(mSpyFolderView).isOpen();
-
-        when(mHoverBubbleTextView.getText()).thenReturn("tooltip");
-        doAnswer((Answer<Void>) invocation -> {
-            Object[] args = invocation.getArguments();
-            ((int[]) args[0])[0] = 0;
-            ((int[]) args[0])[1] = 0;
-            return null;
-        }).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class));
-        when(mHoverBubbleTextView.getWidth()).thenReturn(100);
-        when(mHoverBubbleTextView.getHeight()).thenReturn(100);
-
-        mHoverFolderIcon.mInfo = new FolderInfo();
-        mHoverFolderIcon.mInfo.title = "tooltip";
-        doAnswer((Answer<Void>) invocation -> {
-            Object[] args = invocation.getArguments();
-            ((int[]) args[0])[0] = 0;
-            ((int[]) args[0])[1] = 0;
-            return null;
-        }).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class));
-        when(mHoverFolderIcon.getWidth()).thenReturn(100);
-        when(mHoverFolderIcon.getHeight()).thenReturn(100);
-
-        when(mTaskbarView.getTop()).thenReturn(200);
-
-        mTaskbarHoverToolTipController = new TaskbarHoverToolTipController(
-                taskbarActivityContext, mTaskbarView, mHoverBubbleTextView);
-        mTestableLooper = TestableLooper.get(this);
-    }
-
-    @Test
-    public void onHover_hoverEnterIcon_revealToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                true);
-    }
-
-    @Test
-    public void onHover_hoverExitIcon_closeToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                false);
-    }
-
-    @Test
-    public void onHover_hoverEnterFolderIcon_revealToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                true);
-    }
-
-    @Test
-    public void onHover_hoverExitFolderIcon_closeToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                false);
-    }
-
-    @Test
-    public void onHover_hoverExitFolderOpen_closeToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-        doReturn(true).when(mSpyFolderView).isOpen();
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                false);
-    }
-
-    @Test
-    public void onHover_hoverEnterFolderOpen_noToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        doReturn(true).when(mSpyFolderView).isOpen();
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
-
-        assertThat(hoverConsumed).isFalse();
-    }
-
-    @Test
-    public void onHover_hoverMove_noUpdate() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
-
-        assertThat(hoverConsumed).isFalse();
-    }
-
-    @Test
-    public void onHover_hoverEnterAppPair_revealToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                true);
-    }
-
-    @Test
-    public void onHover_hoverExitAppPair_closeToolTip() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                false);
-    }
-
-    @Test
-    public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() {
-        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
-        when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true);
-
-        boolean hoverConsumed =
-                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
-        waitForIdleSync();
-
-        assertThat(hoverConsumed).isFalse();
-        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
-                true);
-    }
-
-    private void waitForIdleSync() {
-        mTestableLooper.processAllMessages();
-    }
-}
diff --git a/res/values-sw600dp/styles.xml b/res/values-sw600dp/styles.xml
index 63bd46b..db49a3e 100644
--- a/res/values-sw600dp/styles.xml
+++ b/res/values-sw600dp/styles.xml
@@ -14,8 +14,11 @@
   ~ limitations under the License.
   -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <style name="CellStyleDefault">
         <item name="iconDrawablePadding">7dp</item>
     </style>
+    <style name="DropTargetButton" parent="DropTargetButtonBase">
+        <item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">variable-title-large</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/res/values-v31/styles.xml b/res/values-v31/styles.xml
index 932ce38..6ed7dd6 100644
--- a/res/values-v31/styles.xml
+++ b/res/values-v31/styles.xml
@@ -17,7 +17,7 @@
 */
 -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <style name="HomeSettings.Theme" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:listPreferredItemPaddingEnd">16dp</item>
@@ -29,6 +29,7 @@
         <item name="android:windowActionBar">false</item>
         <item name="android:windowNoTitle">true</item>
         <item name="preferenceTheme">@style/HomeSettings.PreferenceTheme</item>
+        <item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">google-sans-flex</item>
     </style>
 
     <style name="HomeSettings.PreferenceTheme" parent="@style/PreferenceThemeOverlay">
@@ -80,11 +81,13 @@
 
     <style name="HomeSettings.CollapsedToolbarTitle"
             parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title">
-        <item name="android:fontFamily">google-sans</item>
+        <item name="android:fontFamily" android:featureFlag="!com.android.launcher3.gsf_res">google-sans</item>
+        <item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">variable-title-large</item>
         <item name="android:textSize">20sp</item>
     </style>
 
     <style name="HomeSettings.ExpandedToolbarTitle" parent="HomeSettings.CollapsedToolbarTitle">
+        <item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">variable-display-small</item>
         <item name="android:textSize">36sp</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 3b48c9e..fc636a5 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -68,7 +68,6 @@
     <string name="app_launch_tracker_class" translatable="false"></string>
     <string name="test_information_handler_class" translatable="false"></string>
     <string name="secondary_display_predictions_class" translatable="false"></string>
-    <string name="widget_holder_factory_class" translatable="false"></string>
     <string name="taskbar_search_session_controller_class" translatable="false"></string>
     <string name="taskbar_model_callbacks_factory_class" translatable="false"></string>
     <string name="taskbar_view_callbacks_factory_class" translatable="false"></string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 39206d3..cf6c560 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -373,7 +373,9 @@
         <item name="android:background">@drawable/drop_target_background</item>
     </style>
 
-    <style name="DropTargetButton" parent="DropTargetButtonBase" />
+    <style name="DropTargetButton" parent="DropTargetButtonBase">
+        <item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">variable-title-medium</item>
+    </style>
 
     <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5c9392d..d5b3ed5 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -532,11 +532,14 @@
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new StateManager<>(this, NORMAL);
 
+        mAppWidgetManager = new WidgetManagerHelper(this);
+        mAppWidgetHolder = LauncherWidgetHolder.newInstance(this);
+        mAppWidgetHolder.setAppWidgetRemovedCallback(
+                appWidgetId -> getWorkspace().removeWidget(appWidgetId));
+
         setupViews();
         updateDisallowBack();
 
-        mAppWidgetManager = new WidgetManagerHelper(this);
-        mAppWidgetHolder = createAppWidgetHolder();
         mAppWidgetHolder.startListening();
         mAppWidgetHolder.addProviderChangeListener(() -> refreshAndBindWidgetsForPackageUser(null));
         mItemInflater = new ItemInflater<>(this, mAppWidgetHolder, getItemOnClickListener(),
@@ -1614,11 +1617,6 @@
         return instance;
     }
 
-    protected LauncherWidgetHolder createAppWidgetHolder() {
-        return LauncherWidgetHolder.HolderFactory.newFactory(this).newInstance(
-                this, appWidgetId -> getWorkspace().removeWidget(appWidgetId));
-    }
-
     @Override
     protected void onNewIntent(Intent intent) {
         if (Utilities.isRunningInTestHarness()) {
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index f223eaa..bf02e03 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -435,6 +435,7 @@
                 }
                 if (currentItem.itemInfo != null && Objects.equals(
                         currentItem.itemInfo.getTargetPackage(), PRIVATE_SPACE_PACKAGE)) {
+                    currentItem.itemInfo.bitmap = mPrivateProviderManager.preparePSBitmapInfo();
                     currentItem.itemInfo.bitmap.creationFlags |= FLAG_NO_BADGE;
                     currentItem.itemInfo.contentDescription =
                             mPrivateProviderManager.getPsAppContentDesc();
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 1bc1b17..0e6a5b8 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -190,15 +190,11 @@
     /** Adds Private Space install app button to the layout. */
     public void addPrivateSpaceInstallAppButton(List<BaseAllAppsAdapter.AdapterItem> adapterItems) {
         Context context = mAllApps.getContext();
-        // Prepare bitmapInfo
-        Intent.ShortcutIconResource shortcut = Intent.ShortcutIconResource.fromContext(
-                context, com.android.launcher3.R.drawable.private_space_install_app_icon);
-        BitmapInfo bitmapInfo = LauncherIcons.obtain(context).createIconBitmap(shortcut);
 
         PrivateSpaceInstallAppButtonInfo itemInfo = new PrivateSpaceInstallAppButtonInfo();
         itemInfo.title = context.getResources().getString(R.string.ps_add_button_label);
         itemInfo.intent = mAppInstallerIntent;
-        itemInfo.bitmap = bitmapInfo;
+        itemInfo.bitmap = preparePSBitmapInfo();
         itemInfo.contentDescription = context.getResources().getString(
                 com.android.launcher3.R.string.ps_add_button_content_description);
         itemInfo.runtimeStatusFlags |= FLAG_NOT_PINNABLE;
@@ -218,6 +214,13 @@
                     .get(mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0);
     }
 
+    BitmapInfo preparePSBitmapInfo() {
+        Context context = mAllApps.getContext();
+        Intent.ShortcutIconResource shortcut = Intent.ShortcutIconResource.fromContext(
+                context, com.android.launcher3.R.drawable.private_space_install_app_icon);
+        return LauncherIcons.obtain(context).createIconBitmap(shortcut);
+    }
+
     /**
      * Resets the current state of Private Profile, w.r.t. to Launcher. The decorator should only
      * be applied upon expand before animating. When collapsing, reset() will remove the decorator
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 44dcc06..d987841 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -163,11 +163,6 @@
             "ENABLE_WIDGET_TRANSITION_FOR_RESIZING", DISABLED,
             "Enable widget transition animation when resizing the widgets");
 
-    // TODO(Block 25): Clean up flags
-    public static final BooleanFlag ENABLE_WIDGET_HOST_IN_BACKGROUND = getDebugFlag(270394384,
-            "ENABLE_WIDGET_HOST_IN_BACKGROUND", ENABLED,
-            "Enable background widget updates listening for widget holder");
-
     // TODO(Block 27): Clean up flags
     public static final BooleanFlag ENABLE_OVERLAY_CONNECTION_OPTIM = getDebugFlag(270392629,
             "ENABLE_OVERLAY_CONNECTION_OPTIM", DISABLED,
diff --git a/src/com/android/launcher3/dagger/LauncherAppModule.java b/src/com/android/launcher3/dagger/LauncherAppModule.java
index c58a414..0fd3219 100644
--- a/src/com/android/launcher3/dagger/LauncherAppModule.java
+++ b/src/com/android/launcher3/dagger/LauncherAppModule.java
@@ -23,6 +23,7 @@
         ApiWrapperModule.class,
         PluginManagerWrapperModule.class,
         StaticObjectModule.class,
+        WidgetModule.class,
         AppModule.class
 })
 public class LauncherAppModule {
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index c499097..f86772e 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.util.WallpaperColorHints;
 import com.android.launcher3.util.window.RefreshRateTracker;
 import com.android.launcher3.util.window.WindowManagerProxy;
+import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import dagger.BindsInstance;
@@ -89,6 +90,7 @@
     WidgetsFilterDataProvider getWidgetsFilterDataProvider();
 
     LoaderCursorFactory getLoaderCursorFactory();
+    WidgetHolderFactory getWidgetHolderFactory();
 
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 39f68bf..9a226df 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -24,7 +24,9 @@
 import static com.android.launcher3.allapps.AlphabeticalAppsList.PRIVATE_SPACE_PACKAGE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
+import static com.android.launcher3.shortcuts.DeepShortcutTextView.GOOGLE_SANS_FLEX_LABEL_LARGE;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.wm.shell.Flags.enableGsf;
 
 import android.animation.AnimatorSet;
 import android.animation.LayoutTransition;
@@ -32,6 +34,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.Typeface;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
@@ -479,6 +482,10 @@
         if (view instanceof DeepShortcutView) {
             // System shortcut takes entire row with icon and text
             final DeepShortcutView shortcutView = (DeepShortcutView) view;
+            if (enableGsf()) {
+                shortcutView.getBubbleText().setTypeface(
+                        Typeface.create(GOOGLE_SANS_FLEX_LABEL_LARGE, Typeface.NORMAL));
+            }
             info.setIconAndLabelFor(shortcutView.getIconView(), shortcutView.getBubbleText());
         } else if (view instanceof ImageView) {
             // System shortcut is just an icon
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
index ded2cee..b1d095b 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
@@ -16,9 +16,12 @@
 
 package com.android.launcher3.shortcuts;
 
+import static com.android.wm.shell.Flags.enableGsf;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -31,6 +34,7 @@
  * A {@link BubbleTextView} that has the shortcut icon on the left and drag handle on the right.
  */
 public class DeepShortcutTextView extends BubbleTextView {
+    public static final String GOOGLE_SANS_FLEX_LABEL_LARGE = "variable-label-large";
 
     private boolean mShowLoadingState;
     private Drawable mLoadingStatePlaceholder;
@@ -47,6 +51,9 @@
     public DeepShortcutTextView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         showLoadingState(true);
+        if (enableGsf()) {
+            setTypeface(Typeface.create(GOOGLE_SANS_FLEX_LABEL_LARGE, Typeface.NORMAL));
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 91b899c..63d2954 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.widget;
 
-import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
-
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
@@ -26,49 +24,18 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.IntConsumer;
-
 /**
  * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
  * which correctly captures all long-press events. This ensures that users can
  * always pick up and move widgets.
  */
-class LauncherAppWidgetHost extends AppWidgetHost {
-    @NonNull
-    private final List<ProviderChangedListener> mProviderChangeListeners;
-
-    @NonNull
-    private final Context mContext;
-
-    @Nullable
-    private final IntConsumer mAppWidgetRemovedCallback;
+class LauncherAppWidgetHost extends ListenableAppWidgetHost {
 
     @Nullable
     private ListenableHostView mViewToRecycle;
 
-    public LauncherAppWidgetHost(@NonNull Context context,
-            @Nullable IntConsumer appWidgetRemovedCallback,
-            List<ProviderChangedListener> providerChangeListeners) {
-        super(context, APPWIDGET_HOST_ID);
-        mContext = context;
-        mAppWidgetRemovedCallback = appWidgetRemovedCallback;
-        mProviderChangeListeners = providerChangeListeners;
-    }
-
-    @Override
-    protected void onProvidersChanged() {
-        if (!mProviderChangeListeners.isEmpty()) {
-            for (LauncherWidgetHolder.ProviderChangedListener callback :
-                    new ArrayList<>(mProviderChangeListeners)) {
-                callback.notifyWidgetProvidersChanged();
-            }
-        }
+    LauncherAppWidgetHost(@NonNull Context context, int appWidgetId) {
+        super(context, appWidgetId);
     }
 
     /**
@@ -94,35 +61,6 @@
     }
 
     /**
-     * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
-     */
-    @Override
-    protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) {
-        LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
-                mContext, appWidget);
-        super.onProviderChanged(appWidgetId, info);
-        // The super method updates the dimensions of the providerInfo. Update the
-        // launcher spans accordingly.
-        info.initSpans(mContext, LauncherAppState.getIDP(mContext));
-    }
-
-    /**
-     * Called on an appWidget is removed for a widgetId
-     *
-     * @param appWidgetId TODO: make this override when SDK is updated
-     */
-    @Override
-    public void onAppWidgetRemoved(int appWidgetId) {
-        if (mAppWidgetRemovedCallback == null) {
-            return;
-        }
-        // Route the call via model thread, in case it comes while a loader-bind is in progress
-        Executors.MODEL_EXECUTOR.execute(
-                () -> Executors.MAIN_EXECUTOR.execute(
-                        () -> mAppWidgetRemovedCallback.accept(appWidgetId)));
-    }
-
-    /**
      * The same as super.clearViews(), except with the scope exposed
      */
     @Override
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 78197e2..642f35a 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -20,10 +20,9 @@
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
 import static com.android.launcher3.Flags.enableWorkspaceInflation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
+import static com.android.launcher3.widget.ListenableAppWidgetHost.getWidgetHolderExecutor;
 
-import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -32,6 +31,7 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.Looper;
+import android.util.Log;
 import android.util.SparseArray;
 import android.widget.Toast;
 
@@ -43,18 +43,23 @@
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.LauncherComponentProvider;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.ListenableAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -62,51 +67,57 @@
  * background.
  */
 public class LauncherWidgetHolder {
+
+    private static final String TAG = "LauncherWidgetHolder";
+
     public static final int APPWIDGET_HOST_ID = 1024;
 
     protected static final int FLAG_LISTENING = 1;
     protected static final int FLAG_STATE_IS_NORMAL = 1 << 1;
     protected static final int FLAG_ACTIVITY_STARTED = 1 << 2;
     protected static final int FLAG_ACTIVITY_RESUMED = 1 << 3;
+
     private static final int FLAGS_SHOULD_LISTEN =
             FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
 
-    @NonNull
-    protected final Context mContext;
-
-    @NonNull
-    private final AppWidgetHost mWidgetHost;
-
-    @NonNull
-    protected final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
-    protected final List<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
-
-    protected AtomicInteger mFlags = new AtomicInteger(FLAG_STATE_IS_NORMAL);
-
     // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
     private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
     // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden
     private static final int SPLASH_SCREEN_STYLE_EMPTY = 0;
 
-    protected LauncherWidgetHolder(@NonNull Context context,
-            @Nullable IntConsumer appWidgetRemovedCallback) {
+    @NonNull
+    protected final Context mContext;
+
+    @NonNull
+    protected final ListenableAppWidgetHost mWidgetHost;
+
+    @NonNull
+    protected final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
+
+    /** package visibility */
+    final List<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
+
+    protected AtomicInteger mFlags = new AtomicInteger(FLAG_STATE_IS_NORMAL);
+
+    @Nullable
+    private Consumer<LauncherAppWidgetHostView> mOnViewCreationCallback;
+
+    /** package visibility */
+    @Nullable IntConsumer mAppWidgetRemovedCallback;
+
+    @AssistedInject
+    protected LauncherWidgetHolder(@Assisted("UI_CONTEXT") @NonNull Context context) {
+        this(context, new LauncherAppWidgetHost(context, APPWIDGET_HOST_ID));
+    }
+
+    protected LauncherWidgetHolder(
+            @NonNull Context context, @NonNull ListenableAppWidgetHost appWidgetHost) {
         mContext = context;
-        mWidgetHost = createHost(context, appWidgetRemovedCallback);
+        mWidgetHost = appWidgetHost;
+        MAIN_EXECUTOR.execute(() ->  mWidgetHost.getHolders().add(this));
     }
 
-    protected AppWidgetHost createHost(
-            Context context, @Nullable IntConsumer appWidgetRemovedCallback) {
-        return new LauncherAppWidgetHost(
-                context, appWidgetRemovedCallback, mProviderChangedListeners);
-    }
-
-    protected LooperExecutor getWidgetHolderExecutor() {
-        return UI_HELPER_EXECUTOR;
-    }
-
-    /**
-     * Starts listening to the widget updates from the server side
-     */
+    /** Starts listening to the widget updates from the server side */
     public void startListening() {
         if (!WIDGETS_ENABLED) {
             return;
@@ -127,13 +138,11 @@
             // TODO: Investigate why widgetHost.startListening() always return non-empty updates
             setListeningFlag(true);
 
-            MAIN_EXECUTOR.execute(() -> updateDeferredView());
+            MAIN_EXECUTOR.execute(this::updateDeferredView);
         });
     }
 
-    /**
-     * Update any views which have been deferred because the host was not listening.
-     */
+    /** Update any views which have been deferred because the host was not listening */
     protected void updateDeferredView() {
         // Update any views which have been deferred because the host was not listening.
         // We go in reverse order and inflate any deferred or cached widget
@@ -180,7 +189,14 @@
      * Called when the launcher is destroyed
      */
     public void destroy() {
-        // No-op
+        try {
+            MAIN_EXECUTOR.submit(() -> {
+                clearViews();
+                mWidgetHost.getHolders().remove(this);
+            }).get();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to remove self from holder list", e);
+        }
     }
 
     /**
@@ -198,8 +214,7 @@
      * Add a listener that is triggered when the providers of the widgets are changed
      * @param listener The listener that notifies when the providers changed
      */
-    public void addProviderChangeListener(
-            @NonNull LauncherWidgetHolder.ProviderChangedListener listener) {
+    public void addProviderChangeListener(@NonNull ProviderChangedListener listener) {
         MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.add(listener));
     }
 
@@ -207,12 +222,23 @@
      * Remove the specified listener from the host
      * @param listener The listener that is to be removed from the host
      */
-    public void removeProviderChangeListener(
-            LauncherWidgetHolder.ProviderChangedListener listener) {
+    public void removeProviderChangeListener(ProviderChangedListener listener) {
         MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.remove(listener));
     }
 
     /**
+     * Sets a callbacks for whenever a widget view is created
+     */
+    public void setOnViewCreationCallback(@Nullable Consumer<LauncherAppWidgetHostView> callback) {
+        mOnViewCreationCallback = callback;
+    }
+
+    /** Sets a callback for listening app widget removals */
+    public void setAppWidgetRemovedCallback(@Nullable IntConsumer callback) {
+        mAppWidgetRemovedCallback = callback;
+    }
+
+    /**
      * Starts the configuration activity for the widget
      * @param activity The activity in which to start the configuration page
      * @param widgetId The ID of the widget
@@ -284,9 +310,7 @@
         activity.startActivityForResult(intent, requestCode);
     }
 
-    /**
-     * Stop the host from listening to the widget updates
-     */
+    /** Stop the host from listening to the widget updates */
     public void stopListening() {
         if (!WIDGETS_ENABLED) {
             return;
@@ -298,8 +322,8 @@
     }
 
     /**
-     * Update {@link FLAG_LISTENING} on {@link mFlags} after making binder calls from
-     * {@link sWidgetHost}.
+     * Update {@link #FLAG_LISTENING} on {@link #mFlags} after making binder calls from
+     * {@link #mWidgetHost}.
      */
     @WorkerThread
     protected void setListeningFlag(final boolean isListening) {
@@ -350,6 +374,7 @@
         }
 
         LauncherAppWidgetHostView view = createViewInternal(appWidgetId, appWidget);
+        if (mOnViewCreationCallback != null) mOnViewCreationCallback.accept(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);
@@ -368,8 +393,8 @@
 
         // Binder can also inflate placeholder widgets in case of backup-restore. Skip
         // attaching such widgets
-        boolean isRealWidget = ((view instanceof PendingAppWidgetHostView pw)
-                ? pw.isDeferredWidget() : true)
+        boolean isRealWidget = (!(view instanceof PendingAppWidgetHostView pw)
+                || pw.isDeferredWidget())
                 && view.getAppWidgetInfo() != null;
         if (isRealWidget && mViews.get(view.getAppWidgetId()) != view) {
             view = recycleExistingView(view);
@@ -446,28 +471,13 @@
         }
     }
 
-    /**
-     * Listener for getting notifications on provider changes.
-     */
-    public interface ProviderChangedListener {
-        /**
-         * Notify the listener that the providers have changed
-         */
-        void notifyWidgetProvidersChanged();
-    }
-
-    /**
-     * Clears all the views from the host
-     */
+    /** Clears all the views from the host */
     public void clearViews() {
-        LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
-        tempHost.clearViews();
+        ((LauncherAppWidgetHost) mWidgetHost).clearViews();
         mViews.clear();
     }
 
-    /**
-     * Clears all the internal widget views
-     */
+    /** Clears all the internal widget views */
     public void clearWidgetViews() {
         clearViews();
     }
@@ -514,32 +524,19 @@
      * Returns the new LauncherWidgetHolder instance
      */
     public static LauncherWidgetHolder newInstance(Context context) {
-        return HolderFactory.newFactory(context).newInstance(context, null);
+        return LauncherComponentProvider.get(context).getWidgetHolderFactory().newInstance(context);
     }
 
-    /**
-     * A factory class that generates new instances of {@code LauncherWidgetHolder}
-     */
-    public static class HolderFactory implements ResourceBasedOverride {
+    /** A factory that generates new instances of {@code LauncherWidgetHolder} */
+    public interface WidgetHolderFactory {
 
-        /**
-         * @param context The context of the caller
-         * @param appWidgetRemovedCallback The callback that is called when widgets are removed
-         * @return A new instance of {@code LauncherWidgetHolder}
-         */
-        public LauncherWidgetHolder newInstance(@NonNull Context context,
-                @Nullable IntConsumer appWidgetRemovedCallback) {
-            return new LauncherWidgetHolder(context, appWidgetRemovedCallback);
-        }
+        LauncherWidgetHolder newInstance(@NonNull Context context);
+    }
 
-        /**
-         * @param context The context of the caller
-         * @return A new instance of factory class for widget holders. If not specified, returning
-         * {@code HolderFactory} by default.
-         */
-        public static HolderFactory newFactory(Context context) {
-            return Overrides.getObject(
-                    HolderFactory.class, context, R.string.widget_holder_factory_class);
-        }
+    /** A factory that generates new instances of {@code LauncherWidgetHolder} */
+    @AssistedFactory
+    public interface WidgetHolderFactoryImpl extends WidgetHolderFactory {
+
+        LauncherWidgetHolder newInstance(@Assisted("UI_CONTEXT") @NonNull Context context);
     }
 }
diff --git a/src/com/android/launcher3/widget/ListenableAppWidgetHost.kt b/src/com/android/launcher3/widget/ListenableAppWidgetHost.kt
new file mode 100644
index 0000000..58bf0aa
--- /dev/null
+++ b/src/com/android/launcher3/widget/ListenableAppWidgetHost.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LooperExecutor
+
+open class ListenableAppWidgetHost(private val ctx: Context, hostId: Int) :
+    AppWidgetHost(ctx, hostId) {
+
+    protected val holders = mutableListOf<LauncherWidgetHolder>()
+
+    override fun onProvidersChanged() {
+        MAIN_EXECUTOR.execute {
+            holders.forEach { holder ->
+                // Listeners might remove themselves from the list during the iteration.
+                // Creating a copy of the list to avoid exceptions for concurrent modification.
+                holder.mProviderChangedListeners.toList().forEach {
+                    it.notifyWidgetProvidersChanged()
+                }
+            }
+        }
+    }
+
+    override fun onAppWidgetRemoved(appWidgetId: Int) {
+        // Route the call via model thread, in case it comes while a loader-bind is in progress
+        MODEL_EXECUTOR.execute {
+            MAIN_EXECUTOR.execute {
+                holders.forEach { it.mAppWidgetRemovedCallback?.accept(appWidgetId) }
+            }
+        }
+    }
+
+    override fun onProviderChanged(appWidgetId: Int, appWidget: AppWidgetProviderInfo) {
+        val info = LauncherAppWidgetProviderInfo.fromProviderInfo(ctx, appWidget)
+        super.onProviderChanged(appWidgetId, info)
+        // The super method updates the dimensions of the providerInfo. Update the
+        // launcher spans accordingly.
+        info.initSpans(ctx, InvariantDeviceProfile.INSTANCE.get(ctx))
+    }
+
+    /** Listener for getting notifications on provider changes. */
+    fun interface ProviderChangedListener {
+        /** Notify the listener that the providers have changed */
+        fun notifyWidgetProvidersChanged()
+    }
+
+    companion object {
+
+        @JvmStatic val widgetHolderExecutor: LooperExecutor = Executors.UI_HELPER_EXECUTOR
+    }
+}
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index cd8e457..1c29f89 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -66,7 +66,7 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.Themes;
-import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
+import com.android.launcher3.widget.ListenableAppWidgetHost.ProviderChangedListener;
 
 import java.util.List;
 
diff --git a/src_no_quickstep/com/android/launcher3/dagger/Modules.kt b/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
index c3bf7c5..7dbe9c3 100644
--- a/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
+++ b/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.dagger
 
+import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory
+import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactoryImpl
+import dagger.Binds
 import dagger.Module
 
 private object Modules {}
@@ -24,6 +27,12 @@
 
 @Module abstract class ApiWrapperModule {}
 
+@Module
+abstract class WidgetModule {
+    @Binds
+    abstract fun bindWidgetHolderFactory(factor: WidgetHolderFactoryImpl): WidgetHolderFactory
+}
+
 @Module abstract class PluginManagerWrapperModule {}
 
 @Module object StaticObjectModule {}
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index a30261e..dc49ba0 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -39,7 +39,6 @@
 import com.android.launcher3.util.SandboxContext
 import com.android.launcher3.util.WindowBounds
 import com.android.launcher3.util.rule.TestStabilityRule
-import com.android.launcher3.util.rule.ZipFilesRule
 import com.android.launcher3.util.rule.setFlags
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.launcher3.util.window.WindowManagerProxy
@@ -52,7 +51,6 @@
 import java.io.StringWriter
 import kotlin.math.max
 import kotlin.math.min
-import org.junit.ClassRule
 import org.junit.Rule
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
@@ -82,13 +80,6 @@
 
     @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
 
-    companion object {
-        @ClassRule
-        @JvmField
-        val resultZipRule =
-            ZipFilesRule(InstrumentationRegistry.getInstrumentation().targetContext, "DumpTest")
-    }
-
     class DeviceSpec(
         val naturalSize: Pair<Int, Int>,
         var densityDpi: Int,
@@ -375,7 +366,6 @@
     private fun writeToDevice(context: Context, fileName: String, content: String) {
         val file = File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName)
         file.writeText(content)
-        resultZipRule.write(file)
     }
 
     protected fun Float.dpToPx(): Float {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt b/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
index b66a9d3..a76ccf0 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
@@ -21,6 +21,7 @@
 import com.android.launcher3.dagger.ApiWrapperModule
 import com.android.launcher3.dagger.AppModule
 import com.android.launcher3.dagger.StaticObjectModule
+import com.android.launcher3.dagger.WidgetModule
 import com.android.launcher3.dagger.WindowManagerProxyModule
 import dagger.Binds
 import dagger.Module
@@ -39,15 +40,27 @@
             ApiWrapperModule::class,
             WindowManagerProxyModule::class,
             StaticObjectModule::class,
+            WidgetModule::class,
             AppModule::class,
         ]
 )
 class AllModulesForTest
 
 /** All modules except the WMProxy */
-@Module(includes = [ApiWrapperModule::class, StaticObjectModule::class, AppModule::class])
+@Module(
+    includes =
+        [ApiWrapperModule::class, StaticObjectModule::class, AppModule::class, WidgetModule::class]
+)
 class AllModulesMinusWMProxy
 
 /** All modules except the ApiWrapper */
-@Module(includes = [WindowManagerProxyModule::class, StaticObjectModule::class, AppModule::class])
+@Module(
+    includes =
+        [
+            WindowManagerProxyModule::class,
+            StaticObjectModule::class,
+            AppModule::class,
+            WidgetModule::class,
+        ]
+)
 class AllModulesMinusApiWrapper
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt
index 79b493a..d5d1f4a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetHostTest.kt
@@ -21,32 +21,22 @@
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.util.ActivityContextWrapper
 import com.android.launcher3.util.Executors
+import com.android.launcher3.util.TestUtil
 import java.util.function.IntConsumer
 import org.junit.Assert.assertNotSame
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertSame
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class LauncherAppWidgetHostTest {
 
-    @Mock private lateinit var onAppWidgetRemovedCallback: IntConsumer
-
     private val context = ActivityContextWrapper(getInstrumentation().targetContext)
-    private lateinit var underTest: LauncherAppWidgetHost
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        underTest = LauncherAppWidgetHost(context, onAppWidgetRemovedCallback, emptyList())
-    }
+    private var underTest = LauncherAppWidgetHost(context, HOST_ID)
 
     @Test
     fun `Host set view to recycle`() {
@@ -74,15 +64,20 @@
 
     @Test
     fun `Runnable called when app widget removed`() {
+        val holder = LauncherWidgetHolder(context, underTest)
+        holder.setAppWidgetRemovedCallback(mock(IntConsumer::class.java))
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
+
         underTest.onAppWidgetRemoved(WIDGET_ID)
 
         Executors.MODEL_EXECUTOR.submit {}.get()
         getInstrumentation().waitForIdleSync()
 
-        verify(onAppWidgetRemovedCallback).accept(WIDGET_ID)
+        verify(holder.mAppWidgetRemovedCallback!!).accept(WIDGET_ID)
     }
 
     companion object {
+        const val HOST_ID = 2233
         const val WIDGET_ID = 10001
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
index 1a659e2..44f29d6 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
@@ -47,7 +47,7 @@
     fun setUp() {
         assertTrue(WIDGETS_ENABLED)
         widgetHolder =
-            LauncherWidgetHolder(ActivityContextWrapper(getInstrumentation().targetContext)) {}
+            LauncherWidgetHolder(ActivityContextWrapper(getInstrumentation().targetContext))
     }
 
     @After
@@ -62,7 +62,7 @@
         widgetHolder.setListeningFlag(false)
         assertFalse(widgetHolder.isListening)
         widgetHolder.startListening()
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         getInstrumentation().waitForIdleSync()
         assertTrue(widgetHolder.isListening)
         verify(testView, times(1)).reInflate()
@@ -73,10 +73,10 @@
     fun holder_start_listening_after_activity_start() {
         widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_RESUMED, true)
         widgetHolder.setActivityStarted(false)
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
         widgetHolder.setActivityStarted(true)
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
     }
 
@@ -84,10 +84,10 @@
     fun holder_start_listening_after_activity_resume() {
         widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_STARTED, true)
         widgetHolder.setActivityResumed(false)
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
         widgetHolder.setActivityResumed(true)
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
     }
 
@@ -95,10 +95,10 @@
     fun holder_start_listening_after_state_normal() {
         widgetHolder.setShouldListenFlag(FLAG_ACTIVITY_RESUMED or FLAG_ACTIVITY_STARTED, true)
         widgetHolder.setStateIsNormal(false)
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
         widgetHolder.setStateIsNormal(true)
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
     }
 
@@ -117,7 +117,7 @@
 
     @Test
     fun holder_add_provider_change_listener() {
-        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        val listener = ListenableAppWidgetHost.ProviderChangedListener {}
         widgetHolder.addProviderChangeListener(listener)
         getInstrumentation().waitForIdleSync()
         assertEquals(1, widgetHolder.mProviderChangedListeners.size)
@@ -127,7 +127,7 @@
 
     @Test
     fun holder_remove_provider_change_listener() {
-        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        val listener = ListenableAppWidgetHost.ProviderChangedListener {}
         widgetHolder.addProviderChangeListener(listener)
         widgetHolder.removeProviderChangeListener(listener)
         getInstrumentation().waitForIdleSync()
@@ -139,7 +139,7 @@
         widgetHolder.setListeningFlag(true)
         assertTrue(widgetHolder.isListening)
         widgetHolder.stopListening()
-        widgetHolder.widgetHolderExecutor.submit {}.get()
+        ListenableAppWidgetHost.widgetHolderExecutor.submit {}.get()
         assertFalse(widgetHolder.isListening)
     }