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/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);
+}