Merge "Convert IconShape to Kotlin" into main
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
deleted file mode 100644
index 488cea5..0000000
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ /dev/null
@@ -1,521 +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.statehandlers;
-
-import static android.view.View.VISIBLE;
-import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Context;
-import android.os.Debug;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.statemanager.BaseState;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.fallback.RecentsState;
-import com.android.wm.shell.desktopmode.IDesktopTaskListener;
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
-
-import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Controls the visibility of the workspace and the resumed / paused state when desktop mode
- * is enabled.
- */
-public class DesktopVisibilityController {
-
-    private static final String TAG = "DesktopVisController";
-    private static final boolean DEBUG = false;
-    private final Set<DesktopVisibilityListener> mDesktopVisibilityListeners = new HashSet<>();
-    private final Set<TaskbarDesktopModeListener> mTaskbarDesktopModeListeners = new HashSet<>();
-
-    private int mVisibleDesktopTasksCount;
-    private boolean mInOverviewState;
-    private boolean mBackgroundStateEnabled;
-    private boolean mGestureInProgress;
-
-    @Nullable
-    private DesktopTaskListenerImpl mDesktopTaskListener;
-
-    @Nullable
-    private Context mContext;
-
-    public DesktopVisibilityController(@NonNull Context context) {
-        setContext(context);
-    }
-
-    /** Sets the context and re-registers the System Ui listener */
-    private void setContext(@Nullable Context context) {
-        unregisterSystemUiListener();
-        mContext = context;
-        registerSystemUiListener();
-    }
-
-    /** Register a listener with System UI to receive updates about desktop tasks state */
-    private void registerSystemUiListener() {
-        if (mContext == null) {
-            return;
-        }
-        if (mDesktopTaskListener != null) {
-            return;
-        }
-        mDesktopTaskListener = new DesktopTaskListenerImpl(this, mContext.getDisplayId());
-        SystemUiProxy.INSTANCE.get(mContext).setDesktopTaskListener(mDesktopTaskListener);
-    }
-
-    /**
-     * Clear listener from System UI that was set with {@link #registerSystemUiListener()}
-     */
-    private void unregisterSystemUiListener() {
-        if (mContext == null) {
-            return;
-        }
-        if (mDesktopTaskListener == null) {
-            return;
-        }
-        SystemUiProxy.INSTANCE.get(mContext).setDesktopTaskListener(null);
-        mDesktopTaskListener.release();
-        mDesktopTaskListener = null;
-    }
-
-    /**
-     * Whether desktop tasks are visible in desktop mode.
-     */
-    public boolean areDesktopTasksVisible() {
-        boolean desktopTasksVisible = mVisibleDesktopTasksCount > 0;
-        if (DEBUG) {
-            Log.d(TAG, "areDesktopTasksVisible: desktopVisible=" + desktopTasksVisible);
-        }
-        return desktopTasksVisible;
-    }
-
-    /**
-     * Whether desktop tasks are visible in desktop mode.
-     */
-    public boolean areDesktopTasksVisibleAndNotInOverview() {
-        boolean desktopTasksVisible = mVisibleDesktopTasksCount > 0;
-        if (DEBUG) {
-            Log.d(TAG, "areDesktopTasksVisible: desktopVisible=" + desktopTasksVisible
-                    + " overview=" + mInOverviewState);
-        }
-        return desktopTasksVisible && !mInOverviewState;
-    }
-
-    /**
-     * Number of visible desktop windows in desktop mode.
-     */
-    public int getVisibleDesktopTasksCount() {
-        return mVisibleDesktopTasksCount;
-    }
-
-    /** Registers a listener for Desktop Mode visibility updates. */
-    public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) {
-        mDesktopVisibilityListeners.add(listener);
-    }
-
-    /** Removes a previously registered Desktop Mode visibility listener. */
-    public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) {
-        mDesktopVisibilityListeners.remove(listener);
-    }
-
-    /** Registers a listener for Taskbar changes in Desktop Mode. */
-    public void registerTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) {
-        mTaskbarDesktopModeListeners.add(listener);
-    }
-
-    /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */
-    public void unregisterTaskbarDesktopModeListener(TaskbarDesktopModeListener listener) {
-        mTaskbarDesktopModeListeners.remove(listener);
-    }
-
-    /**
-     * Sets the number of desktop windows that are visible and updates launcher visibility based on
-     * it.
-     */
-    public void setVisibleDesktopTasksCount(int visibleTasksCount) {
-        if (mContext == null) {
-            return;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "setVisibleDesktopTasksCount: visibleTasksCount=" + visibleTasksCount
-                    + " currentValue=" + mVisibleDesktopTasksCount);
-        }
-
-        if (visibleTasksCount != mVisibleDesktopTasksCount) {
-            final boolean wasVisible = mVisibleDesktopTasksCount > 0;
-            final boolean isVisible = visibleTasksCount > 0;
-            final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview();
-            mVisibleDesktopTasksCount = visibleTasksCount;
-            final boolean areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview();
-            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
-                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
-            }
-
-            if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
-                    && wasVisible != isVisible) {
-                // TODO: b/333533253 - Remove after flag rollout
-                if (mVisibleDesktopTasksCount > 0) {
-                    setLauncherViewsVisibility(View.INVISIBLE);
-                    if (!mInOverviewState) {
-                        // When desktop tasks are visible & we're not in overview, we want launcher
-                        // to appear paused, this ensures that taskbar displays.
-                        markLauncherPaused();
-                    }
-                } else {
-                    setLauncherViewsVisibility(View.VISIBLE);
-                    // If desktop tasks aren't visible, ensure that launcher appears resumed to
-                    // behave normally.
-                    markLauncherResumed();
-                }
-            }
-        }
-    }
-
-    public void onLauncherStateChanged(LauncherState state) {
-        onLauncherStateChanged(
-                state, state == LauncherState.BACKGROUND_APP, state.isRecentsViewVisible);
-    }
-
-    public void onLauncherStateChanged(RecentsState state) {
-        onLauncherStateChanged(
-                state, state == RecentsState.BACKGROUND_APP, state.isRecentsViewVisible());
-    }
-
-    /**
-     * Process launcher state change and update launcher view visibility based on desktop state
-     */
-    public void onLauncherStateChanged(
-            BaseState<?> state, boolean isBackgroundAppState, boolean isRecentsViewVisible) {
-        if (DEBUG) {
-            Log.d(TAG, "onLauncherStateChanged: newState=" + state);
-        }
-        setBackgroundStateEnabled(isBackgroundAppState);
-        // Desktop visibility tracks overview and background state separately
-        setOverviewStateEnabled(!isBackgroundAppState && isRecentsViewVisible);
-    }
-
-    private void setOverviewStateEnabled(boolean overviewStateEnabled) {
-        if (mContext == null) {
-            return;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "setOverviewStateEnabled: enabled=" + overviewStateEnabled
-                    + " currentValue=" + mInOverviewState);
-        }
-        if (overviewStateEnabled != mInOverviewState) {
-            final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview();
-            mInOverviewState = overviewStateEnabled;
-            final boolean areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview();
-            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
-                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
-            }
-            if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
-                return;
-            }
-            // TODO: b/333533253 - Clean up after flag rollout
-
-            if (mInOverviewState) {
-                setLauncherViewsVisibility(View.VISIBLE);
-                markLauncherResumed();
-            } else if (areDesktopTasksVisibleNow && !mGestureInProgress) {
-                // Switching out of overview state and gesture finished.
-                // If desktop tasks are still visible, hide launcher again.
-                setLauncherViewsVisibility(View.INVISIBLE);
-                markLauncherPaused();
-            }
-        }
-    }
-
-    private void notifyDesktopVisibilityListeners(boolean areDesktopTasksVisible) {
-        if (mContext == null) {
-            return;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "notifyDesktopVisibilityListeners: visible=" + areDesktopTasksVisible);
-        }
-        for (DesktopVisibilityListener listener : mDesktopVisibilityListeners) {
-            listener.onDesktopVisibilityChanged(areDesktopTasksVisible);
-        }
-        DisplayController.INSTANCE.get(mContext).notifyConfigChange();
-    }
-
-    private void notifyTaskbarDesktopModeListeners(boolean doesAnyTaskRequireTaskbarRounding) {
-        if (DEBUG) {
-            Log.d(TAG, "notifyTaskbarDesktopModeListeners: doesAnyTaskRequireTaskbarRounding="
-                    + doesAnyTaskRequireTaskbarRounding);
-        }
-        for (TaskbarDesktopModeListener listener : mTaskbarDesktopModeListeners) {
-            listener.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding);
-        }
-    }
-
-    /**
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    private void setBackgroundStateEnabled(boolean backgroundStateEnabled) {
-        if (DEBUG) {
-            Log.d(TAG, "setBackgroundStateEnabled: enabled=" + backgroundStateEnabled
-                    + " currentValue=" + mBackgroundStateEnabled);
-        }
-        if (backgroundStateEnabled != mBackgroundStateEnabled) {
-            mBackgroundStateEnabled = backgroundStateEnabled;
-            if (mBackgroundStateEnabled) {
-                setLauncherViewsVisibility(View.VISIBLE);
-                markLauncherResumed();
-            } else if (areDesktopTasksVisibleAndNotInOverview() && !mGestureInProgress) {
-                // Switching out of background state. If desktop tasks are visible, pause launcher.
-                setLauncherViewsVisibility(View.INVISIBLE);
-                markLauncherPaused();
-            }
-        }
-    }
-
-    /**
-     * Whether recents gesture is currently in progress.
-     *
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    public boolean isRecentsGestureInProgress() {
-        return mGestureInProgress;
-    }
-
-    /**
-     * Notify controller that recents gesture has started.
-     *
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    public void setRecentsGestureStart() {
-        if (DEBUG) {
-            Log.d(TAG, "setRecentsGestureStart");
-        }
-        setRecentsGestureInProgress(true);
-    }
-
-    /**
-     * Notify controller that recents gesture finished with the given
-     * {@link com.android.quickstep.GestureState.GestureEndTarget}
-     *
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    public void setRecentsGestureEnd(@Nullable GestureState.GestureEndTarget endTarget) {
-        if (DEBUG) {
-            Log.d(TAG, "setRecentsGestureEnd: endTarget=" + endTarget);
-        }
-        setRecentsGestureInProgress(false);
-
-        if (endTarget == null) {
-            // Gesture did not result in a new end target. Ensure launchers gets paused again.
-            markLauncherPaused();
-        }
-    }
-
-    /**
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    private void setRecentsGestureInProgress(boolean gestureInProgress) {
-        if (gestureInProgress != mGestureInProgress) {
-            mGestureInProgress = gestureInProgress;
-        }
-    }
-
-    /**
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    private void setLauncherViewsVisibility(int visibility) {
-        if (mContext == null) {
-            return;
-        }
-        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
-            return;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "setLauncherViewsVisibility: visibility=" + visibility + " "
-                    + Debug.getCaller());
-        }
-        if (!(mContext instanceof ActivityContext activity)) {
-            return;
-        }
-        View dragLayer = activity.getDragLayer();
-        if (dragLayer != null) {
-            dragLayer.setVisibility(visibility);
-        }
-        if (!(activity instanceof Launcher launcher)) {
-            return;
-        }
-        View workspaceView = launcher.getWorkspace();
-        if (workspaceView != null) {
-            workspaceView.setVisibility(visibility);
-        }
-        if (launcher instanceof QuickstepLauncher ql
-                && ql.getTaskbarUIController() != null
-                && mVisibleDesktopTasksCount != 0) {
-            ql.getTaskbarUIController().onLauncherVisibilityChanged(visibility == VISIBLE);
-        }
-    }
-
-    /**
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    private void markLauncherPaused() {
-        if (mContext == null) {
-            return;
-        }
-        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
-            return;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "markLauncherPaused " + Debug.getCaller());
-        }
-        StatefulActivity<LauncherState> activity =
-                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
-        if (activity != null) {
-            activity.setPaused();
-        }
-    }
-
-    /**
-     * TODO: b/333533253 - Remove after flag rollout
-     */
-    private void markLauncherResumed() {
-        if (mContext == null) {
-            return;
-        }
-        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
-            return;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "markLauncherResumed " + Debug.getCaller());
-        }
-        StatefulActivity<LauncherState> activity =
-                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
-        // Check activity state before calling setResumed(). Launcher may have been actually
-        // paused (eg fullscreen task moved to front).
-        // In this case we should not mark the activity as resumed.
-        if (activity != null && activity.isResumed()) {
-            activity.setResumed();
-        }
-    }
-
-    public void onDestroy() {
-        setContext(null);
-    }
-
-    public void dumpLogs(String prefix, PrintWriter pw) {
-        pw.println(prefix + "DesktopVisibilityController:");
-
-        pw.println(prefix + "\tmDesktopVisibilityListeners=" + mDesktopVisibilityListeners);
-        pw.println(prefix + "\tmVisibleDesktopTasksCount=" + mVisibleDesktopTasksCount);
-        pw.println(prefix + "\tmInOverviewState=" + mInOverviewState);
-        pw.println(prefix + "\tmBackgroundStateEnabled=" + mBackgroundStateEnabled);
-        pw.println(prefix + "\tmGestureInProgress=" + mGestureInProgress);
-        pw.println(prefix + "\tmDesktopTaskListener=" + mDesktopTaskListener);
-        pw.println(prefix + "\tmContext=" + mContext);
-    }
-
-    /** A listener for when the user enters/exits Desktop Mode. */
-    public interface DesktopVisibilityListener {
-        /**
-         * Callback for when the user enters or exits Desktop Mode
-         *
-         * @param visible whether Desktop Mode is now visible
-         */
-        void onDesktopVisibilityChanged(boolean visible);
-    }
-
-    /**
-     * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher
-     * activity via the controller.
-     */
-    private static class DesktopTaskListenerImpl extends IDesktopTaskListener.Stub {
-
-        private DesktopVisibilityController mController;
-        private final int mDisplayId;
-
-        DesktopTaskListenerImpl(@NonNull DesktopVisibilityController controller, int displayId) {
-            mController = controller;
-            mDisplayId = displayId;
-        }
-
-        /**
-         * Clears any references to the controller.
-         */
-        void release() {
-            mController = null;
-        }
-
-        @Override
-        public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
-            MAIN_EXECUTOR.execute(() -> {
-                if (mController != null && displayId == mDisplayId) {
-                    if (DEBUG) {
-                        Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
-                    }
-                    mController.setVisibleDesktopTasksCount(visibleTasksCount);
-                }
-            });
-        }
-
-        @Override
-        public void onStashedChanged(int displayId, boolean stashed) {
-            Log.w(TAG, "DesktopTaskListenerImpl: onStashedChanged is deprecated");
-        }
-
-        @Override
-        public void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding) {
-            MAIN_EXECUTOR.execute(() -> {
-                if (mController != null && DesktopModeStatus.useRoundedCorners()) {
-                    Log.d(TAG, "DesktopTaskListenerImpl: doesAnyTaskRequireTaskbarRounding= "
-                            + doesAnyTaskRequireTaskbarRounding);
-                    mController.notifyTaskbarDesktopModeListeners(
-                            doesAnyTaskRequireTaskbarRounding);
-                }
-            });
-        }
-
-        public void onEnterDesktopModeTransitionStarted(int transitionDuration) {
-
-        }
-
-        @Override
-        public void onExitDesktopModeTransitionStarted(int transitionDuration) {
-
-        }
-    }
-
-    /** A listener for Taskbar in Desktop Mode. */
-    public interface TaskbarDesktopModeListener {
-        /**
-         * Callback for when task is resized in desktop mode.
-         *
-         * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness.
-         */
-        void onTaskbarCornerRoundingUpdate(boolean doesAnyTaskRequireTaskbarRounding);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
new file mode 100644
index 0000000..5a8302c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -0,0 +1,420 @@
+/*
+ * 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.statehandlers
+
+import android.content.Context
+import android.os.Debug
+import android.util.Log
+import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+import com.android.launcher3.LauncherState
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.statemanager.BaseState
+import com.android.launcher3.statemanager.StatefulActivity
+import com.android.launcher3.uioverrides.QuickstepLauncher
+import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener
+import com.android.quickstep.GestureState.GestureEndTarget
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.fallback.RecentsState
+import com.android.wm.shell.desktopmode.IDesktopTaskListener.Stub
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import java.io.PrintWriter
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+
+/**
+ * Controls the visibility of the workspace and the resumed / paused state when desktop mode is
+ * enabled.
+ */
+@LauncherAppSingleton
+class DesktopVisibilityController
+@Inject
+constructor(
+    @ApplicationContext private val context: Context,
+    systemUiProxy: SystemUiProxy,
+    lifecycleTracker: DaggerSingletonTracker,
+) {
+    private val desktopVisibilityListeners: MutableSet<DesktopVisibilityListener> = HashSet()
+    private val taskbarDesktopModeListeners: MutableSet<TaskbarDesktopModeListener> = HashSet()
+
+    /** Number of visible desktop windows in desktop mode. */
+    var visibleDesktopTasksCount: Int = 0
+        /**
+         * Sets the number of desktop windows that are visible and updates launcher visibility based
+         * on it.
+         */
+        set(visibleTasksCount) {
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    ("setVisibleDesktopTasksCount: visibleTasksCount=" +
+                        visibleTasksCount +
+                        " currentValue=" +
+                        field),
+                )
+            }
+
+            if (visibleTasksCount != field) {
+                val wasVisible = field > 0
+                val isVisible = visibleTasksCount > 0
+                val wereDesktopTasksVisibleBefore = areDesktopTasksVisible()
+                field = visibleTasksCount
+                val areDesktopTasksVisibleNow = areDesktopTasksVisible()
+                if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
+                    notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow)
+                }
+
+                if (
+                    !ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue && wasVisible != isVisible
+                ) {
+                    // TODO: b/333533253 - Remove after flag rollout
+                    if (field > 0) {
+                        if (!inOverviewState) {
+                            // When desktop tasks are visible & we're not in overview, we want
+                            // launcher
+                            // to appear paused, this ensures that taskbar displays.
+                            markLauncherPaused()
+                        }
+                    } else {
+                        // If desktop tasks aren't visible, ensure that launcher appears resumed to
+                        // behave normally.
+                        markLauncherResumed()
+                    }
+                }
+            }
+        }
+
+    private var inOverviewState = false
+    private var backgroundStateEnabled = false
+    private var gestureInProgress = false
+
+    private var desktopTaskListener: DesktopTaskListenerImpl?
+
+    init {
+        desktopTaskListener = DesktopTaskListenerImpl(this, context.displayId)
+        systemUiProxy.setDesktopTaskListener(desktopTaskListener)
+
+        lifecycleTracker.addCloseable {
+            desktopTaskListener = null
+            systemUiProxy.setDesktopTaskListener(null)
+        }
+    }
+
+    /** Whether desktop tasks are visible in desktop mode. */
+    fun areDesktopTasksVisible(): Boolean {
+        val desktopTasksVisible: Boolean = visibleDesktopTasksCount > 0
+        if (DEBUG) {
+            Log.d(TAG, "areDesktopTasksVisible: desktopVisible=$desktopTasksVisible")
+        }
+        return desktopTasksVisible
+    }
+
+    /** Whether desktop tasks are visible in desktop mode. */
+    fun areDesktopTasksVisibleAndNotInOverview(): Boolean {
+        val desktopTasksVisible: Boolean = visibleDesktopTasksCount > 0
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                ("areDesktopTasksVisible: desktopVisible=" +
+                    desktopTasksVisible +
+                    " overview=" +
+                    inOverviewState),
+            )
+        }
+        return desktopTasksVisible && !inOverviewState
+    }
+
+    /** Registers a listener for Taskbar changes in Desktop Mode. */
+    fun registerTaskbarDesktopModeListener(listener: TaskbarDesktopModeListener) {
+        taskbarDesktopModeListeners.add(listener)
+    }
+
+    /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */
+    fun unregisterTaskbarDesktopModeListener(listener: TaskbarDesktopModeListener) {
+        taskbarDesktopModeListeners.remove(listener)
+    }
+
+    fun onLauncherStateChanged(state: LauncherState) {
+        onLauncherStateChanged(
+            state,
+            state === LauncherState.BACKGROUND_APP,
+            state.isRecentsViewVisible,
+        )
+    }
+
+    fun onLauncherStateChanged(state: RecentsState) {
+        onLauncherStateChanged(
+            state,
+            state === RecentsState.BACKGROUND_APP,
+            state.isRecentsViewVisible,
+        )
+    }
+
+    /** Process launcher state change and update launcher view visibility based on desktop state */
+    fun onLauncherStateChanged(
+        state: BaseState<*>,
+        isBackgroundAppState: Boolean,
+        isRecentsViewVisible: Boolean,
+    ) {
+        if (DEBUG) {
+            Log.d(TAG, "onLauncherStateChanged: newState=$state")
+        }
+        setBackgroundStateEnabled(isBackgroundAppState)
+        // Desktop visibility tracks overview and background state separately
+        setOverviewStateEnabled(!isBackgroundAppState && isRecentsViewVisible)
+    }
+
+    private fun setOverviewStateEnabled(overviewStateEnabled: Boolean) {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                ("setOverviewStateEnabled: enabled=" +
+                    overviewStateEnabled +
+                    " currentValue=" +
+                    inOverviewState),
+            )
+        }
+        if (overviewStateEnabled != inOverviewState) {
+            val wereDesktopTasksVisibleBefore = areDesktopTasksVisible()
+            inOverviewState = overviewStateEnabled
+            val areDesktopTasksVisibleNow = areDesktopTasksVisible()
+            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
+                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow)
+            }
+
+            if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) {
+                return
+            }
+
+            // TODO: b/333533253 - Clean up after flag rollout
+            if (inOverviewState) {
+                markLauncherResumed()
+            } else if (areDesktopTasksVisibleNow && !gestureInProgress) {
+                // Switching out of overview state and gesture finished.
+                // If desktop tasks are still visible, hide launcher again.
+                markLauncherPaused()
+            }
+        }
+    }
+
+    /** Registers a listener for Taskbar changes in Desktop Mode. */
+    fun registerDesktopVisibilityListener(listener: DesktopVisibilityListener) {
+        desktopVisibilityListeners.add(listener)
+    }
+
+    /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */
+    fun unregisterDesktopVisibilityListener(listener: DesktopVisibilityListener) {
+        desktopVisibilityListeners.remove(listener)
+    }
+
+    private fun notifyDesktopVisibilityListeners(areDesktopTasksVisible: Boolean) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyDesktopVisibilityListeners: visible=$areDesktopTasksVisible")
+        }
+        for (listener in desktopVisibilityListeners) {
+            listener.onDesktopVisibilityChanged(areDesktopTasksVisible)
+        }
+    }
+
+    private fun notifyTaskbarDesktopModeListeners(doesAnyTaskRequireTaskbarRounding: Boolean) {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "notifyTaskbarDesktopModeListeners: doesAnyTaskRequireTaskbarRounding=" +
+                    doesAnyTaskRequireTaskbarRounding,
+            )
+        }
+        for (listener in taskbarDesktopModeListeners) {
+            listener.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding)
+        }
+    }
+
+    /** TODO: b/333533253 - Remove after flag rollout */
+    private fun setBackgroundStateEnabled(backgroundStateEnabled: Boolean) {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                ("setBackgroundStateEnabled: enabled=" +
+                    backgroundStateEnabled +
+                    " currentValue=" +
+                    this.backgroundStateEnabled),
+            )
+        }
+        if (backgroundStateEnabled != this.backgroundStateEnabled) {
+            this.backgroundStateEnabled = backgroundStateEnabled
+            if (this.backgroundStateEnabled) {
+                markLauncherResumed()
+            } else if (areDesktopTasksVisible() && !gestureInProgress) {
+                // Switching out of background state. If desktop tasks are visible, pause launcher.
+                markLauncherPaused()
+            }
+        }
+    }
+
+    var isRecentsGestureInProgress: Boolean
+        /**
+         * Whether recents gesture is currently in progress.
+         *
+         * TODO: b/333533253 - Remove after flag rollout
+         */
+        get() = gestureInProgress
+        /** TODO: b/333533253 - Remove after flag rollout */
+        private set(gestureInProgress) {
+            if (gestureInProgress != this.gestureInProgress) {
+                this.gestureInProgress = gestureInProgress
+            }
+        }
+
+    /**
+     * Notify controller that recents gesture has started.
+     *
+     * TODO: b/333533253 - Remove after flag rollout
+     */
+    fun setRecentsGestureStart() {
+        if (DEBUG) {
+            Log.d(TAG, "setRecentsGestureStart")
+        }
+        isRecentsGestureInProgress = true
+    }
+
+    /**
+     * Notify controller that recents gesture finished with the given
+     * [com.android.quickstep.GestureState.GestureEndTarget]
+     *
+     * TODO: b/333533253 - Remove after flag rollout
+     */
+    fun setRecentsGestureEnd(endTarget: GestureEndTarget?) {
+        if (DEBUG) {
+            Log.d(TAG, "setRecentsGestureEnd: endTarget=$endTarget")
+        }
+        isRecentsGestureInProgress = false
+
+        if (endTarget == null) {
+            // Gesture did not result in a new end target. Ensure launchers gets paused again.
+            markLauncherPaused()
+        }
+    }
+
+    /** TODO: b/333533253 - Remove after flag rollout */
+    private fun markLauncherPaused() {
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) {
+            return
+        }
+        if (DEBUG) {
+            Log.d(TAG, "markLauncherPaused " + Debug.getCaller())
+        }
+        val activity: StatefulActivity<LauncherState>? =
+            QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext()
+        activity?.setPaused()
+    }
+
+    /** TODO: b/333533253 - Remove after flag rollout */
+    private fun markLauncherResumed() {
+        if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) {
+            return
+        }
+        if (DEBUG) {
+            Log.d(TAG, "markLauncherResumed " + Debug.getCaller())
+        }
+        val activity: StatefulActivity<LauncherState>? =
+            QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext()
+        // Check activity state before calling setResumed(). Launcher may have been actually
+        // paused (eg fullscreen task moved to front).
+        // In this case we should not mark the activity as resumed.
+        if (activity != null && activity.isResumed) {
+            activity.setResumed()
+        }
+    }
+
+    fun dumpLogs(prefix: String, pw: PrintWriter) {
+        pw.println(prefix + "DesktopVisibilityController:")
+
+        pw.println("$prefix\tdesktopVisibilityListeners=$desktopVisibilityListeners")
+        pw.println("$prefix\tvisibleDesktopTasksCount=$visibleDesktopTasksCount")
+        pw.println("$prefix\tinOverviewState=$inOverviewState")
+        pw.println("$prefix\tbackgroundStateEnabled=$backgroundStateEnabled")
+        pw.println("$prefix\tgestureInProgress=$gestureInProgress")
+        pw.println("$prefix\tdesktopTaskListener=$desktopTaskListener")
+        pw.println("$prefix\tcontext=$context")
+    }
+
+    /**
+     * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher
+     * activity via the controller.
+     */
+    private class DesktopTaskListenerImpl(
+        controller: DesktopVisibilityController,
+        private val displayId: Int,
+    ) : Stub() {
+        private val controller = WeakReference(controller)
+
+        override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+            if (displayId != this.displayId) return
+            Executors.MAIN_EXECUTOR.execute {
+                controller.get()?.apply {
+                    if (DEBUG) {
+                        Log.d(TAG, "desktop visible tasks count changed=$visibleTasksCount")
+                    }
+                    visibleDesktopTasksCount = visibleTasksCount
+                }
+            }
+        }
+
+        override fun onStashedChanged(displayId: Int, stashed: Boolean) {
+            Log.w(TAG, "DesktopTaskListenerImpl: onStashedChanged is deprecated")
+        }
+
+        override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
+            if (!DesktopModeStatus.useRoundedCorners()) return
+            Executors.MAIN_EXECUTOR.execute {
+                controller.get()?.apply {
+                    Log.d(
+                        TAG,
+                        "DesktopTaskListenerImpl: doesAnyTaskRequireTaskbarRounding= " +
+                            doesAnyTaskRequireTaskbarRounding,
+                    )
+                    notifyTaskbarDesktopModeListeners(doesAnyTaskRequireTaskbarRounding)
+                }
+            }
+        }
+
+        override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) {}
+
+        override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {}
+    }
+
+    /** A listener for Taskbar in Desktop Mode. */
+    interface TaskbarDesktopModeListener {
+        /**
+         * Callback for when task is resized in desktop mode.
+         *
+         * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness.
+         */
+        fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean)
+    }
+
+    companion object {
+        @JvmField
+        val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getDesktopVisibilityController)
+
+        private const val TAG = "DesktopVisController"
+        private const val DEBUG = false
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 060173a..ee9c6a1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -237,7 +237,6 @@
             @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp,
             TaskbarNavButtonController buttonController,
             ScopedUnfoldTransitionProgressProvider unfoldTransitionProgressProvider,
