DisplayController refactoring for multiple displays

Test: locally tested on Tangor
Flag: com.android.launcher3.enable_overview_on_connected_displays
Bug: 392858637
Change-Id: I18c196c977b3731aa09d2cc93ab6341b8f1636c6
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index a614327..ae6cfa0 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -142,15 +142,17 @@
         mContext = context;
         mDisplayController = displayController;
         mSystemUiProxy = systemUiProxy;
+        // TODO (b/398195845): this needs updating so non-default displays do not rotate with the
+        //  default display.
         mDisplayId = DEFAULT_DISPLAY;
 
         Resources resources = mContext.getResources();
         mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
                 () -> QuickStepContract.getWindowCornerRadius(mContext));
 
-        // Register for navigation mode changes
-        mDisplayController.addChangeListener(this);
-        DisplayController.Info info = mDisplayController.getInfo();
+        // Register for navigation mode and rotation changes
+        mDisplayController.addChangeListenerForDisplay(this, mDisplayId);
+        DisplayController.Info info = mDisplayController.getInfoForDisplay(mDisplayId);
         onDisplayInfoChanged(context, info, CHANGE_ALL);
 
         mOrientationListener = new OrientationEventListener(mContext) {
@@ -174,7 +176,7 @@
         };
 
         lifeCycle.addCloseable(() -> {
-            mDisplayController.removeChangeListener(this);
+            mDisplayController.removeChangeListenerForDisplay(this, mDisplayId);
             mOrientationListener.disable();
             TaskStackChangeListeners.getInstance()
                     .unregisterTaskStackListener(mFrozenTaskListener);
@@ -201,7 +203,8 @@
             return;
         }
 
-        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo(),
+        mOrientationTouchTransformer.createOrAddTouchRegion(
+                mDisplayController.getInfoForDisplay(mDisplayId),
                 "RTH.updateGestureTouchRegions");
     }
 
@@ -258,7 +261,8 @@
 
         if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
             NavigationMode newMode = info.getNavigationMode();
-            mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
+            mOrientationTouchTransformer.setNavigationMode(newMode,
+                    mDisplayController.getInfoForDisplay(mDisplayId),
                     mContext.getResources());
 
             TaskStackChangeListeners.getInstance()
@@ -280,7 +284,8 @@
      */
     void setGesturalHeight(int newGesturalHeight) {
         mOrientationTouchTransformer.setGesturalHeight(
-                newGesturalHeight, mDisplayController.getInfo(), mContext.getResources());
+                newGesturalHeight, mDisplayController.getInfoForDisplay(mDisplayId),
+                mContext.getResources());
     }
 
     /**
@@ -296,7 +301,8 @@
     }
 
     private void enableMultipleRegions(boolean enable) {
-        mOrientationTouchTransformer.enableMultipleRegions(enable, mDisplayController.getInfo());
+        mOrientationTouchTransformer.enableMultipleRegions(enable,
+                mDisplayController.getInfoForDisplay(mDisplayId));
         notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
         if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
             // Clear any previous state from sensor manager
@@ -359,7 +365,8 @@
      * notifies system UI of the primary rotation the user is interacting with
      */
     private void toggleSecondaryNavBarsForRotation() {
-        mOrientationTouchTransformer.setSingleActiveRegion(mDisplayController.getInfo());
+        mOrientationTouchTransformer.setSingleActiveRegion(
+                mDisplayController.getInfoForDisplay(mDisplayId));
         notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
     }
 
diff --git a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
index d2a491d..de7fb89 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;
@@ -42,15 +44,20 @@
     private OrientationRectF mOrientationRectF;
     private OrientationRectF mTouchingOrientationRectF;
     private int mViewRotation;
