Refactor all uses of DisplayController singleton INSTANCE

And make DisplayController display id aware

Test: locally tested on Tangor
Flag: EXEMPT refactor
Bug: 392858637

Change-Id: I805cd7323c48a2988c95b9fda7f6cfe4c153860c
diff --git a/quickstep/src/com/android/launcher3/dagger/Modules.kt b/quickstep/src/com/android/launcher3/dagger/Modules.kt
index 52be413..99631bf 100644
--- a/quickstep/src/com/android/launcher3/dagger/Modules.kt
+++ b/quickstep/src/com/android/launcher3/dagger/Modules.kt
@@ -19,8 +19,10 @@
 import com.android.launcher3.uioverrides.SystemApiWrapper
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl
 import com.android.launcher3.util.ApiWrapper
+import com.android.launcher3.util.PerDisplayObjectProvider
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.quickstep.fallback.window.RecentsDisplayModel
 import com.android.quickstep.util.GestureExclusionManager
 import com.android.quickstep.util.SystemWindowManagerProxy
 import dagger.Binds
@@ -52,3 +54,9 @@
     @JvmStatic
     fun provideGestureExclusionManager(): GestureExclusionManager = GestureExclusionManager.INSTANCE
 }
+
+@Module
+abstract class PerDisplayObjectProviderModule {
+    @Binds
+    abstract fun bindPerDisplayObjectProvider(impl: RecentsDisplayModel): PerDisplayObjectProvider
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 4143157..3f1bf3c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -202,7 +202,7 @@
     @Override
     public void onLauncherVisibilityChanged(boolean isVisible) {
         if (DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(mLauncher)) {
-            DisplayController.INSTANCE.get(mLauncher).notifyConfigChange();
+            DisplayController.get(mLauncher).notifyConfigChange();
         }
         onLauncherVisibilityChanged(isVisible, false /* fromInit */);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index a6d3cde..126888c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -324,9 +324,9 @@
                     new BubbleDragController(this),
                     new BubbleDismissController(this, mDragLayer),
                     new BubbleBarPinController(this, mDragLayer,
-                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                            () -> DisplayController.get(this).getInfo().currentSize),
                     new BubblePinController(this, mDragLayer,
-                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                            () -> DisplayController.get(this).getInfo().currentSize),
                     bubbleBarSwipeController,
                     new BubbleCreator(this)
             ));
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index f704254..eccccdf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -425,7 +425,7 @@
      */
     public void onUserUnlocked() {
         mUserUnlocked = true;
-        DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
+        DisplayController.get(mPrimaryWindowContext).addChangeListener(
                 mRecreationListener);
         recreateTaskbar();
         addTaskbarRootViewToWindow(getDefaultDisplayId());
@@ -797,7 +797,7 @@
         mTaskbarBroadcastReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
 
         if (mUserUnlocked) {
-            DisplayController.INSTANCE.get(mPrimaryWindowContext).removeChangeListener(
+            DisplayController.get(mPrimaryWindowContext).removeChangeListener(
                     mRecreationListener);
         }
         SettingsCache.INSTANCE.get(mPrimaryWindowContext)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c001123..5025afc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -623,7 +623,7 @@
      * Get bubble bar top coordinate on screen when bar is resting
      */
     public int getRestingTopPositionOnScreen() {
-        int displayHeight = DisplayController.INSTANCE.get(getContext()).getInfo().currentSize.y;
+        int displayHeight = DisplayController.get(getContext()).getInfo().currentSize.y;
         int bubbleBarHeight = getBubbleBarBounds().height();
         return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY();
     }
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index 6d588d9..4ab104e 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -290,7 +290,7 @@
             insets = dp.getInsets();
         } else {
             Rect portraitInsets = dp.getInsets();
-            DisplayController displayController = DisplayController.INSTANCE.get(context);
+            DisplayController displayController = DisplayController.get(context);
             @Nullable List<WindowBounds> windowBounds =
                     displayController.getInfo().getCurrentBounds();
             Rect deviceRotationInsets = windowBounds != null
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 9b0e75c..93f31ff 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -28,6 +28,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
+@SuppressWarnings("VisibleForTests")
 public class QuickstepTestInformationHandler extends TestInformationHandler {
 
     protected final Context mContext;
@@ -229,7 +230,7 @@
     }
 
     private void enableTransientTaskbar(boolean enable) {
-        DisplayController.INSTANCE.get(mContext).enableTransientTaskbarForTests(enable);
+        DisplayController.get(mContext).enableTransientTaskbarForTests(enable);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 090ccdc..3742d5a 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -73,6 +73,7 @@
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -141,13 +142,13 @@
     RecentsAnimationDeviceState(
             @ApplicationContext Context context,
             GestureExclusionManager exclusionManager,
-            DisplayController displayController,
+            PerDisplayObjectProvider displayControllerProvider,
             ContextualSearchStateManager contextualSearchStateManager,
             RotationTouchHelper rotationTouchHelper,
             SettingsCache settingsCache,
             DaggerSingletonTracker lifeCycle) {
         mContext = context;
-        mDisplayController = displayController;
+        mDisplayController = displayControllerProvider.getDisplayController(DEFAULT_DISPLAY);
         mExclusionManager = exclusionManager;
         mContextualSearchStateManager = contextualSearchStateManager;
         mRotationTouchHelper = rotationTouchHelper;
@@ -156,7 +157,7 @@
         // Register for exclusion updates
         lifeCycle.addCloseable(this::unregisterExclusionListener);
 
-        // Register for display changes changes
+        // Register for display changes
         mDisplayController.addChangeListener(this);
         onDisplayInfoChanged(context, mDisplayController.getInfo(), CHANGE_ALL);
         lifeCycle.addCloseable(() -> mDisplayController.removeChangeListener(this));
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 1d83d42..f53a8b6 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -48,11 +48,13 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
 import com.android.quickstep.recents.data.TaskVisualsChangeNotifier;
 import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.ExternalDisplaysKt;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -61,6 +63,8 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -72,8 +76,6 @@
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 /**
  * Singleton class to load and manage recents model.
  */
@@ -101,15 +103,17 @@
      public RecentsModel(@ApplicationContext Context context,
             SystemUiProxy systemUiProxy,
             TopTaskTracker topTaskTracker,
-            DisplayController displayController,
             LockedUserState lockedUserState,
+            PerDisplayObjectProvider perDisplayObjectProvider,
             Lazy<ThemeManager> themeManagerLazy,
             DaggerSingletonTracker tracker
             ) {
         // Lazily inject the ThemeManager and access themeManager once the device is
         // unlocked. See b/393248495 for details.
         this(context, new IconProvider(context), systemUiProxy, topTaskTracker,
-                displayController, lockedUserState,themeManagerLazy, tracker);
+                perDisplayObjectProvider.getDisplayController(
+                        ExternalDisplaysKt.getValidDisplayId(context)),
+                lockedUserState, themeManagerLazy, tracker);
     }
 
     @SuppressLint("VisibleForTests")
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index a614327..387adfd 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -136,11 +137,11 @@
 
     @Inject
     RotationTouchHelper(@ApplicationContext Context context,
-            DisplayController displayController,
+            PerDisplayObjectProvider displayControllerProvider,
             SystemUiProxy systemUiProxy,
             DaggerSingletonTracker lifeCycle) {
         mContext = context;
-        mDisplayController = displayController;
+        mDisplayController = displayControllerProvider.getDisplayController(DEFAULT_DISPLAY);
         mSystemUiProxy = systemUiProxy;
         mDisplayId = DEFAULT_DISPLAY;
 
diff --git a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
index d2a491d..9ebaffa 100644
--- a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
@@ -27,6 +29,7 @@
 import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 
 import javax.inject.Inject;
@@ -45,8 +48,10 @@
 
     @Inject
     public SimpleOrientationTouchTransformer(@ApplicationContext Context context,
-            DisplayController displayController,
+            PerDisplayObjectProvider displayControllerProvider,
             DaggerSingletonTracker tracker) {
+        DisplayController displayController = displayControllerProvider.getDisplayController(
+                DEFAULT_DISPLAY);
         displayController.addChangeListener(this);
         tracker.addCloseable(() -> displayController.removeChangeListener(this));
 
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
index 6a7f1af..3fbf59a 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.kt
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -153,7 +153,7 @@
     private fun createIconFactory() =
         BaseIconFactory(
             context,
-            DisplayController.INSTANCE.get(context).info.densityDpi,
+            DisplayController.get(context).info.densityDpi,
             context.resources.getDimensionPixelSize(R.dimen.task_icon_cache_default_icon_size),
         )
 
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index e47223b..7902ad5 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -230,7 +230,7 @@
 
                 // RecentsView never updates the display rotation until swipe-up so the value may
                 // be stale. Use the display value instead.
-                int displayRotation = DisplayController.INSTANCE.get(context).getInfo().rotation;
+                int displayRotation = DisplayController.get(context).getInfo().rotation;
                 tvsLocal.getOrientationState().update(displayRotation, displayRotation);
 
                 tvsLocal.fullScreenProgress.value = 0;
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ba4c65a..7a4c06b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -1112,7 +1112,7 @@
         pw.println("Input state:");
         pw.println("\tmInputMonitorCompat=" + mInputMonitorCompat);
         pw.println("\tmInputEventReceiver=" + mInputEventReceiver);
-        DisplayController.INSTANCE.get(this).dump(pw);
+        DisplayController.get(this).dump(pw);
         pw.println("TouchState:");
         RecentsViewContainer createdOverviewContainer = mOverviewComponentObserver == null ? null
                 : mOverviewComponentObserver.getContainerInterface().getCreatedContainer();
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
index ec531d8..a61c1a0 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -44,7 +44,7 @@
                 DisplayController.getNavigationMode(mContainer.asContext());
         if (sysUINavigationMode == NavigationMode.NO_BUTTON) {
             NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
-                    DisplayController.INSTANCE.get(mContainer.asContext()).getInfo());
+                    DisplayController.get(mContainer.asContext()).getInfo());
             mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mContainer.asContext(),
                     true /* disableHorizontalSwipe */, navBarPosition, this);
         } else {
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 95a3ec2..c7e77b2 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -18,18 +18,23 @@
 
 import android.content.Context
 import android.util.Log
-import android.view.Display
+import android.view.Display.INVALID_DISPLAY
 import com.android.launcher3.Flags
+import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Executors
+import com.android.launcher3.util.PerDisplayObjectProvider
 import com.android.launcher3.util.WallpaperColorHints
+import com.android.launcher3.util.window.WindowManagerProxy
 import com.android.quickstep.DisplayModel
 import com.android.quickstep.FallbackWindowInterface
 import com.android.quickstep.dagger.QuickstepBaseAppComponent
 import com.android.quickstep.fallback.window.RecentsDisplayModel.RecentsDisplayResource
+import com.android.quickstep.util.validDisplayId
 import javax.inject.Inject
 
 @LauncherAppSingleton
@@ -37,9 +42,11 @@
 @Inject
 constructor(
     @ApplicationContext context: Context,
+    private val windowManagerProxy: WindowManagerProxy,
+    private val launcherPrefs: LauncherPrefs,
     private val wallpaperColorHints: WallpaperColorHints,
     tracker: DaggerSingletonTracker,
-) : DisplayModel<RecentsDisplayResource>(context) {
+) : DisplayModel<RecentsDisplayResource>(context), PerDisplayObjectProvider {
 
     companion object {
         private const val TAG = "RecentsDisplayModel"
@@ -57,16 +64,17 @@
     }
 
     init {
-        if (enableOverviewInWindow()) {
-            displayManager.registerDisplayListener(displayListener, Executors.MAIN_EXECUTOR.handler)
-            // In the scenario where displays were added before this display listener was
-            // registered, we should store the RecentsDisplayResources for those displays
-            // directly.
-            displayManager.displays
-                .filter { getDisplayResource(it.displayId) == null }
-                .forEach { storeRecentsDisplayResource(it.displayId, it) }
-            tracker.addCloseable { destroy() }
-        }
+        // Add the display for the context with which we are initialized.
+        storeRecentsDisplayResource(context.validDisplayId)
+
+        displayManager.registerDisplayListener(displayListener, Executors.MAIN_EXECUTOR.handler)
+        // In the scenario where displays were added before this display listener was
+        // registered, we should store the RecentsDisplayResources for those displays
+        // directly.
+        displayManager.displays
+            .filter { getDisplayResource(it.displayId) == null }
+            .forEach { storeRecentsDisplayResource(it.displayId) }
+        tracker.addCloseable { destroy() }
     }
 
     override fun createDisplayResource(displayId: Int) {
@@ -74,26 +82,34 @@
         getDisplayResource(displayId)?.let {
             return
         }
+        if (displayId == INVALID_DISPLAY) {
+            Log.e(TAG, "createDisplayResource: INVALID_DISPLAY")
+            return
+        }
+        storeRecentsDisplayResource(displayId)
+    }
+
+    private fun storeRecentsDisplayResource(displayId: Int): RecentsDisplayResource {
         val display = displayManager.getDisplay(displayId)
         if (display == null) {
             if (DEBUG)
                 Log.w(
                     TAG,
-                    "createDisplayResource: could not create display for displayId=$displayId",
-                    Exception(),
+                    "storeRecentsDisplayResource: could not create display for displayId=$displayId",
                 )
-            return
         }
-        storeRecentsDisplayResource(displayId, display)
-    }
-
-    private fun storeRecentsDisplayResource(displayId: Int, display: Display) {
-        displayResourceArray[displayId] =
-            RecentsDisplayResource(
-                displayId,
-                context.createDisplayContext(display),
-                wallpaperColorHints.hints,
-            )
+        return displayResourceArray[displayId]
+            ?: RecentsDisplayResource(
+                    displayId,
+                    context,
+                    windowManagerProxy,
+                    launcherPrefs,
+                    if (enableOverviewInWindow() && display != null)
+                        context.createDisplayContext(display)
+                    else null,
+                    wallpaperColorHints.hints,
+                )
+                .also { displayResourceArray[displayId] = it }
     }
 
     fun getRecentsWindowManager(displayId: Int): RecentsWindowManager? {
@@ -104,17 +120,36 @@
         return getDisplayResource(displayId)?.fallbackWindowInterface
     }
 
+    override fun getDisplayController(displayId: Int): DisplayController {
+        if (DEBUG) Log.d(TAG, "getDisplayController $displayId")
+        return (getDisplayResource(displayId)
+                ?: storeRecentsDisplayResource(displayId).also {
+                    // We shouldn't get here because the display should already have been
+                    // initialized.
+                    Log.e(TAG, "getDisplayController no such display: $displayId")
+                })
+            .displayController
+    }
+
     data class RecentsDisplayResource(
         var displayId: Int,
-        var displayContext: Context,
+        val appContext: Context,
+        val windowManagerProxy: WindowManagerProxy,
+        val launcherPrefs: LauncherPrefs,
+        var displayContext: Context?, // null when OverviewInWindow not enabled
         val wallpaperColorHints: Int,
     ) : DisplayResource() {
-        val recentsWindowManager = RecentsWindowManager(displayContext, wallpaperColorHints)
-        val fallbackWindowInterface: FallbackWindowInterface =
-            FallbackWindowInterface(recentsWindowManager)
+        val recentsWindowManager =
+            displayContext?.let { RecentsWindowManager(it, wallpaperColorHints) }
+        val fallbackWindowInterface: FallbackWindowInterface? =
+            recentsWindowManager?.let { FallbackWindowInterface(recentsWindowManager) }
+        val lifecycle = DaggerSingletonTracker()
+        val displayController =
+            DisplayController(appContext, windowManagerProxy, launcherPrefs, lifecycle, displayId)
 
         override fun cleanup() {
-            recentsWindowManager.destroy()
+            recentsWindowManager?.destroy()
+            lifecycle.close()
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 503b900..6be1098 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -122,7 +122,7 @@
         mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(mContext);
 
         // Do not use DeviceProfile as the user data might be locked
-        mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize;
+        mDisplaySize = DisplayController.get(context).getInfo().currentSize;
 
         // Init states
         mStateCallback = new MultiStateCallback(STATE_NAMES);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index afe988d..9a572b9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -73,7 +73,7 @@
             InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState,
             NavHandle navHandle, GestureState gestureState) {
         super(delegate, inputMonitor);
-        mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
+        mScreenWidth = DisplayController.get(context).getInfo().currentSize.x;
         mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress();
         ContextualSearchStateManager contextualSearchStateManager =
                 ContextualSearchStateManager.INSTANCE.get(context);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
index 83b556d..fa9744d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -69,7 +69,7 @@
         mDragDistThreshold = context.getResources().getDimensionPixelSize(
                 R.dimen.gestures_onehanded_drag_threshold);
         mSquaredSlop = mDeviceState.getSquaredTouchSlop();
-        mDisplaySize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
+        mDisplaySize = DisplayController.get(mContext).getInfo().currentSize;
         mNavBarSize = ResourceUtils.getNavbarSize(NAVBAR_BOTTOM_GESTURE_SIZE,
                 mContext.getResources());
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
index c91bebe..ca62657 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
@@ -99,7 +99,7 @@
         mProgress = progress;
 
         // Do not use DeviceProfile as the user data might be locked
-        mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize;
+        mDisplaySize = DisplayController.get(context).getInfo().currentSize;
 
         // Init states
         mStateCallback = new MultiStateCallback(STATE_NAMES);
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index fdbd509..bf6c3c3 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -66,7 +66,7 @@
                 /* disableHorizontalSwipe= */ true,
                 new NavBarPosition(
                         NavigationMode.NO_BUTTON,
-                        DisplayController.INSTANCE.get(mContext).getInfo()),
+                        DisplayController.get(mContext).getInfo()),
                 /* onSwipeUp= */ this);
         mMotionPauseDetector = new MotionPauseDetector(context);
 
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 0cc349d..c27fb60 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.logging;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.launcher3.LauncherPrefs.getDevicePrefs;
 import static com.android.launcher3.LauncherPrefs.getPrefs;
 import static com.android.launcher3.graphics.ThemeManager.KEY_THEMED_ICONS;
@@ -54,6 +56,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 
@@ -94,9 +97,10 @@
     @Inject
     SettingsChangeLogger(@ApplicationContext Context context,
             DaggerSingletonTracker tracker,
-            DisplayController displayController,
+            PerDisplayObjectProvider displayControllerProvider,
             SettingsCache settingsCache) {
-        this(context, StatsLogManager.newInstance(context), tracker, displayController,
+        this(context, StatsLogManager.newInstance(context), tracker,
+                displayControllerProvider.getDisplayController(DEFAULT_DISPLAY),
                 settingsCache);
     }
 
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 594c99a..fb521ab 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -247,7 +247,7 @@
         StatsCompatLogger(Context context, ActivityContext activityContext) {
             mContext = context;
             mActivityContext = Optional.ofNullable(activityContext);
-            mDisplayRotation = DisplayController.INSTANCE.get(mContext).getInfo().rotation;
+            mDisplayRotation = DisplayController.get(mContext).getInfo().rotation;
         }
 
         @Override
diff --git a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
index 455b312..9df1fe8 100644
--- a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
+++ b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.util
 
+import android.content.Context
+import android.util.Log
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.Display.INVALID_DISPLAY
 import com.android.systemui.shared.recents.model.Task
@@ -24,17 +26,30 @@
 val Int.isExternalDisplay
     get() = this != DEFAULT_DISPLAY
 
-/** Returns displayId of this [Task], default to [DEFAULT_DISPLAY] */
-val Task?.displayId
+val Int?.validDisplayId: Int
     get() =
-        this?.key?.displayId.let { displayId ->
-            when (displayId) {
-                null -> DEFAULT_DISPLAY
-                INVALID_DISPLAY -> DEFAULT_DISPLAY
-                else -> displayId
-            }
+        when (this) {
+            null -> DEFAULT_DISPLAY
+            INVALID_DISPLAY -> DEFAULT_DISPLAY
+            else -> this
         }
 
+/** Returns displayId of this [Task], default to [DEFAULT_DISPLAY] */
+val Task?.validDisplayId
+    get() = this?.key?.displayId?.validDisplayId ?: DEFAULT_DISPLAY
+
 /** Returns if this task belongs tto [DEFAULT_DISPLAY] */
 val Task?.isExternalDisplay
-    get() = displayId.isExternalDisplay
+    get(): Boolean = this.validDisplayId.isExternalDisplay ?: false
+
+val Context?.validDisplayId
+    get() =
+        try {
+            this?.display?.displayId?.validDisplayId ?: DEFAULT_DISPLAY
+        } catch (ignored: UnsupportedOperationException) {
+            Log.w(
+                "ExternalDisplays",
+                "Tried to get a Display from a Context not associated with one",
+            )
+            DEFAULT_DISPLAY
+        }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index a5be89a..8fb16d7 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -573,7 +573,7 @@
      */
     public DeviceProfile getLauncherDeviceProfile() {
         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
-        Point currentSize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
+        Point currentSize = DisplayController.get(mContext).getInfo().currentSize;
 
         int width, height;
         if ((mRecentsActivityRotation == ROTATION_90 || mRecentsActivityRotation == ROTATION_270)) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 609262f..e86d657 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -91,8 +91,8 @@
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.util.TaskRemovedDuringLaunchListener
-import com.android.quickstep.util.displayId
 import com.android.quickstep.util.isExternalDisplay
+import com.android.quickstep.util.validDisplayId
 import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
@@ -144,7 +144,7 @@
         get() = this === recentsView?.runningTaskView
 
     val displayId: Int
-        get() = taskContainers.firstOrNull()?.task.displayId
+        get() = taskContainers.firstOrNull()?.task.validDisplayId
 
     val isExternalDisplay: Boolean
         get() = displayId.isExternalDisplay
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 bfd53ef..2bdc22a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -31,7 +31,6 @@
 import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
-import com.android.launcher3.taskbar.rules.DisplayControllerModule
 import com.android.launcher3.taskbar.rules.MockedRecentsModelHelper
 import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule
 import com.android.launcher3.taskbar.rules.SandboxParams
@@ -423,9 +422,7 @@
 
 /** TaskbarOverflowComponent used to bind the RecentsModel. */
 @LauncherAppSingleton
-@Component(
-    modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class]
-)
+@Component(modules = [AllModulesForTest::class, FakePrefsModule::class])
 interface TaskbarOverflowComponent : TaskbarSandboxComponent {
 
     @Component.Builder
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index 3cf912c..482d735 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -61,7 +61,7 @@
                 val mode = taskbarMode.mode
 
                 getInstrumentation().runOnMainSync {
-                    DisplayController.INSTANCE[context].let {
+                    DisplayController.get(context).let {
                         if (it is DisplayControllerSpy) {
                             it.infoModifier = { info ->
                                 spy(info) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 95e8980..62d6fdc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -26,11 +26,12 @@
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
-import com.android.launcher3.util.AllModulesForTest
+import com.android.launcher3.util.AllModulesMinusPerDisplayObjectProvider
 import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.FakePrefsModule
 import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
+import com.android.launcher3.util.PerDisplayObjectProvider
 import com.android.launcher3.util.SandboxApplication
 import com.android.launcher3.util.SettingsCache
 import com.android.launcher3.util.SettingsCacheSandbox
@@ -131,14 +132,31 @@
     override fun getInfo(): Info = infoModifier?.invoke(super.getInfo()) ?: super.getInfo()
 }
 
+@LauncherAppSingleton
+class PerDisplayObjectProviderImpl
+@Inject
+constructor(private val displayController: DisplayControllerSpy) : PerDisplayObjectProvider {
+    override fun getDisplayController(displayId: Int): DisplayController {
+        return displayController
+    }
+}
+
 @Module
-abstract class DisplayControllerModule {
-    @Binds abstract fun bindDisplayController(controller: DisplayControllerSpy): DisplayController
+abstract class PerDisplayObjectProviderModule {
+    @Binds
+    abstract fun bindPerDisplayObjectProvider(
+        impl: PerDisplayObjectProviderImpl
+    ): PerDisplayObjectProvider
 }
 
 @LauncherAppSingleton
 @Component(
-    modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class]
+    modules =
+        [
+            AllModulesMinusPerDisplayObjectProvider::class,
+            FakePrefsModule::class,
+            PerDisplayObjectProviderModule::class,
+        ]
 )
 interface TaskbarSandboxComponent : LauncherAppComponent {
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index b652ee8..f5de2b0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -60,7 +60,7 @@
             RecentsAnimationDeviceState(
                 context,
                 exclusionManager,
-                component.displayController,
+                component.perDisplayObjectProvider,
                 component.contextualSearchStateManager,
                 component.rotationTouchHelper,
                 component.settingsCache,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
index c1be1ce..e875e4d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
@@ -25,15 +25,12 @@
 import com.android.launcher3.util.LauncherMultivalentJUnit;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsState;
-import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.views.RecentsViewContainer;
 
-import dagger.BindsInstance;
 import dagger.Component;
 
-import org.junit.Before;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 
@@ -46,16 +43,9 @@
         RecentsWindowSwipeHandler,
         FallbackWindowInterface> {
 
-    @Mock private RecentsDisplayModel mRecentsDisplayModel;
     @Mock private FallbackRecentsView<RecentsWindowManager> mRecentsView;
     @Mock private RecentsWindowManager mRecentsWindowManager;
 
-    @Before
-    public void setRecentsDisplayModel() {
-        mContext.initDaggerComponent(DaggerRecentsWindowSwipeHandlerTestCase_TestComponent.builder()
-                .bindRecentsDisplayModel(mRecentsDisplayModel));
-    }
-
     @NonNull
     @Override
     protected RecentsWindowSwipeHandler createSwipeHandler(long touchTimeMs,
@@ -87,7 +77,6 @@
     interface TestComponent extends LauncherAppComponent {
         @Component.Builder
         interface Builder extends LauncherAppComponent.Builder {
-            @BindsInstance Builder bindRecentsDisplayModel(RecentsDisplayModel model);
             @Override LauncherAppComponent build();
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 7776351..71d377b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -433,7 +433,7 @@
                 DaggerNavHandleLongPressInputConsumerTest_TopTaskTrackerComponent
                         .builder()
                         .bindTopTaskTracker(mTopTaskTracker));
-        mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x;
+        mScreenWidth = DisplayController.get(mContext).getInfo().currentSize.x;
         mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
                 mDeviceState, mNavHandle, mGestureState);
         mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler);
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index 14570b5..2671b80 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -64,7 +64,7 @@
 
     @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
     @Mock private lateinit var mTracker: DaggerSingletonTracker
-    private var displayController: DisplayController = DisplayController.INSTANCE.get(mContext)
+    private var displayController: DisplayController = DisplayController.get(mContext)
     private var settingsCache: SettingsCache = SettingsCache.INSTANCE.get(mContext)
 
     @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
index be76f9e..ab48065 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.util;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -37,11 +38,12 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherAppSingleton;
-import com.android.launcher3.util.AllModulesMinusWMProxy;
+import com.android.launcher3.util.AllModulesMinusWMProxyAndPerDisplayObjectProvider;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.LauncherModelHelper;
 import com.android.launcher3.util.NavigationMode;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.launcher3.util.RotationUtils;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
@@ -166,10 +168,11 @@
             LauncherModelHelper helper = new LauncherModelHelper();
             try {
                 DisplayController mockController = mock(DisplayController.class);
+                PerDisplayObjectProvider objectProvider = mock(PerDisplayObjectProvider.class);
 
                 helper.sandboxContext.initDaggerComponent(
                         DaggerTaskViewSimulatorTest_TaskViewSimulatorTestComponent.builder()
-                                .bindDisplayController(mockController));
+                                .bindPerDisplayObjectProvider(objectProvider));
                 int rotation = mDisplaySize.x > mDisplaySize.y
                         ? Surface.ROTATION_90 : Surface.ROTATION_0;
                 CachedDisplayInfo cdi = new CachedDisplayInfo(mDisplaySize, rotation);
@@ -201,6 +204,8 @@
                 Context configurationContext = helper.sandboxContext.createConfigurationContext(
                         configuration);
 
+                when(objectProvider.getDisplayController(anyInt())).thenReturn(
+                        mockController);
                 DisplayController.Info info = new Info(
                         configurationContext, wmProxy, perDisplayBoundsCache);
                 when(mockController.getInfo()).thenReturn(info);
@@ -281,14 +286,14 @@
     }
 
     @LauncherAppSingleton
-    @Component(modules = {AllModulesMinusWMProxy.class})
+    @Component(modules = {AllModulesMinusWMProxyAndPerDisplayObjectProvider.class})
     interface TaskViewSimulatorTestComponent extends LauncherAppComponent {
 
         @Component.Builder
         interface Builder extends LauncherAppComponent.Builder {
 
             @BindsInstance
-            Builder bindDisplayController(DisplayController controller);
+            Builder bindPerDisplayObjectProvider(PerDisplayObjectProvider provider);
 
             TaskViewSimulatorTestComponent build();
         }
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index c713c3d..06c8745 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -71,9 +71,6 @@
 
     private final LauncherInstrumentation mLauncher;
 
-    static final DisplayController DISPLAY_CONTROLLER =
-            DisplayController.INSTANCE.get(getInstrumentation().getTargetContext());
-
     public NavigationModeSwitchRule(LauncherInstrumentation launcher) {
         mLauncher = launcher;
     }
@@ -168,11 +165,13 @@
                             latch.countDown();
                         }
                     };
+            DisplayController displayController =
+                    DisplayController.get(getInstrumentation().getTargetContext());
             targetContext.getMainExecutor().execute(() ->
-                    DISPLAY_CONTROLLER.addChangeListener(listener));
+                    displayController.addChangeListener(listener));
             latch.await(60, TimeUnit.SECONDS);
             targetContext.getMainExecutor().execute(() ->
-                    DISPLAY_CONTROLLER.removeChangeListener(listener));
+                    displayController.removeChangeListener(listener));
 
             assertTrue(launcher, "Navigation mode didn't change to " + expectedMode,
                     currentSysUiNavigationMode() == expectedMode, description);
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index a0ec635..649b222 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -17,6 +17,8 @@
 
 package com.android.quickstep;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
@@ -45,6 +47,7 @@
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.launcher3.util.RotationUtils;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
@@ -300,9 +303,14 @@
     @Test
     public void testSimpleOrientationTouchTransformer() {
         final DisplayController displayController = mock(DisplayController.class);
+        final PerDisplayObjectProvider displayControllerProvider = mock(
+                PerDisplayObjectProvider.class);
+        doReturn(displayController).when(displayControllerProvider).getDisplayController(
+                DEFAULT_DISPLAY);
         doReturn(mInfo).when(displayController).getInfo();
         final SimpleOrientationTouchTransformer transformer =
-                new SimpleOrientationTouchTransformer(getApplicationContext(), displayController,
+                new SimpleOrientationTouchTransformer(getApplicationContext(),
+                        displayControllerProvider,
                         mock(DaggerSingletonTracker.class));
         final MotionEvent move1 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
         transformer.transform(move1, Surface.ROTATION_90);
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 6277e41..db4480a 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -178,6 +178,8 @@
 
     private ActionMode mCurrentActionMode;
 
+    private DisplayController mDisplayController;
+
     @Override
     public ViewCache getViewCache() {
         return mViewCache;
@@ -224,7 +226,8 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         registerBackDispatcher();
-        DisplayController.INSTANCE.get(this).addChangeListener(this);
+        mDisplayController = DisplayController.get(this);
+        mDisplayController.addChangeListener(this);
     }
 
     @Override
@@ -272,7 +275,9 @@
     protected void onDestroy() {
         super.onDestroy();
         mEventCallbacks[EVENT_DESTROYED].executeAllAndClear();
-        DisplayController.INSTANCE.get(this).removeChangeListener(this);
+        if (mDisplayController != null) {
+            mDisplayController.removeChangeListener(this);
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 900f74d..472cac4 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.launcher3.LauncherPrefs.DB_FILE;
 import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE;
 import static com.android.launcher3.LauncherPrefs.FIXED_LANDSCAPE_MODE;
@@ -256,7 +258,7 @@
         String gridName = getCurrentGridName(context);
         initGrid(context, gridName);
 
-        DisplayController dc = DisplayController.INSTANCE.get(context);
+        DisplayController dc = DisplayController.get(context, DEFAULT_DISPLAY);
         dc.setPriorityListener(
                 (displayContext, info, flags) -> {
                     if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS
@@ -315,7 +317,7 @@
         String gridName = getCurrentGridName(context);
 
         // Get the display info based on default display and interpolate it to existing display
-        Info defaultInfo = DisplayController.INSTANCE.get(context).getInfo();
+        Info defaultInfo = DisplayController.get(context, display.getDisplayId()).getInfo();
         @DeviceType int defaultDeviceType = defaultInfo.getDeviceType();
         DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
                 defaultInfo,
@@ -374,7 +376,7 @@
                 + ", dbFile:" + dbFile
                 + ", LauncherPrefs GRID_NAME:" + LauncherPrefs.get(context).get(GRID_NAME)
                 + ", LauncherPrefs DB_FILE:" + LauncherPrefs.get(context).get(DB_FILE));
-        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+        Info displayInfo = DisplayController.get(context).getInfo();
         List<DisplayOption> allOptions = getPredefinedDeviceProfiles(
                 context,
                 gridName,
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 2a5cd63..fba94fd 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -299,7 +299,7 @@
         @JvmField
         val ALLOW_ROTATION =
             backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
-                RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
+                RotationHelper.getAllowRotationDefaultValue(DisplayController.get(it).info)
             }
 
         @JvmField
diff --git a/src/com/android/launcher3/dagger/LauncherAppModule.java b/src/com/android/launcher3/dagger/LauncherAppModule.java
index c58a414..ece0ff0 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,
+        PerDisplayObjectProviderModule.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 7bd7c3e..fe23093 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -26,11 +26,11 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.DaggerSingletonTracker;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.PerDisplayObjectProvider;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.SettingsCache;
@@ -69,7 +69,7 @@
     LauncherPrefs getLauncherPrefs();
     ThemeManager getThemeManager();
     UserCache getUserCache();
-    DisplayController getDisplayController();
+    PerDisplayObjectProvider getPerDisplayObjectProvider();
     WallpaperColorHints getWallpaperColorHints();
     LockedUserState getLockedUserState();
 
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 911064c..4c39aa0 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -284,7 +284,7 @@
      * Returns the insets of the screen closest to the display given by the context
      */
     private Rect getInsets(Context context) {
-        DisplayController.Info info = DisplayController.INSTANCE.get(context).getInfo();
+        DisplayController.Info info = DisplayController.get(context).getInfo();
         float maxDiff = Float.MAX_VALUE;
         Display display = context.getDisplay();
         Rect insets = new Rect();
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 6008287..1771627 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -292,7 +292,7 @@
          * will remove that preference from the list.
          */
         protected boolean initPreference(Preference preference) {
-            DisplayController.Info info = DisplayController.INSTANCE.get(getContext()).getInfo();
+            DisplayController.Info info = DisplayController.get(getContext()).getInfo();
             switch (preference.getKey()) {
                 case NOTIFICATION_DOTS_PREFERENCE_KEY:
                     return BuildConfig.NOTIFICATION_DOTS_ENABLED;
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 9376518..f6017c1 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -189,7 +189,7 @@
     public void initialize() {
         if (mInitialized) return;
         mInitialized = true;
-        DisplayController displayController = DisplayController.INSTANCE.get(mActivity);
+        DisplayController displayController = DisplayController.get(mActivity);
         DisplayController.Info info = displayController.getInfo();
         setIgnoreAutoRotateSettings(info.isTablet(info.realBounds));
         displayController.addChangeListener(this);
@@ -201,7 +201,7 @@
         if (mDestroyed) return;
         mDestroyed = true;
         mActivity.removeOnDeviceProfileChangeListener(this);
-        DisplayController.INSTANCE.get(mActivity).removeChangeListener(this);
+        DisplayController.get(mActivity).removeChangeListener(this);
         LauncherPrefs.get(mActivity).removeListener(this, ALLOW_ROTATION);
     }
 
diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java
index b7a88db..1de32e0 100644
--- a/src/com/android/launcher3/util/DaggerSingletonTracker.java
+++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java
@@ -38,7 +38,7 @@
     private boolean mClosed = false;
 
     @Inject
-    DaggerSingletonTracker() {
+    public DaggerSingletonTracker() {
     }
 
     /**
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index ee1af81..5ad22bf 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.util;
 
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
@@ -55,7 +56,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.dagger.LauncherAppComponent;
-import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -71,13 +71,10 @@
 import java.util.StringJoiner;
 import java.util.concurrent.CopyOnWriteArrayList;
 
-import javax.inject.Inject;
-
 /**
  * Utility class to cache properties of default display to avoid a system RPC on every call.
  */
 @SuppressLint("NewApi")
-@LauncherAppSingleton
 public class DisplayController implements ComponentCallbacks,
         DesktopVisibilityListener {
 
@@ -89,8 +86,8 @@
     // TODO(b/254119092) remove all logs with this tag
     public static final String TASKBAR_NOT_DESTROYED_TAG = "b/254119092";
 
-    public static final DaggerSingletonObject<DisplayController> INSTANCE =
-            new DaggerSingletonObject<>(LauncherAppComponent::getDisplayController);
+    public static final DaggerSingletonObject<PerDisplayObjectProvider> PROVIDER =
+            new DaggerSingletonObject<>(LauncherAppComponent::getPerDisplayObjectProvider);
 
     public static final int CHANGE_ACTIVE_SCREEN = 1 << 0;
     public static final int CHANGE_ROTATION = 1 << 1;
@@ -125,14 +122,54 @@
 
     private Info mInfo;
     private boolean mDestroyed = false;
+    private final int mDisplayId;
 
-    @Inject
-    protected DisplayController(@ApplicationContext Context context,
+    /**
+     * Get a DisplayController associated with the given Context.
+     * @param context the context (must return a valid display id)
+     * @return the DisplayController instance associated with the display id of the context
+     */
+    public static DisplayController get(Context context) {
+        int displayId = DEFAULT_DISPLAY;
+        if (context != null) {
+            try {
+                displayId = context.getDisplay().getDisplayId();
+            } catch (UnsupportedOperationException ignored) {
+                Log.w(TAG, "DisplayController access from non-display context");
+            }
+        }
+        if (displayId == INVALID_DISPLAY) {
+            displayId = DEFAULT_DISPLAY;
+        }
+        return PROVIDER.get(context).getDisplayController(displayId);
+    }
+
+    /**
+     * Get a DisplayController associated with the given display id.
+     * @param context a context
+     * @param displayId a display id
+     * @return the DisplayController instance associated with the given display id
+     */
+    public static DisplayController get(Context context, int displayId) {
+        return PROVIDER.get(context).getDisplayController(displayId);
+    }
+
+    @VisibleForTesting
+    public DisplayController(@ApplicationContext Context context,
             WindowManagerProxy wmProxy,
             LauncherPrefs prefs,
             DaggerSingletonTracker lifecycle) {
+        this(context, wmProxy, prefs, lifecycle, DEFAULT_DISPLAY);
+    }
+
+    public DisplayController(@ApplicationContext Context context,
+            WindowManagerProxy wmProxy,
+            LauncherPrefs prefs,
+            DaggerSingletonTracker lifecycle,
+            int displayId) {
         mContext = context;
         mWMProxy = wmProxy;
+        mDisplayId = displayId;
 
         if (enableTaskbarPinning()) {
             LauncherPrefChangeListener prefListener = key -> {
@@ -150,11 +187,17 @@
             prefs.addListener(prefListener, TASKBAR_PINNING);
             prefs.addListener(prefListener, TASKBAR_PINNING_IN_DESKTOP_MODE);
             lifecycle.addCloseable(() -> prefs.removeListener(
-                        prefListener, TASKBAR_PINNING, TASKBAR_PINNING_IN_DESKTOP_MODE));
+                    prefListener, TASKBAR_PINNING, TASKBAR_PINNING_IN_DESKTOP_MODE));
         }
 
         Display display = context.getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
+                .getDisplay(displayId);
+        if (display == null) {
+            // Race when a display is rapidly added then removed.
+            mWindowContext = null;
+            mInfo = null;
+            return;
+        }
         mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
         mWindowContext.registerComponentCallbacks(this);
 
@@ -174,20 +217,25 @@
         });
     }
 
-    /**
-     * Returns the current navigation mode
-     */
-    public static NavigationMode getNavigationMode(Context context) {
-        return INSTANCE.get(context).getInfo().getNavigationMode();
+    public int getDisplayId() {
+        return mDisplayId;
     }
 
     /**
-     * Returns whether taskbar is transient or persistent.
+     * Returns the current navigation mode for the display associated with the given Context.
+     */
+    public static NavigationMode getNavigationMode(Context context) {
+        return get(context).getInfo().getNavigationMode();
+    }
+
+    /**
+     * Returns whether taskbar is transient or persistent  for the display associated with the given
+     * Context.
      *
      * @return {@code true} if transient, {@code false} if persistent.
      */
     public static boolean isTransientTaskbar(Context context) {
-        return INSTANCE.get(context).getInfo().isTransientTaskbar();
+        return get(context).getInfo().isTransientTaskbar();
     }
 
     /**
@@ -207,25 +255,27 @@
     }
 
     /**
-     * Returns whether the taskbar is pinned in gesture navigation mode.
+     * Returns whether the taskbar is pinned in gesture navigation mode for the display associated
+     * with the given Context.
      */
     public static boolean isPinnedTaskbar(Context context) {
-        return INSTANCE.get(context).getInfo().isPinnedTaskbar();
+        return get(context).getInfo().isPinnedTaskbar();
     }
 
     /**
-     * Returns whether the taskbar is forced to be pinned when home is visible.
+     * Returns whether the taskbar is forced to be pinned when home is visible for the display
+     * associated with the given Context.
      */
     public static boolean showLockedTaskbarOnHome(Context context) {
-        return INSTANCE.get(context).getInfo().showLockedTaskbarOnHome();
+        return get(context).getInfo().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.
+     * on the display associated with the given Context because the display is a freeform display.
      */
     public static boolean showDesktopTaskbarForFreeformDisplay(Context context) {
-        return INSTANCE.get(context).getInfo().showDesktopTaskbarForFreeformDisplay();
+        return get(context).getInfo().showDesktopTaskbarForFreeformDisplay();
     }
 
     @Override
@@ -261,6 +311,7 @@
     @Override
     public final void onConfigurationChanged(Configuration config) {
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config);
+        if (mWindowContext == null || mInfo == null) return;
         if (config.densityDpi != mInfo.densityDpi
                 || config.fontScale != mInfo.fontScale
                 || !mInfo.mScreenSizeDp.equals(
@@ -295,6 +346,7 @@
 
     @AnyThread
     public void notifyConfigChange() {
+        if (mWindowContext == null || mInfo == null) return;
         Info oldInfo = mInfo;
 
         Context displayInfoContext = mWindowContext;
@@ -348,6 +400,7 @@
     }
 
     private void notifyChange(Context context, int flags) {
+        if (mInfo == null) return;
         if (mPriorityListener != null) {
             mPriorityListener.onDisplayInfoChanged(context, mInfo, flags);
         }
@@ -582,6 +635,7 @@
      * Dumps the current state information
      */
     public void dump(PrintWriter pw) {
+        if (mInfo == null) return;
         Info info = mInfo;
         pw.println("DisplayController.Info:");
         pw.println("  normalizedDisplayInfo=" + info.normalizedDisplayInfo);
diff --git a/src/com/android/launcher3/util/PerDisplayObjectProvider.java b/src/com/android/launcher3/util/PerDisplayObjectProvider.java
new file mode 100644
index 0000000..8cb4e20
--- /dev/null
+++ b/src/com/android/launcher3/util/PerDisplayObjectProvider.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util;
+
+/**
+ * Interface for providers of objects for which there is one per display. The lifecycle of the
+ * object is for the time the display is connected or is that of the app, whichever is shorter.
+ */
+public interface PerDisplayObjectProvider {
+    /**
+     * Get the DisplayController the given display id.
+     *
+     * @param displayId The display id
+     * @return Returns the display controller if the display id is valid and otherwise throws an
+     * IllegalArgumentException.
+     */
+    DisplayController getDisplayController(int displayId);
+}
diff --git a/src_no_quickstep/com/android/launcher3/dagger/Modules.kt b/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
index c3bf7c5..8872b79 100644
--- a/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
+++ b/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
@@ -16,7 +16,16 @@
 
 package com.android.launcher3.dagger
 
+import android.content.Context
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.PerDisplayObjectProvider
+import com.android.launcher3.util.window.WindowManagerProxy
+import dagger.Binds
 import dagger.Module
+import javax.inject.Inject
 
 private object Modules {}
 
@@ -28,5 +37,30 @@
 
 @Module object StaticObjectModule {}
 
+@LauncherAppSingleton
+class DefaultPerDisplayObjectProvider
+@Inject
+constructor(
+    @ApplicationContext context: Context,
+    wmProxy: WindowManagerProxy,
+    launcherPrefs: LauncherPrefs,
+    lifecycleTracker: DaggerSingletonTracker,
+) : PerDisplayObjectProvider {
+    val displayController =
+        DisplayController(context, wmProxy, launcherPrefs, lifecycleTracker, DEFAULT_DISPLAY)
+
+    override fun getDisplayController(displayId: Int): DisplayController {
+        return displayController
+    }
+}
+
+@Module
+abstract class PerDisplayObjectProviderModule {
+    @Binds
+    abstract fun bindPerDisplayObjectProvider(
+        impl: DefaultPerDisplayObjectProvider
+    ): PerDisplayObjectProvider
+}
+
 // Module containing bindings for the final derivative app
 @Module abstract class AppModule {}
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 9c64ec9..56a0543 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -25,16 +25,18 @@
 import android.platform.test.rule.IgnoreLimit
 import android.platform.test.rule.LimitDevicesRule
 import android.util.DisplayMetrics
+import android.view.Display.DEFAULT_DISPLAY
 import android.view.Surface
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.testing.shared.ResourceUtils
-import com.android.launcher3.util.AllModulesMinusWMProxy
+import com.android.launcher3.util.AllModulesMinusWMProxyAndPerDisplayObjectProvider
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.NavigationMode
+import com.android.launcher3.util.PerDisplayObjectProvider
 import com.android.launcher3.util.WindowBounds
 import com.android.launcher3.util.rule.TestStabilityRule
 import com.android.launcher3.util.rule.setFlags
@@ -68,6 +70,7 @@
     protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context
     protected lateinit var context: SandboxContext
     protected open val runningContext: Context = getApplicationContext()
+    private val displayControllerProvider: PerDisplayObjectProvider = mock()
     private val displayController: DisplayController = mock()
     private val windowManagerProxy: WindowManagerProxy = mock()
     private val launcherPrefs: LauncherPrefs = mock()
@@ -312,7 +315,7 @@
             DaggerAbsDPTestSandboxComponent.builder()
                 .bindWMProxy(windowManagerProxy)
                 .bindLauncherPrefs(launcherPrefs)
-                .bindDisplayController(displayController)
+                .bindDisplayControllerProvider(displayControllerProvider)
         )
 
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
@@ -323,6 +326,8 @@
         whenever(launcherPrefs.get(LauncherPrefs.WORKSPACE_SIZE)).thenReturn("")
         whenever(launcherPrefs.get(LauncherPrefs.DB_FILE)).thenReturn("")
         whenever(launcherPrefs.get(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE)).thenReturn(true)
+        whenever(displayControllerProvider.getDisplayController(DEFAULT_DISPLAY))
+            .thenReturn(displayController)
         val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
@@ -365,7 +370,7 @@
 }
 
 @LauncherAppSingleton
-@Component(modules = [AllModulesMinusWMProxy::class])
+@Component(modules = [AllModulesMinusWMProxyAndPerDisplayObjectProvider::class])
 interface AbsDPTestSandboxComponent : LauncherAppComponent {
 
     @Component.Builder
@@ -374,7 +379,7 @@
 
         @BindsInstance fun bindLauncherPrefs(prefs: LauncherPrefs): Builder
 
-        @BindsInstance fun bindDisplayController(displayController: DisplayController): Builder
+        @BindsInstance fun bindDisplayControllerProvider(dc: PerDisplayObjectProvider): Builder
 
         override fun build(): AbsDPTestSandboxComponent
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt b/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
index b66a9d3..8152552 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
@@ -20,6 +20,7 @@
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.dagger.ApiWrapperModule
 import com.android.launcher3.dagger.AppModule
+import com.android.launcher3.dagger.PerDisplayObjectProviderModule
 import com.android.launcher3.dagger.StaticObjectModule
 import com.android.launcher3.dagger.WindowManagerProxyModule
 import dagger.Binds
@@ -39,15 +40,47 @@
             ApiWrapperModule::class,
             WindowManagerProxyModule::class,
             StaticObjectModule::class,
+            PerDisplayObjectProviderModule::class,
             AppModule::class,
         ]
 )
 class AllModulesForTest
 
 /** All modules except the WMProxy */
-@Module(includes = [ApiWrapperModule::class, StaticObjectModule::class, AppModule::class])
+@Module(
+    includes =
+        [
+            ApiWrapperModule::class,
+            StaticObjectModule::class,
+            PerDisplayObjectProviderModule::class,
+            AppModule::class,
+        ]
+)
 class AllModulesMinusWMProxy
 
 /** All modules except the ApiWrapper */
-@Module(includes = [WindowManagerProxyModule::class, StaticObjectModule::class, AppModule::class])
+@Module(
+    includes =
+        [
+            WindowManagerProxyModule::class,
+            StaticObjectModule::class,
+            PerDisplayObjectProviderModule::class,
+            AppModule::class,
+        ]
+)
 class AllModulesMinusApiWrapper
+
+@Module(includes = [ApiWrapperModule::class, StaticObjectModule::class, AppModule::class])
+class AllModulesMinusWMProxyAndPerDisplayObjectProvider
+
+/** All modules except PerDisplayObjectProvider */
+@Module(
+    includes =
+        [
+            ApiWrapperModule::class,
+            WindowManagerProxyModule::class,
+            StaticObjectModule::class,
+            AppModule::class,
+        ]
+)
+class AllModulesMinusPerDisplayObjectProvider
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 588a668..acf5908 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -139,7 +139,7 @@
         whenever(context.resources).thenReturn(resources)
 
         // Initialize DisplayController
-        displayController = DisplayController.INSTANCE.get(context)
+        displayController = DisplayController.get(context)
         displayController.addChangeListener(displayInfoChangeListener)
     }