-            @NonNull DesktopVisibilityController desktopVisibilityController,
             boolean isPrimaryDisplay) {
         super(windowContext);
         mIsPrimaryDisplay = isPrimaryDisplay;
@@ -363,7 +362,7 @@
                 new KeyboardQuickSwitchController(),
                 new TaskbarPinningController(this),
                 bubbleControllersOptional,
-                new TaskbarDesktopModeController(desktopVisibilityController));
+                new TaskbarDesktopModeController(DesktopVisibilityController.INSTANCE.get(this)));
 
         mLauncherPrefs = LauncherPrefs.get(this);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 13f9a51..3fa0e8e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -62,7 +62,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider;
@@ -223,14 +222,11 @@
                 }
             };
 
-    @NonNull private final DesktopVisibilityController mDesktopVisibilityController;
-
     @SuppressLint("WrongConstant")
     public TaskbarManager(
             Context context,
             AllAppsActionManager allAppsActionManager,
-            TaskbarNavButtonCallbacks navCallbacks,
-            @NonNull DesktopVisibilityController desktopVisibilityController) {
+            TaskbarNavButtonCallbacks navCallbacks) {
         Display display =
                 context.getSystemService(DisplayManager.class).getDisplay(context.getDisplayId());
         mWindowContext = context.createWindowContext(display,
@@ -240,7 +236,6 @@
         mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 ? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
                 : null;
-        mDesktopVisibilityController = desktopVisibilityController;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mWindowContext.getSystemService(WindowManager.class);
             createTaskbarRootLayout(getDefaultDisplayId());
@@ -800,7 +795,7 @@
     private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
         TaskbarActivityContext newTaskbar = new TaskbarActivityContext(mWindowContext,
                 mNavigationBarPanelContext, dp, mDefaultNavButtonController,
-                mUnfoldProgressProvider, mDesktopVisibilityController, isDefaultDisplay(displayId));
+                mUnfoldProgressProvider, isDefaultDisplay(displayId));
 
         addTaskbarToMap(displayId, newTaskbar);
         return newTaskbar;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 6d744c2..70c53fa 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1018,9 +1018,9 @@
 
     @Override
     public void setResumed() {
-        DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
+        DesktopVisibilityController desktopVisibilityController =
+                DesktopVisibilityController.INSTANCE.get(this);
         if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
-                && desktopVisibilityController != null
                 && desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview()
                 && !desktopVisibilityController.isRecentsGestureInProgress()) {
             // Return early to skip setting activity to appear as resumed
@@ -1187,12 +1187,6 @@
     }
 
     @Nullable
-    @Override
-    public DesktopVisibilityController getDesktopVisibilityController() {
-        return mTISBindHelper.getDesktopVisibilityController();
-    }
-
-    @Nullable
     public UnfoldTransitionProgressProvider getUnfoldTransitionProgressProvider() {
         return mUnfoldTransitionProgressProvider;
     }
@@ -1347,11 +1341,8 @@
 
     @Override
     public boolean areDesktopTasksVisible() {
-        DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
-        if (desktopVisibilityController != null) {
-            return desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview();
-        }
-        return false;
+        return DesktopVisibilityController.INSTANCE.get(this)
+                .areDesktopTasksVisibleAndNotInOverview();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 1437a6e..7cab751 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -86,8 +86,8 @@
         if (endTarget != null) {
             // We were on our way to this state when we got canceled, end there instead.
             startState = stateFromGestureEndTarget(endTarget);
-            DesktopVisibilityController controller = getDesktopVisibilityController();
-            if (controller != null && controller.areDesktopTasksVisibleAndNotInOverview()
+            if (DesktopVisibilityController.INSTANCE.get(activity)
+                    .areDesktopTasksVisibleAndNotInOverview()
                     && endTarget == LAST_TASK) {
                 // When we are cancelling the transition and going back to last task, move to
                 // rest state instead when desktop tasks are visible.
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index 2171c47..e2ebaa5 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -179,13 +179,6 @@
         mOnInitBackgroundStateUICallback = callback;
     }
 
-    @Nullable
-    public DesktopVisibilityController getDesktopVisibilityController() {
-        CONTAINER_TYPE container = getCreatedContainer();
-
-        return container == null ? null : container.getDesktopVisibilityController();
-    }
-
     /**
      * Called when the gesture ends and the animation starts towards the given target. Used to add
      * an optional additional animation with the same duration.
@@ -241,9 +234,8 @@
         if (endTarget != null) {
             // We were on our way to this state when we got canceled, end there instead.
             startState = stateFromGestureEndTarget(endTarget);
-            DesktopVisibilityController controller = getDesktopVisibilityController();
-            if (controller != null && controller.areDesktopTasksVisibleAndNotInOverview()
-                    && endTarget == LAST_TASK) {
+            if (DesktopVisibilityController.INSTANCE.get(recentsView.getContext())
+                    .areDesktopTasksVisibleAndNotInOverview() && endTarget == LAST_TASK) {
                 // When we are cancelling the transition and going back to last task, move to
                 // rest state instead when desktop tasks are visible.
                 // If a fullscreen task is visible, launcher goes to normal state when the
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 243a577..7af0618 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.MSDLPlayerWrapper;
@@ -105,9 +106,8 @@
         boolean canUseWorkspaceView = workspaceView != null
                 && workspaceView.isAttachedToWindow()
                 && workspaceView.getHeight() > 0
-                && (mContainer.getDesktopVisibilityController() == null
-                || !mContainer.getDesktopVisibilityController()
-                    .areDesktopTasksVisibleAndNotInOverview());
+                && !DesktopVisibilityController.INSTANCE.get(mContainer)
+                        .areDesktopTasksVisibleAndNotInOverview();
 
         mContainer.getRootView().setForceHideBackArrow(true);
 
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 17c17cc..b34b502 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -65,7 +65,6 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
@@ -558,10 +557,4 @@
     public boolean isRecentsViewVisible() {
         return getStateManager().getState().isRecentsViewVisible();
     }
-
-    @Nullable
-    @Override
-    public DesktopVisibilityController getDesktopVisibilityController() {
-        return mTISBindHelper.getDesktopVisibilityController();
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 91d0776..89337e5 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -67,16 +67,13 @@
      * running tasks
      */
     public RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy) {
-        DesktopVisibilityController desktopVisibilityController =
-                sizingStrategy.getDesktopVisibilityController();
-        if (desktopVisibilityController != null) {
-            int visibleTasksCount = desktopVisibilityController.getVisibleDesktopTasksCount();
-            if (visibleTasksCount > 0) {
-                // Allocate +1 to account for a new task added to the desktop mode
-                int numHandles = visibleTasksCount + 1;
-                init(context, sizingStrategy, numHandles, true /* forDesktop */);
-                return;
-            }
+        int visibleTasksCount = DesktopVisibilityController.INSTANCE.get(context)
+                .getVisibleDesktopTasksCount();
+        if (visibleTasksCount > 0) {
+            // Allocate +1 to account for a new task added to the desktop mode
+            int numHandles = visibleTasksCount + 1;
+            init(context, sizingStrategy, numHandles, true /* forDesktop */);
+            return;
         }
 
         // Assume 2 handles needed for split, scale down as needed later on when we actually
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 3bfdc21..aea02af 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -424,18 +424,6 @@
             return tis.mTaskbarManager;
         }
 
-        /**
-         * Returns the {@link DesktopVisibilityController}
-         * <p>
-         * Returns {@code null} if TouchInteractionService is not connected
-         */
-        @Nullable
-        public DesktopVisibilityController getDesktopVisibilityController() {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return null;
-            return tis.mDesktopVisibilityController;
-        }
-
         @VisibleForTesting
         public void injectFakeTrackpadForTesting() {
             TouchInteractionService tis = mTis.get();
@@ -554,7 +542,6 @@
 
     private NavigationMode mGestureStartNavMode = null;
 
-    private DesktopVisibilityController mDesktopVisibilityController;
     private DesktopAppLaunchTransitionManager mDesktopAppLaunchTransitionManager;
 
     @Override
@@ -578,9 +565,7 @@
             initInputMonitor("onTrackpadConnected()");
         });
 
-        mDesktopVisibilityController = new DesktopVisibilityController(this);
-        mTaskbarManager = new TaskbarManager(
-                this, mAllAppsActionManager, mNavCallbacks, mDesktopVisibilityController);
+        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
         mDesktopAppLaunchTransitionManager =
                 new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this));
         mDesktopAppLaunchTransitionManager.registerTransitions();
@@ -741,7 +726,6 @@
             mDesktopAppLaunchTransitionManager.unregisterTransitions();
         }
         mDesktopAppLaunchTransitionManager = null;