+    private final int mDisplayId;
 
     @Inject
     public SimpleOrientationTouchTransformer(@ApplicationContext Context context,
             DisplayController displayController,
             DaggerSingletonTracker tracker) {
-        displayController.addChangeListener(this);
-        tracker.addCloseable(() -> displayController.removeChangeListener(this));
+        // TODO (b/398195845): make sure non-default displays don't get affected by default display
+        // changes.
+        mDisplayId = DEFAULT_DISPLAY;
+        displayController.addChangeListenerForDisplay(this, mDisplayId);
+        tracker.addCloseable(
+                () -> displayController.removeChangeListenerForDisplay(this, mDisplayId));
 
-        onDisplayInfoChanged(context, displayController.getInfo(), CHANGE_ALL);
+        onDisplayInfoChanged(context, displayController.getInfoForDisplay(mDisplayId), CHANGE_ALL);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
index 6a7f1af..f0b9b7b 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.kt
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -72,6 +72,8 @@
     var taskVisualsChangeListener: TaskVisualsChangeListener? = null
 
     init {
+        // TODO (b/397205964): this will need to be updated when we support caches for different
+        //  displays.
         displayController.addChangeListener(this)
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index a0ec635..154d86d 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -300,7 +300,7 @@
     @Test
     public void testSimpleOrientationTouchTransformer() {
         final DisplayController displayController = mock(DisplayController.class);
-        doReturn(mInfo).when(displayController).getInfo();
+        doReturn(mInfo).when(displayController).getInfoForDisplay(anyInt());
         final SimpleOrientationTouchTransformer transformer =
                 new SimpleOrientationTouchTransformer(getApplicationContext(), displayController,
                         mock(DaggerSingletonTracker.class));
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index ceece4d..52f8887 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -18,6 +18,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
+import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET;
@@ -42,9 +43,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.Display;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
@@ -63,8 +66,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -78,8 +83,7 @@
  */
 @SuppressLint("NewApi")
 @LauncherAppSingleton
-public class DisplayController implements ComponentCallbacks,
-        DesktopVisibilityListener {
+public class DisplayController implements DesktopVisibilityListener {
 
     private static final String TAG = "DisplayController";
     private static final boolean DEBUG = false;
@@ -110,19 +114,18 @@
 
     private final WindowManagerProxy mWMProxy;
 
-    // Null for SDK < S
-    private final Context mWindowContext;
+    private final @ApplicationContext Context mAppContext;
 
     // The callback in this listener updates DeviceProfile, which other listeners might depend on
     private DisplayInfoChangeListener mPriorityListener;
-    private final CopyOnWriteArrayList<DisplayInfoChangeListener> mListeners =
-            new CopyOnWriteArrayList<>();
+
+    private final SparseArray<PerDisplayInfo> mPerDisplayInfo =
+            new SparseArray<>();
 
     // We will register broadcast receiver on main thread to ensure not missing changes on
     // TARGET_OVERLAY_PACKAGE and ACTION_OVERLAY_CHANGED.
     private final SimpleBroadcastReceiver mReceiver;
 
-    private Info mInfo;
     private boolean mDestroyed = false;
 
     @Inject
@@ -130,18 +133,20 @@
             WindowManagerProxy wmProxy,
             LauncherPrefs prefs,
             DaggerSingletonTracker lifecycle) {
+        mAppContext = context;
         mWMProxy = wmProxy;
 
         if (enableTaskbarPinning()) {
             LauncherPrefChangeListener prefListener = key -> {
+                Info info = getInfo();
                 boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key)
-                        && mInfo.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
+                        && info.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
                 boolean isTaskbarPinningDesktopModeChanged =
                         TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key)
-                                && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get(
+                                && info.mIsTaskbarPinnedInDesktopMode != prefs.get(
                                 TASKBAR_PINNING_IN_DESKTOP_MODE);
                 if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) {
-                    notifyConfigChange();
+                    notifyConfigChange(DEFAULT_DISPLAY);
                 }
             };
 
@@ -151,23 +156,49 @@
                         prefListener, TASKBAR_PINNING, TASKBAR_PINNING_IN_DESKTOP_MODE));
         }
 
-        Display display = context.getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-        mWindowContext = context.createWindowContext(display, TYPE_APPLICATION, null);
-        mWindowContext.registerComponentCallbacks(this);
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        Display defaultDisplay = displayManager.getDisplay(DEFAULT_DISPLAY);
+        PerDisplayInfo defaultPerDisplayInfo = getOrCreatePerDisplayInfo(defaultDisplay);
 
         // Initialize navigation mode change listener
         mReceiver = new SimpleBroadcastReceiver(context, MAIN_EXECUTOR, this::onIntent);
         mReceiver.registerPkgActions(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
 
-        mInfo = new Info(mWindowContext, wmProxy,
-                wmProxy.estimateInternalDisplayBounds(mWindowContext));
         wmProxy.registerDesktopVisibilityListener(this);
-        FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
+        FileLog.i(TAG, "(CTOR) perDisplayBounds: "
+                + defaultPerDisplayInfo.mInfo.mPerDisplayBounds);
+
+        if (enableOverviewOnConnectedDisplays()) {
+            final DisplayManager.DisplayListener displayListener =
+                    new DisplayManager.DisplayListener() {
+                        @Override
+                        public void onDisplayAdded(int displayId) {
+                            getOrCreatePerDisplayInfo(displayManager.getDisplay(displayId));
+                        }
+
+                        @Override
+                        public void onDisplayChanged(int displayId) {
+                        }
+
+                        @Override
+                        public void onDisplayRemoved(int displayId) {
+                            removePerDisplayInfo(displayId);
+                        }
+                    };
+            displayManager.registerDisplayListener(displayListener, MAIN_EXECUTOR.getHandler());
+            lifecycle.addCloseable(() -> {
+                displayManager.unregisterDisplayListener(displayListener);
+            });
+            // Add any PerDisplayInfos for already-connected displays.
+            Arrays.stream(displayManager.getDisplays())
+                    .forEach((it) ->
+                            getOrCreatePerDisplayInfo(
+                                    displayManager.getDisplay(it.getDisplayId())));
+        }
 
         lifecycle.addCloseable(() -> {
             mDestroyed = true;
-            mWindowContext.unregisterComponentCallbacks(this);
+            defaultPerDisplayInfo.cleanup();
             mReceiver.unregisterReceiverSafely();
             wmProxy.unregisterDesktopVisibilityListener(this);
         });
@@ -236,9 +267,7 @@
 
     @Override
     public void onIsInDesktopModeChanged(int displayId, boolean isInDesktopModeAndNotInOverview) {
-        if (DEFAULT_DISPLAY == displayId) {
-            notifyConfigChange();
-        }
+        notifyConfigChange(displayId);
     }
 
     /**
@@ -261,60 +290,88 @@
         }
         if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) {
             Log.d(TAG, "Overlay changed, notifying listeners");
-            notifyConfigChange();
+            notifyConfigChange(DEFAULT_DISPLAY);
         }
     }
 
+    @VisibleForTesting
+    public void onConfigurationChanged(Configuration config) {
+        onConfigurationChanged(config, DEFAULT_DISPLAY);
+    }
+
     @UiThread
-    @Override
-    public final void onConfigurationChanged(Configuration config) {
+    private void onConfigurationChanged(Configuration config, int displayId) {
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config);
-        if (config.densityDpi != mInfo.densityDpi
-                || config.fontScale != mInfo.fontScale
-                || !mInfo.mScreenSizeDp.equals(
-                        new PortraitSize(config.screenHeightDp, config.screenWidthDp))
-                || mWindowContext.getDisplay().getRotation() != mInfo.rotation
-                || mWMProxy.showLockedTaskbarOnHome(mWindowContext)
-                        != mInfo.showLockedTaskbarOnHome()
-                || mWMProxy.showDesktopTaskbarForFreeformDisplay(mWindowContext)
-                        != mInfo.showDesktopTaskbarForFreeformDisplay()) {
-            notifyConfigChange();
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        Context windowContext = perDisplayInfo.mWindowContext;
+        Info info = perDisplayInfo.mInfo;
+        if (config.densityDpi != info.densityDpi
+                || config.fontScale != info.fontScale
+                || !info.mScreenSizeDp.equals(
+                    new PortraitSize(config.screenHeightDp, config.screenWidthDp))
+                || windowContext.getDisplay().getRotation() != info.rotation
+                || mWMProxy.showLockedTaskbarOnHome(windowContext)
+                != info.showLockedTaskbarOnHome()
+                || mWMProxy.showDesktopTaskbarForFreeformDisplay(windowContext)
+                != info.showDesktopTaskbarForFreeformDisplay()) {
+            notifyConfigChange(displayId);
         }
     }
 
-    @Override
-    public final void onLowMemory() { }
-
     public void setPriorityListener(DisplayInfoChangeListener listener) {
         mPriorityListener = listener;
     }
 
     public void addChangeListener(DisplayInfoChangeListener listener) {
-        mListeners.add(listener);
+        addChangeListenerForDisplay(listener, DEFAULT_DISPLAY);
     }
 
     public void removeChangeListener(DisplayInfoChangeListener listener) {
-        mListeners.remove(listener);
+        removeChangeListenerForDisplay(listener, DEFAULT_DISPLAY);
+    }
+
+    public void addChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId) {
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo != null) {
+            perDisplayInfo.addListener(listener);
+        }
+    }
+
+    public void removeChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId) {
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo != null) {
+            perDisplayInfo.removeListener(listener);
+        }
     }
 
     public Info getInfo() {
-        return mInfo;
+        return mPerDisplayInfo.get(DEFAULT_DISPLAY).mInfo;
+    }
+
+    public @Nullable Info getInfoForDisplay(int displayId) {
+        if (enableOverviewOnConnectedDisplays()) {
+            PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+            if (perDisplayInfo != null) {
+                return perDisplayInfo.mInfo;
+            } else {
+                return null;
+            }
+        } else {
+            return getInfo();
+        }
     }
 
     @AnyThread
     public void notifyConfigChange() {
-        Info oldInfo = mInfo;
+        notifyConfigChange(DEFAULT_DISPLAY);
+    }
 
-        Context displayInfoContext = mWindowContext;
-        Info newInfo = new Info(displayInfoContext, mWMProxy, oldInfo.mPerDisplayBounds);
+    @AnyThread
+    public void notifyConfigChange(int displayId) {
+        notifyConfigChangeForDisplay(displayId);
+    }
 
-        if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
-                || newInfo.getNavigationMode() != oldInfo.getNavigationMode()) {
-            // Cache may not be valid anymore, recreate without cache
-            newInfo = new Info(displayInfoContext, mWMProxy,
-                    mWMProxy.estimateInternalDisplayBounds(displayInfoContext));
-        }
-
+    private int calculateChange(Info oldInfo, Info newInfo) {
         int change = 0;
         if (!newInfo.normalizedDisplayInfo.equals(oldInfo.normalizedDisplayInfo)) {
             change |= CHANGE_ACTIVE_SCREEN;
@@ -336,7 +393,7 @@
         }
         if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned)
                 || (newInfo.mIsTaskbarPinnedInDesktopMode
-                    != oldInfo.mIsTaskbarPinnedInDesktopMode)
+                != oldInfo.mIsTaskbarPinnedInDesktopMode)
                 || newInfo.isPinnedTaskbar() != oldInfo.isPinnedTaskbar()) {
             change |= CHANGE_TASKBAR_PINNING;
         }
@@ -350,23 +407,68 @@
         if (DEBUG) {
             Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
         }
+        return change;
+    }
 
-        if (change != 0) {
-            mInfo = newInfo;
-            final int flags = change;
-            MAIN_EXECUTOR.execute(() -> notifyChange(displayInfoContext, flags));
+    private Info getNewInfo(Info oldInfo, Context displayInfoContext) {
+        Info newInfo = new Info(displayInfoContext, mWMProxy, oldInfo.mPerDisplayBounds);
+
+        if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
+                || newInfo.getNavigationMode() != oldInfo.getNavigationMode()) {
+            // Cache may not be valid anymore, recreate without cache
+            newInfo = new Info(displayInfoContext, mWMProxy,
+                    mWMProxy.estimateInternalDisplayBounds(displayInfoContext));
+        }
+        return newInfo;
+    }
+
+    @AnyThread
+    public void notifyConfigChangeForDisplay(int displayId) {
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo == null) return;
+        Info oldInfo = perDisplayInfo.mInfo;
+        final Info newInfo = getNewInfo(oldInfo, perDisplayInfo.mWindowContext);
+        final int flags = calculateChange(oldInfo, newInfo);
+        if (flags != 0) {
+            MAIN_EXECUTOR.execute(() -> {
+                perDisplayInfo.mInfo = newInfo;
+                if (displayId == DEFAULT_DISPLAY && mPriorityListener != null) {
+                    mPriorityListener.onDisplayInfoChanged(perDisplayInfo.mWindowContext, newInfo,
+                            flags);
+                }
+                perDisplayInfo.notifyListeners(newInfo, flags);
+            });
         }
     }
 
-    private void notifyChange(Context context, int flags) {
-        if (mPriorityListener != null) {
-            mPriorityListener.onDisplayInfoChanged(context, mInfo, flags);
+    private PerDisplayInfo getOrCreatePerDisplayInfo(Display display) {
+        int displayId = display.getDisplayId();
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo != null) {
+            return perDisplayInfo;
         }
+        if (DEBUG) {
+            Log.d(TAG,
+                    String.format("getOrCreatePerDisplayInfo - no cached value found for %d",
+                            displayId));
+        }
+        Context windowContext = mAppContext.createWindowContext(display, TYPE_APPLICATION, null);
+        Info info = new Info(windowContext, mWMProxy,
+                mWMProxy.estimateInternalDisplayBounds(windowContext));
+        perDisplayInfo = new PerDisplayInfo(displayId, windowContext, info);
+        mPerDisplayInfo.put(displayId, perDisplayInfo);
+        return perDisplayInfo;
+    }
 
-        int count = mListeners.size();
-        for (int i = 0; i < count; i++) {
-            mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
-        }
+    /**
+     * Clean up resources for the given display id.
+     * @param displayId The display id
+     */
+    void removePerDisplayInfo(int displayId) {
+        PerDisplayInfo info = mPerDisplayInfo.get(displayId);
+        if (info == null) return;
+        info.cleanup();
+        mPerDisplayInfo.remove(displayId);
     }
 
     public static class Info {
@@ -601,21 +703,29 @@
      * Dumps the current state information
      */
     public void dump(PrintWriter pw) {
-        Info info = mInfo;
-        pw.println("DisplayController.Info:");
-        pw.println("  normalizedDisplayInfo=" + info.normalizedDisplayInfo);
-        pw.println("  rotation=" + info.rotation);
-        pw.println("  fontScale=" + info.fontScale);
-        pw.println("  densityDpi=" + info.densityDpi);
-        pw.println("  navigationMode=" + info.getNavigationMode().name());
-        pw.println("  isTaskbarPinned=" + info.mIsTaskbarPinned);
-        pw.println("  isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode);
-        pw.println("  isInDesktopMode=" + info.mIsInDesktopMode);
-        pw.println("  showLockedTaskbarOnHome=" + info.showLockedTaskbarOnHome());
-        pw.println("  currentSize=" + info.currentSize);
-        info.mPerDisplayBounds.forEach((key, value) -> pw.println(
-                "  perDisplayBounds - " + key + ": " + value));
-        pw.println("  isTransientTaskbar=" + info.isTransientTaskbar());
+        int count = mPerDisplayInfo.size();
+        for (int i = 0; i < count; ++i) {
+            int displayId = mPerDisplayInfo.keyAt(i);
+            Info info = getInfoForDisplay(displayId);
+            if (info == null) {
+                continue;
+            }
+            pw.println(String.format(Locale.ENGLISH, "DisplayController.Info (displayId=%d):",
+                    displayId));
+            pw.println("  normalizedDisplayInfo=" + info.normalizedDisplayInfo);
+            pw.println("  rotation=" + info.rotation);
+            pw.println("  fontScale=" + info.fontScale);
+            pw.println("  densityDpi=" + info.densityDpi);
+            pw.println("  navigationMode=" + info.getNavigationMode().name());
+            pw.println("  isTaskbarPinned=" + info.mIsTaskbarPinned);
+            pw.println("  isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode);
+            pw.println("  isInDesktopMode=" + info.mIsInDesktopMode);
+            pw.println("  showLockedTaskbarOnHome=" + info.showLockedTaskbarOnHome());
+            pw.println("  currentSize=" + info.currentSize);
+            info.mPerDisplayBounds.forEach((key, value) -> pw.println(
+                    "  perDisplayBounds - " + key + ": " + value));
+            pw.println("  isTransientTaskbar=" + info.isTransientTaskbar());
+        }
     }
 
     /**
@@ -643,4 +753,47 @@
         }
     }
 
+    private class PerDisplayInfo implements ComponentCallbacks {
+        final int mDisplayId;
+        final CopyOnWriteArrayList<DisplayInfoChangeListener> mListeners =
+                new CopyOnWriteArrayList<>();
+        final Context mWindowContext;
+        Info mInfo;
+
+        PerDisplayInfo(int displayId, Context windowContext, Info info) {
+            this.mDisplayId = displayId;
+            this.mWindowContext = windowContext;
+            this.mInfo = info;
+            windowContext.registerComponentCallbacks(this);
+        }
+
+        void addListener(DisplayInfoChangeListener listener) {
+            mListeners.add(listener);
+        }
+
+        void removeListener(DisplayInfoChangeListener listener) {
+            mListeners.remove(listener);
+        }
+
+        void notifyListeners(Info info, int flags) {
+            int count = mListeners.size();
+            for (int i = 0; i < count; ++i) {
+                mListeners.get(i).onDisplayInfoChanged(mWindowContext, info, flags);
+            }
+        }
+
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {
+            DisplayController.this.onConfigurationChanged(newConfig, mDisplayId);
+        }
+
+        @Override
+        public void onLowMemory() {}
+
+        void cleanup() {
+            mWindowContext.unregisterComponentCallbacks(this);
+            mListeners.clear();
+        }
+    }
+
 }