-        mDesktopVisibilityController.onDestroy();
 
         LockedUserState.get(this).removeOnUserUnlockedRunnable(mUserUnlockedRunnable);
         ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener);
@@ -1164,7 +1148,7 @@
             createdOverviewContainer.getDeviceProfile().dump(this, "", pw);
         }
         mTaskbarManager.dumpLogs("", pw);
-        mDesktopVisibilityController.dumpLogs("", pw);
+        DesktopVisibilityController.INSTANCE.get(this).dumpLogs("", pw);
         pw.println("ContextualSearchStateManager:");
         ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw);
         SystemUiProxy.INSTANCE.get(this).dump(pw);
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index 20a66dd..549c15b 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -19,6 +19,7 @@
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
@@ -43,4 +44,6 @@
     RecentsDisplayModel getRecentsDisplayModel();
 
     OverviewComponentObserver getOverviewComponentObserver();
+
+    DesktopVisibilityController getDesktopVisibilityController();
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 9625d29..b76e39a 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.statemanager.StatefulContainer;
@@ -268,9 +269,7 @@
 
     @Override
     public void onStateTransitionComplete(RecentsState finalState) {
-        if (mContainer.getDesktopVisibilityController() != null) {
-            mContainer.getDesktopVisibilityController().onLauncherStateChanged(finalState);
-        }
+        DesktopVisibilityController.INSTANCE.get(mContainer).onLauncherStateChanged(finalState);
         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/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 9bd7a19..9a38ff6 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -37,7 +37,6 @@
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
 import com.android.launcher3.R
 import com.android.launcher3.compat.AccessibilityManagerCompat
-import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory
 import com.android.launcher3.statemanager.StatefulContainer
@@ -310,10 +309,6 @@
         return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
     }
 
-    override fun getDesktopVisibilityController(): DesktopVisibilityController? {
-        return tisBindHelper.desktopVisibilityController
-    }
-
     override fun setTaskbarUIController(taskbarUIController: TaskbarUIController?) {
         this.taskbarUIController = taskbarUIController
     }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index a952617..b040723 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -89,7 +89,9 @@
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
         viewAttachedScope =
-            CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
+            CoroutineScope(
+                SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
+            )
         viewData = RecentsDependencies.get(this)
         updateViewDataValues()
         viewModel = RecentsDependencies.get(this)
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index 0f61b95..677875c 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -67,7 +67,9 @@
 
     fun init() {
         overlayInitializedScope =
-            CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskOverlayHelper"))
+            CoroutineScope(
+                SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskOverlayHelper")
+            )
         viewModel =
             TaskOverlayViewModel(
                 task = task,
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
index 724fa40..d00a39c 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
@@ -161,7 +161,11 @@
             statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE)
             return false
         }
-
+        if (isFakeLandscape()) {
+            // TODO (b/383421642): Fake landscape is to be removed in 25Q3 and this entire block
+            // can be removed when that happens.
+            return false
+        }
         return true
     }
 
@@ -197,6 +201,13 @@
         return true
     }
 
+    private fun isFakeLandscape(): Boolean =
+        getRecentsContainerInterface()
+            ?.getCreatedContainer()
+            ?.getOverviewPanel<RecentsView<*, *>>()
+            ?.getPagedOrientationHandler()
+            ?.isLayoutNaturalToLauncher == false
+
     private fun isInSplitscreen(): Boolean {
         return topTaskTracker.getRunningSplitTaskIds().isNotEmpty()
     }
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 7fadc7d..4d56c63 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -27,10 +27,8 @@
 import android.view.WindowMetrics;
 
 import com.android.internal.policy.SystemBarUtils;
-import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
-import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -48,14 +46,13 @@
 @LauncherAppSingleton
 public class SystemWindowManagerProxy extends WindowManagerProxy {
 
-    private final TISBindHelper mTISBindHelper;
+    private final DesktopVisibilityController mDesktopVisibilityController;
+
 
     @Inject
-    public SystemWindowManagerProxy(@ApplicationContext Context context,
-            DaggerSingletonTracker lifecycleTracker) {
+    public SystemWindowManagerProxy(DesktopVisibilityController desktopVisibilityController) {
         super(true);
-        mTISBindHelper = new TISBindHelper(context, binder -> {});
-        lifecycleTracker.addCloseable(mTISBindHelper::onDestroy);
+        mDesktopVisibilityController = desktopVisibilityController;
     }
 
     @Override
@@ -65,10 +62,18 @@
     }
 
     @Override
+    public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) {
+        mDesktopVisibilityController.registerDesktopVisibilityListener(listener);
+    }
+
+    @Override
+    public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) {
+        mDesktopVisibilityController.unregisterDesktopVisibilityListener(listener);
+    }
+
+    @Override
     public boolean isInDesktopMode() {
-        DesktopVisibilityController desktopController =
-                mTISBindHelper.getDesktopVisibilityController();
-        return desktopController != null && desktopController.areDesktopTasksVisible();
+        return mDesktopVisibilityController.areDesktopTasksVisible();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
index b238dec..027dc08 100644
--- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
@@ -26,7 +26,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.TaskbarManager;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.TouchInteractionService;
@@ -110,11 +109,6 @@
         return mBinder == null ? null : mBinder.getTaskbarManager();
     }
 
-    @Nullable
-    public DesktopVisibilityController getDesktopVisibilityController() {
-        return mBinder == null ? null : mBinder.getDesktopVisibilityController();
-    }
-
     /**
      * Sets flag whether a predictive back-to-home animation is in progress
      */
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index b9f44fe..7a7a7f9 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -168,9 +168,7 @@
 
     @Override
     public void onStateTransitionComplete(LauncherState finalState) {
-        if (mContainer.getDesktopVisibilityController() != null) {
-            mContainer.getDesktopVisibilityController().onLauncherStateChanged(finalState);
-        }
+        DesktopVisibilityController.INSTANCE.get(mContainer).onLauncherStateChanged(finalState);
 
         if (!finalState.isRecentsViewVisible) {
             // Clean-up logic that occurs when recents is no longer in use/visible.
@@ -269,34 +267,26 @@
     public void onGestureAnimationStart(Task[] runningTasks,
             RotationTouchHelper rotationTouchHelper) {
         super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
-        DesktopVisibilityController desktopVisibilityController =
-                mContainer.getDesktopVisibilityController();
-        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
-                && desktopVisibilityController != null) {
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             // TODO: b/333533253 - Remove after flag rollout
-            desktopVisibilityController.setRecentsGestureStart();
+            DesktopVisibilityController.INSTANCE.get(mContainer).setRecentsGestureStart();
         }
     }
 
     @Override
     public void onGestureAnimationEnd() {
-        DesktopVisibilityController desktopVisibilityController =
-                mContainer.getDesktopVisibilityController();
+        final DesktopVisibilityController desktopVisibilityController =
+                DesktopVisibilityController.INSTANCE.get(mContainer);
         boolean showDesktopApps = false;
-        GestureState.GestureEndTarget endTarget = null;
-        if (desktopVisibilityController != null) {
-            desktopVisibilityController = mContainer.getDesktopVisibilityController();
-            endTarget = mCurrentGestureEndTarget;
-            if (endTarget == GestureState.GestureEndTarget.LAST_TASK
-                    && desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview()) {
-                // Recents gesture was cancelled and we are returning to the previous task.
-                // After super class has handled clean up, show desktop apps on top again
-                showDesktopApps = true;
-            }
+        GestureState.GestureEndTarget endTarget = mCurrentGestureEndTarget;
+        if (endTarget == GestureState.GestureEndTarget.LAST_TASK
+                && desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview()) {
+            // Recents gesture was cancelled and we are returning to the previous task.
+            // After super class has handled clean up, show desktop apps on top again
+            showDesktopApps = true;
         }
         super.onGestureAnimationEnd();
-        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
-                && desktopVisibilityController != null) {
+        if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             // TODO: b/333533253 - Remove after flag rollout
             desktopVisibilityController.setRecentsGestureEnd(endTarget);
         }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 3983fe9..ab96474 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1250,7 +1250,6 @@
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
-
         // Clear the task data for the removed child if it was visible unless:
         // - It's the initial taskview for entering split screen, we only pretend to dismiss the
         // task
@@ -1258,22 +1257,25 @@
         if (child instanceof TaskView) {
             mTaskViewCount = Math.max(0, --mTaskViewCount);
             if (child != mSplitHiddenTaskView && child != mMovingTaskView) {
-                TaskView taskView = (TaskView) child;
-                for (int i : taskView.getTaskIds()) {
-                    mHasVisibleTaskData.delete(i);
-                }
-                if (child instanceof GroupedTaskView) {
-                    mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
-                } else if (child instanceof DesktopTaskView) {
-                    mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
-                } else {
-                    mTaskViewPool.recycle(taskView);
-                }
-                mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, !hasTaskViews());
+                clearAndRecycleTaskView((TaskView) child);
             }
         }
     }
 
+    private void clearAndRecycleTaskView(TaskView taskView) {
+        for (int i : taskView.getTaskIds()) {
+            mHasVisibleTaskData.delete(i);
+        }
+        if (taskView instanceof GroupedTaskView) {
+            mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
+        } else if (taskView instanceof DesktopTaskView) {
+            mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
+        } else {
+            mTaskViewPool.recycle(taskView);
+        }
+        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, !hasTaskViews());
+    }
+
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
@@ -5434,6 +5436,13 @@
         mSplitHiddenTaskViewIndex = -1;
         if (mSplitHiddenTaskView != null) {
             mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
+            // mSplitHiddenTaskView is set when split select animation starts. The TaskView is only
+            // removed when when the animation finishes. So in the case of overview being dismissed
+            // during the animation, we should not call clearAndRecycleTaskView() because it has
+            // not been removed yet.
+            if (mSplitHiddenTaskView.getParent() == null) {
+                clearAndRecycleTaskView(mSplitHiddenTaskView);
+            }
             mSplitHiddenTaskView = null;
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index a1d22fe..b1a4808 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -29,7 +29,6 @@
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.ScrimView;
@@ -209,9 +208,6 @@
                         .build());
     }
 
-    @Nullable
-    DesktopVisibilityController getDesktopVisibilityController();
-
     void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController);
 
     @Nullable TaskbarUIController getTaskbarUIController();
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index 87771c6..d92c4d0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -44,10 +44,12 @@
         // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside
         // viewAttachedScope.
         recentsViewModel.setRunningTaskShowScreenshot(true)
-        recentsCoroutineScope.launch(dispatcherProvider.main) {
+        recentsCoroutineScope.launch(dispatcherProvider.background) {
             recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
             recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
-            withContext(Dispatchers.Main) { ViewUtils.postFrameDrawn(taskView, onFinishRunnable) }
+            withContext(Dispatchers.Main.immediate) {
+                ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
+            }
         }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
index 6e2f74a..0e066cd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
@@ -17,15 +17,16 @@
 package com.android.launcher3.taskbar
 
 import android.content.Context
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.ConstantItem
 import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.TestUtil
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
 
 object TaskbarControllerTestUtil {
     inline fun runOnMainSync(crossinline runTest: () -> Unit) {
-        getInstrumentation().runOnMainSync { runTest() }
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) { runTest() }
     }
 
     /** Returns a property to read/write the value of a [ConstantItem]. */
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
index 36e8a82..13880f1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -85,7 +85,8 @@
 
     @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this)
 
-    @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 5)
+    val taskbarUnitTestRule = TaskbarUnitTestRule(this, context, this::onControllersInitialized)
 
     @InjectController lateinit var taskbarViewController: TaskbarViewController
     @InjectController lateinit var recentAppsController: TaskbarRecentAppsController
@@ -94,18 +95,29 @@
 
     private var desktopTaskListener: IDesktopTaskListener? = null
 
-    @Before
-    fun ensureRunningAppsShowing() {
+    private var currentControllerInitCallback: () -> Unit = {}
+        set(value) {
+            runOnMainSync { value.invoke() }
+            field = value
+        }
+
+    private fun onControllersInitialized() {
         runOnMainSync {
             if (!recentAppsController.canShowRunningApps) {
                 recentAppsController.onDestroy()
                 recentAppsController.canShowRunningApps = true
                 recentAppsController.init(taskbarUnitTestRule.activityContext.controllers)
             }
-            recentsModel.resolvePendingTaskRequests()
+
+            currentControllerInitCallback.invoke()
         }
     }
 
+    @Before
+    fun ensureRunningAppsShowing() {
+        runOnMainSync { recentsModel.resolvePendingTaskRequests() }
+    }
+
     @Test
     @TaskbarMode(PINNED)
     fun testTaskbarWithMaxNumIcons_pinned() {
@@ -196,7 +208,7 @@
         var initialMaxNumIconViews = maxNumberOfTaskbarIcons
         assertThat(initialMaxNumIconViews).isGreaterThan(0)
 
-        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) }
 
         val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
         assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
@@ -210,7 +222,7 @@
         var initialMaxNumIconViews = maxNumberOfTaskbarIcons
         assertThat(initialMaxNumIconViews).isGreaterThan(0)
 
-        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) }
 
         val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
         assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
@@ -228,7 +240,7 @@
     fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() {
         var initialMaxNumIconViews = maxNumberOfTaskbarIcons
         assertThat(initialMaxNumIconViews).isGreaterThan(0)
-        runOnMainSync {
+        currentControllerInitCallback = {
             bubbleStashController.stashBubbleBarImmediate()
             bubbleBarViewController.setHiddenForBubbles(false)
         }
@@ -247,7 +259,7 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testStashingBubbleBarMaintainsMaxNumIcons_transient() {
-        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) }
 
         val initialNumIcons = currentNumberOfTaskbarIcons
         val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
@@ -261,15 +273,13 @@
     @Test
     @TaskbarMode(PINNED)
     fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() {
-        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) }
 
         val initialNumIcons = currentNumberOfTaskbarIcons
         val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
 
-        runOnMainSync {
-            bubbleBarViewController.setHiddenForBubbles(true)
-            animatorTestRule.advanceTimeBy(150)
-        }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) }
+        runOnMainSync { animatorTestRule.advanceTimeBy(150) }
 
         val maxNumIconViews = maxNumberOfTaskbarIcons
         assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
@@ -282,15 +292,13 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testHidingBubbleBarIncreasesMaxNumIcons_transient() {
-        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) }
 
         val initialNumIcons = currentNumberOfTaskbarIcons
         val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
 
-        runOnMainSync {
-            bubbleBarViewController.setHiddenForBubbles(true)
-            animatorTestRule.advanceTimeBy(150)
-        }
+        currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) }
+        runOnMainSync { animatorTestRule.advanceTimeBy(150) }
 
         val maxNumIconViews = maxNumberOfTaskbarIcons
         assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
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 07b32af..e150568 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
@@ -24,7 +24,6 @@
 import android.provider.Settings.Secure.getUriFor
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.LauncherAppState
-import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarControllers
 import com.android.launcher3.taskbar.TaskbarManager
@@ -72,6 +71,7 @@
 class TaskbarUnitTestRule(
     private val testInstance: Any,
     private val context: TaskbarWindowSandboxContext,
+    private val controllerInjectionCallback: () -> Unit = {},
 ) : TestRule {
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -110,11 +110,13 @@
                                     PendingIntent(IIntentSender.Default())
                                 },
                                 object : TaskbarNavButtonCallbacks {},
-                                DesktopVisibilityController(context),
                             ) {
                             override fun recreateTaskbar() {
                                 super.recreateTaskbar()
-                                if (currentActivityContext != null) injectControllers()
+                                if (currentActivityContext != null) {
+                                    injectControllers()
+                                    controllerInjectionCallback.invoke()
+                                }
                             }
                         }
                     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
index 88774be..61971b1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
@@ -52,6 +52,7 @@
 import com.android.quickstep.DeviceConfigWrapper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TopTaskTracker;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 
@@ -82,6 +83,7 @@
     private @Mock BaseContainerInterface mMockContainerInterface;
     private @Mock RecentsViewContainer mMockRecentsViewContainer;
     private @Mock RecentsView mMockRecentsView;
+    private @Mock RecentsPagedOrientationHandler mMockOrientationHandler;
     private ContextualSearchInvoker mContextualSearchInvoker;
 
     @Before
@@ -190,6 +192,15 @@
     }
 
     @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_isFakeLandscape() {
+        when(mMockRecentsView.getPagedOrientationHandler()).thenReturn(mMockOrientationHandler);
+        when(mMockOrientationHandler.isLayoutNaturalToLauncher()).thenReturn(false);
+        assertFalse("Expect invocation checks to fail in fake landscape.",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+        verifyNoMoreInteractions(mMockStatsLogManager);
+    }
+
+    @Test
     public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticEnabled() {
         try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) {
             assertTrue("Expected invocation unchecked to succeed",
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index 83c8d6c..adf4597 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -29,6 +29,7 @@
         android:importantForAccessibility="no">
 
         <com.android.launcher3.views.AccessibilityActionsView
+            android:id="@+id/accessibility_action_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:contentDescription="@string/home_screen"
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4097dca..f68c8e0 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -630,6 +630,10 @@
         return new ColdRebootStartupLatencyLogger();
     }
 
+    @NonNull View getAccessibilityActionView() {
+        return findViewById(R.id.accessibility_action_view);
+    }
+
     /**
      * Provide {@link OnBackAnimationCallback} in below order:
      * <ol>
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index a064c88..bc751d9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -70,6 +70,7 @@
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.view.ViewCompat;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
@@ -303,6 +304,8 @@
     private final StatsLogManager mStatsLogManager;
 
     private final MSDLPlayerWrapper mMSDLPlayerWrapper;
+    @Nullable
+    private DragController.DragListener mAccessibilityDragListener;
 
     /**
      * Used to inflate the Workspace from XML.
@@ -512,6 +515,9 @@
             }
         }
 
+        if (mAccessibilityDragListener != null) {
+            mAccessibilityDragListener.onDragStart(dragObject, options);
+        }
         if (!mLauncher.isInState(EDIT_MODE)) {
             mLauncher.getStateManager().goToState(SPRING_LOADED);
         }
@@ -548,6 +554,9 @@
             }
         });
 
+        if (mAccessibilityDragListener != null) {
+            mAccessibilityDragListener.onDragEnd();
+        }
         mDragInfo = null;
         mDragSourceInternal = null;
     }
@@ -1656,7 +1665,7 @@
         child.setVisibility(INVISIBLE);
 
         if (options.isAccessibleDrag) {
-            mDragController.addDragListener(
+            mAccessibilityDragListener =
                     new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
                         @Override
                         protected void enableAccessibleDrag(boolean enable,
@@ -1669,7 +1678,7 @@
                                         IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
                             }
                         }
-                    });
+                    };
         }
 
         beginDragShared(child, this, options);
@@ -3519,8 +3528,15 @@
 
     @Override
     protected boolean canAnnouncePageDescription() {
-        // b/383247157: Disable disruptive home screen page announcement
-        return false;
+        return Float.compare(mOverlayProgress, 0f) == 0;
+    }
+
+    @Override
+    protected void announcePageForAccessibility() {
+        // Talkback focuses on AccessibilityActionView by default, so we need to modify the state
+        // description there in order for the change in page scroll to be announced.
+        ViewCompat.setStateDescription(mLauncher.getAccessibilityActionView(),
+                getCurrentPageDescription());
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 26912eb..d8a2a3d 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -56,6 +56,7 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
+import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -70,7 +71,8 @@
  * Utility class to cache properties of default display to avoid a system RPC on every call.
  */
 @SuppressLint("NewApi")
-public class DisplayController implements ComponentCallbacks, SafeCloseable {
+public class DisplayController implements ComponentCallbacks, SafeCloseable,
+        DesktopVisibilityListener {
 
     private static final String TAG = "DisplayController";
     private static final boolean DEBUG = false;
@@ -99,7 +101,6 @@
     private static final String TARGET_OVERLAY_PACKAGE = "android";
 
     private final Context mContext;
-    private final DisplayManager mDM;
 
     // Null for SDK < S
     private final Context mWindowContext;
@@ -121,13 +122,12 @@
     @VisibleForTesting
     protected DisplayController(Context context) {
         mContext = context;
-        mDM = context.getSystemService(DisplayManager.class);
-
         if (enableTaskbarPinning()) {
             attachTaskbarPinningSharedPreferenceChangeListener(mContext);
         }
 
-        Display display = mDM.getDisplay(DEFAULT_DISPLAY);
+        Display display = context.getSystemService(DisplayManager.class)
+                .getDisplay(DEFAULT_DISPLAY);
         mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
         mWindowContext.registerComponentCallbacks(this);
 
@@ -137,6 +137,7 @@
         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
         mInfo = new Info(mWindowContext, wmProxy,
                 wmProxy.estimateInternalDisplayBounds(mWindowContext));
+        wmProxy.registerDesktopVisibilityListener(this);
         FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
     }
 
@@ -215,12 +216,14 @@
             LauncherPrefs.get(mContext).removeListener(
                     mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING_IN_DESKTOP_MODE);
         }
-        if (mWindowContext != null) {
-            mWindowContext.unregisterComponentCallbacks(this);
-        } else {
-            // TODO: unregister broadcast receiver
-        }
+        mWindowContext.unregisterComponentCallbacks(this);
         mReceiver.unregisterReceiverSafely(mContext);
+        WindowManagerProxy.INSTANCE.get(mContext).unregisterDesktopVisibilityListener(this);
+    }
+
+    @Override
+    public void onDesktopVisibilityChanged(boolean visible) {
+        notifyConfigChange();
     }
 
     /**
diff --git a/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
index 8877535..1f01b07 100644
--- a/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
+++ b/src/com/android/launcher3/util/coroutines/DispatcherProvider.kt
@@ -33,7 +33,7 @@
 
     override val default: CoroutineDispatcher = Dispatchers.Default
     override val background: CoroutineDispatcher = bgDispatcher
-    override val main: CoroutineDispatcher = Dispatchers.Main
+    override val main: CoroutineDispatcher = Dispatchers.Main.immediate
     override val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
 }
 
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 1d9751e..e568eed 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -485,4 +485,21 @@
         return new Rect(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
                 cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
     }
+
+    /** Registers a listener for Taskbar changes in Desktop Mode.  */
+    public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) { }
+
+    /** Removes a previously registered listener for Taskbar changes in Desktop Mode.  */
+    public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) { }
+
+    /** A listener for when the user enters/exits Desktop Mode.  */
+    public interface DesktopVisibilityListener {
+        /**
+         * Callback for when the user enters or exits Desktop Mode
+         *
+         * @param visible whether Desktop Mode is now visible
+         */
+        void onDesktopVisibilityChanged(boolean visible);
+    }
+
 }
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
index 237f2a9..cb04e13 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
@@ -114,6 +114,7 @@
      * Similar to {@link TaplWorkspaceTest#testWorkspace} but here we also make sure we can delete
      * the pages.
      */
+    @ScreenRecord // b/381918059
     @Test
     public void testAddAndDeletePageAndFling() {
         Workspace workspace = mLauncher.getWorkspace();